Skip to content

Commit 5ac5b56

Browse files
authored
Merge pull request #1412 from ahoppen/wait-for-prepration-to-propagate
Wait for cancellation to propagate in `testDontReturnEmptyDiagnosticsIfDiagnosticRequestIsCancelled`
2 parents 9c13dbb + 37e6a8a commit 5ac5b56

File tree

10 files changed

+66
-29
lines changed

10 files changed

+66
-29
lines changed

Sources/Diagnose/RunSourcekitdRequestCommand.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ public struct RunSourceKitdRequestCommand: AsyncParsableCommand {
5555
throw ExitCode(1)
5656
}
5757
let sourcekitd = try await DynamicallyLoadedSourceKitD.getOrCreate(
58-
dylibPath: try! AbsolutePath(validating: sourcekitdPath)
58+
dylibPath: try! AbsolutePath(validating: sourcekitdPath),
59+
testHooks: SourceKitDTestHooks()
5960
)
6061

6162
if let lineColumn = position?.split(separator: ":", maxSplits: 2).map(Int.init),

Sources/SourceKitD/DynamicallyLoadedSourceKitD.swift

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,27 @@ extension sourcekitd_api_keys: @unchecked Sendable {}
2626
extension sourcekitd_api_requests: @unchecked Sendable {}
2727
extension sourcekitd_api_values: @unchecked Sendable {}
2828

