Skip to content

Improve reliability of testSuspendResumeProcess #102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 30, 2025
Merged
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
78 changes: 57 additions & 21 deletions Tests/SubprocessTests/SubprocessTests+Linux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,31 +63,56 @@ struct SubprocessLinuxTests {
}

@Test func testSuspendResumeProcess() async throws {
_ = try await Subprocess.run(
let result = try await Subprocess.run(
// This will intentionally hang
.path("/usr/bin/sleep"),
arguments: ["infinity"],
output: .discarded,
error: .discarded
) { subprocess, standardOutput in
try await tryFinally {
// First suspend the process
try subprocess.send(signal: .suspend)
try await waitForCondition(timeout: .seconds(30)) {
let state = try subprocess.state()
return state == .stopped
) { subprocess -> Error? in
do {
try await tryFinally {
// First suspend the process
try subprocess.send(signal: .suspend)
try await waitForCondition(timeout: .seconds(30), comment: "Process did not transition from running to stopped state after $$") {
let state = try subprocess.state()
switch state {
case .running:
return false
case .zombie:
throw ProcessStateError(expectedState: .stopped, actualState: state)
case .stopped, .sleeping, .uninterruptibleWait:
return true
}
}
// Now resume the process
try subprocess.send(signal: .resume)
try await waitForCondition(timeout: .seconds(30), comment: "Process did not transition from stopped to running state after $$") {
let state = try subprocess.state()
switch state {
case .running, .sleeping, .uninterruptibleWait:
return true
case .zombie:
throw ProcessStateError(expectedState: .running, actualState: state)
case .stopped:
return false
}
}
} finally: { error in
// Now kill the process
try subprocess.send(signal: error != nil ? .kill : .terminate)
}
// Now resume the process
try subprocess.send(signal: .resume)
try await waitForCondition(timeout: .seconds(30)) {
let state = try subprocess.state()
return state == .running
}
} finally: { error in
// Now kill the process
try subprocess.send(signal: error != nil ? .kill : .terminate)
for try await _ in standardOutput {}
return nil
} catch {
return error
}
}
if let error = result.value {
#expect(result.terminationStatus == .unhandledException(SIGKILL))
throw error
} else {
#expect(result.terminationStatus == .unhandledException(SIGTERM))
}
}
}

Expand All @@ -99,6 +124,15 @@ fileprivate enum ProcessState: String {
case stopped = "T"
}

fileprivate struct ProcessStateError: Error, CustomStringConvertible {
let expectedState: ProcessState
let actualState: ProcessState

var description: String {
"Process did not transition to \(expectedState) state, but was actually \(actualState)"
}
}

extension Execution {
fileprivate func state() throws -> ProcessState {
let processStatusFile = "/proc/\(processIdentifier.value)/status"
Expand All @@ -124,7 +158,7 @@ extension Execution {
}
}

func waitForCondition(timeout: Duration, _ evaluateCondition: () throws -> Bool) async throws {
func waitForCondition(timeout: Duration, comment: Comment, _ evaluateCondition: () throws -> Bool) async throws {
var currentCondition = try evaluateCondition()
let deadline = ContinuousClock.now + timeout
while ContinuousClock.now < deadline {
Expand All @@ -135,11 +169,13 @@ func waitForCondition(timeout: Duration, _ evaluateCondition: () throws -> Bool)
currentCondition = try evaluateCondition()
}
struct TimeoutError: Error, CustomStringConvertible {
let timeout: Duration
let comment: Comment
var description: String {
"Timed out waiting for condition to be true"
comment.description.replacingOccurrences(of: "$$", with: "\(timeout)")
}
}
throw TimeoutError()
throw TimeoutError(timeout: timeout, comment: comment)
}

func tryFinally(_ work: () async throws -> (), finally: (Error?) async throws -> ()) async throws {
Expand Down
Loading