Skip to content

Commit 0e853a9

Browse files
Improve reliability of testSuspendResumeProcess (#102)
It's valid for a process to end up in a sleep state between transitions, so don't fail the test in that case. Only fail if the process prematurely transitions to a zombie state, which means it unexpectedly exited. This fixes an issue where testSuspendResumeProcess sometimes nondeterministically fails when the process transitions to a sleep state after resuming from stopped.
1 parent e07ce99 commit 0e853a9

File tree

1 file changed

+57
-21
lines changed

1 file changed

+57
-21
lines changed

Tests/SubprocessTests/SubprocessTests+Linux.swift

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -63,31 +63,56 @@ struct SubprocessLinuxTests {
6363
}
6464

6565
@Test func testSuspendResumeProcess() async throws {
66-
_ = try await Subprocess.run(
66+
let result = try await Subprocess.run(
6767
// This will intentionally hang
6868
.path("/usr/bin/sleep"),
6969
arguments: ["infinity"],
70+
output: .discarded,
7071
error: .discarded
71-
) { subprocess, standardOutput in
72-
try await tryFinally {
73-
// First suspend the process
74-
try subprocess.send(signal: .suspend)
75-
try await waitForCondition(timeout: .seconds(30)) {
76-
let state = try subprocess.state()
77-
return state == .stopped
72+
) { subprocess -> Error? in
73+
do {
74+
try await tryFinally {
75+
// First suspend the process
76+
try subprocess.send(signal: .suspend)
77+
try await waitForCondition(timeout: .seconds(30), comment: "Process did not transition from running to stopped state after $$") {
78+
let state = try subprocess.state()
79+
switch state {
80+
case .running:
81+
return false
82+
case .zombie:
83+
throw ProcessStateError(expectedState: .stopped, actualState: state)
84+
case .stopped, .sleeping, .uninterruptibleWait:
85+
return true
86+
}
87+
}
88+
// Now resume the process
89+
try subprocess.send(signal: .resume)
90+
try await waitForCondition(timeout: .seconds(30), comment: "Process did not transition from stopped to running state after $$") {
91+
let state = try subprocess.state()
92+
switch state {
93+
case .running, .sleeping, .uninterruptibleWait:
94+
return true
95+
case .zombie:
96+
throw ProcessStateError(expectedState: .running, actualState: state)
97+
case .stopped:
98+
return false
99+
}
100+
}
101+
} finally: { error in
102+
// Now kill the process
103+
try subprocess.send(signal: error != nil ? .kill : .terminate)
78104
}
79-
// Now resume the process
80-
try subprocess.send(signal: .resume)
81-
try await waitForCondition(timeout: .seconds(30)) {
82-
let state = try subprocess.state()
83-
return state == .running
84-
}
85-
} finally: { error in
86-
// Now kill the process
87-
try subprocess.send(signal: error != nil ? .kill : .terminate)
88-
for try await _ in standardOutput {}
105+
return nil
106+
} catch {
107+
return error
89108
}
90109
}
110+
if let error = result.value {
111+
#expect(result.terminationStatus == .unhandledException(SIGKILL))
112+
throw error
113+
} else {
114+
#expect(result.terminationStatus == .unhandledException(SIGTERM))
115+
}
91116
}
92117
}
93118

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

127+
fileprivate struct ProcessStateError: Error, CustomStringConvertible {
128+
let expectedState: ProcessState
129+
let actualState: ProcessState
130+
131+
var description: String {
132+
"Process did not transition to \(expectedState) state, but was actually \(actualState)"
133+
}
134+
}
135+
102136
extension Execution {
103137
fileprivate func state() throws -> ProcessState {
104138
let processStatusFile = "/proc/\(processIdentifier.value)/status"
@@ -124,7 +158,7 @@ extension Execution {
124158
}
125159
}
126160

127-
func waitForCondition(timeout: Duration, _ evaluateCondition: () throws -> Bool) async throws {
161+
func waitForCondition(timeout: Duration, comment: Comment, _ evaluateCondition: () throws -> Bool) async throws {
128162
var currentCondition = try evaluateCondition()
129163
let deadline = ContinuousClock.now + timeout
130164
while ContinuousClock.now < deadline {
@@ -135,11 +169,13 @@ func waitForCondition(timeout: Duration, _ evaluateCondition: () throws -> Bool)
135169
currentCondition = try evaluateCondition()
136170
}
137171
struct TimeoutError: Error, CustomStringConvertible {
172+
let timeout: Duration
173+
let comment: Comment
138174
var description: String {
139-
"Timed out waiting for condition to be true"
175+
comment.description.replacingOccurrences(of: "$$", with: "\(timeout)")
140176
}
141177
}
142-
throw TimeoutError()
178+
throw TimeoutError(timeout: timeout, comment: comment)
143179
}
144180

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

0 commit comments

Comments
 (0)