///|
#cfg(all(target="native", not(platform="windows")))
async test "simple command execution" {
let (exit_code, output) = @process.collect_stdout("echo", ["Hello, World!"])
inspect(exit_code, content="0")
let text = output.text()
inspect(text.has_prefix("Hello"), content="true")
}
///|
#cfg(all(target="native", not(platform="windows")))
async test "command with exit code" {
let exit_code = @process.run("sh", ["-c", "exit 42"])
inspect(exit_code, content="42")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "spawn command in background" {
@async.with_task_group(group => {
let process = @process.spawn(group, "sh", ["-c", "sleep 0.5; exit 42"])
// do other stuff while the process is running in the background
@async.sleep(250)
// use `.wait()` to wait for the child process, or `.try_wait()` to peek its status
inspect(process.wait(), content="42")
})
}///|
#cfg(all(target="native", not(platform="windows")))
async test "collect stdout" {
let (code, output) = @process.collect_stdout("printf", ["test output"])
inspect(code, content="0")
inspect(output.text(), content="test output")
}
///|
#cfg(all(target="native", not(platform="windows")))
async test "collect stdout with args" {
let (code, output) = @process.collect_stdout("sh", [
"-c", "echo 'line 1'; echo 'line 2'",
])
inspect(code, content="0")
let text = output.text()
inspect(text.contains("line 1"), content="true")
inspect(text.contains("line 2"), content="true")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "collect stderr" {
let (code, output) = @process.collect_stderr("sh", [
"-c", "printf 'error message' >&2",
])
inspect(code, content="0")
inspect(output.text(), content="error message")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "collect both streams" {
let (code, stdout, stderr) = @process.collect_output("sh", [
"-c", "printf 'out msg'; printf 'err msg' >&2",
])
inspect(code, content="0")
inspect(stdout.text(), content="out msg")
inspect(stderr.text(), content="err msg")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "collect merged output" {
let (code, output) = @process.collect_output_merged("sh", [
"-c", "printf 'ab'; printf 'cd' >&2; printf 'ef'",
])
inspect(code, content="0")
inspect(output.text(), content="abcdef")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "read from process with pipe" {
@async.with_task_group(group => {
let (reader, writer) = @process.read_from_process()
defer reader.close()
let _ = @process.spawn(group, "echo", ["Hello from process"], stdout=writer)
let output = reader.read_all().text()
inspect(output.has_prefix("Hello"), content="true")
})
}///|
#cfg(all(target="native", not(platform="windows")))
async test "write to process with pipe" {
@async.with_task_group(group => {
let (cat_read, we_write) = @process.write_to_process()
let (we_read, cat_write) = @process.read_from_process()
let _ = @process.spawn(
group,
"cat",
["-"],
stdin=cat_read,
stdout=cat_write,
)
group.spawn_bg(() => {
defer we_write.close()
we_write.write(b"test input\n")
})
group.spawn_bg(() => {
defer we_read.close()
let output = we_read.read_all().text()
inspect(output.contains("test input"), content="true")
})
})
}///|
#cfg(all(target="native", not(platform="windows")))
async test "redirect input from file" {
@async.with_task_group(root => {
let input_file = "_build/process_test_input.txt"
@fs.write_file(input_file, "file content", create_mode=CreateOrTruncate)
root.add_defer(() => @fs.remove(input_file))
let (code, output) = @process.collect_stdout(
"cat",
[],
stdin=@process.redirect_from_file(input_file),
)
inspect(code, content="0")
inspect(output.text(), content="file content")
})
}///|
#cfg(all(target="native", not(platform="windows")))
async test "redirect output to file" {
@async.with_task_group(root => {
let output_file = "_build/process_test_output.txt"
root.add_defer(() => @fs.remove(output_file))
let code = @process.run(
"echo",
["test output"],
stdout=@process.redirect_to_file(
output_file,
create_mode=CreateOrTruncate,
),
)
inspect(code, content="0")
let content = @fs.read_file(output_file).text()
inspect(content.has_prefix("test output"), content="true")
})
}///|
#cfg(all(target="native", not(platform="windows")))
async test "file to file redirection" {
@async.with_task_group(root => {
let input_file = "_build/process_redirect_in.txt"
let output_file = "_build/process_redirect_out.txt"
@fs.write_file(input_file, "redirect test", create_mode=CreateOrTruncate)
root.add_defer(() => @fs.remove(input_file))
root.add_defer(() => @fs.remove(output_file))
let _ = @process.run(
"cat",
[],
stdin=@process.redirect_from_file(input_file),
stdout=@process.redirect_to_file(
output_file,
create_mode=CreateOrTruncate,
),
)
inspect(@fs.read_file(output_file).text(), content="redirect test")
})
}///|
#cfg(all(target="native", not(platform="windows")))
async test "set environment variable" {
let (code, output) = @process.collect_stdout("sh", ["-c", "echo $MY_VAR"], extra_env={
"MY_VAR": "my_value",
})
inspect(code, content="0")
inspect(output.text().trim(), content="my_value")
}
///|
#cfg(all(target="native", not(platform="windows")))
async test "multiple environment variables" {
let (code, output) = @process.collect_stdout(
"sh",
["-c", "echo $VAR1-$VAR2"],
extra_env={ "VAR1": "first", "VAR2": "second" },
)
inspect(code, content="0")
inspect(output.text().trim(), content="first-second")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "isolated environment" {
let (code, output) = @process.collect_stdout(
"env",
[],
extra_env={ "ONLY_VAR": "only_value" },
inherit_env=false,
)
inspect(code, content="0")
let text = output.text()
inspect(text.contains("ONLY_VAR=only_value"), content="true")
// Parent environment variables won't be present
inspect(text.contains("PATH="), content="false")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "set working directory" {
let (code, output) = @process.collect_stdout("pwd", [], cwd="src")
inspect(code, content="0")
let text = output.text().trim()
// Check that the path ends with "src" (works across OSes)
inspect(text.has_suffix("src"), content="true")
}
///|
#cfg(all(target="native", not(platform="windows")))
async test "relative path in cwd" {
let (code, output) = @process.collect_stdout("ls", [], cwd="src")
inspect(code, content="0")
let text = output.text()
// Should list contents of src directory
let has_content = text.length() > 0
inspect(has_content, content="true")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "spawn and wait" {
let exit_code = @process.run("sleep", ["0.1"])
inspect(exit_code, content="0")
}
///|
#cfg(all(target="native", not(platform="windows")))
async test "wait for specific exit code" {
let exit_code = @process.run("sh", ["-c", "exit 5"])
inspect(exit_code, content="5")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "spawn orphan and wait later" {
let pid = @process.spawn_orphan("sh", ["-c", "sleep 0.1; exit 7"])
// Do other work...
@async.sleep(50)
// Wait for the process to complete
let exit_code = @process.wait_pid(pid)
inspect(exit_code, content="7")
}///|
#cfg(all(target="native", not(platform="windows")))
async test "merge stdout and stderr" {
@async.with_task_group(group => {
let (reader, writer) = @process.read_from_process()
defer reader.close()
let _ = @process.spawn(
group,
"sh",
["-c", "echo 'to stdout'; echo 'to stderr' >&2"],
stdout=writer,
stderr=writer,
)
let output = reader.read_all().text()
inspect(output.contains("to stdout"), content="true")
inspect(output.contains("to stderr"), content="true")
})
}///|
#cfg(all(target="native", not(platform="windows")))
async test "multiple processes to one pipe" {
@async.with_task_group(root => {
let (reader, writer) = @pipe.pipe()
root.spawn_bg(no_wait=true, () => {
defer reader.close()
let output = reader.read_all().text()
inspect(output.contains("first"), content="true")
inspect(output.contains("second"), content="true")
})
defer writer.close()
@async.with_task_group(group => {
@process.spawn(group, "echo", ["first"], stdout=writer) |> ignore
@process.spawn(group, "echo", ["second"], stdout=writer) |> ignore
})
})
}///|
#cfg(all(target="native", not(platform="windows")))
async test "handle process errors" {
// Non-existent command fails
@test_util.assert_raise_async(() => @process.run("nonexistent_command", []))
}
///|
#cfg(all(target="native", not(platform="windows")))
async test "exit code indicates failure" {
let exit_code = @process.run("sh", ["-c", "exit 1"])
let is_failure = exit_code != 0
inspect(is_failure, content="true")
}trait ProcessInputimpl ProcessInput for PipeReadimpl ProcessInput for Inputtrait ProcessOutputimpl ProcessOutput for PipeWriteimpl ProcessOutput for Outputpub(all) struct CancellationHandler(async (Int) -> Unit)pub struct Process {
pid : Int
// private fields
}type ReadFromProcessimpl Reader for ReadFromProcessasync fn _direct_read(self : ReadFromProcess, buf : FixedArray[Byte], offset~ : Int, max_len~ : Int) -> Inttype WriteToProcessimpl Writer for WriteToProcessasync fn collect_output(cmd : StringView, args : ArrayView[String], extra_env? : Map[String, String], inherit_env? : Bool, stdin? : &ProcessInput, cwd? : StringView, no_console_window? : Bool) -> (Int, &Data, &Data)async fn collect_output_merged(cmd : StringView, args : Array[String], extra_env? : Map[String, String], inherit_env? : Bool, stdin? : &ProcessInput, cwd? : StringView, no_console_window? : Bool) -> (Int, &Data)async fn collect_stderr(cmd : StringView, args : ArrayView[String], extra_env? : Map[String, String], inherit_env? : Bool, stdin? : &ProcessInput, stdout? : &ProcessOutput, cwd? : StringView, no_console_window? : Bool) -> (Int, &Data)async fn collect_stdout(cmd : StringView, args : ArrayView[String], extra_env? : Map[String, String], inherit_env? : Bool, stdin? : &ProcessInput, stderr? : &ProcessOutput, cwd? : StringView, no_console_window? : Bool) -> (Int, &Data)async fn redirect_to_file(path : String, append? : Bool, create_mode? : CreateMode, permission? : Int, create? : Int, truncate? : Bool) -> &ProcessOutputasync fn run(cmd : StringView, args : ArrayView[String], extra_env? : Map[String, String], inherit_env? : Bool, stdin? : &ProcessInput, stdout? : &ProcessOutput, stderr? : &ProcessOutput, cwd? : StringView, no_console_window? : Bool, cancel_handler? : CancellationHandler) -> Intasync fn[X] spawn(group : TaskGroup[X], cmd : StringView, args : ArrayView[String], extra_env? : Map[String, String], inherit_env? : Bool, stdin? : &ProcessInput, stdout? : &ProcessOutput, stderr? : &ProcessOutput, cwd? : StringView, no_console_window? : Bool, cancel_handler? : CancellationHandler, no_wait? : Bool) -> Processasync fn spawn_orphan(cmd : StringView, args : ArrayView[String], extra_env? : Map[String, String], inherit_env? : Bool, stdin? : &ProcessInput, stdout? : &ProcessOutput, stderr? : &ProcessOutput, cwd? : StringView, no_console_window? : Bool) -> Intasync fn wait_pid(pid : Int) -> IntAsynchronous programming library for MoonBit