test: port 989_lib_byteid to ww; birth test/testenv + test/byteid
test/testenv (package testenv) is the shared plumbing for ww-native test drivers — subprocess launch with captured output, file IO, string search, byte-sorted directory listing, scratch ownership — lifted from the proven package_test.ww idiom; a utility, not a framework. test/byteid/libbyteid_test.ww carries every assertion of the retired C carrier: the 44-entry roster (fixtures + sentinel-guarded import probes + the zero-dep root-only build), both-stage sep builds with the resolved-unit proof, per-package .s concat compare with the empty-concat guard, the ID/DIVERGE/WWREJECT pin discipline, and the lib/ corpus completeness scan (negative-verified against a planted un-enrolled module). Concat order strengthened from shell-glob to explicit byte-lexicographic. The roster fills by cursor-driven field writes because append() rejects struct-call-result sources (#34). Byteid carriers 11 -> 10; docs counts move; test-byteid runs the ww test via the wwtest/ pattern with WW_TEST_REPO.
This commit is contained in:
434
test/byteid/libbyteid_test.ww
Normal file
434
test/byteid/libbyteid_test.ww
Normal file
@@ -0,0 +1,434 @@
|
||||
package libbyteid_test;
|
||||
|
||||
// cstage-vs-wwstage byte-identity gate over the whole lib/ surface, and
|
||||
// the ONE owner of wwstage-DRIVER-leg byte identity. Port of the
|
||||
// retired native carrier test/wcc/989_lib_byteid.c; every assertion
|
||||
// preserved, .s concat order strengthened from shell-glob to explicit
|
||||
// byte-lexicographic.
|
||||
//
|
||||
// The bootstrap gates (991-995) byte-id only the modules the selfhost
|
||||
// tools import. Every other lib/ module compiles through the CSTAGE
|
||||
// driver alone elsewhere, so a cs≠ww divergence there ships gate-green
|
||||
// (regex.finish did, task #21 FC0). This gate closes the class: each
|
||||
// lib test fixture is sep-built through BOTH driver stages and the
|
||||
// per-package asm compared.
|
||||
//
|
||||
// #94 sep layout: under sep EACH package compiles to its OWN
|
||||
// <stem>.sepwork/<pkg>.s — the lib body lands in <pkg>.s, NOT in
|
||||
// __root.s. The byte-id concats EVERY per-package .s (lib body +
|
||||
// lib/test's auto-bundled runner), never just __root.s: comparing the
|
||||
// root alone would byte-id the harness wrapper while the engine ships
|
||||
// uncovered.
|
||||
//
|
||||
// Three pinned outcomes, loud over blind (task #21 ruling):
|
||||
// ID — byte-identical across all per-package .s, zero tolerance.
|
||||
// DIVERGE — known cs≠ww divergence (task #59, cite). Both stages
|
||||
// must still compile and the asm must still DIFFER; a fix
|
||||
// fails the entry demanding graduation to ID.
|
||||
// WWREJECT — cstage compiles, wwstage errors (task #59, cite);
|
||||
// graduation pinned the same way.
|
||||
//
|
||||
// Fixtureless modules are covered by import-probe stubs: `import`
|
||||
// drags the whole module into its own sep package. The driver silently
|
||||
// skips an unresolvable import, so each probe carries a sentinel that
|
||||
// must appear in a per-package .unit.ww, and the completeness scan
|
||||
// fails loudly on any lib/ dir not enrolled here.
|
||||
|
||||
import os;
|
||||
import os.exec;
|
||||
import strings;
|
||||
import testenv;
|
||||
import time;
|
||||
|
||||
def MID: i32 = 0;
|
||||
def MDIVERGE: i32 = 1;
|
||||
def MWWREJECT: i32 = 2;
|
||||
def NENTEXPECT: i32 = 44;
|
||||
|
||||
type ent = struct {
|
||||
fixture: str, // repo-relative .ww; "" -> probe entry
|
||||
probe: str, // inline import-probe source
|
||||
inc: str, // extra -I dir beyond dirname(fixture)
|
||||
mode: i32,
|
||||
cite: str, // task #59 entry for non-ID modes
|
||||
sentinel: str, // probe-only: proof the module body landed
|
||||
moddir: str, // probe-only: completeness key
|
||||
};
|
||||
|
||||
// #34: append() rejects struct-call-result sources, so the roster is a
|
||||
// preallocated slice filled by per-row field writes behind a running
|
||||
// cursor; the cursor total re-proves the NENTEXPECT count.
|
||||
fn zeroent(e: *ent) void = {
|
||||
e.fixture = ""; e.probe = ""; e.inc = ""; e.mode = MID;
|
||||
e.cite = ""; e.sentinel = ""; e.moddir = "";
|
||||
};
|
||||
|
||||
fn fx(es: []ent, k: *i32, f: str) void = {
|
||||
let i: i32 = *k;
|
||||
es[i].fixture = f;
|
||||
*k = i + 1;
|
||||
};
|
||||
|
||||
fn fxi(es: []ent, k: *i32, f: str, inc: str) void = {
|
||||
let i: i32 = *k;
|
||||
es[i].fixture = f;
|
||||
es[i].inc = inc;
|
||||
*k = i + 1;
|
||||
};
|
||||
|
||||
fn pr(es: []ent, k: *i32, p: str, sentinel: str, moddir: str) void = {
|
||||
let i: i32 = *k;
|
||||
es[i].probe = p;
|
||||
es[i].sentinel = sentinel;
|
||||
es[i].moddir = moddir;
|
||||
*k = i + 1;
|
||||
};
|
||||
|
||||
fn pri(es: []ent, k: *i32, p: str, inc: str, sentinel: str,
|
||||
moddir: str) void = {
|
||||
let i: i32 = *k;
|
||||
es[i].probe = p;
|
||||
es[i].inc = inc;
|
||||
es[i].sentinel = sentinel;
|
||||
es[i].moddir = moddir;
|
||||
*k = i + 1;
|
||||
};
|
||||
|
||||
// The 44-unit roster. Graduation history lives in git (the retired C
|
||||
// carrier's table comments); cites are kept only where a non-ID pin
|
||||
// would need them.
|
||||
fn corpus() []ent = {
|
||||
let es: []ent = alloc([], NENTEXPECT: u64)!;
|
||||
es.len = NENTEXPECT;
|
||||
let z: i32 = 0;
|
||||
for (z < es.len) { zeroent(&es[z]); z += 1; };
|
||||
let n: i32 = 0;
|
||||
let k: *i32 = &n;
|
||||
fx(es, k, "lib/bytes/bytestest.ww");
|
||||
fx(es, k, "lib/dirs/dirstest.ww");
|
||||
fx(es, k, "lib/encoding/base32/base32_test.ww");
|
||||
fx(es, k, "lib/encoding/hex/hextest.ww");
|
||||
fx(es, k, "lib/encoding/utf8/utf8test.ww");
|
||||
fx(es, k, "lib/getopt/getopttest.ww");
|
||||
fx(es, k, "lib/hash/adler32/adler32_test.ww");
|
||||
fx(es, k, "lib/hash/crc16/crc16_test.ww");
|
||||
fx(es, k, "lib/hash/crc32/crc32_test.ww");
|
||||
fx(es, k, "lib/hash/crc64/crc64_test.ww");
|
||||
fx(es, k, "lib/hash/siphash/siphash_test.ww");
|
||||
fx(es, k, "lib/math/checked/checked_test.ww");
|
||||
fx(es, k, "lib/math/random/random_test.ww");
|
||||
fx(es, k, "lib/memio/memiotest.ww");
|
||||
fx(es, k, "lib/os/ostest.ww");
|
||||
pr(es, k, "package main;\nimport os.exec;\nfn main() i32 = { return 0; };\n",
|
||||
"package exec;", "lib/os/exec");
|
||||
fx(es, k, "lib/regex/regex_test.ww");
|
||||
fx(es, k, "lib/strconv/test/ftostest.ww");
|
||||
fx(es, k, "lib/strconv/test/stoftest.ww");
|
||||
fx(es, k, "lib/strconv/test/inttest.ww");
|
||||
fx(es, k, "lib/strings/stringstest.ww");
|
||||
fx(es, k, "lib/temp/temptest.ww");
|
||||
fx(es, k, "lib/time/timetest.ww");
|
||||
fx(es, k, "lib/bufio/bufiotest.ww");
|
||||
fx(es, k, "lib/fmt/fmttest.ww");
|
||||
pr(es, k, "package main;\nimport sort;\nfn main() i32 = { return 0; };\n",
|
||||
"package sort;", "lib/sort");
|
||||
pr(es, k, "package main;\nimport path;\nfn main() i32 = { return 0; };\n",
|
||||
"package path;", "lib/path");
|
||||
pr(es, k, "package main;\nimport endian;\nfn main() i32 = { return 0; };\n",
|
||||
"package endian;", "lib/endian");
|
||||
pr(es, k, "package main;\nimport net;\nfn main() i32 = { return 0; };\n",
|
||||
"package net;", "lib/net");
|
||||
pr(es, k, "package main;\nimport hash;\nfn main() i32 = { return 0; };\n",
|
||||
"package hash;", "lib/hash");
|
||||
// fnv lives off the driver's default root (lib/hash/fnv)
|
||||
pri(es, k, "package main;\nimport fnv;\nfn main() i32 = { return 0; };\n",
|
||||
"lib/hash/fnv", "package fnv;", "lib/hash/fnv");
|
||||
// sentinel is a fn, not `package math;` — lib/math is also package
|
||||
// math, so a silent fallback there would still match
|
||||
pr(es, k, "package main;\nimport crypto.math;\nfn main() i32 = { return 0; };\n",
|
||||
"fn rotl32", "lib/crypto/math");
|
||||
pr(es, k, "package main;\nimport c.libc;\nfn main() i32 = { return 0; };\n",
|
||||
"package libc;", "lib/c/libc");
|
||||
// root-only, ZERO-dep build -S: the one driver-leg edge every
|
||||
// import probe misses (dep-count-0 unit composition). Folded in
|
||||
// from the retired 815/940/951 driver-parity carriers. No
|
||||
// sentinel: there is no dep unit to prove.
|
||||
pr(es, k, "package main;\nfn main() i32 = { return 0; };\n", "", "");
|
||||
fx(es, k, "lib/ascii/asciitest.ww");
|
||||
fx(es, k, "lib/encoding/base64/base64_test.ww");
|
||||
fx(es, k, "lib/errors/errnotest.ww");
|
||||
fx(es, k, "lib/log/logtest.ww");
|
||||
fx(es, k, "lib/os/stattest.ww");
|
||||
// toktest/asttest resolve `import syntax` via -I lib/ww, cf 905
|
||||
fxi(es, k, "lib/ww/syntax/toktest.ww", "lib/ww");
|
||||
fxi(es, k, "lib/ww/syntax/asttest.ww", "lib/ww");
|
||||
fx(es, k, "lib/crypto/sha256/sha256_test.ww");
|
||||
fx(es, k, "lib/fnmatch/fnmatchtest.ww");
|
||||
fx(es, k, "lib/shlex/shlextest.ww");
|
||||
assert(n == NENTEXPECT);
|
||||
return es;
|
||||
};
|
||||
|
||||
// every lib/ dir holding .ww source must be accounted for — enrolled in
|
||||
// the corpus (fixture dirname or probe moddir) or on this covered list;
|
||||
// otherwise a new module ships with zero byte-id coverage.
|
||||
fn covered() []str = {
|
||||
let cs: []str = [];
|
||||
// selfhost-embedded: byte-id'd by the 990-997 gates
|
||||
append(cs, "lib/io");
|
||||
append(cs, "lib/math");
|
||||
append(cs, "lib/rt");
|
||||
append(cs, "lib/types");
|
||||
// module body dragged into the lib/strconv/test fixtures
|
||||
append(cs, "lib/strconv");
|
||||
// #17: the @test runner is AUTO-BUNDLED into every -T sep build,
|
||||
// so every @test fixture byte-ids it cs/ww; 911_attest_record
|
||||
// also compares it directly. It has no _test.ww of its own.
|
||||
append(cs, "lib/test");
|
||||
return cs;
|
||||
};
|
||||
|
||||
fn fail(label: str, why: str) void = {
|
||||
let m: str = strings.concat("lib_byteid FAIL: ", label, " -- ", why, "\n");
|
||||
os.write(2, m.ptr, m.len: u64);
|
||||
assert(false);
|
||||
};
|
||||
|
||||
fn dirnameof(p: str) str = {
|
||||
let last: i32 = -1;
|
||||
let i: i32 = 0;
|
||||
for (i < p.len) {
|
||||
if (p[i] == '/') { last = i; };
|
||||
i += 1;
|
||||
};
|
||||
assert(last > 0);
|
||||
return strings.sub(p, 0, last);
|
||||
};
|
||||
|
||||
fn basenameof(p: str) str = {
|
||||
let last: i32 = -1;
|
||||
let i: i32 = 0;
|
||||
for (i < p.len) {
|
||||
if (p[i] == '/') { last = i; };
|
||||
i += 1;
|
||||
};
|
||||
return strings.sub(p, last + 1, p.len);
|
||||
};
|
||||
|
||||
fn stripext(base: str) str = {
|
||||
assert(strings.hassuffix(base, ".ww"));
|
||||
return strings.sub(base, 0, base.len - 3);
|
||||
};
|
||||
|
||||
// Concatenate every per-package .s under `sepdir` in byte-sorted order;
|
||||
// "" when the directory is missing or holds no .s.
|
||||
fn catasm(sepdir: str) str = {
|
||||
if (!testenv.isdir(sepdir)) { return ""; };
|
||||
let names: []str = testenv.listdir(sepdir);
|
||||
let out: str = "";
|
||||
let i: i32 = 0;
|
||||
for (i < names.len) {
|
||||
if (strings.hassuffix(names[i], ".s")) {
|
||||
let body: str = testenv.readfile(
|
||||
strings.concat(sepdir, "/", names[i]));
|
||||
out = strings.concat(out, body);
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
return out;
|
||||
};
|
||||
|
||||
fn buildstage(td: str, name: str, drv: str, sub: str, incs: []str,
|
||||
stem: str, base: str) bool = {
|
||||
let av: []str = [];
|
||||
append(av, drv);
|
||||
append(av, sub);
|
||||
append(av, "-S");
|
||||
let i: i32 = 0;
|
||||
for (i < incs.len) {
|
||||
append(av, "-I");
|
||||
append(av, incs[i]);
|
||||
i += 1;
|
||||
};
|
||||
append(av, "-o");
|
||||
append(av, stem);
|
||||
append(av, base);
|
||||
let co: testenv.commandout;
|
||||
testenv.runcommand(td, td, name, av,
|
||||
(180i64 * (time.second: i64)): time.duration, &co);
|
||||
return co.termination == exec.termination.EXIT && co.code == 0;
|
||||
};
|
||||
|
||||
fn checkone(e: *ent) void = {
|
||||
let td: str = testenv.fresh();
|
||||
let label: str = "import-probe";
|
||||
if (e.fixture.len != 0) { label = e.fixture; }
|
||||
else { if (e.moddir.len != 0) { label = e.moddir; }; };
|
||||
|
||||
let base: str = "probe.ww";
|
||||
if (e.fixture.len != 0) {
|
||||
base = basenameof(e.fixture);
|
||||
testenv.writefile(strings.concat(td, "/", base),
|
||||
testenv.readfile(strings.concat(testenv.repo(), "/",
|
||||
e.fixture)));
|
||||
} else {
|
||||
testenv.writefile(strings.concat(td, "/", base), e.probe);
|
||||
};
|
||||
|
||||
// dirname(fixture) leads the search path so bare same-module
|
||||
// imports resolve as they do under the in-tree driver run.
|
||||
let incs: []str = [];
|
||||
if (e.fixture.len != 0) {
|
||||
append(incs, strings.concat(testenv.repo(), "/",
|
||||
dirnameof(e.fixture)));
|
||||
};
|
||||
if (e.inc.len != 0) {
|
||||
append(incs, strings.concat(testenv.repo(), "/", e.inc));
|
||||
};
|
||||
|
||||
// @test fixtures are main-less, so `test -S` carries -T
|
||||
// (synthesizes the entry + auto-bundles lib/test); import probes
|
||||
// carry their own fn main(), which -T loud-rejects (910), so they
|
||||
// `build -S`.
|
||||
let sub: str = "test";
|
||||
if (e.fixture.len == 0) { sub = "build"; };
|
||||
let stem: str = stripext(base);
|
||||
let stemc: str = strings.concat(td, "/c_", stem);
|
||||
let stemw: str = strings.concat(td, "/w_", stem);
|
||||
|
||||
let ce: bool = buildstage(td, "cstage", testenv.driver("ww"), sub,
|
||||
incs, stemc, base);
|
||||
let we: bool = buildstage(td, "wwstage", testenv.driver("ww_ww"), sub,
|
||||
incs, stemw, base);
|
||||
|
||||
// the cstage build resolves the units; its __root.unit.ww proves
|
||||
// resolution ran. WWREJECT still requires cstage to compile.
|
||||
if (!ce || !testenv.exists(strings.concat(stemc,
|
||||
".sepwork/__root.unit.ww"))) {
|
||||
fail(label, "cstage produced no resolved unit");
|
||||
};
|
||||
|
||||
// probe coverage: the driver SILENTLY SKIPS an unresolvable
|
||||
// import, shrinking the probe to an empty main that byte-ids
|
||||
// trivially. The sentinel must appear in some per-package
|
||||
// .unit.ww to prove the module landed.
|
||||
if (e.sentinel.len != 0) {
|
||||
let sepc: str = strings.concat(stemc, ".sepwork");
|
||||
let names: []str = testenv.listdir(sepc);
|
||||
let found: bool = false;
|
||||
let i: i32 = 0;
|
||||
for (i < names.len) {
|
||||
if (strings.hassuffix(names[i], ".unit.ww")) {
|
||||
if (testenv.has(testenv.readfile(
|
||||
strings.concat(sepc, "/", names[i])),
|
||||
e.sentinel)) {
|
||||
found = true;
|
||||
};
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
if (!found) {
|
||||
fail(label, "import silently dropped; probe covers nothing");
|
||||
};
|
||||
};
|
||||
|
||||
if (e.mode == MWWREJECT) {
|
||||
if (we) {
|
||||
fail(label, strings.concat("wwstage now compiles this; ",
|
||||
"graduate the ", e.cite, " pin to ID or DIVERGE"));
|
||||
};
|
||||
testenv.clean(td);
|
||||
return;
|
||||
};
|
||||
if (!we) { fail(label, "wwstage rejected"); };
|
||||
|
||||
let cs: str = catasm(strings.concat(stemc, ".sepwork"));
|
||||
let ws: str = catasm(strings.concat(stemw, ".sepwork"));
|
||||
// two empty concats compare equal — that green covers nothing
|
||||
if (cs.len == 0 || ws.len == 0) {
|
||||
fail(label, "empty .s concat");
|
||||
};
|
||||
let idsame: bool = testenv.same(cs, ws);
|
||||
if (e.mode == MID && !idsame) {
|
||||
fail(label, "cs vs ww asm differs (byte-id broken)");
|
||||
};
|
||||
if (e.mode == MDIVERGE && idsame) {
|
||||
fail(label, strings.concat("now byte-identical; graduate the ",
|
||||
e.cite, " pin to ID"));
|
||||
};
|
||||
testenv.clean(td);
|
||||
};
|
||||
|
||||
fn accounted(dir: str, es: []ent, cov: []str) bool = {
|
||||
let i: i32 = 0;
|
||||
for (i < es.len) {
|
||||
if (es[i].fixture.len != 0) {
|
||||
if (testenv.same(dir, dirnameof(es[i].fixture))) {
|
||||
return true;
|
||||
};
|
||||
} else { if (es[i].moddir.len != 0) {
|
||||
if (testenv.same(dir, es[i].moddir)) { return true; };
|
||||
}; };
|
||||
i += 1;
|
||||
};
|
||||
i = 0;
|
||||
for (i < cov.len) {
|
||||
if (testenv.same(dir, cov[i])) { return true; };
|
||||
i += 1;
|
||||
};
|
||||
return false;
|
||||
};
|
||||
|
||||
// Directories under `dir` (repo-relative `rel`) holding .ww source,
|
||||
// excluding generated .sepwork trees.
|
||||
fn wwdirs(dir: str, rel: str) []str = {
|
||||
let out: []str = [];
|
||||
let names: []str = testenv.listdir(dir);
|
||||
let hasww: bool = false;
|
||||
let i: i32 = 0;
|
||||
for (i < names.len) {
|
||||
let p: str = strings.concat(dir, "/", names[i]);
|
||||
if (testenv.isdir(p)) {
|
||||
if (!strings.hassuffix(names[i], ".sepwork")) {
|
||||
let sub: []str = wwdirs(p,
|
||||
strings.concat(rel, "/", names[i]));
|
||||
let j: i32 = 0;
|
||||
for (j < sub.len) {
|
||||
append(out, sub[j]);
|
||||
j += 1;
|
||||
};
|
||||
};
|
||||
} else {
|
||||
if (strings.hassuffix(names[i], ".ww")) { hasww = true; };
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
if (hasww) { append(out, rel); };
|
||||
return out;
|
||||
};
|
||||
|
||||
@test fn corpuscomplete() void = {
|
||||
let es: []ent = corpus();
|
||||
assert(es.len == NENTEXPECT);
|
||||
let dirs: []str = wwdirs(strings.concat(testenv.repo(), "/lib"), "lib");
|
||||
// an empty scan means the walk itself broke — never pass on that
|
||||
assert(dirs.len > 0);
|
||||
let cov: []str = covered();
|
||||
let i: i32 = 0;
|
||||
for (i < dirs.len) {
|
||||
if (!accounted(dirs[i], es, cov)) {
|
||||
fail(dirs[i], "un-enrolled lib module; add it to the corpus");
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
|
||||
@test fn roster() void = {
|
||||
let es: []ent = corpus();
|
||||
assert(es.len == NENTEXPECT);
|
||||
let i: i32 = 0;
|
||||
for (i < es.len) {
|
||||
checkone(&es[i]);
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user