Skip to content

Commit 4211d5f

Browse files
authored
Merge pull request #76 from swiftlang/eng/PR-fix-testSuspendResumeProcess
Improve timing sensitivity of testSuspendResumeProcess
2 parents 4e026f0 + 71c9ba1 commit 4211d5f

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
@@ -63,52 +63,97 @@ struct SubprocessLinuxTests {
6363
}
6464

6565
@Test func testSuspendResumeProcess() async throws {
66-
func isProcessSuspended(_ pid: pid_t) throws -> Bool {
67-
let status = try Data(
68-
contentsOf: URL(filePath: "/proc/\(pid)/status")
69-
)
70-
let statusString = try #require(
71-
String(data: status, encoding: .utf8)
72-
)
73-
// Parse the status string
74-
let stats = statusString.split(separator: "\n")
75-
if let index = stats.firstIndex(
76-
where: { $0.hasPrefix("State:") }
77-
) {
78-
let processState = stats[index].split(
79-
separator: ":"
80-
).map {
81-
$0.trimmingCharacters(
82-
in: .whitespacesAndNewlines
83-
)
84-
}
85-
86-
return processState[1].hasPrefix("T")
87-
}
88-
return false
89-
}
90-
9166
_ = try await Subprocess.run(
9267
// This will intentionally hang
9368
.path("/usr/bin/sleep"),
9469
arguments: ["infinity"],
9570
error: .discarded
9671
) { subprocess, standardOutput in
97-
// First suspend the process
98-
try subprocess.send(signal: .suspend)
99-
#expect(
100-
try isProcessSuspended(subprocess.processIdentifier.value)
101-
)
102-
// Now resume the process
103-
try subprocess.send(signal: .resume)
104-
#expect(
105-
try isProcessSuspended(subprocess.processIdentifier.value) == false
106-
)
107-
// Now kill the process
108-
try subprocess.send(signal: .terminate)
109-
for try await _ in standardOutput {}
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
78+
}
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 {}
89+
}
11090
}
11191
}
11292
}
11393

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

0 commit comments

Comments
 (0)