From 16c83e70d34194c9eb537757d9bb4e98c770da18 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 11 Jun 2026 00:08:39 +0900 Subject: [PATCH] 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) --- Makefile | 5 + cmd/wcc/check.c | 118 +++++++++++++----- cmd/ww/main.c | 67 +++++++++-- lib/test/run.ww | 108 +++++++++++++++++ selfhost/cmd/w6c/main.combined.ww | 125 ++++++++++++++----- selfhost/cmd/wcc/check.ww | 125 ++++++++++++++----- selfhost/cmd/ww/main.combined.ww | 64 ++++++++-- selfhost/cmd/ww/main.ww | 64 ++++++++-- selfhost/cmd/wwdump/main.combined.ww | 125 ++++++++++++++----- test/wcc/910_at_test.c | 38 ++++-- test/wcc/911_attest_record.c | 173 +++++++++++++++++++++++++++ test/wcc/949_driver_flagargs.c | 15 ++- test/wcc/989_lib_byteid.c | 33 +++-- test/wcc/997_at_test_ww.c | 64 +++++++--- test/wcc/data/attest_record.ww | 31 +++++ 15 files changed, 961 insertions(+), 194 deletions(-) create mode 100644 lib/test/run.ww create mode 100644 test/wcc/911_attest_record.c create mode 100644 test/wcc/data/attest_record.ww diff --git a/Makefile b/Makefile index adc8435b..59e54e1b 100644 --- a/Makefile +++ b/Makefile @@ -469,6 +469,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ $(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \ $(BIN)/test_dyn_ww $(BIN)/test_selfcheck $(BIN)/test_at_test_ww \ + $(BIN)/test_attest_record \ $(BIN)/test_fmt_run $(BIN)/test_log_run $(BIN)/test_fnmatch_run \ $(BIN)/test_shlex_run $(BIN)/test_getenv_run $(BIN)/test_dirs_run \ $(BIN)/test_stat_run $(BIN)/test_time_run \ @@ -2156,6 +2157,10 @@ $(BIN)/test_at_test_ww: test/wcc/997_at_test_ww.c $(BIN)/ww_ww $(BIN)/w6c_ww \ $(BIN)/w6a_ww $(BIN)/w6l_ww $(BIN)/w6c $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_attest_record: test/wcc/911_attest_record.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_fmt_run: test/wcc/970_fmt_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index f04b9edb..743c6aaa 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -2974,14 +2974,22 @@ check_file(Checker *c, Node *file) * untouched: the appended N_FNDECL rides the existing cgfn path, so * byte-id holds at the gated choke point by construction. * - * D1-D5 SANCTIONED PLAN-9-LEAN REDUCTION of hare-test (user-ratified, - * reinstatable post-CSP; this is a documented departure, not a hare - * cite): D1 no __test_array linker section — straight-line calls; - * D2 call @test fns by their real symbols (no testfunc.%d rename); - * 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 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 — the old D3), synthesize a value + * table `[](str, *fn() void) = {("foo", &foo), ...}` and 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). The table is the harec + * __test_array reduced to an in-source value table (D1: no linker + * section; D2: real symbols, no testfunc.%d rename). 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). + * The table rides cgen's #117 slice-of-tuple-global DATA path; the + * `run` callee resolves bare against the auto-bundled lib/test (the + * synth runs post-pass-1, so lib/test's `run` sits in the same flat "" + * bucket as the @test fns — bare, like the @test calls themselves). */ if (c->is_test) { Pos fp = file->pos; @@ -2991,8 +2999,10 @@ check_file(Checker *c, Node *file) && strcmp(d->str, "main") == 0 && d->body != NULL) err(c, d->pos, "test mode: main is synthesized " "by -T; remove the explicit main"); - /* (c) collect @test fns in file->list order, build the body. */ - Node *bhead = NULL, *btail = NULL; + /* (c) collect @test fns in file->list order; build one table row + * `("", &)` per validated @test fn. */ + Node *rhead = NULL, *rtail = NULL; + int ntest = 0; for (Node *d = file->list; d; d = d->next) { if (d->kind != N_FNDECL) continue; @@ -3015,22 +3025,72 @@ check_file(Checker *c, Node *file) d->str); continue; } + Node *nm = newnode(c->a, N_STRLIT, fp); + nm->str = d->str; + nm->strlen = strlen(d->str); + Node *id = newnode(c->a, N_IDENT, fp); + id->str = d->str; + Node *amp = newnode(c->a, N_UN, fp); + amp->op = TK_AMP; + amp->lhs = id; + Node *row = newnode(c->a, N_TUPLE, fp); + row->list = nm; + nm->next = amp; + if (rhead == NULL) rhead = row; + else rtail->next = row; + rtail = row; + ntest++; + } + + Node *body = newnode(c->a, N_BLOCK, fp); + Node *tab = NULL; + if (ntest == 0) { + /* no @test fns in this unit — exit 0, nothing to run. */ + Node *ret = newnode(c->a, N_RETURN, fp); + ret->lhs = newnode(c->a, N_INTLIT, fp); + ret->lhs->uval = 0; + body->list = ret; + } else { + /* const __wwtests: [](str, *fn() void) = [rows...]; + * cstage tuple-type elements chain raw via ->next (no + * N_TPARAM wrap; parse.c:341). */ + Node *e0 = newnode(c->a, N_TNAME, fp); + e0->str = "str"; e0->strlen = 3; + Node *vfn = newnode(c->a, N_TFN, fp); + vfn->lhs = newnode(c->a, N_TNAME, fp); + vfn->lhs->str = "void"; vfn->lhs->strlen = 4; + Node *e1 = newnode(c->a, N_TPTR, fp); + e1->lhs = vfn; + Node *tup = newnode(c->a, N_TTUPLE, fp); + tup->list = e0; e0->next = e1; + Node *tsl = newnode(c->a, N_TSLICE, fp); + tsl->lhs = tup; + Node *arr = newnode(c->a, N_ARRLIT, fp); + arr->list = rhead; + tab = newnode(c->a, N_LET, fp); + tab->op = TK_CONST; + tab->str = "__wwtests"; + tab->lhs = tsl; + tab->rhs = arr; + /* pass 1 already ran, so install the table's name now — + * pass 2 (below) cexprs its rhs and main references it. */ + Type *tt = resolve_type(c, tsl); + tab->type = tt; + scope_define_in_module(c->cur, tab->str, NULL, SK_VAR, + tt, tab); + + /* return run(__wwtests); */ + Node *arg = newnode(c->a, N_IDENT, fp); + arg->str = "__wwtests"; Node *call = newnode(c->a, N_CALL, fp); call->lhs = newnode(c->a, N_IDENT, fp); - call->lhs->str = d->str; - Node *es = newnode(c->a, N_EXPRSTMT, fp); - es->lhs = call; - if (bhead == NULL) bhead = es; - else btail->next = es; - btail = es; + call->lhs->str = "run"; + call->list = arg; + Node *ret = newnode(c->a, N_RETURN, fp); + ret->lhs = call; + body->list = ret; } - Node *ret = newnode(c->a, N_RETURN, fp); - ret->lhs = newnode(c->a, N_INTLIT, fp); - ret->lhs->uval = 0; - if (bhead == NULL) bhead = ret; - else btail->next = ret; - Node *body = newnode(c->a, N_BLOCK, fp); - body->list = bhead; + Node *m = newnode(c->a, N_FNDECL, fp); m->str = "main"; m->export = 1; @@ -3039,12 +3099,16 @@ check_file(Checker *c, Node *file) m->body = body; m->type = build_fn_type(c, m); /* pass 1 already ran, so the install loop never stamped m's - * type; set it explicitly (pass 2 below reads d->type). */ + * type; set it explicitly (pass 2 below reads d->type). + * Append the table const (if any) then main to file->list. */ Node *tl = file->list; - if (tl == NULL) file->list = m; - else { + if (tl == NULL) { + file->list = tab ? tab : m; + if (tab) tab->next = m; + } else { while (tl->next) tl = tl->next; - tl->next = m; + if (tab) { tl->next = tab; tab->next = m; } + else tl->next = m; } } diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 5dcfa2bc..56e28be7 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -470,6 +470,19 @@ build_one(const char *src, int entry_is_dir, const char *out, return 1; } struct ImportSet visited = {0}; + /* #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 filename-keyed expand path + * (the visited set dedupes if a fixture imports it explicitly). + * The wwstage twin (selfhost/cmd/ww/main.ww) mirrors this. */ + if (is_test) { + char tpath[1024]; + int tdir = 0; + if (locate_import(srcdir, "test", tpath, sizeof tpath, &tdir)) { + if (tdir) expand_dir(cf, tpath, &visited, srcdir); + else expand(cf, tpath, &visited, srcdir); + } + } if (entry_is_dir) expand_dir(cf, srcd, &visited, srcdir); else expand(cf, src, &visited, srcdir); fclose(cf); @@ -792,9 +805,16 @@ do_test(int argc, char **argv) { const char *src = NULL; char incs[2048] = {0}; - /* test builds to a temp and runs it: -l/-L/-o carry no meaning here, so - * reject them (and any unknown flag) rather than silently swallow — - * byte-identical to the wwstage twin (selfhost/cmd/ww/main.ww dotest). */ + /* -c (compile-only, Go's `go test -c`) + -o build the test + * binary (and its lib/test-inclusive combined, via build_one's + * is_test auto-bundle + the T3 objstem redirect) WITHOUT running it — + * the byte-id gates feed .combined.ww to raw w6c -T / w6c_ww -T. + * -T stays internal to w6c; the driver never sees it. -l/-L carry no + * meaning for a test build, so they (and any unknown flag) are rejected + * rather than silently swallowed — byte-identical wording to the + * wwstage twin (selfhost/cmd/ww/main.ww dotest). */ + int compileonly = 0; + char outstem[1024] = {0}; for (int i = 0; i < argc; i++) { if (argv[i][0] == '-') { if (argv[i][1] == 'I') { @@ -812,6 +832,17 @@ do_test(int argc, char **argv) size_t n = strlen(incs); snprintf(incs + n, sizeof incs - n, "%s%s", n ? ":" : "", dir); + } else if (strcmp(argv[i], "-c") == 0) { + compileonly = 1; + } else if (strcmp(argv[i], "-o") == 0) { + if (i + 1 >= argc) { + fprintf(stderr, + "ww test: -o needs an argument\n"); + return 2; + } + snprintf(outstem, sizeof outstem, "%s", argv[++i]); + } else if (argv[i][1] == 'o' && argv[i][2]) { + snprintf(outstem, sizeof outstem, "%s", argv[i] + 2); } else { fprintf(stderr, "ww test: unknown flag\n"); return 2; @@ -833,25 +864,37 @@ do_test(int argc, char **argv) return 1; } char tmp[1024]; - snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d", getpid()); - if (build_one(resolved, is_dir, tmp, NULL, incs, "", "", 1) != 0) return 1; - int rc = run(tmp); - unlink(tmp); + const char *outp; + if (outstem[0]) outp = outstem; + else { snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d", getpid()); outp = tmp; } + if (build_one(resolved, is_dir, outp, outstem[0] ? outstem : NULL, + incs, "", "", 1) != 0) return 1; + if (compileonly) return 0; + int rc = run(outp); + if (!outstem[0]) unlink(outp); return rc; } if (S_ISREG(st.st_mode)) { - /* single .ww file — build+run it. */ + /* single .ww file — build, then run unless -c (compile-only). */ char tmp[1024]; - snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d", getpid()); - if (build_one(target, 0, tmp, NULL, incs, "", "", 1) != 0) return 1; - int rc = run(tmp); - unlink(tmp); + const char *outp; + if (outstem[0]) outp = outstem; + else { snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d", getpid()); outp = tmp; } + if (build_one(target, 0, outp, outstem[0] ? outstem : NULL, + incs, "", "", 1) != 0) return 1; + if (compileonly) return 0; + int rc = run(outp); + if (!outstem[0]) unlink(outp); return rc; } if (!S_ISDIR(st.st_mode)) { fprintf(stderr, "ww test: %s is neither file nor directory\n", target); return 1; } + if (compileonly || outstem[0]) { + fprintf(stderr, "ww test: -c/-o need a single test file\n"); + return 2; + } /* directory — run every *_test.ww inside. */ char **files = NULL; int n = 0; diff --git a/lib/test/run.ww b/lib/test/run.ww new file mode 100644 index 00000000..a5dc1cf9 --- /dev/null +++ b/lib/test/run.ww @@ -0,0 +1,108 @@ +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-filtering is task #17 +// commit-3 (drew spec §d), not here. + +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 nfail: 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; + }; + i += 1; + }; + tstputuint(tests.len - nfail); + tstputs(" passed, "); + tstputuint(nfail); + tstputs(" failed\n"); + return nfail; +}; + +// 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); +}; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 3e78d3f5..e57808da 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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 + // `("", &)` 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; }; }; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 9f4ff229..c594c81a 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -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 + // `("", &)` 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; }; }; }; diff --git a/selfhost/cmd/ww/main.combined.ww b/selfhost/cmd/ww/main.combined.ww index c49e78de..638cfda5 100644 --- a/selfhost/cmd/ww/main.combined.ww +++ b/selfhost/cmd/ww/main.combined.ww @@ -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 ; 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 : 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); }; diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 3ed0ecfc..ea793afd 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -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 ; 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 : 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); }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index ccbb7460..2f87138a 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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 + // `("", &)` 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; }; }; }; diff --git a/test/wcc/910_at_test.c b/test/wcc/910_at_test.c index af31a871..f1231f29 100644 --- a/test/wcc/910_at_test.c +++ b/test/wcc/910_at_test.c @@ -54,9 +54,10 @@ static int run_fixture(const char *bin, const char *comp, const char *drv) { int pid = getpid(); - char src[256], comb[256], asmf[256], obj[256], exe[256]; + char src[256], comb[256], binout[256], asmf[256], obj[256], exe[256]; char rt[1024], cmd[4096]; snprintf(src, sizeof src, "/tmp/at910_%s_%d.ww", comp, pid); + snprintf(binout, sizeof binout, "/tmp/at910_%s_%d", comp, pid); snprintf(comb, sizeof comb, "/tmp/at910_%s_%d.combined.ww", comp, pid); snprintf(asmf, sizeof asmf, "/tmp/at910_%s_%d.s", comp, pid); snprintf(obj, sizeof obj, "/tmp/at910_%s_%d.o", comp, pid); @@ -66,12 +67,12 @@ run_fixture(const char *bin, const char *comp, const char *drv) snprintf(cmd, sizeof cmd, "cp test/wcc/data/attest_pass.ww %s", src); if (runwait(cmd) != 0) { fprintf(stderr, "910 FAIL: cp\n"); return 1; } - /* Driver resolves `import rt` into .combined.ww. The build - * itself exits nonzero (the @test-only fixture has no main — that - * is exactly what -T synthesizes), but the combined unit is written - * before any compile step, so we depend on the ARTIFACT, not the - * exit code. */ - snprintf(cmd, sizeof cmd, "%s/%s build %s > /dev/null 2>&1", bin, drv, src); + /* `test -c -o ` (compile-only) resolves imports AND auto-bundles + * lib/test (#17) into .combined.ww — the synth's run() callee + * must link below — WITHOUT running the harness. We depend on the + * combined ARTIFACT; `-o` keeps the binary off the CWD. */ + snprintf(cmd, sizeof cmd, "%s/%s test -c -o %s %s > /dev/null 2>&1", + bin, drv, binout, src); runwait(cmd); if (access(comb, 0) != 0) { fprintf(stderr, "910 FAIL: %s build produced no %s\n", drv, comb); @@ -94,6 +95,10 @@ run_fixture(const char *bin, const char *comp, const char *drv) return 1; } unlink(src); unlink(comb); unlink(asmf); unlink(obj); unlink(exe); + unlink(binout); + char tmp[300]; + snprintf(tmp, sizeof tmp, "%s.s", binout); unlink(tmp); + snprintf(tmp, sizeof tmp, "%s.o", binout); unlink(tmp); return 0; } @@ -161,9 +166,18 @@ nondrop(const char *bin, const char *comp) fprintf(stderr, "910 FAIL: %s non-T nondrop compile\n", comp); return 1; } + /* The -T compile synthesizes a main that calls lib/test's run(), so it + * needs the lib/test-inclusive combined `ww test -c` writes (the plain + * compile above has no synth, so it stays on the raw fixture). */ + char stem[256], comb[300]; + snprintf(stem, sizeof stem, "/tmp/at910nd_%s_c_%d", comp, pid); + snprintf(comb, sizeof comb, "%s.combined.ww", stem); snprintf(cmd, sizeof cmd, - "%s/%s -T test/wcc/data/attest_nondrop.ww -o %s 2>/dev/null", - bin, comp, tee); + "%s/ww test -c -o %s test/wcc/data/attest_nondrop.ww > /dev/null 2>&1", + bin, stem); + runwait(cmd); + snprintf(cmd, sizeof cmd, + "%s/%s -T %s -o %s 2>/dev/null", bin, comp, comb, tee); if (runwait(cmd) != 0) { fprintf(stderr, "910 FAIL: %s -T nondrop compile\n", comp); return 1; @@ -184,7 +198,11 @@ nondrop(const char *bin, const char *comp) break; } } - unlink(plain); unlink(tee); + unlink(plain); unlink(tee); unlink(comb); + char tmp[300]; + snprintf(tmp, sizeof tmp, "%s.s", stem); unlink(tmp); + snprintf(tmp, sizeof tmp, "%s.o", stem); unlink(tmp); + unlink(stem); return rc; } diff --git a/test/wcc/911_attest_record.c b/test/wcc/911_attest_record.c new file mode 100644 index 00000000..bb6574a1 --- /dev/null +++ b/test/wcc/911_attest_record.c @@ -0,0 +1,173 @@ +/* + * 911_attest_record — #17 record-and-continue harness (lib/test + the + * table-synth -T entry). Sibling of 910_at_test (which pins the synth + * itself + the #6 non-T drop); this one pins the RUNTIME behaviour the + * fork+wait4 runner adds: + * + * 1. RECORD-AND-CONTINUE — `ww test` the mixed fixture (one pass, then + * assert-fail/SIGSEGV/SIGFPE, then a final pass). The runner must + * proceed past every failure, emit the right per-test `name ... ok/ + * FAIL` line (table-driven below), print the right `P passed, F + * failed` summary, and exit with the failure count (nonzero). + * 2. ALL-PASS — `ww test` the all-pass fixture exits 0 (no false + * failure from the new runner). + * 3. BYTE-ID (rule 10) — `ww test -c` the mixed fixture into a + * lib/test-inclusive combined, then `w6c -T` and `w6c_ww -T` must + * emit byte-identical asm (the table + fork loop, both stages). + * + * The fork changes failure OUTPUT, not pass behaviour: a passing fixture + * still exits 0, so the 35 converted lib tests stay green (904/967/...). + */ +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +static const char * +absbin(void) +{ + const char *b = getenv("BIN"); + if (!b) b = "out/bin"; + if (b[0] == '/') return b; + static char buf[2048]; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return NULL; + snprintf(buf, sizeof buf, "%s/%s", cwd, b); + return buf; +} + +static int +slurp(const char *path, char *out, size_t outsz) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + size_t n = fread(out, 1, outsz - 1, f); + out[n] = '\0'; + fclose(f); + return 0; +} + +/* Each expected per-test verdict line, in source/collection order. */ +static const char *expect_lines[] = { + "rec_pass_a ... ok", + "rec_assert ... FAIL", + "rec_segv ... FAIL", + "rec_div0 ... FAIL", + "rec_pass_b ... ok", + "2 passed, 3 failed", +}; + +static int +record_continue(const char *bin) +{ + int pid = getpid(); + char out[256], cmd[4096], buf[8192]; + snprintf(out, sizeof out, "/tmp/at911_%d.out", pid); + + /* `ww test` builds the fixture (auto-bundling lib/test) and runs it; + * the exit code is the runner's failure count. */ + snprintf(cmd, sizeof cmd, + "%s/ww test test/wcc/data/attest_record.ww > %s 2>/dev/null", + bin, out); + int rc = runwait(cmd); + if (rc != 3) { + fprintf(stderr, "911 FAIL: record-and-continue exit %d " + "(expected 3 failures)\n", rc); + return 1; + } + if (slurp(out, buf, sizeof buf) != 0) { + fprintf(stderr, "911 FAIL: cannot read runner output\n"); + return 1; + } + for (size_t i = 0; i < sizeof expect_lines / sizeof expect_lines[0]; i++) { + if (strstr(buf, expect_lines[i]) == NULL) { + fprintf(stderr, "911 FAIL: missing runner line '%s'\n" + "--- got ---\n%s\n", expect_lines[i], buf); + unlink(out); + return 1; + } + } + unlink(out); + return 0; +} + +static int +all_pass(const char *bin) +{ + char cmd[4096]; + snprintf(cmd, sizeof cmd, + "%s/ww test test/wcc/data/attest_pass.ww > /dev/null 2>&1", bin); + int rc = runwait(cmd); + if (rc != 0) { + fprintf(stderr, "911 FAIL: all-pass fixture exit %d " + "(expected 0)\n", rc); + return 1; + } + return 0; +} + +static int +byteid(const char *bin) +{ + int pid = getpid(); + char stem[256], comb[256], cs[256], ws[256], cmd[4096]; + snprintf(stem, sizeof stem, "/tmp/at911_bid_%d", pid); + snprintf(comb, sizeof comb, "%s.combined.ww", stem); + snprintf(cs, sizeof cs, "/tmp/at911_bid_c_%d.s", pid); + snprintf(ws, sizeof ws, "/tmp/at911_bid_w_%d.s", pid); + + /* `ww test -c -o ` (compile-only) writes a lib/test-inclusive + * combined beside -o without running the harness. */ + snprintf(cmd, sizeof cmd, + "%s/ww test -c -o %s test/wcc/data/attest_record.ww > /dev/null 2>&1", + bin, stem); + runwait(cmd); + if (access(comb, 0) != 0) { + fprintf(stderr, "911 FAIL: ww test -c produced no %s\n", comb); + return 1; + } + snprintf(cmd, sizeof cmd, "%s/w6c -T %s -o %s 2>/dev/null", bin, comb, cs); + if (runwait(cmd) != 0) { fprintf(stderr, "911 FAIL: w6c -T (byteid)\n"); return 1; } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -T %s -o %s 2>/dev/null", bin, comb, ws); + if (runwait(cmd) != 0) { fprintf(stderr, "911 FAIL: w6c_ww -T (byteid)\n"); return 1; } + + char bc[262144], bw[262144]; + int rc = 0; + if (slurp(cs, bc, sizeof bc) < 0 || slurp(ws, bw, sizeof bw) < 0) { + fprintf(stderr, "911 FAIL: slurp byteid asm\n"); + rc = 1; + } else if (strcmp(bc, bw) != 0) { + fprintf(stderr, "911 FAIL: -T asm differs between stages\n"); + rc = 1; + } + unlink(comb); unlink(cs); unlink(ws); + snprintf(cmd, sizeof cmd, "%s.s", stem); unlink(cmd); + snprintf(cmd, sizeof cmd, "%s.o", stem); unlink(cmd); + unlink(stem); + return rc; +} + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) { fprintf(stderr, "911 FAIL: getcwd\n"); return 1; } + + if (record_continue(bin) != 0) return 1; + if (all_pass(bin) != 0) return 1; + if (byteid(bin) != 0) return 1; + + printf("@test record-and-continue: 3 fault classes recorded + run " + "proceeds + counts + all-pass exit 0 + cs/ww byte-id (#17)\n"); + return 0; +} diff --git a/test/wcc/949_driver_flagargs.c b/test/wcc/949_driver_flagargs.c index 23f60385..8ee773af 100644 --- a/test/wcc/949_driver_flagargs.c +++ b/test/wcc/949_driver_flagargs.c @@ -5,9 +5,11 @@ * build/run — a lone -I/-L/-l/-o (flag with no following argument) is a * hard error "ww : -X needs an argument" (rc 2), not a * silently-swallowed positional. - * test — only -I carries meaning; -l/-L/-o and any unknown flag are - * rejected with "ww test: unknown flag" (rc 2); a lone -I is - * "ww test: -I needs an argument" (rc 2). + * test — -I/-c/-o carry meaning; -l/-L and any unknown flag are + * rejected with "ww test: unknown flag" (rc 2); a lone -I/-o + * is "ww test: -X needs an argument" (rc 2); -c/-o without a + * single test file is "ww test: -c/-o need a single test file" + * (rc 2, #17). * Each row asserts the expected rc + stderr substring AND that the two * drivers are byte-identical (rule 10): the cstage parse_build_flags / * do_test must match the wwstage main.ww dobuild/dorun/dotest verbatim. @@ -36,9 +38,14 @@ static const struct row rows[] = { { "run -o", 2, "ww run: -o needs an argument" }, { "run -l", 2, "ww run: -l needs an argument" }, { "test -l", 2, "ww test: unknown flag" }, - { "test -o x", 2, "ww test: unknown flag" }, { "test -zz", 2, "ww test: unknown flag" }, { "test -I", 2, "ww test: -I needs an argument" }, + /* #17: -c (compile-only) + -o are now meaningful for `test`; a lone -o + * needs an arg, and -c/-o require a single test file (not the default + * "." directory enumeration). */ + { "test -o", 2, "ww test: -o needs an argument" }, + { "test -o x", 2, "ww test: -c/-o need a single test file" }, + { "test -c", 2, "ww test: -c/-o need a single test file" }, }; /* Run "/ " capturing rc + stderr text into err (NUL- diff --git a/test/wcc/989_lib_byteid.c b/test/wcc/989_lib_byteid.c index 17154f0a..3d4d85fa 100644 --- a/test/wcc/989_lib_byteid.c +++ b/test/wcc/989_lib_byteid.c @@ -254,18 +254,29 @@ check_one(const char *bin, const char *cwd, const struct ent *e, int idx) } /* exit deliberately ignored: the combined unit is written before - * codegen (cf 990 resolveunit) and only it matters here. */ - snprintf(cmd, sizeof cmd, - "cd %s && timeout 180 %s/ww build %s %s >/dev/null 2>&1", - td, bin, incs, base); + * codegen (cf 990 resolveunit) and only it matters here. A @test + * fixture builds via `ww test -c -o ` (compile-only) so the + * resolved unit AUTO-BUNDLES lib/test — the #17 synth's `run()` callee + * must resolve when w6c/w6c_ww -T compile the combined below. Both the + * `-o ` (test) and the next-to-source (build) paths land the + * combined at .combined.ww. Import-probes carry their own main + * and stay a plain non-test build. */ + char stem[512]; + snprintf(stem, sizeof stem, "%s", base); + char *sd = strrchr(stem, '.'); + if (sd) *sd = '\0'; + if (e->fixture) + snprintf(cmd, sizeof cmd, + "cd %s && timeout 180 %s/ww test -c -o %s %s %s >/dev/null 2>&1", + td, bin, stem, incs, base); + else + snprintf(cmd, sizeof cmd, + "cd %s && timeout 180 %s/ww build %s %s >/dev/null 2>&1", + td, bin, incs, base); runwait(cmd); char comb[512]; - snprintf(comb, sizeof comb, "%s/%s", td, base); - char *dot = strrchr(comb, '.'); - if (dot) *dot = '\0'; - size_t cn = strlen(comb); - snprintf(comb + cn, sizeof comb - cn, ".combined.ww"); + snprintf(comb, sizeof comb, "%s/%s.combined.ww", td, stem); if (access(comb, 0) != 0) { fprintf(stderr, "lib_byteid FAIL: %s — no resolved unit\n", label); goto out; @@ -357,6 +368,10 @@ static const char *covered[] = { "lib/io", "lib/math", "lib/rt", "lib/types", "lib/ww/parse", /* module body dragged into the lib/strconv/test fixtures */ "lib/strconv", + /* #17: the @test runner is AUTO-BUNDLED into every -T combined, so + * every @test fixture above byte-ids it cs/ww; 911_attest_record also + * compares it directly. It has no _test.ww of its own. */ + "lib/test", NULL, }; diff --git a/test/wcc/997_at_test_ww.c b/test/wcc/997_at_test_ww.c index 26cc0e80..9ebcd6a6 100644 --- a/test/wcc/997_at_test_ww.c +++ b/test/wcc/997_at_test_ww.c @@ -66,9 +66,10 @@ static int run_fixture(const char *bin) { int pid = getpid(); - char src[256], comb[256], asmf[256], obj[256], exe[256]; + char src[256], comb[256], binout[256], asmf[256], obj[256], exe[256]; char rt[1024], cmd[4096]; snprintf(src, sizeof src, "/tmp/at997_%d.ww", pid); + snprintf(binout, sizeof binout, "/tmp/at997_%d", pid); snprintf(comb, sizeof comb, "/tmp/at997_%d.combined.ww", pid); snprintf(asmf, sizeof asmf, "/tmp/at997_%d.s", pid); snprintf(obj, sizeof obj, "/tmp/at997_%d.o", pid); @@ -78,10 +79,12 @@ run_fixture(const char *bin) snprintf(cmd, sizeof cmd, "cp test/wcc/data/attest_pass.ww %s", src); if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: cp\n"); return 1; } - /* ww_ww build resolves `import rt` into the combined unit; it exits - * nonzero (no main — that is what -T synthesizes), so we gate on the - * combined ARTIFACT existing, not the exit code. */ - snprintf(cmd, sizeof cmd, "%s/ww_ww build %s > /dev/null 2>&1", bin, src); + /* `ww_ww test -c -o ` (compile-only) resolves imports AND auto- + * bundles lib/test (#17) into .combined.ww — the synth's run() + * callee must link below — WITHOUT running the harness. We gate on the + * combined ARTIFACT existing; `-o` keeps the binary off the CWD. */ + snprintf(cmd, sizeof cmd, "%s/ww_ww test -c -o %s %s > /dev/null 2>&1", + bin, binout, src); runwait(cmd); if (access(comb, 0) != 0) { fprintf(stderr, "997 FAIL: ww_ww build produced no %s\n", comb); @@ -101,23 +104,38 @@ run_fixture(const char *bin) return 1; } unlink(src); unlink(comb); unlink(asmf); unlink(obj); unlink(exe); + unlink(binout); + char tmp[300]; + snprintf(tmp, sizeof tmp, "%s.s", binout); unlink(tmp); + snprintf(tmp, sizeof tmp, "%s.o", binout); unlink(tmp); return 0; } -/* byteid — `w6c -T` and `w6c_ww -T` on the same source must agree. */ +/* byteid — `w6c -T` and `w6c_ww -T` on the same unit must agree. The #17 + * synth's run() callee only resolves against the auto-bundled lib/test, so + * we byte-compare the lib/test-INCLUSIVE combined (raw -T of the fixture + * would loud-reject the undefined run); `ww test -c` writes that combined. */ static int byteid(const char *bin) { int pid = getpid(); - char cs[256], ws[256], cmd[4096]; + char stem[256], comb[256], cs[256], ws[256], cmd[4096]; + snprintf(stem, sizeof stem, "/tmp/at997_bid_%d", pid); + snprintf(comb, sizeof comb, "%s.combined.ww", stem); snprintf(cs, sizeof cs, "/tmp/at997_bid_c_%d.s", pid); snprintf(ws, sizeof ws, "/tmp/at997_bid_w_%d.s", pid); - snprintf(cmd, sizeof cmd, "%s/w6c -T test/wcc/data/attest_pass.ww -o %s 2>/dev/null", - bin, cs); + snprintf(cmd, sizeof cmd, + "%s/ww test -c -o %s test/wcc/data/attest_pass.ww > /dev/null 2>&1", + bin, stem); + runwait(cmd); + if (access(comb, 0) != 0) { + fprintf(stderr, "997 FAIL: ww test -c produced no %s\n", comb); + return 1; + } + snprintf(cmd, sizeof cmd, "%s/w6c -T %s -o %s 2>/dev/null", bin, comb, cs); if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6c -T (byteid)\n"); return 1; } - snprintf(cmd, sizeof cmd, "%s/w6c_ww -T test/wcc/data/attest_pass.ww -o %s 2>/dev/null", - bin, ws); + snprintf(cmd, sizeof cmd, "%s/w6c_ww -T %s -o %s 2>/dev/null", bin, comb, ws); if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6c_ww -T (byteid)\n"); return 1; } char *bc = NULL, *bw = NULL; @@ -131,7 +149,11 @@ byteid(const char *bin) rc = 1; } free(bc); free(bw); - unlink(cs); unlink(ws); + unlink(comb); unlink(cs); unlink(ws); + char tmp[300]; + snprintf(tmp, sizeof tmp, "%s.s", stem); unlink(tmp); + snprintf(tmp, sizeof tmp, "%s.o", stem); unlink(tmp); + unlink(stem); return rc; } @@ -180,10 +202,12 @@ static int nondrop(const char *bin) { int pid = getpid(); - char wp[256], wt[256], cp[256], cmd[4096]; + char wp[256], wt[256], cp[256], stem[256], comb[300], cmd[4096]; snprintf(wp, sizeof wp, "/tmp/at997nd_wp_%d.s", pid); snprintf(wt, sizeof wt, "/tmp/at997nd_wt_%d.s", pid); snprintf(cp, sizeof cp, "/tmp/at997nd_cp_%d.s", pid); + snprintf(stem, sizeof stem, "/tmp/at997nd_c_%d", pid); + snprintf(comb, sizeof comb, "%s.combined.ww", stem); snprintf(cmd, sizeof cmd, "%s/w6c_ww test/wcc/data/attest_nondrop.ww -o %s 2>/dev/null", bin, wp); @@ -191,8 +215,14 @@ nondrop(const char *bin) fprintf(stderr, "997 FAIL: w6c_ww non-T nondrop compile\n"); return 1; } + /* the -T compile synthesizes a main calling lib/test's run(), so it + * needs the lib/test-inclusive combined `ww test -c` writes. */ snprintf(cmd, sizeof cmd, - "%s/w6c_ww -T test/wcc/data/attest_nondrop.ww -o %s 2>/dev/null", bin, wt); + "%s/ww test -c -o %s test/wcc/data/attest_nondrop.ww > /dev/null 2>&1", + bin, stem); + runwait(cmd); + snprintf(cmd, sizeof cmd, + "%s/w6c_ww -T %s -o %s 2>/dev/null", bin, comb, wt); if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6c_ww -T nondrop compile\n"); return 1; @@ -232,7 +262,11 @@ nondrop(const char *bin) } free(bc); free(bw); } - unlink(wp); unlink(wt); unlink(cp); + unlink(wp); unlink(wt); unlink(cp); unlink(comb); + char tmp[300]; + snprintf(tmp, sizeof tmp, "%s.s", stem); unlink(tmp); + snprintf(tmp, sizeof tmp, "%s.o", stem); unlink(tmp); + unlink(stem); return rc; } diff --git a/test/wcc/data/attest_record.ww b/test/wcc/data/attest_record.ww new file mode 100644 index 00000000..50588c0b --- /dev/null +++ b/test/wcc/data/attest_record.ww @@ -0,0 +1,31 @@ +// @test fixture for #17 record-and-continue: a mix of one passing test, +// then the three runtime fault classes the lib/test fork+wait4 runner +// must each catch and PROCEED past (failed assert → SIGABRT, nil deref → +// SIGSEGV, divide-by-zero → SIGFPE), then a final passing test to prove +// the run reaches the tail after every failure. Driver: +// test/wcc/911_attest_record.c. The runner reports in source order, so +// the expected per-test verdicts are pinned positionally there. + +package data; + +@test fn rec_pass_a() void = { + assert(1 + 1 == 2); +}; + +@test fn rec_assert() void = { + assert(1 == 2); +}; + +@test fn rec_segv() void = { + let p: *i32 = nil: *i32; + *p = 7i32; +}; + +@test fn rec_div0() void = { + let z: i32 = 0; + let _: i32 = 1 / z; +}; + +@test fn rec_pass_b() void = { + assert(true); +};