Skip to content

Enable CI testing for Linux #77

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
13 changes: 12 additions & 1 deletion .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,18 @@ jobs:
with:
linux_os_versions: '["noble", "jammy", "focal", "rhel-ubi9"]'
linux_swift_versions: '["6.1", "nightly-main"]'
linux_build_command: 'swift build'
linux_pre_build_command: |
if command -v apt-get >/dev/null 2>&1 ; then # noble, jammy, focal
apt-get update -y

# Test dependencies
apt-get install -y procps
elif command -v dnf >/dev/null 2>&1 ; then # rhel-ubi9
dnf update -y

# Test dependencies
dnf install -y procps
fi
windows_swift_versions: '["6.1", "nightly-main"]'
windows_build_command: 'swift build'
enable_macos_checks: true
Expand Down
144 changes: 103 additions & 41 deletions Tests/SubprocessTests/SubprocessTests+Linux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,34 @@ import Testing
// MARK: PlatformOption Tests
@Suite(.serialized)
struct SubprocessLinuxTests {
@Test func testSubprocessPlatformOptionsPreSpawnProcessConfigurator() async throws {
@Test(
.enabled(
if: getgid() == 0,
"This test requires root privileges"
)
)
func testSubprocessPlatformOptionsPreSpawnProcessConfigurator() async throws {
var platformOptions = PlatformOptions()
platformOptions.preSpawnProcessConfigurator = {
setgid(4321)
guard setgid(4321) == 0 else {
// Returns EPERM when:
// The calling process is not privileged (does not have the
// CAP_SETGID capability in its user namespace), and gid does
// not match the real group ID or saved set-group-ID of the
// calling process.
perror("setgid")
abort()
}
}
let idResult = try await Subprocess.run(
.path("/usr/bin/id"),
arguments: ["-g"],
platformOptions: platformOptions,
output: .string
output: .string,
error: .string
)
let error = try #require(idResult.standardError)
try #require(error == "")
#expect(idResult.terminationStatus.isSuccess)
let id = try #require(idResult.standardOutput)
#expect(
Expand All @@ -46,52 +63,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