diff --git a/Sources/LSPTestSupport/Assertions.swift b/Sources/LSPTestSupport/Assertions.swift index 26d80e9f5..66b310104 100644 --- a/Sources/LSPTestSupport/Assertions.swift +++ b/Sources/LSPTestSupport/Assertions.swift @@ -12,7 +12,7 @@ import XCTest -/// Same as `assertNoThrow` but executes the trailing closure. +/// Same as `XCTAssertNoThrow` but executes the trailing closure. public func assertNoThrow( _ expression: () throws -> T, _ message: @autoclosure () -> String = "", @@ -22,6 +22,20 @@ public func assertNoThrow( XCTAssertNoThrow(try expression(), message(), file: file, line: line) } +/// Same as `assertNoThrow` but allows the closure to be `async`. +public func assertNoThrow( + _ expression: () async throws -> T, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line +) async { + do { + _ = try await expression() + } catch { + XCTFail("Expression was not expected to throw but threw \(error)", file: file, line: line) + } +} + /// Same as `XCTAssertThrows` but executes the trailing closure. public func assertThrowsError( _ expression: @autoclosure () async throws -> T, @@ -77,6 +91,17 @@ public func assertNotNil( XCTAssertNotNil(expression, message(), file: file, line: line) } +/// Same as `XCTUnwrap` but doesn't take autoclosures and thus `expression` +/// can contain `await`. +public func unwrap( + _ expression: T?, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line +) throws -> T { + return try XCTUnwrap(expression, file: file, line: line) +} + extension XCTestCase { private struct ExpectationNotFulfilledError: Error, CustomStringConvertible { var expecatations: [XCTestExpectation] diff --git a/Sources/LSPTestSupport/TestJSONRPCConnection.swift b/Sources/LSPTestSupport/TestJSONRPCConnection.swift index e6de86a3e..5a953927b 100644 --- a/Sources/LSPTestSupport/TestJSONRPCConnection.swift +++ b/Sources/LSPTestSupport/TestJSONRPCConnection.swift @@ -112,19 +112,6 @@ public final class TestClient: MessageHandler { }) } - public func handleNextNotification(_ handler: @escaping (Notification) -> Void) { - guard oneShotNotificationHandlers.isEmpty else { - XCTFail("unexpected one shot notification handler registered") - return - } - appendOneShotNotificationHandler(handler) - } - - public func handleNextRequest(_ handler: @escaping (Request) -> Void) { - precondition(oneShotRequestHandlers.isEmpty) - appendOneShotRequestHandler(handler) - } - public func handle(_ params: N, from clientID: ObjectIdentifier) where N: NotificationType { let notification = Notification(params, clientID: clientID) @@ -168,58 +155,6 @@ extension TestClient: Connection { ) -> RequestID { return server.send(request, reply: reply) } - - /// Send a notification and expect a notification in reply synchronously. - /// For testing notifications that behave like requests - e.g. didChange & publishDiagnostics. - public func sendNoteSync( - _ notification: some NotificationType, - _ handler: @escaping (Notification) -> Void - ) { - - let expectation = XCTestExpectation(description: "sendNoteSync - note received") - - handleNextNotification { (note: Notification) in - handler(note) - expectation.fulfill() - } - - send(notification) - - let result = XCTWaiter.wait(for: [expectation], timeout: defaultTimeout) - guard result == .completed else { - XCTFail("error \(result) waiting for notification in response to \(notification)") - return - } - } - - /// Send a notification and expect two notifications in reply synchronously. - /// For testing notifications that behave like requests - e.g. didChange & publishDiagnostics. - public func sendNoteSync( - _ notification: NSend, - _ handler1: @escaping (Notification) -> Void, - _ handler2: @escaping (Notification) -> Void - ) where NSend: NotificationType { - - let expectation = XCTestExpectation(description: "sendNoteSync - note received") - expectation.expectedFulfillmentCount = 2 - - handleNextNotification { (note: Notification) in - handler1(note) - expectation.fulfill() - } - appendOneShotNotificationHandler { (note: Notification) in - handler2(note) - expectation.fulfill() - } - - send(notification) - - let result = XCTWaiter.wait(for: [expectation], timeout: defaultTimeout) - guard result == .completed else { - XCTFail("wait for notification in response to \(notification) failed with \(result)") - return - } - } } public final class TestServer: MessageHandler { diff --git a/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift b/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift index b8d19a057..235472b28 100644 --- a/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift +++ b/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift @@ -53,13 +53,16 @@ public final class SKSwiftPMTestWorkspace { public let toolchain: Toolchain /// Connection to the language server. - public let testServer: TestSourceKitServer - - public var sk: TestClient { testServer.client } + public let testClient: TestSourceKitLSPClient /// When `testServer` is not `nil`, the workspace will be opened in that server, otherwise a new server will be created for the workspace - public init(projectDir: URL, tmpDir: URL, toolchain: Toolchain, testServer: TestSourceKitServer? = nil) async throws { - self.testServer = testServer ?? TestSourceKitServer(connectionKind: .local) + public init( + projectDir: URL, + tmpDir: URL, + toolchain: Toolchain, + testClient: TestSourceKitLSPClient? = nil + ) async throws { + self.testClient = testClient ?? TestSourceKitLSPClient() self.projectDir = URL( fileURLWithPath: try resolveSymlinks(AbsolutePath(validating: projectDir.path)).pathString @@ -105,7 +108,7 @@ public final class SKSwiftPMTestWorkspace { listenToUnitEvents: false ) - let server = self.testServer.server! + let server = self.testClient.server let workspace = await Workspace( documentManager: DocumentManager(), rootUri: DocumentURI(sources.rootDirectory), @@ -153,7 +156,7 @@ extension SKSwiftPMTestWorkspace { extension SKSwiftPMTestWorkspace { public func openDocument(_ url: URL, language: Language) throws { - sk.send( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: DocumentURI(url), @@ -166,7 +169,7 @@ extension SKSwiftPMTestWorkspace { } public func closeDocument(_ url: URL) { - sk.send(DidCloseTextDocumentNotification(textDocument: TextDocumentIdentifier(DocumentURI(url)))) + testClient.send(DidCloseTextDocumentNotification(textDocument: TextDocumentIdentifier(DocumentURI(url)))) } } @@ -174,7 +177,7 @@ extension XCTestCase { public func staticSourceKitSwiftPMWorkspace( name: String, - server: TestSourceKitServer? = nil + testClient: TestSourceKitLSPClient? = nil ) async throws -> SKSwiftPMTestWorkspace? { let testDirName = testDirectoryName let toolchain = ToolchainRegistry.shared.default! @@ -183,7 +186,7 @@ extension XCTestCase { tmpDir: URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) .appendingPathComponent("sk-test-data/\(testDirName)/\(name)", isDirectory: true), toolchain: toolchain, - testServer: server + testClient: testClient ) let hasClangFile: Bool = workspace.sources.locations.contains { _, loc in diff --git a/Sources/SKTestSupport/SKTibsTestWorkspace.swift b/Sources/SKTestSupport/SKTibsTestWorkspace.swift index bab0208bc..f85bc448f 100644 --- a/Sources/SKTestSupport/SKTibsTestWorkspace.swift +++ b/Sources/SKTestSupport/SKTibsTestWorkspace.swift @@ -36,12 +36,11 @@ fileprivate extension SourceKitServer { public final class SKTibsTestWorkspace { public let tibsWorkspace: TibsTestWorkspace - public let testServer: TestSourceKitServer + public let testClient: TestSourceKitLSPClient public var index: IndexStoreDB { tibsWorkspace.index } public var builder: TibsBuilder { tibsWorkspace.builder } public var sources: TestSources { tibsWorkspace.sources } - public var sk: TestClient { testServer.client } public init( immutableProjectDir: URL, @@ -50,9 +49,9 @@ public final class SKTibsTestWorkspace { removeTmpDir: Bool, toolchain: Toolchain, clientCapabilities: ClientCapabilities, - testServer: TestSourceKitServer? = nil + testClient: TestSourceKitLSPClient? = nil ) async throws { - self.testServer = testServer ?? TestSourceKitServer(connectionKind: .local) + self.testClient = testClient ?? TestSourceKitLSPClient() self.tibsWorkspace = try TibsTestWorkspace( immutableProjectDir: immutableProjectDir, persistentBuildDir: persistentBuildDir, @@ -69,9 +68,9 @@ public final class SKTibsTestWorkspace { tmpDir: URL, toolchain: Toolchain, clientCapabilities: ClientCapabilities, - testServer: TestSourceKitServer? = nil + testClient: TestSourceKitLSPClient? = nil ) async throws { - self.testServer = testServer ?? TestSourceKitServer(connectionKind: .local) + self.testClient = testClient ?? TestSourceKitLSPClient() self.tibsWorkspace = try TibsTestWorkspace( projectDir: projectDir, @@ -99,8 +98,8 @@ public final class SKTibsTestWorkspace { indexDelegate: indexDelegate ) - await workspace.buildSystemManager.setDelegate(testServer.server!) - await testServer.server!.setWorkspaces([workspace]) + await workspace.buildSystemManager.setDelegate(testClient.server) + await testClient.server.setWorkspaces([workspace]) } } @@ -123,7 +122,7 @@ extension SKTibsTestWorkspace { extension SKTibsTestWorkspace { public func openDocument(_ url: URL, language: Language) throws { - sk.send( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: DocumentURI(url), @@ -143,7 +142,7 @@ extension XCTestCase { clientCapabilities: ClientCapabilities = .init(), tmpDir: URL? = nil, removeTmpDir: Bool = true, - server: TestSourceKitServer? = nil + testClient: TestSourceKitLSPClient? = nil ) async throws -> SKTibsTestWorkspace? { let testDirName = testDirectoryName let workspace = try await SKTibsTestWorkspace( @@ -157,7 +156,7 @@ extension XCTestCase { removeTmpDir: removeTmpDir, toolchain: ToolchainRegistry.shared.default!, clientCapabilities: clientCapabilities, - testServer: server + testClient: testClient ) if workspace.builder.targets.contains(where: { target in !target.clangTUs.isEmpty }) diff --git a/Sources/SKTestSupport/TestServer.swift b/Sources/SKTestSupport/TestServer.swift deleted file mode 100644 index dc7aeb8e5..000000000 --- a/Sources/SKTestSupport/TestServer.swift +++ /dev/null @@ -1,146 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 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 Foundation -import LSPTestSupport -import LanguageServerProtocol -import LanguageServerProtocolJSONRPC -import SKCore -import SKSupport -import SourceKitLSP - -public final class TestSourceKitServer { - public enum ConnectionKind { - case local, jsonrpc - } - - enum ConnectionImpl { - case local( - clientConnection: LocalConnection, - serverConnection: LocalConnection - ) - case jsonrpc( - clientToServer: Pipe, - serverToClient: Pipe, - clientConnection: JSONRPCConnection, - serverConnection: JSONRPCConnection - ) - } - - public static let serverOptions: SourceKitServer.Options = SourceKitServer.Options() - - /// If the server is not using the global module cache, the path of the local - /// module cache. - /// - /// This module cache will be deleted when the test server is destroyed. - private let moduleCache: URL? - - public let client: TestClient - let connImpl: ConnectionImpl - - public var hasShutdown: Bool = false - - /// The server, if it is in the same process. - public let server: SourceKitServer? - - /// - Parameters: - /// - useGlobalModuleCache: If `false`, the server will use its own module - /// cache in an empty temporary directory instead of the global module cache. - public init(connectionKind: ConnectionKind = .local, useGlobalModuleCache: Bool = true) { - if !useGlobalModuleCache { - moduleCache = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) - } else { - moduleCache = nil - } - var serverOptions = Self.serverOptions - if let moduleCache { - serverOptions.buildSetup.flags.swiftCompilerFlags += ["-module-cache-path", moduleCache.path] - } - - switch connectionKind { - case .local: - let clientConnection = LocalConnection() - let serverConnection = LocalConnection() - client = TestClient(server: serverConnection) - server = SourceKitServer( - client: clientConnection, - options: serverOptions, - onExit: { - clientConnection.close() - } - ) - - clientConnection.start(handler: client) - serverConnection.start(handler: server!) - - connImpl = .local(clientConnection: clientConnection, serverConnection: serverConnection) - - case .jsonrpc: - let clientToServer: Pipe = Pipe() - let serverToClient: Pipe = Pipe() - - let clientConnection = JSONRPCConnection( - protocol: MessageRegistry.lspProtocol, - inFD: serverToClient.fileHandleForReading, - outFD: clientToServer.fileHandleForWriting - ) - let serverConnection = JSONRPCConnection( - protocol: MessageRegistry.lspProtocol, - inFD: clientToServer.fileHandleForReading, - outFD: serverToClient.fileHandleForWriting - ) - - client = TestClient(server: clientConnection) - server = SourceKitServer( - client: serverConnection, - options: serverOptions, - onExit: { - serverConnection.close() - } - ) - - clientConnection.start(receiveHandler: client) { - // FIXME: keep the pipes alive until we close the connection. This - // should be fixed systemically. - withExtendedLifetime((clientToServer, serverToClient)) {} - } - serverConnection.start(receiveHandler: server!) { - // FIXME: keep the pipes alive until we close the connection. This - // should be fixed systemically. - withExtendedLifetime((clientToServer, serverToClient)) {} - } - - connImpl = .jsonrpc( - clientToServer: clientToServer, - serverToClient: serverToClient, - clientConnection: clientConnection, - serverConnection: serverConnection - ) - } - } - - deinit { - close() - - if let moduleCache { - try? FileManager.default.removeItem(at: moduleCache) - } - } - - func close() { - if !hasShutdown { - hasShutdown = true - _ = try! self.client.sendSync(ShutdownRequest()) - self.client.send(ExitNotification()) - } - } -} diff --git a/Sources/SKTestSupport/TestSourceKitLSPClient.swift b/Sources/SKTestSupport/TestSourceKitLSPClient.swift new file mode 100644 index 000000000..cbeb6e0e4 --- /dev/null +++ b/Sources/SKTestSupport/TestSourceKitLSPClient.swift @@ -0,0 +1,248 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2020 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 Foundation +import LSPTestSupport +import LanguageServerProtocol +import LanguageServerProtocolJSONRPC +import SKCore +import SKSupport +import SourceKitLSP +import XCTest + +extension SourceKitServer.Options { + /// The default SourceKitServer options for testing. + public static var testDefault = Self() +} + +/// A mock SourceKit-LSP client (aka. a mock editor) that behaves like an editor +/// for testing purposes. +/// +/// It can send requests to the LSP server and receive requests or notifications +/// that the server sends to the client. +public final class TestSourceKitLSPClient: MessageHandler { + /// A function that takes a request and returns the request's response. + 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 + + /// If the server is not using the global module cache, the path of the local + /// module cache. + /// + /// This module cache will be deleted when the test server is destroyed. + private let moduleCache: URL? + + /// The server that handles the requests. + public let server: SourceKitServer + + /// The connection via which the server sends requests and notifications to us. + private let serverToClientConnection: LocalConnection + + /// Stream of the notifications that the server has sent to the client. + private let notifications: AsyncStream + + /// Continuation to add a new notification from the ``server`` to the `notifications` stream. + private let notificationYielder: AsyncStream.Continuation + + /// The request handlers that have been set by `handleNextRequest`. + /// + /// 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] = [] + + /// - Parameters: + /// - useGlobalModuleCache: If `false`, the server will use its own module + /// cache in an empty temporary directory instead of the global module cache. + public init(useGlobalModuleCache: Bool = true) { + if !useGlobalModuleCache { + moduleCache = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) + } else { + moduleCache = nil + } + var serverOptions = SourceKitServer.Options.testDefault + if let moduleCache { + serverOptions.buildSetup.flags.swiftCompilerFlags += ["-module-cache-path", moduleCache.path] + } + + var notificationYielder: AsyncStream.Continuation! + self.notifications = AsyncStream { continuation in + notificationYielder = continuation + } + self.notificationYielder = notificationYielder + + let clientConnection = LocalConnection() + self.serverToClientConnection = clientConnection + server = SourceKitServer( + client: clientConnection, + options: serverOptions, + onExit: { + clientConnection.close() + } + ) + + self.serverToClientConnection.start(handler: WeakMessageHandler(self)) + } + + deinit { + // 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), from: ObjectIdentifier(self)) { result in + sema.signal() + } + sema.wait() + self.send(ExitNotification()) + + if let moduleCache { + try? FileManager.default.removeItem(at: moduleCache) + } + } + + // MARK: - Sending messages + + /// 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), from: ObjectIdentifier(self)) { result in + continuation.resume(with: result) + } + } + } + + /// Send the notification to `server`. + public func send(_ notification: some NotificationType) { + server.handle(notification, from: ObjectIdentifier(self)) + } + + // MARK: - Handling messages sent to the editor + + /// Await the next notification that is sent to the client. + /// + /// - Note: This also returns any notifications sent before the call to + /// `nextNotification`. + public func nextNotification(timeout: TimeInterval = defaultTimeout) async throws -> any NotificationType { + struct TimeoutError: Error, CustomStringConvertible { + var description: String = "Failed to receive next notification within timeout" + } + + return try await withThrowingTaskGroup(of: (any NotificationType).self) { taskGroup in + taskGroup.addTask { + for await notification in self.notifications { + return notification + } + throw TimeoutError() + } + taskGroup.addTask { + try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) + throw TimeoutError() + } + let result = try await taskGroup.next()! + taskGroup.cancelAll() + return result + } + } + + /// Await the next diagnostic notification sent to the client. + /// + /// If the next notification is not a `PublishDiagnosticsNotification`, this + /// methods throws. + public func nextDiagnosticsNotification() async throws -> PublishDiagnosticsNotification { + struct CastError: Error, CustomStringConvertible { + let actualType: any NotificationType.Type + + var description: String { "Expected a publish diagnostics notification but got '\(actualType)'" } + } + + let nextNotification = try await nextNotification() + guard let diagnostics = nextNotification as? PublishDiagnosticsNotification else { + throw CastError(actualType: type(of: nextNotification)) + } + return diagnostics + } + + /// Handle the next request that is sent to the client with the given handler. + /// + /// By default, `TestSourceKitServer` emits an `XCTFail` if a request is sent + /// to the client, since it doesn't know how to handle it. This allows the + /// simulation of a single request's handling on the client. + /// + /// If the next request that is sent to the client is of a different kind than + /// the given handler, `TestSourceKitServer` will emit an `XCTFail`. + public func handleNextRequest(_ requestHandler: @escaping RequestHandler) { + requestHandlers.append(requestHandler) + } + + // MARK: - Conformance to MessageHandler + + /// - Important: Implementation detail of `TestSourceKitServer`. Do not call + /// from tests. + public func handle(_ params: some NotificationType, from clientID: ObjectIdentifier) { + notificationYielder.yield(params) + } + + /// - Important: Implementation detail of `TestSourceKitServer`. Do not call + /// from tests. + public func handle( + _ params: Request, + id: LanguageServerProtocol.RequestID, + from clientID: ObjectIdentifier, + reply: @escaping (LSPResult) -> Void + ) { + guard let requestHandler = requestHandlers.first else { + XCTFail("Received unexpected request \(Request.method)") + reply(.failure(.methodNotFound(Request.method))) + return + } + guard let requestHandler = requestHandler as? RequestHandler else { + print("\(RequestHandler.self)") + XCTFail("Received request of unexpected type \(Request.method)") + reply(.failure(.methodNotFound(Request.method))) + return + } + reply(.success(requestHandler(params))) + requestHandlers.removeFirst() + } +} + +// MARK: - WeakMessageHelper + +/// Wrapper around a weak `MessageHandler`. +/// +/// This allows us to set the ``TestSourceKitServer`` as the message handler of +/// `SourceKitServer` without retaining it. +private class WeakMessageHandler: MessageHandler { + private weak var handler: (any MessageHandler)? + + init(_ handler: any MessageHandler) { + self.handler = handler + } + + func handle(_ params: some LanguageServerProtocol.NotificationType, from clientID: ObjectIdentifier) { + handler?.handle(params, from: clientID) + } + + func handle( + _ params: Request, + id: LanguageServerProtocol.RequestID, + from clientID: ObjectIdentifier, + reply: @escaping (LanguageServerProtocol.LSPResult) -> Void + ) { + guard let handler = handler else { + reply(.failure(.unknown("Handler has been deallocated"))) + return + } + handler.handle(params, id: id, from: clientID, reply: reply) + } +} diff --git a/Sources/SourceKitLSP/SourceKitServer.swift b/Sources/SourceKitLSP/SourceKitServer.swift index eea1cfabc..cfaa1d1b8 100644 --- a/Sources/SourceKitLSP/SourceKitServer.swift +++ b/Sources/SourceKitLSP/SourceKitServer.swift @@ -535,7 +535,11 @@ extension SourceKitServer: MessageHandler { // completion session but it only makes sense for the client to request // more results for this completion session after it has received the // initial results. - messageHandlingQueue.async(barrier: false) { + // FIXME: (async) We need more granular request handling. Completion requests + // to the same file depend on each other because we only have one global + // code completion session in sourcekitd but they don't need to be full + // barriers to any other request. + messageHandlingQueue.async(barrier: R.self is CompletionRequest.Type) { let cancellationToken = CancellationToken() let request = Request( diff --git a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift index 34a56d11f..9f4539c7a 100644 --- a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift +++ b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift @@ -27,9 +27,6 @@ import SourceKitD /// `codecomplete.close` requests. actor CodeCompletionSession { private unowned let server: SwiftLanguageServer - /// The queue on which `update` and `close` are executed to ensure in-order - /// execution. - private let queue: AsyncQueue = AsyncQueue(.serial) private let snapshot: DocumentSnapshot let utf8StartOffset: Int private let position: Position @@ -72,16 +69,13 @@ actor CodeCompletionSession { in snapshot: DocumentSnapshot, options: SKCompletionOptions ) async throws -> CompletionList { - let task = queue.asyncThrowing { - switch self.state { - case .closed: - self.state = .open - return try await self.open(filterText: filterText, position: position, in: snapshot, options: options) - case .open: - return try await self.updateImpl(filterText: filterText, position: position, in: snapshot, options: options) - } + switch self.state { + case .closed: + self.state = .open + return try await self.open(filterText: filterText, position: position, in: snapshot, options: options) + case .open: + return try await self.updateImpl(filterText: filterText, position: position, in: snapshot, options: options) } - return try await task.value } private func open( @@ -185,19 +179,17 @@ actor CodeCompletionSession { _ = try? server.sourcekitd.sendSync(req) } - func close() { + func close() async { // Temporary back-reference to server to keep it alive during close(). let server = self.server - queue.async { - switch self.state { - case .closed: - // Already closed, nothing to do. - break - case .open: - self.sendClose(server) - self.state = .closed - } + switch self.state { + case .closed: + // Already closed, nothing to do. + break + case .open: + self.sendClose(server) + self.state = .closed } } } diff --git a/Tests/LanguageServerProtocolJSONRPCTests/ConnectionTests.swift b/Tests/LanguageServerProtocolJSONRPCTests/ConnectionTests.swift index 56f3631f9..919356e89 100644 --- a/Tests/LanguageServerProtocolJSONRPCTests/ConnectionTests.swift +++ b/Tests/LanguageServerProtocolJSONRPCTests/ConnectionTests.swift @@ -60,7 +60,7 @@ class ConnectionTests: XCTestCase { let clientConnection = connection.clientConnection let expectation = self.expectation(description: "note received") - client.handleNextNotification { (note: Notification) in + client.appendOneShotNotificationHandler { (note: Notification) in XCTAssertEqual(note.params.string, "hello!") expectation.fulfill() } @@ -83,7 +83,7 @@ class ConnectionTests: XCTestCase { let expectation2 = self.expectation(description: "note received") - client.handleNextNotification { (note: Notification) in + client.appendOneShotNotificationHandler { (note: Notification) in XCTAssertEqual(note.params.string, "no way!") expectation2.fulfill() } @@ -125,7 +125,7 @@ class ConnectionTests: XCTestCase { let client = connection.client let expectation = self.expectation(description: "note received") - client.handleNextNotification { (note: Notification) in + client.appendOneShotNotificationHandler { (note: Notification) in XCTAssertEqual(note.params.string, "hello!") expectation.fulfill() } @@ -218,7 +218,7 @@ class ConnectionTests: XCTestCase { let server = connection.server let expectation = self.expectation(description: "received notification") - client.handleNextNotification { (note: Notification) in + client.appendOneShotNotificationHandler { (note: Notification) in expectation.fulfill() } @@ -232,7 +232,7 @@ class ConnectionTests: XCTestCase { let client = connection.client let expectation = self.expectation(description: "received notification") - client.handleNextNotification { (note: Notification) in + client.appendOneShotNotificationHandler { (note: Notification) in expectation.fulfill() } let notification = EchoNotification(string: "about to close!") diff --git a/Tests/LanguageServerProtocolTests/ConnectionTests.swift b/Tests/LanguageServerProtocolTests/ConnectionTests.swift index 903f2ca96..bc7e9b038 100644 --- a/Tests/LanguageServerProtocolTests/ConnectionTests.swift +++ b/Tests/LanguageServerProtocolTests/ConnectionTests.swift @@ -68,7 +68,7 @@ class ConnectionTests: XCTestCase { let client = connection.client let expectation = self.expectation(description: "note received") - client.handleNextNotification { (note: Notification) in + client.appendOneShotNotificationHandler { (note: Notification) in XCTAssertEqual(note.params.string, "hello!") expectation.fulfill() } diff --git a/Tests/SKSwiftPMWorkspaceTests/SwiftPMWorkspaceTests.swift b/Tests/SKSwiftPMWorkspaceTests/SwiftPMWorkspaceTests.swift index f67d7e6c0..8fcd17fe7 100644 --- a/Tests/SKSwiftPMWorkspaceTests/SwiftPMWorkspaceTests.swift +++ b/Tests/SKSwiftPMWorkspaceTests/SwiftPMWorkspaceTests.swift @@ -17,6 +17,7 @@ import PackageModel import SKCore import SKSwiftPMWorkspace import SKTestSupport +import SourceKitLSP import TSCBasic import XCTest @@ -44,7 +45,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: TestSourceKitServer.serverOptions.buildSetup + buildSetup: SourceKitServer.Options.testDefault.buildSetup ) ) } @@ -71,7 +72,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: TestSourceKitServer.serverOptions.buildSetup + buildSetup: SourceKitServer.Options.testDefault.buildSetup ) ) } @@ -98,7 +99,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: ToolchainRegistry(), fileSystem: fs, - buildSetup: TestSourceKitServer.serverOptions.buildSetup + buildSetup: SourceKitServer.Options.testDefault.buildSetup ) ) } @@ -126,7 +127,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: TestSourceKitServer.serverOptions.buildSetup + buildSetup: SourceKitServer.Options.testDefault.buildSetup ) let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") @@ -231,7 +232,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: TestSourceKitServer.serverOptions.buildSetup + buildSetup: SourceKitServer.Options.testDefault.buildSetup ) let source = try resolveSymlinks(packageRoot.appending(component: "Package.swift")) @@ -265,7 +266,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: TestSourceKitServer.serverOptions.buildSetup + buildSetup: SourceKitServer.Options.testDefault.buildSetup ) let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") @@ -309,7 +310,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: TestSourceKitServer.serverOptions.buildSetup + buildSetup: SourceKitServer.Options.testDefault.buildSetup ) let aswift = packageRoot.appending(components: "Sources", "libA", "a.swift") @@ -371,7 +372,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: TestSourceKitServer.serverOptions.buildSetup + buildSetup: SourceKitServer.Options.testDefault.buildSetup ) let aswift = packageRoot.appending(components: "Sources", "libA", "a.swift") @@ -407,7 +408,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: TestSourceKitServer.serverOptions.buildSetup + buildSetup: SourceKitServer.Options.testDefault.buildSetup ) let acxx = packageRoot.appending(components: "Sources", "lib", "a.cpp") @@ -499,7 +500,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: ToolchainRegistry.shared, fileSystem: fs, - buildSetup: TestSourceKitServer.serverOptions.buildSetup + buildSetup: SourceKitServer.Options.testDefault.buildSetup ) let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") @@ -547,7 +548,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: TestSourceKitServer.serverOptions.buildSetup + buildSetup: SourceKitServer.Options.testDefault.buildSetup ) let aswift1 = packageRoot.appending(components: "Sources", "lib", "a.swift") @@ -606,7 +607,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: TestSourceKitServer.serverOptions.buildSetup + buildSetup: SourceKitServer.Options.testDefault.buildSetup ) let acxx = packageRoot.appending(components: "Sources", "lib", "a.cpp") @@ -651,7 +652,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: TestSourceKitServer.serverOptions.buildSetup + buildSetup: SourceKitServer.Options.testDefault.buildSetup ) let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") @@ -687,7 +688,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, - buildSetup: TestSourceKitServer.serverOptions.buildSetup + buildSetup: SourceKitServer.Options.testDefault.buildSetup ) assertEqual(await ws._packageRoot, try resolveSymlinks(tempDir.appending(component: "pkg"))) @@ -734,7 +735,7 @@ private func check( private func buildPath( root: AbsolutePath, - config: BuildSetup = TestSourceKitServer.serverOptions.buildSetup, + config: BuildSetup = SourceKitServer.Options.testDefault.buildSetup, platform: String ) -> AbsolutePath { let buildPath = config.path ?? root.appending(component: ".build") diff --git a/Tests/SourceKitDTests/CrashRecoveryTests.swift b/Tests/SourceKitDTests/CrashRecoveryTests.swift index bacf74156..2e2213a68 100644 --- a/Tests/SourceKitDTests/CrashRecoveryTests.swift +++ b/Tests/SourceKitDTests/CrashRecoveryTests.swift @@ -53,19 +53,12 @@ final class CrashRecoveryTests: XCTestCase { // Open the document. Wait for the semantic diagnostics to know it has been fully opened and we are not entering any data races about outstanding diagnostics when we crash sourcekitd. - let documentOpened = self.expectation(description: "documentOpened") - documentOpened.expectedFulfillmentCount = 2 - ws.sk.handleNextNotification({ (note: LanguageServerProtocol.Notification) in - log("Received diagnostics for open - syntactic") - documentOpened.fulfill() - }) - ws.sk.appendOneShotNotificationHandler({ - (note: LanguageServerProtocol.Notification) in - log("Received diagnostics for open - semantic") - documentOpened.fulfill() - }) try ws.openDocument(loc.url, language: .swift) - try await fulfillmentOfOrThrow([documentOpened]) + + // Wait for syntactic and semantic diagnsotics to be produced to make sure the + // document open got handled by sourcekitd + _ = try await ws.testClient.nextDiagnosticsNotification() + _ = try await ws.testClient.nextDiagnosticsNotification() // Make a change to the file that's not saved to disk. This way we can check that we re-open the correct in-memory state. @@ -79,23 +72,17 @@ final class CrashRecoveryTests: XCTestCase { } """ ) - ws.sk.sendNoteSync( + ws.testClient.send( DidChangeTextDocumentNotification( textDocument: VersionedTextDocumentIdentifier(loc.docUri, version: 2), contentChanges: [addFuncChange] - ), - { (note: LanguageServerProtocol.Notification) -> Void in - log("Received diagnostics for text edit - syntactic") - }, - { (note: LanguageServerProtocol.Notification) -> Void in - log("Received diagnostics for text edit - semantic") - } + ) ) // Do a sanity check and verify that we get the expected result from a hover response before crashing sourcekitd. let hoverRequest = HoverRequest(textDocument: loc.docIdentifier, position: Position(line: 1, utf16index: 6)) - let preCrashHoverResponse = try ws.sk.sendSync(hoverRequest) + let preCrashHoverResponse = try await ws.testClient.send(hoverRequest) precondition( preCrashHoverResponse?.contains(string: "foo()") ?? false, "Sanity check failed. The Hover response did not contain foo(), even before crashing sourcekitd. Received response: \(String(describing: preCrashHoverResponse))" @@ -104,12 +91,11 @@ final class CrashRecoveryTests: XCTestCase { // Crash sourcekitd let sourcekitdServer = - await ws.testServer.server!._languageService( + await ws.testClient.server._languageService( for: loc.docUri, .swift, - in: ws.testServer.server!.workspaceForDocument(uri: loc.docUri)! - ) - as! SwiftLanguageServer + in: ws.testClient.server.workspaceForDocument(uri: loc.docUri)! + ) as! SwiftLanguageServer let sourcekitdCrashed = expectation(description: "sourcekitd has crashed") let sourcekitdRestarted = expectation(description: "sourcekitd has been restarted (syntactic only)") @@ -135,18 +121,18 @@ final class CrashRecoveryTests: XCTestCase { // Check that we have syntactic functionality again - _ = try ws.sk.sendSync(FoldingRangeRequest(textDocument: loc.docIdentifier)) + _ = try await ws.testClient.send(FoldingRangeRequest(textDocument: loc.docIdentifier)) // sourcekitd's semantic request timer is only started when the first semantic request comes in. // Send a hover request (which will fail) to trigger that timer. // Afterwards wait for semantic functionality to be restored. - _ = try? ws.sk.sendSync(hoverRequest) + _ = try? await ws.testClient.send(hoverRequest) try await fulfillmentOfOrThrow([semanticFunctionalityRestored], timeout: 30) // Check that we get the same hover response from the restored in-memory state - assertNoThrow { - let postCrashHoverResponse = try ws.sk.sendSync(hoverRequest) + await assertNoThrow { + let postCrashHoverResponse = try await ws.testClient.send(hoverRequest) XCTAssertTrue(postCrashHoverResponse?.contains(string: "foo()") ?? false) } } @@ -156,10 +142,10 @@ final class CrashRecoveryTests: XCTestCase { /// - ws: The workspace for which the clangd server shall be crashed /// - document: The URI of a C/C++/... document in the workspace private func crashClangd(for ws: SKTibsTestWorkspace, document docUri: DocumentURI) async throws { - let clangdServer = await ws.testServer.server!._languageService( + let clangdServer = await ws.testClient.server._languageService( for: docUri, .cpp, - in: ws.testServer.server!.workspaceForDocument(uri: docUri)! + in: ws.testClient.server.workspaceForDocument(uri: docUri)! )! let clangdCrashed = self.expectation(description: "clangd crashed") @@ -201,7 +187,7 @@ final class CrashRecoveryTests: XCTestCase { } """ ) - ws.sk.send( + ws.testClient.send( DidChangeTextDocumentNotification( textDocument: VersionedTextDocumentIdentifier(loc.docUri, version: 2), contentChanges: [addFuncChange] @@ -213,7 +199,7 @@ final class CrashRecoveryTests: XCTestCase { let expectedHoverRange = Position(line: 1, utf16index: 5)..) in - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) - } + ) ) + let diags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(diags.diagnostics.count, 1) + XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) // Modify the build settings and inform the delegate. // This should trigger a new publish diagnostics and we should no longer have errors. @@ -190,11 +181,9 @@ final class BuildSystemTests: XCTestCase { buildSystem.buildSettingsByFile[doc] = newSettings let expectation = XCTestExpectation(description: "refresh") - sk.handleNextNotification { (note: Notification) in - XCTAssertEqual(note.params.diagnostics.count, 0) - XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) - expectation.fulfill() - } + let refreshedDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(refreshedDiags.diagnostics.count, 0) + XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) await buildSystem.delegate?.fileBuildSettingsChanged([doc]) @@ -216,11 +205,9 @@ final class BuildSystemTests: XCTestCase { foo() """ - sk.allowUnexpectedNotification = false - - let documentManager = await self.testServer.server!._documentManager + let documentManager = await self.testClient.server._documentManager - sk.sendNoteSync( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: doc, @@ -228,38 +215,29 @@ final class BuildSystemTests: XCTestCase { version: 12, text: text ) - ), - { (note: Notification) in - // Syntactic analysis - no expected errors here. - XCTAssertEqual(note.params.diagnostics.count, 0) - XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) - }, - { (note: Notification) in - // Semantic analysis - expect one error here. - XCTAssertEqual(note.params.diagnostics.count, 1) - } + ) ) + let syntacticDiags1 = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(syntacticDiags1.diagnostics.count, 0) + XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) + + let semanticDiags1 = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(semanticDiags1.diagnostics.count, 1) // 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 - let expectation = XCTestExpectation(description: "refresh") - expectation.expectedFulfillmentCount = 2 - sk.handleNextNotification { (note: Notification) in - // Semantic analysis - SourceKit currently caches diagnostics so we still see an error. - XCTAssertEqual(note.params.diagnostics.count, 1) - expectation.fulfill() - } - sk.appendOneShotNotificationHandler { (note: Notification) in - // Semantic analysis - no expected errors here because we fixed the settings. - XCTAssertEqual(note.params.diagnostics.count, 0) - expectation.fulfill() - } await buildSystem.delegate?.fileBuildSettingsChanged([doc]) - try await fulfillmentOfOrThrow([expectation]) + let syntacticDiags2 = try await testClient.nextDiagnosticsNotification() + // Semantic analysis - SourceKit currently caches diagnostics so we still see an error. + XCTAssertEqual(syntacticDiags2.diagnostics.count, 1) + + let semanticDiags2 = try await testClient.nextDiagnosticsNotification() + // Semantic analysis - no expected errors here because we fixed the settings. + XCTAssertEqual(semanticDiags2.diagnostics.count, 0) } func testClangdDocumentFallbackWithholdsDiagnostics() async throws { @@ -283,11 +261,9 @@ final class BuildSystemTests: XCTestCase { } """ - sk.allowUnexpectedNotification = false - - let documentManager = await self.testServer.server!._documentManager + let documentManager = await self.testClient.server._documentManager - sk.sendNoteSync( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: doc, @@ -295,29 +271,23 @@ final class BuildSystemTests: XCTestCase { version: 12, text: text ) - ), - { (note: Notification) in - // Expect diagnostics to be withheld. - XCTAssertEqual(note.params.diagnostics.count, 0) - XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) - } + ) ) + let openDiags = try await testClient.nextDiagnosticsNotification() + // Expect diagnostics to be withheld. + XCTAssertEqual(openDiags.diagnostics.count, 0) + XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) // 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 - let expectation = XCTestExpectation(description: "refresh due to fallback --> primary") - sk.handleNextNotification { (note: Notification) in - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) - expectation.fulfill() - } - await buildSystem.delegate?.fileBuildSettingsChanged([doc]) - try await fulfillmentOfOrThrow([expectation]) + let refreshedDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(refreshedDiags.diagnostics.count, 1) + XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) } func testSwiftDocumentFallbackWithholdsSemanticDiagnostics() async throws { @@ -337,11 +307,9 @@ final class BuildSystemTests: XCTestCase { func """ - sk.allowUnexpectedNotification = false - - let documentManager = await self.testServer.server!._documentManager + let documentManager = await self.testClient.server._documentManager - sk.sendNoteSync( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: doc, @@ -349,35 +317,28 @@ final class BuildSystemTests: XCTestCase { version: 12, text: text ) - ), - { (note: Notification) in - // Syntactic analysis - one expected errors here (for `func`). - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) - }, - { (note: Notification) in - // Should be the same syntactic analysis since we are using fallback arguments - XCTAssertEqual(note.params.diagnostics.count, 1) - } + ) ) + let openSyntacticDiags = try await testClient.nextDiagnosticsNotification() + // Syntactic analysis - one expected errors here (for `func`). + XCTAssertEqual(openSyntacticDiags.diagnostics.count, 1) + XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) + let openSemanticDiags = try await testClient.nextDiagnosticsNotification() + // Should be the same syntactic analysis since we are using fallback arguments + XCTAssertEqual(openSemanticDiags.diagnostics.count, 1) // Swap from fallback settings to primary build system settings. buildSystem.buildSettingsByFile[doc] = primarySettings - let expectation = XCTestExpectation(description: "refresh due to fallback --> primary") - expectation.expectedFulfillmentCount = 2 - sk.handleNextNotification { (note: Notification) in - // Syntactic analysis with new args - one expected errors here (for `func`). - XCTAssertEqual(note.params.diagnostics.count, 1) - expectation.fulfill() - } - sk.appendOneShotNotificationHandler { (note: Notification) in - // Semantic analysis - two errors since `-DFOO` was not passed. - XCTAssertEqual(note.params.diagnostics.count, 2) - expectation.fulfill() - } + await buildSystem.delegate?.fileBuildSettingsChanged([doc]) - try await fulfillmentOfOrThrow([expectation]) + let refreshedSyntacticDiags = try await testClient.nextDiagnosticsNotification() + // Syntactic analysis with new args - one expected errors here (for `func`). + XCTAssertEqual(refreshedSyntacticDiags.diagnostics.count, 1) + + let refreshedSemanticDiags = try await testClient.nextDiagnosticsNotification() + // Semantic analysis - two errors since `-DFOO` was not passed. + XCTAssertEqual(refreshedSemanticDiags.diagnostics.count, 2) } func testMainFilesChanged() async throws { @@ -386,40 +347,17 @@ final class BuildSystemTests: XCTestCase { let ws = try await mutableSourceKitTibsTestWorkspace(name: "MainFiles")! let unique_h = ws.testLoc("unique").docIdentifier.uri - ws.testServer.client.allowUnexpectedNotification = false - - let expectation = self.expectation(description: "initial") - ws.testServer.client.handleNextNotification { (note: Notification) in - // Should withhold diagnostics since we should be using fallback arguments. - XCTAssertEqual(note.params.diagnostics.count, 0) - expectation.fulfill() - } - try ws.openDocument(unique_h.fileURL!, language: .cpp) - try await fulfillmentOfOrThrow([expectation]) - let use_d = self.expectation(description: "update settings to d.cpp") - ws.testServer.client.handleNextNotification { (note: Notification) in - XCTAssertEqual(note.params.diagnostics.count, 1) - if let diag = note.params.diagnostics.first { - XCTAssertEqual(diag.severity, .warning) - XCTAssertEqual(diag.message, "UNIQUE_INCLUDED_FROM_D") - } - use_d.fulfill() - } + let openSyntacticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(openSyntacticDiags.diagnostics.count, 0) try ws.buildAndIndex() - try await fulfillmentOfOrThrow([use_d]) - - let use_c = self.expectation(description: "update settings to c.cpp") - ws.testServer.client.handleNextNotification { (note: Notification) in - XCTAssertEqual(note.params.diagnostics.count, 1) - if let diag = note.params.diagnostics.first { - XCTAssertEqual(diag.severity, .warning) - XCTAssertEqual(diag.message, "UNIQUE_INCLUDED_FROM_C") - } - use_c.fulfill() - } + let diagsFromD = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(diagsFromD.diagnostics.count, 1) + let diagFromD = try XCTUnwrap(diagsFromD.diagnostics.first) + XCTAssertEqual(diagFromD.severity, .warning) + XCTAssertEqual(diagFromD.message, "UNIQUE_INCLUDED_FROM_D") try ws.edit(rebuild: true) { (changes, _) in changes.write( @@ -436,7 +374,11 @@ final class BuildSystemTests: XCTestCase { ) } - try await fulfillmentOfOrThrow([use_c]) + let diagsFromC = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(diagsFromC.diagnostics.count, 1) + let diagFromC = try XCTUnwrap(diagsFromC.diagnostics.first) + XCTAssertEqual(diagFromC.severity, .warning) + XCTAssertEqual(diagFromC.message, "UNIQUE_INCLUDED_FROM_C") } private func clangBuildSettings(for uri: DocumentURI) -> FileBuildSettings { diff --git a/Tests/SourceKitLSPTests/CallHierarchyTests.swift b/Tests/SourceKitLSPTests/CallHierarchyTests.swift index 44fb28bf6..613c2a91e 100644 --- a/Tests/SourceKitLSPTests/CallHierarchyTests.swift +++ b/Tests/SourceKitLSPTests/CallHierarchyTests.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import ISDBTestSupport +import LSPTestSupport import LanguageServerProtocol import TSCBasic import XCTest @@ -24,35 +25,32 @@ final class CallHierarchyTests: XCTestCase { // Requests - func callHierarchy(at testLoc: TestLocation) throws -> [CallHierarchyItem] { + func callHierarchy(at testLoc: TestLocation) async throws -> [CallHierarchyItem] { let textDocument = testLoc.docIdentifier let request = CallHierarchyPrepareRequest(textDocument: textDocument, position: Position(testLoc)) - let items = try ws.sk.sendSync(request) - return items ?? [] + return try await ws.testClient.send(request) ?? [] } - func incomingCalls(at testLoc: TestLocation) throws -> [CallHierarchyIncomingCall] { - guard let item = try callHierarchy(at: testLoc).first else { + func incomingCalls(at testLoc: TestLocation) async throws -> [CallHierarchyIncomingCall] { + guard let item = try await callHierarchy(at: testLoc).first else { XCTFail("call hierarchy at \(testLoc) was empty") return [] } let request = CallHierarchyIncomingCallsRequest(item: item) - let calls = try ws.sk.sendSync(request) - return calls ?? [] + return try await ws.testClient.send(request) ?? [] } - func outgoingCalls(at testLoc: TestLocation) throws -> [CallHierarchyOutgoingCall] { - guard let item = try callHierarchy(at: testLoc).first else { + func outgoingCalls(at testLoc: TestLocation) async throws -> [CallHierarchyOutgoingCall] { + guard let item = try await callHierarchy(at: testLoc).first else { XCTFail("call hierarchy at \(testLoc) was empty") return [] } let request = CallHierarchyOutgoingCallsRequest(item: item) - let calls = try ws.sk.sendSync(request) - return calls ?? [] + return try await ws.testClient.send(request) ?? [] } - func usr(at testLoc: TestLocation) throws -> String { - guard let item = try callHierarchy(at: testLoc).first else { + func usr(at testLoc: TestLocation) async throws -> String { + guard let item = try await callHierarchy(at: testLoc).first else { XCTFail("call hierarchy at \(testLoc) was empty") return "" } @@ -112,24 +110,24 @@ final class CallHierarchyTests: XCTestCase { ) } - let aUsr = try usr(at: testLoc("a")) - let bUsr = try usr(at: testLoc("b")) - let cUsr = try usr(at: testLoc("c")) - let dUsr = try usr(at: testLoc("d")) + let aUsr = try await usr(at: testLoc("a")) + let bUsr = try await usr(at: testLoc("b")) + let cUsr = try await usr(at: testLoc("c")) + let dUsr = try await usr(at: testLoc("d")) // Test outgoing call hierarchy - XCTAssertEqual(try outgoingCalls(at: testLoc("a")), []) - XCTAssertEqual( - try outgoingCalls(at: testLoc("b")), + assertEqual(try await outgoingCalls(at: testLoc("a")), []) + assertEqual( + try await outgoingCalls(at: testLoc("b")), [ outCall(try item("a()", .function, usr: aUsr, at: "a"), at: "b->a"), outCall(try item("c()", .function, usr: cUsr, at: "c"), at: "b->c"), outCall(try item("b(x:)", .function, usr: bUsr, at: "b"), at: "b->b"), ] ) - XCTAssertEqual( - try outgoingCalls(at: testLoc("c")), + assertEqual( + try await outgoingCalls(at: testLoc("c")), [ outCall(try item("a()", .function, usr: aUsr, at: "a"), at: "c->a"), outCall(try item("d()", .function, usr: dUsr, at: "d"), at: "c->d"), @@ -139,21 +137,21 @@ final class CallHierarchyTests: XCTestCase { // Test incoming call hierarchy - XCTAssertEqual( - try incomingCalls(at: testLoc("a")), + assertEqual( + try await incomingCalls(at: testLoc("a")), [ inCall(try item("b(x:)", .function, usr: bUsr, at: "b"), at: "b->a"), inCall(try item("c()", .function, usr: cUsr, at: "c"), at: "c->a"), ] ) - XCTAssertEqual( - try incomingCalls(at: testLoc("b")), + assertEqual( + try await incomingCalls(at: testLoc("b")), [ inCall(try item("b(x:)", .function, usr: bUsr, at: "b"), at: "b->b") ] ) - XCTAssertEqual( - try incomingCalls(at: testLoc("d")), + assertEqual( + try await incomingCalls(at: testLoc("d")), [ inCall(try item("c()", .function, usr: cUsr, at: "c"), at: "c->d") ] diff --git a/Tests/SourceKitLSPTests/CodeActionTests.swift b/Tests/SourceKitLSPTests/CodeActionTests.swift index 7bc1dc253..55f12dd5e 100644 --- a/Tests/SourceKitLSPTests/CodeActionTests.swift +++ b/Tests/SourceKitLSPTests/CodeActionTests.swift @@ -203,10 +203,8 @@ final class CodeActionTests: XCTestCase { let textDocument = TextDocumentIdentifier(loc.url) let start = Position(line: 2, utf16index: 0) let request = CodeActionRequest(range: start..) in - // syntactic diagnostics - XCTAssertEqual(note.params.uri, def.docUri) - XCTAssertEqual(note.params.diagnostics, []) - syntacticDiagnosticsReceived.fulfill() - } - - var diags: [Diagnostic]! = nil - ws.sk.appendOneShotNotificationHandler { (note: Notification) in - // semantic diagnostics - XCTAssertEqual(note.params.uri, def.docUri) - XCTAssertEqual(note.params.diagnostics.count, 1) - diags = note.params.diagnostics - semanticDiagnosticsReceived.fulfill() - } + let syntacticDiags = try await ws.testClient.nextDiagnosticsNotification() + XCTAssertEqual(syntacticDiags.uri, def.docUri) + XCTAssertEqual(syntacticDiags.diagnostics, []) - try await fulfillmentOfOrThrow([syntacticDiagnosticsReceived, semanticDiagnosticsReceived]) + let semanticDiags = try await ws.testClient.nextDiagnosticsNotification() + XCTAssertEqual(semanticDiags.uri, def.docUri) + XCTAssertEqual(semanticDiags.diagnostics.count, 1) let textDocument = TextDocumentIdentifier(def.url) let actionsRequest = CodeActionRequest( range: def.position..) in + ws.testClient.handleNextRequest { (request: ApplyEditRequest) -> ApplyEditResponse in defer { editReceived.fulfill() } - guard let change = request.params.edit.changes?[def.docUri]?.spm_only else { - return XCTFail("Expected exactly one edit") + guard let change = request.edit.changes?[def.docUri]?.spm_only else { + XCTFail("Expected exactly one edit") + return ApplyEditResponse(applied: false, failureReason: "Expected exactly one edit") } XCTAssertEqual( change.newText.trimmingTrailingWhitespace(), @@ -377,9 +361,9 @@ final class CodeActionTests: XCTestCase { """ ) - request.reply(ApplyEditResponse(applied: true, failureReason: nil)) + return ApplyEditResponse(applied: true, failureReason: nil) } - _ = try ws.sk.sendSync(ExecuteCommandRequest(command: command.command, arguments: command.arguments)) + _ = try await ws.testClient.send(ExecuteCommandRequest(command: command.command, arguments: command.arguments)) try await fulfillmentOfOrThrow([editReceived]) } diff --git a/Tests/SourceKitLSPTests/CompilationDatabaseTests.swift b/Tests/SourceKitLSPTests/CompilationDatabaseTests.swift index c7ad28caa..edc2d6820 100644 --- a/Tests/SourceKitLSPTests/CompilationDatabaseTests.swift +++ b/Tests/SourceKitLSPTests/CompilationDatabaseTests.swift @@ -29,7 +29,7 @@ final class CompilationDatabaseTests: XCTestCase { textDocument: loc.docIdentifier, position: Position(line: 9, utf16index: 3) ) - let preChangeHighlightResponse = try ws.sk.sendSync(highlightRequest) + let preChangeHighlightResponse = try await ws.testClient.send(highlightRequest) XCTAssertEqual( preChangeHighlightResponse, [ @@ -56,7 +56,7 @@ final class CompilationDatabaseTests: XCTestCase { builder.write(newCompilationDatabaseStr, to: compilationDatabaseUrl) }) - ws.sk.send( + ws.testClient.send( DidChangeWatchedFilesNotification(changes: [ FileEvent(uri: DocumentURI(compilationDatabaseUrl), type: .changed) ]) @@ -74,7 +74,7 @@ final class CompilationDatabaseTests: XCTestCase { // Updating the build settings takes a few seconds. // Send highlight requests every second until we receive correct results. for _ in 0..<30 { - let postChangeHighlightResponse = try ws.sk.sendSync(highlightRequest) + let postChangeHighlightResponse = try await ws.testClient.send(highlightRequest) if postChangeHighlightResponse == expectedPostEditHighlight { didReceiveCorrectHighlight = true diff --git a/Tests/SourceKitLSPTests/DocumentColorTests.swift b/Tests/SourceKitLSPTests/DocumentColorTests.swift index 25d1c2258..dd739f970 100644 --- a/Tests/SourceKitLSPTests/DocumentColorTests.swift +++ b/Tests/SourceKitLSPTests/DocumentColorTests.swift @@ -17,22 +17,15 @@ import SourceKitLSP import XCTest final class DocumentColorTests: XCTestCase { - /// Connection and lifetime management for the service. - var connection: TestSourceKitServer! = nil + /// The mock client used to communicate with the SourceKit-LSP server. + /// + /// - Note: Set before each test run in `setUp`. + private var testClient: TestSourceKitLSPClient! = nil - /// The primary interface to make requests to the SourceKitServer. - var sk: TestClient! = nil - - override func tearDown() { - sk = nil - connection = nil - } - - func initialize() throws { - connection = TestSourceKitServer() - sk = connection.client + override func setUp() async throws { + testClient = TestSourceKitLSPClient() let documentCapabilities = TextDocumentClientCapabilities() - _ = try sk.sendSync( + _ = try await testClient.send( InitializeRequest( processId: nil, rootPath: nil, @@ -45,10 +38,16 @@ final class DocumentColorTests: XCTestCase { ) } - func performDocumentColorRequest(text: String) throws -> [ColorInformation] { + override func tearDown() { + testClient = nil + } + + // MARK: - Helpers + + private func performDocumentColorRequest(text: String) async throws -> [ColorInformation] { let url = URL(fileURLWithPath: "/\(UUID())/a.swift") - sk.send( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: DocumentURI(url), @@ -60,14 +59,17 @@ final class DocumentColorTests: XCTestCase { ) let request = DocumentColorRequest(textDocument: TextDocumentIdentifier(url)) - return try sk.sendSync(request) + return try await testClient.send(request) } - func performColorPresentationRequest(text: String, color: Color, range: Range) throws -> [ColorPresentation] - { + private func performColorPresentationRequest( + text: String, + color: Color, + range: Range + ) async throws -> [ColorPresentation] { let url = URL(fileURLWithPath: "/\(UUID())/a.swift") - sk.send( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: DocumentURI(url), @@ -83,21 +85,21 @@ final class DocumentColorTests: XCTestCase { color: color, range: range ) - return try sk.sendSync(request) + return try await testClient.send(request) } - func testEmptyText() throws { - try initialize() - let colors = try performDocumentColorRequest(text: "") + // MARK: - Tests + + func testEmptyText() async throws { + let colors = try await performDocumentColorRequest(text: "") XCTAssertEqual(colors, []) } - func testSimple() throws { - try initialize() + func testSimple() async throws { let text = #""" #colorLiteral(red: 0.1, green: 0.2, blue: 0.3, alpha: 0.4) """# - let colors = try performDocumentColorRequest(text: text) + let colors = try await performDocumentColorRequest(text: text) XCTAssertEqual( colors, @@ -110,8 +112,7 @@ final class DocumentColorTests: XCTestCase { ) } - func testWeirdWhitespace() throws { - try initialize() + func testWeirdWhitespace() async throws { let text = #""" let x = #colorLiteral(red:0.5,green:0.5,blue:0.5,alpha:0.5) let y = #colorLiteral( @@ -128,7 +129,7 @@ final class DocumentColorTests: XCTestCase { : \#t0.5, alpha:0.5 ) """# - let colors = try performDocumentColorRequest(text: text) + let colors = try await performDocumentColorRequest(text: text) XCTAssertEqual( colors, @@ -145,8 +146,7 @@ final class DocumentColorTests: XCTestCase { ) } - func testPresentation() throws { - try initialize() + func testPresentation() async throws { let text = """ let x = #colorLiteral(red: 0.5, green: 0.5, blue: 0.5, alpha: 0.5); """ @@ -155,7 +155,7 @@ final class DocumentColorTests: XCTestCase { let newText = """ #colorLiteral(red: \(color.red), green: \(color.green), blue: \(color.blue), alpha: \(color.alpha)) """ - let presentations = try performColorPresentationRequest(text: text, color: color, range: range) + let presentations = try await performColorPresentationRequest(text: text, color: color, range: range) XCTAssertEqual(presentations.count, 1) let presentation = presentations[0] XCTAssertEqual(presentation.label, "Color Literal") diff --git a/Tests/SourceKitLSPTests/DocumentSymbolTests.swift b/Tests/SourceKitLSPTests/DocumentSymbolTests.swift index e0572362a..e46371c58 100644 --- a/Tests/SourceKitLSPTests/DocumentSymbolTests.swift +++ b/Tests/SourceKitLSPTests/DocumentSymbolTests.swift @@ -19,23 +19,16 @@ import XCTest final class DocumentSymbolTests: XCTestCase { typealias DocumentSymbolCapabilities = TextDocumentClientCapabilities.DocumentSymbol - /// Connection and lifetime management for the service. - var connection: TestSourceKitServer! = nil + /// The mock client used to communicate with the SourceKit-LSP server. + /// + /// - Note: Set before each test run in `setUp`. + private var testClient: TestSourceKitLSPClient! = nil - /// The primary interface to make requests to the SourceKitServer. - var sk: TestClient! = nil - - override func tearDown() { - sk = nil - connection = nil - } - - func initialize(capabilities: DocumentSymbolCapabilities) throws { - connection = TestSourceKitServer() - sk = connection.client + override func setUp() async throws { + testClient = TestSourceKitLSPClient() var documentCapabilities = TextDocumentClientCapabilities() - documentCapabilities.documentSymbol = capabilities - _ = try sk.sendSync( + documentCapabilities.documentSymbol = DocumentSymbolCapabilities() + _ = try await testClient.send( InitializeRequest( processId: nil, rootPath: nil, @@ -48,10 +41,16 @@ final class DocumentSymbolTests: XCTestCase { ) } - func performDocumentSymbolRequest(text: String) throws -> DocumentSymbolResponse { + override func tearDown() { + testClient = nil + } + + // MARK: - Helpers + + private func performDocumentSymbolRequest(text: String) async throws -> DocumentSymbolResponse { let url = URL(fileURLWithPath: "/\(UUID())/a.swift") - sk.send( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: DocumentURI(url), @@ -63,32 +62,28 @@ final class DocumentSymbolTests: XCTestCase { ) let request = DocumentSymbolRequest(textDocument: TextDocumentIdentifier(url)) - return try sk.sendSync(request)! + return try await testClient.send(request)! } - func range(from startTuple: (Int, Int), to endTuple: (Int, Int)) -> Range { + private func range(from startTuple: (Int, Int), to endTuple: (Int, Int)) -> Range { let startPos = Position(line: startTuple.0, utf16index: startTuple.1) let endPos = Position(line: endTuple.0, utf16index: endTuple.1) return startPos..) in - req.reply(ApplyEditResponse(applied: true, failureReason: nil)) + ws.testClient.handleNextRequest { (req: ApplyEditRequest) -> ApplyEditResponse in + return ApplyEditResponse(applied: true, failureReason: nil) } - let result = try withExtendedLifetime(ws) { try ws.sk.sendSync(request) } + let result = try await ws.testClient.send(request) guard case .dictionary(let resultDict) = result else { XCTFail("Result is not a dictionary.") @@ -118,11 +117,11 @@ final class ExecuteCommandTests: XCTestCase { let request = ExecuteCommandRequest(command: command.command, arguments: command.arguments) - ws.testServer.client.handleNextRequest { (req: Request) in - req.reply(ApplyEditResponse(applied: true, failureReason: nil)) + ws.testClient.handleNextRequest { (req: ApplyEditRequest) -> ApplyEditResponse in + return ApplyEditResponse(applied: true, failureReason: nil) } - let result = try withExtendedLifetime(ws) { try ws.sk.sendSync(request) } + let result = try await ws.testClient.send(request) guard case .dictionary(let resultDict) = result else { XCTFail("Result is not a dictionary.") diff --git a/Tests/SourceKitLSPTests/FoldingRangeTests.swift b/Tests/SourceKitLSPTests/FoldingRangeTests.swift index 4bd6ef44e..0b7436802 100644 --- a/Tests/SourceKitLSPTests/FoldingRangeTests.swift +++ b/Tests/SourceKitLSPTests/FoldingRangeTests.swift @@ -44,7 +44,7 @@ final class FoldingRangeTests: XCTestCase { } let request = FoldingRangeRequest(textDocument: TextDocumentIdentifier(uri)) - let ranges = try withExtendedLifetime(ws) { try ws.sk.sendSync(request) } + let ranges = try await ws.testClient.send(request) let expected = [ FoldingRange(startLine: 0, startUTF16Index: 0, endLine: 1, endUTF16Index: 18, kind: .comment), @@ -73,7 +73,7 @@ final class FoldingRangeTests: XCTestCase { } let request = FoldingRangeRequest(textDocument: TextDocumentIdentifier(uri)) - let ranges = try withExtendedLifetime(ws) { try ws.sk.sendSync(request) } + let ranges = try await ws.testClient.send(request) let expected = [ FoldingRange(startLine: 0, endLine: 1, kind: .comment), @@ -99,7 +99,7 @@ final class FoldingRangeTests: XCTestCase { return } let request = FoldingRangeRequest(textDocument: TextDocumentIdentifier(url)) - let ranges = try withExtendedLifetime(ws) { try ws.sk.sendSync(request) } + let ranges = try await ws.testClient.send(request) XCTAssertEqual(ranges?.count, expectedRanges, "Failed rangeLimit test at line \(line)") } @@ -118,7 +118,7 @@ final class FoldingRangeTests: XCTestCase { } let request = FoldingRangeRequest(textDocument: TextDocumentIdentifier(url)) - let ranges = try withExtendedLifetime(ws) { try ws.sk.sendSync(request) } + let ranges = try await ws.testClient.send(request) XCTAssertEqual(ranges?.count, 0) } @@ -136,7 +136,7 @@ final class FoldingRangeTests: XCTestCase { else { return } let request = FoldingRangeRequest(textDocument: TextDocumentIdentifier(url)) - let ranges = try withExtendedLifetime(ws) { try ws.sk.sendSync(request) } + let ranges = try await ws.testClient.send(request) let expected = [ FoldingRange(startLine: 0, startUTF16Index: 0, endLine: 2, endUTF16Index: 65, kind: .comment), @@ -159,7 +159,7 @@ final class FoldingRangeTests: XCTestCase { else { return } let request = FoldingRangeRequest(textDocument: TextDocumentIdentifier(url)) - let ranges = try withExtendedLifetime(ws) { try ws.sk.sendSync(request) } + let ranges = try await ws.testClient.send(request) let expected = [ FoldingRange(startLine: 0, startUTF16Index: 12, endLine: 2, endUTF16Index: 0, kind: nil), diff --git a/Tests/SourceKitLSPTests/ImplementationTests.swift b/Tests/SourceKitLSPTests/ImplementationTests.swift index 13933d121..583734896 100644 --- a/Tests/SourceKitLSPTests/ImplementationTests.swift +++ b/Tests/SourceKitLSPTests/ImplementationTests.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import ISDBTestSupport +import LSPTestSupport import LanguageServerProtocol import TSCBasic import XCTest @@ -23,10 +24,10 @@ final class ImplementationTests: XCTestCase { try ws.openDocument(ws.testLoc("a.swift").url, language: .swift) try ws.openDocument(ws.testLoc("b.swift").url, language: .swift) - func impls(at testLoc: TestLocation) throws -> Set { + func impls(at testLoc: TestLocation) async throws -> Set { let textDocument = testLoc.docIdentifier let request = ImplementationRequest(textDocument: textDocument, position: Position(testLoc)) - let response = try ws.sk.sendSync(request) + let response = try await ws.testClient.send(request) guard case .locations(let implementations) = response else { XCTFail("Response was not locations") return [] @@ -48,31 +49,31 @@ final class ImplementationTests: XCTestCase { ) } - try XCTAssertEqual(impls(at: testLoc("Protocol")), [loc("StructConformance")]) - try XCTAssertEqual(impls(at: testLoc("ProtocolStaticVar")), [loc("StructStaticVar")]) - try XCTAssertEqual(impls(at: testLoc("ProtocolStaticFunction")), [loc("StructStaticFunction")]) - try XCTAssertEqual(impls(at: testLoc("ProtocolVariable")), [loc("StructVariable")]) - try XCTAssertEqual(impls(at: testLoc("ProtocolFunction")), [loc("StructFunction")]) - try XCTAssertEqual(impls(at: testLoc("Class")), [loc("SubclassConformance")]) - try XCTAssertEqual(impls(at: testLoc("ClassClassVar")), [loc("SubclassClassVar")]) - try XCTAssertEqual(impls(at: testLoc("ClassClassFunction")), [loc("SubclassClassFunction")]) - try XCTAssertEqual(impls(at: testLoc("ClassVariable")), [loc("SubclassVariable")]) - try XCTAssertEqual(impls(at: testLoc("ClassFunction")), [loc("SubclassFunction")]) + try assertEqual(await impls(at: testLoc("Protocol")), [loc("StructConformance")]) + try assertEqual(await impls(at: testLoc("ProtocolStaticVar")), [loc("StructStaticVar")]) + try assertEqual(await impls(at: testLoc("ProtocolStaticFunction")), [loc("StructStaticFunction")]) + try assertEqual(await impls(at: testLoc("ProtocolVariable")), [loc("StructVariable")]) + try assertEqual(await impls(at: testLoc("ProtocolFunction")), [loc("StructFunction")]) + try assertEqual(await impls(at: testLoc("Class")), [loc("SubclassConformance")]) + try assertEqual(await impls(at: testLoc("ClassClassVar")), [loc("SubclassClassVar")]) + try assertEqual(await impls(at: testLoc("ClassClassFunction")), [loc("SubclassClassFunction")]) + try assertEqual(await impls(at: testLoc("ClassVariable")), [loc("SubclassVariable")]) + try assertEqual(await impls(at: testLoc("ClassFunction")), [loc("SubclassFunction")]) - try XCTAssertEqual( - impls(at: testLoc("Sepulcidae")), + try assertEqual( + await impls(at: testLoc("Sepulcidae")), [loc("ParapamphiliinaeConformance"), loc("XyelulinaeConformance"), loc("TrematothoracinaeConformance")] ) - try XCTAssertEqual( - impls(at: testLoc("Parapamphiliinae")), + try assertEqual( + await impls(at: testLoc("Parapamphiliinae")), [loc("MicramphiliusConformance"), loc("PamparaphiliusConformance")] ) - try XCTAssertEqual(impls(at: testLoc("Xyelulinae")), [loc("XyelulaConformance")]) - try XCTAssertEqual(impls(at: testLoc("Trematothoracinae")), []) + try assertEqual(await impls(at: testLoc("Xyelulinae")), [loc("XyelulaConformance")]) + try assertEqual(await impls(at: testLoc("Trematothoracinae")), []) - try XCTAssertEqual(impls(at: testLoc("Prozaiczne")), [loc("MurkwiaConformance2"), loc("SepulkaConformance1")]) - try XCTAssertEqual( - impls(at: testLoc("Sepulkowate")), + try assertEqual(await impls(at: testLoc("Prozaiczne")), [loc("MurkwiaConformance2"), loc("SepulkaConformance1")]) + try assertEqual( + await impls(at: testLoc("Sepulkowate")), [ loc("MurkwiaConformance1"), loc("SepulkaConformance2"), loc("PćmaŁagodnaConformance"), loc("PćmaZwyczajnaConformance"), @@ -80,13 +81,13 @@ final class ImplementationTests: XCTestCase { ) // FIXME: sourcekit returns wrong locations for the function (subclasses that don't override it, and extensions that don't implement it) // try XCTAssertEqual(impls(at: testLoc("rozpocznijSepulenie")), [loc("MurkwiaFunc"), loc("SepulkaFunc"), loc("PćmaŁagodnaFunc"), loc("PćmaZwyczajnaFunc")]) - try XCTAssertEqual(impls(at: testLoc("Murkwia")), []) - try XCTAssertEqual(impls(at: testLoc("MurkwiaFunc")), []) - try XCTAssertEqual( - impls(at: testLoc("Sepulka")), + try assertEqual(await impls(at: testLoc("Murkwia")), []) + try assertEqual(await impls(at: testLoc("MurkwiaFunc")), []) + try assertEqual( + await impls(at: testLoc("Sepulka")), [loc("SepulkaDwuusznaConformance"), loc("SepulkaPrzechylnaConformance")] ) - try XCTAssertEqual(impls(at: testLoc("SepulkaVar")), [loc("SepulkaDwuusznaVar"), loc("SepulkaPrzechylnaVar")]) - try XCTAssertEqual(impls(at: testLoc("SepulkaFunc")), []) + try assertEqual(await impls(at: testLoc("SepulkaVar")), [loc("SepulkaDwuusznaVar"), loc("SepulkaPrzechylnaVar")]) + try assertEqual(await impls(at: testLoc("SepulkaFunc")), []) } } diff --git a/Tests/SourceKitLSPTests/InlayHintTests.swift b/Tests/SourceKitLSPTests/InlayHintTests.swift index fbb001833..e80e630d6 100644 --- a/Tests/SourceKitLSPTests/InlayHintTests.swift +++ b/Tests/SourceKitLSPTests/InlayHintTests.swift @@ -17,21 +17,14 @@ import SourceKitLSP import XCTest final class InlayHintTests: XCTestCase { - /// Connection and lifetime management for the service. - var connection: TestSourceKitServer! = nil - - /// The primary interface to make requests to the SourceKitServer. - var sk: TestClient! = nil - - override func tearDown() { - sk = nil - connection = nil - } - - override func setUp() { - connection = TestSourceKitServer() - sk = connection.client - _ = try! sk.sendSync( + /// The mock client used to communicate with the SourceKit-LSP server. + /// + /// - Note: Set before each test run in `setUp`. + private var testClient: TestSourceKitLSPClient! = nil + + override func setUp() async throws { + testClient = TestSourceKitLSPClient() + _ = try await testClient.send( InitializeRequest( processId: nil, rootPath: nil, @@ -44,10 +37,16 @@ final class InlayHintTests: XCTestCase { ) } - func performInlayHintRequest(text: String, range: Range? = nil) throws -> [InlayHint] { + override func tearDown() { + testClient = nil + } + + // MARK: - Helpers + + func performInlayHintRequest(text: String, range: Range? = nil) async throws -> [InlayHint] { let url = URL(fileURLWithPath: "/\(UUID())/a.swift") - sk.send( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: DocumentURI(url), @@ -61,14 +60,18 @@ final class InlayHintTests: XCTestCase { let request = InlayHintRequest(textDocument: TextDocumentIdentifier(url), range: range) do { - return try sk.sendSync(request) + return try await testClient.send(request) } catch let error as ResponseError where error.message.contains("unknown request: source.request.variable.type") { throw XCTSkip("toolchain does not support variable.type request") } } - private func makeInlayHint(position: Position, kind: InlayHintKind, label: String, hasEdit: Bool = true) -> InlayHint - { + private func makeInlayHint( + position: Position, + kind: InlayHintKind, + label: String, + hasEdit: Bool = true + ) -> InlayHint { let textEdits: [TextEdit]? if hasEdit { textEdits = [TextEdit(range: position.. Double { let result = x * x @@ -126,7 +131,7 @@ final class InlayHintTests: XCTestCase { } """ let range = Position(line: 6, utf16index: 0)..) in - let diagnostics = note.params.diagnostics - // It seems we either get no diagnostics or a `-Wswitch` warning. Either is fine - // as long as our code action works properly. - XCTAssert( - diagnostics.isEmpty || (diagnostics.count == 1 && diagnostics.first?.code == .string("-Wswitch")), - "Unexpected diagnostics \(diagnostics)" - ) - expectation.fulfill() - } - try ws.openDocument(loc.url, language: .cpp) - try await fulfillmentOfOrThrow([expectation]) + let diagsNotification = try await ws.testClient.nextDiagnosticsNotification() + let diagnostics = diagsNotification.diagnostics + // It seems we either get no diagnostics or a `-Wswitch` warning. Either is fine + // as long as our code action works properly. + XCTAssert( + diagnostics.isEmpty || (diagnostics.count == 1 && diagnostics.first?.code == .string("-Wswitch")), + "Unexpected diagnostics \(diagnostics)" + ) let codeAction = CodeActionRequest( range: Position(loc)..) in - XCTAssertNotNil(request.params.edit.changes) - request.reply(ApplyEditResponse(applied: true, failureReason: nil)) + ws.testClient.handleNextRequest { (request: ApplyEditRequest) -> ApplyEditResponse in + XCTAssertNotNil(request.edit.changes) applyEdit.fulfill() + return ApplyEditResponse(applied: true, failureReason: nil) } let executeCommand = ExecuteCommandRequest( command: command.command, arguments: command.arguments ) - _ = try ws.sk.sendSync(executeCommand) + _ = try await ws.testClient.send(executeCommand) try await fulfillmentOfOrThrow([applyEdit]) } @@ -289,23 +284,17 @@ final class LocalClangTests: XCTestCase { let loc = ws.testLoc("unused_b") - let expectation = XCTestExpectation(description: "diagnostics") - - ws.sk.handleNextNotification { (note: Notification) in - // Don't use exact equality because of differences in recent clang. - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - note.params.diagnostics.first?.range, - Position(loc)..) in - XCTAssertEqual(note.params.diagnostics.count, 0) - expectation.fulfill() - } - try ws.openDocument(loc.url, language: .objective_c) - try await fulfillmentOfOrThrow([expectation]) - withExtendedLifetime(ws) {} + let diags = try await ws.testClient.nextDiagnosticsNotification() + XCTAssertEqual(diags.diagnostics.count, 0) } func testSemanticHighlighting() async throws { @@ -334,18 +316,14 @@ final class LocalClangTests: XCTestCase { } let mainLoc = ws.testLoc("Object:include:main") - let diagnostics = self.expectation(description: "diagnostics") - ws.sk.handleNextNotification { (note: Notification) in - diagnostics.fulfill() - XCTAssertEqual(note.params.diagnostics.count, 0) - } - try ws.openDocument(mainLoc.url, language: .c) - try await fulfillmentOfOrThrow([diagnostics]) + + let diags = try await ws.testClient.nextDiagnosticsNotification() + XCTAssertEqual(diags.diagnostics.count, 0) let request = DocumentSemanticTokensRequest(textDocument: mainLoc.docIdentifier) do { - let reply = try ws.sk.sendSync(request) + let reply = try await ws.testClient.send(request) XCTAssertNotNil(reply) } catch let e { if let error = e as? ResponseError { @@ -363,16 +341,11 @@ final class LocalClangTests: XCTestCase { let cFileLoc = ws.testLoc("Object:ref:main") - // Initially the workspace should build fine. - let documentOpened = self.expectation(description: "documentOpened") - ws.sk.handleNextNotification({ (note: LanguageServerProtocol.Notification) in - XCTAssert(note.params.diagnostics.isEmpty) - documentOpened.fulfill() - }) - try ws.openDocument(cFileLoc.url, language: .cpp) - try await fulfillmentOfOrThrow([documentOpened], timeout: 5) + // Initially the workspace should build fine. + let initialDiags = try await ws.testClient.nextDiagnosticsNotification() + XCTAssert(initialDiags.diagnostics.isEmpty) // We rename Object to MyObject in the header. _ = try ws.sources.edit { builder in @@ -383,21 +356,16 @@ final class LocalClangTests: XCTestCase { builder.write(headerFile, to: headerFilePath) } - // Now we should get a diagnostic in main.c file because `Object` is no longer defined. - let updatedNotificationsReceived = self.expectation(description: "updatedNotificationsReceived") - ws.sk.handleNextNotification({ (note: LanguageServerProtocol.Notification) in - XCTAssertFalse(note.params.diagnostics.isEmpty) - updatedNotificationsReceived.fulfill() - }) - - let clangdServer = await ws.testServer.server!._languageService( + let clangdServer = await ws.testClient.server._languageService( for: cFileLoc.docUri, .cpp, - in: ws.testServer.server!.workspaceForDocument(uri: cFileLoc.docUri)! + in: ws.testClient.server.workspaceForDocument(uri: cFileLoc.docUri)! )! await clangdServer.documentDependenciesUpdated(cFileLoc.docUri) - try await fulfillmentOfOrThrow([updatedNotificationsReceived], timeout: 5) + // Now we should get a diagnostic in main.c file because `Object` is no longer defined. + let editedDiags = try await ws.testClient.nextDiagnosticsNotification() + XCTAssertFalse(editedDiags.diagnostics.isEmpty) } } diff --git a/Tests/SourceKitLSPTests/LocalSwiftTests.swift b/Tests/SourceKitLSPTests/LocalSwiftTests.swift index 45004f8e5..79f62e104 100644 --- a/Tests/SourceKitLSPTests/LocalSwiftTests.swift +++ b/Tests/SourceKitLSPTests/LocalSwiftTests.swift @@ -24,16 +24,14 @@ typealias Notification = LanguageServerProtocol.Notification final class LocalSwiftTests: XCTestCase { - /// Connection and lifetime management for the service. - var connection: TestSourceKitServer! = nil - - /// The primary interface to make requests to the SourceKitServer. - var sk: TestClient! = nil - - override func setUp() { - connection = TestSourceKitServer() - sk = connection.client - _ = try! sk.sendSync( + /// The mock client used to communicate with the SourceKit-LSP server. + /// + /// - Note: Set before each test run in `setUp`. + private var testClient: TestSourceKitLSPClient! = nil + + override func setUp() async throws { + testClient = TestSourceKitLSPClient() + _ = try await self.testClient.send( InitializeRequest( processId: nil, rootPath: nil, @@ -57,19 +55,18 @@ final class LocalSwiftTests: XCTestCase { } override func tearDown() { - sk = nil - connection = nil + testClient = nil } - func testEditing() async { + // MARK: - Tests + + func testEditing() async throws { let url = URL(fileURLWithPath: "/\(UUID())/a.swift") let uri = DocumentURI(url) - sk.allowUnexpectedNotification = false + let documentManager = await testClient.server._documentManager - let documentManager = await connection.server!._documentManager - - sk.sendNoteSync( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: uri, @@ -79,139 +76,121 @@ final class LocalSwiftTests: XCTestCase { func """ ) - ), - { (note: Notification) in - log("Received diagnostics for open - syntactic") - XCTAssertEqual(note.params.version, 12) - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual("func", documentManager.latestSnapshot(uri)!.text) - }, - { (note: Notification) in - log("Received diagnostics for open - semantic") - XCTAssertEqual(note.params.version, 12) - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - note.params.diagnostics.first?.range.lowerBound, - Position(line: 0, utf16index: 4) - ) - } + ) + ) + let syntacticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(syntacticDiags.version, 12) + XCTAssertEqual(syntacticDiags.diagnostics.count, 1) + XCTAssertEqual("func", documentManager.latestSnapshot(uri)!.text) + let semanticOpenDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(semanticOpenDiags.version, 12) + XCTAssertEqual(semanticOpenDiags.diagnostics.count, 1) + XCTAssertEqual( + semanticOpenDiags.diagnostics.first?.range.lowerBound, + Position(line: 0, utf16index: 4) ) - sk.sendNoteSync( + testClient.send( DidChangeTextDocumentNotification( textDocument: .init(uri, version: 13), contentChanges: [ .init(range: Range(Position(line: 0, utf16index: 4)), text: " foo() {}\n") ] - ), - { (note: Notification) in - log("Received diagnostics for edit 1 - syntactic") - // 1 = remaining semantic error - // 0 = semantic update finished already - XCTAssertEqual(note.params.version, 13) - XCTAssertLessThanOrEqual(note.params.diagnostics.count, 1) - XCTAssertEqual("func foo() {}\n", documentManager.latestSnapshot(uri)!.text) - }, - { (note: Notification) in - log("Received diagnostics for edit 1 - semantic") - XCTAssertEqual(note.params.version, 13) - XCTAssertEqual(note.params.diagnostics.count, 0) - } + ) ) + let edit1SyntacticDiags = try await testClient.nextDiagnosticsNotification() + // 1 = remaining semantic error + XCTAssertEqual(edit1SyntacticDiags.version, 13) + XCTAssertLessThanOrEqual(edit1SyntacticDiags.diagnostics.count, 1) + XCTAssertEqual("func foo() {}\n", documentManager.latestSnapshot(uri)!.text) - sk.sendNoteSync( + let edit1SemanticDiags = try await testClient.nextDiagnosticsNotification() + // 0 = semantic update finished already + XCTAssertEqual(edit1SemanticDiags.version, 13) + XCTAssertEqual(edit1SemanticDiags.diagnostics.count, 0) + + testClient.send( DidChangeTextDocumentNotification( textDocument: .init(uri, version: 14), contentChanges: [ .init(range: Range(Position(line: 1, utf16index: 0)), text: "bar()") ] - ), - { (note: Notification) in - log("Received diagnostics for edit 2 - syntactic") - XCTAssertEqual(note.params.version, 14) - // 1 = semantic update finished already - // 0 = only syntactic - XCTAssertLessThanOrEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - """ - func foo() {} - bar() - """, - documentManager.latestSnapshot(uri)!.text - ) - }, - { (note: Notification) in - log("Received diagnostics for edit 2 - semantic") - XCTAssertEqual(note.params.version, 14) - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - note.params.diagnostics.first?.range.lowerBound, - Position(line: 1, utf16index: 0) - ) - } + ) + ) + let edit2SyntacticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(edit2SyntacticDiags.version, 14) + // 0 = only syntactic + XCTAssertLessThanOrEqual(edit2SyntacticDiags.diagnostics.count, 1) + XCTAssertEqual( + """ + func foo() {} + bar() + """, + documentManager.latestSnapshot(uri)!.text + ) + let edit2SemanticDiags = try await testClient.nextDiagnosticsNotification() + // 1 = semantic update finished already + XCTAssertEqual(edit2SemanticDiags.version, 14) + XCTAssertEqual(edit2SemanticDiags.diagnostics.count, 1) + XCTAssertEqual( + edit2SemanticDiags.diagnostics.first?.range.lowerBound, + Position(line: 1, utf16index: 0) ) - sk.sendNoteSync( + testClient.send( DidChangeTextDocumentNotification( textDocument: .init(uri, version: 14), contentChanges: [ .init(range: Position(line: 1, utf16index: 0)..) in - log("Received diagnostics for edit 3 - syntactic") - // 1 = remaining semantic error - // 0 = semantic update finished already - XCTAssertEqual(note.params.version, 14) - XCTAssertLessThanOrEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - """ - func foo() {} - foo() - """, - documentManager.latestSnapshot(uri)!.text - ) - }, - { (note: Notification) in - log("Received diagnostics for edit 3 - semantic") - XCTAssertEqual(note.params.version, 14) - XCTAssertEqual(note.params.diagnostics.count, 0) - } + ) + ) + let edit3SyntacticDiags = try await testClient.nextDiagnosticsNotification() + // 1 = remaining semantic error + XCTAssertEqual(edit3SyntacticDiags.version, 14) + XCTAssertLessThanOrEqual(edit3SyntacticDiags.diagnostics.count, 1) + XCTAssertEqual( + """ + func foo() {} + foo() + """, + documentManager.latestSnapshot(uri)!.text ) - sk.sendNoteSync( + let edit3SemanticDiags = try await testClient.nextDiagnosticsNotification() + // 0 = semantic update finished already + XCTAssertEqual(edit3SemanticDiags.version, 14) + XCTAssertEqual(edit3SemanticDiags.diagnostics.count, 0) + + testClient.send( DidChangeTextDocumentNotification( textDocument: .init(uri, version: 15), contentChanges: [ .init(range: Position(line: 1, utf16index: 0)..) in - log("Received diagnostics for edit 4 - syntactic") - XCTAssertEqual(note.params.version, 15) - // 1 = semantic update finished already - // 0 = only syntactic - XCTAssertLessThanOrEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - """ - func foo() {} - fooTypo() - """, - documentManager.latestSnapshot(uri)!.text - ) - }, - { (note: Notification) in - log("Received diagnostics for edit 4 - semantic") - XCTAssertEqual(note.params.version, 15) - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - note.params.diagnostics.first?.range.lowerBound, - Position(line: 1, utf16index: 0) - ) - } + ) + ) + let edit4SyntacticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(edit4SyntacticDiags.version, 15) + // 1 = semantic update finished already + XCTAssertLessThanOrEqual(edit4SyntacticDiags.diagnostics.count, 1) + XCTAssertEqual( + """ + func foo() {} + fooTypo() + """, + documentManager.latestSnapshot(uri)!.text + ) + let edit4SemanticDiags = try await testClient.nextDiagnosticsNotification() + // 0 = only syntactic + XCTAssertEqual(edit4SemanticDiags.version, 15) + XCTAssertEqual(edit4SemanticDiags.diagnostics.count, 1) + XCTAssertEqual( + edit4SemanticDiags.diagnostics.first?.range.lowerBound, + Position(line: 1, utf16index: 0) ) - sk.sendNoteSync( + testClient.send( DidChangeTextDocumentNotification( textDocument: .init(uri, version: 16), contentChanges: [ @@ -223,40 +202,35 @@ final class LocalSwiftTests: XCTestCase { """ ) ] - ), - { (note: Notification) in - log("Received diagnostics for edit 5 - syntactic") - XCTAssertEqual(note.params.version, 16) - // Could be remaining semantic error or new one. - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - """ - func bar() {} - foo() - """, - documentManager.latestSnapshot(uri)!.text - ) - }, - { (note: Notification) in - log("Received diagnostics for edit 5 - semantic") - XCTAssertEqual(note.params.version, 16) - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - note.params.diagnostics.first?.range.lowerBound, - Position(line: 1, utf16index: 0) - ) - } + ) + ) + let edit5SyntacticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(edit5SyntacticDiags.version, 16) + // Could be remaining semantic error or new one. + XCTAssertEqual(edit5SyntacticDiags.diagnostics.count, 1) + XCTAssertEqual( + """ + func bar() {} + foo() + """, + documentManager.latestSnapshot(uri)!.text + ) + let edit5SemanticDiags = try await testClient.nextDiagnosticsNotification() + log("Received diagnostics for edit 5 - semantic") + XCTAssertEqual(edit5SemanticDiags.version, 16) + XCTAssertEqual(edit5SemanticDiags.diagnostics.count, 1) + XCTAssertEqual( + edit5SemanticDiags.diagnostics.first?.range.lowerBound, + Position(line: 1, utf16index: 0) ) } - func testEditingNonURL() async { + func testEditingNonURL() async throws { let uri = DocumentURI(string: "urn:uuid:A1B08909-E791-469E-BF0F-F5790977E051") - sk.allowUnexpectedNotification = false + let documentManager = await testClient.server._documentManager - let documentManager = await connection.server!._documentManager - - sk.sendNoteSync( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: uri, @@ -266,139 +240,121 @@ final class LocalSwiftTests: XCTestCase { func """ ) - ), - { (note: Notification) in - log("Received diagnostics for open - syntactic") - XCTAssertEqual(note.params.version, 12) - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual("func", documentManager.latestSnapshot(uri)!.text) - }, - { (note: Notification) in - log("Received diagnostics for open - semantic") - XCTAssertEqual(note.params.version, 12) - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - note.params.diagnostics.first?.range.lowerBound, - Position(line: 0, utf16index: 4) - ) - } + ) ) + let openSyntacticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(openSyntacticDiags.version, 12) + XCTAssertEqual(openSyntacticDiags.diagnostics.count, 1) + XCTAssertEqual("func", documentManager.latestSnapshot(uri)!.text) - sk.sendNoteSync( + let openSemanticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(openSemanticDiags.version, 12) + XCTAssertEqual(openSemanticDiags.diagnostics.count, 1) + XCTAssertEqual( + openSemanticDiags.diagnostics.first?.range.lowerBound, + Position(line: 0, utf16index: 4) + ) + + testClient.send( DidChangeTextDocumentNotification( textDocument: .init(uri, version: 13), contentChanges: [ .init(range: Range(Position(line: 0, utf16index: 4)), text: " foo() {}\n") ] - ), - { (note: Notification) in - log("Received diagnostics for edit 1 - syntactic") - XCTAssertEqual(note.params.version, 13) - // 1 = remaining semantic error - // 0 = semantic update finished already - XCTAssertLessThanOrEqual(note.params.diagnostics.count, 1) - XCTAssertEqual("func foo() {}\n", documentManager.latestSnapshot(uri)!.text) - }, - { (note: Notification) in - log("Received diagnostics for edit 1 - semantic") - XCTAssertEqual(note.params.version, 13) - XCTAssertEqual(note.params.diagnostics.count, 0) - } + ) ) + let edit1SyntacticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(edit1SyntacticDiags.version, 13) + // 1 = remaining semantic error + // 0 = semantic update finished already + XCTAssertLessThanOrEqual(edit1SyntacticDiags.diagnostics.count, 1) + XCTAssertEqual("func foo() {}\n", documentManager.latestSnapshot(uri)!.text) + + let edit1SemanticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(edit1SemanticDiags.version, 13) + XCTAssertEqual(edit1SemanticDiags.diagnostics.count, 0) - sk.sendNoteSync( + testClient.send( DidChangeTextDocumentNotification( textDocument: .init(uri, version: 14), contentChanges: [ .init(range: Range(Position(line: 1, utf16index: 0)), text: "bar()") ] - ), - { (note: Notification) in - log("Received diagnostics for edit 2 - syntactic") - XCTAssertEqual(note.params.version, 14) - // 1 = semantic update finished already - // 0 = only syntactic - XCTAssertLessThanOrEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - """ - func foo() {} - bar() - """, - documentManager.latestSnapshot(uri)!.text - ) - }, - { (note: Notification) in - log("Received diagnostics for edit 2 - semantic") - XCTAssertEqual(note.params.version, 14) - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - note.params.diagnostics.first?.range.lowerBound, - Position(line: 1, utf16index: 0) - ) - } + ) + ) + let edit2SyntacticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(edit2SyntacticDiags.version, 14) + // 1 = semantic update finished already + // 0 = only syntactic + XCTAssertLessThanOrEqual(edit2SyntacticDiags.diagnostics.count, 1) + XCTAssertEqual( + """ + func foo() {} + bar() + """, + documentManager.latestSnapshot(uri)!.text + ) + let edit2SemanticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(edit2SemanticDiags.version, 14) + XCTAssertEqual(edit2SemanticDiags.diagnostics.count, 1) + XCTAssertEqual( + edit2SemanticDiags.diagnostics.first?.range.lowerBound, + Position(line: 1, utf16index: 0) ) - sk.sendNoteSync( + testClient.send( DidChangeTextDocumentNotification( textDocument: .init(uri, version: 14), contentChanges: [ .init(range: Position(line: 1, utf16index: 0)..) in - log("Received diagnostics for edit 3 - syntactic") - XCTAssertEqual(note.params.version, 14) - // 1 = remaining semantic error - // 0 = semantic update finished already - XCTAssertLessThanOrEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - """ - func foo() {} - foo() - """, - documentManager.latestSnapshot(uri)!.text - ) - }, - { (note: Notification) in - log("Received diagnostics for edit 3 - semantic") - XCTAssertEqual(note.params.version, 14) - XCTAssertEqual(note.params.diagnostics.count, 0) - } + ) + ) + let edit3SyntacticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(edit3SyntacticDiags.version, 14) + // 1 = remaining semantic error + // 0 = semantic update finished already + XCTAssertLessThanOrEqual(edit3SyntacticDiags.diagnostics.count, 1) + XCTAssertEqual( + """ + func foo() {} + foo() + """, + documentManager.latestSnapshot(uri)!.text ) + let edit3SemanticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(edit3SemanticDiags.version, 14) + XCTAssertEqual(edit3SemanticDiags.diagnostics.count, 0) - sk.sendNoteSync( + testClient.send( DidChangeTextDocumentNotification( textDocument: .init(uri, version: 15), contentChanges: [ .init(range: Position(line: 1, utf16index: 0)..) in - log("Received diagnostics for edit 4 - syntactic") - XCTAssertEqual(note.params.version, 15) - // 1 = semantic update finished already - // 0 = only syntactic - XCTAssertLessThanOrEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - """ - func foo() {} - fooTypo() - """, - documentManager.latestSnapshot(uri)!.text - ) - }, - { (note: Notification) in - log("Received diagnostics for edit 4 - semantic") - XCTAssertEqual(note.params.version, 15) - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - note.params.diagnostics.first?.range.lowerBound, - Position(line: 1, utf16index: 0) - ) - } + ) + ) + let edit4SyntacticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(edit4SyntacticDiags.version, 15) + // 1 = semantic update finished already + // 0 = only syntactic + XCTAssertLessThanOrEqual(edit4SyntacticDiags.diagnostics.count, 1) + XCTAssertEqual( + """ + func foo() {} + fooTypo() + """, + documentManager.latestSnapshot(uri)!.text + ) + let edit4SemanticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(edit4SemanticDiags.version, 15) + XCTAssertEqual(edit4SemanticDiags.diagnostics.count, 1) + XCTAssertEqual( + edit4SemanticDiags.diagnostics.first?.range.lowerBound, + Position(line: 1, utf16index: 0) ) - sk.sendNoteSync( + testClient.send( DidChangeTextDocumentNotification( textDocument: .init(uri, version: 16), contentChanges: [ @@ -410,33 +366,29 @@ final class LocalSwiftTests: XCTestCase { """ ) ] - ), - { (note: Notification) in - log("Received diagnostics for edit 5 - syntactic") - XCTAssertEqual(note.params.version, 16) - // Could be remaining semantic error or new one. - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - """ - func bar() {} - foo() - """, - documentManager.latestSnapshot(uri)!.text - ) - }, - { (note: Notification) in - log("Received diagnostics for edit 5 - semantic") - XCTAssertEqual(note.params.version, 16) - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - note.params.diagnostics.first?.range.lowerBound, - Position(line: 1, utf16index: 0) - ) - } + ) + ) + let edit5SyntacticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(edit5SyntacticDiags.version, 16) + // Could be remaining semantic error or new one. + XCTAssertEqual(edit5SyntacticDiags.diagnostics.count, 1) + XCTAssertEqual( + """ + func bar() {} + foo() + """, + documentManager.latestSnapshot(uri)!.text + ) + let edit5SemanticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(edit5SemanticDiags.version, 16) + XCTAssertEqual(edit5SemanticDiags.diagnostics.count, 1) + XCTAssertEqual( + edit5SemanticDiags.diagnostics.first?.range.lowerBound, + Position(line: 1, utf16index: 0) ) } - func testExcludedDocumentSchemeDiagnostics() { + func testExcludedDocumentSchemeDiagnostics() async throws { let includedURL = URL(fileURLWithPath: "/a.swift") let includedURI = DocumentURI(includedURL) @@ -446,11 +398,9 @@ final class LocalSwiftTests: XCTestCase { func """ - sk.allowUnexpectedNotification = false - // Open the excluded URI first so our later notification handlers can confirm // that no diagnostics were emitted for this excluded URI. - sk.send( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: excludedURI, @@ -461,7 +411,7 @@ final class LocalSwiftTests: XCTestCase { ) ) - sk.sendNoteSync( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: includedURI, @@ -469,27 +419,21 @@ final class LocalSwiftTests: XCTestCase { version: 1, text: text ) - ), - { (note: Notification) in - log("Received diagnostics for open - syntactic") - XCTAssertEqual(note.params.uri, includedURI) - }, - { (note: Notification) in - log("Received diagnostics for open - semantic") - XCTAssertEqual(note.params.uri, includedURI) - } + ) ) + let syntacticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(syntacticDiags.uri, includedURI) + let semanticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(semanticDiags.uri, includedURI) } - func testCrossFileDiagnostics() { + func testCrossFileDiagnostics() async throws { let urlA = URL(fileURLWithPath: "/\(UUID())/a.swift") let urlB = URL(fileURLWithPath: "/\(UUID())/b.swift") let uriA = DocumentURI(urlA) let uriB = DocumentURI(urlB) - sk.allowUnexpectedNotification = false - - sk.sendNoteSync( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: uriA, @@ -499,26 +443,23 @@ final class LocalSwiftTests: XCTestCase { foo() """ ) - ), - { (note: Notification) in - log("Received diagnostics for open - syntactic") - XCTAssertEqual(note.params.version, 12) - // 1 = semantic update finished already - // 0 = only syntactic - XCTAssertLessThanOrEqual(note.params.diagnostics.count, 1) - }, - { (note: Notification) in - log("Received diagnostics for open - semantic") - XCTAssertEqual(note.params.version, 12) - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - note.params.diagnostics.first?.range.lowerBound, - Position(line: 0, utf16index: 0) - ) - } + ) ) + let openASyntacticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(openASyntacticDiags.version, 12) + // 1 = semantic update finished already + // 0 = only syntactic + XCTAssertLessThanOrEqual(openASyntacticDiags.diagnostics.count, 1) - sk.sendNoteSync( + let openASemanticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(openASemanticDiags.version, 12) + XCTAssertEqual(openASemanticDiags.diagnostics.count, 1) + XCTAssertEqual( + openASemanticDiags.diagnostics.first?.range.lowerBound, + Position(line: 0, utf16index: 0) + ) + + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: uriB, @@ -528,51 +469,44 @@ final class LocalSwiftTests: XCTestCase { bar() """ ) - ), - { (note: Notification) in - log("Received diagnostics for open - syntactic") - XCTAssertEqual(note.params.version, 12) - // 1 = semantic update finished already - // 0 = only syntactic - XCTAssertLessThanOrEqual(note.params.diagnostics.count, 1) - }, - { (note: Notification) in - log("Received diagnostics for open - semantic") - XCTAssertEqual(note.params.version, 12) - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - note.params.diagnostics.first?.range.lowerBound, - Position(line: 0, utf16index: 0) - ) - } + ) + ) + let openBSyntacticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(openBSyntacticDiags.version, 12) + // 1 = semantic update finished already + // 0 = only syntactic + XCTAssertLessThanOrEqual(openBSyntacticDiags.diagnostics.count, 1) + + let openBSemanticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(openBSemanticDiags.version, 12) + XCTAssertEqual(openBSemanticDiags.diagnostics.count, 1) + XCTAssertEqual( + openBSemanticDiags.diagnostics.first?.range.lowerBound, + Position(line: 0, utf16index: 0) ) - sk.sendNoteSync( + testClient.send( DidChangeTextDocumentNotification( textDocument: .init(uriA, version: 13), contentChanges: [ .init(range: nil, text: "foo()\n") ] - ), - { (note: Notification) in - log("Received diagnostics for edit 1 - syntactic") - XCTAssertEqual(note.params.version, 13) - XCTAssertEqual(note.params.diagnostics.count, 1) - }, - { (note: Notification) in - log("Received diagnostics for edit 1 - semantic") - XCTAssertEqual(note.params.version, 13) - XCTAssertEqual(note.params.diagnostics.count, 1) - } + ) ) + let editASyntacticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(editASyntacticDiags.version, 13) + XCTAssertEqual(editASyntacticDiags.diagnostics.count, 1) + + let editASemanticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(editASemanticDiags.version, 13) + XCTAssertEqual(editASemanticDiags.diagnostics.count, 1) } - func testDiagnosticsReopen() { + func testDiagnosticsReopen() async throws { let urlA = URL(fileURLWithPath: "/\(UUID())/a.swift") let uriA = DocumentURI(urlA) - sk.allowUnexpectedNotification = false - sk.sendNoteSync( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: uriA, @@ -582,28 +516,26 @@ final class LocalSwiftTests: XCTestCase { foo() """ ) - ), - { (note: Notification) in - log("Received diagnostics for open - syntactic") - XCTAssertEqual(note.params.version, 12) - // 1 = semantic update finished already - // 0 = only syntactic - XCTAssertLessThanOrEqual(note.params.diagnostics.count, 1) - }, - { (note: Notification) in - log("Received diagnostics for open - semantic") - XCTAssertEqual(note.params.version, 12) - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - note.params.diagnostics.first?.range.lowerBound, - Position(line: 0, utf16index: 0) - ) - } + ) + ) + + let open1SyntacticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(open1SyntacticDiags.version, 12) + // 1 = semantic update finished already + // 0 = only syntactic + XCTAssertLessThanOrEqual(open1SyntacticDiags.diagnostics.count, 1) + + let open1SemanticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(open1SemanticDiags.version, 12) + XCTAssertEqual(open1SemanticDiags.diagnostics.count, 1) + XCTAssertEqual( + open1SemanticDiags.diagnostics.first?.range.lowerBound, + Position(line: 0, utf16index: 0) ) - sk.send(DidCloseTextDocumentNotification(textDocument: .init(urlA))) + testClient.send(DidCloseTextDocumentNotification(textDocument: .init(urlA))) - sk.sendNoteSync( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: uriA, @@ -613,36 +545,32 @@ final class LocalSwiftTests: XCTestCase { var """ ) - ), - { (note: Notification) in - log("Received diagnostics for open - syntactic") - XCTAssertEqual(note.params.version, 13) - // 1 = syntactic, no cached semantic diagnostic from previous version - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - note.params.diagnostics.first?.range.lowerBound, - Position(line: 0, utf16index: 3) - ) - }, - { (note: Notification) in - log("Received diagnostics for open - semantic") - XCTAssertEqual(note.params.version, 13) - XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual( - note.params.diagnostics.first?.range.lowerBound, - Position(line: 0, utf16index: 3) - ) - } + ) + ) + + let open2SyntacticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(open2SyntacticDiags.version, 13) + // 1 = syntactic, no cached semantic diagnostic from previous version + XCTAssertEqual(open2SyntacticDiags.diagnostics.count, 1) + XCTAssertEqual( + open2SyntacticDiags.diagnostics.first?.range.lowerBound, + Position(line: 0, utf16index: 3) + ) + + let open2SemanticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(open2SemanticDiags.version, 13) + XCTAssertEqual(open2SemanticDiags.diagnostics.count, 1) + XCTAssertEqual( + open2SemanticDiags.diagnostics.first?.range.lowerBound, + Position(line: 0, utf16index: 3) ) } - func testEducationalNotesAreUsedAsDiagnosticCodes() { + func testEducationalNotesAreUsedAsDiagnosticCodes() async throws { let url = URL(fileURLWithPath: "/\(UUID())/a.swift") let uri = DocumentURI(url) - sk.allowUnexpectedNotification = false - - sk.sendNoteSync( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: uri, @@ -650,27 +578,23 @@ final class LocalSwiftTests: XCTestCase { version: 12, text: "@propertyWrapper struct Bar {}" ) - ), - { (note: Notification) in - log("Received diagnostics for open - syntactic") - }, - { (note: Notification) in - log("Received diagnostics for open - semantic") - XCTAssertEqual(note.params.diagnostics.count, 1) - let diag = note.params.diagnostics.first! - XCTAssertEqual(diag.code, .string("property-wrapper-requirements")) - XCTAssertEqual(diag.codeDescription?.href.fileURL?.lastPathComponent, "property-wrapper-requirements.md") - } + ) ) + // syntactic diags + _ = try await testClient.nextDiagnosticsNotification() + + let semanticDiags = try await testClient.nextDiagnosticsNotification() + XCTAssertEqual(semanticDiags.diagnostics.count, 1) + let diag = semanticDiags.diagnostics.first! + XCTAssertEqual(diag.code, .string("property-wrapper-requirements")) + XCTAssertEqual(diag.codeDescription?.href.fileURL?.lastPathComponent, "property-wrapper-requirements.md") } - func testFixitsAreIncludedInPublishDiagnostics() { + func testFixitsAreIncludedInPublishDiagnostics() async throws { let url = URL(fileURLWithPath: "/\(UUID())/a.swift") let uri = DocumentURI(url) - sk.allowUnexpectedNotification = false - - sk.sendNoteSync( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: uri, @@ -682,44 +606,40 @@ final class LocalSwiftTests: XCTestCase { } """ ) - ), - { (note: Notification) in - log("Received diagnostics for open - syntactic") - }, - { (note: Notification) in - log("Received diagnostics for open - semantic") - XCTAssertEqual(note.params.diagnostics.count, 1) - let diag = note.params.diagnostics.first! - XCTAssertNotNil(diag.codeActions) - XCTAssertEqual(diag.codeActions!.count, 1) - let fixit = diag.codeActions!.first! + ) + ) + // syntactic diags + _ = try await testClient.nextDiagnosticsNotification() - // Expected Fix-it: Replace `let a` with `_` because it's never used - let expectedTextEdit = TextEdit( - range: Position(line: 1, utf16index: 2)..) in - log("Received diagnostics for open - syntactic") - }, - { (note: Notification) in - log("Received diagnostics for open - semantic") - XCTAssertEqual(note.params.diagnostics.count, 1) - let diag = note.params.diagnostics.first! - XCTAssertEqual(diag.relatedInformation?.count, 2) - if let note1 = diag.relatedInformation?.first(where: { $0.message.contains("'?'") }) { - XCTAssertEqual(note1.codeActions?.count, 1) - if let fixit = note1.codeActions?.first { - // Expected Fix-it: Replace `let a` with `_` because it's never used - let expectedTextEdit = TextEdit( - range: Position(line: 1, utf16index: 7)..) in - log("Received diagnostics for open - syntactic") - }, - { (note: Notification) in - log("Received diagnostics for open - semantic") - XCTAssertEqual(note.params.diagnostics.count, 1) - let diag = note.params.diagnostics.first! - XCTAssertNotNil(diag.codeActions) - XCTAssertEqual(diag.codeActions!.count, 1) - let fixit = diag.codeActions!.first! - - // Expected Fix-it: Insert `;` - let expectedTextEdit = TextEdit( - range: Position(line: 1, utf16index: 11).. XCTestExpectation { let refreshExpectation = expectation(description: "\(#function) - refresh received") - sk.appendOneShotRequestHandler { (req: Request) in - req.reply(VoidResponse()) + testClient.handleNextRequest { (req: WorkspaceSemanticTokensRefreshRequest) -> VoidResponse in refreshExpectation.fulfill() + return VoidResponse() } return refreshExpectation } @@ -84,22 +87,21 @@ final class SemanticTokensTests: XCTestCase { // We will wait for the server to dynamically register semantic tokens let registerCapabilityExpectation = expectation(description: "\(#function) - register semantic tokens capability") - sk.appendOneShotRequestHandler { (req: Request) in - let registrations = req.params.registrations + testClient.handleNextRequest { (req: RegisterCapabilityRequest) -> VoidResponse in XCTAssert( - registrations.contains { reg in + req.registrations.contains { reg in reg.method == SemanticTokensRegistrationOptions.method } ) - req.reply(VoidResponse()) registerCapabilityExpectation.fulfill() + return VoidResponse() } // We will wait for the first refresh request to make sure that the semantic tokens are ready let refreshExpectation = expectSemanticTokensRefresh() - sk.send( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: uri, @@ -124,7 +126,7 @@ final class SemanticTokensTests: XCTestCase { expectations.append(expectSemanticTokensRefresh()) } - sk.send( + testClient.send( DidChangeTextDocumentNotification( textDocument: VersionedTextDocumentIdentifier( uri, @@ -150,21 +152,33 @@ final class SemanticTokensTests: XCTestCase { ) } - private func performSemanticTokensRequest(range: Range? = nil) throws -> [Token] { + private func performSemanticTokensRequest(range: Range? = nil) async throws -> [Token] { let response: DocumentSemanticTokensResponse! if let range = range { - response = try sk.sendSync(DocumentSemanticTokensRangeRequest(textDocument: textDocument, range: range)) + response = try await testClient.send( + DocumentSemanticTokensRangeRequest( + textDocument: TextDocumentIdentifier(uri), + range: range + ) + ) } else { - response = try sk.sendSync(DocumentSemanticTokensRequest(textDocument: textDocument)) + response = try await testClient.send( + DocumentSemanticTokensRequest( + textDocument: TextDocumentIdentifier(uri) + ) + ) } return [Token](lspEncodedTokens: response.data) } - private func openAndPerformSemanticTokensRequest(text: String, range: Range? = nil) throws -> [Token] { + private func openAndPerformSemanticTokensRequest( + text: String, + range: Range? = nil + ) async throws -> [Token] { openDocument(text: text) - return try performSemanticTokensRequest(range: range) + return try await performSemanticTokensRequest(range: range) } func testIntArrayCoding() { @@ -215,7 +229,7 @@ final class SemanticTokensTests: XCTestCase { """ openDocument(text: text) - guard let snapshot = await connection.server?._documentManager.latestSnapshot(uri) else { + guard let snapshot = await testClient.server._documentManager.latestSnapshot(uri) else { fatalError("Could not fetch document snapshot for \(#function)") } @@ -241,13 +255,13 @@ final class SemanticTokensTests: XCTestCase { ) } - func testEmpty() throws { + func testEmpty() async throws { let text = "" - let tokens = try openAndPerformSemanticTokensRequest(text: text) + let tokens = try await openAndPerformSemanticTokensRequest(text: text) XCTAssertEqual(tokens, []) } - func testRanged() throws { + func testRanged() async throws { let text = """ let x = 1 let test = 20 @@ -256,7 +270,7 @@ final class SemanticTokensTests: XCTestCase { """ let start = Position(line: 1, utf16index: 0) let end = Position(line: 2, utf16index: 5) - let tokens = try openAndPerformSemanticTokensRequest(text: text, range: start..() {} """ - let tokens = try openAndPerformSemanticTokensRequest(text: text) + let tokens = try await openAndPerformSemanticTokensRequest(text: text) XCTAssertEqual( tokens, [ @@ -437,9 +451,9 @@ final class SemanticTokensTests: XCTestCase { ) } - func testSemanticTokensForFunctionSignatures() throws { + func testSemanticTokensForFunctionSignatures() async throws { let text = "func f(x: Int, _ y: String) {}" - let tokens = try openAndPerformSemanticTokensRequest(text: text) + let tokens = try await openAndPerformSemanticTokensRequest(text: text) XCTAssertEqual( tokens, [ @@ -453,9 +467,9 @@ final class SemanticTokensTests: XCTestCase { ) } - func testSemanticTokensForFunctionSignaturesWithEmoji() throws { + func testSemanticTokensForFunctionSignaturesWithEmoji() async throws { let text = "func x👍y() {}" - let tokens = try openAndPerformSemanticTokensRequest(text: text) + let tokens = try await openAndPerformSemanticTokensRequest(text: text) XCTAssertEqual( tokens, [ @@ -465,7 +479,7 @@ final class SemanticTokensTests: XCTestCase { ) } - func testSemanticTokensForStaticMethods() throws { + func testSemanticTokensForStaticMethods() async throws { let text = """ class X { deinit {} @@ -475,7 +489,7 @@ final class SemanticTokensTests: XCTestCase { X.f() X.g() """ - let tokens = try openAndPerformSemanticTokensRequest(text: text) + let tokens = try await openAndPerformSemanticTokensRequest(text: text) XCTAssertEqual( tokens, [ @@ -502,7 +516,7 @@ final class SemanticTokensTests: XCTestCase { ) } - func testSemanticTokensForEnumMembers() throws { + func testSemanticTokensForEnumMembers() async throws { let text = """ enum Maybe { case none @@ -512,7 +526,7 @@ final class SemanticTokensTests: XCTestCase { let x = Maybe.none let y: Maybe = .some(42) """ - let tokens = try openAndPerformSemanticTokensRequest(text: text) + let tokens = try await openAndPerformSemanticTokensRequest(text: text) XCTAssertEqual( tokens, [ @@ -543,11 +557,11 @@ final class SemanticTokensTests: XCTestCase { ) } - func testRegexSemanticTokens() throws { + func testRegexSemanticTokens() async throws { let text = """ let r = /a[bc]*/ """ - let tokens = try openAndPerformSemanticTokensRequest(text: text) + let tokens = try await openAndPerformSemanticTokensRequest(text: text) XCTAssertEqual( tokens, [ @@ -558,11 +572,11 @@ final class SemanticTokensTests: XCTestCase { ) } - func testOperatorDeclaration() throws { + func testOperatorDeclaration() async throws { let text = """ infix operator ?= :ComparisonPrecedence """ - let tokens = try openAndPerformSemanticTokensRequest(text: text) + let tokens = try await openAndPerformSemanticTokensRequest(text: text) XCTAssertEqual( tokens, [ @@ -574,29 +588,29 @@ final class SemanticTokensTests: XCTestCase { ) } - func testEmptyEdit() throws { + func testEmptyEdit() async throws { let text = """ let x: String = "test" var y = 123 """ openDocument(text: text) - let before = try performSemanticTokensRequest() + let before = try await performSemanticTokensRequest() let pos = Position(line: 0, utf16index: 1) editDocument(range: pos..) in - // Semantic analysis: no errors expected here. - XCTAssertEqual(note.params.diagnostics.count, 0) - startExpectation.fulfill() - } - ws.sk.appendOneShotNotificationHandler { (note: Notification) in - // Semantic analysis: expect module import error. - XCTAssertEqual(note.params.diagnostics.count, 1) - if let diagnostic = note.params.diagnostics.first { - XCTAssert( - diagnostic.message.contains("no such module"), - "expected module import error but found \"\(diagnostic.message)\"" - ) - } - startExpectation.fulfill() - } try ws.openDocument(moduleRef.url, language: .swift) - try await fulfillmentOfOrThrow([startExpectation]) + + let initialSyntacticDiags = try await ws.testClient.nextDiagnosticsNotification() + // Semantic analysis: no errors expected here. + XCTAssertEqual(initialSyntacticDiags.diagnostics.count, 0) + + let initialSemanticDiags = try await ws.testClient.nextDiagnosticsNotification() + // Semantic analysis: expect module import error. + XCTAssertEqual(initialSemanticDiags.diagnostics.count, 1) + if let diagnostic = initialSemanticDiags.diagnostics.first { + XCTAssert( + diagnostic.message.contains("no such module"), + "expected module import error but found \"\(diagnostic.message)\"" + ) + } try ws.buildAndIndex() - let finishExpectation = XCTestExpectation(description: "post-build diagnostics") - finishExpectation.expectedFulfillmentCount = 2 - ws.sk.handleNextNotification { (note: Notification) in - // Semantic analysis - SourceKit currently caches diagnostics so we still see an error. - XCTAssertEqual(note.params.diagnostics.count, 1) - finishExpectation.fulfill() - } - ws.sk.appendOneShotNotificationHandler { (note: Notification) in - // Semantic analysis: no more errors expected, import should resolve since we built. - XCTAssertEqual(note.params.diagnostics.count, 0) - finishExpectation.fulfill() - } - await server.filesDependenciesUpdated([DocumentURI(moduleRef.url)]) + await ws.testClient.server.filesDependenciesUpdated([DocumentURI(moduleRef.url)]) + + let updatedSyntacticDiags = try await ws.testClient.nextDiagnosticsNotification() + // Semantic analysis - SourceKit currently caches diagnostics so we still see an error. + XCTAssertEqual(updatedSyntacticDiags.diagnostics.count, 1) - try await fulfillmentOfOrThrow([finishExpectation]) + let updatedSemanticDiags = try await ws.testClient.nextDiagnosticsNotification() + // Semantic analysis: no more errors expected, import should resolve since we built. + XCTAssertEqual(updatedSemanticDiags.diagnostics.count, 0) } func testDependenciesUpdatedCXXTibs() async throws { guard let ws = try await mutableSourceKitTibsTestWorkspace(name: "GeneratedHeader") else { return } defer { withExtendedLifetime(ws) {} } // Keep workspace alive for callbacks. - guard let server = ws.testServer.server else { - XCTFail("Unable to fetch SourceKitServer to notify for build system events.") - return - } let moduleRef = ws.testLoc("libX:call:main") - let startExpectation = XCTestExpectation(description: "initial diagnostics") - ws.sk.handleNextNotification { (note: Notification) in - // Expect one error: - // - Implicit declaration of function invalid - XCTAssertEqual(note.params.diagnostics.count, 1) - startExpectation.fulfill() - } let generatedHeaderURL = moduleRef.url.deletingLastPathComponent() .appendingPathComponent("lib-generated.h", isDirectory: false) @@ -333,23 +274,23 @@ final class SKTests: XCTestCase { // files without a recently upstreamed extension. try "".write(to: generatedHeaderURL, atomically: true, encoding: .utf8) try ws.openDocument(moduleRef.url, language: .c) - try await fulfillmentOfOrThrow([startExpectation]) + + let openDiags = try await ws.testClient.nextDiagnosticsNotification() + // Expect one error: + // - Implicit declaration of function invalid + XCTAssertEqual(openDiags.diagnostics.count, 1) // Update the header file to have the proper contents for our code to build. let contents = "int libX(int value);" try contents.write(to: generatedHeaderURL, atomically: true, encoding: .utf8) try ws.buildAndIndex() - let finishExpectation = XCTestExpectation(description: "post-build diagnostics") - ws.sk.handleNextNotification { (note: Notification) in - // No more errors expected, import should resolve since we the generated header file - // now has the proper contents. - XCTAssertEqual(note.params.diagnostics.count, 0) - finishExpectation.fulfill() - } - await server.filesDependenciesUpdated([DocumentURI(moduleRef.url)]) + await ws.testClient.server.filesDependenciesUpdated([DocumentURI(moduleRef.url)]) - try await fulfillmentOfOrThrow([finishExpectation]) + let updatedDiags = try await ws.testClient.nextDiagnosticsNotification() + // No more errors expected, import should resolve since we the generated header file + // now has the proper contents. + XCTAssertEqual(updatedDiags.diagnostics.count, 0) } func testClangdGoToInclude() async throws { @@ -367,7 +308,7 @@ final class SKTests: XCTestCase { textDocument: mainLoc.docIdentifier, position: includePosition ) - let resp = try withExtendedLifetime(ws) { try ws.sk.sendSync(goToInclude) } + let resp = try await ws.testClient.send(goToInclude) let locationsOrLinks = try XCTUnwrap(resp, "No response for go-to-#include") switch locationsOrLinks { @@ -398,7 +339,7 @@ final class SKTests: XCTestCase { textDocument: refLoc.docIdentifier, position: refPos ) - let resp = try withExtendedLifetime(ws) { try ws.sk.sendSync(goToDefinition) } + let resp = try await ws.testClient.send(goToDefinition) let locationsOrLinks = try XCTUnwrap(resp, "No response for go-to-definition") switch locationsOrLinks { @@ -430,7 +371,7 @@ final class SKTests: XCTestCase { textDocument: mainLoc.docIdentifier, position: includePosition ) - let resp = try ws.sk.sendSync(goToInclude) + let resp = try await ws.testClient.send(goToInclude) let locationsOrLinks = try XCTUnwrap(resp, "No response for go-to-declaration") switch locationsOrLinks { diff --git a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift index 0e4e035d8..ecb71cc46 100644 --- a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift +++ b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift @@ -18,10 +18,10 @@ import XCTest final class SwiftCompletionTests: XCTestCase { - typealias CompletionCapabilities = TextDocumentClientCapabilities.Completion + private typealias CompletionCapabilities = TextDocumentClientCapabilities.Completion /// Base document text to use for completion tests. - let text: String = """ + private let text: String = """ struct S { /// Documentation for `abc`. var abc: Int @@ -36,24 +36,13 @@ final class SwiftCompletionTests: XCTestCase { } """ - /// Connection and lifetime management for the service. - var connection: TestSourceKitServer! = nil + // MARK: - Helpers - /// The primary interface to make requests to the SourceKitServer. - var sk: TestClient! = nil - - override func tearDown() { - shutdownServer() - } - - func shutdownServer() { - sk = nil - connection = nil - } - - func initializeServer(options: SKCompletionOptions = .init(), capabilities: CompletionCapabilities? = nil) throws { - connection = TestSourceKitServer() - sk = connection.client + private func initializeServer( + options: SKCompletionOptions = .init(), + capabilities: CompletionCapabilities? = nil + ) async throws -> TestSourceKitLSPClient { + let testClient = TestSourceKitLSPClient() var documentCapabilities: TextDocumentClientCapabilities? if let capabilities = capabilities { documentCapabilities = TextDocumentClientCapabilities() @@ -61,7 +50,7 @@ final class SwiftCompletionTests: XCTestCase { } else { documentCapabilities = nil } - _ = try sk.sendSync( + _ = try await testClient.send( InitializeRequest( processId: nil, rootPath: nil, @@ -77,10 +66,11 @@ final class SwiftCompletionTests: XCTestCase { workspaceFolders: nil ) ) + return testClient } - func openDocument(text: String? = nil, url: URL) { - sk.send( + func openDocument(testClient: TestSourceKitLSPClient, text: String? = nil, url: URL) { + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: DocumentURI(url), @@ -92,24 +82,26 @@ final class SwiftCompletionTests: XCTestCase { ) } - func testCompletionClientFilter() throws { - try testCompletionBasic(options: SKCompletionOptions(serverSideFiltering: false, maxResults: nil)) + // MARK: - Tests + + func testCompletionClientFilter() async throws { + try await testCompletionBasic(options: SKCompletionOptions(serverSideFiltering: false, maxResults: nil)) } - func testCompletionServerFilter() throws { - try testCompletionBasic(options: SKCompletionOptions(serverSideFiltering: true, maxResults: nil)) + func testCompletionServerFilter() async throws { + try await testCompletionBasic(options: SKCompletionOptions(serverSideFiltering: true, maxResults: nil)) } - func testCompletionDefaultFilter() throws { - try testCompletionBasic(options: SKCompletionOptions()) + func testCompletionDefaultFilter() async throws { + try await testCompletionBasic(options: SKCompletionOptions()) } - func testCompletionBasic(options: SKCompletionOptions) throws { - try initializeServer(options: options) + func testCompletionBasic(options: SKCompletionOptions) async throws { + let testClient = try await initializeServer(options: options) let url = URL(fileURLWithPath: "/\(UUID())/a.swift") - openDocument(url: url) + openDocument(testClient: testClient, url: url) - let selfDot = try sk.sendSync( + let selfDot = try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 5, utf16index: 9) @@ -134,7 +126,7 @@ final class SwiftCompletionTests: XCTestCase { } for col in 10...12 { - let inIdent = try sk.sendSync( + let inIdent = try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 5, utf16index: col) @@ -160,7 +152,7 @@ final class SwiftCompletionTests: XCTestCase { XCTAssertEqual(abc.insertTextFormat, .plain) } - let after = try sk.sendSync( + let after = try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 6, utf16index: 0) @@ -169,16 +161,16 @@ final class SwiftCompletionTests: XCTestCase { XCTAssertNotEqual(after, selfDot) } - func testCompletionSnippetSupport() throws { + func testCompletionSnippetSupport() async throws { var capabilities = CompletionCapabilities() capabilities.completionItem = CompletionCapabilities.CompletionItem(snippetSupport: true) - try initializeServer(capabilities: capabilities) + let testClient = try await initializeServer(capabilities: capabilities) let url = URL(fileURLWithPath: "/\(UUID())/a.swift") - openDocument(url: url) + openDocument(testClient: testClient, url: url) - func getTestMethodCompletion(_ position: Position, label: String) throws -> CompletionItem? { - let selfDot = try sk.sendSync( + func getTestMethodCompletion(_ position: Position, label: String) async throws -> CompletionItem? { + let selfDot = try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: position @@ -187,15 +179,7 @@ final class SwiftCompletionTests: XCTestCase { return selfDot.items.first { $0.label == label } } - func getTestMethodACompletion() throws -> CompletionItem? { - return try getTestMethodCompletion(Position(line: 5, utf16index: 9), label: "test(a: Int)") - } - - func getTestMethodBCompletion() throws -> CompletionItem? { - return try getTestMethodCompletion(Position(line: 9, utf16index: 9), label: "test(b: Int)") - } - - var test = try getTestMethodACompletion() + var test = try await getTestMethodCompletion(Position(line: 5, utf16index: 9), label: "test(a: Int)") XCTAssertNotNil(test) if let test = test { XCTAssertEqual(test.kind, .method) @@ -214,7 +198,7 @@ final class SwiftCompletionTests: XCTestCase { XCTAssertEqual(test.insertTextFormat, .snippet) } - test = try getTestMethodBCompletion() + test = try await getTestMethodCompletion(Position(line: 9, utf16index: 9), label: "test(b: Int)") XCTAssertNotNil(test) if let test = test { XCTAssertEqual(test.kind, .method) @@ -232,13 +216,26 @@ final class SwiftCompletionTests: XCTestCase { XCTAssertEqual(test.insertText, "test(${1:b: Int})") XCTAssertEqual(test.insertTextFormat, .snippet) } + } - shutdownServer() + func testCompletionNoSnippetSupport() async throws { + var capabilities = CompletionCapabilities() capabilities.completionItem?.snippetSupport = false - try initializeServer(capabilities: capabilities) - openDocument(url: url) + let testClient = try await initializeServer(capabilities: capabilities) + let url = URL(fileURLWithPath: "/\(UUID())/a.swift") + openDocument(testClient: testClient, url: url) - test = try getTestMethodACompletion() + func getTestMethodCompletion(_ position: Position, label: String) async throws -> CompletionItem? { + let selfDot = try await testClient.send( + CompletionRequest( + textDocument: TextDocumentIdentifier(url), + position: position + ) + ) + return selfDot.items.first { $0.label == label } + } + + var test = try await getTestMethodCompletion(Position(line: 5, utf16index: 9), label: "test(a: Int)") XCTAssertNotNil(test) if let test = test { XCTAssertEqual(test.kind, .method) @@ -254,7 +251,7 @@ final class SwiftCompletionTests: XCTestCase { XCTAssertEqual(test.insertTextFormat, .plain) } - test = try getTestMethodBCompletion() + test = try await getTestMethodCompletion(Position(line: 9, utf16index: 9), label: "test(b: Int)") XCTAssertNotNil(test) if let test = test { XCTAssertEqual(test.kind, .method) @@ -272,21 +269,21 @@ final class SwiftCompletionTests: XCTestCase { } } - func testCompletionPositionClientFilter() throws { - try testCompletionPosition(options: SKCompletionOptions(serverSideFiltering: false, maxResults: nil)) + func testCompletionPositionClientFilter() async throws { + try await testCompletionPosition(options: SKCompletionOptions(serverSideFiltering: false, maxResults: nil)) } - func testCompletionPositionServerFilter() throws { - try testCompletionPosition(options: SKCompletionOptions(serverSideFiltering: true, maxResults: nil)) + func testCompletionPositionServerFilter() async throws { + try await testCompletionPosition(options: SKCompletionOptions(serverSideFiltering: true, maxResults: nil)) } - func testCompletionPosition(options: SKCompletionOptions) throws { - try initializeServer(options: options) + func testCompletionPosition(options: SKCompletionOptions) async throws { + let testClient = try await initializeServer(options: options) let url = URL(fileURLWithPath: "/\(UUID())/a.swift") - openDocument(text: "foo", url: url) + openDocument(testClient: testClient, text: "foo", url: url) for col in 0...3 { - let inOrAfterFoo = try sk.sendSync( + let inOrAfterFoo = try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 0, utf16index: col) @@ -296,7 +293,7 @@ final class SwiftCompletionTests: XCTestCase { XCTAssertFalse(inOrAfterFoo.items.isEmpty) } - let outOfRange1 = try sk.sendSync( + let outOfRange1 = try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 0, utf16index: 4) @@ -304,7 +301,7 @@ final class SwiftCompletionTests: XCTestCase { ) XCTAssertTrue(outOfRange1.isIncomplete) - let outOfRange2 = try sk.sendSync( + let outOfRange2 = try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 1, utf16index: 0) @@ -313,8 +310,8 @@ final class SwiftCompletionTests: XCTestCase { XCTAssertTrue(outOfRange2.isIncomplete) } - func testCompletionOptional() throws { - try initializeServer() + func testCompletionOptional() async throws { + let testClient = try await initializeServer() let url = URL(fileURLWithPath: "/\(UUID())/a.swift") let text = """ struct Foo { @@ -323,10 +320,10 @@ final class SwiftCompletionTests: XCTestCase { let a: Foo? = Foo(bar: 1) a.ba """ - openDocument(text: text, url: url) + openDocument(testClient: testClient, text: text, url: url) for col in 2...4 { - let response = try sk.sendSync( + let response = try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 4, utf16index: col) @@ -347,8 +344,8 @@ final class SwiftCompletionTests: XCTestCase { } } - func testCompletionOverride() throws { - try initializeServer() + func testCompletionOverride() async throws { + let testClient = try await initializeServer() let url = URL(fileURLWithPath: "/\(UUID())/a.swift") let text = """ class Base { @@ -358,9 +355,9 @@ final class SwiftCompletionTests: XCTestCase { func // don't delete trailing space in this file } """ - openDocument(text: text, url: url) + openDocument(testClient: testClient, text: text, url: url) - let response = try sk.sendSync( + let response = try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 4, utf16index: 7) @@ -383,8 +380,8 @@ final class SwiftCompletionTests: XCTestCase { ) } - func testCompletionOverrideInNewLine() throws { - try initializeServer() + func testCompletionOverrideInNewLine() async throws { + let testClient = try await initializeServer() let url = URL(fileURLWithPath: "/\(UUID())/a.swift") let text = """ class Base { @@ -395,9 +392,9 @@ final class SwiftCompletionTests: XCTestCase { // don't delete trailing space in this file } """ - openDocument(text: text, url: url) + openDocument(testClient: testClient, text: text, url: url) - let response = try sk.sendSync( + let response = try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 5, utf16index: 2) @@ -420,10 +417,13 @@ final class SwiftCompletionTests: XCTestCase { ) } - func testMaxResults() throws { - try initializeServer(options: SKCompletionOptions(serverSideFiltering: true, maxResults: nil)) + func testMaxResults() async throws { + let testClient = try await initializeServer( + options: SKCompletionOptions(serverSideFiltering: true, maxResults: nil) + ) let url = URL(fileURLWithPath: "/\(UUID())/a.swift") openDocument( + testClient: testClient, text: """ struct S { func f1() {} @@ -440,10 +440,10 @@ final class SwiftCompletionTests: XCTestCase { ) // Server-wide option - XCTAssertEqual( + assertEqual( 5, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 9) @@ -453,10 +453,10 @@ final class SwiftCompletionTests: XCTestCase { ) // Explicit option - XCTAssertEqual( + assertEqual( 5, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 9), @@ -472,10 +472,10 @@ final class SwiftCompletionTests: XCTestCase { // MARK: Limited - XCTAssertEqual( + assertEqual( 5, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 9), @@ -489,10 +489,10 @@ final class SwiftCompletionTests: XCTestCase { ) ) - XCTAssertEqual( + assertEqual( 3, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 9), @@ -505,10 +505,10 @@ final class SwiftCompletionTests: XCTestCase { ) ) ) - XCTAssertEqual( + assertEqual( 1, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 9), @@ -523,10 +523,10 @@ final class SwiftCompletionTests: XCTestCase { ) // 0 also means unlimited - XCTAssertEqual( + assertEqual( 5, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 9), @@ -542,10 +542,10 @@ final class SwiftCompletionTests: XCTestCase { // MARK: With filter='f' - XCTAssertEqual( + assertEqual( 5, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 10), @@ -558,10 +558,10 @@ final class SwiftCompletionTests: XCTestCase { ) ) ) - XCTAssertEqual( + assertEqual( 3, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 10), @@ -577,10 +577,11 @@ final class SwiftCompletionTests: XCTestCase { } - func testRefilterAfterIncompleteResults() throws { - try initializeServer(options: SKCompletionOptions(serverSideFiltering: true, maxResults: 20)) + func testRefilterAfterIncompleteResults() async throws { + let testClient = try await initializeServer(options: SKCompletionOptions(serverSideFiltering: true, maxResults: 20)) let url = URL(fileURLWithPath: "/\(UUID())/a.swift") openDocument( + testClient: testClient, text: """ struct S { func fooAbc() {} @@ -596,10 +597,10 @@ final class SwiftCompletionTests: XCTestCase { url: url ) - XCTAssertEqual( + assertEqual( 5, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 10), @@ -609,10 +610,10 @@ final class SwiftCompletionTests: XCTestCase { ) ) - XCTAssertEqual( + assertEqual( 3, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 11), @@ -621,10 +622,10 @@ final class SwiftCompletionTests: XCTestCase { ) ) ) - XCTAssertEqual( + assertEqual( 2, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 12), @@ -633,10 +634,10 @@ final class SwiftCompletionTests: XCTestCase { ) ) ) - XCTAssertEqual( + assertEqual( 1, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 13), @@ -645,10 +646,10 @@ final class SwiftCompletionTests: XCTestCase { ) ) ) - XCTAssertEqual( + assertEqual( 0, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 14), @@ -657,10 +658,10 @@ final class SwiftCompletionTests: XCTestCase { ) ) ) - XCTAssertEqual( + assertEqual( 2, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 12), @@ -673,8 +674,8 @@ final class SwiftCompletionTests: XCTestCase { // Not valid for the current session. // We explicitly keep the session and fail any requests that don't match so that the editor // can rely on `.triggerFromIncompleteCompletions` always being fast. - XCTAssertThrowsError( - try sk.sendSync( + await assertThrowsError( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 0), @@ -682,10 +683,10 @@ final class SwiftCompletionTests: XCTestCase { ) ) ) - XCTAssertEqual( + assertEqual( 1, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 13), @@ -696,9 +697,9 @@ final class SwiftCompletionTests: XCTestCase { ) // Trigger kind changed => OK (20 is maxResults since we're outside the member completion) - XCTAssertEqual( + assertEqual( 20, - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 0), @@ -708,10 +709,13 @@ final class SwiftCompletionTests: XCTestCase { ) } - func testRefilterAfterIncompleteResultsWithEdits() throws { - try initializeServer(options: SKCompletionOptions(serverSideFiltering: true, maxResults: nil)) + func testRefilterAfterIncompleteResultsWithEdits() async throws { + let testClient = try await initializeServer( + options: SKCompletionOptions(serverSideFiltering: true, maxResults: nil) + ) let url = URL(fileURLWithPath: "/\(UUID())/a.swift") openDocument( + testClient: testClient, text: """ struct S { func fooAbc() {} @@ -728,10 +732,10 @@ final class SwiftCompletionTests: XCTestCase { ) // 'f' - XCTAssertEqual( + assertEqual( 5, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 10), @@ -742,10 +746,10 @@ final class SwiftCompletionTests: XCTestCase { ) // 'fz' - XCTAssertEqual( + assertEqual( 0, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 11), @@ -755,7 +759,7 @@ final class SwiftCompletionTests: XCTestCase { ) ) - sk.send( + testClient.send( DidChangeTextDocumentNotification( textDocument: VersionedTextDocumentIdentifier(DocumentURI(url), version: 1), contentChanges: [ @@ -765,10 +769,10 @@ final class SwiftCompletionTests: XCTestCase { ) // 'fA' - XCTAssertEqual( + assertEqual( 1, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 11), @@ -779,8 +783,8 @@ final class SwiftCompletionTests: XCTestCase { ) // 'fA ' - XCTAssertThrowsError( - try sk.sendSync( + await assertThrowsError( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 12), @@ -789,7 +793,7 @@ final class SwiftCompletionTests: XCTestCase { ) ) - sk.send( + testClient.send( DidChangeTextDocumentNotification( textDocument: VersionedTextDocumentIdentifier(DocumentURI(url), version: 1), contentChanges: [ @@ -799,10 +803,10 @@ final class SwiftCompletionTests: XCTestCase { ) // 'fAb' - XCTAssertEqual( + assertEqual( 1, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 11), @@ -812,7 +816,7 @@ final class SwiftCompletionTests: XCTestCase { ) ) - sk.send( + testClient.send( DidChangeTextDocumentNotification( textDocument: VersionedTextDocumentIdentifier(DocumentURI(url), version: 1), contentChanges: [ @@ -822,10 +826,10 @@ final class SwiftCompletionTests: XCTestCase { ) // 'fb' - XCTAssertEqual( + assertEqual( 2, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 11), @@ -835,7 +839,7 @@ final class SwiftCompletionTests: XCTestCase { ) ) - sk.send( + testClient.send( DidChangeTextDocumentNotification( textDocument: VersionedTextDocumentIdentifier(DocumentURI(url), version: 1), contentChanges: [ @@ -845,10 +849,10 @@ final class SwiftCompletionTests: XCTestCase { ) // 'fbd' - XCTAssertEqual( + assertEqual( 1, countFs( - try sk.sendSync( + try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 7, utf16index: 12), @@ -861,10 +865,13 @@ final class SwiftCompletionTests: XCTestCase { /// Regression test for https://bugs.swift.org/browse/SR-13561 to make sure the a session /// close waits for its respective open to finish to prevent a session geting stuck open. - func testSessionCloseWaitsforOpen() throws { - try initializeServer(options: SKCompletionOptions(serverSideFiltering: true, maxResults: nil)) + func testSessionCloseWaitsforOpen() async throws { + let testClient = try await initializeServer( + options: SKCompletionOptions(serverSideFiltering: true, maxResults: nil) + ) let url = URL(fileURLWithPath: "/\(UUID())/file.swift") openDocument( + testClient: testClient, text: """ struct S { func forSomethingCrazy() {} @@ -893,32 +900,17 @@ final class SwiftCompletionTests: XCTestCase { ) // Code completion for "self.forSome" - let forSomeExpectation = XCTestExpectation(description: "self.forSome code completion") - _ = sk.send(forSomeComplete) { result in - defer { forSomeExpectation.fulfill() } - guard let list = result.success else { - XCTFail("Request failed: \(String(describing: result.failure))") - return - } - XCTAssertEqual(2, countFs(list)) - } + async let forSomeResult = testClient.send(forSomeComplete) // Code completion for "self.prin", previously could immediately invalidate // the previous request. - let printExpectation = XCTestExpectation(description: "self.prin code completion") - _ = sk.send(printComplete) { result in - defer { printExpectation.fulfill() } - guard let list = result.success else { - XCTFail("Request failed: \(String(describing: result.failure))") - return - } - XCTAssertEqual(1, list.items.count) - } + async let printResult = testClient.send(printComplete) - wait(for: [forSomeExpectation, printExpectation], timeout: defaultTimeout) + assertEqual(2, countFs(try await forSomeResult)) + assertEqual(1, try await printResult.items.count) // Try code completion for "self.forSome" again to verify that it still works. - let result = try sk.sendSync(forSomeComplete) + let result = try await testClient.send(forSomeComplete) XCTAssertEqual(2, countFs(result)) } } diff --git a/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift b/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift index 5ea689eb5..1912ede87 100644 --- a/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift +++ b/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift @@ -22,21 +22,19 @@ import XCTest final class SwiftInterfaceTests: XCTestCase { - /// Connection and lifetime management for the service. - var connection: TestSourceKitServer! = nil + /// The mock client used to communicate with the SourceKit-LSP server. + /// + /// - Note: Set before each test run in `setUp`. + private var testClient: TestSourceKitLSPClient! = nil - /// The primary interface to make requests to the SourceKitServer. - var sk: TestClient! = nil - - override func setUp() { + override func setUp() async throws { // This is the only test that references modules from the SDK (Foundation). // `testSystemModuleInterface` has been flaky for a long while and a // hypothesis is that it was failing because of a malformed global module // cache that might still be present from previous CI runs. If we use a // local module cache, we define away that source of bugs. - connection = TestSourceKitServer(useGlobalModuleCache: false) - sk = connection.client - _ = try! sk.sendSync( + testClient = TestSourceKitLSPClient(useGlobalModuleCache: false) + _ = try await testClient.send( InitializeRequest( processId: nil, rootPath: nil, @@ -60,15 +58,16 @@ final class SwiftInterfaceTests: XCTestCase { } override func tearDown() { - sk = nil - connection = nil + testClient = nil } - func testSystemModuleInterface() throws { + // MARK: - Tests + + func testSystemModuleInterface() async throws { let url = URL(fileURLWithPath: "/\(UUID())/a.swift") let uri = DocumentURI(url) - sk.send( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: uri, @@ -81,7 +80,7 @@ final class SwiftInterfaceTests: XCTestCase { ) ) - let _resp = try sk.sendSync( + let _resp = try await testClient.send( DefinitionRequest( textDocument: TextDocumentIdentifier(url), position: Position(line: 0, utf16index: 10) @@ -109,8 +108,8 @@ final class SwiftInterfaceTests: XCTestCase { let importedModule = ws.testLoc("lib:import") try ws.openDocument(importedModule.url, language: .swift) let openInterface = OpenInterfaceRequest(textDocument: importedModule.docIdentifier, name: "lib", symbolUSR: nil) - let interfaceDetails = try XCTUnwrap(ws.sk.sendSync(openInterface)) - XCTAssertTrue(interfaceDetails.uri.pseudoPath.hasSuffix("/lib.swiftinterface")) + let interfaceDetails = try unwrap(await ws.testClient.send(openInterface)) + XCTAssert(interfaceDetails.uri.pseudoPath.hasSuffix("/lib.swiftinterface")) let fileContents = try XCTUnwrap( interfaceDetails.uri.fileURL.flatMap({ try String(contentsOf: $0, encoding: .utf8) }) ) @@ -134,9 +133,9 @@ final class SwiftInterfaceTests: XCTestCase { ws: SKSwiftPMTestWorkspace, swiftInterfaceFile: String, linePrefix: String - ) throws { + ) async throws { try ws.openDocument(testLoc.url, language: .swift) - let definition = try ws.sk.sendSync( + let definition = try await ws.testClient.send( DefinitionRequest( textDocument: testLoc.docIdentifier, position: testLoc.position @@ -164,21 +163,21 @@ final class SwiftInterfaceTests: XCTestCase { let withTaskGroupRef = ws.testLoc("lib.withTaskGroup") // Test stdlib with one submodule - try testSystemSwiftInterface( + try await testSystemSwiftInterface( stringRef, ws: ws, swiftInterfaceFile: "/Swift.String.swiftinterface", linePrefix: "@frozen public struct String" ) // Test stdlib with two submodules - try testSystemSwiftInterface( + try await testSystemSwiftInterface( intRef, ws: ws, swiftInterfaceFile: "/Swift.Math.Integers.swiftinterface", linePrefix: "@frozen public struct Int" ) // Test concurrency - try testSystemSwiftInterface( + try await testSystemSwiftInterface( withTaskGroupRef, ws: ws, swiftInterfaceFile: "/_Concurrency.swiftinterface", @@ -191,14 +190,13 @@ final class SwiftInterfaceTests: XCTestCase { try ws.buildAndIndex() let importedModule = ws.testLoc("lib:import") try ws.openDocument(importedModule.url, language: .swift) - let _resp = try withExtendedLifetime(ws) { - try ws.sk.sendSync( + let _resp = + try await ws.testClient.send( DefinitionRequest( textDocument: importedModule.docIdentifier, position: importedModule.position ) ) - } let resp = try XCTUnwrap(_resp) guard case .locations(let locations) = resp else { XCTFail("Unexpected response: \(resp)") diff --git a/Tests/SourceKitLSPTests/SwiftPMIntegration.swift b/Tests/SourceKitLSPTests/SwiftPMIntegration.swift index 347bfe93d..70a5eda29 100644 --- a/Tests/SourceKitLSPTests/SwiftPMIntegration.swift +++ b/Tests/SourceKitLSPTests/SwiftPMIntegration.swift @@ -23,7 +23,7 @@ final class SwiftPMIntegrationTests: XCTestCase { let call = ws.testLoc("Lib.foo:call") let def = ws.testLoc("Lib.foo:def") try ws.openDocument(call.url, language: .swift) - let refs = try ws.sk.sendSync( + let refs = try await ws.testClient.send( ReferencesRequest( textDocument: call.docIdentifier, position: call.position, @@ -39,9 +39,9 @@ final class SwiftPMIntegrationTests: XCTestCase { ] ) - let completions = try withExtendedLifetime(ws) { - try ws.sk.sendSync(CompletionRequest(textDocument: call.docIdentifier, position: call.position)) - } + let completions = try await ws.testClient.send( + CompletionRequest(textDocument: call.docIdentifier, position: call.position) + ) XCTAssertEqual( completions.items, @@ -101,23 +101,23 @@ final class SwiftPMIntegrationTests: XCTestCase { try ws.openDocument(newFile.url, language: .swift) try ws.openDocument(oldFile.url, language: .swift) - let completionsBeforeDidChangeNotification = try withExtendedLifetime(ws) { - try ws.sk.sendSync(CompletionRequest(textDocument: newFile.docIdentifier, position: newFile.position)) - } + let completionsBeforeDidChangeNotification = try await ws.testClient.send( + CompletionRequest(textDocument: newFile.docIdentifier, position: newFile.position) + ) XCTAssertEqual(completionsBeforeDidChangeNotification.items, []) ws.closeDocument(newFile.url) // Send a `DidChangeWatchedFilesNotification` and verify that we now get cross-file code completion. - ws.sk.send( + ws.testClient.send( DidChangeWatchedFilesNotification(changes: [ FileEvent(uri: newFile.docUri, type: .created) ]) ) try ws.openDocument(newFile.url, language: .swift) - let completions = try withExtendedLifetime(ws) { - try ws.sk.sendSync(CompletionRequest(textDocument: newFile.docIdentifier, position: newFile.position)) - } + let completions = try await ws.testClient.send( + CompletionRequest(textDocument: newFile.docIdentifier, position: newFile.position) + ) XCTAssertEqual( completions.items, @@ -154,9 +154,9 @@ final class SwiftPMIntegrationTests: XCTestCase { // Check that we get code completion for `baz` (defined in the new file) in the old file. // I.e. check that the existing file's build settings have been updated to include the new file. - let oldFileCompletions = try withExtendedLifetime(ws) { - try ws.sk.sendSync(CompletionRequest(textDocument: oldFile.docIdentifier, position: oldFile.position)) - } + let oldFileCompletions = try await ws.testClient.send( + CompletionRequest(textDocument: oldFile.docIdentifier, position: oldFile.position) + ) XCTAssert( oldFileCompletions.items.contains( CompletionItem( @@ -187,9 +187,9 @@ final class SwiftPMIntegrationTests: XCTestCase { // Check that we don't get cross-file code completion before we send a `DidChangeWatchedFilesNotification` to make sure we didn't include the file in the initial retrieval of build settings. try ws.openDocument(otherLib.url, language: .swift) - let completionsBeforeDidChangeNotification = try withExtendedLifetime(ws) { - try ws.sk.sendSync(CompletionRequest(textDocument: otherLib.docIdentifier, position: otherLib.position)) - } + let completionsBeforeDidChangeNotification = try await ws.testClient.send( + CompletionRequest(textDocument: otherLib.docIdentifier, position: otherLib.position) + ) XCTAssertEqual(completionsBeforeDidChangeNotification.items, []) // Add the otherlib target to Package.swift @@ -212,7 +212,7 @@ final class SwiftPMIntegrationTests: XCTestCase { } // Send a `DidChangeWatchedFilesNotification` and verify that we now get cross-file code completion. - ws.sk.send( + ws.testClient.send( DidChangeWatchedFilesNotification(changes: [ FileEvent(uri: packageTargets.docUri, type: .changed) ]) @@ -251,9 +251,9 @@ final class SwiftPMIntegrationTests: XCTestCase { // Updating the build settings takes a few seconds. Send code completion requests every second until we receive correct results. for _ in 0..<30 { - let completions = try withExtendedLifetime(ws) { - try ws.sk.sendSync(CompletionRequest(textDocument: otherLib.docIdentifier, position: otherLib.position)) - } + let completions = try await ws.testClient.send( + CompletionRequest(textDocument: otherLib.docIdentifier, position: otherLib.position) + ) if completions.items == expectedCompletions { didReceiveCorrectCompletions = true diff --git a/Tests/SourceKitLSPTests/TypeHierarchyTests.swift b/Tests/SourceKitLSPTests/TypeHierarchyTests.swift index c5f711e11..047c287aa 100644 --- a/Tests/SourceKitLSPTests/TypeHierarchyTests.swift +++ b/Tests/SourceKitLSPTests/TypeHierarchyTests.swift @@ -24,30 +24,30 @@ final class TypeHierarchyTests: XCTestCase { // Requests - func typeHierarchy(at testLoc: TestLocation) throws -> [TypeHierarchyItem] { + func typeHierarchy(at testLoc: TestLocation) async throws -> [TypeHierarchyItem] { let textDocument = testLoc.docIdentifier let request = TypeHierarchyPrepareRequest(textDocument: textDocument, position: Position(testLoc)) - let items = try ws.sk.sendSync(request) + let items = try await ws.testClient.send(request) return items ?? [] } - func supertypes(at testLoc: TestLocation) throws -> [TypeHierarchyItem] { - guard let item = try typeHierarchy(at: testLoc).first else { + func supertypes(at testLoc: TestLocation) async throws -> [TypeHierarchyItem] { + guard let item = try await typeHierarchy(at: testLoc).first else { XCTFail("Type hierarchy at \(testLoc) was empty") return [] } let request = TypeHierarchySupertypesRequest(item: item) - let types = try ws.sk.sendSync(request) + let types = try await ws.testClient.send(request) return types ?? [] } - func subtypes(at testLoc: TestLocation) throws -> [TypeHierarchyItem] { - guard let item = try typeHierarchy(at: testLoc).first else { + func subtypes(at testLoc: TestLocation) async throws -> [TypeHierarchyItem] { + guard let item = try await typeHierarchy(at: testLoc).first else { XCTFail("Type hierarchy at \(testLoc) was empty") return [] } let request = TypeHierarchySubtypesRequest(item: item) - let types = try ws.sk.sendSync(request) + let types = try await ws.testClient.send(request) return types ?? [] } @@ -105,25 +105,25 @@ final class TypeHierarchyTests: XCTestCase { // Test type hierarchy preparation assertEqualIgnoringData( - try typeHierarchy(at: testLoc("P")), + try await typeHierarchy(at: testLoc("P")), [ try item("P", .interface, at: "P") ] ) assertEqualIgnoringData( - try typeHierarchy(at: testLoc("A")), + try await typeHierarchy(at: testLoc("A")), [ try item("A", .class, at: "A") ] ) assertEqualIgnoringData( - try typeHierarchy(at: testLoc("S")), + try await typeHierarchy(at: testLoc("S")), [ try item("S", .struct, at: "S") ] ) assertEqualIgnoringData( - try typeHierarchy(at: testLoc("E")), + try await typeHierarchy(at: testLoc("E")), [ try item("E", .enum, at: "E") ] @@ -131,35 +131,35 @@ final class TypeHierarchyTests: XCTestCase { // Test supertype hierarchy - assertEqualIgnoringData(try supertypes(at: testLoc("A")), []) + assertEqualIgnoringData(try await supertypes(at: testLoc("A")), []) assertEqualIgnoringData( - try supertypes(at: testLoc("B")), + try await supertypes(at: testLoc("B")), [ try item("A", .class, at: "A"), try item("P", .interface, at: "P"), ] ) assertEqualIgnoringData( - try supertypes(at: testLoc("C")), + try await supertypes(at: testLoc("C")), [ try item("B", .class, at: "B") ] ) assertEqualIgnoringData( - try supertypes(at: testLoc("D")), + try await supertypes(at: testLoc("D")), [ try item("A", .class, at: "A") ] ) assertEqualIgnoringData( - try supertypes(at: testLoc("S")), + try await supertypes(at: testLoc("S")), [ try item("P", .interface, at: "P"), try item("X", .interface, at: "X"), // Retroactive conformance ] ) assertEqualIgnoringData( - try supertypes(at: testLoc("E")), + try await supertypes(at: testLoc("E")), [ try item("P", .interface, at: "P"), try item("Y", .interface, at: "Y"), // Retroactive conformance @@ -170,20 +170,20 @@ final class TypeHierarchyTests: XCTestCase { // Test subtype hierarchy (includes extensions) assertEqualIgnoringData( - try subtypes(at: testLoc("A")), + try await subtypes(at: testLoc("A")), [ try item("B", .class, at: "B"), try item("D", .class, at: "D"), ] ) assertEqualIgnoringData( - try subtypes(at: testLoc("B")), + try await subtypes(at: testLoc("B")), [ try item("C", .class, at: "C") ] ) assertEqualIgnoringData( - try subtypes(at: testLoc("P")), + try await subtypes(at: testLoc("P")), [ try item("B", .class, at: "B"), try item("S", .struct, at: "S"), @@ -191,32 +191,32 @@ final class TypeHierarchyTests: XCTestCase { ] ) assertEqualIgnoringData( - try subtypes(at: testLoc("E")), + try await subtypes(at: testLoc("E")), [ try item("E: Y, Z", .null, detail: "Extension at a.swift:19", at: "extE:Y,Z") ] ) assertEqualIgnoringData( - try subtypes(at: testLoc("S")), + try await subtypes(at: testLoc("S")), [ try item("S: X", .null, detail: "Extension at a.swift:15", at: "extS:X"), try item("S", .null, detail: "Extension at a.swift:16", at: "extS"), ] ) assertEqualIgnoringData( - try subtypes(at: testLoc("X")), + try await subtypes(at: testLoc("X")), [ try item("S: X", .null, detail: "Extension at a.swift:15", at: "extS:X") ] ) assertEqualIgnoringData( - try subtypes(at: testLoc("Y")), + try await subtypes(at: testLoc("Y")), [ try item("E: Y, Z", .null, detail: "Extension at a.swift:19", at: "extE:Y,Z") ] ) assertEqualIgnoringData( - try subtypes(at: testLoc("Z")), + try await subtypes(at: testLoc("Z")), [ try item("E: Y, Z", .null, detail: "Extension at a.swift:19", at: "extE:Y,Z") ] @@ -229,9 +229,9 @@ final class TypeHierarchyTests: XCTestCase { let declLoc = testLoc(name) let occurLoc = testLoc(occurrence + name) - try assertEqualIgnoringData(typeHierarchy(at: occurLoc), typeHierarchy(at: declLoc)) - try assertEqualIgnoringData(supertypes(at: occurLoc), supertypes(at: declLoc)) - try assertEqualIgnoringData(subtypes(at: occurLoc), subtypes(at: declLoc)) + try assertEqualIgnoringData(await typeHierarchy(at: occurLoc), await typeHierarchy(at: declLoc)) + try assertEqualIgnoringData(await supertypes(at: occurLoc), await supertypes(at: declLoc)) + try assertEqualIgnoringData(await subtypes(at: occurLoc), await subtypes(at: declLoc)) } } } diff --git a/Tests/SourceKitLSPTests/WorkspaceTests.swift b/Tests/SourceKitLSPTests/WorkspaceTests.swift index 619f9e9c2..ef33b9f93 100644 --- a/Tests/SourceKitLSPTests/WorkspaceTests.swift +++ b/Tests/SourceKitLSPTests/WorkspaceTests.swift @@ -25,20 +25,21 @@ final class WorkspaceTests: XCTestCase { guard let ws = try await staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage") else { return } try ws.buildAndIndex() - guard let otherWs = try await staticSourceKitSwiftPMWorkspace(name: "OtherSwiftPMPackage", server: ws.testServer) + guard + let otherWs = try await staticSourceKitSwiftPMWorkspace(name: "OtherSwiftPMPackage", testClient: ws.testClient) else { return } try otherWs.buildAndIndex() - assert(ws.testServer === otherWs.testServer, "Sanity check: The two workspaces should be opened in the same server") + assert(ws.testClient === otherWs.testClient, "Sanity check: The two workspaces should be opened in the same server") let call = ws.testLoc("Lib.foo:call") let otherCall = otherWs.testLoc("FancyLib.sayHello:call") try ws.openDocument(call.url, language: .swift) - let completions = try withExtendedLifetime(ws) { - try ws.sk.sendSync(CompletionRequest(textDocument: call.docIdentifier, position: call.position)) - } + let completions = try await ws.testClient.send( + CompletionRequest(textDocument: call.docIdentifier, position: call.position) + ) XCTAssertEqual( completions.items, @@ -74,9 +75,9 @@ final class WorkspaceTests: XCTestCase { try ws.openDocument(otherCall.url, language: .swift) - let otherCompletions = try withExtendedLifetime(ws) { - try ws.sk.sendSync(CompletionRequest(textDocument: otherCall.docIdentifier, position: otherCall.position)) - } + let otherCompletions = try await ws.testClient.send( + CompletionRequest(textDocument: otherCall.docIdentifier, position: otherCall.position) + ) XCTAssertEqual( otherCompletions.items, @@ -121,22 +122,16 @@ final class WorkspaceTests: XCTestCase { let loc = ws.testLoc("main_file") - let expectation = self.expectation(description: "diagnostics") - - ws.sk.handleNextNotification { (note: Notification) in - XCTAssertEqual(note.params.diagnostics.count, 0) - expectation.fulfill() - } - try ws.openDocument(loc.url, language: .objective_c) - try await fulfillmentOfOrThrow([expectation]) + let diags = try await ws.testClient.nextDiagnosticsNotification() + XCTAssertEqual(diags.diagnostics.count, 0) let otherWs = try await staticSourceKitTibsWorkspace( name: "ClangCrashRecoveryBuildSettings", - server: ws.testServer + testClient: ws.testClient )! - assert(ws.testServer === otherWs.testServer, "Sanity check: The two workspaces should be opened in the same server") + assert(ws.testClient === otherWs.testClient, "Sanity check: The two workspaces should be opened in the same server") let otherLoc = otherWs.testLoc("loc") try otherWs.openDocument(otherLoc.url, language: .cpp) @@ -152,7 +147,7 @@ final class WorkspaceTests: XCTestCase { textDocument: otherLoc.docIdentifier, position: Position(line: 9, utf16index: 3) ) - let highlightResponse = try otherWs.sk.sendSync(highlightRequest) + let highlightResponse = try await otherWs.testClient.send(highlightRequest) XCTAssertEqual(highlightResponse, expectedHighlightResponse) } @@ -160,12 +155,13 @@ final class WorkspaceTests: XCTestCase { guard let otherWs = try await staticSourceKitSwiftPMWorkspace(name: "OtherSwiftPMPackage") else { return } try otherWs.buildAndIndex() - guard let ws = try await staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage", server: otherWs.testServer) else { + guard let ws = try await staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage", testClient: otherWs.testClient) + else { return } try ws.buildAndIndex() - assert(ws.testServer === otherWs.testServer, "Sanity check: The two workspaces should be opened in the same server") + assert(ws.testClient === otherWs.testClient, "Sanity check: The two workspaces should be opened in the same server") let otherLib = ws.testLoc("OtherLib.topLevelFunction:libMember") let packageTargets = ws.testLoc("Package.swift:targets") @@ -177,7 +173,7 @@ final class WorkspaceTests: XCTestCase { // to OtherSwiftPMPackage by default (because it provides fallback build // settings for it). assertEqual( - await ws.testServer.server!.workspaceForDocument(uri: otherLib.docUri)?.rootUri, + await ws.testClient.server.workspaceForDocument(uri: otherLib.docUri)?.rootUri, DocumentURI(otherWs.sources.rootDirectory) ) @@ -200,7 +196,7 @@ final class WorkspaceTests: XCTestCase { builder.write(packageManifestContents, to: packageManifest) } - ws.sk.send( + ws.testClient.send( DidChangeWatchedFilesNotification(changes: [ FileEvent(uri: packageTargets.docUri, type: .changed) ]) @@ -215,7 +211,7 @@ final class WorkspaceTests: XCTestCase { // Updating the build settings takes a few seconds. Send code completion requests every second until we receive correct results. for _ in 0..<30 { - if await ws.testServer.server!.workspaceForDocument(uri: otherLib.docUri)?.rootUri + if await ws.testClient.server.workspaceForDocument(uri: otherLib.docUri)?.rootUri == DocumentURI(ws.sources.rootDirectory) { didReceiveCorrectWorkspaceMembership = true @@ -237,19 +233,9 @@ final class WorkspaceTests: XCTestCase { try ws.openDocument(swiftLoc.url, language: .swift) try ws.openDocument(cLoc.url, language: .c) - let receivedResponse = self.expectation(description: "Received completion response") - - _ = ws.sk.send(CompletionRequest(textDocument: cLoc.docIdentifier, position: cLoc.position)) { result in - defer { - receivedResponse.fulfill() - } - guard case .success(_) = result else { - XCTFail("Expected a successful response") - return - } + await assertNoThrow { + _ = try await ws.testClient.send(CompletionRequest(textDocument: cLoc.docIdentifier, position: cLoc.position)) } - - try await fulfillmentOfOrThrow([receivedResponse]) } func testChangeWorkspaceFolders() async throws { @@ -265,9 +251,8 @@ final class WorkspaceTests: XCTestCase { let otherPackLoc = ws.testLoc("otherPackage:call") - let testServer = TestSourceKitServer(connectionKind: .local) - let sk = testServer.client - _ = try sk.sendSync( + let testClient = TestSourceKitLSPClient() + _ = try await testClient.send( InitializeRequest( rootURI: nil, capabilities: ClientCapabilities(workspace: .init(workspaceFolders: true)), @@ -279,7 +264,7 @@ final class WorkspaceTests: XCTestCase { let docString = try String(data: Data(contentsOf: otherPackLoc.url), encoding: .utf8)! - sk.send( + testClient.send( DidOpenTextDocumentNotification( textDocument: TextDocumentItem( uri: otherPackLoc.docUri, @@ -290,7 +275,7 @@ final class WorkspaceTests: XCTestCase { ) ) - let preChangeWorkspaceResponse = try sk.sendSync( + let preChangeWorkspaceResponse = try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(otherPackLoc.docUri), position: otherPackLoc.position @@ -303,7 +288,7 @@ final class WorkspaceTests: XCTestCase { "Did not expect to receive cross-module code completion results if we opened the parent directory of the package" ) - sk.send( + testClient.send( DidChangeWorkspaceFoldersNotification( event: WorkspaceFoldersChangeEvent(added: [ WorkspaceFolder(uri: DocumentURI(ws.sources.rootDirectory)) @@ -311,7 +296,7 @@ final class WorkspaceTests: XCTestCase { ) ) - let postChangeWorkspaceResponse = try sk.sendSync( + let postChangeWorkspaceResponse = try await testClient.send( CompletionRequest( textDocument: TextDocumentIdentifier(otherPackLoc.docUri), position: otherPackLoc.position