Skip to content

Commit ca960bc

Browse files
committed
Introduce a MultiEntrySemaphore for testing
Compare to the `while !AtomicBool.value { sleep }` pattern, this eventually times out, avoiding a timeout of the entire `swift test` invocation.
1 parent 572660a commit ca960bc

File tree

3 files changed

+58
-15
lines changed

3 files changed

+58
-15
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 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+
import SwiftExtensions
14+
import XCTest
15+
16+
/// A semaphore that, once signaled, will pass on every `wait` call. Ie. the semaphore only needs to be signaled once
17+
/// and from that point onwards it can be acquired as many times as necessary.
18+
///
19+
/// Use cases of this are for example to delay indexing until a some other task has been performed. But once that is
20+
/// done, all index operations should be able to run, not just one.
21+
package final class MultiEntrySemaphore: Sendable {
22+
private let name: String
23+
private let signaled = AtomicBool(initialValue: false)
24+
25+
package init(name: String) {
26+
self.name = name
27+
}
28+
29+
package func signal() {
30+
signaled.value = true
31+
}
32+
33+
package func waitOrThrow() async throws {
34+
do {
35+
try await repeatUntilExpectedResult(sleepInterval: .seconds(0.01)) { signaled.value }
36+
} catch {
37+
struct TimeoutError: Error, CustomStringConvertible {
38+
let name: String
39+
var description: String { "\(name) timed out" }
40+
}
41+
throw TimeoutError(name: "\(name) timed out")
42+
}
43+
}
44+
45+
package func waitOrXCTFail(file: StaticString = #filePath, line: UInt = #line) async {
46+
do {
47+
try await waitOrThrow()
48+
} catch {
49+
XCTFail("\(error)", file: file, line: line)
50+
}
51+
}
52+
}

Tests/SourceKitLSPTests/BackgroundIndexingTests.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1937,12 +1937,10 @@ final class BackgroundIndexingTests: XCTestCase {
19371937
}
19381938

19391939
func testIsIndexingRequest() async throws {
1940-
let checkedIsIndexStatus = AtomicBool(initialValue: false)
1940+
let checkedIsIndexStatus = MultiEntrySemaphore(name: "Checked is index status")
19411941
let hooks = Hooks(
19421942
indexHooks: IndexHooks(updateIndexStoreTaskDidStart: { task in
1943-
while !checkedIsIndexStatus.value {
1944-
try? await Task.sleep(for: .seconds(0.1))
1945-
}
1943+
await checkedIsIndexStatus.waitOrXCTFail()
19461944
})
19471945
)
19481946
let project = try await SwiftPMTestProject(
@@ -1956,7 +1954,7 @@ final class BackgroundIndexingTests: XCTestCase {
19561954
)
19571955
let isIndexingResponseWhileIndexing = try await project.testClient.send(IsIndexingRequest())
19581956
XCTAssert(isIndexingResponseWhileIndexing.indexing)
1959-
checkedIsIndexStatus.value = true
1957+
checkedIsIndexStatus.signal()
19601958

19611959
try await repeatUntilExpectedResult {
19621960
try await project.testClient.send(IsIndexingRequest()).indexing == false

Tests/SourceKitLSPTests/PullDiagnosticsTests.swift

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -279,20 +279,13 @@ final class PullDiagnosticsTests: XCTestCase {
279279
}
280280

281281
func testDiagnosticsWaitForDocumentToBePrepared() async throws {
282-
let diagnosticRequestSent = AtomicBool(initialValue: false)
282+
let diagnosticRequestSent = MultiEntrySemaphore(name: "Diagnostic request sent")
283283
var testHooks = Hooks()
284284
testHooks.indexHooks.preparationTaskDidStart = { @Sendable taskDescription in
285285
// Only start preparation after we sent the diagnostic request. In almost all cases, this should not give
286286
// preparation enough time to finish before the diagnostic request is handled unless we wait for preparation in
287287
// the diagnostic request.
288-
while diagnosticRequestSent.value == false {
289-
do {
290-
try await Task.sleep(for: .seconds(0.01))
291-
} catch {
292-
XCTFail("Did not expect sleep to fail")
293-
break
294-
}
295-
}
288+
await diagnosticRequestSent.waitOrXCTFail()
296289
}
297290

298291
let project = try await SwiftPMTestProject(
@@ -331,7 +324,7 @@ final class PullDiagnosticsTests: XCTestCase {
331324
XCTAssertEqual(diagnostics.success?.fullReport?.items, [])
332325
receivedDiagnostics.fulfill()
333326
}
334-
diagnosticRequestSent.value = true
327+
diagnosticRequestSent.signal()
335328
try await fulfillmentOfOrThrow([receivedDiagnostics])
336329
}
337330

0 commit comments

Comments
 (0)