Skip to content

Improve timing sensitivity of testSuspendResumeProcess #76

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 16, 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
121 changes: 83 additions & 38 deletions Tests/SubprocessTests/SubprocessTests+Linux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,52 +46,97 @@ struct SubprocessLinuxTests {
}

@Test func testSuspendResumeProcess() async throws {
func isProcessSuspended(_ pid: pid_t) throws -> Bool {
let status = try Data(
contentsOf: URL(filePath: "/proc/\(pid)/status")
)
let statusString = try #require(
String(data: status, encoding: .utf8)
)
// Parse the status string
let stats = statusString.split(separator: "\n")
if let index = stats.firstIndex(
where: { $0.hasPrefix("State:") }
) {
let processState = stats[index].split(
separator: ":"
).map {
$0.trimmingCharacters(
in: .whitespacesAndNewlines
)
}

return processState[1].hasPrefix("T")
}
return false
}

_ = try await Subprocess.run(
// This will intentionally hang
.path("/usr/bin/sleep"),
arguments: ["infinity"],
error: .discarded
) { subprocess, standardOutput in
// First suspend the process
try subprocess.send(signal: .suspend)
#expect(
try isProcessSuspended(subprocess.processIdentifier.value)
)
// Now resume the process
try subprocess.send(signal: .resume)
#expect(
try isProcessSuspended(subprocess.processIdentifier.value) == false
)
// Now kill the process
try subprocess.send(signal: .terminate)
for try await _ in standardOutput {}
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
}
// 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 {}
}
}
}
}

fileprivate enum ProcessState: String {
case running = "R"
case sleeping = "S"
case uninterruptibleWait = "D"
case zombie = "Z"
case stopped = "T"
}

extension Execution {
fileprivate func state() throws -> ProcessState {
let processStatusFile = "/proc/\(processIdentifier.value)/status"
let processStatusData = try Data(
contentsOf: URL(filePath: processStatusFile)
)
let stateMatches = try String(decoding: processStatusData, as: UTF8.self)
.split(separator: "\n")
.compactMap({ line in
return try #/^State:\s+(?<status>[A-Z])\s+.*/#.wholeMatch(in: line)
})
guard let status = stateMatches.first, stateMatches.count == 1, let processState = ProcessState(rawValue: String(status.output.status)) else {
struct ProcStatusParseError: Error, CustomStringConvertible {
let filePath: String
let contents: Data
var description: String {
"Could not parse \(filePath):\n\(String(decoding: contents, as: UTF8.self))"
}
}
throw ProcStatusParseError(filePath: processStatusFile, contents: processStatusData)
}
return processState
}
}

func waitForCondition(timeout: Duration, _ evaluateCondition: () throws -> Bool) async throws {
var currentCondition = try evaluateCondition()
let deadline = ContinuousClock.now + timeout
while ContinuousClock.now < deadline {
if currentCondition {
return
}
try await Task.sleep(for: .milliseconds(10))
currentCondition = try evaluateCondition()
}
struct TimeoutError: Error, CustomStringConvertible {
var description: String {
"Timed out waiting for condition to be true"
}
}
throw TimeoutError()
}

func tryFinally(_ work: () async throws -> (), finally: (Error?) async throws -> ()) async throws {
let error: Error?
do {
try await work()
error = nil
} catch let e {
error = e
}
try await finally(error)
if let error {
throw error
}
}

#endif // canImport(Glibc) || canImport(Bionic) || canImport(Musl)
Loading