Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 38 additions & 36 deletions Tests/SubprocessTests/ProcessMonitoringTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,42 +227,44 @@ extension SubprocessProcessMonitoringTests {
}

@Test func testDoesNotReapUnrelatedChildProcess() async throws {
// Make sure we don't reap child exit status that we didn't spawn
let child1 = self.immediateExitProcess(withExitCode: 0)
let child2 = self.immediateExitProcess(withExitCode: 0)
try await withSpawnedExecution(config: child1) { child1Execution in
try await withSpawnedExecution(config: child2) { child2Execution in
// Monitor child2, but make sure we don't reap child1's status
let status = try await monitorProcessTermination(
for: child2Execution.processIdentifier
)
#expect(status.isSuccess)
// Make sure we can still fetch child 1
#if os(Windows)
let rc = WaitForSingleObject(
child1Execution.processIdentifier.processDescriptor,
INFINITE
)
#expect(rc == WAIT_OBJECT_0)
var child1Status: DWORD = 0
let rc2 = GetExitCodeProcess(
child1Execution.processIdentifier.processDescriptor,
&child1Status
)
#expect(rc2 == true)
#expect(child1Status == 0)
#else
var siginfo = siginfo_t()
let rc = waitid(
P_PID,
id_t(child1Execution.processIdentifier.value),
&siginfo,
WEXITED
)
#expect(rc == 0)
#expect(siginfo.si_code == CLD_EXITED)
#expect(siginfo.si_status == 0)
#endif
try await withTimeout(.seconds(60), description: #function) {
// Make sure we don't reap child exit status that we didn't spawn
let child1 = self.immediateExitProcess(withExitCode: 0)
let child2 = self.immediateExitProcess(withExitCode: 0)
try await withSpawnedExecution(config: child1) { child1Execution in
try await withSpawnedExecution(config: child2) { child2Execution in
// Monitor child2, but make sure we don't reap child1's status
let status = try await monitorProcessTermination(
for: child2Execution.processIdentifier
)
#expect(status.isSuccess)
// Make sure we can still fetch child 1
#if os(Windows)
let rc = WaitForSingleObject(
child1Execution.processIdentifier.processDescriptor,
INFINITE
)
#expect(rc == WAIT_OBJECT_0)
var child1Status: DWORD = 0
let rc2 = GetExitCodeProcess(
child1Execution.processIdentifier.processDescriptor,
&child1Status
)
#expect(rc2 == true)
#expect(child1Status == 0)
#else
var siginfo = siginfo_t()
let rc = waitid(
P_PID,
id_t(child1Execution.processIdentifier.value),
&siginfo,
WEXITED
)
#expect(rc == 0)
#expect(siginfo.si_code == CLD_EXITED)
#expect(siginfo.si_status == 0)
#endif
}
}
}
}
Expand Down
48 changes: 48 additions & 0 deletions Tests/SubprocessTests/TestSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,51 @@ extension Trait where Self == ConditionTrait {
)
}
}

// MARK: - Test Timeout

// Adapted from swift-build to surface hangs in tests as diagnosable failures
// rather than CI job timeouts.
// https://github.com/swiftlang/swift-build/blob/main/Sources/SWBTestSupport/Timeout.swift

internal struct TimeoutError: Error, CustomStringConvertible {
internal var description: String

internal init(description: String) {
self.description = description
}
}

@discardableResult
internal func withTimeout<T: Sendable>(
_ timeout: Duration,
description: String,
_ body: sending () async throws -> T
) async throws -> T {
try await withoutActuallyEscaping(body) { escapingClosure in
// This lets `body` be captured in the `Sendable` function `work`.
// since `work` is `Sendable`, we can smuggle it through.
nonisolated(unsafe) let smuggled = escapingClosure
let work: @Sendable () async throws -> T = {
try await smuggled()
}
return try await withThrowingTaskGroup(of: T.self) { group in
group.addTask {
try await work()
}
group.addTask {
try await Task.sleep(for: timeout)
throw TimeoutError(
description: "\(description) exceeded timeout of \(timeout)"
)
}
defer { group.cancelAll() }
guard let result = try await group.next() else {
throw TimeoutError(
description: "\(description) exceeded timeout of \(timeout)"
)
}
return result
}
}
}