// shlextest — exercises lib/shlex. Run with // `out/bin/ww run lib/shlex/shlextest.ww`. // // Two cohorts, grouped Hare-style — one @test fn per cohort, table- // driven inside via per-arity helpers: // // • split rows go through [[check1]] / [[check2]] / [[check3]] / // [[checkerr]]. Inputs are lifted from ref/hare/shlex/+test.ha // @test fn split() (the de-facto spec for this port). Per-row // arity is fixed (1, 2, or 3 expected tokens across the Hare // cases), so we ship per-arity helpers rather than a full // variadic check that would obscure the row data. // // • quote rows go through [[checkquote]] — uniform (input, expected) // shape against a memio.dynamic sink, mirroring Hare's escape.ha // testquote table. // // Failure path: each @test fn bumps `signalled` to its slot index, // the helpers do `exit(signalled + 10)` on miscompare so the harness // reports `WEXITSTATUS = 11..N` pointing at the failing scenario. // Same convention as fnmatchtest / logtest. package shlex; import shlex; import io; import memio; // Direct rt_syscall binding rather than `use os;` — os exports // read/write/close, which collide with io.read/write/close under the // driver's flat-scope concat. Mirrors fnmatchtest / logtest / fmttest // / bufiotest. @symbol("rt_syscall") fn syscall1ww(num: i64, a: i64) i64; fn doexit(code: i32) void = { syscall1ww(60i64, code: i64); }; let signalled: i32 = 0; fn fail() void = { doexit(signalled + 10); }; fn streq(a: str, b: str) bool = { if (a.len != b.len) { return false; }; let i: i32 = 0; for (i < a.len) { if (a[i] != b[i]) { return false; }; i += 1; }; return true; }; // checkN — split `in` and assert the result is a slice of length N // matching the named expected tokens. Leaks the result (test process // is short-lived; same precedent as fnmatchtest). fn check1(in: str, e0: str) void = { let r = shlex.split(in); match (r) { case shlex.syntaxerr => { fail(); }; case let s: []str => { if (s.len != 1) { fail(); }; if (!streq(s[0], e0)) { fail(); }; }; }; }; fn check2(in: str, e0: str, e1: str) void = { let r = shlex.split(in); match (r) { case shlex.syntaxerr => { fail(); }; case let s: []str => { if (s.len != 2) { fail(); }; if (!streq(s[0], e0)) { fail(); }; if (!streq(s[1], e1)) { fail(); }; }; }; }; fn check3(in: str, e0: str, e1: str, e2: str) void = { let r = shlex.split(in); match (r) { case shlex.syntaxerr => { fail(); }; case let s: []str => { if (s.len != 3) { fail(); }; if (!streq(s[0], e0)) { fail(); }; if (!streq(s[1], e1)) { fail(); }; if (!streq(s[2], e2)) { fail(); }; }; }; }; fn checkerr(in: str) void = { let r = shlex.split(in); match (r) { case shlex.syntaxerr => {}; case let s: []str => { fail(); }; }; }; fn checkempty(in: str) void = { let r = shlex.split(in); match (r) { case shlex.syntaxerr => { fail(); }; case let s: []str => { if (s.len != 0) { fail(); }; }; }; }; fn checkquote(in: str, expected: str) void = { let st: memio.stream = memio.dynamic(); let snk: io.stream = &st.vt; let r = shlex.quote(snk, in); let n: size = 0; match (r) { case let v: size => { n = v; }; case let _e: io.error => { fail(); }; }; if (n != expected.len: size) { fail(); }; let view: str = memio.string(&st); if (!streq(view, expected)) { fail(); }; let _c: (void | io.error) = io.close(snk); }; // ---- split: Hare's @test fn split() table -------------------------- // // 9 success rows + 3 syntaxerr rows ported VERBATIM from // ref/hare/shlex/+test.ha; plus one ww-specific edge (empty input → // empty []str) confirmed by drew. // // Local @test fns are `test_*`-prefixed because `use shlex;` flat- // concats shlex's exported names (split / quote / quotestr / strerror) // into the fixture's namespace, and bare `fn split() ...` would // duplicate-define them. Retires when task #17 (cgen mod-mangles fn // labels) lands. @test fn test_split() void = { check1("hello\\ world", "hello world"); check1("'hello\\ world'", "hello\\ world"); check1("\"hello\\\\world\"", "hello\\world"); // "hello "'"'"world"'"' → hello "world" check1("\"hello \"'\"'\"world\"'\"'", "hello \"world\""); check3("hello '' world", "hello", "", "world"); check2("Empty ''", "Empty", ""); check2(" Leading spaces", "Leading", "spaces"); check3("with\\ backslashes 'single quoted' \"double quoted\"", "with backslashes", "single quoted", "double quoted"); check2("'multiple spaces' 42", "multiple spaces", "42"); // Invalid checkerr("\"dangling double quote"); checkerr("'dangling single quote"); checkerr("unterminated\\ backslash \\"); // Empty input → empty []str (ww edge confirmed by drew). checkempty(""); }; // ---- quote: Hare's testquote rows + the empty-input edge ---------- // // 4 rows from ref/hare/shlex/+test.ha @test fn quote(). The empty- // input row (→ `''`) is implementation-specific (Hare's testquote // doesn't cover it) but is documented behaviour per shlex.ww's // quote() header — exercised here so the contract is load-bearing. @test fn test_quote() void = { checkquote("hello", "hello"); checkquote("hello world", "'hello world'"); checkquote("'hello' \"world\"", "''\"'\"'hello'\"'\"' \"world\"'"); checkquote("hello\\world", "'hello\\world'"); checkquote("", "''"); }; // ---- quotestr ------------------------------------------------------ @test fn test_quotestr() void = { let r: str = shlex.quotestr("hello world"); if (!streq(r, "'hello world'")) { fail(); }; // leak r — short-lived test process, same precedent as fnmatchtest. }; // ---- strerror ------------------------------------------------------ @test fn test_strerror() void = { let e: shlex.syntaxerr; let s: str = shlex.strerror(e); if (!streq(s, "Invalid shell syntax")) { fail(); }; }; export fn main() i32 = { signalled = 1; test_split(); signalled = 2; test_quote(); signalled = 3; test_quotestr(); signalled = 4; test_strerror(); return 0; };