Skip to content

Add a new test project type that uses a custom build server #2035

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions Sources/BuildSystemIntegration/BuildSystemManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@ private extension BuildSystemSpec {
_ createBuildSystem: @Sendable (_ connectionToSourceKitLSP: any Connection) async throws -> BuiltInBuildSystem?
) async -> BuildSystemAdapter? {
let connectionToSourceKitLSP = LocalConnection(
receiverName: "BuildSystemManager for \(projectRoot.lastPathComponent)"
receiverName: "BuildSystemManager for \(projectRoot.lastPathComponent)",
handler: messagesToSourceKitLSPHandler
)
connectionToSourceKitLSP.start(handler: messagesToSourceKitLSPHandler)

let buildSystem = await orLog("Creating build system") {
try await createBuildSystem(connectionToSourceKitLSP)
Expand All @@ -197,9 +197,9 @@ private extension BuildSystemSpec {
buildSystemHooks: buildSystemHooks
)
let connectionToBuildSystem = LocalConnection(
receiverName: "\(type(of: buildSystem)) for \(projectRoot.lastPathComponent)"
receiverName: "\(type(of: buildSystem)) for \(projectRoot.lastPathComponent)",
handler: buildSystemAdapter
)
connectionToBuildSystem.start(handler: buildSystemAdapter)
return .builtIn(buildSystemAdapter, connectionToBuildSystem: connectionToBuildSystem)
}

Expand Down Expand Up @@ -266,9 +266,9 @@ private extension BuildSystemSpec {
#endif
case .injected(let injector):
let connectionToSourceKitLSP = LocalConnection(
receiverName: "BuildSystemManager for \(projectRoot.lastPathComponent)"
receiverName: "BuildSystemManager for \(projectRoot.lastPathComponent)",
handler: messagesToSourceKitLSPHandler
)
connectionToSourceKitLSP.start(handler: messagesToSourceKitLSPHandler)
return .injected(
await injector(projectRoot, connectionToSourceKitLSP)
)
Expand Down Expand Up @@ -510,8 +510,7 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
connectionToSourceKitLSP: legacyBuildServer.connectionToSourceKitLSP,
buildSystemHooks: buildSystemHooks
)
let connectionToBuildSystem = LocalConnection(receiverName: "Legacy BSP server")
connectionToBuildSystem.start(handler: adapter)
let connectionToBuildSystem = LocalConnection(receiverName: "Legacy BSP server", handler: adapter)
self.buildSystemAdapter = .builtIn(adapter, connectionToBuildSystem: connectionToBuildSystem)
}
Task {
Expand Down
3 changes: 1 addition & 2 deletions Sources/BuildSystemIntegration/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ add_library(BuildSystemIntegration STATIC
LegacyBuildServerBuildSystem.swift
MainFilesProvider.swift
SplitShellCommand.swift
SwiftPMBuildSystem.swift
TestBuildSystem.swift)
SwiftPMBuildSystem.swift)
set_target_properties(BuildSystemIntegration PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
target_link_libraries(BuildSystemIntegration PUBLIC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@ actor LegacyBuildServerBuildSystem: MessageHandler, BuiltInBuildSystem {
self.configPath = configPath
self.indexDatabasePath = nil
self.indexStorePath = nil
self.connectionToSourceKitLSP = LocalConnection(receiverName: "BuildSystemManager")
await self.connectionToSourceKitLSP.start(handler: externalBuildSystemAdapter.messagesToSourceKitLSPHandler)
self.connectionToSourceKitLSP = LocalConnection(
receiverName: "BuildSystemManager",
handler: await externalBuildSystemAdapter.messagesToSourceKitLSPHandler
)
await externalBuildSystemAdapter.changeMessageToSourceKitLSPHandler(to: self)
self.buildServer = await externalBuildSystemAdapter.connectionToBuildServer
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ package final class LocalConnection: Connection, Sendable {
self.name = receiverName
}

package convenience init(receiverName: String, handler: MessageHandler) {
self.init(receiverName: receiverName)
self.start(handler: handler)
}

deinit {
queue.sync {
if state != .closed {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,61 @@
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2025, right? :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah… Probably from some copy-pasting

// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation
import BuildSystemIntegration
import LanguageServerProtocolExtensions
import SKLogging
import SKOptions
import SourceKitLSP
import SwiftExtensions
import ToolchainRegistry
import XCTest

#if compiler(>=6)
package import BuildServerProtocol
package import Foundation
package import LanguageServerProtocol
package import SKOptions
#else
import BuildServerProtocol
import Foundation
import LanguageServerProtocol
import SKOptions
#endif

/// Build system to be used for testing BuildSystem and BuildSystemDelegate functionality with SourceKitLSPServer
/// and other components.
package actor TestBuildSystem: MessageHandler {
private let connectionToSourceKitLSP: any Connection

/// Build settings by file.
private var buildSettingsByFile: [DocumentURI: TextDocumentSourceKitOptionsResponse] = [:]

package func setBuildSettings(for uri: DocumentURI, to buildSettings: TextDocumentSourceKitOptionsResponse?) {
buildSettingsByFile[uri] = buildSettings
connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil))
}
// MARK: - CustomBuildServer

private let initializeData: SourceKitInitializeBuildResponseData
/// A build server that can be injected into `CustomBuildServerTestProject`.
package protocol CustomBuildServer: MessageHandler {
init(projectRoot: URL, connectionToSourceKitLSP: any Connection)

package init(
initializeData: SourceKitInitializeBuildResponseData = SourceKitInitializeBuildResponseData(
sourceKitOptionsProvider: true
),
connectionToSourceKitLSP: any Connection
) {
self.initializeData = initializeData
self.connectionToSourceKitLSP = connectionToSourceKitLSP
}
func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse
func onBuildInitialized(_ notification: OnBuildInitializedNotification) throws
func buildShutdown(_ request: BuildShutdownRequest) async throws -> VoidResponse
func onBuildExit(_ notification: OnBuildExitNotification) throws
func workspaceBuildTargetsRequest(
_ request: WorkspaceBuildTargetsRequest
) async throws -> WorkspaceBuildTargetsResponse
func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse
func textDocumentSourceKitOptionsRequest(
_ request: TextDocumentSourceKitOptionsRequest
) async throws -> TextDocumentSourceKitOptionsResponse?
func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> VoidResponse
func waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest) async -> VoidResponse
nonisolated func onWatchedFilesDidChange(_ notification: OnWatchedFilesDidChangeNotification) throws
func workspaceWaitForBuildSystemUpdatesRequest(
_ request: WorkspaceWaitForBuildSystemUpdatesRequest
) async throws -> VoidResponse
nonisolated func cancelRequest(_ notification: CancelRequestNotification) throws
}

extension CustomBuildServer {
package nonisolated func handle(_ notification: some NotificationType) {
do {
switch notification {
Expand All @@ -64,7 +72,7 @@ package actor TestBuildSystem: MessageHandler {
throw ResponseError.methodNotFound(type(of: notification).method)
}
} catch {
logger.error("Error while handling BSP notification")
logger.error("Error while handling BSP notification: \(error.forLogging)")
}
}

Expand Down Expand Up @@ -102,10 +110,18 @@ package actor TestBuildSystem: MessageHandler {
reply(.failure(ResponseError.methodNotFound(type(of: request).method)))
}
}
}

func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse {
return InitializeBuildResponse(
displayName: "TestBuildSystem",
package extension CustomBuildServer {
// MARK: Helper functions for the implementation of BSP methods

func initializationResponse(
initializeData: SourceKitInitializeBuildResponseData = SourceKitInitializeBuildResponseData(
sourceKitOptionsProvider: true
)
) -> InitializeBuildResponse {
InitializeBuildResponse(
displayName: "\(type(of: self))",
version: "",
bspVersion: "2.2.0",
capabilities: BuildServerCapabilities(),
Expand All @@ -114,17 +130,25 @@ package actor TestBuildSystem: MessageHandler {
)
}

nonisolated func onBuildInitialized(_ notification: OnBuildInitializedNotification) throws {
// Nothing to do
func dummyTargetSourcesResponse(_ files: some Sequence<DocumentURI>) -> BuildTargetSourcesResponse {
return BuildTargetSourcesResponse(items: [
SourcesItem(target: .dummy, sources: files.map { SourceItem(uri: $0, kind: .file, generated: false) })
])
}

// MARK: Default implementation for all build server methods that usually don't need customization.

func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse {
return initializationResponse()
}

nonisolated func onBuildInitialized(_ notification: OnBuildInitializedNotification) throws {}

func buildShutdown(_ request: BuildShutdownRequest) async throws -> VoidResponse {
return VoidResponse()
}

nonisolated func onBuildExit(_ notification: OnBuildExitNotification) throws {
// Nothing to do
}
nonisolated func onBuildExit(_ notification: OnBuildExitNotification) throws {}

func workspaceBuildTargetsRequest(
_ request: WorkspaceBuildTargetsRequest
Expand All @@ -142,32 +166,15 @@ package actor TestBuildSystem: MessageHandler {
])
}

func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse {
return BuildTargetSourcesResponse(items: [
SourcesItem(
target: .dummy,
sources: buildSettingsByFile.keys.map { SourceItem(uri: $0, kind: .file, generated: false) }
)
])
}

func textDocumentSourceKitOptionsRequest(
_ request: TextDocumentSourceKitOptionsRequest
) async throws -> TextDocumentSourceKitOptionsResponse? {
return buildSettingsByFile[request.textDocument.uri]
}

func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> VoidResponse {
return VoidResponse()
}

package func waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest) async -> VoidResponse {
func waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest) async -> VoidResponse {
return VoidResponse()
}

nonisolated func onWatchedFilesDidChange(_ notification: OnWatchedFilesDidChangeNotification) throws {
// Not watching any files
}
nonisolated func onWatchedFilesDidChange(_ notification: OnWatchedFilesDidChangeNotification) throws {}

func workspaceWaitForBuildSystemUpdatesRequest(
_ request: WorkspaceWaitForBuildSystemUpdatesRequest
Expand All @@ -177,3 +184,40 @@ package actor TestBuildSystem: MessageHandler {

nonisolated func cancelRequest(_ notification: CancelRequestNotification) throws {}
}

// MARK: - CustomBuildServerTestProject

/// A test project that launches a custom build server in-process.
///
/// In contrast to `ExternalBuildServerTestProject`, the custom build system runs in-process and is implemented in
/// Swift.
package final class CustomBuildServerTestProject<BuildServer: CustomBuildServer>: MultiFileTestProject {
private let buildServerBox = ThreadSafeBox<BuildServer?>(initialValue: nil)

package init(
files: [RelativeFileLocation: String],
buildServer buildServerType: BuildServer.Type,
options: SourceKitLSPOptions? = nil,
enableBackgroundIndexing: Bool = false,
testName: String = #function
) async throws {
let hooks: Hooks = Hooks(
buildSystemHooks: BuildSystemHooks(injectBuildServer: { [buildServerBox] projectRoot, connectionToSourceKitLSP in
let buildServer = BuildServer(projectRoot: projectRoot, connectionToSourceKitLSP: connectionToSourceKitLSP)
buildServerBox.value = buildServer
return LocalConnection(receiverName: "TestBuildSystem", handler: buildServer)
})
)
try await super.init(
files: files,
options: options,
hooks: hooks,
enableBackgroundIndexing: enableBackgroundIndexing,
testName: testName
)
}

package func buildServer(file: StaticString = #filePath, line: UInt = #line) throws -> BuildServer {
try XCTUnwrap(buildServerBox.value, "Accessing build server before it has been created", file: file, line: line)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ private let skTestSupportInputsDirectory: URL = {
///
/// The build server can contain `$SDK_ARGS`, which will replaced by `"-sdk", "/path/to/sdk"` on macOS and by an empty
/// string on all other platforms.
package class BuildServerTestProject: MultiFileTestProject {
package class ExternalBuildServerTestProject: MultiFileTestProject {
package init(
files: [RelativeFileLocation: String],
buildServerConfigLocation: RelativeFileLocation = ".bsp/sourcekit-lsp.json",
Expand Down
Loading