test: ww-native behavior harness (runww.ww) + 2 pilot cases
First piece of the Go-model test rebuild: runww.ww is a ww program (the test/run.go analog) that drives compiler cases through BOTH stages and asserts behavior/diagnostics. Directives //ww:run / //ww:run-exit N / //ww:error "<substr>" / //ww:compile; spawn+stderr-capture lift the driver's procrun (main.ww:160) + dup2(2). The //ww:error check requires rc!=0 AND the diagnostic substring (the #20 non-vacuity guard -- a crash can't pass), and a malformed error directive fails loudly. Bulk corpus migration + ww test wiring are follow-up folds; case spawns need os.envp() (#28) and per-pid /tmp paths (#29) first.
This commit is contained in:
311
test/runww.ww
Normal file
311
test/runww.ww
Normal file
@@ -0,0 +1,311 @@
|
||||
// runww — ww-native behavior-test harness, the analog of Go's
|
||||
// test/run.go. Each case file under test/wcc/data/<name>/case.ww carries
|
||||
// a leading `//ww:` directive describing how to test it; runww builds (and
|
||||
// optionally runs) the case through BOTH compiler stages and checks the
|
||||
// directive holds. This replaces the per-test C harnesses that embedded ww
|
||||
// source as C string literals and duplicated fork/exec/grep boilerplate.
|
||||
//
|
||||
// Directives (first `//ww:` line of the case):
|
||||
// //ww:run compile + execute, expect exit 0
|
||||
// //ww:run-exit N compile + execute, expect exit N
|
||||
// //ww:error "<sub>" compilation must FAIL with <sub> on stderr
|
||||
// //ww:compile compile must succeed (don't run)
|
||||
//
|
||||
// Invoke (pilot): `BIN=out/bin out/bin/ww run test/runww.ww <case.ww>...`.
|
||||
// Full `ww test` integration is a later fold.
|
||||
//
|
||||
// Spawn + stderr capture are lifted from the driver's procrun
|
||||
// (selfhost/cmd/ww/main.ww:154: fork+execve+wait4+real-exit-code); the
|
||||
// only addition is the child-side dup2(efd, 2) redirect, mirroring the C
|
||||
// harnesses' `2>errf` so the parent can grep the diagnostic. #20
|
||||
// non-vacuity: a `//ww:error` row passes ONLY if the build failed AND the
|
||||
// diagnostic substring is present — a crash (no diagnostic) FAILS the row.
|
||||
//
|
||||
// Env: the harness execve's the child with a nil envp, so a case that
|
||||
// `import`s a lib module (needing WW_SRCLIB/WW_LIB) is out of pilot scope
|
||||
// until os exposes an envp() forwarder; the pilot cases are self-contained.
|
||||
|
||||
package main;
|
||||
|
||||
import os;
|
||||
|
||||
type dkind = enum i32 {
|
||||
NONE = 0,
|
||||
RUN = 1,
|
||||
RUNEXIT = 2,
|
||||
ERROR = 3,
|
||||
COMPILE = 4,
|
||||
};
|
||||
|
||||
type dirv = struct {
|
||||
kind: dkind,
|
||||
code: i32, // expected exit for RUNEXIT
|
||||
subp: *u8, // diagnostic substring (view into the case buffer)
|
||||
subn: i32,
|
||||
};
|
||||
|
||||
fn pr(s: str) void = {
|
||||
os.write(1i32, s.ptr, s.len: u64);
|
||||
};
|
||||
|
||||
fn mkstr(p: *u8, n: i32) str = {
|
||||
let s: str;
|
||||
s.ptr = p;
|
||||
s.len = n;
|
||||
return s;
|
||||
};
|
||||
|
||||
// cstr — NUL-terminated heap copy for an execve argv element.
|
||||
fn cstr(s: str) *u8 = {
|
||||
let b: []u8 = alloc([], (s.len: u64) + 1u64)!;
|
||||
b.len = s.len + 1;
|
||||
let i: i32 = 0;
|
||||
for (i < s.len) { b[i] = s[i]; i += 1; };
|
||||
b[s.len] = 0u8;
|
||||
return b.ptr;
|
||||
};
|
||||
|
||||
fn joinp(dir: str, name: str) str = {
|
||||
let n: i32 = dir.len + name.len;
|
||||
let b: []u8 = alloc([], n: u64)!;
|
||||
b.len = n;
|
||||
let i: i32 = 0;
|
||||
for (i < dir.len) { b[i] = dir[i]; i += 1; };
|
||||
let j: i32 = 0;
|
||||
for (j < name.len) { b[dir.len + j] = name[j]; j += 1; };
|
||||
return mkstr(b.ptr, n);
|
||||
};
|
||||
|
||||
fn findsub(hp: *u8, hn: i32, np: *u8, nn: i32) i32 = {
|
||||
if (nn == 0) { return 0; };
|
||||
let i: i32 = 0;
|
||||
for (i + nn <= hn) {
|
||||
let j: i32 = 0;
|
||||
let ok: bool = true;
|
||||
for (j < nn) {
|
||||
if (hp[i + j] != np[j]) { ok = false; break; };
|
||||
j += 1;
|
||||
};
|
||||
if (ok) { return i; };
|
||||
i += 1;
|
||||
};
|
||||
return -1;
|
||||
};
|
||||
|
||||
fn contains(hp: *u8, hn: i32, needle: str) bool = {
|
||||
return findsub(hp, hn, needle.ptr, needle.len) >= 0;
|
||||
};
|
||||
|
||||
fn matchat(hp: *u8, hn: i32, off: i32, lit: str) bool = {
|
||||
if (off + lit.len > hn) { return false; };
|
||||
let i: i32 = 0;
|
||||
for (i < lit.len) {
|
||||
if (hp[off + i] != lit[i]) { return false; };
|
||||
i += 1;
|
||||
};
|
||||
return true;
|
||||
};
|
||||
|
||||
// readfile — slurp `path` into `buf`, returning bytes read or -1.
|
||||
fn readfile(path: str, buf: []u8) i64 = {
|
||||
let fd: i32 = os.open(path, os.flag.RDONLY, 0i32);
|
||||
if (fd < 0) { return -1i64; };
|
||||
let total: u64 = 0u64;
|
||||
for (true) {
|
||||
let r: i64 = os.read(fd, buf.ptr + total, (buf.len: u64) - total);
|
||||
if (r <= 0i64) { break; };
|
||||
total += r: u64;
|
||||
if (total >= buf.len: u64) { break; };
|
||||
};
|
||||
os.close(fd);
|
||||
return total: i64;
|
||||
};
|
||||
|
||||
fn parsedir(cp: *u8, cn: i32, out: *dirv) void = {
|
||||
out.kind = dkind.NONE;
|
||||
out.code = 0;
|
||||
out.subp = nil: *u8;
|
||||
out.subn = 0;
|
||||
|
||||
let i: i32 = findsub(cp, cn, "//ww:".ptr, 5i32);
|
||||
if (i < 0) { return; };
|
||||
let d: i32 = i + 5; // first byte after "//ww:"
|
||||
|
||||
let le: i32 = d; // end of the directive line
|
||||
for (le < cn) {
|
||||
if (cp[le] == 10u8) { break; }; // '\n'
|
||||
le += 1;
|
||||
};
|
||||
|
||||
if (matchat(cp, cn, d, "run-exit")) {
|
||||
out.kind = dkind.RUNEXIT;
|
||||
let k: i32 = d + 8;
|
||||
for (k < le) { if (cp[k] != 32u8) { break; }; k += 1; }; // skip ' '
|
||||
let v: i32 = 0;
|
||||
for (k < le) {
|
||||
let c: u8 = cp[k];
|
||||
if (c < 48u8) { break; };
|
||||
if (c > 57u8) { break; };
|
||||
v = v * 10 + (c - 48u8): i32;
|
||||
k += 1;
|
||||
};
|
||||
out.code = v;
|
||||
return;
|
||||
};
|
||||
if (matchat(cp, cn, d, "run")) { out.kind = dkind.RUN; return; };
|
||||
if (matchat(cp, cn, d, "compile")) { out.kind = dkind.COMPILE; return; };
|
||||
if (matchat(cp, cn, d, "error")) {
|
||||
out.kind = dkind.ERROR;
|
||||
// substring between the two double-quote bytes (34)
|
||||
let q1: i32 = -1;
|
||||
let k: i32 = d;
|
||||
for (k < le) { if (cp[k] == 34u8) { q1 = k; break; }; k += 1; };
|
||||
// #20 non-vacuity: an absent or empty substring would collapse the
|
||||
// ERROR check to a bare rc!=0, which a crash satisfies — the exact
|
||||
// hole this harness closes. Reject the directive loudly instead.
|
||||
if (q1 < 0) { out.kind = dkind.NONE; return; };
|
||||
let q2: i32 = -1;
|
||||
k = q1 + 1;
|
||||
for (k < le) { if (cp[k] == 34u8) { q2 = k; break; }; k += 1; };
|
||||
if (q2 < 0) { out.kind = dkind.NONE; return; };
|
||||
if (q2 == q1 + 1) { out.kind = dkind.NONE; return; };
|
||||
out.subp = cp + (q1 + 1): u64;
|
||||
out.subn = q2 - (q1 + 1);
|
||||
return;
|
||||
};
|
||||
};
|
||||
|
||||
// runcap — fork+execve `drv` with `argv`, child stderr redirected into
|
||||
// errpath. Returns the child's real exit code (1 on signal, -1 on
|
||||
// fork/wait failure). Lifted from selfhost/cmd/ww/main.ww:154 procrun.
|
||||
fn runcap(drv: str, argv: []*u8, errpath: str) i32 = {
|
||||
let pid: i32 = os.fork();
|
||||
if (pid < 0) { return -1; };
|
||||
if (pid == 0) {
|
||||
let efd: i32 = os.open(errpath,
|
||||
os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
|
||||
if (efd >= 0) { os.dup2(efd, 2i32); };
|
||||
os.execve(drv, argv.ptr, nil: **u8);
|
||||
os.exit(127);
|
||||
};
|
||||
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;
|
||||
};
|
||||
|
||||
// runcase — build (and maybe run) one case through `drv`; return whether
|
||||
// the directive held.
|
||||
fn runcase(drv: str, casepath: str, d: *dirv, errpath: str, tmpout: str) bool = {
|
||||
let argv: []*u8 = alloc([], 6u64)!;
|
||||
if (d.kind == dkind.COMPILE) {
|
||||
argv.len = 6;
|
||||
argv[0] = cstr(drv);
|
||||
argv[1] = cstr("build");
|
||||
argv[2] = cstr("-o");
|
||||
argv[3] = cstr(tmpout);
|
||||
argv[4] = cstr(casepath);
|
||||
argv[5] = nil: *u8;
|
||||
} else {
|
||||
// RUN / RUNEXIT / ERROR all go through `ww run`: on a reject the
|
||||
// compile fails before execution, so the diagnostic still lands on
|
||||
// stderr and nothing is left to clean up.
|
||||
argv.len = 4;
|
||||
argv[0] = cstr(drv);
|
||||
argv[1] = cstr("run");
|
||||
argv[2] = cstr(casepath);
|
||||
argv[3] = nil: *u8;
|
||||
};
|
||||
|
||||
let rc: i32 = runcap(drv, argv, errpath);
|
||||
|
||||
if (d.kind == dkind.ERROR) {
|
||||
if (rc == 0) { return false; }; // built+ran ok → not a reject
|
||||
let eb: []u8 = alloc([], 65536u64)!;
|
||||
eb.len = 65536;
|
||||
let en: i64 = readfile(errpath, eb);
|
||||
if (en < 0i64) { return false; };
|
||||
// #20: rc!=0 AND the diagnostic body present (a crash has no body).
|
||||
return contains(eb.ptr, en: i32, mkstr(d.subp, d.subn));
|
||||
};
|
||||
if (d.kind == dkind.RUNEXIT) { return rc == d.code; };
|
||||
if (d.kind == dkind.RUN) { return rc == 0; };
|
||||
if (d.kind == dkind.COMPILE) { return rc == 0; };
|
||||
return false;
|
||||
};
|
||||
|
||||
export fn main() int = {
|
||||
let a: []str = os.args();
|
||||
if (a.len < 2) {
|
||||
pr("runww: usage: runww <case.ww>...\n");
|
||||
return 2;
|
||||
};
|
||||
|
||||
let bin: str = "out/bin";
|
||||
match (os.getenv("BIN")) {
|
||||
case let v: str => { bin = v; };
|
||||
case void => { };
|
||||
};
|
||||
let cdrv: str = joinp(bin, "/ww");
|
||||
let wdrv: str = joinp(bin, "/ww_ww");
|
||||
let wwok: bool = os.access(wdrv, 1i32) == 0; // X_OK gate, mirrors C harness
|
||||
|
||||
// Fixed scratch paths: the pilot drives cases sequentially in one
|
||||
// process, so reuse is safe; per-pid uniqueness for parallel invocation
|
||||
// is a later wiring fold (task #29).
|
||||
let errpath: str = "/tmp/runww.err";
|
||||
let tmpout: str = "/tmp/runww.bin";
|
||||
|
||||
let total: i32 = 0;
|
||||
let fail: i32 = 0;
|
||||
|
||||
let ci: i32 = 1;
|
||||
for (ci < a.len) {
|
||||
let casepath: str = a[ci];
|
||||
|
||||
let cbuf: []u8 = alloc([], 65536u64)!;
|
||||
cbuf.len = 65536;
|
||||
let cn: i64 = readfile(casepath, cbuf);
|
||||
if (cn < 0i64) {
|
||||
pr("FAIL read "); pr(casepath); pr("\n");
|
||||
fail += 1; total += 1; ci += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
let d: dirv;
|
||||
parsedir(cbuf.ptr, cn: i32, &d);
|
||||
if (d.kind == dkind.NONE) {
|
||||
pr("FAIL no-directive "); pr(casepath); pr("\n");
|
||||
fail += 1; total += 1; ci += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
total += 1;
|
||||
if (runcase(cdrv, casepath, &d, errpath, tmpout)) {
|
||||
pr("PASS cstage "); pr(casepath); pr("\n");
|
||||
} else {
|
||||
pr("FAIL cstage "); pr(casepath); pr("\n");
|
||||
fail += 1;
|
||||
};
|
||||
|
||||
if (wwok) {
|
||||
total += 1;
|
||||
if (runcase(wdrv, casepath, &d, errpath, tmpout)) {
|
||||
pr("PASS wwstage "); pr(casepath); pr("\n");
|
||||
} else {
|
||||
pr("FAIL wwstage "); pr(casepath); pr("\n");
|
||||
fail += 1;
|
||||
};
|
||||
};
|
||||
|
||||
ci += 1;
|
||||
};
|
||||
|
||||
if (fail > 0) {
|
||||
pr("runww: FAIL\n");
|
||||
return 1;
|
||||
};
|
||||
pr("runww: all ok\n");
|
||||
return 0;
|
||||
};
|
||||
8
test/wcc/data/runww_enum_reject/case.ww
Normal file
8
test/wcc/data/runww_enum_reject/case.ww
Normal file
@@ -0,0 +1,8 @@
|
||||
//ww:error "enum storage type must be integer"
|
||||
// Lifted from test/wcc/850_enum_reject.c reject_row "nonint_stor": a
|
||||
// non-integer enum storage type must be LOUD-rejected by both stages.
|
||||
// The directive's substring is the shared diagnostic body (no file:line
|
||||
// prefix, which differs cstage vs wwstage) — the #20 non-vacuity anchor.
|
||||
package main;
|
||||
type e = enum f64 { A };
|
||||
fn main() i32 = { return e.A as i32; };
|
||||
7
test/wcc/data/runww_enum_run/case.ww
Normal file
7
test/wcc/data/runww_enum_run/case.ww
Normal file
@@ -0,0 +1,7 @@
|
||||
//ww:run-exit 6
|
||||
// Lifted from test/wcc/850_enum_reject.c ok_row "distinct": a valid enum
|
||||
// with forward member arithmetic must BUILD and run. main returns
|
||||
// e.A (0) + e.C (B+1 = 6) == 6, signalled via the process exit code.
|
||||
package main;
|
||||
type e = enum { A, B = 5, C = B + 1 };
|
||||
fn main() i32 = { return e.A as i32 + e.C as i32; };
|
||||
Reference in New Issue
Block a user