Skip to content

Commit 2270ab9

Browse files
committed
Fix race condition that caused testMainFileChangesIfIncludeIsAdded to fail
In some situations, we could return the timeout error from the timeout task, but receive a notification from the `self.notifications` `AsyncStream`. That notification would then be dropped and never get delivered to the test case, which can cause test cases to fail.
1 parent a22bf90 commit 2270ab9

File tree

2 files changed

+22
-15
lines changed

2 files changed

+22
-15
lines changed

Sources/SKTestSupport/TestSourceKitLSPClient.swift

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -248,21 +248,26 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable {
248248
/// - Note: This also returns any notifications sent before the call to
249249
/// `nextNotification`.
250250
package func nextNotification(timeout: Duration = .seconds(defaultTimeout)) async throws -> any NotificationType {
251-
return try await withThrowingTaskGroup(of: (any NotificationType).self) { taskGroup in
252-
taskGroup.addTask {
253-
for await notification in self.notifications {
254-
return notification
255-
}
256-
throw NotificationTimeoutError()
257-
}
258-
taskGroup.addTask {
259-
try await Task.sleep(for: timeout)
260-
throw NotificationTimeoutError()
251+
// The task that gets the next notification from `self.notifications`.
252+
let notificationYielder = Task {
253+
for await notification in self.notifications {
254+
return notification
261255
}
262-
let result = try await taskGroup.next()!
263-
taskGroup.cancelAll()
264-
return result
256+
throw NotificationTimeoutError()
257+
}
258+
// After `timeout`, we tell `notificationYielder` that we are no longer interested in its result by cancelling it.
259+
// We wait for `notificationYielder` to accept this cancellation instead of returning immediately to avoid a
260+
// situation where `notificationYielder` continues running, eats the first notification but it then never gets
261+
// delivered to the test because we already delivered a timeout.
262+
let cancellationTask = Task {
263+
try await Task.sleep(for: timeout)
264+
notificationYielder.cancel()
265+
}
266+
defer {
267+
// We have received a value and don't need the cancellationTask anymore
268+
cancellationTask.cancel()
265269
}
270+
return try await notificationYielder.value
266271
}
267272

268273
/// Await the next diagnostic notification sent to the client.

Tests/SourceKitLSPTests/MainFilesProviderTests.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import LanguageServerProtocol
14+
import SKLogging
1415
import SKTestSupport
1516
import SourceKitLSP
1617
import SwiftExtensions
@@ -202,10 +203,11 @@ final class MainFilesProviderTests: XCTestCase {
202203
// diagnostics.
203204
try await repeatUntilExpectedResult {
204205
let refreshedDiags = try? await project.testClient.nextDiagnosticsNotification(timeout: .seconds(1))
205-
guard let diagnostic = refreshedDiags?.diagnostics.only else {
206+
guard refreshedDiags?.diagnostics.only?.message == "Unused variable 'fromMyFancyLibrary'" else {
207+
logger.debug("Received unexpected diagnostics: \(refreshedDiags?.forLogging)")
206208
return false
207209
}
208-
return diagnostic.message == "Unused variable 'fromMyFancyLibrary'"
210+
return true
209211
}
210212
}
211213
}

0 commit comments

Comments
 (0)