mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fc5265d685
* test: tighten newest regression ownership * test(ui): stabilize request-driven e2e waits * fix(ci): stabilize lifecycle-bound test observations * test(ci): pin current Telegram job cap * test(ui): wait for terminal selection owner * test(mac): use shared unread wait policy
124 lines
5.1 KiB
Swift
124 lines
5.1 KiB
Swift
import Foundation
|
|
import Subprocess
|
|
import Testing
|
|
@testable import OpenClaw
|
|
|
|
#if canImport(Darwin)
|
|
struct ManagedProcessTests {
|
|
@Test func `launch failures preserve the executable description`() async {
|
|
let executable = "/tmp/openclaw-missing-process-\(UUID().uuidString)"
|
|
|
|
do {
|
|
_ = try await self.start(executable: executable)
|
|
Issue.record("expected the missing executable to fail")
|
|
} catch {
|
|
#expect(error.localizedDescription.contains(executable))
|
|
}
|
|
}
|
|
|
|
@Test func `abortive termination interrupts the stdin grace period`() async throws {
|
|
let directory = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("openclaw-managed-process-abort-\(UUID().uuidString)", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: directory) }
|
|
let eofFile = directory.appendingPathComponent("eof")
|
|
let termFile = directory.appendingPathComponent("term")
|
|
let stdinPipe = Pipe()
|
|
let configuration = Subprocess.Configuration(
|
|
.path(.init("/bin/sh")),
|
|
arguments: Arguments([
|
|
"-c",
|
|
#"trap 'touch "$2"; exit 0' TERM; while IFS= read -r _; do :; done; touch "$1"; while :; do sleep 1; done"#,
|
|
"managed-process",
|
|
eofFile.path,
|
|
termFile.path,
|
|
]))
|
|
let process = ManagedProcess.launch(
|
|
configuration: configuration,
|
|
input: .fileDescriptor(
|
|
.init(rawValue: stdinPipe.fileHandleForReading.fileDescriptor),
|
|
closeAfterSpawningProcess: false),
|
|
output: .discarded,
|
|
error: .discarded,
|
|
closeAfterSpawn: [stdinPipe.fileHandleForReading],
|
|
closeStdinForGracefulShutdown: stdinPipe.fileHandleForWriting,
|
|
gracefulShutdownTimeout: .seconds(2))
|
|
_ = try await process.waitUntilStarted()
|
|
|
|
let startedAt = ContinuousClock.now
|
|
let termination = Task { await process.terminate() }
|
|
let eofDeadline = ContinuousClock.now + .seconds(1)
|
|
while !FileManager.default.fileExists(atPath: eofFile.path), ContinuousClock.now < eofDeadline {
|
|
try await Task.sleep(for: .milliseconds(10))
|
|
}
|
|
process.requestTermination(gracefully: false)
|
|
await termination.value
|
|
|
|
#expect(FileManager.default.fileExists(atPath: eofFile.path))
|
|
#expect(FileManager.default.fileExists(atPath: termFile.path))
|
|
#expect(ContinuousClock.now - startedAt < .seconds(1))
|
|
}
|
|
|
|
@Test func `instant exits cannot outrun process monitoring`() async throws {
|
|
for _ in 0..<32 {
|
|
let process = try await self.start(executable: "/usr/bin/true")
|
|
defer { process.requestTermination() }
|
|
|
|
let deadline = ContinuousClock.now + .seconds(1)
|
|
while process.isRunning, ContinuousClock.now < deadline {
|
|
try await Task.sleep(for: .milliseconds(1))
|
|
}
|
|
#expect(!process.isRunning)
|
|
await process.terminate()
|
|
}
|
|
}
|
|
|
|
@Test func `termination escalates for a TERM-resistant descendant`() async throws {
|
|
let directory = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("openclaw-managed-process-\(UUID().uuidString)", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: directory) }
|
|
let childPIDFile = directory.appendingPathComponent("child.pid")
|
|
let process = try await self.start(
|
|
executable: "/bin/sh",
|
|
arguments: [
|
|
"-c",
|
|
"""
|
|
/bin/sh -c 'trap "" TERM; echo $$ > "$CHILD_PID_FILE"; while :; do sleep 1; done' &
|
|
while :; do sleep 1; done
|
|
""",
|
|
],
|
|
environment: ["CHILD_PID_FILE": childPIDFile.path])
|
|
defer { process.requestTermination() }
|
|
let childPID = try await TestProcessSupport.waitForPID(
|
|
in: childPIDFile,
|
|
timeout: .seconds(1))
|
|
|
|
let startedAt = ContinuousClock.now
|
|
await process.terminate()
|
|
|
|
#expect(ContinuousClock.now - startedAt < .seconds(2))
|
|
#expect(!process.isRunning)
|
|
#expect(TestProcessSupport.processIsGone(childPID))
|
|
}
|
|
|
|
private func start(
|
|
executable: String,
|
|
arguments: [String] = [],
|
|
environment: [String: String] = [:]) async throws -> ManagedProcess
|
|
{
|
|
let configuration = Subprocess.Configuration(
|
|
.path(.init(executable)),
|
|
arguments: Arguments(arguments),
|
|
environment: ManagedProcess.environment(from: environment))
|
|
let process = ManagedProcess.launch(
|
|
configuration: configuration,
|
|
input: .none,
|
|
output: .discarded,
|
|
error: .discarded)
|
|
_ = try await process.waitUntilStarted()
|
|
return process
|
|
}
|
|
}
|
|
#endif
|