Something went wrong. Try again.
The Monad language. Dependent types, functional programming compiled with LLVM. Hobby project. monad-lang.org
dependent-types language compiler programming-language functional-programming
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276/// Tests for `std/src/process.mo`.////// A separate file because `process.mo` is re-exported by `std/src/lib.mo`/// and therefore seeded ambiently into every module closure, which makes/// a `#[test]` inside it undiscoverable -- see that file's note. Same/// reason `path_tests.mo` and `sha256_tests.mo` are separate.////// Same mote, so `Proc.first_quote` stays package-private and is still/// reachable here -- no API had to be widened to test it.use lib::process {}// ─── Tests: shell_quote ───#[test]def test_shell_quote_wraps_in_single_quotes : Bool := String.beq (Proc.shell_quote "abc") "'abc'"#[test]def test_shell_quote_keeps_a_space_in_one_word : Bool := String.beq (Proc.shell_quote "a b") "'a b'"#[test]def test_shell_quote_empty_is_an_empty_word : Bool := String.beq (Proc.shell_quote "") "''"/// The whole point: an embedded quote closes, escapes, and reopens.#[test]def test_shell_quote_escapes_an_embedded_quote : Bool := String.beq (Proc.shell_quote "it's") "'it'\\''s'"#[test]def test_shell_quote_escapes_every_quote : Bool := String.beq (Proc.shell_quote "'a'") "''\\''a'\\'''"#[test]def test_shell_quote_leaves_a_dollar_sign_alone : Bool := String.beq (Proc.shell_quote "$HOME") "'$HOME'"#[test]def test_first_quote_finds_nothing_in_a_plain_word : Bool := match Proc.first_quote "abc" { Option.none => true, Option.some _ => false }#[test]def test_first_quote_finds_the_first_of_two : Bool := match Proc.first_quote "ab'c'd" { Option.some i => i == 2, Option.none => false }// ─── Tests: capture ───#[test]def test_capture_returns_status_and_stdout : IO Bool := do { let r <- Proc.capture "echo" ["hi"]; match r { Pair.pair code out => return (code == 0 && String.beq out "hi\n") }}/// Exit status and stderr in one assertion, because both are things/// `exec_cmd` alone cannot give a caller.#[test]def test_capture_merges_stderr_and_keeps_the_exit_status : IO Bool := do { let r <- Proc.capture "sh" ["-c", "echo e >&2; exit 3"]; match r { Pair.pair code out => return (code == 3 && String.beq out "e\n") }}/// The injection test. `$HOME` must arrive at `echo` as four literal/// characters; if `Proc.shell_quote` is ever dropped from the argv/// construction, the shell expands it here and this fails.#[test]def test_capture_does_not_expand_a_dollar_sign : IO Bool := do { let r <- Proc.capture "echo" ["$HOME"]; match r { Pair.pair _code out => return (String.beq out "$HOME\n") }}/// A word with a quote in it survives the round trip through `sh -c`.#[test]def test_capture_passes_an_embedded_quote_through : IO Bool := do { let r <- Proc.capture "echo" ["it's"]; match r { Pair.pair _code out => return (String.beq out "it's\n") }}/// A command that does not exist is a nonzero status and a message, not a/// crash and not an empty string.#[test]def test_capture_of_a_missing_command_is_nonzero : IO Bool := do { let r <- Proc.capture "monad_no_such_command_exists" []; match r { Pair.pair code _out => return (Bool.not (code == 0)) }}// ─── Tests: spawn ──────────────────────────────────────────────────────────//// The piped-subprocess natives. These spawn real children (`echo`, `cat`,// `sh`) rather than mocking the pipe, so each one exercises the actual// fork/exec path on whichever backend runs it. Read assertions route// through the `pr_*` helpers below for the same reason toolkit's framing// tests use `fr_*`: one match per helper, so no `#[test]` def nests two// differently-typed matches -- the recorded self-hosted codegen hazard --// and a prefixed name that cannot collide with another mote's test// helpers in the shared whole-program scope./// The payload says `expected` -- `Result.ok (Option.some expected)`.def pr_some_is (o : Option String) (expected : String) : Bool := match o { Option.some s => String.beq s expected, Option.none => false }def pr_line_is (l : Result String (Option String)) (expected : String) : Bool := match l { Result.ok o => pr_some_is o expected, Result.err _ => false }/// End of stream: `Result.ok Option.none`, which is a clean end and not/// an error -- the distinction a protocol loop's termination hangs on.def pr_none_is (o : Option String) : Bool := match o { Option.none => true, Option.some _s => false }def pr_is_eof (l : Result String (Option String)) : Bool := match l { Result.ok o => pr_none_is o, Result.err _ => false }/// A write reports the byte count it accepted.def pr_wrote (w : Result String U64) (expected : U64) : Bool := match w { Result.ok n => U64.beq n expected, Result.err _ => false }/// The whole read loop in miniature: one line, then end of stream, then a/// clean exit. Everything a newline-framed protocol peer does, in order.#[test]def test_spawn_echo_reads_a_line_then_eof_then_waits_zero : IO Bool := do { let r <- Proc.spawn "echo" ["hi"]; match r { Result.err _ => return false, Result.ok c => do { let l1 <- Proc.read_line c; let l2 <- Proc.read_line c; let code <- Proc.wait c; return (pr_line_is l1 "hi" && pr_is_eof l2 && code == 0) } }}/// `cat` is the honest roundtrip: what we wrote must come back, and the/// byte count must be the string's length. `wait` closing stdin first is/// what lets the child exit at all, so this also pins that ordering.#[test]def test_spawn_cat_roundtrips_a_written_line : IO Bool := do { let r <- Proc.spawn "cat" []; match r { Result.err _ => return false, Result.ok c => do { let w <- Proc.write c "hello\n"; let l <- Proc.read_line c; let code <- Proc.wait c; return (pr_wrote w 6u64 && pr_line_is l "hello" && code == 0) } }}/// A server's stderr is its log stream, not its protocol stream: "err"/// must come out of `read_stderr_line` while "out" stays on stdout. If/// the pipes were merged (as `capture` merges them) this fails -- which/// is exactly why `capture` cannot back an MCP client.#[test]def test_spawn_keeps_stderr_out_of_the_protocol_stream : IO Bool := do { let r <- Proc.spawn "sh" ["-c", "echo err >&2; echo out"]; match r { Result.err _ => return false, Result.ok c => do { let e <- Proc.read_stderr_line c; let l <- Proc.read_line c; let code <- Proc.wait c; return (pr_line_is e "err" && pr_line_is l "out" && code == 0) } }}/// `exec_cmd`'s status convention, reached through the piped path: the/// child's own exit status, not a signal death.#[test]def test_spawn_wait_reports_the_exit_status : IO Bool := do { let r <- Proc.spawn "sh" ["-c", "exit 3"]; match r { Result.err _ => return false, Result.ok c => do { let code <- Proc.wait c; return (code == 3) } }}/// `kill` is the whole teardown: it SIGKILLs, reaps, and frees the/// slot. A `wait` AFTER `kill` therefore does not see a child at all/// -- its -1 is the freed-slot answer, not a signal-death report (the/// signal death was already reaped inside `kill` itself). This pins/// exactly that residual `wait`, because a disposer that calls both is/// the order old callers used and must keep working.#[test]def test_spawn_wait_after_kill_reports_a_freed_slot : IO Bool := do { let r <- Proc.spawn "sh" ["-c", "sleep 30"]; match r { Result.err _ => return false, Result.ok c => do { Proc.kill c; let code <- Proc.wait c; return (code == (0 - 1)) } }}/// A frame without its newline is a truncated message, not a message:/// `printf abc` (no newline) must read as end of stream, not "abc". Both/// backends agree on discarding, and this is the pin that keeps them/// agreeing.#[test]def test_spawn_discards_a_partial_line_at_eof : IO Bool := do { let r <- Proc.spawn "sh" ["-c", "printf abc"]; match r { Result.err _ => return false, Result.ok c => do { let l <- Proc.read_line c; let code <- Proc.wait c; return (pr_is_eof l && code == 0) } }}/// A spawn that cannot happen is `Result.err` with the OS message, not a/// native failure -- the caller composes it like every other error.#[test]def test_spawn_of_a_missing_command_is_an_error : IO Bool := do { let r <- Proc.spawn "monad_no_such_command_exists" []; match r { Result.err _ => return true, Result.ok c => do { let _code <- Proc.wait c; return false } }}/// A line is BOUNDED: 2 MB with no newline at all must be `Result.err`/// (the read failing at the bound), not a 2 MB "line" -- a peer that/// withholds its newline is a flood, and the flood must fail, not/// grow. Both backends carry the same number, and this is the pin that/// keeps them agreeing.#[test]def test_spawn_read_line_is_bounded : IO Bool := do { let r <- Proc.spawn "sh" ["-c", "head -c 2000000 /dev/zero | tr '\\0' 'a'"]; match r { Result.err _ => return false, Result.ok c => do { let l <- Proc.read_line c; // The child exits on its own once the pipe closes; the // verdict is the read's, so `wait` is cleanup either way. let _code <- Proc.wait c; return (match l { Result.err _ => true, Result.ok _ => false, }) } }}