Skip to content

Commit bb6f4b4

Browse files
committed
Allow waiting for the next notification from SourceKit-LSP when the previous wait timed out
When `TestSourceKitLSPClient.nextNotification` timed out, it would cancel `AsyncSequence.Iterator.next()` and thus also cancel the underlying `AsyncStream` (which I only now found out). This means that calling `nextNotification` again would never return any notification. Hence, if we didn’t receive a `PublishDiagnosticsNotification` within the first timeout, we would never get one.
1 parent 8ba80a0 commit bb6f4b4

File tree

4 files changed

+49
-37
lines changed

4 files changed

+49
-37
lines changed

Sources/SKTestSupport/RepeatUntilExpectedResult.swift

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

1313
import SKLogging
14+
import SwiftExtensions
1415
import XCTest
1516

1617
/// Runs the body repeatedly once per second until it returns `true`, giving up after `timeout`.
@@ -34,9 +35,3 @@ package func repeatUntilExpectedResult(
3435
}
3536
XCTFail("Failed to get expected result", file: file, line: line)
3637
}
37-
38-
fileprivate extension Duration {
39-
var seconds: Double {
40-
return Double(self.components.attoseconds) / 1_000_000_000_000_000_000 + Double(self.components.seconds)
41-
}
42-
}

Sources/SKTestSupport/TestSourceKitLSPClient.swift

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,29 @@ fileprivate struct NotificationTimeoutError: Error, CustomStringConvertible {
5050
var description: String = "Failed to receive next notification within timeout"
5151
}
5252

53+
/// A list of notifications that has been received by the SourceKit-LSP server but not handled from the test case yet.
54+
///
55+
/// We can't use an `AsyncStream` for this because an `AsyncStream` is cancelled if a task that calls
56+
/// `AsyncStream.Iterator.next` is cancelled and we want to be able to wait for new notifications even if waiting for a
57+
/// a previous notification timed out.
58+
actor PendingNotifications {
59+
private var values: [any NotificationType] = []
60+
61+
func add(_ value: any NotificationType) {
62+
values.insert(value, at: 0)
63+
}
64+
65+
func next(timeout: Duration, pollingInterval: Duration = .milliseconds(10)) async throws -> any NotificationType {
66+
for _ in 0..<Int(timeout.seconds / pollingInterval.seconds) {
67+
if let value = values.popLast() {
68+
return value
69+
}
70+
try await Task.sleep(for: pollingInterval)
71+
}
72+
throw NotificationTimeoutError()
73+
}
74+
}
75+
5376
/// A mock SourceKit-LSP client (aka. a mock editor) that behaves like an editor
5477
/// for testing purposes.
5578
///
@@ -76,10 +99,7 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable {
7699
private let serverToClientConnection: LocalConnection
77100

78101
/// Stream of the notifications that the server has sent to the client.
79-
private let notifications: AsyncStream<any NotificationType>
80-
81-
/// Continuation to add a new notification from the ``server`` to the `notifications` stream.
82-
private let notificationYielder: AsyncStream<any NotificationType>.Continuation
102+
private let notifications: PendingNotifications
83103

84104
/// The request handlers that have been set by `handleNextRequest`.
85105
///
@@ -134,11 +154,7 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable {
134154
options.sourcekitdRequestTimeout = defaultTimeout
135155
}
136156

137-
var notificationYielder: AsyncStream<any NotificationType>.Continuation!
138-
self.notifications = AsyncStream { continuation in
139-
notificationYielder = continuation
140-
}
141-
self.notificationYielder = notificationYielder
157+
self.notifications = PendingNotifications()
142158

143159
let serverToClientConnection = LocalConnection(receiverName: "client")
144160
self.serverToClientConnection = serverToClientConnection
@@ -248,26 +264,7 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable {
248264
/// - Note: This also returns any notifications sent before the call to
249265
/// `nextNotification`.
250266
package func nextNotification(timeout: Duration = .seconds(defaultTimeout)) async throws -> any NotificationType {
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
255-
}
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()
269-
}
270-
return try await notificationYielder.value
267+
return try await notifications.next(timeout: timeout)
271268
}
272269

273270
/// Await the next diagnostic notification sent to the client.
@@ -340,8 +337,10 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable {
340337
// MARK: - Conformance to MessageHandler
341338

342339
/// - Important: Implementation detail of `TestSourceKitLSPServer`. Do not call from tests.
343-
package func handle(_ params: some NotificationType) {
344-
notificationYielder.yield(params)
340+
package func handle(_ notification: some NotificationType) {
341+
Task {
342+
await notifications.add(notification)
343+
}
345344
}
346345

347346
/// - Important: Implementation detail of `TestSourceKitLSPClient`. Do not call from tests.

Sources/SwiftExtensions/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ add_library(SwiftExtensions STATIC
88
CartesianProduct.swift
99
Collection+Only.swift
1010
Collection+PartitionIntoBatches.swift
11+
Duration+Seconds.swift
1112
FileManagerExtensions.swift
1213
NSLock+WithLock.swift
1314
PipeAsStringHandler.swift
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
extension Duration {
14+
package var seconds: Double {
15+
return Double(self.components.attoseconds) / 1_000_000_000_000_000_000 + Double(self.components.seconds)
16+
}
17+
}

0 commit comments

Comments
 (0)