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:
118
cmd/wcc/check.c
118
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
|
||||
* `("<name>", &<name>)` 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <stem> 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 <stem>.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;
|
||||
|
||||
Reference in New Issue
Block a user