29+
public struct SourceKitDTestHooks: Sendable {
30+
public var sourcekitdRequestDidStart: (@Sendable (SKDRequestDictionary) -> Void)?
31+
32+
public init(sourcekitdRequestDidStart: (@Sendable (SKDRequestDictionary) -> Void)? = nil) {
33+
self.sourcekitdRequestDidStart = sourcekitdRequestDidStart
34+
}
35+
}
36+
2937
/// Wrapper for sourcekitd, taking care of initialization, shutdown, and notification handler
3038
/// multiplexing.
3139
///
3240
/// Users of this class should not call the api functions `initialize`, `shutdown`, or
3341
/// `set_notification_handler`, which are global state managed internally by this class.
3442
public actor DynamicallyLoadedSourceKitD: SourceKitD {
3543
/// The path to the sourcekitd dylib.
36-
public let path: AbsolutePath
44+
private let path: AbsolutePath
3745

3846
/// The handle to the dylib.
39-
let dylib: DLHandle
47+
private let dylib: DLHandle
48+
49+
public let testHooks: SourceKitDTestHooks
4050

4151
/// The sourcekitd API functions.
4252
public let api: sourcekitd_api_functions_t
@@ -55,18 +65,23 @@ public actor DynamicallyLoadedSourceKitD: SourceKitD {
5565
/// List of notification handlers that will be called for each notification.
5666
private var notificationHandlers: [WeakSKDNotificationHandler] = []
5767

58-
public static func getOrCreate(dylibPath: AbsolutePath) async throws -> SourceKitD {
68+
/// If there is already a `sourcekitd` instance from the given return it, otherwise create a new one.
69+
///
70+
/// `testHooks` are only considered when an instance is being created. If a sourcekitd instance at the given path
71+
/// already exists, its test hooks will be used.
72+
public static func getOrCreate(dylibPath: AbsolutePath, testHooks: SourceKitDTestHooks) async throws -> SourceKitD {
5973
try await SourceKitDRegistry.shared
60-
.getOrAdd(dylibPath, create: { try DynamicallyLoadedSourceKitD(dylib: dylibPath) })
74+
.getOrAdd(dylibPath, create: { try DynamicallyLoadedSourceKitD(dylib: dylibPath, testHooks: testHooks) })
6175
}
6276

63-
init(dylib path: AbsolutePath) throws {
77+
init(dylib path: AbsolutePath, testHooks: SourceKitDTestHooks) throws {
6478
self.path = path
6579
#if os(Windows)
6680
self.dylib = try dlopen(path.pathString, mode: [])
6781
#else
6882
self.dylib = try dlopen(path.pathString, mode: [.lazy, .local, .first])
6983
#endif
84+
self.testHooks = testHooks
7085
self.api = try sourcekitd_api_functions_t(self.dylib)
7186
self.keys = sourcekitd_api_keys(api: self.api)
7287
self.requests = sourcekitd_api_requests(api: self.api)

Sources/SourceKitD/SourceKitD.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ extension sourcekitd_api_request_handle_t: @unchecked Sendable {}
3131
/// *Implementors* are expected to handle initialization and shutdown, e.g. during `init` and
3232
/// `deinit` or by wrapping an existing sourcekitd session that outlives this object.
3333
public protocol SourceKitD: AnyObject, Sendable {
34+
var testHooks: SourceKitDTestHooks { get }
35+
3436
/// The sourcekitd API functions.
3537
var api: sourcekitd_api_functions_t { get }
3638

@@ -96,18 +98,20 @@ extension SourceKitD {
9698
/// - req: The request to send to sourcekitd.
9799
/// - fileContents: The contents of the file that the request operates on. If sourcekitd crashes, the file contents
98100
/// will be logged.
99-
public func send(_ req: SKDRequestDictionary, fileContents: String?) async throws -> SKDResponseDictionary {
100-
log(request: req)
101+
public func send(_ request: SKDRequestDictionary, fileContents: String?) async throws -> SKDResponseDictionary {
102+
log(request: request)
103+
104+
testHooks.sourcekitdRequestDidStart?(request)
101105

102106
let sourcekitdResponse: SKDResponse = try await withCancellableCheckedThrowingContinuation { continuation in
103107
var handle: sourcekitd_api_request_handle_t? = nil
104-
api.send_request(req.dict, &handle) { response in
108+
api.send_request(request.dict, &handle) { response in
105109
continuation.resume(returning: SKDResponse(response!, sourcekitd: self))
106110
}
107111
return handle
108112
} cancel: { handle in
109113
if let handle {
110-
logRequestCancellation(request: req)
114+
logRequestCancellation(request: request)
111115
api.cancel_request(handle)
112116
}
113117
}
@@ -116,7 +120,7 @@ extension SourceKitD {
116120

117121
guard let dict = sourcekitdResponse.value else {
118122
if sourcekitdResponse.error == .connectionInterrupted {
119-
log(crashedRequest: req, fileContents: fileContents)
123+
log(crashedRequest: request, fileContents: fileContents)
120124
}
121125
throw sourcekitdResponse.error!
122126
}

Sources/SourceKitD/SourceKitDRegistry.swift

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ extension NSLock {
3535
public actor SourceKitDRegistry {
3636

3737
/// Mapping from path to active SourceKitD instance.
38-
var active: [AbsolutePath: SourceKitD] = [:]
38+
private var active: [AbsolutePath: SourceKitD] = [:]
3939

4040
/// Instances that have been unregistered, but may be resurrected if accessed before destruction.
41-
var cemetary: [AbsolutePath: WeakSourceKitD] = [:]
41+
private var cemetary: [AbsolutePath: WeakSourceKitD] = [:]
4242

4343
/// Initialize an empty registry.
4444
public init() {}
@@ -79,14 +79,8 @@ public actor SourceKitDRegistry {
7979
}
8080
return existing
8181
}
82-
83-
/// Remove all SourceKitD instances, including weak ones.
84-
public func clear() {
85-
active.removeAll()
86-
cemetary.removeAll()
87-
}
8882
}
8983

90-
struct WeakSourceKitD {
84+
fileprivate struct WeakSourceKitD {
9185
weak var value: SourceKitD?
9286
}

Sources/SourceKitLSP/SourceKitLSPServer+Options.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import LanguageServerProtocol
1515
import SKCore
1616
import SKSupport
1717
import SemanticIndex
18+
import SourceKitD
1819

1920
import struct TSCBasic.AbsolutePath
2021
import struct TSCBasic.RelativePath
2122

2223
extension SourceKitLSPServer {
23-
2424
/// Configuration options for the SourceKitServer.
2525
public struct Options: Sendable {
2626
/// Additional compiler flags (e.g. `-Xswiftc` for SwiftPM projects) and other build-related
@@ -52,6 +52,8 @@ extension SourceKitLSPServer {
5252
/// Experimental features that are enabled.
5353
public var experimentalFeatures: Set<ExperimentalFeature>
5454

55+
public var sourcekitdTestHooks: SourceKitDTestHooks
56+
5557
public var indexTestHooks: IndexTestHooks
5658

5759
public init(
@@ -63,6 +65,7 @@ extension SourceKitLSPServer {
6365
generatedInterfacesPath: AbsolutePath = defaultDirectoryForGeneratedInterfaces,
6466
swiftPublishDiagnosticsDebounceDuration: TimeInterval = 2, /* 2s */
6567
experimentalFeatures: Set<ExperimentalFeature> = [],
68+
sourcekitdTestHooks: SourceKitDTestHooks = SourceKitDTestHooks(),
6669
indexTestHooks: IndexTestHooks = IndexTestHooks()
6770
) {
6871
self.buildSetup = buildSetup
@@ -73,6 +76,7 @@ extension SourceKitLSPServer {
7376
self.generatedInterfacesPath = generatedInterfacesPath
7477
self.swiftPublishDiagnosticsDebounceDuration = swiftPublishDiagnosticsDebounceDuration
7578
self.experimentalFeatures = experimentalFeatures
79+
self.sourcekitdTestHooks = sourcekitdTestHooks
7680
self.indexTestHooks = indexTestHooks
7781
}
7882
}

Sources/SourceKitLSP/Swift/SwiftLanguageService.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,10 @@ public actor SwiftLanguageService: LanguageService, Sendable {
195195
guard let sourcekitd = toolchain.sourcekitd else { return nil }
196196
self.sourceKitLSPServer = sourceKitLSPServer
197197
self.swiftFormat = toolchain.swiftFormat
198-
self.sourcekitd = try await DynamicallyLoadedSourceKitD.getOrCreate(dylibPath: sourcekitd)
198+
self.sourcekitd = try await DynamicallyLoadedSourceKitD.getOrCreate(
199+
dylibPath: sourcekitd,
200+
testHooks: options.sourcekitdTestHooks
201+
)
199202
self.capabilityRegistry = workspace.capabilityRegistry
200203
self.semanticIndexManager = workspace.semanticIndexManager
201204
self.serverOptions = options

Tests/DiagnoseTests/DiagnoseTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,8 @@ private class InProcessSourceKitRequestExecutor: SourceKitRequestExecutor {
318318
logger.info("Sending request: \(requestString)")
319319

320320
let sourcekitd = try await DynamicallyLoadedSourceKitD.getOrCreate(
321-
dylibPath: try! AbsolutePath(validating: sourcekitd.path)
321+
dylibPath: try! AbsolutePath(validating: sourcekitd.path),
322+
testHooks: SourceKitDTestHooks()
322323
)
323324
let response = try await sourcekitd.run(requestYaml: requestString)
324325

Tests/SourceKitDTests/SourceKitDRegistryTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ private nonisolated(unsafe) var nextToken = AtomicUInt32(initialValue: 0)
6363

6464
final class FakeSourceKitD: SourceKitD {
6565
let token: UInt32
66+
var testHooks: SourceKitDTestHooks { SourceKitDTestHooks() }
6667
var api: sourcekitd_api_functions_t { fatalError() }
6768
var keys: sourcekitd_api_keys { fatalError() }
6869
var requests: sourcekitd_api_requests { fatalError() }

Tests/SourceKitDTests/SourceKitDTests.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ import class TSCBasic.Process
2828
final class SourceKitDTests: XCTestCase {
2929
func testMultipleNotificationHandlers() async throws {
3030
let sourcekitdPath = await ToolchainRegistry.forTesting.default!.sourcekitd!
31-
let sourcekitd = try await DynamicallyLoadedSourceKitD.getOrCreate(dylibPath: sourcekitdPath)
31+
let sourcekitd = try await DynamicallyLoadedSourceKitD.getOrCreate(
32+
dylibPath: sourcekitdPath,
33+
testHooks: SourceKitDTestHooks()
34+
)
3235
let keys = sourcekitd.keys
3336
let path = DocumentURI(for: .swift).pseudoPath
3437

Tests/SourceKitLSPTests/PullDiagnosticsTests.swift

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -310,18 +310,28 @@ final class PullDiagnosticsTests: XCTestCase {
310310
}
311311

312312
func testDontReturnEmptyDiagnosticsIfDiagnosticRequestIsCancelled() async throws {
313+
let diagnosticSourcekitdRequestDidStart = self.expectation(description: "diagnostic sourcekitd request did start")
313314
let diagnosticRequestCancelled = self.expectation(description: "diagnostic request cancelled")
314315
var serverOptions = SourceKitLSPServer.Options.testDefault
315-
serverOptions.indexTestHooks.preparationTaskDidStart = { _ in
316-
await self.fulfillment(of: [diagnosticRequestCancelled], timeout: defaultTimeout)
316+
serverOptions.sourcekitdTestHooks.sourcekitdRequestDidStart = { request in
317+
guard request.description.contains("source.request.diagnostics") else {
318+
return
319+
}
320+
diagnosticSourcekitdRequestDidStart.fulfill()
321+
self.wait(for: [diagnosticRequestCancelled], timeout: defaultTimeout)
322+
// Poll until the `CancelRequestNotification` has been propagated to the request handling.
323+
for _ in 0..<Int(defaultTimeout * 100) {
324+
if Task.isCancelled {
325+
break
326+
}
327+
usleep(10_000)
328+
}
317329
}
318330
let project = try await SwiftPMTestProject(
319331
files: [
320332
"Lib.swift": "let x: String = 1"
321333
],
322-
serverOptions: serverOptions,
323-
enableBackgroundIndexing: true,
324-
pollIndex: false
334+
serverOptions: serverOptions
325335
)
326336
let (uri, _) = try project.openDocument("Lib.swift")
327337

@@ -332,6 +342,7 @@ final class PullDiagnosticsTests: XCTestCase {
332342
XCTAssertEqual(result, .failure(ResponseError.cancelled))
333343
diagnosticResponseReceived.fulfill()
334344
}
345+
try await fulfillmentOfOrThrow([diagnosticSourcekitdRequestDidStart])
335346
project.testClient.send(CancelRequestNotification(id: requestID))
336347
diagnosticRequestCancelled.fulfill()
337348
try await fulfillmentOfOrThrow([diagnosticResponseReceived])

0 commit comments

Comments
 (0)