Skip to content

Commit 9c867df

Browse files
authored
Merge pull request #2057 from ahoppen/test-timeout
Add a few timeout checks to tests
2 parents f9fc58d + 9cd599c commit 9c867df

17 files changed

+168
-95
lines changed

Sources/Diagnose/TraceFromSignpostsCommand.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import Foundation
1515
import RegexBuilder
1616
import SwiftExtensions
1717

18-
import class TSCBasic.Process
19-
2018
/// Shared instance of the regex that is used to extract Signpost lines from `log stream --signpost`.
2119
fileprivate struct LogParseRegex {
2220
@MainActor static let shared = LogParseRegex()

Sources/SKTestSupport/IndexedSingleSwiftFileTestProject.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
@_spi(Testing) import BuildSystemIntegration
1414
package import Foundation
1515
package import LanguageServerProtocol
16+
import SKLogging
1617
import SKOptions
1718
import SourceKitLSP
1819
import SwiftExtensions
@@ -126,7 +127,11 @@ package struct IndexedSingleSwiftFileTestProject {
126127

127128
// Run swiftc to build the index store
128129
do {
129-
try await Process.checkNonZeroExit(arguments: [swiftc.filePath] + compilerArguments)
130+
let compilerArgumentsCopy = compilerArguments
131+
let output = try await withTimeout(defaultTimeoutDuration) {
132+
try await Process.checkNonZeroExit(arguments: [swiftc.filePath] + compilerArgumentsCopy)
133+
}
134+
logger.debug("swiftc output:\n\(output)")
130135
} catch {
131136
if !allowBuildFailure {
132137
throw error
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+
}

Sources/SKTestSupport/RepeatUntilExpectedResult.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import XCTest
2121
///
2222
/// `sleepInterval` is the duration to wait before re-executing the body.
2323
package func repeatUntilExpectedResult(
24-
timeout: Duration = .seconds(defaultTimeout),
24+
timeout: Duration = defaultTimeoutDuration,
2525
sleepInterval: Duration = .seconds(1),
2626
_ body: () async throws -> Bool,
2727
file: StaticString = #filePath,

Sources/SKTestSupport/SkipUnless.swift

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -222,19 +222,22 @@ package actor SkipUnless {
222222
throw FailedToCrateInputFileError()
223223
}
224224
// If we can't compile for wasm, this fails complaining that it can't find the stdlib for wasm.
225-
let process = Process(
226-
args: try swiftFrontend.filePath,
227-
"-typecheck",
228-
try input.filePath,
229-
"-triple",
230-
"wasm32-unknown-none-wasm",
231-
"-enable-experimental-feature",
232-
"Embedded",
233-
"-Xcc",
234-
"-fdeclspec"
235-
)
236-
try process.launch()
237-
let result = try await process.waitUntilExit()
225+
let result = try await withTimeout(defaultTimeoutDuration) {
226+
try await Process.run(
227+
arguments: [
228+
try swiftFrontend.filePath,
229+
"-typecheck",
230+
try input.filePath,
231+
"-triple",
232+
"wasm32-unknown-none-wasm",
233+
"-enable-experimental-feature",
234+
"Embedded",
235+
"-Xcc",
236+
"-fdeclspec",
237+
],
238+
workingDirectory: nil
239+
)
240+
}
238241
if result.exitStatus == .terminated(code: 0) {
239242
return .featureSupported
240243
}
@@ -266,7 +269,7 @@ package actor SkipUnless {
266269
sourcekitd.keys.useNewAPI: 1
267270
],
268271
]),
269-
timeout: .seconds(defaultTimeout),
272+
timeout: defaultTimeoutDuration,
270273
fileContents: nil
271274
)
272275
return response[sourcekitd.keys.useNewAPI] == 1

Sources/SKTestSupport/SwiftPMDependencyProject.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
package import Foundation
1414
import LanguageServerProtocolExtensions
1515
import SwiftExtensions
16+
import TSCExtensions
1617
import XCTest
1718

1819
import struct TSCBasic.AbsolutePath
@@ -45,11 +46,12 @@ package class SwiftPMDependencyProject {
4546
}
4647
// We can't use `workingDirectory` because Amazon Linux doesn't support working directories (or at least
4748
// TSCBasic.Process doesn't support working directories on Amazon Linux)
48-
let process = TSCBasic.Process(
49-
arguments: [try git.filePath, "-C", try workingDirectory.filePath] + arguments
50-
)
51-
try process.launch()
52-
let processResult = try await process.waitUntilExit()
49+
let processResult = try await withTimeout(defaultTimeoutDuration) {
50+
try await TSCBasic.Process.run(
51+
arguments: [try git.filePath, "-C", try workingDirectory.filePath] + arguments,
52+
workingDirectory: nil
53+
)
54+
}
5355
guard processResult.exitStatus == .terminated(code: 0) else {
5456
throw Error.processedTerminatedWithNonZeroExitCode(processResult)
5557
}

Sources/SKTestSupport/SwiftPMTestProject.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
package import Foundation
1414
package import LanguageServerProtocol
15+
import SKLogging
1516
package import SKOptions
1617
package import SourceKitLSP
1718
import SwiftExtensions
@@ -260,7 +261,16 @@ package class SwiftPMTestProject: MultiFileTestProject {
260261
"-Xswiftc", "-module-cache-path", "-Xswiftc", try globalModuleCache.filePath,
261262
]
262263
}
263-
try await Process.checkNonZeroExit(arguments: arguments)
264+
let argumentsCopy = arguments
265+
let output = try await withTimeout(defaultTimeoutDuration) {
266+
try await Process.checkNonZeroExit(arguments: argumentsCopy)
267+
}
268+
logger.debug(
269+
"""
270+
'swift build' output:
271+
\(output)
272+
"""
273+
)
264274
}
265275

