diff --git a/Sources/BuildSystemIntegration/BuildSystemManager.swift b/Sources/BuildSystemIntegration/BuildSystemManager.swift index 68d3ce95..6a40227e 100644 --- a/Sources/BuildSystemIntegration/BuildSystemManager.swift +++ b/Sources/BuildSystemIntegration/BuildSystemManager.swift @@ -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) @@ -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) } @@ -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) ) @@ -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 { diff --git a/Sources/BuildSystemIntegration/CMakeLists.txt b/Sources/BuildSystemIntegration/CMakeLists.txt index cd00ff1d..eae79056 100644 --- a/Sources/BuildSystemIntegration/CMakeLists.txt +++ b/Sources/BuildSystemIntegration/CMakeLists.txt @@ -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 diff --git a/Sources/BuildSystemIntegration/LegacyBuildServerBuildSystem.swift b/Sources/BuildSystemIntegration/LegacyBuildServerBuildSystem.swift index ccef17ba..8f7090db 100644 --- a/Sources/BuildSystemIntegration/LegacyBuildServerBuildSystem.swift +++ b/Sources/BuildSystemIntegration/LegacyBuildServerBuildSystem.swift @@ -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 } diff --git a/Sources/LanguageServerProtocolExtensions/LocalConnection.swift b/Sources/LanguageServerProtocolExtensions/LocalConnection.swift index 1c0a0afa..4b0fea48 100644 --- a/Sources/LanguageServerProtocolExtensions/LocalConnection.swift +++ b/Sources/LanguageServerProtocolExtensions/LocalConnection.swift @@ -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 { diff --git a/Sources/BuildSystemIntegration/TestBuildSystem.swift b/Sources/SKTestSupport/CustomBuildServerTestProject.swift similarity index 54% rename from Sources/BuildSystemIntegration/TestBuildSystem.swift rename to Sources/SKTestSupport/CustomBuildServerTestProject.swift index 9d726cb2..461c509f 100644 --- a/Sources/BuildSystemIntegration/TestBuildSystem.swift +++ b/Sources/SKTestSupport/CustomBuildServerTestProject.swift @@ -2,7 +2,7 @@ // // 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 // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -10,45 +10,53 @@ // //===----------------------------------------------------------------------===// -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 +// MARK: - CustomBuildServer - /// Build settings by file. - private var buildSettingsByFile: [DocumentURI: TextDocumentSourceKitOptionsResponse] = [:] +/// A build server that can be injected into `CustomBuildServerTestProject`. +package protocol CustomBuildServer: MessageHandler { + init(projectRoot: URL, connectionToSourceKitLSP: any Connection) - package func setBuildSettings(for uri: DocumentURI, to buildSettings: TextDocumentSourceKitOptionsResponse?) { - buildSettingsByFile[uri] = buildSettings - connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil)) - } - - private let initializeData: SourceKitInitializeBuildResponseData - - 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 { @@ -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)") } } @@ -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(), @@ -114,17 +130,25 @@ package actor TestBuildSystem: MessageHandler { ) } - nonisolated func onBuildInitialized(_ notification: OnBuildInitializedNotification) throws { - // Nothing to do + func dummyTargetSourcesResponse(_ files: some Sequence) -> 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 @@ -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 @@ -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: MultiFileTestProject { + private let buildServerBox = ThreadSafeBox(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) + } +} diff --git a/Sources/SKTestSupport/BuildServerTestProject.swift b/Sources/SKTestSupport/ExternalBuildServerTestProject.swift similarity index 97% rename from Sources/SKTestSupport/BuildServerTestProject.swift rename to Sources/SKTestSupport/ExternalBuildServerTestProject.swift index 55ecd537..740fca75 100644 --- a/Sources/SKTestSupport/BuildServerTestProject.swift +++ b/Sources/SKTestSupport/ExternalBuildServerTestProject.swift @@ -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", diff --git a/Tests/BuildSystemIntegrationTests/BuildServerBuildSystemTests.swift b/Tests/BuildSystemIntegrationTests/BuildServerBuildSystemTests.swift index 91265006..405540e7 100644 --- a/Tests/BuildSystemIntegrationTests/BuildServerBuildSystemTests.swift +++ b/Tests/BuildSystemIntegrationTests/BuildServerBuildSystemTests.swift @@ -28,7 +28,7 @@ import WinSDK final class BuildServerBuildSystemTests: XCTestCase { func testBuildSettingsFromBuildServer() async throws { - let project = try await BuildServerTestProject( + let project = try await ExternalBuildServerTestProject( files: [ "Test.swift": """ #if DEBUG @@ -85,7 +85,7 @@ final class BuildServerBuildSystemTests: XCTestCase { func testBuildTargetsChanged() async throws { try SkipUnless.longTestsEnabled() - let project = try await BuildServerTestProject( + let project = try await ExternalBuildServerTestProject( files: [ "Test.swift": """ #if DEBUG @@ -168,7 +168,7 @@ final class BuildServerBuildSystemTests: XCTestCase { func testSettingsOfSingleFileChanged() async throws { try SkipUnless.longTestsEnabled() - let project = try await BuildServerTestProject( + let project = try await ExternalBuildServerTestProject( files: [ "Test.swift": """ #if DEBUG @@ -251,7 +251,7 @@ final class BuildServerBuildSystemTests: XCTestCase { func testCrashRecovery() async throws { try SkipUnless.longTestsEnabled() - let project = try await BuildServerTestProject( + let project = try await ExternalBuildServerTestProject( files: [ "Crash.swift": "", "Test.swift": """ @@ -322,7 +322,7 @@ final class BuildServerBuildSystemTests: XCTestCase { } func testBuildServerConfigAtLegacyLocation() async throws { - let project = try await BuildServerTestProject( + let project = try await ExternalBuildServerTestProject( files: [ "Test.swift": """ #if DEBUG @@ -378,7 +378,7 @@ final class BuildServerBuildSystemTests: XCTestCase { } func testBuildSettingsDataPassThrough() async throws { - let project = try await BuildServerTestProject( + let project = try await ExternalBuildServerTestProject( files: [ "Test.swift": "" ], @@ -431,7 +431,7 @@ final class BuildServerBuildSystemTests: XCTestCase { } func testBuildSettingsForFilePartOfMultipleTargets() async throws { - let project = try await BuildServerTestProject( + let project = try await ExternalBuildServerTestProject( files: [ "Test.swift": "" ], @@ -526,48 +526,37 @@ final class BuildServerBuildSystemTests: XCTestCase { func testDontBlockBuildServerInitializationIfBuildSystemIsUnresponsive() async throws { // A build server that responds to the initialize request but not to any other requests. - final class UnresponsiveBuildServer: MessageHandler { - func handle(_ notification: some LanguageServerProtocol.NotificationType) {} + final class UnresponsiveBuildServer: CustomBuildServer { + init(projectRoot: URL, connectionToSourceKitLSP: any Connection) {} - func handle( - _ request: Request, - id: RequestID, - reply: @escaping @Sendable (LSPResult) -> Void - ) { - switch request { - case is InitializeBuildRequest: - reply( - .success( - InitializeBuildResponse( - displayName: "UnresponsiveBuildServer", - version: "", - bspVersion: "2.2.0", - capabilities: BuildServerCapabilities() - ) as! Request.Response - ) - ) - default: - #if os(Windows) - Sleep(60 * 60 * 1000 /*ms*/) - #else - sleep(60 * 60 /*s*/) - #endif - XCTFail("Build server should be terminated before finishing the timeout") - } + func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse { + #if os(Windows) + Sleep(60 * 60 * 1000 /*ms*/) + #else + sleep(60 * 60 /*s*/) + #endif + XCTFail("Build server should be terminated before finishing the timeout") + throw ResponseError.methodNotFound(BuildTargetSourcesRequest.method) + } + + func textDocumentSourceKitOptionsRequest( + _ request: TextDocumentSourceKitOptionsRequest + ) async throws -> TextDocumentSourceKitOptionsResponse? { + #if os(Windows) + Sleep(60 * 60 * 1000 /*ms*/) + #else + sleep(60 * 60 /*s*/) + #endif + XCTFail("Build server should be terminated before finishing the timeout") + throw ResponseError.methodNotFound(TextDocumentSourceKitOptionsRequest.method) } } - // Creating the `MultiFileTestProject` waits for the initialize response and times out if it doesn't receive one. + // Creating the `CustomBuildServerTestProject` waits for the initialize response and times out if it doesn't receive one. // Make sure that we get that response back. - _ = try await MultiFileTestProject( + _ = try await CustomBuildServerTestProject( files: ["Test.swift": ""], - hooks: Hooks( - buildSystemHooks: BuildSystemHooks(injectBuildServer: { _, _ in - let connection = LocalConnection(receiverName: "Unresponsive build system") - connection.start(handler: UnresponsiveBuildServer()) - return connection - }) - ) + buildServer: UnresponsiveBuildServer.self ) } } diff --git a/Tests/BuildSystemIntegrationTests/BuildSystemManagerTests.swift b/Tests/BuildSystemIntegrationTests/BuildSystemManagerTests.swift index 47c6e47d..629ba39c 100644 --- a/Tests/BuildSystemIntegrationTests/BuildSystemManagerTests.swift +++ b/Tests/BuildSystemIntegrationTests/BuildSystemManagerTests.swift @@ -22,6 +22,30 @@ import TSCBasic import ToolchainRegistry import XCTest +fileprivate actor TestBuildSystem: CustomBuildServer { + private let connectionToSourceKitLSP: any Connection + private var buildSettingsByFile: [DocumentURI: TextDocumentSourceKitOptionsResponse] = [:] + + func setBuildSettings(for uri: DocumentURI, to buildSettings: TextDocumentSourceKitOptionsResponse?) { + buildSettingsByFile[uri] = buildSettings + connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil)) + } + + init(projectRoot: URL, connectionToSourceKitLSP: any Connection) { + self.connectionToSourceKitLSP = connectionToSourceKitLSP + } + + func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) -> BuildTargetSourcesResponse { + return dummyTargetSourcesResponse(buildSettingsByFile.keys) + } + + func textDocumentSourceKitOptionsRequest( + _ request: TextDocumentSourceKitOptionsRequest + ) async throws -> TextDocumentSourceKitOptionsResponse? { + return buildSettingsByFile[request.textDocument.uri] + } +} + fileprivate extension BuildSystemManager { func fileBuildSettingsChanged(_ changedFiles: Set) async { handle(OnBuildTargetDidChangeNotification(changes: nil)) @@ -45,11 +69,9 @@ final class BuildSystemManagerTests: XCTestCase { let spec = BuildSystemSpec( kind: .injected({ projectRoot, connectionToSourceKitLSP in assert(testBuildSystem.value == nil, "Build system injector hook can only create a single TestBuildSystem") - let buildSystem = TestBuildSystem(connectionToSourceKitLSP: connectionToSourceKitLSP) + let buildSystem = TestBuildSystem(projectRoot: projectRoot, connectionToSourceKitLSP: connectionToSourceKitLSP) testBuildSystem.value = buildSystem - let connection = LocalConnection(receiverName: "TestBuildSystem") - connection.start(handler: buildSystem) - return connection + return LocalConnection(receiverName: "TestBuildSystem", handler: buildSystem) }), projectRoot: dummyPath, configPath: dummyPath diff --git a/Tests/BuildSystemIntegrationTests/LegacyBuildServerBuildSystemTests.swift b/Tests/BuildSystemIntegrationTests/LegacyBuildServerBuildSystemTests.swift index 0294ee7c..38866bfd 100644 --- a/Tests/BuildSystemIntegrationTests/LegacyBuildServerBuildSystemTests.swift +++ b/Tests/BuildSystemIntegrationTests/LegacyBuildServerBuildSystemTests.swift @@ -21,7 +21,7 @@ import XCTest final class LegacyBuildServerBuildSystemTests: XCTestCase { func testBuildSettingsFromBuildServer() async throws { - let project = try await BuildServerTestProject( + let project = try await ExternalBuildServerTestProject( files: [ "Test.swift": """ #if DEBUG @@ -57,7 +57,7 @@ final class LegacyBuildServerBuildSystemTests: XCTestCase { } func testBuildSettingsFromBuildServerChanged() async throws { - let project = try await BuildServerTestProject( + let project = try await ExternalBuildServerTestProject( files: [ "Test.swift": """ #if DEBUG diff --git a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift index a7d35f2b..69d3cf85 100644 --- a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift +++ b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift @@ -1954,31 +1954,50 @@ final class BackgroundIndexingTests: XCTestCase { } func testIndexFileIfBuildTargetsChange() async throws { - let testBuildSystem = ThreadSafeBox(initialValue: nil) - let project = try await MultiFileTestProject( + actor BuildServer: CustomBuildServer { + private let projectRoot: URL + private let connectionToSourceKitLSP: any Connection + private var buildSettingsByFile: [DocumentURI: TextDocumentSourceKitOptionsResponse] = [:] + + package func setBuildSettings(for uri: DocumentURI, to buildSettings: TextDocumentSourceKitOptionsResponse?) { + buildSettingsByFile[uri] = buildSettings + connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil)) + } + + init(projectRoot: URL, connectionToSourceKitLSP: any Connection) { + self.projectRoot = projectRoot + self.connectionToSourceKitLSP = connectionToSourceKitLSP + } + + func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse { + return initializationResponse( + initializeData: SourceKitInitializeBuildResponseData( + indexDatabasePath: try projectRoot.appendingPathComponent("index-db").filePath, + indexStorePath: try projectRoot.appendingPathComponent("index-store").filePath, + prepareProvider: true, + sourceKitOptionsProvider: true + ) + ) + } + + func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) -> BuildTargetSourcesResponse { + return dummyTargetSourcesResponse(buildSettingsByFile.keys) + } + + func textDocumentSourceKitOptionsRequest( + _ request: TextDocumentSourceKitOptionsRequest + ) -> TextDocumentSourceKitOptionsResponse? { + return buildSettingsByFile[request.textDocument.uri] + } + } + + let project = try await CustomBuildServerTestProject( files: [ "Test.swift": """ func 1️⃣myTestFunc() {} """ ], - hooks: Hooks( - buildSystemHooks: BuildSystemHooks(injectBuildServer: { projectRoot, connectionToSourceKitLSP in - assert(testBuildSystem.value == nil, "Build system injector hook can only create a single TestBuildSystem") - let buildSystem = TestBuildSystem( - initializeData: SourceKitInitializeBuildResponseData( - indexDatabasePath: projectRoot.appendingPathComponent("index-db").path, - indexStorePath: projectRoot.appendingPathComponent("index-store").path, - prepareProvider: true, - sourceKitOptionsProvider: true - ), - connectionToSourceKitLSP: connectionToSourceKitLSP - ) - testBuildSystem.value = buildSystem - let connection = LocalConnection(receiverName: "TestBuildSystem") - connection.start(handler: buildSystem) - return connection - }) - ), + buildServer: BuildServer.self, enableBackgroundIndexing: true ) let fileUrl = try XCTUnwrap(project.uri(for: "Test.swift").fileURL) @@ -1990,7 +2009,7 @@ final class BackgroundIndexingTests: XCTestCase { // We don't initially index Test.swift because we don't have build settings for it. - try await XCTUnwrap(testBuildSystem.value).setBuildSettings( + try await project.buildServer().setBuildSettings( for: DocumentURI(fileUrl), to: TextDocumentSourceKitOptionsResponse(compilerArguments: compilerArguments) ) @@ -2086,65 +2105,59 @@ final class BackgroundIndexingTests: XCTestCase { // The build system returns too many arguments to fit them into a command line invocation, so we need to use a // response file to invoke the indexer. - let project = try await BuildServerTestProject( + final class BuildServer: CustomBuildServer { + private let projectRoot: URL + private var testFileURL: URL { projectRoot.appendingPathComponent("Test File.swift") } + + init(projectRoot: URL, connectionToSourceKitLSP: any LanguageServerProtocol.Connection) { + self.projectRoot = projectRoot + } + + func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse { + return initializationResponse( + initializeData: SourceKitInitializeBuildResponseData( + indexDatabasePath: try projectRoot.appendingPathComponent("index-db").filePath, + indexStorePath: try projectRoot.appendingPathComponent("index-store").filePath, + prepareProvider: true, + sourceKitOptionsProvider: true + ) + ) + } + + func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse { + return BuildTargetSourcesResponse(items: [ + SourcesItem( + target: .dummy, + sources: [ + SourceItem(uri: URI(testFileURL), kind: .file, generated: false) + ] + ) + ]) + } + + func textDocumentSourceKitOptionsRequest( + _ request: TextDocumentSourceKitOptionsRequest + ) async throws -> TextDocumentSourceKitOptionsResponse? { + var arguments = + [try testFileURL.filePath] + (0..<50_000).map { "-DTHIS_IS_AN_OPTION_THAT_CONTAINS_MANY_BYTES_\($0)" } + if let defaultSDKPath { + arguments += ["-sdk", defaultSDKPath] + } + return TextDocumentSourceKitOptionsResponse( + compilerArguments: arguments + ) + } + + } + + let project = try await CustomBuildServerTestProject( files: [ // File name contains a space to ensure we escape it in the response file. "Test File.swift": """ func 1️⃣myTestFunc() {} """ ], - buildServer: """ - class BuildServer(AbstractBuildServer): - - def initialize(self, request: Dict[str, object]) -> Dict[str, object]: - return { - "displayName": "test server", - "version": "0.1", - "bspVersion": "2.0", - "rootUri": "blah", - "capabilities": {"languageIds": ["swift", "c", "cpp", "objective-c", "objective-c"]}, - "data": { - "indexDatabasePath": r"$TEST_DIR/index-db", - "indexStorePath": r"$TEST_DIR/index", - "prepareProvider": True, - "sourceKitOptionsProvider": True, - }, - } - - def workspace_build_targets(self, request: Dict[str, object]) -> Dict[str, object]: - return { - "targets": [ - { - "id": {"uri": "bsp://dummy"}, - "tags": [], - "languageIds": [], - "dependencies": [], - "capabilities": {}, - } - ] - } - - def buildtarget_sources(self, request: Dict[str, object]) -> Dict[str, object]: - return { - "items": [ - { - "target": {"uri": "bsp://dummy"}, - "sources": [ - {"uri": "$TEST_DIR_URL/Test.swift", "kind": 1, "generated": False} - ], - } - ] - } - - def textdocument_sourcekitoptions(self, request: Dict[str, object]) -> Dict[str, object]: - return { - "compilerArguments": [r"$TEST_DIR/Test File.swift", "-DDEBUG", $SDK_ARGS] + \ - [f"-DTHIS_IS_AN_OPTION_THAT_CONTAINS_MANY_BYTES_{i}" for i in range(0, 50_000)] - } - - def buildtarget_prepare(self, request: Dict[str, object]) -> Dict[str, object]: - return {} - """, + buildServer: BuildServer.self, enableBackgroundIndexing: true ) try await project.testClient.send(PollIndexRequest()) diff --git a/Tests/SourceKitLSPTests/BuildSystemTests.swift b/Tests/SourceKitLSPTests/BuildSystemTests.swift index 0553d81b..2010ea53 100644 --- a/Tests/SourceKitLSPTests/BuildSystemTests.swift +++ b/Tests/SourceKitLSPTests/BuildSystemTests.swift @@ -23,6 +23,30 @@ import TSCBasic import ToolchainRegistry import XCTest +fileprivate actor TestBuildSystem: CustomBuildServer { + private let connectionToSourceKitLSP: any Connection + private var buildSettingsByFile: [DocumentURI: TextDocumentSourceKitOptionsResponse] = [:] + + func setBuildSettings(for uri: DocumentURI, to buildSettings: TextDocumentSourceKitOptionsResponse?) { + buildSettingsByFile[uri] = buildSettings + connectionToSourceKitLSP.send(OnBuildTargetDidChangeNotification(changes: nil)) + } + + init(projectRoot: URL, connectionToSourceKitLSP: any Connection) { + self.connectionToSourceKitLSP = connectionToSourceKitLSP + } + + func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) -> BuildTargetSourcesResponse { + return dummyTargetSourcesResponse(buildSettingsByFile.keys) + } + + func textDocumentSourceKitOptionsRequest( + _ request: TextDocumentSourceKitOptionsRequest + ) async throws -> TextDocumentSourceKitOptionsResponse? { + return buildSettingsByFile[request.textDocument.uri] + } +} + final class BuildSystemTests: XCTestCase { /// The mock client used to communicate with the SourceKit-LSP server.p /// @@ -54,11 +78,12 @@ final class BuildSystemTests: XCTestCase { buildSystemSpec: BuildSystemSpec( kind: .injected({ projectRoot, connectionToSourceKitLSP in assert(testBuildSystem.value == nil, "Build system injector hook can only create a single TestBuildSystem") - let buildSystem = TestBuildSystem(connectionToSourceKitLSP: connectionToSourceKitLSP) + let buildSystem = TestBuildSystem( + projectRoot: projectRoot, + connectionToSourceKitLSP: connectionToSourceKitLSP + ) testBuildSystem.value = buildSystem - let connection = LocalConnection(receiverName: "TestBuildSystem") - connection.start(handler: buildSystem) - return connection + return LocalConnection(receiverName: "TestBuildSystem", handler: buildSystem) }), projectRoot: URL(fileURLWithPath: "/"), configPath: URL(fileURLWithPath: "/")