ww test: fork-isolated record-and-continue harness (lib/test, both stages)
lib/test/run.ww: fork+wait4 runner; each @test runs in its own child, abort/SEGV/FPE decoded from wait-status, failures recorded and the run continues; exit = fail count. Tests are hermetic: module globals do not persist test-to-test (fresh fork image; sanctioned divergence from harec's shared-process __test_main, no setjmp/signal layer needed). -T synth (both stages) emits a module-global (str,*fn() void) table + return run(table) instead of straight-line calls. Driver twins bundle lib/test under test mode and gain ww test -c/-o (go test -c) so the byte-id gates diff the same artifact the real path builds. Gates 989/910/997 rewired onto it; new 911 pins record-and-continue across all three fault classes; 949 +3 rows. (#17-team commit-2)
This commit is contained in:
@@ -15801,14 +15801,20 @@ export fn checkfile(c: *checker, file: *node) void = {
|
||||
// untouched: the appended N_FNDECL rides cgfn, byte-id by
|
||||
// construction (rule 10; cstage twin at cmd/wcc/check.c).
|
||||
//
|
||||
// D1-D5 SANCTIONED PLAN-9-LEAN REDUCTION of hare-test (user-ratified,
|
||||
// reinstatable post-CSP; documented departure, not a hare cite):
|
||||
// D1 no __test_array section — straight-line calls; D2 call @test
|
||||
// fns by their real symbols (no testfunc.%d); D3 no per-test
|
||||
// setjmp/onabort isolation — run-to-completion = pass, abort/div0/
|
||||
// SIGSEGV/nonzero = fail and STOPS the run; D4 no sort, no fnmatch
|
||||
// filter — source/collection order; D5 no reflective file:line, no
|
||||
// per-test ok/FAIL line — failure is the nonzero process exit.
|
||||
// #17 RECORD-AND-CONTINUE (rob ruling 2026-06-10; drew-17-attest-spec
|
||||
// §a): instead of straight-line `foo(); bar();` calls (which abort the
|
||||
// whole run on the first failing @test — old D3), synthesize a value
|
||||
// table `[](str, *fn() void) = {("foo", &foo), ...}` + a single call to
|
||||
// the lib/test runner. The runner forks per test and reads the child's
|
||||
// wait-status, so abort/div0/SIGSEGV/nonzero each fail THAT test and the
|
||||
// run proceeds (lib/test/run.ww). Table = harec __test_array reduced to
|
||||
// a value table (D1 no section; D2 real symbols). RETAINED reductions
|
||||
// (user-ratified, reinstatable post-CSP): D4 no sort, no fnmatch filter
|
||||
// (source/collection order; fnmatch is #17 commit-3); D5 no reflective
|
||||
// file:line (the runner prints `name ... ok/FAIL` + a count summary).
|
||||
// Table rides cgen's #117 slice-of-tuple-global path; `run` resolves
|
||||
// bare against the auto-bundled lib/test (synth runs post-pass-1, so
|
||||
// run sits in the same flat "" bucket as the @test fns).
|
||||
if (c.istest != 0) {
|
||||
let pf: str = file.file;
|
||||
let pl: i32 = file.line;
|
||||
@@ -15824,9 +15830,11 @@ export fn checkfile(c: *checker, file: *node) void = {
|
||||
};
|
||||
u = u.next;
|
||||
};
|
||||
// (c) collect @test fns in file.list order; build the body chain.
|
||||
let bhead: *node = nil;
|
||||
let btail: *node = nil;
|
||||
// (c) collect @test fns in file.list order; build one table row
|
||||
// `("<name>", &<name>)` per validated @test fn.
|
||||
let rhead: *node = nil;
|
||||
let rtail: *node = nil;
|
||||
let ntest: i32 = 0;
|
||||
let t: *node = file.list;
|
||||
for (t != nil) {
|
||||
if (t.kind == nkind.N_FNDECL) {
|
||||
@@ -15853,28 +15861,77 @@ export fn checkfile(c: *checker, file: *node) void = {
|
||||
cerr("' must be fn() void\n");
|
||||
c.errs += 1;
|
||||
} else {
|
||||
let call: *node = newnode(nkind.N_CALL, pf, pl, pc);
|
||||
let cid: *node = newnode(nkind.N_IDENT, pf, pl, pc);
|
||||
cid.str = t.str;
|
||||
call.lhs = cid;
|
||||
let es: *node = newnode(nkind.N_EXPRSTMT, pf, pl, pc);
|
||||
es.lhs = call;
|
||||
if (bhead == nil) { bhead = es; }
|
||||
else { btail.next = es; };
|
||||
btail = es;
|
||||
let nm: *node = newnode(nkind.N_STRLIT, pf, pl, pc);
|
||||
nm.str = t.str;
|
||||
let id: *node = newnode(nkind.N_IDENT, pf, pl, pc);
|
||||
id.str = t.str;
|
||||
let amp: *node = newnode(nkind.N_UN, pf, pl, pc);
|
||||
amp.op = tkind.TK_AMP;
|
||||
amp.lhs = id;
|
||||
let row: *node = newnode(nkind.N_TUPLE, pf, pl, pc);
|
||||
row.list = nm;
|
||||
nm.next = amp;
|
||||
if (rhead == nil) { rhead = row; }
|
||||
else { rtail.next = row; };
|
||||
rtail = row;
|
||||
ntest += 1i32;
|
||||
};
|
||||
};
|
||||
};
|
||||
t = t.next;
|
||||
};
|
||||
let ret: *node = newnode(nkind.N_RETURN, pf, pl, pc);
|
||||
let zero: *node = newnode(nkind.N_INTLIT, pf, pl, pc);
|
||||
zero.uval = 0u64;
|
||||
ret.lhs = zero;
|
||||
if (bhead == nil) { bhead = ret; } else { btail.next = ret; };
|
||||
btail = ret;
|
||||
|
||||
let body: *node = newnode(nkind.N_BLOCK, pf, pl, pc);
|
||||
body.list = bhead;
|
||||
let tab: *node = nil;
|
||||
if (ntest == 0i32) {
|
||||
// no @test fns in this unit — exit 0, nothing to run.
|
||||
let ret: *node = newnode(nkind.N_RETURN, pf, pl, pc);
|
||||
let zero: *node = newnode(nkind.N_INTLIT, pf, pl, pc);
|
||||
zero.uval = 0u64;
|
||||
ret.lhs = zero;
|
||||
body.list = ret;
|
||||
} else {
|
||||
// const __wwtests: [](str, *fn() void) = [rows...];
|
||||
// wwstage tuple-type elements wrap in N_TPARAM (parse.ww:308).
|
||||
let e0: *node = newnode(nkind.N_TNAME, pf, pl, pc);
|
||||
e0.str = "str";
|
||||
let p0: *node = newnode(nkind.N_TPARAM, pf, pl, pc);
|
||||
p0.lhs = e0;
|
||||
let vret: *node = newnode(nkind.N_TNAME, pf, pl, pc);
|
||||
vret.str = "void";
|
||||
let vfn: *node = newnode(nkind.N_TFN, pf, pl, pc);
|
||||
vfn.lhs = vret;
|
||||
let e1: *node = newnode(nkind.N_TPTR, pf, pl, pc);
|
||||
e1.lhs = vfn;
|
||||
let p1: *node = newnode(nkind.N_TPARAM, pf, pl, pc);
|
||||
p1.lhs = e1;
|
||||
let tup: *node = newnode(nkind.N_TTUPLE, pf, pl, pc);
|
||||
tup.list = p0;
|
||||
p0.next = p1;
|
||||
let tsl: *node = newnode(nkind.N_TSLICE, pf, pl, pc);
|
||||
tsl.lhs = tup;
|
||||
let arr: *node = newnode(nkind.N_ARRLIT, pf, pl, pc);
|
||||
arr.list = rhead;
|
||||
tab = newnode(nkind.N_LET, pf, pl, pc);
|
||||
tab.op = tkind.TK_CONST;
|
||||
tab.str = "__wwtests";
|
||||
tab.lhs = tsl;
|
||||
tab.rhs = arr;
|
||||
// pass 1 already ran; install the table name now so main
|
||||
// resolves it (declmod returns "" for tab.nmod="").
|
||||
installdecl(c, file, tab);
|
||||
|
||||
let arg: *node = newnode(nkind.N_IDENT, pf, pl, pc);
|
||||
arg.str = "__wwtests";
|
||||
let call: *node = newnode(nkind.N_CALL, pf, pl, pc);
|
||||
let cid: *node = newnode(nkind.N_IDENT, pf, pl, pc);
|
||||
cid.str = "run";
|
||||
call.lhs = cid;
|
||||
call.list = arg;
|
||||
let ret: *node = newnode(nkind.N_RETURN, pf, pl, pc);
|
||||
ret.lhs = call;
|
||||
body.list = ret;
|
||||
};
|
||||
let m: *node = newnode(nkind.N_FNDECL, pf, pl, pc);
|
||||
m.str = "main";
|
||||
m.exported = 1i32;
|
||||
@@ -15882,16 +15939,18 @@ export fn checkfile(c: *checker, file: *node) void = {
|
||||
rety.str = "i32";
|
||||
m.lhs = rety;
|
||||
m.body = body;
|
||||
// Append to file.list tail. No type_ pre-set: installdecl
|
||||
// never stamps a fn's type_ on the ww side either — cgfn reads
|
||||
// lhs/list on demand, like every parsed fn. Pass-2 below
|
||||
// resolve-walks the appended body.
|
||||
// Append the table const (if any) then main to file.list tail. No
|
||||
// type_ pre-set on main: installdecl never stamps a fn's type_ on
|
||||
// the ww side — cgfn reads lhs/list on demand. Pass-2 below
|
||||
// resolve-walks the appended bodies.
|
||||
let tl: *node = file.list;
|
||||
if (tl == nil) {
|
||||
file.list = m;
|
||||
if (tab != nil) { file.list = tab; tab.next = m; }
|
||||
else { file.list = m; };
|
||||
} else {
|
||||
for (tl.next != nil) { tl = tl.next; };
|
||||
tl.next = m;
|
||||
if (tab != nil) { tl.next = tab; tab.next = m; }
|
||||
else { tl.next = m; };
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -5469,14 +5469,20 @@ export fn checkfile(c: *checker, file: *node) void = {
|
||||
// untouched: the appended N_FNDECL rides cgfn, byte-id by
|
||||
// construction (rule 10; cstage twin at cmd/wcc/check.c).
|
||||
//
|
||||
// D1-D5 SANCTIONED PLAN-9-LEAN REDUCTION of hare-test (user-ratified,
|
||||
// reinstatable post-CSP; documented departure, not a hare cite):
|
||||
// D1 no __test_array section — straight-line calls; D2 call @test
|
||||
// fns by their real symbols (no testfunc.%d); D3 no per-test
|
||||
// setjmp/onabort isolation — run-to-completion = pass, abort/div0/
|
||||
// SIGSEGV/nonzero = fail and STOPS the run; D4 no sort, no fnmatch
|
||||
// filter — source/collection order; D5 no reflective file:line, no
|
||||
// per-test ok/FAIL line — failure is the nonzero process exit.
|
||||
// #17 RECORD-AND-CONTINUE (rob ruling 2026-06-10; drew-17-attest-spec
|
||||
// §a): instead of straight-line `foo(); bar();` calls (which abort the
|
||||
// whole run on the first failing @test — old D3), synthesize a value
|
||||
// table `[](str, *fn() void) = {("foo", &foo), ...}` + a single call to
|
||||
// the lib/test runner. The runner forks per test and reads the child's
|
||||
// wait-status, so abort/div0/SIGSEGV/nonzero each fail THAT test and the
|
||||
// run proceeds (lib/test/run.ww). Table = harec __test_array reduced to
|
||||
// a value table (D1 no section; D2 real symbols). RETAINED reductions
|
||||
// (user-ratified, reinstatable post-CSP): D4 no sort, no fnmatch filter
|
||||
// (source/collection order; fnmatch is #17 commit-3); D5 no reflective
|
||||
// file:line (the runner prints `name ... ok/FAIL` + a count summary).
|
||||
// Table rides cgen's #117 slice-of-tuple-global path; `run` resolves
|
||||
// bare against the auto-bundled lib/test (synth runs post-pass-1, so
|
||||
// run sits in the same flat "" bucket as the @test fns).
|
||||
if (c.istest != 0) {
|
||||
let pf: str = file.file;
|
||||
let pl: i32 = file.line;
|
||||
@@ -5492,9 +5498,11 @@ export fn checkfile(c: *checker, file: *node) void = {
|
||||
};
|
||||
u = u.next;
|
||||
};
|
||||
// (c) collect @test fns in file.list order; build the body chain.
|
||||
let bhead: *node = nil;
|
||||
let btail: *node = nil;
|
||||
// (c) collect @test fns in file.list order; build one table row
|
||||
// `("<name>", &<name>)` per validated @test fn.
|
||||
let rhead: *node = nil;
|
||||
let rtail: *node = nil;
|
||||
let ntest: i32 = 0;
|
||||
let t: *node = file.list;
|
||||
for (t != nil) {
|
||||
if (t.kind == nkind.N_FNDECL) {
|
||||
@@ -5521,28 +5529,77 @@ export fn checkfile(c: *checker, file: *node) void = {
|
||||
cerr("' must be fn() void\n");
|
||||
c.errs += 1;
|
||||
} else {
|
||||
let call: *node = newnode(nkind.N_CALL, pf, pl, pc);
|
||||
let cid: *node = newnode(nkind.N_IDENT, pf, pl, pc);
|
||||
cid.str = t.str;
|
||||
call.lhs = cid;
|
||||
let es: *node = newnode(nkind.N_EXPRSTMT, pf, pl, pc);
|
||||
es.lhs = call;
|
||||
if (bhead == nil) { bhead = es; }
|
||||
else { btail.next = es; };
|
||||
btail = es;
|
||||
let nm: *node = newnode(nkind.N_STRLIT, pf, pl, pc);
|
||||
nm.str = t.str;
|
||||
let id: *node = newnode(nkind.N_IDENT, pf, pl, pc);
|
||||
id.str = t.str;
|
||||
let amp: *node = newnode(nkind.N_UN, pf, pl, pc);
|
||||
amp.op = tkind.TK_AMP;
|
||||
amp.lhs = id;
|
||||
let row: *node = newnode(nkind.N_TUPLE, pf, pl, pc);
|
||||
row.list = nm;
|
||||
nm.next = amp;
|
||||
if (rhead == nil) { rhead = row; }
|
||||
else { rtail.next = row; };
|
||||
rtail = row;
|
||||
ntest += 1i32;
|
||||
};
|
||||
};
|
||||
};
|
||||
t = t.next;
|
||||
};
|
||||
let ret: *node = newnode(nkind.N_RETURN, pf, pl, pc);
|
||||
let zero: *node = newnode(nkind.N_INTLIT, pf, pl, pc);
|
||||
zero.uval = 0u64;
|
||||
ret.lhs = zero;
|
||||
if (bhead == nil) { bhead = ret; } else { btail.next = ret; };
|
||||
btail = ret;
|
||||
|
||||
let body: *node = newnode(nkind.N_BLOCK, pf, pl, pc);
|
||||
body.list = bhead;
|
||||
let tab: *node = nil;
|
||||
if (ntest == 0i32) {
|
||||
// no @test fns in this unit — exit 0, nothing to run.
|
||||
let ret: *node = newnode(nkind.N_RETURN, pf, pl, pc);
|
||||
let zero: *node = newnode(nkind.N_INTLIT, pf, pl, pc);
|
||||
zero.uval = 0u64;
|
||||
ret.lhs = zero;
|
||||
body.list = ret;
|
||||
} else {
|
||||
// const __wwtests: [](str, *fn() void) = [rows...];
|
||||
// wwstage tuple-type elements wrap in N_TPARAM (parse.ww:308).
|
||||
let e0: *node = newnode(nkind.N_TNAME, pf, pl, pc);
|
||||
e0.str = "str";
|
||||
let p0: *node = newnode(nkind.N_TPARAM, pf, pl, pc);
|
||||
p0.lhs = e0;
|
||||
let vret: *node = newnode(nkind.N_TNAME, pf, pl, pc);
|
||||
vret.str = "void";
|
||||
let vfn: *node = newnode(nkind.N_TFN, pf, pl, pc);
|
||||
vfn.lhs = vret;
|
||||
let e1: *node = newnode(nkind.N_TPTR, pf, pl, pc);
|
||||
e1.lhs = vfn;
|
||||
let p1: *node = newnode(nkind.N_TPARAM, pf, pl, pc);
|
||||
p1.lhs = e1;
|
||||
let tup: *node = newnode(nkind.N_TTUPLE, pf, pl, pc);
|
||||
tup.list = p0;
|
||||
p0.next = p1;
|
||||
let tsl: *node = newnode(nkind.N_TSLICE, pf, pl, pc);
|
||||
tsl.lhs = tup;
|
||||
let arr: *node = newnode(nkind.N_ARRLIT, pf, pl, pc);
|
||||
arr.list = rhead;
|
||||
tab = newnode(nkind.N_LET, pf, pl, pc);
|
||||
tab.op = tkind.TK_CONST;
|
||||
tab.str = "__wwtests";
|
||||
tab.lhs = tsl;
|
||||
tab.rhs = arr;
|
||||
// pass 1 already ran; install the table name now so main
|
||||
// resolves it (declmod returns "" for tab.nmod="").
|
||||
installdecl(c, file, tab);
|
||||
|
||||
let arg: *node = newnode(nkind.N_IDENT, pf, pl, pc);
|
||||
arg.str = "__wwtests";
|
||||
let call: *node = newnode(nkind.N_CALL, pf, pl, pc);
|
||||
let cid: *node = newnode(nkind.N_IDENT, pf, pl, pc);
|
||||
cid.str = "run";
|
||||
call.lhs = cid;
|
||||
call.list = arg;
|
||||
let ret: *node = newnode(nkind.N_RETURN, pf, pl, pc);
|
||||
ret.lhs = call;
|
||||
body.list = ret;
|
||||
};
|
||||
let m: *node = newnode(nkind.N_FNDECL, pf, pl, pc);
|
||||
m.str = "main";
|
||||
m.exported = 1i32;
|
||||
@@ -5550,16 +5607,18 @@ export fn checkfile(c: *checker, file: *node) void = {
|
||||
rety.str = "i32";
|
||||
m.lhs = rety;
|
||||
m.body = body;
|
||||
// Append to file.list tail. No type_ pre-set: installdecl
|
||||
// never stamps a fn's type_ on the ww side either — cgfn reads
|
||||
// lhs/list on demand, like every parsed fn. Pass-2 below
|
||||
// resolve-walks the appended body.
|
||||
// Append the table const (if any) then main to file.list tail. No
|
||||
// type_ pre-set on main: installdecl never stamps a fn's type_ on
|
||||
// the ww side — cgfn reads lhs/list on demand. Pass-2 below
|
||||
// resolve-walks the appended bodies.
|
||||
let tl: *node = file.list;
|
||||
if (tl == nil) {
|
||||
file.list = m;
|
||||
if (tab != nil) { file.list = tab; tab.next = m; }
|
||||
else { file.list = m; };
|
||||
} else {
|
||||
for (tl.next != nil) { tl = tl.next; };
|
||||
tl.next = m;
|
||||
if (tab != nil) { tl.next = tab; tab.next = m; }
|
||||
else { tl.next = m; };
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -3724,6 +3724,19 @@ fn buildone(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, objstem: *u8, inc
|
||||
c.out = cf;
|
||||
c.dirs = searchpath.ptr;
|
||||
c.visit = nil;
|
||||
// #17 auto-bundle lib/test: the -T synth's main calls lib/test's
|
||||
// run(), but @test files don't `import test;`. Pull it like an
|
||||
// implicit import through the same locate+expand path (the visit
|
||||
// set dedupes a fixture that imports it explicitly). cstage twin
|
||||
// in cmd/ww/main.c buildone.
|
||||
if (istest != 0) {
|
||||
let td: i32 = 0;
|
||||
let tp: *u8 = locateimport(searchpath.ptr, "test".ptr, "test".len: u64, &td);
|
||||
if (tp != nil) {
|
||||
if (td != 0) { expanddir(&c, tp); }
|
||||
else { expand(&c, tp); };
|
||||
};
|
||||
};
|
||||
if (entryisdir != 0) { expanddir(&c, srcd.ptr); }
|
||||
else { expand(&c, src); };
|
||||
};
|
||||
@@ -4267,20 +4280,31 @@ 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) i32 = {
|
||||
fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8) i32 = {
|
||||
let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!;
|
||||
tmp.len = os.PATH_MAX;
|
||||
makeruntmp(tmp.ptr);
|
||||
if (buildone(selfdir, src, 0, tmp.ptr, nil, incs, nil, 1i32) != 0) {
|
||||
os.remove(pathstr(tmp.ptr));
|
||||
// -o redirects the binary + its combined (objstem, T3) to <stem>; the
|
||||
// default temp keeps the combined next to the source as before.
|
||||
let outp: *u8 = nil;
|
||||
let objstem: *u8 = nil;
|
||||
if (outstem != nil) {
|
||||
outp = outstem;
|
||||
objstem = outstem;
|
||||
} else {
|
||||
makeruntmp(tmp.ptr);
|
||||
outp = tmp.ptr;
|
||||
};
|
||||
if (buildone(selfdir, src, 0, outp, objstem, incs, nil, 1i32) != 0) {
|
||||
if (outstem == nil) { os.remove(pathstr(outp)); };
|
||||
return 1;
|
||||
};
|
||||
if (compileonly != 0) { return 0; };
|
||||
let execargv: []*u8 = alloc([], 2u64)!;
|
||||
execargv.len = 2;
|
||||
execargv[0] = tmp.ptr;
|
||||
execargv[0] = outp;
|
||||
execargv[1] = nil;
|
||||
let rc: i32 = procrun(tmp.ptr, execargv.ptr);
|
||||
os.remove(pathstr(tmp.ptr));
|
||||
let rc: i32 = procrun(outp, execargv.ptr);
|
||||
if (outstem == nil) { os.remove(pathstr(outp)); };
|
||||
return rc;
|
||||
};
|
||||
|
||||
@@ -4371,6 +4395,11 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
||||
let incoff: u64 = 0u64;
|
||||
cstrseal(incs.ptr, 0u64);
|
||||
|
||||
// -c (compile-only) + -o <stem>: build the test binary + its lib/test-
|
||||
// inclusive combined WITHOUT running it, for the byte-id gates. cstage
|
||||
// twin: cmd/ww/main.c do_test (error wording identical).
|
||||
let compileonly: i32 = 0;
|
||||
let outstem: *u8 = nil;
|
||||
let i: i32 = start;
|
||||
for (i < argc) {
|
||||
let p: *u8 = argv[i];
|
||||
@@ -4393,10 +4422,23 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
||||
};
|
||||
incoff = cstrinto(incs.ptr, incoff, dir);
|
||||
cstrseal(incs.ptr, incoff);
|
||||
} else { if (p[1u64] == 99u8 && p[2u64] == 0u8) { // "-c"
|
||||
compileonly = 1;
|
||||
} else { if (p[1u64] == 111u8) { // '-o'
|
||||
if (p[2u64] != 0u8) {
|
||||
outstem = p + 2u64;
|
||||
} else {
|
||||
if (i + 1 >= argc) {
|
||||
cerr("ww test: -o needs an argument\n");
|
||||
return 2;
|
||||
};
|
||||
i += 1;
|
||||
outstem = argv[i];
|
||||
};
|
||||
} else {
|
||||
cerr("ww test: unknown flag\n");
|
||||
return 2;
|
||||
};
|
||||
}; }; };
|
||||
} else {
|
||||
if (target == nil) { target = p; };
|
||||
};
|
||||
@@ -4410,10 +4452,14 @@ 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);
|
||||
return runsingletest(selfdir, target, incs.ptr, compileonly, outstem);
|
||||
};
|
||||
};
|
||||
|
||||
if (compileonly != 0 || outstem != nil) {
|
||||
cerr("ww test: -c/-o need a single test file\n");
|
||||
return 2;
|
||||
};
|
||||
// otherwise treat target as a directory; enumerate *_test.ww
|
||||
return rundirtests(selfdir, target);
|
||||
};
|
||||
|
||||
@@ -938,6 +938,19 @@ fn buildone(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, objstem: *u8, inc
|
||||
c.out = cf;
|
||||
c.dirs = searchpath.ptr;
|
||||
c.visit = nil;
|
||||
// #17 auto-bundle lib/test: the -T synth's main calls lib/test's
|
||||
// run(), but @test files don't `import test;`. Pull it like an
|
||||
// implicit import through the same locate+expand path (the visit
|
||||
// set dedupes a fixture that imports it explicitly). cstage twin
|
||||
// in cmd/ww/main.c buildone.
|
||||
if (istest != 0) {
|
||||
let td: i32 = 0;
|
||||
let tp: *u8 = locateimport(searchpath.ptr, "test".ptr, "test".len: u64, &td);
|
||||
if (tp != nil) {
|
||||
if (td != 0) { expanddir(&c, tp); }
|
||||
else { expand(&c, tp); };
|
||||
};
|
||||
};
|
||||
if (entryisdir != 0) { expanddir(&c, srcd.ptr); }
|
||||
else { expand(&c, src); };
|
||||
};
|
||||
@@ -1481,20 +1494,31 @@ 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) i32 = {
|
||||
fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8) i32 = {
|
||||
let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!;
|
||||
tmp.len = os.PATH_MAX;
|
||||
makeruntmp(tmp.ptr);
|
||||
if (buildone(selfdir, src, 0, tmp.ptr, nil, incs, nil, 1i32) != 0) {
|
||||
os.remove(pathstr(tmp.ptr));
|
||||
// -o redirects the binary + its combined (objstem, T3) to <stem>; the
|
||||
// default temp keeps the combined next to the source as before.
|
||||
let outp: *u8 = nil;
|
||||
let objstem: *u8 = nil;
|
||||
if (outstem != nil) {
|
||||
outp = outstem;
|
||||
objstem = outstem;
|
||||
} else {
|
||||
makeruntmp(tmp.ptr);
|
||||
outp = tmp.ptr;
|
||||
};
|
||||
if (buildone(selfdir, src, 0, outp, objstem, incs, nil, 1i32) != 0) {
|
||||
if (outstem == nil) { os.remove(pathstr(outp)); };
|
||||
return 1;
|
||||
};
|
||||
if (compileonly != 0) { return 0; };
|
||||
let execargv: []*u8 = alloc([], 2u64)!;
|
||||
execargv.len = 2;
|
||||
execargv[0] = tmp.ptr;
|
||||
execargv[0] = outp;
|
||||
execargv[1] = nil;
|
||||
let rc: i32 = procrun(tmp.ptr, execargv.ptr);
|
||||
os.remove(pathstr(tmp.ptr));
|
||||
let rc: i32 = procrun(outp, execargv.ptr);
|
||||
if (outstem == nil) { os.remove(pathstr(outp)); };
|
||||
return rc;
|
||||
};
|
||||
|
||||
@@ -1585,6 +1609,11 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
||||
let incoff: u64 = 0u64;
|
||||
cstrseal(incs.ptr, 0u64);
|
||||
|
||||
// -c (compile-only) + -o <stem>: build the test binary + its lib/test-
|
||||
// inclusive combined WITHOUT running it, for the byte-id gates. cstage
|
||||
// twin: cmd/ww/main.c do_test (error wording identical).
|
||||
let compileonly: i32 = 0;
|
||||
let outstem: *u8 = nil;
|
||||
let i: i32 = start;
|
||||
for (i < argc) {
|
||||
let p: *u8 = argv[i];
|
||||
@@ -1607,10 +1636,23 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
||||
};
|
||||
incoff = cstrinto(incs.ptr, incoff, dir);
|
||||
cstrseal(incs.ptr, incoff);
|
||||
} else { if (p[1u64] == 99u8 && p[2u64] == 0u8) { // "-c"
|
||||
compileonly = 1;
|
||||
} else { if (p[1u64] == 111u8) { // '-o'
|
||||
if (p[2u64] != 0u8) {
|
||||
outstem = p + 2u64;
|
||||
} else {
|
||||
if (i + 1 >= argc) {
|
||||
cerr("ww test: -o needs an argument\n");
|
||||
return 2;
|
||||
};
|
||||
i += 1;
|
||||
outstem = argv[i];
|
||||
};
|
||||
} else {
|
||||
cerr("ww test: unknown flag\n");
|
||||
return 2;
|
||||
};
|
||||
}; }; };
|
||||
} else {
|
||||
if (target == nil) { target = p; };
|
||||
};
|
||||
@@ -1624,10 +1666,14 @@ 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);
|
||||
return runsingletest(selfdir, target, incs.ptr, compileonly, outstem);
|
||||
};
|
||||
};
|
||||
|
||||
if (compileonly != 0 || outstem != nil) {
|
||||
cerr("ww test: -c/-o need a single test file\n");
|
||||
return 2;
|
||||
};
|
||||
// otherwise treat target as a directory; enumerate *_test.ww
|
||||
return rundirtests(selfdir, target);
|
||||
};
|
||||
|
||||
@@ -15801,14 +15801,20 @@ export fn checkfile(c: *checker, file: *node) void = {
|
||||
// untouched: the appended N_FNDECL rides cgfn, byte-id by
|
||||
// construction (rule 10; cstage twin at cmd/wcc/check.c).
|
||||
//
|
||||
// D1-D5 SANCTIONED PLAN-9-LEAN REDUCTION of hare-test (user-ratified,
|
||||
// reinstatable post-CSP; documented departure, not a hare cite):
|
||||
// D1 no __test_array section — straight-line calls; D2 call @test
|
||||
// fns by their real symbols (no testfunc.%d); D3 no per-test
|
||||
// setjmp/onabort isolation — run-to-completion = pass, abort/div0/
|
||||
// SIGSEGV/nonzero = fail and STOPS the run; D4 no sort, no fnmatch
|
||||
// filter — source/collection order; D5 no reflective file:line, no
|
||||
// per-test ok/FAIL line — failure is the nonzero process exit.
|
||||
// #17 RECORD-AND-CONTINUE (rob ruling 2026-06-10; drew-17-attest-spec
|
||||
// §a): instead of straight-line `foo(); bar();` calls (which abort the
|
||||
// whole run on the first failing @test — old D3), synthesize a value
|
||||
// table `[](str, *fn() void) = {("foo", &foo), ...}` + a single call to
|
||||
// the lib/test runner. The runner forks per test and reads the child's
|
||||
// wait-status, so abort/div0/SIGSEGV/nonzero each fail THAT test and the
|
||||
// run proceeds (lib/test/run.ww). Table = harec __test_array reduced to
|
||||
// a value table (D1 no section; D2 real symbols). RETAINED reductions
|
||||
// (user-ratified, reinstatable post-CSP): D4 no sort, no fnmatch filter
|
||||
// (source/collection order; fnmatch is #17 commit-3); D5 no reflective
|
||||
// file:line (the runner prints `name ... ok/FAIL` + a count summary).
|
||||
// Table rides cgen's #117 slice-of-tuple-global path; `run` resolves
|
||||
// bare against the auto-bundled lib/test (synth runs post-pass-1, so
|
||||
// run sits in the same flat "" bucket as the @test fns).
|
||||
if (c.istest != 0) {
|
||||
let pf: str = file.file;
|
||||
let pl: i32 = file.line;
|
||||
@@ -15824,9 +15830,11 @@ export fn checkfile(c: *checker, file: *node) void = {
|
||||
};
|
||||
u = u.next;
|
||||
};
|
||||
// (c) collect @test fns in file.list order; build the body chain.
|
||||
let bhead: *node = nil;
|
||||
let btail: *node = nil;
|
||||
// (c) collect @test fns in file.list order; build one table row
|
||||
// `("<name>", &<name>)` per validated @test fn.
|
||||
let rhead: *node = nil;
|
||||
let rtail: *node = nil;
|
||||
let ntest: i32 = 0;
|
||||
let t: *node = file.list;
|
||||
for (t != nil) {
|
||||
if (t.kind == nkind.N_FNDECL) {
|
||||
@@ -15853,28 +15861,77 @@ export fn checkfile(c: *checker, file: *node) void = {
|
||||
cerr("' must be fn() void\n");
|
||||
c.errs += 1;
|
||||
} else {
|
||||
let call: *node = newnode(nkind.N_CALL, pf, pl, pc);
|
||||
let cid: *node = newnode(nkind.N_IDENT, pf, pl, pc);
|
||||
cid.str = t.str;
|
||||
call.lhs = cid;
|
||||
let es: *node = newnode(nkind.N_EXPRSTMT, pf, pl, pc);
|
||||
es.lhs = call;
|
||||
if (bhead == nil) { bhead = es; }
|
||||
else { btail.next = es; };
|
||||
btail = es;
|
||||
let nm: *node = newnode(nkind.N_STRLIT, pf, pl, pc);
|
||||
nm.str = t.str;
|
||||
let id: *node = newnode(nkind.N_IDENT, pf, pl, pc);
|
||||
id.str = t.str;
|
||||
let amp: *node = newnode(nkind.N_UN, pf, pl, pc);
|
||||
amp.op = tkind.TK_AMP;
|
||||
amp.lhs = id;
|
||||
let row: *node = newnode(nkind.N_TUPLE, pf, pl, pc);
|
||||
row.list = nm;
|
||||
nm.next = amp;
|
||||
if (rhead == nil) { rhead = row; }
|
||||
else { rtail.next = row; };
|
||||
rtail = row;
|
||||
ntest += 1i32;
|
||||
};
|
||||
};
|
||||
};
|
||||
t = t.next;
|
||||
};
|
||||
let ret: *node = newnode(nkind.N_RETURN, pf, pl, pc);
|
||||
let zero: *node = newnode(nkind.N_INTLIT, pf, pl, pc);
|
||||
zero.uval = 0u64;
|
||||
ret.lhs = zero;
|
||||
if (bhead == nil) { bhead = ret; } else { btail.next = ret; };
|
||||
btail = ret;
|
||||
|
||||
let body: *node = newnode(nkind.N_BLOCK, pf, pl, pc);
|
||||
body.list = bhead;
|
||||
let tab: *node = nil;
|
||||
if (ntest == 0i32) {
|
||||
// no @test fns in this unit — exit 0, nothing to run.
|
||||
let ret: *node = newnode(nkind.N_RETURN, pf, pl, pc);
|
||||
let zero: *node = newnode(nkind.N_INTLIT, pf, pl, pc);
|
||||
zero.uval = 0u64;
|
||||
ret.lhs = zero;
|
||||
body.list = ret;
|
||||
} else {
|
||||
// const __wwtests: [](str, *fn() void) = [rows...];
|
||||
// wwstage tuple-type elements wrap in N_TPARAM (parse.ww:308).
|
||||
let e0: *node = newnode(nkind.N_TNAME, pf, pl, pc);
|
||||
e0.str = "str";
|
||||
let p0: *node = newnode(nkind.N_TPARAM, pf, pl, pc);
|
||||
p0.lhs = e0;
|
||||
let vret: *node = newnode(nkind.N_TNAME, pf, pl, pc);
|
||||
vret.str = "void";
|
||||
let vfn: *node = newnode(nkind.N_TFN, pf, pl, pc);
|
||||
vfn.lhs = vret;
|
||||
let e1: *node = newnode(nkind.N_TPTR, pf, pl, pc);
|
||||
e1.lhs = vfn;
|
||||
let p1: *node = newnode(nkind.N_TPARAM, pf, pl, pc);
|
||||
p1.lhs = e1;
|
||||
let tup: *node = newnode(nkind.N_TTUPLE, pf, pl, pc);
|
||||
tup.list = p0;
|
||||
p0.next = p1;
|
||||
let tsl: *node = newnode(nkind.N_TSLICE, pf, pl, pc);
|
||||
tsl.lhs = tup;
|
||||
let arr: *node = newnode(nkind.N_ARRLIT, pf, pl, pc);
|
||||
arr.list = rhead;
|
||||
tab = newnode(nkind.N_LET, pf, pl, pc);
|
||||
tab.op = tkind.TK_CONST;
|
||||
tab.str = "__wwtests";
|
||||
tab.lhs = tsl;
|
||||
tab.rhs = arr;
|
||||
// pass 1 already ran; install the table name now so main
|
||||
// resolves it (declmod returns "" for tab.nmod="").
|
||||
installdecl(c, file, tab);
|
||||
|
||||
let arg: *node = newnode(nkind.N_IDENT, pf, pl, pc);
|
||||
arg.str = "__wwtests";
|
||||
let call: *node = newnode(nkind.N_CALL, pf, pl, pc);
|
||||
let cid: *node = newnode(nkind.N_IDENT, pf, pl, pc);
|
||||
cid.str = "run";
|
||||
call.lhs = cid;
|
||||
call.list = arg;
|
||||
let ret: *node = newnode(nkind.N_RETURN, pf, pl, pc);
|
||||
ret.lhs = call;
|
||||
body.list = ret;
|
||||
};
|
||||
let m: *node = newnode(nkind.N_FNDECL, pf, pl, pc);
|
||||
m.str = "main";
|
||||
m.exported = 1i32;
|
||||
@@ -15882,16 +15939,18 @@ export fn checkfile(c: *checker, file: *node) void = {
|
||||
rety.str = "i32";
|
||||
m.lhs = rety;
|
||||
m.body = body;
|
||||
// Append to file.list tail. No type_ pre-set: installdecl
|
||||
// never stamps a fn's type_ on the ww side either — cgfn reads
|
||||
// lhs/list on demand, like every parsed fn. Pass-2 below
|
||||
// resolve-walks the appended body.
|
||||
// Append the table const (if any) then main to file.list tail. No
|
||||
// type_ pre-set on main: installdecl never stamps a fn's type_ on
|
||||
// the ww side — cgfn reads lhs/list on demand. Pass-2 below
|
||||
// resolve-walks the appended bodies.
|
||||
let tl: *node = file.list;
|
||||
if (tl == nil) {
|
||||
file.list = m;
|
||||
if (tab != nil) { file.list = tab; tab.next = m; }
|
||||
else { file.list = m; };
|
||||
} else {
|
||||
for (tl.next != nil) { tl = tl.next; };
|
||||
tl.next = m;
|
||||
if (tab != nil) { tl.next = tab; tab.next = m; }
|
||||
else { tl.next = m; };
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user