diff --git a/.gitignore b/.gitignore index 32c80ecff..2000e2996 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ default.profraw Package.resolved /.build /.index-build +/.linux-build /Packages /*.xcodeproj /*.sublime-project diff --git a/Sources/LSPTestSupport/TestJSONRPCConnection.swift b/Sources/LSPTestSupport/TestJSONRPCConnection.swift index 4bec75ba2..fd46ebac7 100644 --- a/Sources/LSPTestSupport/TestJSONRPCConnection.swift +++ b/Sources/LSPTestSupport/TestJSONRPCConnection.swift @@ -17,7 +17,7 @@ import XCTest import class Foundation.Pipe -public final class TestJSONRPCConnection { +public final class TestJSONRPCConnection: Sendable { public let clientToServer: Pipe = Pipe() public let serverToClient: Pipe = Pipe() @@ -151,7 +151,7 @@ public actor TestClient: MessageHandler { /// Send a request to the LSP server and (asynchronously) receive a reply. public nonisolated func send( _ request: Request, - reply: @escaping (LSPResult) -> Void + reply: @Sendable @escaping (LSPResult) -> Void ) -> RequestID { return connectionToServer.send(request, reply: reply) } diff --git a/Sources/SKTestSupport/DefaultSDKPath.swift b/Sources/SKTestSupport/DefaultSDKPath.swift new file mode 100644 index 000000000..eb64d55ab --- /dev/null +++ b/Sources/SKTestSupport/DefaultSDKPath.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// 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 +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import class TSCBasic.Process + +#if !os(macOS) +import Foundation +#endif + +private func xcrunMacOSSDKPath() -> String? { + guard var path = try? Process.checkNonZeroExit(arguments: ["/usr/bin/xcrun", "--show-sdk-path", "--sdk", "macosx"]) + else { + return nil + } + if path.last == "\n" { + path = String(path.dropLast()) + } + return path +} + +/// The default sdk path to use. +public let defaultSDKPath: String? = { + #if os(macOS) + return xcrunMacOSSDKPath() + #else + return ProcessInfo.processInfo.environment["SDKROOT"] + #endif +}() diff --git a/Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift b/Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift index cae4aef53..9a2d3b833 100644 --- a/Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift +++ b/Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift @@ -11,7 +11,6 @@ //===----------------------------------------------------------------------===// import Foundation -import ISDBTibs import LanguageServerProtocol @_spi(Testing) import SKCore import SourceKitLSP @@ -66,7 +65,7 @@ public struct IndexedSingleSwiftFileTestProject { compilerArguments.append("-index-ignore-system-modules") } - if let sdk = TibsBuilder.defaultSDKPath { + if let sdk = defaultSDKPath { compilerArguments += ["-sdk", sdk] // The following are needed so we can import XCTest diff --git a/Sources/SKTestSupport/SkipUnless.swift b/Sources/SKTestSupport/SkipUnless.swift index b8b5b9410..803b8a507 100644 --- a/Sources/SKTestSupport/SkipUnless.swift +++ b/Sources/SKTestSupport/SkipUnless.swift @@ -27,14 +27,16 @@ import enum TSCBasic.ProcessEnv // MARK: - Skip checks /// Namespace for functions that are used to skip unsupported tests. -public enum SkipUnless { +public actor SkipUnless { private enum FeatureCheckResult { case featureSupported case featureUnsupported(skipMessage: String) } + private static let shared = SkipUnless() + /// For any feature that has already been evaluated, the result of whether or not it should be skipped. - private static var checkCache: [String: FeatureCheckResult] = [:] + private var checkCache: [String: FeatureCheckResult] = [:] /// Throw an `XCTSkip` if any of the following conditions hold /// - The Swift version of the toolchain used for testing (`ToolchainRegistry.forTesting.default`) is older than @@ -49,7 +51,7 @@ public enum SkipUnless { /// /// Independently of these checks, the tests are never skipped in Swift CI (identified by the presence of the `SWIFTCI_USE_LOCAL_DEPS` environment). Swift CI is assumed to always build its own toolchain, which is thus /// guaranteed to be up-to-date. - private static func skipUnlessSupportedByToolchain( + private func skipUnlessSupportedByToolchain( swiftVersion: SwiftVersion, featureName: String = #function, file: StaticString, @@ -96,10 +98,10 @@ public enum SkipUnless { } public static func sourcekitdHasSemanticTokensRequest( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) async throws { - try await skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) { + try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) { let testClient = try await TestSourceKitLSPClient() let uri = DocumentURI.for(.swift) testClient.openDocument("0.bitPattern", uri: uri) @@ -127,10 +129,10 @@ public enum SkipUnless { } public static func sourcekitdSupportsRename( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) async throws { - try await skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) { + try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) { let testClient = try await TestSourceKitLSPClient() let uri = DocumentURI.for(.swift) let positions = testClient.openDocument("func 1️⃣test() {}", uri: uri) @@ -147,10 +149,10 @@ public enum SkipUnless { /// Whether clangd has support for the `workspace/indexedRename` request. public static func clangdSupportsIndexBasedRename( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) async throws { - try await skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) { + try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) { let testClient = try await TestSourceKitLSPClient() let uri = DocumentURI.for(.c) let positions = testClient.openDocument("void 1️⃣test() {}", uri: uri) @@ -177,10 +179,10 @@ public enum SkipUnless { /// toolchain’s SwiftPM stores the Swift modules on the top level but we synthesize compiler arguments expecting the /// modules to be in a `Modules` subdirectory. public static func swiftpmStoresModulesInSubdirectory( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) async throws { - try await skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) { + try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) { let workspace = try await SwiftPMTestProject( files: ["test.swift": ""], build: true @@ -195,21 +197,21 @@ public enum SkipUnless { } public static func toolchainContainsSwiftFormat( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) async throws { - try await skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) { + try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(5, 11), file: file, line: line) { return await ToolchainRegistry.forTesting.default?.swiftFormat != nil } } public static func sourcekitdReturnsRawDocumentationResponse( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) async throws { struct ExpectedMarkdownContentsError: Error {} - return try await skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(6, 0), file: file, line: line) { + return try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(6, 0), file: file, line: line) { // The XML-based doc comment conversion did not preserve `Precondition`. let testClient = try await TestSourceKitLSPClient() let uri = DocumentURI.for(.swift) @@ -235,10 +237,10 @@ public enum SkipUnless { /// Checks whether the index contains a fix that prevents it from adding relations to non-indexed locals /// (https://github.com/apple/swift/pull/72930). public static func indexOnlyHasContainedByRelationsToIndexedDecls( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) async throws { - return try await skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(6, 0), file: file, line: line) { + return try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(6, 0), file: file, line: line) { let project = try await IndexedSingleSwiftFileTestProject( """ func foo() {} diff --git a/Sources/SKTestSupport/TestSourceKitLSPClient.swift b/Sources/SKTestSupport/TestSourceKitLSPClient.swift index f3854c039..53ffd7adf 100644 --- a/Sources/SKTestSupport/TestSourceKitLSPClient.swift +++ b/Sources/SKTestSupport/TestSourceKitLSPClient.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------===// +import CAtomics import Foundation import LSPTestSupport import LanguageServerProtocol @@ -22,7 +23,7 @@ import XCTest extension SourceKitLSPServer.Options { /// The default SourceKitLSPServer options for testing. - public static var testDefault = Self(swiftPublishDiagnosticsDebounceDuration: 0) + public static let testDefault = Self(swiftPublishDiagnosticsDebounceDuration: 0) } /// A mock SourceKit-LSP client (aka. a mock editor) that behaves like an editor @@ -35,7 +36,8 @@ public final class TestSourceKitLSPClient: MessageHandler { public typealias RequestHandler = (Request) -> Request.Response /// The ID that should be assigned to the next request sent to the `server`. - private var nextRequestID: Int = 0 + /// `nonisolated(unsafe)` is fine because `nextRequestID` is atomic. + private nonisolated(unsafe) var nextRequestID = AtomicUInt32(initialValue: 0) /// If the server is not using the global module cache, the path of the local /// module cache. @@ -66,12 +68,12 @@ public final class TestSourceKitLSPClient: MessageHandler { /// /// Conceptually, this is an array of `RequestHandler` but /// since we can't express this in the Swift type system, we use `[Any]`. - private var requestHandlers: [Any] = [] + private nonisolated(unsafe) var requestHandlers = ThreadSafeBox<[Any]>(initialValue: []) /// A closure that is called when the `TestSourceKitLSPClient` is destructed. /// /// This allows e.g. a `IndexedSingleSwiftFileTestProject` to delete its temporary files when they are no longer needed. - private let cleanUp: () -> Void + private let cleanUp: @Sendable () -> Void /// - Parameters: /// - serverOptions: The equivalent of the command line options with which sourcekit-lsp should be started @@ -97,7 +99,7 @@ public final class TestSourceKitLSPClient: MessageHandler { usePullDiagnostics: Bool = true, workspaceFolders: [WorkspaceFolder]? = nil, preInitialization: ((TestSourceKitLSPClient) -> Void)? = nil, - cleanUp: @escaping () -> Void = {} + cleanUp: @Sendable @escaping () -> Void = {} ) async throws { if !useGlobalModuleCache { moduleCache = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) @@ -169,8 +171,7 @@ public final class TestSourceKitLSPClient: MessageHandler { // It's really unfortunate that there are no async deinits. If we had async // deinits, we could await the sending of a ShutdownRequest. let sema = DispatchSemaphore(value: 0) - nextRequestID += 1 - server.handle(ShutdownRequest(), id: .number(nextRequestID)) { result in + server.handle(ShutdownRequest(), id: .number(Int(nextRequestID.fetchAndIncrement()))) { result in sema.signal() } sema.wait() @@ -186,9 +187,8 @@ public final class TestSourceKitLSPClient: MessageHandler { /// Send the request to `server` and return the request result. public func send(_ request: R) async throws -> R.Response { - nextRequestID += 1 return try await withCheckedThrowingContinuation { continuation in - server.handle(request, id: .number(self.nextRequestID)) { result in + server.handle(request, id: .number(Int(nextRequestID.fetchAndIncrement()))) { result in continuation.resume(with: result) } } @@ -273,7 +273,7 @@ public final class TestSourceKitLSPClient: MessageHandler { /// If the next request that is sent to the client is of a different kind than /// the given handler, `TestSourceKitLSPClient` will emit an `XCTFail`. public func handleNextRequest(_ requestHandler: @escaping RequestHandler) { - requestHandlers.append(requestHandler) + requestHandlers.value.append(requestHandler) } // MARK: - Conformance to MessageHandler @@ -290,19 +290,21 @@ public final class TestSourceKitLSPClient: MessageHandler { id: LanguageServerProtocol.RequestID, reply: @escaping (LSPResult) -> Void ) { - let requestHandlerAndIndex = requestHandlers.enumerated().compactMap { - (index, handler) -> (RequestHandler, Int)? in - guard let handler = handler as? RequestHandler else { - return nil + requestHandlers.withLock { requestHandlers in + let requestHandlerAndIndex = requestHandlers.enumerated().compactMap { + (index, handler) -> (RequestHandler, Int)? in + guard let handler = handler as? RequestHandler else { + return nil + } + return (handler, index) + }.first + guard let (requestHandler, index) = requestHandlerAndIndex else { + reply(.failure(.methodNotFound(Request.method))) + return } - return (handler, index) - }.first - guard let (requestHandler, index) = requestHandlerAndIndex else { - reply(.failure(.methodNotFound(Request.method))) - return + reply(.success(requestHandler(params))) + requestHandlers.remove(at: index) } - reply(.success(requestHandler(params))) - requestHandlers.remove(at: index) } // MARK: - Convenience functions @@ -396,8 +398,10 @@ public struct DocumentPositions { /// /// This allows us to set the ``TestSourceKitLSPClient`` as the message handler of /// `SourceKitLSPServer` without retaining it. -private class WeakMessageHandler: MessageHandler { - private weak var handler: (any MessageHandler)? +private final class WeakMessageHandler: MessageHandler, Sendable { + // `nonisolated(unsafe)` is fine because `handler` is never modified, only if the weak reference is deallocated, which + // is atomic. + private nonisolated(unsafe) weak var handler: (any MessageHandler)? init(_ handler: any MessageHandler) { self.handler = handler @@ -410,7 +414,7 @@ private class WeakMessageHandler: MessageHandler { func handle( _ params: Request, id: LanguageServerProtocol.RequestID, - reply: @escaping (LanguageServerProtocol.LSPResult) -> Void + reply: @Sendable @escaping (LanguageServerProtocol.LSPResult) -> Void ) { guard let handler = handler else { reply(.failure(.unknown("Handler has been deallocated"))) diff --git a/Sources/SKTestSupport/Utils.swift b/Sources/SKTestSupport/Utils.swift index cebdc730d..44fb41851 100644 --- a/Sources/SKTestSupport/Utils.swift +++ b/Sources/SKTestSupport/Utils.swift @@ -70,7 +70,7 @@ public func testScratchDir(testName: String = #function) throws -> URL { /// The temporary directory will be deleted at the end of `directory` unless the /// `SOURCEKITLSP_KEEP_TEST_SCRATCH_DIR` environment variable is set. public func withTestScratchDir( - _ body: (AbsolutePath) async throws -> T, + @_inheritActorContext _ body: @Sendable (AbsolutePath) async throws -> T, testName: String = #function ) async throws -> T { let scratchDirectory = try testScratchDir(testName: testName) diff --git a/Sources/SKTestSupport/WrappedSemaphore.swift b/Sources/SKTestSupport/WrappedSemaphore.swift index ee2036557..cdbf3be6b 100644 --- a/Sources/SKTestSupport/WrappedSemaphore.swift +++ b/Sources/SKTestSupport/WrappedSemaphore.swift @@ -17,8 +17,8 @@ import Dispatch /// /// This should only be used for tests that test priority escalation and thus cannot await a `Task` (which would cause /// priority elevations). -public struct WrappedSemaphore { - let semaphore = DispatchSemaphore(value: 0) +public struct WrappedSemaphore: Sendable { + private let semaphore = DispatchSemaphore(value: 0) public init() {} diff --git a/Tests/DiagnoseTests/DiagnoseTests.swift b/Tests/DiagnoseTests/DiagnoseTests.swift index f4502a40f..c1daf1d9c 100644 --- a/Tests/DiagnoseTests/DiagnoseTests.swift +++ b/Tests/DiagnoseTests/DiagnoseTests.swift @@ -19,32 +19,31 @@ import SKTestSupport import SourceKitD import XCTest -import class ISDBTibs.TibsBuilder import struct TSCBasic.AbsolutePath -final class DiagnoseTests: XCTestCase { - /// If a default SDK is present on the test machine, return the `-sdk` argument that can be placed in the request - /// YAML. Otherwise, return an empty string. - private var sdkArg: String { - if let sdk = TibsBuilder.defaultSDKPath { - return """ - "-sdk", "\(sdk)", - """ - } else { - return "" - } +/// If a default SDK is present on the test machine, return the `-sdk` argument that can be placed in the request +/// YAML. Otherwise, return an empty string. +private let sdkArg: String = { + if let sdk = defaultSDKPath { + return """ + "-sdk", "\(sdk)", + """ + } else { + return "" } - - /// If a default SDK is present on the test machine, return the `-sdk` argument that can be placed in the request - /// YAML. Otherwise, return an empty string. - private var sdkArgs: [String] { - if let sdk = TibsBuilder.defaultSDKPath { - return ["-sdk", "\(sdk)"] - } else { - return [] - } +}() + +/// If a default SDK is present on the test machine, return the `-sdk` argument that can be placed in the request +/// YAML. Otherwise, return an empty string. +private let sdkArgs: [String] = { + if let sdk = defaultSDKPath { + return ["-sdk", "\(sdk)"] + } else { + return [] } +}() +final class DiagnoseTests: XCTestCase { func testRemoveCodeItemsAndMembers() async throws { // We consider the test case reproducing if cursor info returns the two ambiguous results including their doc // comments. @@ -232,25 +231,25 @@ final class DiagnoseTests: XCTestCase { private func assertReduceSourceKitD( _ markedFileContents: String, request: String, - reproducerPredicate: @escaping (String) -> Bool, + reproducerPredicate: @Sendable @escaping (String) -> Bool, expectedReducedFileContents: String, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) async throws { let (markers, fileContents) = extractMarkers(markedFileContents) let toolchain = try await unwrap(ToolchainRegistry.forTesting.default) logger.debug("Using \(toolchain.path?.pathString ?? "") to reduce source file") - let requestExecutor = try InProcessSourceKitRequestExecutor( - toolchain: toolchain, - reproducerPredicate: NSPredicate(block: { (requestResponse, _) -> Bool in - reproducerPredicate(requestResponse as! String) - }) - ) let markerOffset = try XCTUnwrap(markers["1️⃣"], "Failed to find position marker 1️⃣ in file contents") try await withTestScratchDir { scratchDir in + let requestExecutor = try InProcessSourceKitRequestExecutor( + toolchain: toolchain, + reproducerPredicate: NSPredicate(block: { (requestResponse, _) -> Bool in + reproducerPredicate(requestResponse as! String) + }) + ) let testFilePath = scratchDir.appending(component: "test.swift").pathString try fileContents.write(toFile: testFilePath, atomically: false, encoding: .utf8) diff --git a/Tests/LanguageServerProtocolJSONRPCTests/CodingTests.swift b/Tests/LanguageServerProtocolJSONRPCTests/CodingTests.swift index a8d9f5a99..fbd9d656d 100644 --- a/Tests/LanguageServerProtocolJSONRPCTests/CodingTests.swift +++ b/Tests/LanguageServerProtocolJSONRPCTests/CodingTests.swift @@ -283,9 +283,8 @@ final class CodingTests: XCTestCase { return $0 == .string("unknown") ? nil : InitializeResult.self } - let info = defaultCodingInfo.merging([CodingUserInfoKey.responseTypeCallbackKey: responseTypeCallback]) { - (_, new) in new - } + var info = defaultCodingInfo as [CodingUserInfoKey: Any] + info[CodingUserInfoKey.responseTypeCallbackKey] = responseTypeCallback checkMessageDecodingError( MessageDecodingError.invalidRequest( @@ -367,7 +366,7 @@ final class CodingTests: XCTestCase { } } -let defaultCodingInfo: [CodingUserInfoKey: Any] = [CodingUserInfoKey.messageRegistryKey: MessageRegistry.lspProtocol] +let defaultCodingInfo = [CodingUserInfoKey.messageRegistryKey: MessageRegistry.lspProtocol] private func checkMessageCoding( _ value: Request, @@ -418,7 +417,7 @@ private func checkMessageCoding( return $0 == .string("unknown") ? nil : Response.self } - var codingInfo = defaultCodingInfo + var codingInfo = defaultCodingInfo as [CodingUserInfoKey: Any] codingInfo[.responseTypeCallbackKey] = callback checkCoding(JSONRPCMessage.response(value, id: id), json: json, userInfo: codingInfo, file: file, line: line) { diff --git a/Tests/LanguageServerProtocolJSONRPCTests/ConnectionPerfTests.swift b/Tests/LanguageServerProtocolJSONRPCTests/ConnectionPerfTests.swift index cb01c9686..8ba626dbb 100644 --- a/Tests/LanguageServerProtocolJSONRPCTests/ConnectionPerfTests.swift +++ b/Tests/LanguageServerProtocolJSONRPCTests/ConnectionPerfTests.swift @@ -32,11 +32,11 @@ class ConnectionPerfTests: PerfTestCase { let expectation = self.expectation(description: "response received") self.startMeasuring() _ = client.send(EchoRequest(string: "hello!")) { _ in - self.stopMeasuring() expectation.fulfill() } - waitForExpectations(timeout: defaultTimeout) + wait(for: [expectation], timeout: defaultTimeout) + self.stopMeasuring() } } diff --git a/Tests/LanguageServerProtocolJSONRPCTests/ConnectionTests.swift b/Tests/LanguageServerProtocolJSONRPCTests/ConnectionTests.swift index 0f8662da3..3f4838974 100644 --- a/Tests/LanguageServerProtocolJSONRPCTests/ConnectionTests.swift +++ b/Tests/LanguageServerProtocolJSONRPCTests/ConnectionTests.swift @@ -37,7 +37,7 @@ class ConnectionTests: XCTestCase { XCTAssertEqual("a/b", dec.string) } - func testEcho() { + func testEcho() async throws { let client = connection.client let expectation = self.expectation(description: "response received") @@ -48,7 +48,7 @@ class ConnectionTests: XCTestCase { expectation.fulfill() } - waitForExpectations(timeout: defaultTimeout) + try await fulfillmentOfOrThrow([expectation]) } func testMessageBuffer() async throws { @@ -95,7 +95,7 @@ class ConnectionTests: XCTestCase { XCTAssert(connection.serverToClientConnection.requestBufferIsEmpty) } - func testEchoError() { + func testEchoError() async throws { let client = connection.client let expectation = self.expectation(description: "response received 1") let expectation2 = self.expectation(description: "response received 2") @@ -114,7 +114,7 @@ class ConnectionTests: XCTestCase { expectation2.fulfill() } - waitForExpectations(timeout: defaultTimeout) + try await fulfillmentOfOrThrow([expectation, expectation2]) } func testEchoNote() async throws { @@ -131,7 +131,7 @@ class ConnectionTests: XCTestCase { try await fulfillmentOfOrThrow([expectation]) } - func testUnknownRequest() { + func testUnknownRequest() async throws { let client = connection.client let expectation = self.expectation(description: "response received") @@ -145,10 +145,10 @@ class ConnectionTests: XCTestCase { expectation.fulfill() } - waitForExpectations(timeout: defaultTimeout) + try await fulfillmentOfOrThrow([expectation]) } - func testUnknownNotification() { + func testUnknownNotification() async throws { let client = connection.client let expectation = self.expectation(description: "note received") @@ -167,10 +167,10 @@ class ConnectionTests: XCTestCase { expectation.fulfill() } - waitForExpectations(timeout: defaultTimeout) + try await fulfillmentOfOrThrow([expectation]) } - func testUnexpectedResponse() { + func testUnexpectedResponse() async throws { let client = connection.client let expectation = self.expectation(description: "response received") @@ -186,10 +186,10 @@ class ConnectionTests: XCTestCase { expectation.fulfill() } - waitForExpectations(timeout: defaultTimeout) + try await fulfillmentOfOrThrow([expectation]) } - func testSendAfterClose() { + func testSendAfterClose() async throws { let client = connection.client let expectation = self.expectation(description: "note received") @@ -206,7 +206,7 @@ class ConnectionTests: XCTestCase { connection.clientToServerConnection.close() connection.clientToServerConnection.close() - waitForExpectations(timeout: defaultTimeout) + try await fulfillmentOfOrThrow([expectation]) } func testSendBeforeClose() async throws { @@ -229,7 +229,7 @@ class ConnectionTests: XCTestCase { /// DispatchIO can make its callback at any time, so this test is to try to /// provoke a race between those things and ensure the closeHandler is called /// exactly once. - func testCloseRace() { + func testCloseRace() async throws { for _ in 0...100 { let to = Pipe() let from = Pipe() @@ -274,9 +274,8 @@ class ConnectionTests: XCTestCase { #endif conn.close() - withExtendedLifetime(conn) { - waitForExpectations(timeout: defaultTimeout) - } + try await fulfillmentOfOrThrow([expectation]) + withExtendedLifetime(conn) {} } } diff --git a/Tests/LanguageServerProtocolTests/ConnectionTests.swift b/Tests/LanguageServerProtocolTests/ConnectionTests.swift index d26e14770..73e5c447a 100644 --- a/Tests/LanguageServerProtocolTests/ConnectionTests.swift +++ b/Tests/LanguageServerProtocolTests/ConnectionTests.swift @@ -26,7 +26,7 @@ class ConnectionTests: XCTestCase { connection.close() } - func testEcho() { + func testEcho() async throws { let client = connection.client let expectation = self.expectation(description: "response received") @@ -37,10 +37,10 @@ class ConnectionTests: XCTestCase { expectation.fulfill() } - waitForExpectations(timeout: defaultTimeout) + try await fulfillmentOfOrThrow([expectation]) } - func testEchoError() { + func testEchoError() async throws { let client = connection.client let expectation = self.expectation(description: "response received 1") let expectation2 = self.expectation(description: "response received 2") @@ -57,7 +57,7 @@ class ConnectionTests: XCTestCase { expectation2.fulfill() } - waitForExpectations(timeout: defaultTimeout) + try await fulfillmentOfOrThrow([expectation, expectation2]) } func testEchoNote() async throws { diff --git a/Tests/SKCoreTests/BuildServerBuildSystemTests.swift b/Tests/SKCoreTests/BuildServerBuildSystemTests.swift index b1e8870bf..26d1d19a3 100644 --- a/Tests/SKCoreTests/BuildServerBuildSystemTests.swift +++ b/Tests/SKCoreTests/BuildServerBuildSystemTests.swift @@ -20,34 +20,57 @@ import SKTestSupport import TSCBasic import XCTest -final class BuildServerBuildSystemTests: XCTestCase { - /// The path to the INPUTS directory of shared test projects. - private static var skTestSupportInputsDirectory: URL = { - #if os(macOS) - // FIXME: Use Bundle.module.resourceURL once the fix for SR-12912 is released. - - var resources = XCTestCase.productsDirectory - .appendingPathComponent("SourceKitLSP_SKTestSupport.bundle") - .appendingPathComponent("Contents") - .appendingPathComponent("Resources") - if !FileManager.default.fileExists(atPath: resources.path) { - // Xcode and command-line swiftpm differ about the path. - resources.deleteLastPathComponent() - resources.deleteLastPathComponent() - } - #else - let resources = XCTestCase.productsDirectory - .appendingPathComponent("SourceKitLSP_SKTestSupport.resources") - #endif - guard FileManager.default.fileExists(atPath: resources.path) else { - fatalError("missing resources \(resources.path)") - } - return resources.appendingPathComponent("INPUTS", isDirectory: true).standardizedFileURL - }() +/// The bundle of the currently executing test. +private let testBundle: Bundle = { + #if os(macOS) + if let bundle = Bundle.allBundles.first(where: { $0.bundlePath.hasSuffix(".xctest") }) { + return bundle + } + fatalError("couldn't find the test bundle") + #else + return Bundle.main + #endif +}() + +/// The path to the built products directory. +private let productsDirectory: URL = { + #if os(macOS) + return testBundle.bundleURL.deletingLastPathComponent() + #else + return testBundle.bundleURL + #endif +}() + +/// The path to the INPUTS directory of shared test projects. +private let skTestSupportInputsDirectory: URL = { + #if os(macOS) + // FIXME: Use Bundle.module.resourceURL once the fix for SR-12912 is released. + + var resources = + productsDirectory + .appendingPathComponent("SourceKitLSP_SKTestSupport.bundle") + .appendingPathComponent("Contents") + .appendingPathComponent("Resources") + if !FileManager.default.fileExists(atPath: resources.path) { + // Xcode and command-line swiftpm differ about the path. + resources.deleteLastPathComponent() + resources.deleteLastPathComponent() + } + #else + let resources = XCTestCase.productsDirectory + .appendingPathComponent("SourceKitLSP_SKTestSupport.resources") + #endif + guard FileManager.default.fileExists(atPath: resources.path) else { + fatalError("missing resources \(resources.path)") + } + return resources.appendingPathComponent("INPUTS", isDirectory: true).standardizedFileURL +}() +final class BuildServerBuildSystemTests: XCTestCase { private var root: AbsolutePath { try! AbsolutePath( - validating: Self.skTestSupportInputsDirectory + validating: + skTestSupportInputsDirectory .appendingPathComponent(testDirectoryName, isDirectory: true).path ) } diff --git a/Tests/SKCoreTests/BuildSystemManagerTests.swift b/Tests/SKCoreTests/BuildSystemManagerTests.swift index bb15ad142..18e0f04ef 100644 --- a/Tests/SKCoreTests/BuildSystemManagerTests.swift +++ b/Tests/SKCoreTests/BuildSystemManagerTests.swift @@ -88,6 +88,7 @@ final class BuildSystemManagerTests: XCTestCase { await assertEqual(bsm._cachedMainFile(for: d), nil) } + @MainActor func testSettingsMainFile() async throws { let a = try DocumentURI(string: "bsm:a.swift") let mainFiles = ManualMainFilesProvider([a: [a]]) @@ -112,6 +113,7 @@ final class BuildSystemManagerTests: XCTestCase { try await fulfillmentOfOrThrow([changed]) } + @MainActor func testSettingsMainFileInitialNil() async throws { let a = try DocumentURI(string: "bsm:a.swift") let mainFiles = ManualMainFilesProvider([a: [a]]) @@ -134,6 +136,7 @@ final class BuildSystemManagerTests: XCTestCase { try await fulfillmentOfOrThrow([changed]) } + @MainActor func testSettingsMainFileWithFallback() async throws { let a = try DocumentURI(string: "bsm:a.swift") let mainFiles = ManualMainFilesProvider([a: [a]]) @@ -164,6 +167,7 @@ final class BuildSystemManagerTests: XCTestCase { try await fulfillmentOfOrThrow([revert]) } + @MainActor func testSettingsMainFileInitialIntersect() async throws { let a = try DocumentURI(string: "bsm:a.swift") let b = try DocumentURI(string: "bsm:b.swift") @@ -205,6 +209,7 @@ final class BuildSystemManagerTests: XCTestCase { try await fulfillmentOfOrThrow([changedBothA, changedBothB]) } + @MainActor func testSettingsMainFileUnchanged() async throws { let a = try DocumentURI(string: "bsm:a.swift") let b = try DocumentURI(string: "bsm:b.swift") @@ -236,6 +241,7 @@ final class BuildSystemManagerTests: XCTestCase { try await fulfillmentOfOrThrow([changed]) } + @MainActor func testSettingsHeaderChangeMainFile() async throws { let h = try DocumentURI(string: "bsm:header.h") let cpp1 = try DocumentURI(string: "bsm:main.cpp") @@ -292,6 +298,7 @@ final class BuildSystemManagerTests: XCTestCase { try await fulfillmentOfOrThrow([changed4]) } + @MainActor func testSettingsOneMainTwoHeader() async throws { let h1 = try DocumentURI(string: "bsm:header1.h") let h2 = try DocumentURI(string: "bsm:header2.h") @@ -340,6 +347,7 @@ final class BuildSystemManagerTests: XCTestCase { try await fulfillmentOfOrThrow([changed1, changed2]) } + @MainActor func testSettingsChangedAfterUnregister() async throws { let a = try DocumentURI(string: "bsm:a.swift") let b = try DocumentURI(string: "bsm:b.swift") @@ -384,6 +392,7 @@ final class BuildSystemManagerTests: XCTestCase { try await fulfillmentOfOrThrow([changedB]) } + @MainActor func testDependenciesUpdated() async throws { let a = try DocumentURI(string: "bsm:a.swift") let mainFiles = ManualMainFilesProvider([a: [a]]) @@ -434,6 +443,7 @@ private final actor ManualMainFilesProvider: MainFilesProvider { } /// A simple `BuildSystem` that wraps a dictionary, for testing. +@MainActor class ManualBuildSystem: BuildSystem { var projectRoot = try! AbsolutePath(validating: "/") diff --git a/Tests/SKSwiftPMWorkspaceTests/SwiftPMBuildSystemTests.swift b/Tests/SKSwiftPMWorkspaceTests/SwiftPMBuildSystemTests.swift index 2ad593636..d77850e32 100644 --- a/Tests/SKSwiftPMWorkspaceTests/SwiftPMBuildSystemTests.swift +++ b/Tests/SKSwiftPMWorkspaceTests/SwiftPMBuildSystemTests.swift @@ -24,7 +24,7 @@ import XCTest import struct PackageModel.BuildFlags #if canImport(SPMBuildCore) -import SPMBuildCore +@preconcurrency import SPMBuildCore #endif fileprivate extension SwiftPMBuildSystem { diff --git a/Tests/SourceKitDTests/CrashRecoveryTests.swift b/Tests/SourceKitDTests/CrashRecoveryTests.swift index 9ef320318..f69c2968a 100644 --- a/Tests/SourceKitDTests/CrashRecoveryTests.swift +++ b/Tests/SourceKitDTests/CrashRecoveryTests.swift @@ -279,18 +279,20 @@ final class CrashRecoveryTests: XCTestCase { let clangdRestartedFirstTime = self.expectation(description: "clangd restarted for the first time") let clangdRestartedSecondTime = self.expectation(description: "clangd restarted for the second time") - var clangdHasRestartedFirstTime = false + let clangdHasRestartedFirstTime = ThreadSafeBox(initialValue: false) await clangdServer.addStateChangeHandler { (oldState, newState) in switch newState { case .connectionInterrupted: clangdCrashed.fulfill() case .connected: - if !clangdHasRestartedFirstTime { - clangdRestartedFirstTime.fulfill() - clangdHasRestartedFirstTime = true - } else { - clangdRestartedSecondTime.fulfill() + clangdHasRestartedFirstTime.withLock { clangdHasRestartedFirstTime in + if !clangdHasRestartedFirstTime { + clangdRestartedFirstTime.fulfill() + clangdHasRestartedFirstTime = true + } else { + clangdRestartedSecondTime.fulfill() + } } default: break diff --git a/Tests/SourceKitDTests/SourceKitDRegistryTests.swift b/Tests/SourceKitDTests/SourceKitDRegistryTests.swift index d4680d751..8b43fcff2 100644 --- a/Tests/SourceKitDTests/SourceKitDRegistryTests.swift +++ b/Tests/SourceKitDTests/SourceKitDRegistryTests.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------===// +import CAtomics import LSPTestSupport import SourceKitD import TSCBasic @@ -40,7 +41,7 @@ final class SourceKitDRegistryTests: XCTestCase { let registry = SourceKitDRegistry() @inline(never) - func scope(registry: SourceKitDRegistry) async throws -> Int { + func scope(registry: SourceKitDRegistry) async throws -> UInt32 { let a = await FakeSourceKitD.getOrCreate(try AbsolutePath(validating: "/a"), in: registry) await assertTrue(a === FakeSourceKitD.getOrCreate(try AbsolutePath(validating: "/a"), in: registry)) @@ -58,10 +59,10 @@ final class SourceKitDRegistryTests: XCTestCase { } } -private var nextToken = 0 +private nonisolated(unsafe) var nextToken = AtomicUInt32(initialValue: 0) final class FakeSourceKitD: SourceKitD { - let token: Int + let token: UInt32 var api: sourcekitd_api_functions_t { fatalError() } var keys: sourcekitd_api_keys { fatalError() } var requests: sourcekitd_api_requests { fatalError() } @@ -69,8 +70,7 @@ final class FakeSourceKitD: SourceKitD { func addNotificationHandler(_ handler: SKDNotificationHandler) { fatalError() } func removeNotificationHandler(_ handler: SKDNotificationHandler) { fatalError() } private init() { - token = nextToken - nextToken += 1 + token = nextToken.fetchAndIncrement() } static func getOrCreate(_ path: AbsolutePath, in registry: SourceKitDRegistry) async -> SourceKitD { diff --git a/Tests/SourceKitDTests/SourceKitDTests.swift b/Tests/SourceKitDTests/SourceKitDTests.swift index 8a2ebe18f..6a4dcb10f 100644 --- a/Tests/SourceKitDTests/SourceKitDTests.swift +++ b/Tests/SourceKitDTests/SourceKitDTests.swift @@ -32,7 +32,7 @@ final class SourceKitDTests: XCTestCase { let keys = sourcekitd.keys let path = DocumentURI.for(.swift).pseudoPath - let isExpectedNotification = { (response: SKDResponse) -> Bool in + let isExpectedNotification = { @Sendable (response: SKDResponse) -> Bool in if let notification: sourcekitd_api_uid_t = response.value?[keys.notification], let name: String = response.value?[keys.name] { @@ -95,10 +95,10 @@ final class SourceKitDTests: XCTestCase { } } -private class ClosureNotificationHandler: SKDNotificationHandler { - let f: (SKDResponse) -> Void +private final class ClosureNotificationHandler: SKDNotificationHandler { + let f: @Sendable (SKDResponse) -> Void - init(_ f: @escaping (SKDResponse) -> Void) { + init(_ f: @Sendable @escaping (SKDResponse) -> Void) { self.f = f } diff --git a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift index 3e1bbf702..f423ac3ac 100644 --- a/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift +++ b/Tests/SourceKitLSPTests/BackgroundIndexingTests.swift @@ -202,9 +202,10 @@ final class BackgroundIndexingTests: XCTestCase { // Wait for indexing to finish without elevating the priority let semaphore = WrappedSemaphore() + let testClient = project.testClient Task(priority: .low) { await assertNoThrow { - try await project.testClient.send(PollIndexRequest()) + try await testClient.send(PollIndexRequest()) } semaphore.signal() } diff --git a/Tests/SourceKitLSPTests/BuildSystemTests.swift b/Tests/SourceKitLSPTests/BuildSystemTests.swift index c3eb343c8..279aac8fd 100644 --- a/Tests/SourceKitLSPTests/BuildSystemTests.swift +++ b/Tests/SourceKitLSPTests/BuildSystemTests.swift @@ -21,11 +21,11 @@ import XCTest /// Build system to be used for testing BuildSystem and BuildSystemDelegate functionality with SourceKitLSPServer /// and other components. -final class TestBuildSystem: BuildSystem { - var projectRoot: AbsolutePath = try! AbsolutePath(validating: "/") - var indexStorePath: AbsolutePath? = nil - var indexDatabasePath: AbsolutePath? = nil - var indexPrefixMappings: [PathPrefixMapping] = [] +actor TestBuildSystem: BuildSystem { + let projectRoot: AbsolutePath = try! AbsolutePath(validating: "/") + let indexStorePath: AbsolutePath? = nil + let indexDatabasePath: AbsolutePath? = nil + let indexPrefixMappings: [PathPrefixMapping] = [] weak var delegate: BuildSystemDelegate? @@ -34,10 +34,14 @@ final class TestBuildSystem: BuildSystem { } /// Build settings by file. - var buildSettingsByFile: [DocumentURI: FileBuildSettings] = [:] + private var buildSettingsByFile: [DocumentURI: FileBuildSettings] = [:] /// Files currently being watched by our delegate. - var watchedFiles: Set = [] + private var watchedFiles: Set = [] + + func setBuildSettings(for uri: DocumentURI, to buildSettings: FileBuildSettings) { + buildSettingsByFile[uri] = buildSettings + } func buildSettings( for document: DocumentURI, @@ -160,7 +164,7 @@ final class BuildSystemTests: XCTestCase { } """ - buildSystem.buildSettingsByFile[doc] = FileBuildSettings(compilerArguments: args) + await buildSystem.setBuildSettings(for: doc, to: FileBuildSettings(compilerArguments: args)) let documentManager = await self.testClient.server._documentManager @@ -173,7 +177,7 @@ final class BuildSystemTests: XCTestCase { // Modify the build settings and inform the delegate. // This should trigger a new publish diagnostics and we should no longer have errors. let newSettings = FileBuildSettings(compilerArguments: args + ["-DFOO"]) - buildSystem.buildSettingsByFile[doc] = newSettings + await buildSystem.setBuildSettings(for: doc, to: newSettings) await buildSystem.delegate?.fileBuildSettingsChanged([doc]) @@ -194,7 +198,7 @@ final class BuildSystemTests: XCTestCase { .buildSettings(for: doc, language: .swift)! .compilerArguments - buildSystem.buildSettingsByFile[doc] = FileBuildSettings(compilerArguments: args) + await buildSystem.setBuildSettings(for: doc, to: FileBuildSettings(compilerArguments: args)) let text = """ #if FOO @@ -214,7 +218,7 @@ final class BuildSystemTests: XCTestCase { // Modify the build settings and inform the delegate. // This should trigger a new publish diagnostics and we should no longer have errors. let newSettings = FileBuildSettings(compilerArguments: args + ["-DFOO"]) - buildSystem.buildSettingsByFile[doc] = newSettings + await buildSystem.setBuildSettings(for: doc, to: newSettings) await buildSystem.delegate?.fileBuildSettingsChanged([doc]) @@ -248,7 +252,7 @@ final class BuildSystemTests: XCTestCase { // Modify the build settings and inform the delegate. // This should trigger a new publish diagnostics and we should see a diagnostic. let newSettings = FileBuildSettings(compilerArguments: args) - buildSystem.buildSettingsByFile[doc] = newSettings + await buildSystem.setBuildSettings(for: doc, to: newSettings) await buildSystem.delegate?.fileBuildSettingsChanged([doc]) @@ -283,7 +287,7 @@ final class BuildSystemTests: XCTestCase { XCTAssertEqual(text, try documentManager.latestSnapshot(doc).text) // Swap from fallback settings to primary build system settings. - buildSystem.buildSettingsByFile[doc] = primarySettings + await buildSystem.setBuildSettings(for: doc, to: primarySettings) await buildSystem.delegate?.fileBuildSettingsChanged([doc]) diff --git a/Tests/SourceKitLSPTests/CodeActionTests.swift b/Tests/SourceKitLSPTests/CodeActionTests.swift index 8a340a21e..481facae6 100644 --- a/Tests/SourceKitLSPTests/CodeActionTests.swift +++ b/Tests/SourceKitLSPTests/CodeActionTests.swift @@ -20,7 +20,7 @@ private typealias CodeActionCapabilities = TextDocumentClientCapabilities.CodeAc private typealias CodeActionLiteralSupport = CodeActionCapabilities.CodeActionLiteralSupport private typealias CodeActionKindCapabilities = CodeActionLiteralSupport.CodeActionKind -private var clientCapabilitiesWithCodeActionSupport: ClientCapabilities = { +private let clientCapabilitiesWithCodeActionSupport: ClientCapabilities = { var documentCapabilities = TextDocumentClientCapabilities() var codeActionCapabilities = CodeActionCapabilities() let codeActionKinds = CodeActionKindCapabilities(valueSet: [.refactor, .quickFix]) diff --git a/Tests/SourceKitLSPTests/LocalSwiftTests.swift b/Tests/SourceKitLSPTests/LocalSwiftTests.swift index 85d22d99c..bbde96fbf 100644 --- a/Tests/SourceKitLSPTests/LocalSwiftTests.swift +++ b/Tests/SourceKitLSPTests/LocalSwiftTests.swift @@ -1352,7 +1352,9 @@ final class LocalSwiftTests: XCTestCase { let uri = DocumentURI(url) let reusedNodeCallback = self.expectation(description: "reused node callback called") - var reusedNodes: [Syntax] = [] + // nonisolated(unsafe) is fine because the variable will only be read after all writes from reusedNodeCallback are + // done. + nonisolated(unsafe) var reusedNodes: [Syntax] = [] let swiftLanguageService = await testClient.server._languageService(for: uri, .swift, in: testClient.server.workspaceForDocument(uri: uri)!) as! SwiftLanguageService