`ww test <file> <pattern>` runs only the @test fns whose names match the fnmatch glob; no pattern runs all (byte-for-byte the pre-filter path); zero matches prints "No tests run" and exits 0 (Hare ground truth ref/hare/test/+test.ha:114-117). A pattern in directory mode is rejected "ww test: pattern needs a single test file" (rc 2), identical wording in both twins (cmd/ww/main.c do_test + selfhost/cmd/ww/main.ww dotest). Mechanism (a): rt/start.s stashes argc/argv into rt_argc/rt_argv getters (rt_envp twin shape, -T synth untouched so 990-997 byte-id holds); lib/os.args() rebuilds the []str view, build-once-cached; lib/test/run.ww imports fnmatch and filters av[1..] (argv[0] is the binary path). The driver forwards the 2nd positional as argv[1] via fork/execv (cstage) / procrun (wwstage) so glob metachars aren't shell-expanded. os.args() is the first `alloc`-caller in the base os module, so os.ww now imports rt — the `alloc` builtin's malloc lowers to rt_malloc only when the rt binding is bundled (mirror lib/strings/strings.ww:30); without it a plain `ww build` of any os-importing program links bare libc `malloc` (undefined). os is bundled by ~every program, so this is load-bearing. The lib/test floor rises os-only -> os+fnmatch+ascii+strings in every -T build; the bundled `ascii` module vs a `@test fn ascii` collision that exposed is closed by the preceding #30 promote commit. 989_test_filter pins the full matrix on both twins byte-identically; 949 gains the dir-mode reject row. (#17)
161 lines
6.3 KiB
Plaintext
161 lines
6.3 KiB
Plaintext
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
|
|
// <file> [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` stays the bound
|
|
// runner name (the synth's callee). A user `fn run` (or `const __wwtests`)
|
|
// in a @test unit collides with the synth: cstage — the shipping `ww test`
|
|
// path — rejects it loudly ("duplicate fn run" / type mismatch, rc!=0).
|
|
// wwstage TOLERATES the duplicate (#23, pre-existing rule-10 gap: cstage
|
|
// rejects a duplicate fn, wwstage does not), so the collision is loud on
|
|
// the reference stage but silent on wwstage until #23 lands.
|
|
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);
|
|
};
|