266276
/// Resolve package dependencies for the package at `path`.
@@ -274,6 +284,14 @@ package class SwiftPMTestProject: MultiFileTestProject {
274284
"resolve",
275285
"--package-path", try path.filePath,
276286
]
277-
try await Process.checkNonZeroExit(arguments: arguments)
287+
let output = try await withTimeout(defaultTimeoutDuration) {
288+
try await Process.checkNonZeroExit(arguments: arguments)
289+
}
290+
logger.debug(
291+
"""
292+
'swift package resolve' output:
293+
\(output)
294+
"""
295+
)
278296
}
279297
}

Sources/SKTestSupport/TestSourceKitLSPClient.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable {
197197
preInitialization?(self)
198198
if initialize {
199199
let capabilities = capabilities
200-
try await withTimeout(.seconds(defaultTimeout)) {
200+
try await withTimeout(defaultTimeoutDuration) {
201201
_ = try await self.send(
202202
InitializeRequest(
203203
processId: nil,
@@ -270,7 +270,7 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable {
270270
///
271271
/// - Note: This also returns any notifications sent before the call to
272272
/// `nextNotification`.
273-
package func nextNotification(timeout: Duration = .seconds(defaultTimeout)) async throws -> any NotificationType {
273+
package func nextNotification(timeout: Duration = defaultTimeoutDuration) async throws -> any NotificationType {
274274
return try await notifications.next(timeout: timeout)
275275
}
276276

@@ -279,7 +279,7 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable {
279279
/// If the next notification is not a `PublishDiagnosticsNotification`, this
280280
/// methods throws.
281281
package func nextDiagnosticsNotification(
282-
timeout: Duration = .seconds(defaultTimeout)
282+
timeout: Duration = defaultTimeoutDuration
283283
) async throws -> PublishDiagnosticsNotification {
284284
guard !usePullDiagnostics else {
285285
struct PushDiagnosticsError: Error, CustomStringConvertible {
@@ -295,7 +295,7 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable {
295295
package func nextNotification<ExpectedNotificationType: NotificationType>(
296296
ofType: ExpectedNotificationType.Type,
297297
satisfying predicate: (ExpectedNotificationType) throws -> Bool = { _ in true },
298-
timeout: Duration = .seconds(defaultTimeout)
298+
timeout: Duration = defaultTimeoutDuration
299299
) async throws -> ExpectedNotificationType {
300300
while true {
301301
let nextNotification = try await nextNotification(timeout: timeout)

Sources/SKTestSupport/Timeouts.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ package let defaultTimeout: TimeInterval = {
2222
}
2323
return 180
2424
}()
25+
26+
package var defaultTimeoutDuration: Duration { .seconds(defaultTimeout) }

Sources/SourceKitLSP/Swift/DocumentFormatting.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,9 @@ extension SwiftLanguageService {
196196
writeStream.send(snapshot.text)
197197
try writeStream.close()
198198

199-
let result = try await process.waitUntilExitStoppingProcessOnTaskCancellation()
199+
let result = try await withTimeout(.seconds(60)) {
200+
try await process.waitUntilExitStoppingProcessOnTaskCancellation()
201+
}
200202
guard result.exitStatus == .terminated(code: 0) else {
201203
let swiftFormatErrorMessage: String
202204
switch result.stderrOutput {

Tests/SourceKitDTests/SourceKitDTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,15 @@ final class SourceKitDTests: XCTestCase {
8484
keys.compilerArgs: args,
8585
])
8686

87-
_ = try await sourcekitd.send(req, timeout: .seconds(defaultTimeout), fileContents: nil)
87+
_ = try await sourcekitd.send(req, timeout: defaultTimeoutDuration, fileContents: nil)
8888

8989
try await fulfillmentOfOrThrow([expectation1, expectation2])
9090

9191
let close = sourcekitd.dictionary([
9292
keys.request: sourcekitd.requests.editorClose,
9393
keys.name: path,
9494
])
95-
_ = try await sourcekitd.send(close, timeout: .seconds(defaultTimeout), fileContents: nil)
95+
_ = try await sourcekitd.send(close, timeout: defaultTimeoutDuration, fileContents: nil)
9696
}
9797
}
9898

Tests/SourceKitLSPTests/BackgroundIndexingTests.swift

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1335,14 +1335,24 @@ final class BackgroundIndexingTests: XCTestCase {
13351335
// - The user runs `swift package update`
13361336
// - This updates `Package.resolved`, which we watch
13371337
// - We reload the package, which updates `Dependency.swift` in `.build/index-build/checkouts`, which we also watch.
1338-
try await Process.run(
1339-
arguments: [
1340-
unwrap(ToolchainRegistry.forTesting.default?.swift?.filePath),
1341-
"package", "update",
1342-
"--package-path", project.scratchDirectory.filePath,
1343-
],
1344-
workingDirectory: nil
1338+
let projectURL = project.scratchDirectory
1339+
let packageUpdateOutput = try await withTimeout(defaultTimeoutDuration) {
1340+
try await Process.run(
1341+
arguments: [
1342+
unwrap(ToolchainRegistry.forTesting.default?.swift?.filePath),
1343+
"package", "update",
1344+
"--package-path", projectURL.filePath,
1345+
],
1346+
workingDirectory: nil
1347+
)
1348+
}
1349+
logger.debug(
1350+
"""
1351+
'swift package update' output:
1352+
\(packageUpdateOutput)
1353+
"""
13451354
)
1355+
13461356
XCTAssertNotEqual(try String(contentsOf: packageResolvedURL, encoding: .utf8), originalPackageResolvedContents)
13471357
project.testClient.send(
13481358
DidChangeWatchedFilesNotification(changes: [
@@ -1901,12 +1911,10 @@ final class BackgroundIndexingTests: XCTestCase {
19011911
}
19021912

19031913
func testIsIndexingRequest() async throws {
1904-
let checkedIsIndexStatus = AtomicBool(initialValue: false)
1914+
let checkedIsIndexStatus = MultiEntrySemaphore(name: "Checked is index status")
19051915
let hooks = Hooks(
19061916
indexHooks: IndexHooks(updateIndexStoreTaskDidStart: { task in
1907-
while !checkedIsIndexStatus.value {
1908-
try? await Task.sleep(for: .seconds(0.1))
1909-
}
1917+
await checkedIsIndexStatus.waitOrXCTFail()
19101918
})
19111919
)
19121920
let project = try await SwiftPMTestProject(
@@ -1920,7 +1928,7 @@ final class BackgroundIndexingTests: XCTestCase {
19201928
)
19211929
let isIndexingResponseWhileIndexing = try await project.testClient.send(IsIndexingRequest())
19221930
XCTAssert(isIndexingResponseWhileIndexing.indexing)
1923-
checkedIsIndexStatus.value = true
1931+
checkedIsIndexStatus.signal()
19241932

19251933
try await repeatUntilExpectedResult {
19261934
try await project.testClient.send(IsIndexingRequest()).indexing == false

Tests/SourceKitLSPTests/CompilationDatabaseTests.swift

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,21 +65,12 @@ final class CompilationDatabaseTests: XCTestCase {
6565
DocumentHighlight(range: positions["5️⃣"]..<positions["6️⃣"], kind: .text),
6666
]
6767

68-
var didReceiveCorrectHighlight = false
69-
7068
// Updating the build settings takes a few seconds.
7169
// Send highlight requests every second until we receive correct results.
72-
for _ in 0..<30 {
70+
try await repeatUntilExpectedResult {
7371
let postChangeHighlightResponse = try await project.testClient.send(highlightRequest)
74-
75-
if postChangeHighlightResponse == expectedPostEditHighlight {
76-
didReceiveCorrectHighlight = true
77-
break
78-
}
79-
try await Task.sleep(for: .seconds(1))
72+
return postChangeHighlightResponse == expectedPostEditHighlight
8073
}
81-
82-
XCTAssert(didReceiveCorrectHighlight)
8374
}
8475

8576
func testJSONCompilationDatabaseWithDifferentToolchainsForSwift() async throws {

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)