From b817e5d4985050cdc6dac2d050910ce879c46204 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sun, 21 Jun 2026 15:28:59 +0900 Subject: [PATCH] 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 "" / //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. --- test/runww.ww | 311 ++++++++++++++++++++++++ test/wcc/data/runww_enum_reject/case.ww | 8 + test/wcc/data/runww_enum_run/case.ww | 7 + 3 files changed, 326 insertions(+) create mode 100644 test/runww.ww create mode 100644 test/wcc/data/runww_enum_reject/case.ww create mode 100644 test/wcc/data/runww_enum_run/case.ww diff --git a/test/runww.ww b/test/runww.ww new file mode 100644 index 00000000..404c3e27 --- /dev/null +++ b/test/runww.ww @@ -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//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 "" compilation must FAIL with on stderr +// //ww:compile compile must succeed (don't run) +// +// Invoke (pilot): `BIN=out/bin out/bin/ww run test/runww.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 ...\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; +}; diff --git a/test/wcc/data/runww_enum_reject/case.ww b/test/wcc/data/runww_enum_reject/case.ww new file mode 100644 index 00000000..c87267c4 --- /dev/null +++ b/test/wcc/data/runww_enum_reject/case.ww @@ -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; }; diff --git a/test/wcc/data/runww_enum_run/case.ww b/test/wcc/data/runww_enum_run/case.ww new file mode 100644 index 00000000..d88a6df3 --- /dev/null +++ b/test/wcc/data/runww_enum_run/case.ww @@ -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; };