package test; // Plan-9-lean port of Hare's @test runner (ref/hare/test/+test.ha // __test_main:97-115, run_test:284, do_test:250). MECHANISM DIVERGENCE // (drew-17-attest-spec.md §a; rob ruling 2026-06-10, task #17): Hare // isolates each test in ONE process via arch::setjmp + an rt::onabort // hook + a SIGSEGV signal handler; ww has none of those primitives, so // we fork per test and read the child's wait-status. A clean child // exit(0) is a pass; abort/div0/SIGSEGV/nonzero-exit all surface as a // failing child status, so the fork boundary catches EVERY fault class // the setjmp+signal path catches, at a process boundary. The proven // fork+wait4+WEXITSTATUS decode is procrun (selfhost/cmd/ww/main.ww: // 154-177). CONSEQUENCE (sanctioned): module globals do NOT persist // test-to-test — each test runs in a fresh fork snapshot. Hare's tests // share one process, so its globals persist; ww's are hermetic. // // D1/D4/D5 reductions also retained from the -T synth era: no // __test_array linker section (the synth hands us a value table), no // sort, no file:line reflection. // // fnmatch NAME-FILTER (task #17 commit-3, drew spec §d): when the test // binary is invoked with trailing argv (the driver passes `ww test // [pattern]` through as argv[1..]), each pattern is a shell glob // matched against the test name via [[fnmatch.fnmatch]]; a test runs iff // some pattern matches. No pattern (argv.len == 1) runs ALL tests — the // no-filter path is byte-for-byte the pre-filter behavior. Zero matches // prints "No tests run" and exits 0, mirroring Hare's __test_main // (ref/hare/test/+test.ha:104-117). The synth (`run(__wwtests)`) and the // value table are UNCHANGED — the pattern reaches us via os.args, so // 990-997 byte-id and the no-pattern path hold. // // BUNDLE-FLOOR CONSEQUENCE (sanctioned, task #17): importing fnmatch // raises lib/test's auto-bundle floor from os-only to // os+fnmatch+ascii+strings (+their transitive bytes/encoding.utf8/types/ // rt) in EVERY -T build. fnmatch's module-private symbols co-bundle into // the same flat "" scope as the test's modules (pre-#8, no package // isolation); a same-named module-private fn in a tested module would // collide. Accepted as the cost of name-filtering; #8 (real packages) // retires it. import fnmatch; import os; // run — execute each test in `tests` in a forked subprocess, report a // per-test status line, and return the failure count (the synthesized // `export fn main()` returns this, so the process exits nonzero iff any // test failed). The table is `[](str, *fn() void)` — Hare models a test // as `struct { name: str, func: *fn() void }` (+test.ha:23-26); the -T // synth emits the value-table tuple form (rob ruling), so the runner // reads `.0`/`.1` off each row rather than named fields. // // The private helpers carry a `tst` prefix: until real packages (#8) give // lib/test symbol isolation, every -T build flat-bundles this module into // the SAME bare-leaf scope as the test's own modules, so an un-prefixed // `puts`/`runone` collides with a same-named module-private fn (lib/dirs // and lib/temp both ship a private `puts(off, s)`). `run` is the bound // runner name (the synth's callee), reached module-qualified as `test.run`: // the -T synth prepends `use test;` before name-binding (#80), so this // runner keys into the `test` module namespace — a symbol distinct from any // bare user `fn run` in the @test unit. The two COEXIST: `run` and // `test.run` are separate symbols (ref/hare/test/+test.ha:97 — the runner // is its own `test` module), so a user `fn run` is no longer a duplicate and // both stages accept it under @test/-T. export fn run(tests: [](str, *fn() void)) i32 = { let av: []str = os.args(); let nfail: i32 = 0; let nrun: i32 = 0; let i: i32 = 0; for (i < tests.len) { let row: (str, *fn() void) = tests[i]; let name: str = row.0; let f: *fn() void = row.1; if (tstenabled(name, av)) { tstputs(name); tstputs(" ... "); let st: i32 = tstrunone(f); if (st == 0) { tstputs("ok\n"); } else { tstputs("FAIL\n"); nfail += 1; }; nrun += 1; }; i += 1; }; // Zero tests selected (an over-narrow pattern, or none matched): Hare // prints "No tests run" and returns 0 regardless of whether a pattern // was given (ref/hare/test/+test.ha:114-117). if (nrun == 0) { tstputs("No tests run\n"); return 0; }; tstputuint(nrun - nfail); tstputs(" passed, "); tstputuint(nfail); tstputs(" failed\n"); return nfail; }; // tstenabled — does `name` pass the name-filter? `av` is os.args(): // av[0] is the program name, av[1..] the glob patterns the driver // forwarded. No patterns (av.len <= 1) enables every test; otherwise a // test is enabled iff SOME pattern fnmatch-matches (Hare's OR-over-args, // ref/hare/test/+test.ha:106-113). DIVERGENCE: Hare iterates all of // os::args including [0]; ww skips [0] — the test binary path is never a // test-name pattern (the driver passes the pattern as the first // post-program arg, never argv[0]). fn tstenabled(name: str, av: []str) bool = { if (av.len <= 1) { return true; }; let j: i32 = 1; for (j < av.len) { if (fnmatch.fnmatch(av[j], name, fnmatch.flag.NONE)) { return true; }; j += 1; }; return false; }; // runone — fork, run `f` in the child, decode the child's wait-status. // Byte-for-byte the procrun (main.ww:154-177) status decode: low 7 bits // are the killing signal (abort=SIGABRT, div0/SIGSEGV), 0 if the child // exited; next byte is the exit code. fn tstrunone(f: *fn() void) i32 = { let pid: i32 = os.fork(); if (pid < 0) { return -1; }; if (pid == 0) { (*f)(); os.exit(0i32); }; let status: i32 = 0; let r: i32 = os.wait4(pid, &status, 0i32, nil: *void); if (r < 0) { return -1; }; if ((status & 127i32) != 0) { return 1; }; return (status >> 8i32) & 255i32; }; fn tstputs(s: str) void = { os.write(os.STDOUT_FILENO, s.ptr, s.len: u64); }; // tstputuint — write a non-negative i32 in decimal. Lean substitute for // fmt::printf (Hare's test uses fmt); keeping lib/test's bundle floor at // os-only avoids pulling the io/strconv stack into every -T build. fn tstputuint(n: i32) void = { let buf: [16]u8; let i: i32 = 16; let v: i32 = n; if (v == 0) { i -= 1; buf[i] = 48u8; } else { for (v > 0) { i -= 1; buf[i] = (48 + v % 10): u8; v = v / 10; }; }; os.write(os.STDOUT_FILENO, &buf[i], (16 - i): u64); };