Skip to content

Commit 78f9968

Browse files
authored
Merge pull request #1819 from ahoppen/notification-wait
2 parents 283c524 + bb6f4b4 commit 78f9968

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
@@ -52,6 +52,29 @@ fileprivate struct NotificationTimeoutError: Error, CustomStringConvertible {
5252
var description: String = "Failed to receive next notification within timeout"
5353
}
5454

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

80103
/// Stream of the notifications that the server has sent to the client.
81-
private let notifications: AsyncStream<any NotificationType>
82-
83-
/// Continuation to add a new notification from the ``server`` to the `notifications` stream.
84-
private let notificationYielder: AsyncStream<any NotificationType>.Continuation
104+
private let notifications: PendingNotifications
85105

86106
/// The request handlers that have been set by `handleNextRequest`.
87107
///
@@ -136,11 +156,7 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable {
136156
options.sourcekitdRequestTimeout = defaultTimeout
137157
}
138158

139-
var notificationYielder: AsyncStream<any NotificationType>.Continuation!
140-
self.notifications = AsyncStream { continuation in
141-
notificationYielder = continuation
142-
}
143-
self.notificationYielder = notificationYielder
159+
self.notifications = PendingNotifications()
144160

145161
let serverToClientConnection = LocalConnection(receiverName: "client")
146162
self.serverToClientConnection = serverToClientConnection
@@ -250,26 +266,7 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable {
250266
/// - Note: This also returns any notifications sent before the call to
251267
/// `nextNotification`.
252268
package func nextNotification(timeout: Duration = .seconds(defaultTimeout)) async throws -> any NotificationType {
253-
// The task that gets the next notification from `self.notifications`.
254-
let notificationYielder = Task {
255-
for await notification in self.notifications {
256-
return notification
257-
}
258-
throw NotificationTimeoutError()
259-
}
260-
// After `timeout`, we tell `notificationYielder` that we are no longer interested in its result by cancelling it.
261-
// We wait for `notificationYielder` to accept this cancellation instead of returning immediately to avoid a
262-
// situation where `notificationYielder` continues running, eats the first notification but it then never gets
263-
// delivered to the test because we already delivered a timeout.
264-
let cancellationTask = Task {
265-
try await Task.sleep(for: timeout)
266-
notificationYielder.cancel()
267-
}
268-
defer {
269-
// We have received a value and don't need the cancellationTask anymore
270-
cancellationTask.cancel()
271-
}
272-
return try await notificationYielder.value
269+
return try await notifications.next(timeout: timeout)
273270
}
274271

275272
/// Await the next diagnostic notification sent to the client.
@@ -342,8 +339,10 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable {
342339
// MARK: - Conformance to MessageHandler
343340

344341
/// - Important: Implementation detail of `TestSourceKitLSPServer`. Do not call from tests.
345-
package func handle(_ params: some NotificationType) {
346-
notificationYielder.yield(params)
342+
package func handle(_ notification: some NotificationType) {
343+
Task {
344+
await notifications.add(notification)
345+
}
347346
}
348347

349348
/// - 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)