Skip to content

Commit 37e6a8a

Browse files
committed
Wait for cancellation to propagate in testDontReturnEmptyDiagnosticsIfDiagnosticRequestIsCancelled
I saw a few non-deterministic test failures. I think the issue was that handling of the `CancelRequestNotification` is done asynchronously, which left a short window in which the sourcekitd diagnostics request could run and return results instead of being cancelled. Wait for the diagnostic request to actually be cancelled before running it for real. While doing this, also introduce proper sourcekitd test hooks instead of relying on the preparation test hooks, which just got run as a side effect.
1 parent e4d8331 commit 37e6a8a

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
@@ -25,17 +25,27 @@ extension sourcekitd_api_keys: @unchecked Sendable {}
2525
extension sourcekitd_api_requests: @unchecked Sendable {}
2626
extension sourcekitd_api_values: @unchecked Sendable {}
2727

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

3745
/// The handle to the dylib.
38-
let dylib: DLHandle
46+
private let dylib: DLHandle
47+
48+
public let testHooks: SourceKitDTestHooks
3949

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

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

62-
init(dylib path: AbsolutePath) throws {
76+
init(dylib path: AbsolutePath, testHooks: SourceKitDTestHooks) throws {
6377
self.path = path
6478
#if os(Windows)
6579
self.dylib = try dlopen(path.pathString, mode: [])
6680
#else
6781
self.dylib = try dlopen(path.pathString, mode: [.lazy, .local, .first])
6882
#endif
83+
self.testHooks = testHooks
6984
self.api = try sourcekitd_api_functions_t(self.dylib)
7085
self.keys = sourcekitd_api_keys(api: self.api)
7186
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
@@ -30,6 +30,8 @@ extension sourcekitd_api_request_handle_t: @unchecked Sendable {}
3030
/// *Implementors* are expected to handle initialization and shutdown, e.g. during `init` and
3131
/// `deinit` or by wrapping an existing sourcekitd session that outlives this object.
3232
public protocol SourceKitD: AnyObject, Sendable {
33+
var testHooks: SourceKitDTestHooks { get }
34+
3335
/// The sourcekitd API functions.
3436
var api: sourcekitd_api_functions_t { get }
3537

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

101105
let sourcekitdResponse: SKDResponse = try await withCancellableCheckedThrowingContinuation { continuation in
102106
var handle: sourcekitd_api_request_handle_t? = nil
103-
api.send_request(req.dict, &handle) { response in
107+
api.send_request(request.dict, &handle) { response in
104108
continuation.resume(returning: SKDResponse(response!, sourcekitd: self))
105109
}
106110
return handle
107111
} cancel: { handle in
108112
if let handle {
109-
logRequestCancellation(request: req)
113+
logRequestCancellation(request: request)
110114
api.cancel_request(handle)
111115
}
112116
}
@@ -115,7 +119,7 @@ extension SourceKitD {
115119

116120
guard let dict = sourcekitdResponse.value else {
117121
if sourcekitdResponse.error == .connectionInterrupted {
118-
log(crashedRequest: req, fileContents: fileContents)
122+
log(crashedRequest: request, fileContents: fileContents)
119123
}
120124
throw sourcekitdResponse.error!
121125
}

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
@@ -194,7 +194,10 @@ public actor SwiftLanguageService: LanguageService, Sendable {
194194
guard let sourcekitd = toolchain.sourcekitd else { return nil }
195195
self.sourceKitLSPServer = sourceKitLSPServer
196196
self.swiftFormat = toolchain.swiftFormat
197-
self.sourcekitd = try await DynamicallyLoadedSourceKitD.getOrCreate(dylibPath: sourcekitd)
197+
self.sourcekitd = try await DynamicallyLoadedSourceKitD.getOrCreate(
198+
dylibPath: sourcekitd,
199+
testHooks: options.sourcekitdTestHooks
200+
)
198201
self.capabilityRegistry = workspace.capabilityRegistry
199202
self.semanticIndexManager = workspace.semanticIndexManager
200203
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)