diff --git a/cmd/wwfixture/wwfixture.ww b/cmd/wwfixture/wwfixture.ww new file mode 100644 index 00000000..ed427b11 --- /dev/null +++ b/cmd/wwfixture/wwfixture.ww @@ -0,0 +1,8 @@ +package main; + +import os; +import wwfixture; + +export fn main() int = { + return wwfixture.command(os.args()); +}; diff --git a/internal/wwfixture/command.ww b/internal/wwfixture/command.ww new file mode 100644 index 00000000..b3463c6a --- /dev/null +++ b/internal/wwfixture/command.ww @@ -0,0 +1,150 @@ +package wwfixture; + +import fmt; +import os; +import strings; + +type options = struct { + root: str, + patterns: []str, + jobs: i32, + list: bool, + keep: bool, + timeoutms: i64, + input: str, +}; + +fn usage() void = { + fmt.errorln("usage: wwfixture [-root path] [-run glob]... [-list] [-j N] [-keep] [-timeout-ms N]"); + fmt.errorln(" wwfixture validate [-root path] [-run glob]... STREAM"); +}; + +fn positive(s: str, limit: i64, out: *i64) bool = { + if (s.len == 0) { return false; }; + let n: i64 = 0i64; + let i: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (c < '0': u8 || c > '9': u8) { return false; }; + let digit: i64 = (c - '0': u8): i64; + if (n > (limit - digit) / 10i64) { return false; }; + n = n * 10i64 + digit; + i += 1; + }; + if (n == 0i64) { return false; }; + *out = n; + return true; +}; + +fn initoptions(out: *options) void = { + out.root = ""; + let patterns: []str = alloc([], 16u64)!; + out.patterns = patterns; + out.jobs = 4; + out.list = false; + out.keep = false; + out.timeoutms = 30000i64; + out.input = ""; +}; + +fn parseoptions(args: []str, start: i32, validate: bool, + out: *options) bool = { + initoptions(out); + let i: i32 = start; + for (i < args.len) { + let a: str = args[i]; + if (a == "-root" || a == "-run") { + if (i + 1 >= args.len) { return false; }; + if (a == "-root") { + if (out.root.len != 0) { return false; }; + out.root = args[i + 1]; + } else { append(out.patterns, args[i + 1]); }; + i += 2; + continue; + }; + if (!validate && a == "-list") { + if (out.list) { return false; }; + out.list = true; i += 1; continue; + }; + if (!validate && a == "-keep") { + if (out.keep) { return false; }; + out.keep = true; i += 1; continue; + }; + if (!validate && (a == "-j" || a == "-timeout-ms")) { + if (i + 1 >= args.len) { return false; }; + let n: i64 = 0i64; + let limit: i64 = 64i64; + if (a == "-timeout-ms") { limit = 3600000i64; }; + if (!positive(args[i + 1], limit, &n)) { return false; }; + if (a == "-j") { out.jobs = n: i32; } + else { out.timeoutms = n; }; + i += 2; + continue; + }; + if (validate && a.len > 0 && a[0] != '-': u8 && out.input.len == 0) { + out.input = a; i += 1; continue; + }; + return false; + }; + if (validate && out.input.len == 0) { return false; }; + return true; +}; + +fn listfixtures(c: *corpus, ids: []identity) bool = { + let i: i32 = 0; + for (i < ids.len) { + let f: *fixture = &c.fixtures[ids[i].fixtureindex]; + let line: str = strings.concat(f.id, "\t", stageword(ids[i].stage), "\n"); + if (!writeall(os.STDOUT_FILENO, line)) { return false; }; + i += 1; + }; + return true; +}; + +fn validatecommand(opts: *options) int = { + let c: corpus; + if (!loadcorpus(opts.root, &c)) { + fmt.errorln("wwfixture validate: ", error()); + return 1; + }; + let ids: []identity; + selectidentities(&c, opts.patterns, &ids); + let data: str; + if (!readfile(opts.input, &data)) { + fmt.errorln("wwfixture validate: ", error()); + return 1; + }; + if (!validatestream(data, &c, ids)) { + fmt.errorln("wwfixture validate: ", protocolerror()); + return 1; + }; + return 0; +}; + +export fn command(args: []str) int = { + if (args.len < 1) { usage(); return 2; }; + let validate: bool = args.len > 1 && args[1] == "validate"; + let start: i32 = 1; + if (validate) { start = 2; }; + let opts: options; + if (!parseoptions(args, start, validate, &opts)) { + usage(); + return 2; + }; + if (validate) { return validatecommand(&opts); }; + let c: corpus; + if (!loadcorpus(opts.root, &c)) { + fmt.errorln("wwfixture: ", error()); + return 1; + }; + let ids: []identity; + selectidentities(&c, opts.patterns, &ids); + if (opts.list) { + if (!listfixtures(&c, ids)) { + fmt.errorln("wwfixture: result output failed"); + return 1; + }; + return 0; + }; + return runfixtures(&opts, &c, ids); +}; diff --git a/internal/wwfixture/corpus.ww b/internal/wwfixture/corpus.ww new file mode 100644 index 00000000..583aedbe --- /dev/null +++ b/internal/wwfixture/corpus.ww @@ -0,0 +1,464 @@ +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; +}; diff --git a/internal/wwfixture/protocol.ww b/internal/wwfixture/protocol.ww new file mode 100644 index 00000000..7f0bd1c7 --- /dev/null +++ b/internal/wwfixture/protocol.ww @@ -0,0 +1,524 @@ +package wwfixture; + +import os; +import strings; + +let protocolerr: str = ""; + +export fn protocolerror() str = { return protocolerr; }; + +fn protocolfail(message: str) bool = { + protocolerr = strings.dup(message); + return false; +}; + +fn protocolput(b: *[]u8, s: str) void = { + let i: i32 = 0; + for (i < s.len) { + append(*b, s[i]); + i += 1; + }; +}; + +fn protocolfield(b: *[]u8, s: str) void = { + append(*b, '\t': u8); + protocolput(b, s); +}; + +fn protocolu64(b: *[]u8, value: u64) void = { + let digits: [20]u8; + let n: i32 = 0; + let v: u64 = value; + if (v == 0u64) { + append(*b, '0': u8); + return; + }; + for (v != 0u64) { + digits[n] = '0': u8 + (v % 10u64): u8; + n += 1; + v /= 10u64; + }; + for (n > 0) { + n -= 1; + append(*b, digits[n]); + }; +}; + +fn protocoli64(b: *[]u8, value: i64) void = { + if (value < 0i64) { + append(*b, '-': u8); + protocolu64(b, (-value): u64); + return; + }; + protocolu64(b, value: u64); +}; + +fn protocolintfield(b: *[]u8, value: i64) void = { + append(*b, '\t': u8); + protocoli64(b, value); +}; + +fn protocolnewline(b: *[]u8) void = { append(*b, '\n': u8); }; + +fn protocolwriteall(fd: i32, b: []u8) bool = { + let off: i32 = 0; + for (off < b.len) { + let n: i64 = os.write(fd, b.ptr + (off: u64), + (b.len - off): u64); + if (n == -4i64) { continue; }; + if (n <= 0i64) { return protocolfail("cannot write terminal stream"); }; + off += n: i32; + }; + return true; +}; + +fn safeprotocolfield(s: str) bool = { + if (s.len == 0) { return false; }; + let i: i32 = 0; + for (i < s.len) { + if (s[i] < 32u8 || s[i] == 127u8) { return false; }; + i += 1; + }; + return true; +}; + +fn validrunid(s: str) bool = { + if (s.len != 16) { return false; }; + let i: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (!((c >= '0': u8 && c <= '9': u8) + || (c >= 'a': u8 && c <= 'f': u8))) { return false; }; + i += 1; + }; + return true; +}; + +fn validstage(s: stage) bool = { return s == stage.C || s == stage.WW; }; + +fn validphase(p: phase) bool = { + return p == phase.COMPILE || p == phase.BUILD || p == phase.RUN; +}; + +fn validverdict(v: verdict) bool = { + return v == verdict.PASS || v == verdict.FAIL || v == verdict.ERROR; +}; + +fn validtermination(t: termination) bool = { + return t == termination.EXIT || t == termination.SIGNAL + || t == termination.TIMEOUT || t == termination.SETUP; +}; + +fn validexpected(c: *corpus, ids: []identity) bool = { + if (ids.len > nativecount) { + return protocolfail("selected count exceeds native matrix"); + }; + let i: i32 = 0; + for (i < ids.len) { + let id: *identity = &ids[i]; + if (id.ordinal != i + 1) { + return protocolfail("expected ordinals are not consecutive"); + }; + if (id.fixtureindex < 0 || id.fixtureindex >= c.fixtures.len) { + return protocolfail("expected fixture index is invalid"); + }; + if (!validstage(id.stage)) { + return protocolfail("expected stage is invalid"); + }; + let j: i32 = 0; + for (j < i) { + if (ids[j].fixtureindex == id.fixtureindex + && ids[j].stage == id.stage) { + return protocolfail("duplicate expected identity"); + }; + j += 1; + }; + i += 1; + }; + return true; +}; + +fn validcapture(r: *cellresult) bool = { + if (!safeprotocolfield(r.capture)) { + return protocolfail("unsafe or empty capture field"); + }; + if (r.verdict != verdict.PASS && r.capture == "-") { + return protocolfail("non-pass result has no retained capture"); + }; + return true; +}; + +fn validterminationcode(r: *cellresult) bool = { + if (r.termination == termination.EXIT) { + if (r.code < 0 || r.code > 255) { + return protocolfail("exit code is out of range"); + }; + return true; + }; + if (r.termination == termination.SIGNAL) { + if (r.code < 1 || r.code > 64) { + return protocolfail("signal number is out of range"); + }; + return true; + }; + if (r.termination == termination.TIMEOUT) { + if (r.code != 0) { return protocolfail("timeout code is not zero"); }; + return true; + }; + if (r.code >= 0 || r.code < -4095) { + return protocolfail("setup code is not a negative errno"); + }; + return true; +}; + +fn validresult(f: *fixture, r: *cellresult) bool = { + if (!safeprotocolfield(r.id)) { + return protocolfail("unsafe or empty result identity"); + }; + if (!validstage(r.stage) || !validphase(r.phase) + || !validverdict(r.verdict) || !validtermination(r.termination)) { + return protocolfail("invalid result enum"); + }; + if (r.durationns < 0i64) { return protocolfail("negative duration"); }; + if (!validcapture(r) || !validterminationcode(r)) { return false; }; + if (r.verdict == verdict.PASS && r.termination != termination.EXIT) { + return protocolfail("pass is not a normal exit"); + }; + if (r.termination == termination.SETUP && r.verdict != verdict.ERROR) { + return protocolfail("setup termination is not an error"); + }; + if (r.verdict == verdict.FAIL && r.termination == termination.SETUP) { + return protocolfail("fail result has setup termination"); + }; + if (f.directive == directive.ERROR) { + if (r.phase != phase.COMPILE) { + return protocolfail("error fixture has non-compile phase"); + }; + if (r.verdict == verdict.PASS + && (r.termination != termination.EXIT || r.code == 0)) { + return protocolfail("compile-fail pass is not a normal nonzero exit"); + }; + return true; + }; + if (f.directive == directive.COMPILE) { + if (r.phase != phase.COMPILE) { + return protocolfail("compile fixture has non-compile phase"); + }; + if (r.verdict == verdict.PASS + && (r.termination != termination.EXIT || r.code != 0)) { + return protocolfail("compile pass is not a normal exit zero"); + }; + if (r.verdict != verdict.PASS && r.termination == termination.EXIT + && r.code == 0) { + return protocolfail("non-pass compile is a normal exit zero"); + }; + return true; + }; + if (f.directive != directive.RUN && f.directive != directive.RUNEXIT) { + return protocolfail("invalid fixture directive"); + }; + if (r.phase == phase.COMPILE) { + return protocolfail("positive fixture has compile phase"); + }; + if (r.phase == phase.BUILD) { + if (r.verdict == verdict.PASS) { + return protocolfail("positive build cannot be a terminal pass"); + }; + if (r.termination == termination.EXIT && r.code == 0) { + return protocolfail("positive build stopped after normal exit zero"); + }; + return true; + }; + let expected: i32 = 0; + if (f.directive == directive.RUNEXIT) { expected = f.exitcode; }; + if (r.verdict == verdict.PASS + && (r.termination != termination.EXIT || r.code != expected)) { + return protocolfail("runtime pass does not match expected exit"); + }; + if (r.verdict == verdict.FAIL && r.termination == termination.EXIT + && r.code == expected) { + return protocolfail("runtime fail matches expected exit"); + }; + return true; +}; + +fn validrow(c: *corpus, expected: *identity, r: *cellresult) bool = { + if (r.ordinal != expected.ordinal) { + return protocolfail("missing, duplicate, or out-of-order ordinal"); + }; + let f: *fixture = &c.fixtures[expected.fixtureindex]; + if (r.id != f.id || r.stage != expected.stage) { + return protocolfail("unexpected result identity"); + }; + return validresult(f, r); +}; + +fn putheader(b: *[]u8, runid: str, selected: i32) void = { + protocolput(b, "wwfix"); + protocolintfield(b, protocolversion: i64); + protocolfield(b, runid); + protocolintfield(b, corpuscount: i64); + protocolintfield(b, nativecount: i64); + protocolfield(b, corpushash); + protocolintfield(b, selected: i64); + protocolnewline(b); +}; + +fn putrow(b: *[]u8, r: *cellresult) void = { + protocolput(b, "case"); + protocolintfield(b, r.ordinal: i64); + protocolfield(b, r.id); + protocolfield(b, stageword(r.stage)); + protocolfield(b, phaseword(r.phase)); + protocolfield(b, verdictword(r.verdict)); + protocolfield(b, terminationword(r.termination)); + protocolintfield(b, r.code: i64); + protocolintfield(b, r.durationns); + protocolfield(b, r.capture); + protocolnewline(b); +}; + +fn putfooter(b: *[]u8, selected: i32, failures: i32) void = { + protocolput(b, "end"); + protocolintfield(b, selected: i64); + protocolintfield(b, failures: i64); + protocolnewline(b); +}; + +fn writestream(fd: i32, runid: str, c: *corpus, ids: []identity, + results: []cellresult) bool = { + protocolerr = ""; + if (!validrunid(runid)) { return protocolfail("invalid run ID"); }; + if (!validexpected(c, ids)) { return false; }; + if (results.len != ids.len) { + return protocolfail("selected result count mismatch"); + }; + let failures: i32 = 0; + let i: i32 = 0; + for (i < results.len) { + if (!validrow(c, &ids[i], &results[i])) { return false; }; + if (results[i].verdict != verdict.PASS) { failures += 1; }; + i += 1; + }; + let b: []u8; + putheader(&b, runid, ids.len); + i = 0; + for (i < results.len) { + putrow(&b, &results[i]); + i += 1; + }; + putfooter(&b, ids.len, failures); + return protocolwriteall(fd, b); +}; + +fn splitfields(line: str, out: *[]str) bool = { + let fields: []str; + let start: i32 = 0; + let i: i32 = 0; + for (i <= line.len) { + if (i == line.len || line[i] == '\t': u8) { + append(fields, line[start:i]); + start = i + 1; + }; + i += 1; + }; + *out = fields; + return true; +}; + +fn nextline(data: str, off: *i32, out: *str) bool = { + if (*off >= data.len) { return false; }; + let end: i32 = *off; + for (end < data.len && data[end] != '\n': u8) { end += 1; }; + if (end == data.len) { return false; }; + *out = data[*off:end]; + *off = end + 1; + return true; +}; + +fn parseu64field(s: str, out: *u64) bool = { + if (s.len == 0) { return protocolfail("empty integer"); }; + if (s.len > 1 && s[0] == '0': u8) { + return protocolfail("non-canonical integer"); + }; + let n: u64 = 0u64; + let i: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (c < '0': u8 || c > '9': u8) { + return protocolfail("malformed integer"); + }; + let d: u64 = (c - '0': u8): u64; + if (n > 1844674407370955161u64 + || (n == 1844674407370955161u64 && d > 5u64)) { + return protocolfail("integer overflow"); + }; + n = n * 10u64 + d; + i += 1; + }; + *out = n; + return true; +}; + +fn parsei32nonnegative(s: str, out: *i32) bool = { + let n: u64 = 0u64; + if (!parseu64field(s, &n)) { return false; }; + if (n > 2147483647u64) { return protocolfail("i32 overflow"); }; + *out = n: i32; + return true; +}; + +fn parsei64nonnegative(s: str, out: *i64) bool = { + let n: u64 = 0u64; + if (!parseu64field(s, &n)) { return false; }; + if (n > 9223372036854775807u64) { return protocolfail("i64 overflow"); }; + *out = n: i64; + return true; +}; + +fn parsei32field(s: str, out: *i32) bool = { + if (s.len == 0) { return protocolfail("empty integer"); }; + if (s[0] != '-': u8) { return parsei32nonnegative(s, out); }; + if (s.len == 1 || (s.len > 2 && s[1] == '0': u8)) { + return protocolfail("malformed negative integer"); + }; + let magnitude: str; + magnitude.ptr = s.ptr + 1u64; + magnitude.len = s.len - 1; + let n: u64 = 0u64; + if (!parseu64field(magnitude, &n)) { return false; }; + if (n == 0u64 || n > 2147483648u64) { + return protocolfail("i32 overflow"); + }; + *out = -(n: i32); + return true; +}; + +fn parsestagefield(s: str, out: *stage) bool = { + if (s == "c") { *out = stage.C; return true; }; + if (s == "ww") { *out = stage.WW; return true; }; + return protocolfail("unknown stage"); +}; + +fn parsephasefield(s: str, out: *phase) bool = { + if (s == "compile") { *out = phase.COMPILE; return true; }; + if (s == "build") { *out = phase.BUILD; return true; }; + if (s == "run") { *out = phase.RUN; return true; }; + return protocolfail("unknown phase"); +}; + +fn parseverdictfield(s: str, out: *verdict) bool = { + if (s == "pass") { *out = verdict.PASS; return true; }; + if (s == "fail") { *out = verdict.FAIL; return true; }; + if (s == "error") { *out = verdict.ERROR; return true; }; + return protocolfail("unknown verdict"); +}; + +fn parseterminationfield(s: str, out: *termination) bool = { + if (s == "exit") { *out = termination.EXIT; return true; }; + if (s == "signal") { *out = termination.SIGNAL; return true; }; + if (s == "timeout") { *out = termination.TIMEOUT; return true; }; + if (s == "setup") { *out = termination.SETUP; return true; }; + return protocolfail("unknown termination"); +}; + +fn parseheader(line: str, selected: i32) bool = { + let fields: []str; + splitfields(line, &fields); + if (fields.len != 7) { return protocolfail("malformed header field count"); }; + if (fields[0] != "wwfix") { return protocolfail("missing or malformed header"); }; + let version: i32 = 0; + let observedcorpus: i32 = 0; + let observednative: i32 = 0; + let observedselected: i32 = 0; + if (!parsei32nonnegative(fields[1], &version) + || !parsei32nonnegative(fields[3], &observedcorpus) + || !parsei32nonnegative(fields[4], &observednative) + || !parsei32nonnegative(fields[6], &observedselected)) { return false; }; + if (version != protocolversion) { return protocolfail("wrong protocol version"); }; + if (!validrunid(fields[2])) { return protocolfail("invalid run ID"); }; + if (observedcorpus != corpuscount || observednative != nativecount + || strings.compare(fields[5], corpushash) != 0) { + return protocolfail("wrong corpus identity"); + }; + if (observedselected != selected) { + return protocolfail("header selected count mismatch"); + }; + return true; +}; + +fn parserow(line: str, out: *cellresult) bool = { + let fields: []str; + splitfields(line, &fields); + if (fields.len != 10) { return protocolfail("malformed case field count"); }; + if (fields[0] != "case") { return protocolfail("missing case record"); }; + if (!parsei32nonnegative(fields[1], &out.ordinal) + || !parsestagefield(fields[3], &out.stage) + || !parsephasefield(fields[4], &out.phase) + || !parseverdictfield(fields[5], &out.verdict) + || !parseterminationfield(fields[6], &out.termination) + || !parsei32field(fields[7], &out.code) + || !parsei64nonnegative(fields[8], &out.durationns)) { return false; }; + out.id = fields[2]; + out.capture = fields[9]; + return true; +}; + +fn parsefooter(line: str, selected: i32, failures: i32) bool = { + let fields: []str; + splitfields(line, &fields); + if (fields.len != 3 || fields[0] != "end") { + return protocolfail("missing or malformed footer"); + }; + let observedselected: i32 = 0; + let observedfailures: i32 = 0; + if (!parsei32nonnegative(fields[1], &observedselected) + || !parsei32nonnegative(fields[2], &observedfailures)) { return false; }; + if (observedselected != selected || observedfailures != failures) { + return protocolfail("inconsistent footer"); + }; + return true; +}; + +fn validatestream(data: str, c: *corpus, ids: []identity) bool = { + protocolerr = ""; + if (data.len == 0) { return protocolfail("missing header"); }; + if (data[data.len - 1] != '\n': u8) { + return protocolfail("incomplete final line"); + }; + let off: i32 = 0; + let line: str; + if (!nextline(data, &off, &line)) { return protocolfail("missing header"); }; + if (!parseheader(line, ids.len)) { return false; }; + if (!validexpected(c, ids)) { return false; }; + let failures: i32 = 0; + let i: i32 = 0; + for (i < ids.len) { + if (!nextline(data, &off, &line)) { + return protocolfail("missing case record"); + }; + let r: cellresult; + if (!parserow(line, &r) || !validrow(c, &ids[i], &r)) { return false; }; + if (r.verdict != verdict.PASS) { failures += 1; }; + i += 1; + }; + if (!nextline(data, &off, &line)) { return protocolfail("missing footer"); }; + if (!parsefooter(line, ids.len, failures)) { return false; }; + if (off != data.len) { return protocolfail("trailing records"); }; + return true; +}; + +export fn validate(data: str, root: str, patterns: []str) bool = { + protocolerr = ""; + let c: corpus; + if (!loadcorpus(root, &c)) { + protocolerr = strings.concat("cannot load corpus: ", error()); + return false; + }; + let ids: []identity; + if (!selectidentities(&c, patterns, &ids)) { + protocolerr = strings.concat("cannot select corpus: ", error()); + return false; + }; + return validatestream(data, &c, ids); +}; diff --git a/internal/wwfixture/run.ww b/internal/wwfixture/run.ww new file mode 100644 index 00000000..963960b4 --- /dev/null +++ b/internal/wwfixture/run.ww @@ -0,0 +1,566 @@ +package wwfixture; + +import fmt; +import os; +import os.exec; +import strings; +import temp; +import time; + +def fixturepoll: time.duration = 1000000i64: time.duration; +def fixturegrace: time.duration = 250000000i64: time.duration; + +type cellstate = enum i32 { + QUEUED = 0, + COMPILE = 1, + BUILD = 2, + RUN = 3, + DONE = 4, +}; + +type fixturecell = struct { + fixtureindex: i32, + stage: stage, + state: cellstate, + dir: str, + began: time.instant, + deadline: time.instant, + result: cellresult, +}; + +fn rundecimal(value: i32) str = { + let digits: [11]u8; + let n: i32 = 0; + let v: i64 = value: i64; + let negative: bool = v < 0i64; + if (negative) { v = -v; }; + if (v == 0i64) { digits[0] = '0': u8; n = 1; }; + for (v > 0i64) { + digits[n] = '0': u8 + (v % 10i64): u8; + n += 1; + v /= 10i64; + }; + let capacity: i32 = n; + if (negative) { capacity += 1; }; + let out: []u8 = alloc([], capacity: u64)!; + if (negative) { append(out, '-': u8); }; + let i: i32 = n; + for (i > 0) { i -= 1; append(out, digits[i]); }; + return strings.frombytes(out); +}; + +fn numbered(ordinal: i32) str = { + let b: []u8 = alloc([], 4u64)!; + b.len = 4; + let v: i32 = ordinal; + let i: i32 = 3; + for (i >= 0) { + b[i] = '0': u8 + (v % 10): u8; + v /= 10; + i -= 1; + }; + return strings.frombytes(b); +}; + +fn runid(path: str) str = { + let start: i32 = 0; + let i: i32 = 0; + for (i < path.len) { + if (path[i] == '/': u8) { start = i + 1; }; + i += 1; + }; + return strings.dup(path[start:path.len]); +}; + +fn makeenv(root: str, s: stage) []str = { + let env: []str = alloc([], 7u64)!; + append(env, "PATH=/usr/bin:/bin"); + append(env, "LC_ALL=C"); + append(env, strings.concat("WW_SRCLIB=", join(root, "lib"))); + append(env, strings.concat("WW_LIB=", join(root, "out/lib"))); + let suffix: str = ""; + if (s == stage.WW) { suffix = "_ww"; }; + append(env, strings.concat("WW_W6C=", join(root, + strings.concat("out/bin/w6c", suffix)))); + append(env, strings.concat("WW_W6A=", join(root, + strings.concat("out/bin/w6a", suffix)))); + append(env, strings.concat("WW_W6L=", join(root, + strings.concat("out/bin/w6l", suffix)))); + return env; +}; + +fn initialresult(id: *identity, f: *fixture, dir: str) cellresult = { + let r: cellresult; + r.ordinal = id.ordinal; + r.id = f.id; + r.stage = id.stage; + r.phase = phase.COMPILE; + if (f.directive == directive.RUN || f.directive == directive.RUNEXIT) { + r.phase = phase.BUILD; + }; + r.verdict = verdict.ERROR; + r.termination = termination.SETUP; + r.code = -5; + r.durationns = 0i64; + r.capture = dir; + return r; +}; + +fn procmessage(reason: str, p: *exec.result) str = { + return strings.concat(reason, + "\ntermination=", rundecimal(p.termination as i32), + " code=", rundecimal(p.code), + " errno=", rundecimal(p.errno), + " cleanup-errno=", rundecimal(p.cleanuperrno)); +}; + +fn maptermination(p: *exec.result, r: *cellresult) void = { + if (p.termination == exec.termination.TIMEOUT) { + r.termination = termination.TIMEOUT; + r.code = 0; + return; + }; + if (p.errno != 0 || p.cleanuperrno != 0 + || p.termination == exec.termination.ERROR) { + r.termination = termination.SETUP; + r.code = -p.errno; + if (r.code == 0) { r.code = -p.cleanuperrno; }; + if (r.code == 0) { r.code = -5; }; + return; + }; + if (p.termination == exec.termination.EXIT) { + r.termination = termination.EXIT; + r.code = p.code; + return; + }; + if (p.termination == exec.termination.SIGNAL) { + r.termination = termination.SIGNAL; + r.code = p.code; + return; + }; + r.termination = termination.SETUP; + r.code = -5; +}; + +fn processerror(p: *exec.result) bool = { + return p.errno != 0 || p.cleanuperrno != 0 + || p.termination == exec.termination.ERROR; +}; + +fn finishmessage(c: *fixturecell, p: *exec.result, reason: str) bool = { + let message: str = procmessage(reason, p); + if (writeharness(c.dir, message)) { return true; }; + c.result.verdict = verdict.ERROR; + return false; +}; + +fn startcommand(root: str, f: *fixture, c: *fixturecell, + h: *exec.process) void = { + let argv: []str; + let out: str; + let err: str; + if (c.state == cellstate.COMPILE) { + let frontend: str = join(root, "out/bin/w6c"); + if (c.stage == stage.WW) { frontend = join(root, "out/bin/w6c_ww"); }; + let compileargv: []str = alloc([], 4u64)!; + argv = compileargv; + append(argv, frontend); append(argv, "-o"); + append(argv, join(c.dir, "case.s")); append(argv, f.source); + out = join(c.dir, "compile.stdout"); + err = join(c.dir, "compile.stderr"); + } else if (c.state == cellstate.BUILD) { + let driver: str = join(root, "out/bin/ww"); + if (c.stage == stage.WW) { driver = join(root, "out/bin/ww_ww"); }; + let buildargv: []str = alloc([], 5u64)!; + argv = buildargv; + append(argv, driver); append(argv, "build"); append(argv, "-o"); + append(argv, join(c.dir, "program")); append(argv, f.source); + out = join(c.dir, "build.stdout"); + err = join(c.dir, "build.stderr"); + } else { + let program: str = join(c.dir, "program"); + let runargv: []str = alloc([], 1u64)!; + argv = runargv; + append(argv, program); + out = join(c.dir, "run.stdout"); + err = join(c.dir, "run.stderr"); + }; + let env: []str = makeenv(root, c.stage); + let cmd: exec.command; + cmd.path = argv[0]; + cmd.argv = argv; + cmd.env = env; + cmd.dir = c.dir; + cmd.stdoutpath = out; + cmd.stderrpath = err; + cmd.deadline = c.deadline; + cmd.grace = fixturegrace; + exec.start(h, &cmd); +}; + +// Returns true only when the cell reached a terminal result. A successful +// positive build starts its program directly and keeps the scheduler slot. +fn collect(root: str, f: *fixture, c: *fixturecell, + h: *exec.process, captureok: *bool) bool = { + let p: *exec.result = &h.result; + c.result.durationns = time.diff(c.began, + time.now(time.clock.monotonic)): i64; + maptermination(p, &c.result); + if (c.state == cellstate.COMPILE) { + c.result.phase = phase.COMPILE; + if (processerror(p)) { + c.result.verdict = verdict.ERROR; + if (!finishmessage(c, p, "compiler process failed")) { *captureok = false; }; + } else if (f.directive == directive.COMPILE) { + if (c.result.termination == termination.EXIT && c.result.code == 0) { + c.result.verdict = verdict.PASS; + } else { + c.result.verdict = verdict.FAIL; + if (!finishmessage(c, p, + "compiler did not exit normally with status 0")) { + *captureok = false; + }; + }; + } else if (c.result.termination == termination.EXIT && c.result.code != 0) { + let diagnostic: str; + let required: str = f.diagnostic; + if (c.stage == stage.WW && f.wwdiagnostic.len != 0) { + required = f.wwdiagnostic; + }; + let path: str = join(c.dir, "compile.stderr"); + if (!readfile(path, &diagnostic)) { + c.result.verdict = verdict.ERROR; + if (!finishmessage(c, p, "cannot read compiler diagnostics")) { + *captureok = false; + }; + } else if (strings.contains(diagnostic, required)) { + c.result.verdict = verdict.PASS; + } else { + c.result.verdict = verdict.FAIL; + if (!finishmessage(c, p, "required diagnostic fragment was not observed")) { + *captureok = false; + }; + }; + } else { + c.result.verdict = verdict.FAIL; + if (!finishmessage(c, p, "compiler did not exit normally with a nonzero status")) { + *captureok = false; + }; + }; + c.state = cellstate.DONE; + return true; + }; + if (c.state == cellstate.BUILD) { + c.result.phase = phase.BUILD; + if (!processerror(p) && c.result.termination == termination.EXIT + && c.result.code == 0) { + c.state = cellstate.RUN; + startcommand(root, f, c, h); + return false; + }; + if (processerror(p)) { c.result.verdict = verdict.ERROR; } + else { c.result.verdict = verdict.FAIL; }; + if (!finishmessage(c, p, "positive fixture build did not exit normally with status 0")) { + *captureok = false; + }; + c.state = cellstate.DONE; + return true; + }; + c.result.phase = phase.RUN; + let expected: i32 = 0; + if (f.directive == directive.RUNEXIT) { expected = f.exitcode; }; + if (processerror(p)) { + c.result.verdict = verdict.ERROR; + if (!finishmessage(c, p, "fixture program process failed")) { *captureok = false; }; + } else if (c.result.termination == termination.EXIT + && c.result.code == expected) { + c.result.verdict = verdict.PASS; + } else { + c.result.verdict = verdict.FAIL; + if (!finishmessage(c, p, "fixture program did not exit normally with the expected status")) { + *captureok = false; + }; + }; + c.state = cellstate.DONE; + return true; +}; + +fn finishinterrupted(c: *fixturecell, p: *exec.result, + signo: i32, captureok: *bool) void = { + c.result.phase = phase.COMPILE; + if (c.state == cellstate.BUILD) { c.result.phase = phase.BUILD; } + else if (c.state == cellstate.RUN) { c.result.phase = phase.RUN; }; + c.result.durationns = time.diff(c.began, + time.now(time.clock.monotonic)): i64; + maptermination(p, &c.result); + c.result.verdict = verdict.ERROR; + let why: str = strings.concat("coordinator interrupted by signal ", + rundecimal(signo)); + if (signo < 0) { why = "coordinator interrupt monitor failed"; }; + if (!finishmessage(c, p, why)) { *captureok = false; }; + c.state = cellstate.DONE; +}; + +fn finishqueued(c: *fixturecell, code: i32, reason: str, + captureok: *bool) void = { + c.result.verdict = verdict.ERROR; + c.result.termination = termination.SETUP; + c.result.code = -code; + if (c.result.code == 0) { c.result.code = -4; }; + c.result.durationns = 0i64; + let p: exec.result; + p.termination = exec.termination.ERROR; + p.errno = code; + if (!finishmessage(c, &p, reason)) { *captureok = false; }; + c.state = cellstate.DONE; +}; + +fn ensurecapture(dir: str, reason: str) bool = { + let rc: i32 = os.mkdir(dir, 448i32); + if (rc != 0 && rc != -17) { return false; }; + return writeharness(dir, reason); +}; + +fn cleanuppassingcapture(c: *fixturecell) bool = { + let r: *cellresult = &c.result; + if (removeall(c.dir)) { + r.capture = "-"; + return true; + }; + r.verdict = verdict.ERROR; + r.termination = termination.SETUP; + r.code = -5; + return ensurecapture(c.dir, "passing capture cleanup failed"); +}; + +fn cleanupcaptures(opts: *options, cells: []fixturecell, + runroot: str) bool = { + let ok: bool = true; + let kept: bool = false; + let i: i32 = 0; + for (i < cells.len) { + let r: *cellresult = &cells[i].result; + if (r.verdict == verdict.PASS && !opts.keep) { + if (r.capture != "-" && !cleanuppassingcapture(&cells[i])) { + ok = false; + }; + if (r.capture != "-") { kept = true; }; + } else { kept = true; }; + i += 1; + }; + if (!kept) { + if (os.rmdir(runroot) != 0) { + ok = false; + if (cells.len > 0) { + let c: *fixturecell = &cells[0]; + c.result.verdict = verdict.ERROR; + c.result.termination = termination.SETUP; + c.result.code = -5; + c.result.capture = c.dir; + ensurecapture(c.dir, "run directory cleanup failed"); + }; + }; + }; + return ok; +}; + +fn preparecells(opts: *options, c: *corpus, ids: []identity, + runroot: str, outcells: *[]fixturecell, + outhandles: *[]exec.process) bool = { + let cells: []fixturecell = alloc([], ids.len: u64)!; + let handles: []exec.process = alloc([], ids.len: u64)!; + let i: i32 = 0; + for (i < ids.len) { + let f: *fixture = &c.fixtures[ids[i].fixtureindex]; + let cell: fixturecell; + cell.fixtureindex = ids[i].fixtureindex; + cell.stage = ids[i].stage; + cell.state = cellstate.QUEUED; + cell.dir = join(runroot, numbered(ids[i].ordinal)); + cell.result = initialresult(&ids[i], f, cell.dir); + append(cells, cell); + let handle: exec.process; + append(handles, handle); + i += 1; + }; + *outcells = cells; + *outhandles = handles; + let ok: bool = true; + i = 0; + for (i < cells.len) { + if (os.mkdir(cells[i].dir, 448i32) != 0) { + ok = false; + i += 1; + continue; + }; + i += 1; + }; + if (!ok) { fmt.errorln("wwfixture: cannot create numbered capture directory"); }; + return ok; +}; + +fn cancelall(handles: []exec.process) bool = { + let i: i32 = 0; + for (i < handles.len) { exec.cancel(&handles[i]); i += 1; }; + let pending: bool = true; + for (pending) { + pending = false; + i = 0; + for (i < handles.len) { + if (!exec.poll(&handles[i])) { pending = true; }; + i += 1; + }; + if (pending) { time.sleep(fixturepoll, time.clock.monotonic); }; + }; + let ok: bool = true; + i = 0; + for (i < handles.len) { + if (handles[i].pid > 0 && (handles[i].result.errno != 0 + || handles[i].result.cleanuperrno != 0)) { ok = false; }; + i += 1; + }; + return ok; +}; + +fn runfixtures(opts: *options, c: *corpus, ids: []identity) int = { + let runroot: str = strings.dup(temp.dir()); + let rid: str = runid(runroot); + let cells: []fixturecell; + let handles: []exec.process; + let prepared: bool = preparecells(opts, c, ids, runroot, &cells, &handles); + let watch: exec.interrupt; + let captureok: bool = true; + let watchok: bool = false; + let interrupted: i32 = 0; + let active: i32 = 0; + let complete: i32 = 0; + let next: i32 = 0; + if (!prepared) { + let rootcaptureok: bool = writeharness(runroot, + "cannot create one or more numbered capture directories"); + let i: i32 = 0; + for (i < cells.len) { + let cellcaptureok: bool = true; + finishqueued(&cells[i], 5, + "fixture setup stopped before process launch", &cellcaptureok); + if (!cellcaptureok) { + cells[i].result.capture = runroot; + if (!rootcaptureok) { captureok = false; }; + }; + i += 1; + }; + complete = cells.len; + } else if (cells.len > 0) { + watchok = exec.interruptopen(&watch); + }; + if (prepared && cells.len > 0 && !watchok) { + let code: i32 = watch.errno; + let i: i32 = 0; + for (i < cells.len) { + finishqueued(&cells[i], code, + "cannot establish coordinator interrupt monitor", &captureok); + i += 1; + }; + complete = cells.len; + }; + for (complete < cells.len) { + let signo: i32 = exec.interruptpoll(&watch); + if (signo != 0) { + interrupted = signo; + let cleanupok: bool = cancelall(handles); + if (!cleanupok) { captureok = false; }; + let i: i32 = 0; + for (i < cells.len) { + if (cells[i].state == cellstate.DONE) { i += 1; continue; }; + if (cells[i].state == cellstate.QUEUED) { + let ec: i32 = 4; + if (signo < 0) { ec = -signo; }; + finishqueued(&cells[i], ec, + "coordinator stopped before cell launch", &captureok); + } else { + finishinterrupted(&cells[i], &handles[i].result, + signo, &captureok); + }; + i += 1; + }; + complete = cells.len; + break; + }; + for (next < cells.len && active < opts.jobs) { + let current: *fixturecell = &cells[next]; + let f: *fixture = &c.fixtures[current.fixtureindex]; + current.state = cellstate.COMPILE; + if (f.directive == directive.RUN || f.directive == directive.RUNEXIT) { + current.state = cellstate.BUILD; + }; + current.began = time.now(time.clock.monotonic); + current.deadline = time.add(current.began, + (opts.timeoutms * (time.millisecond: i64)): time.duration); + startcommand(c.root, f, current, &handles[next]); + active += 1; + next += 1; + }; + let i: i32 = 0; + for (i < next) { + if (cells[i].state == cellstate.DONE + || cells[i].state == cellstate.QUEUED) { i += 1; continue; }; + if (exec.poll(&handles[i])) { + let f: *fixture = &c.fixtures[cells[i].fixtureindex]; + if (collect(c.root, f, &cells[i], &handles[i], &captureok)) { + if (cells[i].result.verdict == verdict.PASS && !opts.keep + && !cleanuppassingcapture(&cells[i])) { + captureok = false; + }; + active -= 1; + complete += 1; + }; + }; + i += 1; + }; + if (active > 0) { time.sleep(fixturepoll, time.clock.monotonic); }; + }; + if (watchok && !exec.interruptclose(&watch)) { + captureok = false; + if (cells.len > 0) { + let c0: *fixturecell = &cells[0]; + if (!writefile(join(c0.dir, "coordinator.txt"), + "cannot close coordinator interrupt monitor\n")) { + captureok = false; + }; + if (c0.result.verdict == verdict.PASS) { + c0.result.verdict = verdict.ERROR; + c0.result.termination = termination.SETUP; + c0.result.code = -watch.errno; + if (c0.result.code == 0) { c0.result.code = -5; }; + }; + }; + }; + if (!cleanupcaptures(opts, cells, runroot)) { captureok = false; }; + let results: []cellresult = alloc([], cells.len: u64)!; + let failures: i32 = 0; + let i: i32 = 0; + for (i < cells.len) { + append(results, cells[i].result); + if (cells[i].result.verdict != verdict.PASS) { failures += 1; }; + i += 1; + }; + if (!writestream(os.STDOUT_FILENO, rid, c, ids, results)) { + let retained: bool = true; + let rc: i32 = os.mkdir(runroot, 448i32); + if (rc != 0 && rc != -17) { retained = false; }; + if (retained && !writefile(join(runroot, "publication.txt"), + strings.concat("result publication failed: ", protocolerror(), "\n"))) { + retained = false; + }; + if (retained) { + fmt.errorln(strings.concat("wwfixture: ", protocolerror(), + "; capture: ", runroot)); + } else { + fmt.errorln(strings.concat("wwfixture: ", protocolerror(), + "; cannot retain publication capture")); + }; + return 1; + }; + if (!captureok || interrupted != 0 || failures != 0) { return 1; }; + return 0; +}; diff --git a/internal/wwfixture/types.ww b/internal/wwfixture/types.ww new file mode 100644 index 00000000..96c4e3c2 --- /dev/null +++ b/internal/wwfixture/types.ww @@ -0,0 +1,98 @@ +package wwfixture; + +def protocolversion: i32 = 1; +def corpuscount: i32 = 1224; +def errorcount: i32 = 314; +def compilecount: i32 = 12; +def runcount: i32 = 136; +def runexitcount: i32 = 762; +def nativecount: i32 = 2448; +def corpushash: str = "8b522e42cbce8b389e33e917229dede8045d85f1af0c54630cfef83c4ef242be"; + +type directive = enum i32 { + ERROR = 0, + RUN = 1, + RUNEXIT = 2, + COMPILE = 3, +}; + +type stage = enum i32 { + C = 0, + WW = 1, +}; + +type phase = enum i32 { + COMPILE = 0, + BUILD = 1, + RUN = 2, +}; + +type verdict = enum i32 { + PASS = 0, + FAIL = 1, + ERROR = 2, +}; + +type termination = enum i32 { + EXIT = 0, + SIGNAL = 1, + TIMEOUT = 2, + SETUP = 3, +}; + +type fixture = struct { + name: str, + id: str, + source: str, + directive: directive, + diagnostic: str, + wwdiagnostic: str, + exitcode: i32, +}; + +type corpus = struct { + root: str, + fixtures: []fixture, +}; + +type identity = struct { + ordinal: i32, + fixtureindex: i32, + stage: stage, +}; + +type cellresult = struct { + ordinal: i32, + id: str, + stage: stage, + phase: phase, + verdict: verdict, + termination: termination, + code: i32, + durationns: i64, + capture: str, +}; + +fn stageword(s: stage) str = { + if (s == stage.C) { return "c"; }; + return "ww"; +}; + +fn phaseword(p: phase) str = { + if (p == phase.COMPILE) { return "compile"; }; + if (p == phase.BUILD) { return "build"; }; + return "run"; +}; + +fn verdictword(v: verdict) str = { + if (v == verdict.PASS) { return "pass"; }; + if (v == verdict.FAIL) { return "fail"; }; + return "error"; +}; + +fn terminationword(t: termination) str = { + if (t == termination.EXIT) { return "exit"; }; + if (t == termination.SIGNAL) { return "signal"; }; + if (t == termination.TIMEOUT) { return "timeout"; }; + return "setup"; +}; diff --git a/test/wwfixture/integration.sh b/test/wwfixture/integration.sh new file mode 100644 index 00000000..0b157f36 --- /dev/null +++ b/test/wwfixture/integration.sh @@ -0,0 +1,442 @@ +#!/bin/sh +set -eu + +LC_ALL=C +export LC_ALL + +repo=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +tool=$repo/out/bin/wwfixture +work=$(mktemp -d "${TMPDIR:-/tmp}/wwfixture-test.XXXXXXXX") +coordinator= + +cleanup() +{ + if [ -n "$coordinator" ]; then + kill -KILL "$coordinator" 2> /dev/null || : + wait "$coordinator" 2> /dev/null || : + fi + for stream in "$work"/*.tab; do + [ -f "$stream" ] || continue + awk -F '\t' '$1 == "case" && $10 ~ /^\/tmp\/[0-9a-f]{16}\/[0-9]{4}$/ { + p=$10; sub("/[0-9]{4}$", "", p); print p + }' "$stream" + done | sort -u | while IFS= read -r run; do + if [ -d "$run" ]; then + find "$run" -depth -delete + fi + done + find "$work" -depth -delete +} + +trap cleanup EXIT HUP INT TERM + +fail() +{ + echo "test/wwfixture: $*" >&2 + exit 1 +} + +clone_corpus() +{ + dest=$1 + mkdir -p "$dest/test/wcc/data" + for source in "$repo"/test/wcc/data/*/case.ww; do + name=${source%/case.ww} + name=${name##*/} + mkdir "$dest/test/wcc/data/$name" + cp "$source" "$dest/test/wcc/data/$name/case.ww" + done + ln -s "$repo/out" "$dest/out" + ln -s "$repo/lib" "$dest/lib" +} + +cleanup_capture() +{ + stream=$1 + capture=$(awk -F '\t' '$1 == "case" && $10 != "-" { print $10; exit }' \ + "$stream") + [ -n "$capture" ] || return 0 + run=${capture%/*} + id=${run#/tmp/} + [ "$run" != "$id" ] || fail "capture outside /tmp" + [ "${#id}" -eq 16 ] || fail "malformed capture run ID" + case $id in *[!0-9a-f]*) fail "unsafe capture run ID" ;; esac + [ -d "$run" ] || fail "missing retained capture" + find "$run" -depth -delete +} + +cleanup_run() +{ + run=$1 + id=${run#/tmp/} + [ "$run" != "$id" ] || fail "retained run outside /tmp" + [ "${#id}" -eq 16 ] || fail "malformed retained run ID" + case $id in *[!0-9a-f]*) fail "unsafe retained run ID" ;; esac + [ -d "$run" ] || fail "missing retained run" + find "$run" -depth -delete +} + +replace_directive() +{ + path=$1 + line=$2 + tmp=$path.new + { + printf '%s\n' "$line" + sed -n '2,$p' "$path" + } > "$tmp" + mv "$tmp" "$path" +} + +expect_invalid() +{ + root=$1 + pattern=$2 + stream=$3 + if "$tool" validate -root "$root" -run "$pattern" "$stream" \ + > /dev/null 2>&1; then + fail "validator accepted $(basename -- "$stream")" + fi +} + +base=$work/base +clone_corpus "$base" + +# Filtering and listing publish identities only; no synthetic SKIP records. +list=$work/list.tab +"$tool" -root "$base" -run 'compiler.runww_dup_main_reject/c' -list > "$list" +[ "$(wc -l < "$list")" -eq 1 ] || fail "filtered list count" +[ "$(sed -n '1p' "$list")" = "compiler.runww_dup_main_reject c" ] \ + || fail "filtered list identity" + +# Real positive fixtures must build successfully before their programs run. +runstream=$work/run-pass.tab +"$tool" -root "$base" -run 'compiler.idx_dot_aggret_subtail_run/c' \ + -j 1 > "$runstream" +"$tool" validate -root "$base" \ + -run 'compiler.idx_dot_aggret_subtail_run/c' "$runstream" +awk -F '\t' 'NR == 2 && $5 == "run" && $6 == "pass" && \ + $7 == "exit" && $8 == "0" && $10 == "-" { ok=1 } \ + END { exit !ok }' "$runstream" || fail "run fixture terminal row" + +runexitstream=$work/run-exit-pass.tab +"$tool" -root "$base" -run 'compiler.runww_enum_run/c' -j 1 \ + > "$runexitstream" +"$tool" validate -root "$base" -run 'compiler.runww_enum_run/c' \ + "$runexitstream" +awk -F '\t' 'NR == 2 && $5 == "run" && $6 == "pass" && \ + $7 == "exit" && $8 == "6" && $10 == "-" { ok=1 } \ + END { exit !ok }' "$runexitstream" || fail "run-exit fixture terminal row" + +# Compile fixtures stop at the frontend and publish one terminal row per stage. +compilepattern=compiler.r78_strict_package_main_accept +compilestream=$work/compile-pass.tab +"$tool" -root "$base" -run "$compilepattern" -j 1 > "$compilestream" +"$tool" validate -root "$base" -run "$compilepattern" "$compilestream" +awk -F '\t' 'NR == 1 && $7 == "2" { h=1 } \ + $1 == "case" && $4 == "c" && $5 == "compile" && $6 == "pass" && \ + $7 == "exit" && $8 == "0" && $10 == "-" { c=1 } \ + $1 == "case" && $4 == "ww" && $5 == "compile" && $6 == "pass" && \ + $7 == "exit" && $8 == "0" && $10 == "-" { w=1 } \ + END { exit !(h && c && w) }' "$compilestream" \ + || fail "compile fixture terminal rows" + +# A compile directive cannot turn a frontend rejection into a pass. +compilefailroot=$work/compile-failure +cp -R "$base" "$compilefailroot" +compilecase=$compilefailroot/test/wcc/data/r78_strict_package_main_accept/case.ww +printf '%s\n' '//ww:compile' 'package main;' 'fn main( {' > "$compilecase" +compilefailstream=$work/compile-failure.tab +if "$tool" -root "$compilefailroot" -run "$compilepattern/c" -j 1 \ + > "$compilefailstream"; then + fail "compile rejection satisfied compile directive" +fi +"$tool" validate -root "$compilefailroot" -run "$compilepattern/c" \ + "$compilefailstream" +awk -F '\t' 'NR == 2 && $5 == "compile" && $6 == "fail" && \ + $7 == "exit" && $8 != "0" && $10 != "-" { ok=1 } \ + END { exit !ok }' "$compilefailstream" \ + || fail "compile failure terminal row" +cleanup_capture "$compilefailstream" + +# A build exit equal to the requested runtime exit is still a build failure. +buildroot=$work/build-failure +cp -R "$base" "$buildroot" +buildpattern=compiler.r816_one_elem +buildcase=$buildroot/test/wcc/data/r816_one_elem/case.ww +printf '%s\n' '//ww:run-exit 1' 'package main;' \ + 'fn main( {' > "$buildcase" +buildstream=$work/build-failure.tab +if "$tool" -root "$buildroot" \ + -run "$buildpattern/c" -j 1 > "$buildstream"; then + fail "build failure satisfied runtime expectation" +fi +"$tool" validate -root "$buildroot" \ + -run "$buildpattern/c" "$buildstream" +awk -F '\t' 'NR == 2 && $5 == "build" && $6 == "fail" && \ + $7 == "exit" && $8 == "1" { ok=1 } END { exit !ok }' "$buildstream" \ + || fail "build failure terminal row" +cleanup_capture "$buildstream" + +# A normal rejecting compile without the required stderr fragment fails. +diagroot=$work/diagnostic-mismatch +cp -R "$base" "$diagroot" +replace_directive "$diagroot/test/wcc/data/runww_dup_main_reject/case.ww" \ + '//ww:error "fragment that is deliberately absent"' +diagstream=$work/diagnostic-mismatch.tab +if "$tool" -root "$diagroot" -run 'compiler.runww_dup_main_reject/c' \ + -j 1 > "$diagstream"; then + fail "diagnostic mismatch passed" +fi +"$tool" validate -root "$diagroot" -run 'compiler.runww_dup_main_reject/c' \ + "$diagstream" +awk -F '\t' 'NR == 2 && $5 == "compile" && $6 == "fail" && \ + $7 == "exit" && $8 != "0" && $10 != "-" { ok=1 } \ + END { exit !ok }' "$diagstream" || fail "diagnostic mismatch terminal row" +cleanup_capture "$diagstream" + +# A labeled error directive selects the exact diagnostic for each stage. +stagediagpattern=compiler.r828_arrlit_ret_overlong +stagediagstream=$work/stage-diagnostic-pass.tab +"$tool" -root "$base" -run "$stagediagpattern" -j 1 > "$stagediagstream" +"$tool" validate -root "$base" -run "$stagediagpattern" "$stagediagstream" +awk -F '\t' '$1 == "case" && $4 == "c" && $5 == "compile" && \ + $6 == "pass" && $7 == "exit" && $8 != "0" && $10 == "-" { c=1 } \ + $1 == "case" && $4 == "ww" && $5 == "compile" && \ + $6 == "pass" && $7 == "exit" && $8 != "0" && $10 == "-" { w=1 } \ + END { exit !(c && w) }' "$stagediagstream" \ + || fail "stage-specific diagnostic terminal rows" + +# Swapping the labeled fragments must fail both cells. This catches either +# fragment being shared across stages or treated as an unordered alternative. +stageswaproot=$work/stage-diagnostic-swapped +cp -R "$base" "$stageswaproot" +replace_directive \ + "$stageswaproot/test/wcc/data/r828_arrlit_ret_overlong/case.ww" \ + '//ww:error c "array literal has 3 elements but declared array holds 2" ww "return [3]int not assignable to [2]int"' +stageswapstream=$work/stage-diagnostic-swapped.tab +if "$tool" -root "$stageswaproot" -run "$stagediagpattern" -j 1 \ + > "$stageswapstream"; then + fail "swapped stage-specific diagnostics passed" +fi +"$tool" validate -root "$stageswaproot" \ + -run "$stagediagpattern" "$stageswapstream" +awk -F '\t' '$1 == "case" && $4 == "c" && $5 == "compile" && \ + $6 == "fail" && $7 == "exit" && $8 != "0" && $10 != "-" { c=1 } \ + $1 == "case" && $4 == "ww" && $5 == "compile" && \ + $6 == "fail" && $7 == "exit" && $8 != "0" && $10 != "-" { w=1 } \ + END { exit !(c && w) }' "$stageswapstream" \ + || fail "stage-specific diagnostics were shared across stages" +cleanup_capture "$stageswapstream" + +# Signals remain signals and can never satisfy a numeric runtime expectation. +signalroot=$work/signal +cp -R "$base" "$signalroot" +signalcase=$signalroot/test/wcc/data/idx_dot_aggret_subtail_run/case.ww +printf '%s\n' '//ww:run' 'package main;' 'import os;' \ + 'export fn main() int = {' \ + ' os.kill(os.getpid(), os.SIGKILL);' ' return 0;' '};' > "$signalcase" +signalstream=$work/signal.tab +if "$tool" -root "$signalroot" -run 'compiler.idx_dot_aggret_subtail_run/c' \ + -j 1 > "$signalstream"; then + fail "signaled fixture passed" +fi +"$tool" validate -root "$signalroot" \ + -run 'compiler.idx_dot_aggret_subtail_run/c' "$signalstream" +awk -F '\t' 'NR == 2 && $5 == "run" && $6 == "fail" && \ + $7 == "signal" && $8 == "9" { ok=1 } END { exit !ok }' "$signalstream" \ + || fail "signal terminal row" +cleanup_capture "$signalstream" + +# One total deadline covers build and run; the busy program is terminated. +timeoutroot=$work/timeout +cp -R "$base" "$timeoutroot" +timeoutcase=$timeoutroot/test/wcc/data/idx_dot_aggret_subtail_run/case.ww +printf '%s\n' '//ww:run' 'package main;' 'export fn main() int = {' \ + ' for (true) { };' ' return 0;' '};' > "$timeoutcase" +timeoutstream=$work/timeout.tab +if "$tool" -root "$timeoutroot" -run 'compiler.idx_dot_aggret_subtail_run/c' \ + -j 1 -timeout-ms 250 > "$timeoutstream"; then + fail "timed out fixture passed" +fi +"$tool" validate -root "$timeoutroot" \ + -run 'compiler.idx_dot_aggret_subtail_run/c' "$timeoutstream" +awk -F '\t' 'NR == 2 && $6 == "fail" && $7 == "timeout" && \ + $8 == "0" && $9 >= 200000000 { ok=1 } END { exit !ok }' "$timeoutstream" \ + || fail "timeout terminal row" +cleanup_capture "$timeoutstream" + +# The real coordinator consumes SIGTERM, owns active cleanup, and still emits +# exactly one terminal record for the active cell and the queued cell. +interruptstream=$work/interrupted.tab +"$tool" -root "$timeoutroot" -run 'compiler.idx_dot_aggret_subtail_run' \ + -j 1 -timeout-ms 10000 > "$interruptstream" & +coordinator=$! +program= +tries=0 +while [ "$tries" -lt 300 ]; do + if [ -r "/proc/$coordinator/task/$coordinator/children" ]; then + for child in $(sed -n '1p' "/proc/$coordinator/task/$coordinator/children"); do + exe=$(readlink "/proc/$child/exe" 2> /dev/null || :) + case $exe in /tmp/*/program) program=$child ;; esac + done + fi + [ -z "$program" ] || break + tries=$((tries + 1)) + sleep 0.01 +done +[ -n "$program" ] || fail "coordinator runtime child did not start" +kill -TERM "$coordinator" +if wait "$coordinator"; then + fail "interrupted coordinator returned success" +fi +coordinator= +[ ! -e "/proc/$program" ] || fail "interrupted leader was not reaped" +"$tool" validate -root "$timeoutroot" \ + -run 'compiler.idx_dot_aggret_subtail_run' "$interruptstream" +awk -F '\t' 'NR == 1 && $7 == "2" { h=1 } \ + NR == 2 && $2 == "1" && $5 == "run" && $6 == "error" && \ + $7 == "signal" && $8 == "15" { a=1 } \ + NR == 3 && $2 == "2" && $5 == "build" && $6 == "error" && \ + $7 == "setup" && $8 == "-4" && $9 == "0" { q=1 } \ + NR == 4 && $1 == "end" && $2 == "2" && $3 == "2" { e=1 } \ + END { exit !(h && a && q && e) }' "$interruptstream" \ + || fail "interrupted coordinator terminal accounting" +cleanup_capture "$interruptstream" + +# Directive parsing and corpus pinning happen before filtering or execution. +malformed=$work/malformed +cp -R "$base" "$malformed" +replace_directive "$malformed/test/wcc/data/runww_dup_main_reject/case.ww" \ + '//ww:run-exit 01' +if "$tool" -root "$malformed" -list > "$work/malformed.out" \ + 2> "$work/malformed.err"; then + fail "malformed directive accepted" +fi +[ ! -s "$work/malformed.out" ] || fail "malformed directive published output" + +stagemalformed=$work/stage-malformed +cp -R "$base" "$stagemalformed" +stagemalformedcase=$stagemalformed/test/wcc/data/r828_arrlit_ret_overlong/case.ww +stagemalformedn=0 +for directive in \ + '//ww:error c "" ww "WW fragment"' \ + '//ww:error c "C fragment" ww ""' \ + '//ww:error c "C fragment" c "WW fragment"' \ + '//ww:error c "C fragment" ww "WW fragment" trailing' +do + stagemalformedn=$((stagemalformedn + 1)) + replace_directive "$stagemalformedcase" "$directive" + out=$work/stage-malformed-$stagemalformedn.out + err=$work/stage-malformed-$stagemalformedn.err + if "$tool" -root "$stagemalformed" -list > "$out" 2> "$err"; then + fail "malformed stage-specific directive accepted" + fi + [ ! -s "$out" ] \ + || fail "malformed stage-specific directive published output" + grep -F 'malformed stage-specific error directive' "$err" > /dev/null \ + || fail "wrong malformed stage-specific diagnostic" +done + +countdrift=$work/count-drift +cp -R "$base" "$countdrift" +find "$countdrift/test/wcc/data/a2s_mismatch_callarg" -depth -delete +if "$tool" -root "$countdrift" -list > "$work/count.out" \ + 2> "$work/count.err"; then + fail "corpus count drift accepted" +fi +[ ! -s "$work/count.out" ] || fail "count drift published output" + +hashdrift=$work/hash-drift +cp -R "$base" "$hashdrift" +mv "$hashdrift/test/wcc/data/a2s_mismatch_callarg" \ + "$hashdrift/test/wcc/data/a2s_mismatch_callarg_changed" +if "$tool" -root "$hashdrift" -list > "$work/hash.out" \ + 2> "$work/hash.err"; then + fail "corpus hash drift accepted" +fi +[ ! -s "$work/hash.out" ] || fail "hash drift published output" + +# A terminal-stream sink failure must force a nonzero producer status. +if "$tool" -root "$base" -run 'compiler.runww_dup_main_reject/c' -j 1 \ + > /dev/full 2> "$work/full.err"; then + fail "result publication failure returned success" +fi +publication=$(sed -n 's/^wwfixture: .*; capture: \(\/tmp\/[0-9a-f][0-9a-f]*\)$/\1/p' \ + "$work/full.err") +[ -n "$publication" ] || fail "publication failure capture was not retained" +[ -f "$publication/publication.txt" ] \ + || fail "publication failure diagnostic was not retained" +cleanup_run "$publication" + +# Strict decoder: start with an independently produced, valid two-cell stream. +good=$work/good.tab +pattern=compiler.runww_dup_main_reject +"$tool" -root "$base" -run "$pattern" -j 1 > "$good" +"$tool" validate -root "$base" -run "$pattern" "$good" + +bad=$work/bad-no-header.tab +sed '1d' "$good" > "$bad" +expect_invalid "$base" "$pattern" "$bad" + +bad=$work/bad-version.tab +awk -F '\t' 'BEGIN { OFS="\t" } NR == 1 { $2=2 } { print }' "$good" > "$bad" +expect_invalid "$base" "$pattern" "$bad" + +bad=$work/bad-corpus.tab +awk -F '\t' 'BEGIN { OFS="\t" } NR == 1 { $4=105 } { print }' "$good" > "$bad" +expect_invalid "$base" "$pattern" "$bad" + +bad=$work/bad-missing-ordinal.tab +sed '2d' "$good" > "$bad" +expect_invalid "$base" "$pattern" "$bad" + +bad=$work/bad-duplicate-ordinal.tab +awk 'NR == 2 { print; print; next } { print }' "$good" > "$bad" +expect_invalid "$base" "$pattern" "$bad" + +bad=$work/bad-identity.tab +awk -F '\t' 'BEGIN { OFS="\t" } NR == 2 { $3="compiler.not_selected" } \ + { print }' "$good" > "$bad" +expect_invalid "$base" "$pattern" "$bad" + +bad=$work/bad-stage.tab +awk -F '\t' 'BEGIN { OFS="\t" } NR == 2 { $4="ww" } { print }' \ + "$good" > "$bad" +expect_invalid "$base" "$pattern" "$bad" + +bad=$work/bad-field-count.tab +awk -F '\t' 'BEGIN { OFS="\t" } NR == 2 { print $0, "extra"; next } \ + { print }' "$good" > "$bad" +expect_invalid "$base" "$pattern" "$bad" + +bad=$work/bad-enum.tab +awk -F '\t' 'BEGIN { OFS="\t" } NR == 2 { $6="bogus" } { print }' \ + "$good" > "$bad" +expect_invalid "$base" "$pattern" "$bad" + +bad=$work/bad-contradiction.tab +awk -F '\t' 'BEGIN { OFS="\t" } NR == 2 { $8=0 } { print }' "$good" > "$bad" +expect_invalid "$base" "$pattern" "$bad" + +bad=$work/bad-incomplete-line.tab +bytes=$(wc -c < "$good") +dd if="$good" of="$bad" bs=1 count=$((bytes - 1)) 2> /dev/null +expect_invalid "$base" "$pattern" "$bad" + +bad=$work/bad-missing-footer.tab +sed '$d' "$good" > "$bad" +expect_invalid "$base" "$pattern" "$bad" + +bad=$work/bad-footer.tab +awk -F '\t' 'BEGIN { OFS="\t" } $1 == "end" { $3=1 } { print }' \ + "$good" > "$bad" +expect_invalid "$base" "$pattern" "$bad" + +bad=$work/bad-trailing.tab +cp "$good" "$bad" +printf '%s\n' 'case 3 compiler.trailing c compile pass exit 1 0 -' \ + >> "$bad" +expect_invalid "$base" "$pattern" "$bad" + +echo "wwfixture integration: PASS"