Skip to content

Commit 420dc8a

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 af37a84 commit 420dc8a

File tree

10 files changed

+63
-29
lines changed

10 files changed

+63
-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
@@ -24,6 +24,8 @@ import SKSupport
2424
/// *Implementors* are expected to handle initialization and shutdown, e.g. during `init` and
2525
/// `deinit` or by wrapping an existing sourcekitd session that outlives this object.
2626
public protocol SourceKitD: AnyObject, Sendable {
27+
var testHooks: SourceKitDTestHooks { get }
28+
2729
/// The sourcekitd API functions.
2830
var api: sourcekitd_api_functions_t { get }
2931

@@ -89,18 +91,20 @@ extension SourceKitD {
8991
/// - req: The request to send to sourcekitd.
9092
/// - fileContents: The contents of the file that the request operates on. If sourcekitd crashes, the file contents
9193
/// will be logged.
92-
public func send(_ req: SKDRequestDictionary, fileContents: String?) async throws -> SKDResponseDictionary {
93-
log(request: req)
94+
public func send(_ request: SKDRequestDictionary, fileContents: String?) async throws -> SKDResponseDictionary {
95+
log(request: request)
96+
97+
testHooks.sourcekitdRequestDidStart?(request)
9498

9599
let sourcekitdResponse: SKDResponse = try await withCancellableCheckedThrowingContinuation { continuation in
96100
var handle: sourcekitd_api_request_handle_t? = nil
97-
api.send_request(req.dict, &handle) { response in
101+
api.send_request(request.dict, &handle) { response in
98102
continuation.resume(returning: SKDResponse(response!, sourcekitd: self))
99103
}
100104
return handle
101105
} cancel: { handle in
102106
if let handle {
103-
logRequestCancellation(request: req)
107+
logRequestCancellation(request: request)
104108
api.cancel_request(handle)
105109
}
106110
}
@@ -109,7 +113,7 @@ extension SourceKitD {
109113

110114
guard let dict = sourcekitdResponse.value else {
111115
if sourcekitdResponse.error == .connectionInterrupted {
112-
log(crashedRequest: req, fileContents: fileContents)
116+
log(crashedRequest: request, fileContents: fileContents)
113117
}
114118
throw sourcekitdResponse.error!
115119
}

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: [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: [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: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -312,16 +312,24 @@ final class PullDiagnosticsTests: XCTestCase {
312312
func testDontReturnEmptyDiagnosticsIfDiagnosticRequestIsCancelled() async throws {
313313
let diagnosticRequestCancelled = self.expectation(description: "diagnostic request cancelled")
314314
var serverOptions = SourceKitLSPServer.Options.testDefault
315-
serverOptions.indexTestHooks.preparationTaskDidStart = { _ in
316-
await self.fulfillment(of: [diagnosticRequestCancelled], timeout: defaultTimeout)
315+
serverOptions.sourcekitdTestHooks.sourcekitdRequestDidStart = { request in
316+
guard request.description.contains("source.request.diagnostics") else {
317+
return
318+
}
319+
self.wait(for: [diagnosticRequestCancelled], timeout: defaultTimeout)
320+
// Poll until the `CancelRequestNotification` has been propagated to the request handling.
321+
for _ in 0..<Int(defaultTimeout * 100) {
322+
if Task.isCancelled {
323+
break
324+
}
325+
usleep(10_000)
326+
}
317327
}
318328
let project = try await SwiftPMTestProject(
319329
files: [
320330
"Lib.swift": "let x: String = 1"
321331
],
322-
serverOptions: serverOptions,
323-
enableBackgroundIndexing: true,
324-
pollIndex: false
332+
serverOptions: serverOptions
325333
)
326334
let (uri, _) = try project.openDocument("Lib.swift")
327335

0 commit comments

Comments
 (0)