1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
//! Cross-engine regression tests for the `run` builtin: argv-list process
//! spawn returning `R (M t t) t`. Mirrors the trm/padl shape — every engine
//! must agree on the result Map.
//!
//! The `run` builtin is the new 0.12.0 process-spawn primitive. argv-list
//! only (no shell, no interpolation, no glob). Non-zero exit is NOT an
//! error — it surfaces as `Ok({"code":"<n>",...})`. Spawn failure IS an
//! error and surfaces as `Err(...)`.
//!
//! Coverage matrix (every test runs on tree/VM/Cranelift):
//! - happy path: stdout captured, code is "0"
//! - non-zero exit: code is "1", no Err
//! - spawn failure: returns Err (command not found)
//! - argv with multiple args: each entry passed verbatim
//! - empty argv list works (`run "true" []`)
//! - stderr captured separately
//! - `$` sigil equivalence: `$cmd argv` produces identical Map to
//! `run cmd argv`
//! - `post` name is GONE — `post u b` produces ILO-T005 (rename pst)
use std::process::Command;
fn ilo() -> Command {
Command::new(env!("CARGO_BIN_EXE_ilo"))
}
#[cfg(feature = "cranelift")]
const ENGINES_ALL: &[&str] = &["--vm", "--jit"];
#[cfg(not(feature = "cranelift"))]
const ENGINES_ALL: &[&str] = &["--vm"];
/// Run an ilo source string on `engine`, expect success, return stdout.
///
/// Positional argv shape mirrors `tests/regression_pad.rs`: the source
/// string is the first positional, the engine flag follows, then the
/// entry-function name + its call args.
fn run_ok(engine: &str, src: &str, args: &[&str]) -> String {
let mut cmd = ilo();
cmd.arg(src).arg(engine);
for a in args {
cmd.arg(a);
}
let out = cmd.output().expect("failed to run ilo");
assert!(
out.status.success(),
"ilo {engine} {src:?} {args:?} failed: stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let s = String::from_utf8_lossy(&out.stdout);
s.trim_end_matches('\n').to_string()
}
/// Run an ilo source string on `engine`, expect non-success, return stderr.
fn run_err(engine: &str, src: &str, args: &[&str]) -> String {
let mut cmd = ilo();
cmd.arg(src).arg(engine);
for a in args {
cmd.arg(a);
}
let out = cmd.output().expect("failed to run ilo");
assert!(
!out.status.success(),
"ilo {engine} {src:?} {args:?} unexpectedly succeeded: stdout={}",
String::from_utf8_lossy(&out.stdout)
);
String::from_utf8_lossy(&out.stderr).to_string()
}
// ─── Happy path ──────────────────────────────────────────────────────────
#[test]
fn run_echo_returns_stdout_and_zero_code_cross_engine() {
// `run "echo" ["hi"]` — pull stdout out and print it. The outer fn
// returns R t t so `run!` propagates spawn failures cleanly.
let src = r#"f>t;m=run!! "echo" ["hi"];??mget m "stdout" """#;
for engine in ENGINES_ALL {
let out = run_ok(engine, src, &["f"]);
assert!(
out.contains("hi"),
"{engine}: expected stdout=hi, got {out:?}"
);
}
}
#[test]
fn run_echo_code_is_zero_cross_engine() {
// Pull `code` out: "0" on success.
let src = r#"f>t;m=run!! "echo" ["hi"];??mget m "code" """#;
for engine in ENGINES_ALL {
let out = run_ok(engine, src, &["f"]);
assert_eq!(out, "0", "{engine}: expected code=0 from echo");
}
}
// ─── Non-zero exit is NOT an error ───────────────────────────────────────
#[test]
fn run_false_returns_code_one_no_err_cross_engine() {
// `false` exits 1 — must surface as Ok({"code":"1",...}), NOT Err.
// Using `run!` proves the result is Ok (otherwise `!` would propagate
// the Err and abort with exit 1 and no stdout).
let src = r#"f>t;m=run!! "false" [];??mget m "code" """#;
for engine in ENGINES_ALL {
let out = run_ok(engine, src, &["f"]);
assert_eq!(out, "1", "{engine}: expected code=1 from false");
}
}
// ─── Spawn failure IS an error ───────────────────────────────────────────
#[test]
fn run_unknown_cmd_returns_err_cross_engine() {
// Spawn failure (command not found) must surface as Err. We branch
// on the Result so the test exits cleanly (status=success) and we
// can assert on the Err message.
let src = r#"f>t;r=run "no-such-cmd-xyz123-ilo-test" [];?r{~_:"unexpected ok";^er:er}"#;
for engine in ENGINES_ALL {
let out = run_ok(engine, src, &["f"]);
assert!(
out.contains("failed to spawn") && out.contains("no-such-cmd-xyz123-ilo-test"),
"{engine}: expected spawn-failure Err, got {out:?}"
);
}
}
// ─── argv with multiple args, each entry passed verbatim ─────────────────
#[test]
fn run_argv_multiple_args_each_verbatim_cross_engine() {
// `printf "%s|%s|%s" a b c` — printf is a portable utility that prints
// its argv entries with a known separator, proving each list element
// is passed as a distinct argv entry (no shell join, no glob).
let src = r#"f>t;m=run!! "printf" ["%s|%s|%s" "a" "b" "c"];??mget m "stdout" """#;
for engine in ENGINES_ALL {
let out = run_ok(engine, src, &["f"]);
assert_eq!(
out, "a|b|c",
"{engine}: expected argv entries to be passed verbatim"
);
}
}
// ─── Empty argv list works ───────────────────────────────────────────────
#[test]
fn run_true_empty_argv_returns_code_zero_cross_engine() {
// `run "true" []` — empty argv list. Spawn must round-trip and code
// must be "0".
let src = r#"f>t;m=run!! "true" [];??mget m "code" """#;
for engine in ENGINES_ALL {
let out = run_ok(engine, src, &["f"]);
assert_eq!(
out, "0",
"{engine}: expected code=0 from `true` with empty argv"
);
}
}
// ─── Stderr captured separately ──────────────────────────────────────────
#[test]
fn run_stderr_captured_separately_cross_engine() {
// `sh -c "echo err 1>&2"` emits ONLY to stderr — the test exercises
// `run`'s argv plumbing (zero shell on ilo's side) via a utility that
// happens to be a shell. ilo passes `["-c", "echo err 1>&2"]` to `sh`
// as distinct argv entries; sh itself does the parsing.
let src = r#"f>t;m=run!! "sh" ["-c" "echo err 1>&2"];??mget m "stderr" """#;
for engine in ENGINES_ALL {
let out = run_ok(engine, src, &["f"]);
assert!(
out.contains("err"),
"{engine}: expected stderr=err, got {out:?}"
);
}
}
#[test]
fn run_stderr_only_leaves_stdout_empty_cross_engine() {
// Same call as above but pulls stdout; should be empty because the
// child emitted only to stderr.
let src = r#"f>t;m=run!! "sh" ["-c" "echo err 1>&2"];??mget m "stdout" """#;
for engine in ENGINES_ALL {
let out = run_ok(engine, src, &["f"]);
assert_eq!(
out, "",
"{engine}: stderr-only emit must leave stdout empty, got {out:?}"
);
}
}
// ─── `$` sigil equivalence: $cmd argv ≡ run cmd argv ────────────────────
#[test]
fn dollar_sigil_equivalent_to_run_cross_engine() {
// `$cmd argv` must produce a Map identical to `run cmd argv`. We
// pull stdout from each and confirm they match.
let lhs_src = r#"f>t;m=$!!"echo" ["hi"];??mget m "stdout" """#;
let rhs_src = r#"f>t;m=run!! "echo" ["hi"];??mget m "stdout" """#;
for engine in ENGINES_ALL {
let lhs = run_ok(engine, lhs_src, &["f"]);
let rhs = run_ok(engine, rhs_src, &["f"]);
assert_eq!(
lhs, rhs,
"{engine}: $ sigil and run must produce identical Maps"
);
assert!(
lhs.contains("hi"),
"{engine}: expected stdout=hi via $ sigil"
);
}
}
// ─── `post` is undefined post-rename ────────────────────────────────────
#[test]
fn post_is_undefined_after_pst_rename() {
// 0.12.0 renamed `post` → `pst`. `post u b` must now error
// ILO-T005 (undefined function) with a did-you-mean to `pst`.
let stderr = run_err(
"--vm",
r#"f u:t b:t>R t t;post u b"#,
&["f", "http://x", "body"],
);
assert!(
stderr.contains("ILO-T005"),
"expected ILO-T005, got: {stderr}"
);
assert!(
stderr.contains("post"),
"expected error to mention post, got: {stderr}"
);
assert!(
stderr.contains("pst"),
"expected did-you-mean to suggest pst, got: {stderr}"
);
}
// ─── pst smoke test (basic verify path) ──────────────────────────────────
#[test]
fn pst_parses_and_verifies() {
// Smoke test — `pst url body` must verify cleanly. We don't make a
// real HTTP call; just confirm the AST round-trips.
let out = ilo()
.args(["--ast", r#"f url:t body:t>R t t;pst url body"#])
.output()
.expect("failed to run ilo");
assert!(
out.status.success(),
"pst should verify cleanly; stderr={}",
String::from_utf8_lossy(&out.stderr)
);
}