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:
@@ -95,6 +95,29 @@ export fn compare(a: instant, b: instant) i8 = {
|
||||
return 0i8;
|
||||
};
|
||||
|
||||
// rt — runtime primitives exposed to ww programs.
|
||||
// Mirrors Hare's rt:: module placement (ref/hare/rt/).
|
||||
|
||||
package rt;
|
||||
|
||||
// malloc — mmap-backed page allocator. Untyped: `malloc(n)` returns a
|
||||
// `*void`; callers cast to the target type. Diverges from Hare: Hare
|
||||
// exposes `alloc` / `free` as typed language builtins that the
|
||||
// compiler lowers to rt::malloc/rt::free; ww has no such builtins,
|
||||
// so the rt-symbol surface is exposed directly. Stdlib callers that
|
||||
// need a typed allocation pattern wrap this with a cast plus a stored
|
||||
// capacity (see [[strings.dup]], [[memio.dynamic]]).
|
||||
//
|
||||
// OOM: rt_malloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with no
|
||||
// error path. The raw Linux mmap syscall returns a negative errno cast
|
||||
// to `*void` on failure (e.g. `(void*)-12` for ENOMEM); the
|
||||
// `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention that
|
||||
// rt_malloc doesn't apply. Neither `== nil` nor `== (void*)-1` catches
|
||||
// it; any deref of such a return faults. Today the stdlib does not
|
||||
// check; OOM faults on first dereference. A typed fallible variant is
|
||||
// a future task (task #39). ref/hare/rt/malloc.ha:27.
|
||||
@symbol("rt_malloc") export fn malloc(n: u64) *void;
|
||||
|
||||
// os — process and filesystem facade. The body of each call lands
|
||||
// either in libwwrt.a (rt_syscall trampoline) or libc bindings,
|
||||
// depending on how the program was linked.
|
||||
@@ -102,6 +125,12 @@ export fn compare(a: instant, b: instant) i8 = {
|
||||
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;
|
||||
@@ -500,6 +529,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
|
||||
@@ -548,6 +583,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 +
|
||||
@@ -781,29 +856,6 @@ export fn exists(path: str) bool = {
|
||||
return r >= 0i64;
|
||||
};
|
||||
|
||||
// rt — runtime primitives exposed to ww programs.
|
||||
// Mirrors Hare's rt:: module placement (ref/hare/rt/).
|
||||
|
||||
package rt;
|
||||
|
||||
// malloc — mmap-backed page allocator. Untyped: `malloc(n)` returns a
|
||||
// `*void`; callers cast to the target type. Diverges from Hare: Hare
|
||||
// exposes `alloc` / `free` as typed language builtins that the
|
||||
// compiler lowers to rt::malloc/rt::free; ww has no such builtins,
|
||||
// so the rt-symbol surface is exposed directly. Stdlib callers that
|
||||
// need a typed allocation pattern wrap this with a cast plus a stored
|
||||
// capacity (see [[strings.dup]], [[memio.dynamic]]).
|
||||
//
|
||||
// OOM: rt_malloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with no
|
||||
// error path. The raw Linux mmap syscall returns a negative errno cast
|
||||
// to `*void` on failure (e.g. `(void*)-12` for ENOMEM); the
|
||||
// `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention that
|
||||
// rt_malloc doesn't apply. Neither `== nil` nor `== (void*)-1` catches
|
||||
// it; any deref of such a return faults. Today the stdlib does not
|
||||
// check; OOM faults on first dereference. A typed fallible variant is
|
||||
// a future task (task #39). ref/hare/rt/malloc.ha:27.
|
||||
@symbol("rt_malloc") export fn malloc(n: u64) *void;
|
||||
|
||||
// types — integer limits. Mirrors Hare's types::limits (I8_MAX, …)
|
||||
// platform-fixed for amd64. Numeric helpers live in lib/math, matching
|
||||
// Hare's split between types::limits and math::.
|
||||
@@ -4280,7 +4332,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
||||
// directory: open the dir, getdents64, build+run each *_test.ww,
|
||||
// report ok/FAIL per file, return 0 iff all pass.
|
||||
|
||||
fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8) i32 = {
|
||||
fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8, pattern: *u8) i32 = {
|
||||
let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!;
|
||||
tmp.len = os.PATH_MAX;
|
||||
// -o redirects the binary + its combined (objstem, T3) to <stem>; the
|
||||
@@ -4299,10 +4351,19 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *
|
||||
return 1;
|
||||
};
|
||||
if (compileonly != 0) { return 0; };
|
||||
let execargv: []*u8 = alloc([], 2u64)!;
|
||||
execargv.len = 2;
|
||||
// #17 fnmatch filter: forward `pattern` as argv[1] so lib/test run()
|
||||
// reads it via os.args. procrun execve's a NUL-terminated argv, so the
|
||||
// terminator (not .len) bounds the vector. cstage twin: run_test_bin.
|
||||
let execargv: []*u8 = alloc([], 3u64)!;
|
||||
execargv[0] = outp;
|
||||
execargv[1] = nil;
|
||||
if (pattern != nil) {
|
||||
execargv.len = 3;
|
||||
execargv[1] = pattern;
|
||||
execargv[2] = nil;
|
||||
} else {
|
||||
execargv.len = 2;
|
||||
execargv[1] = nil;
|
||||
};
|
||||
let rc: i32 = procrun(outp, execargv.ptr);
|
||||
if (outstem == nil) { os.remove(pathstr(outp)); };
|
||||
return rc;
|
||||
@@ -4390,6 +4451,10 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
||||
// transitive imports (e.g. 905_nkname asttest → tok); coupled to the
|
||||
// -T flip (task #5/#10).
|
||||
let target: *u8 = nil;
|
||||
// #17: optional 2nd positional = fnmatch name-filter pattern, forwarded
|
||||
// to the test binary as argv[1] (single-file/module only; dir-mode
|
||||
// rejects). cstage twin: do_test `pattern`.
|
||||
let patarg: *u8 = nil;
|
||||
let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!;
|
||||
incs.len = ((os.PATH_MAX: u64) * 2u64): i32;
|
||||
let incoff: u64 = 0u64;
|
||||
@@ -4440,7 +4505,8 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
||||
return 2;
|
||||
}; }; };
|
||||
} else {
|
||||
if (target == nil) { target = p; };
|
||||
if (target == nil) { target = p; }
|
||||
else { if (patarg == nil) { patarg = p; }; };
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
@@ -4452,7 +4518,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
||||
// single-file mode: literal *.ww that exists
|
||||
if (cstrendswithlit(target, ".ww")) {
|
||||
if (os.access(pathstr(target), 0i32) == 0) {
|
||||
return runsingletest(selfdir, target, incs.ptr, compileonly, outstem);
|
||||
return runsingletest(selfdir, target, incs.ptr, compileonly, outstem, patarg);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4460,6 +4526,12 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
||||
cerr("ww test: -c/-o need a single test file\n");
|
||||
return 2;
|
||||
};
|
||||
// #17: a name-filter pattern is per-binary; dir mode builds one binary
|
||||
// per *_test.ww, so a single pattern can't route. cstage twin parity.
|
||||
if (patarg != nil) {
|
||||
cerr("ww test: pattern needs a single test file\n");
|
||||
return 2;
|
||||
};
|
||||
// otherwise treat target as a directory; enumerate *_test.ww
|
||||
return rundirtests(selfdir, target);
|
||||
};
|
||||
|
||||
@@ -1494,7 +1494,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
||||
// directory: open the dir, getdents64, build+run each *_test.ww,
|
||||
// report ok/FAIL per file, return 0 iff all pass.
|
||||
|
||||
fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8) i32 = {
|
||||
fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8, pattern: *u8) i32 = {
|
||||
let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!;
|
||||
tmp.len = os.PATH_MAX;
|
||||
// -o redirects the binary + its combined (objstem, T3) to <stem>; the
|
||||
@@ -1513,10 +1513,19 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *
|
||||
return 1;
|
||||
};
|
||||
if (compileonly != 0) { return 0; };
|
||||
let execargv: []*u8 = alloc([], 2u64)!;
|
||||
execargv.len = 2;
|
||||
// #17 fnmatch filter: forward `pattern` as argv[1] so lib/test run()
|
||||
// reads it via os.args. procrun execve's a NUL-terminated argv, so the
|
||||
// terminator (not .len) bounds the vector. cstage twin: run_test_bin.
|
||||
let execargv: []*u8 = alloc([], 3u64)!;
|
||||
execargv[0] = outp;
|
||||
execargv[1] = nil;
|
||||
if (pattern != nil) {
|
||||
execargv.len = 3;
|
||||
execargv[1] = pattern;
|
||||
execargv[2] = nil;
|
||||
} else {
|
||||
execargv.len = 2;
|
||||
execargv[1] = nil;
|
||||
};
|
||||
let rc: i32 = procrun(outp, execargv.ptr);
|
||||
if (outstem == nil) { os.remove(pathstr(outp)); };
|
||||
return rc;
|
||||
@@ -1604,6 +1613,10 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
||||
// transitive imports (e.g. 905_nkname asttest → tok); coupled to the
|
||||
// -T flip (task #5/#10).
|
||||
let target: *u8 = nil;
|
||||
// #17: optional 2nd positional = fnmatch name-filter pattern, forwarded
|
||||
// to the test binary as argv[1] (single-file/module only; dir-mode
|
||||
// rejects). cstage twin: do_test `pattern`.
|
||||
let patarg: *u8 = nil;
|
||||
let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!;
|
||||
incs.len = ((os.PATH_MAX: u64) * 2u64): i32;
|
||||
let incoff: u64 = 0u64;
|
||||
@@ -1654,7 +1667,8 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
||||
return 2;
|
||||
}; }; };
|
||||
} else {
|
||||
if (target == nil) { target = p; };
|
||||
if (target == nil) { target = p; }
|
||||
else { if (patarg == nil) { patarg = p; }; };
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
@@ -1666,7 +1680,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
||||
// single-file mode: literal *.ww that exists
|
||||
if (cstrendswithlit(target, ".ww")) {
|
||||
if (os.access(pathstr(target), 0i32) == 0) {
|
||||
return runsingletest(selfdir, target, incs.ptr, compileonly, outstem);
|
||||
return runsingletest(selfdir, target, incs.ptr, compileonly, outstem, patarg);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1674,6 +1688,12 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
||||
cerr("ww test: -c/-o need a single test file\n");
|
||||
return 2;
|
||||
};
|
||||
// #17: a name-filter pattern is per-binary; dir mode builds one binary
|
||||
// per *_test.ww, so a single pattern can't route. cstage twin parity.
|
||||
if (patarg != nil) {
|
||||
cerr("ww test: pattern needs a single test file\n");
|
||||
return 2;
|
||||
};
|
||||
// otherwise treat target as a directory; enumerate *_test.ww
|
||||
return rundirtests(selfdir, target);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user