ww test: @test name-filter via fnmatch (both stages)
`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)
This commit is contained in:
@@ -229,7 +229,7 @@ fn findflag(hs: []help, r: rune) (helpkind | void) = {
|
||||
|
||||
// tryparse — parse `argv` against `help`, filling `*out` on success.
|
||||
// `argv` must include the program name in argv[0]; matches Hare and
|
||||
// matches the slice [[os.args]] would hand back.
|
||||
// matches the slice [[os.args]] hands back.
|
||||
//
|
||||
// Recognised forms:
|
||||
// -X FLAG-kind option
|
||||
|
||||
52
lib/os/os.ww
52
lib/os/os.ww
@@ -5,6 +5,12 @@
|
||||
package os;
|
||||
|
||||
import time;
|
||||
// [[args]] allocates the []str view via the `alloc` builtin, whose malloc
|
||||
// lowers to rt_malloc only when the rt binding is in the bundle (mirror
|
||||
// lib/strings/strings.ww:30 — every alloc-using module imports rt). os is
|
||||
// bundled by ~every program, so without this a plain `ww build` of any
|
||||
// os-importing program links bare libc `malloc` (undefined). Task #17.
|
||||
import rt;
|
||||
|
||||
@symbol("rt_syscall") fn syscall0(num: nr) i64;
|
||||
@symbol("rt_syscall") fn syscall1(num: nr, a: i64) i64;
|
||||
@@ -403,6 +409,12 @@ export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = {
|
||||
// We don't expose `rtenvp` directly; [[getenv]] is the only consumer.
|
||||
@symbol("rt_envp") fn rtenvp() **u8;
|
||||
|
||||
// rt_argc / rt_argv — runtime-side getters for the argc/argv captured by
|
||||
// rt/start.s at process entry (same DATAW-slot + TEXT-getter shape as
|
||||
// rt_envp). [[args]] is the only consumer.
|
||||
@symbol("rt_argc") fn rtargc() i64;
|
||||
@symbol("rt_argv") fn rtargv() **u8;
|
||||
|
||||
// getenv — POSIX getenv. Returns a borrowed `str` view over the value
|
||||
// bytes of the named environment variable, or void if the name is not
|
||||
// present. The view is valid for the process lifetime — the bytes
|
||||
@@ -451,6 +463,46 @@ export fn getenv(name: str) (str | void) = {
|
||||
return;
|
||||
};
|
||||
|
||||
// argsbuilt / argscache — build-once cache for [[args]]. drew ruling
|
||||
// (task #17): an explicit `built` sentinel, NOT len==0 overloading (a
|
||||
// real argv always has argv[0], but the sentinel keeps the contract
|
||||
// honest and decoupled from content). args() is loop-callable; under
|
||||
// ww's no-free model a per-call rebuild would leak the slice each call,
|
||||
// so the slice is materialised once and reused.
|
||||
let argsbuilt: bool = false;
|
||||
let argscache: []str;
|
||||
|
||||
// args — the process arguments as a borrowed []str. args[0] is the
|
||||
// program name; args[1..] are the invocation arguments. Each str views
|
||||
// the NUL-terminated argv bytes in place (valid for the process
|
||||
// lifetime), so the slice must not be mutated or freed by the caller.
|
||||
//
|
||||
// DIVERGENCE (Hare-fidelity, task #26): Hare's `os::args` is a `[]str`
|
||||
// GLOBAL populated by an @init that walks rt's argv
|
||||
// (ref/hare/os/+linux/start.ha). ww has no @init mechanism, so the
|
||||
// faithful global is not expressible; this is the fn-shaped equivalent
|
||||
// (Hare NAME kept, shape diverged). Revisit if @init lands (#26).
|
||||
export fn args() []str = {
|
||||
if (argsbuilt) { return argscache; };
|
||||
let argc: i32 = rtargc(): i32;
|
||||
let argv: **u8 = rtargv();
|
||||
let r: []str = alloc([], argc: u64)!;
|
||||
let i: i32 = 0;
|
||||
for (i < argc) {
|
||||
let c: *u8 = argv[i];
|
||||
let n: i32 = 0;
|
||||
for (c[n] != 0u8) { n += 1; };
|
||||
let s: str;
|
||||
s.ptr = c;
|
||||
s.len = n;
|
||||
append(r, s);
|
||||
i += 1;
|
||||
};
|
||||
argscache = r;
|
||||
argsbuilt = true;
|
||||
return r;
|
||||
};
|
||||
|
||||
// ---- stat / lstat / fstat / exists -----------------------------------
|
||||
//
|
||||
// Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 +
|
||||
|
||||
@@ -16,9 +16,29 @@ package test;
|
||||
//
|
||||
// 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-filtering is task #17
|
||||
// commit-3 (drew spec §d), not here.
|
||||
// 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
|
||||
@@ -41,30 +61,62 @@ import os;
|
||||
// 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;
|
||||
tstputs(name);
|
||||
tstputs(" ... ");
|
||||
let st: i32 = tstrunone(f);
|
||||
if (st == 0) {
|
||||
tstputs("ok\n");
|
||||
} else {
|
||||
tstputs("FAIL\n");
|
||||
nfail += 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;
|
||||
};
|
||||
tstputuint(tests.len - nfail);
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user