Discovers test/wcc/data, hard-pins corpus identity (1,224 fixtures, 314 error / 12 compile / 136 run / 762 run-exit, sha256 name hash), schedules both frontend cells per fixture through os.exec with per-cell capture dirs, and emits the strict wwfix TSV result stream. integration.sh is the black-box owner of the CLI/protocol boundary.
465 lines
14 KiB
Plaintext
465 lines
14 KiB
Plaintext
package wwfixture;
|
|
|
|
import crypto.sha256;
|
|
import encoding.hex;
|
|
import fnmatch;
|
|
import hash;
|
|
import os;
|
|
import strings;
|
|
|
|
def dirbufsize: i32 = 8192;
|
|
def typemask: u32 = 61440u32;
|
|
|
|
let fixtureerror: str = "";
|
|
|
|
export fn error() str = { return fixtureerror; };
|
|
|
|
fn seterror(message: str, detail: str) bool = {
|
|
if (detail.len == 0) { fixtureerror = strings.dup(message); }
|
|
else { fixtureerror = strings.concat(message, ": ", detail); };
|
|
return false;
|
|
};
|
|
|
|
fn startswith(s: str, prefix: str) bool = {
|
|
if (s.len < prefix.len) { return false; };
|
|
let i: i32 = 0;
|
|
for (i < prefix.len) {
|
|
if (s[i] != prefix[i]) { return false; };
|
|
i += 1;
|
|
};
|
|
return true;
|
|
};
|
|
|
|
fn safeatom(s: str) bool = {
|
|
if (s.len == 0) { return false; };
|
|
let i: i32 = 0;
|
|
for (i < s.len) {
|
|
let c: u8 = s[i];
|
|
if (!((c >= 'a': u8 && c <= 'z': u8)
|
|
|| (c >= '0': u8 && c <= '9': u8) || c == '_': u8)) {
|
|
return false;
|
|
};
|
|
i += 1;
|
|
};
|
|
return true;
|
|
};
|
|
|
|
fn join(a: str, b: str) str = {
|
|
if (a.len == 0) { return strings.dup(b); };
|
|
if (a[a.len - 1] == '/': u8) { return strings.concat(a, b); };
|
|
return strings.concat(a, "/", b);
|
|
};
|
|
|
|
fn currentdir(out: *str) bool = {
|
|
let b: []u8 = alloc([], os.PATH_MAX: u64)!;
|
|
b.len = os.PATH_MAX;
|
|
let n: i64 = os.getcwd(b.ptr, b.len: u64);
|
|
if (n <= 1i64 || n > b.len: i64) { return seterror("cannot resolve current directory", ""); };
|
|
b.len = (n - 1i64): i32;
|
|
*out = strings.dup(strings.frombytes(b));
|
|
return true;
|
|
};
|
|
|
|
fn absoluteroot(root: str, out: *str) bool = {
|
|
if (root.len == 0) { return currentdir(out); };
|
|
if (root[0] == '/': u8) { *out = strings.dup(root); return true; };
|
|
let cwd: str;
|
|
if (!currentdir(&cwd)) { return false; };
|
|
*out = join(cwd, root);
|
|
return true;
|
|
};
|
|
|
|
fn readfile(path: str, out: *str) bool = {
|
|
let st: os.filestat;
|
|
match (os.stat(&st, path)) {
|
|
case void => void;
|
|
case let e: os.oserror => return seterror("cannot stat file", path);
|
|
};
|
|
if (st.sz > 2147483647u64) { return seterror("file is too large", path); };
|
|
let fd: i32 = os.open(path, os.flag.RDONLY, 0i32);
|
|
if (fd < 0) { return seterror("cannot open file", path); };
|
|
if (st.sz == 0u64) {
|
|
if (os.close(fd) != 0) { return seterror("cannot close file", path); };
|
|
let empty: str;
|
|
empty.ptr = nil: *u8; empty.len = 0;
|
|
*out = empty;
|
|
return true;
|
|
};
|
|
let b: []u8 = alloc([], st.sz)!;
|
|
b.len = st.sz: i32;
|
|
let got: i64 = match (os.readall(fd, b.ptr, st.sz)) {
|
|
case let n: i64 => yield n;
|
|
case let e: os.oserror => {
|
|
os.close(fd);
|
|
return seterror("cannot read file", path);
|
|
};
|
|
};
|
|
if (os.close(fd) != 0) { return seterror("cannot close file", path); };
|
|
if (got != st.sz: i64) { return seterror("short read", path); };
|
|
*out = strings.dup(strings.frombytes(b));
|
|
return true;
|
|
};
|
|
|
|
fn writeall(fd: i32, s: str) bool = {
|
|
let off: i32 = 0;
|
|
for (off < s.len) {
|
|
let n: i64 = os.write(fd, s.ptr + (off: u64), (s.len - off): u64);
|
|
if (n == -4i64) { continue; };
|
|
if (n <= 0i64) { return false; };
|
|
off += n: i32;
|
|
};
|
|
return true;
|
|
};
|
|
|
|
fn writefile(path: str, data: str) bool = {
|
|
let fd: i32 = os.open(path,
|
|
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384i32);
|
|
if (fd < 0) { return false; };
|
|
let ok: bool = writeall(fd, data);
|
|
if (os.close(fd) != 0) { ok = false; };
|
|
return ok;
|
|
};
|
|
|
|
fn writeharness(dir: str, message: str) bool = {
|
|
let path: str = join(dir, "harness.txt");
|
|
return writefile(path, strings.concat(message, "\n"));
|
|
};
|
|
|
|
fn direntname(buf: []u8, off: i32, reclen: i32, out: *str) bool = {
|
|
if (reclen < 20 || off < 0 || off + reclen > buf.len) { return false; };
|
|
let first: i32 = off + 19;
|
|
let last: i32 = first;
|
|
let limit: i32 = off + reclen;
|
|
for (last < limit && buf[last] != 0u8) { last += 1; };
|
|
if (last == limit) { return false; };
|
|
out.ptr = buf.ptr + (first: u64);
|
|
out.len = last - first;
|
|
return true;
|
|
};
|
|
|
|
fn filekind(path: str, out: *u32) i32 = {
|
|
let st: os.filestat;
|
|
match (os.lstat(&st, path)) {
|
|
case void => { *out = (st.mode: u32) & typemask; return 1; };
|
|
case let e: os.oserror => {
|
|
if ((e: i64) == -2i64) { return 0; };
|
|
seterror("cannot inspect path", path);
|
|
return -1;
|
|
};
|
|
};
|
|
};
|
|
|
|
fn sortnames(names: []str) void = {
|
|
let i: i32 = 1;
|
|
for (i < names.len) {
|
|
let v: str = names[i];
|
|
let j: i32 = i;
|
|
for (j > 0 && strings.compare(names[j - 1], v) > 0) {
|
|
names[j] = names[j - 1];
|
|
j -= 1;
|
|
};
|
|
names[j] = v;
|
|
i += 1;
|
|
};
|
|
};
|
|
|
|
fn parsenumber(s: str, out: *i32) bool = {
|
|
if (s.len == 0 || (s.len > 1 && s[0] == '0': u8)) { return false; };
|
|
let value: i32 = 0;
|
|
let i: i32 = 0;
|
|
for (i < s.len) {
|
|
let c: u8 = s[i];
|
|
if (c < '0': u8 || c > '9': u8) { return false; };
|
|
value = value * 10 + ((c - '0': u8): i32);
|
|
if (value > 255) { return false; };
|
|
i += 1;
|
|
};
|
|
*out = value;
|
|
return true;
|
|
};
|
|
|
|
fn parsedirective(path: str, data: str, out: *fixture) bool = {
|
|
if (data.len == 0) { return seterror("empty fixture", path); };
|
|
let end: i32 = 0;
|
|
for (end < data.len && data[end] != 10u8) { end += 1; };
|
|
if (end == data.len) { return seterror("fixture directive line is not LF terminated", path); };
|
|
let line: str;
|
|
line.ptr = data.ptr; line.len = end;
|
|
out.diagnostic = "";
|
|
out.wwdiagnostic = "";
|
|
out.exitcode = 0;
|
|
if (line == "//ww:run") { out.directive = directive.RUN; return true; };
|
|
let runexit: str = "//ww:run-exit ";
|
|
if (startswith(line, runexit)) {
|
|
let tail: str;
|
|
tail.ptr = line.ptr + (runexit.len: u64);
|
|
tail.len = line.len - runexit.len;
|
|
if (!parsenumber(tail, &out.exitcode)) {
|
|
return seterror("malformed run-exit directive", path);
|
|
};
|
|
out.directive = directive.RUNEXIT;
|
|
return true;
|
|
};
|
|
let stagedprefix: str = "//ww:error c \"";
|
|
if (startswith(line, stagedprefix)) {
|
|
let cend: i32 = stagedprefix.len;
|
|
for (cend < line.len && line[cend] != 34u8) { cend += 1; };
|
|
if (cend == stagedprefix.len || cend + 6 >= line.len
|
|
|| line[cend] != 34u8 || line[cend + 1] != ' ': u8
|
|
|| line[cend + 2] != 'w': u8 || line[cend + 3] != 'w': u8
|
|
|| line[cend + 4] != ' ': u8 || line[cend + 5] != 34u8
|
|
|| line[line.len - 1] != 34u8 || cend + 6 >= line.len - 1) {
|
|
return seterror("malformed stage-specific error directive", path);
|
|
};
|
|
let cbody: str;
|
|
cbody.ptr = line.ptr + (stagedprefix.len: u64);
|
|
cbody.len = cend - stagedprefix.len;
|
|
let wwbody: str;
|
|
wwbody.ptr = line.ptr + ((cend + 6): u64);
|
|
wwbody.len = line.len - cend - 7;
|
|
let i: i32 = 0;
|
|
for (i < cbody.len) {
|
|
if (cbody[i] == 34u8 || cbody[i] == 9u8 || cbody[i] == 13u8) {
|
|
return seterror("malformed stage-specific error directive", path);
|
|
};
|
|
i += 1;
|
|
};
|
|
i = 0;
|
|
for (i < wwbody.len) {
|
|
if (wwbody[i] == 34u8 || wwbody[i] == 9u8 || wwbody[i] == 13u8) {
|
|
return seterror("malformed stage-specific error directive", path);
|
|
};
|
|
i += 1;
|
|
};
|
|
out.directive = directive.ERROR;
|
|
out.diagnostic = strings.dup(cbody);
|
|
out.wwdiagnostic = strings.dup(wwbody);
|
|
return true;
|
|
};
|
|
let errprefix: str = "//ww:error \"";
|
|
if (startswith(line, errprefix) && line.len > errprefix.len + 1
|
|
&& line[line.len - 1] == 34u8) {
|
|
let body: str;
|
|
body.ptr = line.ptr + (errprefix.len: u64);
|
|
body.len = line.len - errprefix.len - 1;
|
|
let i: i32 = 0;
|
|
for (i < body.len) {
|
|
if (body[i] == 34u8 || body[i] == 9u8 || body[i] == 13u8) {
|
|
return seterror("malformed error directive", path);
|
|
};
|
|
i += 1;
|
|
};
|
|
out.directive = directive.ERROR;
|
|
out.diagnostic = strings.dup(body);
|
|
return true;
|
|
};
|
|
if (line == "//ww:compile") {
|
|
out.directive = directive.COMPILE;
|
|
return true;
|
|
};
|
|
return seterror("malformed fixture directive", path);
|
|
};
|
|
|
|
fn discovernames(root: str, out: *[]str) bool = {
|
|
let datadir: str = join(root, "test/wcc/data");
|
|
let fd: i32 = os.open(datadir, os.flag.RDONLY, 0i32);
|
|
if (fd < 0) { return seterror("cannot open fixture root", datadir); };
|
|
let names: []str = alloc([], 128u64)!;
|
|
let buf: []u8 = alloc([], dirbufsize: u64)!;
|
|
buf.len = dirbufsize;
|
|
for (true) {
|
|
let n: i64 = os.getdents64(fd, buf.ptr, buf.len: u64);
|
|
if (n < 0i64) { os.close(fd); return seterror("cannot enumerate fixture root", datadir); };
|
|
if (n == 0i64) { break; };
|
|
let off: i32 = 0;
|
|
for (off < n: i32) {
|
|
if (off + 18 >= n: i32) { os.close(fd); return seterror("malformed directory record", datadir); };
|
|
let reclen: i32 = (buf[off + 16]: i32) + ((buf[off + 17]: i32) * 256);
|
|
if (reclen <= 0 || off + reclen > n: i32) {
|
|
os.close(fd); return seterror("malformed directory record", datadir);
|
|
};
|
|
let name: str;
|
|
if (!direntname(buf, off, reclen, &name)) {
|
|
os.close(fd); return seterror("malformed directory record", datadir);
|
|
};
|
|
if (name != "." && name != "..") {
|
|
let dir: str = join(datadir, name);
|
|
let kind: u32 = 0u32;
|
|
let status: i32 = filekind(dir, &kind);
|
|
if (status < 0) { os.close(fd); return false; };
|
|
if (status == 1 && kind == (os.mode.LINK: u32)) {
|
|
os.close(fd); return seterror("symlink fixture directory is forbidden", dir);
|
|
};
|
|
if (status == 1 && kind == (os.mode.DIR: u32)) {
|
|
let source: str = join(dir, "case.ww");
|
|
let sourcekind: u32 = 0u32;
|
|
let sourcestatus: i32 = filekind(source, &sourcekind);
|
|
if (sourcestatus < 0) { os.close(fd); return false; };
|
|
if (sourcestatus == 1 && sourcekind == (os.mode.LINK: u32)) {
|
|
os.close(fd); return seterror("symlink fixture source is forbidden", source);
|
|
};
|
|
if (sourcestatus == 1 && sourcekind == (os.mode.REG: u32)) {
|
|
if (!safeatom(name)) { os.close(fd); return seterror("invalid fixture directory name", name); };
|
|
append(names, strings.dup(name));
|
|
};
|
|
};
|
|
};
|
|
off += reclen;
|
|
};
|
|
};
|
|
if (os.close(fd) != 0) { return seterror("cannot close fixture root", datadir); };
|
|
sortnames(names);
|
|
*out = names;
|
|
return true;
|
|
};
|
|
|
|
fn identityhash(names: []str) str = {
|
|
let state: sha256.state = sha256.sha256();
|
|
let h: *hash.hash = (&state): *hash.hash;
|
|
let nl: [1]u8 = [10u8];
|
|
let i: i32 = 0;
|
|
for (i < names.len) {
|
|
hash.write(h, strings.toutf8(names[i]));
|
|
hash.write(h, nl[0:1]);
|
|
i += 1;
|
|
};
|
|
let digest: [32]u8;
|
|
hash.sum(h, digest[0:32]);
|
|
return strings.dup(hex.encodestr(digest[0:32]));
|
|
};
|
|
|
|
fn loadcorpus(rootarg: str, out: *corpus) bool = {
|
|
fixtureerror = "";
|
|
let root: str;
|
|
if (!absoluteroot(rootarg, &root)) { return false; };
|
|
let names: []str;
|
|
if (!discovernames(root, &names)) { return false; };
|
|
let digest: str = identityhash(names);
|
|
if (names.len != corpuscount || digest != corpushash) {
|
|
return seterror("compiler corpus identity drift",
|
|
strings.concat("expected ", rundecimal(corpuscount), "/",
|
|
corpushash, ", observed ", rundecimal(names.len), "/", digest));
|
|
};
|
|
let fixtures: []fixture = alloc([], names.len: u64)!;
|
|
let errors: i32 = 0;
|
|
let compiles: i32 = 0;
|
|
let runs: i32 = 0;
|
|
let runexits: i32 = 0;
|
|
let i: i32 = 0;
|
|
for (i < names.len) {
|
|
let f: fixture;
|
|
f.name = names[i];
|
|
f.id = strings.concat("compiler.", f.name);
|
|
f.source = join(join(join(root, "test/wcc/data"), f.name), "case.ww");
|
|
let data: str;
|
|
if (!readfile(f.source, &data)) { return false; };
|
|
if (!parsedirective(f.source, data, &f)) { return false; };
|
|
if (f.directive == directive.ERROR) { errors += 1; }
|
|
else { if (f.directive == directive.COMPILE) { compiles += 1; }
|
|
else { if (f.directive == directive.RUN) { runs += 1; }
|
|
else { if (f.directive == directive.RUNEXIT) { runexits += 1; };
|
|
}; }; };
|
|
append(fixtures, f);
|
|
i += 1;
|
|
};
|
|
let derivednative: i32 = fixtures.len * 2;
|
|
if (errors != errorcount || compiles != compilecount
|
|
|| runs != runcount || runexits != runexitcount
|
|
|| derivednative != nativecount) {
|
|
return seterror("compiler stage matrix drift",
|
|
strings.concat("expected ", rundecimal(errorcount), " errors, ",
|
|
rundecimal(compilecount), " compiles, ",
|
|
rundecimal(runcount), " runs, and ",
|
|
rundecimal(runexitcount), " run-exits"));
|
|
};
|
|
out.root = root;
|
|
out.fixtures = fixtures;
|
|
return true;
|
|
};
|
|
|
|
fn cellname(f: *fixture, s: stage) str = {
|
|
return strings.concat(f.id, "/", stageword(s));
|
|
};
|
|
|
|
fn selectedby(patterns: []str, f: *fixture, s: stage) bool = {
|
|
if (patterns.len == 0) { return true; };
|
|
let full: str = cellname(f, s);
|
|
let i: i32 = 0;
|
|
for (i < patterns.len) {
|
|
if (fnmatch.fnmatch(patterns[i], f.id, fnmatch.flag.NONE)
|
|
|| fnmatch.fnmatch(patterns[i], full, fnmatch.flag.NONE)) {
|
|
return true;
|
|
};
|
|
i += 1;
|
|
};
|
|
return false;
|
|
};
|
|
|
|
fn selectidentities(c: *corpus, patterns: []str, out: *[]identity) bool = {
|
|
let ids: []identity = alloc([], nativecount: u64)!;
|
|
let ordinal: i32 = 0;
|
|
let i: i32 = 0;
|
|
for (i < c.fixtures.len) {
|
|
let si: i32 = 0;
|
|
for (si < 2) {
|
|
let s: stage = stage.C;
|
|
if (si == 1) { s = stage.WW; };
|
|
if (selectedby(patterns, &c.fixtures[i], s)) {
|
|
ordinal += 1;
|
|
let id: identity;
|
|
id.ordinal = ordinal;
|
|
id.fixtureindex = i;
|
|
id.stage = s;
|
|
append(ids, id);
|
|
};
|
|
si += 1;
|
|
};
|
|
i += 1;
|
|
};
|
|
*out = ids;
|
|
return true;
|
|
};
|
|
|
|
fn isdot(name: str) bool = { return name == "." || name == ".."; };
|
|
|
|
// Callers restrict this to directories minted beneath one temp.dir result;
|
|
// lstat plus the opened-directory inode check keeps cleanup from following a
|
|
// replaced path outside that ownership boundary.
|
|
fn removeall(path: str) bool = {
|
|
let st: os.filestat;
|
|
match (os.lstat(&st, path)) {
|
|
case void => void;
|
|
case let e: os.oserror => return false;
|
|
};
|
|
let kind: u32 = (st.mode: u32) & typemask;
|
|
if (kind != (os.mode.DIR: u32)) { return os.remove(path) == 0; };
|
|
let fd: i32 = os.open(path, os.flag.RDONLY, 0i32);
|
|
if (fd < 0) { return false; };
|
|
let opened: os.filestat;
|
|
match (os.fstat(&opened, fd)) {
|
|
case void => void;
|
|
case let e: os.oserror => { os.close(fd); return false; };
|
|
};
|
|
if (((opened.mode: u32) & typemask) != (os.mode.DIR: u32)
|
|
|| opened.inode != st.inode) { os.close(fd); return false; };
|
|
let ok: bool = true;
|
|
let buf: []u8 = alloc([], dirbufsize: u64)!;
|
|
buf.len = dirbufsize;
|
|
let n: i64 = os.getdents64(fd, buf.ptr, buf.len: u64);
|
|
for (n > 0i64) {
|
|
let off: i32 = 0;
|
|
for (off < n: i32) {
|
|
let reclen: i32 = (buf[off + 16]: i32) + ((buf[off + 17]: i32) * 256);
|
|
if (reclen < 20 || off + reclen > n: i32) { ok = false; break; };
|
|
let name: str;
|
|
if (!direntname(buf, off, reclen, &name)) { ok = false; break; };
|
|
if (!isdot(name) && !removeall(join(path, name))) { ok = false; };
|
|
off += reclen;
|
|
};
|
|
n = os.getdents64(fd, buf.ptr, buf.len: u64);
|
|
};
|
|
if (n < 0i64 || os.close(fd) != 0) { ok = false; };
|
|
if (os.rmdir(path) != 0) { ok = false; };
|
|
return ok;
|
|
};
|