Skip to content

Commit 4883ee7

Browse files
authored
Merge pull request #2088 from ahoppen/cancellation-issue
Wait for request to be sent to sourcekitd before cancelling it in `SwiftSourceKitPluginTests.testCancellation`
2 parents 7eba708 + 4a4c114 commit 4883ee7

File tree

11 files changed

+85
-26
lines changed

11 files changed

+85
-26
lines changed

Sources/SKTestSupport/Assertions.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,26 @@ package func assertNotNil<T>(
119119
XCTAssertNotNil(expression, message(), file: file, line: line)
120120
}
121121

122+
/// Check that the string contains the given substring.
123+
package func assertContains(
124+
_ string: some StringProtocol,
125+
_ substring: some StringProtocol,
126+
file: StaticString = #filePath,
127+
line: UInt = #line
128+
) {
129+
XCTAssert(string.contains(substring), "Expected to contain '\(substring)': \(string)", file: file, line: line)
130+
}
131+
132+
/// Check that the sequence contains the given element.
133+
package func assertContains<Element: Equatable>(
134+
_ sequence: some Sequence<Element>,
135+
_ element: Element,
136+
file: StaticString = #filePath,
137+
line: UInt = #line
138+
) {
139+
XCTAssert(sequence.contains(element), "Expected to contain '\(element)': \(sequence)", file: file, line: line)
140+
}
141+
122142
/// Same as `XCTUnwrap` but doesn't take autoclosures and thus `expression`
123143
/// can contain `await`.
124144
package func unwrap<T>(

Sources/SourceKitD/DynamicallyLoadedSourceKitD.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ package actor DynamicallyLoadedSourceKitD: SourceKitD {
108108
/// List of notification handlers that will be called for each notification.
109109
private var notificationHandlers: [WeakSKDNotificationHandler] = []
110110

111+
/// List of hooks that should be executed for every request sent to sourcekitd.
112+
private var requestHandlingHooks: [UUID: (SKDRequestDictionary) -> Void] = [:]
113+
111114
package static func getOrCreate(
112115
dylibPath: URL,
113116
pluginPaths: PluginPaths?
@@ -204,6 +207,29 @@ package actor DynamicallyLoadedSourceKitD: SourceKitD {
204207
notificationHandlers.removeAll(where: { $0.value == nil || $0.value === handler })
205208
}
206209

210+
/// Execute `body` and invoke `hook` for every sourcekitd request that is sent during the execution time of `body`.
211+
///
212+
/// Note that `hook` will not only be executed for requests sent *by* body but this may also include sourcekitd
213+
/// requests that were sent by other clients of the same `DynamicallyLoadedSourceKitD` instance that just happen to
214+
/// send a request during that time.
215+
///
216+
/// This is intended for testing only.
217+
package func withRequestHandlingHook(
218+
body: () async throws -> Void,
219+
hook: @escaping (SKDRequestDictionary) -> Void
220+
) async rethrows {
221+
let id = UUID()
222+
requestHandlingHooks[id] = hook
223+
defer { requestHandlingHooks[id] = nil }
224+
try await body()
225+
}
226+
227+
package func didSend(request: SKDRequestDictionary) {
228+
for hook in requestHandlingHooks.values {
229+
hook(request)
230+
}
231+
}
232+
207233
package nonisolated func log(request: SKDRequestDictionary) {
208234
logger.info(
209235
"""

Sources/SourceKitD/SourceKitD.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ package protocol SourceKitD: AnyObject, Sendable {
8383
/// Removes a previously registered notification handler.
8484
func removeNotificationHandler(_ handler: SKDNotificationHandler) async
8585

86+
/// A function that gets called after the request has been sent to sourcekitd but but does not wait for results to be
87+
/// received. This can be used by clients to implement hooks that should be executed for every sourcekitd request.
88+
func didSend(request: SKDRequestDictionary) async
89+
8690
/// Log the given request.
8791
///
8892
/// This log call is issued during normal operation. It is acceptable for the logger to truncate the log message
@@ -146,6 +150,9 @@ extension SourceKitD {
146150
self.api.send_request(request.dict, &handle) { response in
147151
continuation.resume(returning: SKDResponse(response!, sourcekitd: self))
148152
}
153+
Task {
154+
await self.didSend(request: request)
155+
}
149156
if let handle {
150157
return SourceKitDRequestHandle(handle: handle)
151158
}

Tests/BuildSystemIntegrationTests/BuildServerBuildSystemTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ final class BuildServerBuildSystemTests: XCTestCase {
501501
allowFallbackSettings: false
502502
)
503503
)
504-
XCTAssert(try XCTUnwrap(firstOptions).compilerArguments.contains("-DFIRST"))
504+
assertContains(try XCTUnwrap(firstOptions).compilerArguments, "-DFIRST")
505505

506506
let secondOptions = try await project.testClient.send(
507507
SourceKitOptionsRequest(
@@ -511,7 +511,7 @@ final class BuildServerBuildSystemTests: XCTestCase {
511511
allowFallbackSettings: false
512512
)
513513
)
514-
XCTAssert(try XCTUnwrap(secondOptions).compilerArguments.contains("-DSECOND"))
514+
assertContains(try XCTUnwrap(secondOptions).compilerArguments, "-DSECOND")
515515

516516
let optionsWithoutTarget = try await project.testClient.send(
517517
SourceKitOptionsRequest(
@@ -521,7 +521,7 @@ final class BuildServerBuildSystemTests: XCTestCase {
521521
)
522522
)
523523
// We currently pick the canonical target alphabetically, which means that `bsp://first` wins over `bsp://second`
524-
XCTAssert(try XCTUnwrap(optionsWithoutTarget).compilerArguments.contains("-DFIRST"))
524+
assertContains(try XCTUnwrap(optionsWithoutTarget).compilerArguments, "-DFIRST")
525525
}
526526

527527
func testDontBlockBuildServerInitializationIfBuildSystemIsUnresponsive() async throws {

Tests/BuildSystemIntegrationTests/BuildSystemManagerTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ final class BuildSystemManagerTests: XCTestCase {
116116
await manager.registerForChangeNotifications(for: d, language: .c)
117117
await assertEqual(manager.cachedMainFile(for: a), c)
118118
let bMain = await manager.cachedMainFile(for: b)
119-
XCTAssert(Set([c, d]).contains(bMain))
119+
assertContains([c, d], bMain)
120120
await assertEqual(manager.cachedMainFile(for: c), c)
121121
await assertEqual(manager.cachedMainFile(for: d), d)
122122

Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,8 +1118,8 @@ final class SwiftPMBuildSystemTests: XCTestCase {
11181118
fallbackAfterTimeout: false
11191119
)
11201120
let compilerArgs = try XCTUnwrap(settings?.compilerArguments)
1121-
XCTAssert(compilerArgs.contains("-package-description-version"))
1122-
XCTAssert(compilerArgs.contains(try versionSpecificManifestURL.filePath))
1121+
assertContains(compilerArgs, "-package-description-version")
1122+
assertContains(compilerArgs, try versionSpecificManifestURL.filePath)
11231123
}
11241124
}
11251125

@@ -1152,7 +1152,7 @@ final class SwiftPMBuildSystemTests: XCTestCase {
11521152
)
11531153
let compilerArgs = try XCTUnwrap(settings?.compilerArguments)
11541154
assertArgumentsContain("-package-description-version", "5.1.0", arguments: compilerArgs)
1155-
XCTAssert(compilerArgs.contains(try manifestURL.filePath))
1155+
assertContains(compilerArgs, try manifestURL.filePath)
11561156
}
11571157
}
11581158
}

Tests/SourceKitDTests/SourceKitDRegistryTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ final class FakeSourceKitD: SourceKitD {
7474
var values: sourcekitd_api_values { fatalError() }
7575
func addNotificationHandler(_ handler: SKDNotificationHandler) { fatalError() }
7676
func removeNotificationHandler(_ handler: SKDNotificationHandler) { fatalError() }
77+
func didSend(request: SKDRequestDictionary) {}
7778
private init() {
7879
token = nextToken.fetchAndIncrement()
7980
}

Tests/SourceKitLSPTests/CompilationDatabaseTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ final class CompilationDatabaseTests: XCTestCase {
151151
XCTFail("Expected ResponseError, got \(error)")
152152
return
153153
}
154-
XCTAssert(error.message.contains("No language service"))
154+
assertContains(error.message, "No language service")
155155
}
156156
}
157157

@@ -217,7 +217,7 @@ final class CompilationDatabaseTests: XCTestCase {
217217
)
218218
)
219219
let hoverContent = try XCTUnwrap(hover?.contents.markupContent?.value)
220-
XCTAssert(hoverContent.contains("void main()"))
220+
assertContains(hoverContent, "void main()")
221221

222222
// But for `testFromDummyToolchain.swift`, we can't launch sourcekitd (because it doesn't exist, we just provided a
223223
// dummy), so we should receive an error. The exact error here is not super relevant, the important part is that we
@@ -235,7 +235,7 @@ final class CompilationDatabaseTests: XCTestCase {
235235
XCTFail("Expected ResponseError, got \(error)")
236236
return
237237
}
238-
XCTAssert(error.message.contains("No language service"))
238+
assertContains(error.message, "No language service")
239239
}
240240
}
241241
}

Tests/SourceKitLSPTests/LocalSwiftTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,7 @@ final class LocalSwiftTests: XCTestCase {
730730

731731
XCTAssertEqual(fixit.title, "Use 'new(_:hotness:)' instead")
732732
XCTAssertEqual(fixit.diagnostics?.count, 1)
733-
XCTAssert(fixit.diagnostics?.first?.message.contains("is deprecated") == true)
733+
assertContains(try XCTUnwrap(fixit.diagnostics?.first?.message), "is deprecated")
734734
XCTAssertEqual(
735735
fixit.edit?.changes?[uri],
736736
[

Tests/SourceKitLSPTests/WorkspaceTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,7 +1181,7 @@ final class WorkspaceTests: XCTestCase {
11811181
)
11821182
)
11831183
let options = try XCTUnwrap(optionsOptional)
1184-
XCTAssert(options.compilerArguments.contains("-module-name"))
1184+
assertContains(options.compilerArguments, "-module-name")
11851185
XCTAssertEqual(options.kind, .normal)
11861186
XCTAssertNil(options.didPrepareTarget)
11871187
}
@@ -1366,7 +1366,7 @@ final class WorkspaceTests: XCTestCase {
13661366
allowFallbackSettings: false
13671367
)
13681368
)
1369-
XCTAssert(options.compilerArguments.contains("-DHAVE_SETTINGS"))
1369+
assertContains(options.compilerArguments, "-DHAVE_SETTINGS")
13701370
}
13711371

13721372
func testOutputPaths() async throws {

Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -311,22 +311,27 @@ final class SwiftSourceKitPluginTests: XCTestCase {
311311
compilerArguments: [path]
312312
)
313313

314+
let slowCompletionRequestSent = self.expectation(description: "slow completion result sent")
314315
let slowCompletionResultReceived = self.expectation(description: "slow completion")
315-
let slowCompletionTask = Task {
316-
do {
317-
_ = try await sourcekitd.completeOpen(
318-
path: path,
319-
position: positions["1️⃣"],
320-
filter: ""
321-
)
322-
XCTFail("Expected completion to be cancelled")
323-
} catch {
324-
XCTAssert(error is CancellationError, "Expected completion to be cancelled, failed with \(error)")
316+
let dynamicallyLoadedSourceKitd = try XCTUnwrap(sourcekitd as? DynamicallyLoadedSourceKitD)
317+
try await dynamicallyLoadedSourceKitd.withRequestHandlingHook {
318+
let slowCompletionTask = Task {
319+
await assertThrowsError(try await sourcekitd.completeOpen(path: path, position: positions["1️⃣"], filter: "")) {
320+
XCTAssert($0 is CancellationError, "Expected completion to be cancelled, failed with \($0)")
321+
}
322+
slowCompletionResultReceived.fulfill()
325323
}
326-
slowCompletionResultReceived.fulfill()
324+
// Wait for the slow completion request to actually be sent to sourcekitd. Otherwise, we might hit a cancellation
325+
// check somewhere during request sending and we aren't actually sending the completion request to sourcekitd.
326+
try await fulfillmentOfOrThrow(slowCompletionRequestSent)
327+
328+
slowCompletionTask.cancel()
329+
try await fulfillmentOfOrThrow(slowCompletionResultReceived, timeout: 30)
330+
} hook: { request in
331+
// Check that we aren't matching against a request sent by something else that has handle to the same sourcekitd.
332+
assertContains(request.description.replacing(#"\\"#, with: #"\"#), path)
333+
slowCompletionRequestSent.fulfill()
327334
}
328-
slowCompletionTask.cancel()
329-
try await fulfillmentOfOrThrow(slowCompletionResultReceived, timeout: 30)
330335

331336
let fastCompletionStarted = Date()
332337
let result = try await sourcekitd.completeOpen(

0 commit comments

Comments
 (0)