Skip to content

Commit 71c9ba1

Browse files
committed
Improve timing sensitivity of testSuspendResumeProcess
Wait up to 30 seconds for the process to enter the desired state. Closes #28
1 parent 80bd50f commit 71c9ba1

File tree

1 file changed

+83
-38
lines changed

1 file changed

+83
-38
lines changed

Tests/SubprocessTests/SubprocessTests+Linux.swift

Lines changed: 83 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -46,52 +46,97 @@ struct SubprocessLinuxTests {
4646
}
4747

4848
@Test func testSuspendResumeProcess() async throws {
49-
func isProcessSuspended(_ pid: pid_t) throws -> Bool {
50-
let status = try Data(
51-
contentsOf: URL(filePath: "/proc/\(pid)/status")
52-
)
53-
let statusString = try #require(
54-
String(data: status, encoding: .utf8)
55-
)
56-
// Parse the status string
57-
let stats = statusString.split(separator: "\n")
58-
if let index = stats.firstIndex(
59-
where: { $0.hasPrefix("State:") }
60-
) {
61-
let processState = stats[index].split(
62-
separator: ":"
63-
).map {
64-
$0.trimmingCharacters(
65-
in: .whitespacesAndNewlines
66-
)
67-
}
68-
69-
return processState[1].hasPrefix("T")
70-
}
71-
return false
72-
}
73-
7449
_ = try await Subprocess.run(
7550
// This will intentionally hang
7651
.path("/usr/bin/sleep"),
7752
arguments: ["infinity"],
7853
error: .discarded
7954
) { subprocess, standardOutput in
80-
// First suspend the process
81-
try subprocess.send(signal: .suspend)
82-
#expect(
83-
try isProcessSuspended(subprocess.processIdentifier.value)
84-
)
85-
// Now resume the process
86-
try subprocess.send(signal: .resume)
87-
#expect(
88-
try isProcessSuspended(subprocess.processIdentifier.value) == false
89-
)
90-
// Now kill the process
91-
try subprocess.send(signal: .terminate)
92-
for try await _ in standardOutput {}
55+
try await tryFinally {
56+
// First suspend the process
57+
try subprocess.send(signal: .suspend)
58+
try await waitForCondition(timeout: .seconds(30)) {
59+
let state = try subprocess.state()
60+
return state == .stopped
61+
}
62+
// Now resume the process
63+
try subprocess.send(signal: .resume)
64+
try await waitForCondition(timeout: .seconds(30)) {
65+
let state = try subprocess.state()
66+
return state == .running
67+
}
68+
} finally: { error in
69+
// Now kill the process
70+
try subprocess.send(signal: error != nil ? .kill : .terminate)
71+
for try await _ in standardOutput {}
72+
}
9373
}
9474
}
9575
}
9676

77+
fileprivate enum ProcessState: String {
78+
case running = "R"
79+
case sleeping = "S"
80+
case uninterruptibleWait = "D"
81+
case zombie = "Z"
82+
case stopped = "T"
83+
}
84+
85+
extension Execution {
86+
fileprivate func state() throws -> ProcessState {
87+
let processStatusFile = "/proc/\(processIdentifier.value)/status"
88+
let processStatusData = try Data(
89+
contentsOf: URL(filePath: processStatusFile)
90+
)
91+
let stateMatches = try String(decoding: processStatusData, as: UTF8.self)
92+
.split(separator: "\n")
93+
.compactMap({ line in
94+
return try #/^State:\s+(?<status>[A-Z])\s+.*/#.wholeMatch(in: line)
95+
})
96+
guard let status = stateMatches.first, stateMatches.count == 1, let processState = ProcessState(rawValue: String(status.output.status)) else {
97+
struct ProcStatusParseError: Error, CustomStringConvertible {
98+
let filePath: String
99+
let contents: Data
100+
var description: String {
101+
"Could not parse \(filePath):\n\(String(decoding: contents, as: UTF8.self))"
102+
}
103+
}
104+
throw ProcStatusParseError(filePath: processStatusFile, contents: processStatusData)
105+
}
106+
return processState
107+
}
108+
}
109+
110+
func waitForCondition(timeout: Duration, _ evaluateCondition: () throws -> Bool) async throws {
111+
var currentCondition = try evaluateCondition()
112+
let deadline = ContinuousClock.now + timeout
113+
while ContinuousClock.now < deadline {
114+
if currentCondition {
115+
return
116+
}
117+
try await Task.sleep(for: .milliseconds(10))
118+
currentCondition = try evaluateCondition()
119+
}
120+
struct TimeoutError: Error, CustomStringConvertible {
121+
var description: String {
122+
"Timed out waiting for condition to be true"
123+
}
124+
}
125+
throw TimeoutError()
126+
}
127+
128+
func tryFinally(_ work: () async throws -> (), finally: (Error?) async throws -> ()) async throws {
129+
let error: Error?
130+
do {
131+
try await work()
132+
error = nil
133+
} catch let e {
134+
error = e
135+
}
136+
try await finally(error)
137+
if let error {
138+
throw error
139+
}
140+
}
141+
97142
#endif // canImport(Glibc) || canImport(Bionic) || canImport(Musl)

0 commit comments

Comments
 (0)