Every section banner dies (103 -> 0) across test/lang, the observer suites, the C carriers, and the five comment-heavy corpus fixtures; banner provenance (#N cites, carrier numbers, repair-cluster labels) folded into headers or adjacent WHY comments. Narration deleted; row provenance, ref cites, divergence pins, and layout contracts kept (fwd-ref decl-order guards and bootstrap-gate corpus rationale restored where the sweep over-cut). Comment-only proven: all 3742 wwbuild workdir .s byte-identical before/after; test-commit and test-byteid (161 lang + 1399 data, 0 pinned-divergent) green.
272 lines
7.2 KiB
Plaintext
272 lines
7.2 KiB
Plaintext
package testenv;
|
|
|
|
// A utility surface lifted from the proven test/package/package_test.ww
|
|
// idiom — not a framework: callers own discovery, assertions, and
|
|
// verdicts.
|
|
|
|
import endian;
|
|
import os;
|
|
import os.exec;
|
|
import strings;
|
|
import temp;
|
|
import time;
|
|
|
|
export type commandout = struct {
|
|
termination: exec.termination,
|
|
code: i32,
|
|
stdout: str,
|
|
stderr: str,
|
|
};
|
|
|
|
fn envrequired(name: str) str = {
|
|
match (os.getenv(name)) {
|
|
case let value: str => {
|
|
assert(value.len != 0);
|
|
return strings.dup(value);
|
|
};
|
|
case void => abort("missing test environment");
|
|
};
|
|
};
|
|
|
|
// The repository root, provided by the Make target as WW_TEST_REPO.
|
|
export fn repo() str = { return envrequired("WW_TEST_REPO"); };
|
|
|
|
export fn driver(name: str) str = {
|
|
return strings.concat(repo(), "/out/bin/", name);
|
|
};
|
|
|
|
export fn fresh() str = { return strings.dup(temp.dir()); };
|
|
|
|
export fn readfile(path: str) str = {
|
|
let fd: i32 = os.open(path, os.flag.RDONLY, 0i32);
|
|
assert(fd >= 0);
|
|
let sr: (i64 | os.oserror) = os.filesize(fd);
|
|
let n: i64 = -1i64;
|
|
match (sr) {
|
|
case let v: i64 => n = v;
|
|
case let e: os.oserror => abort("filesize failed");
|
|
};
|
|
assert(n >= 0i64);
|
|
let b: []u8 = alloc([], (n + 1i64): u64)!;
|
|
b.len = (n + 1i64): i32;
|
|
let rr: (i64 | os.oserror) = os.readall(fd, b.ptr, n: u64);
|
|
os.close(fd);
|
|
let got: i64 = -1i64;
|
|
match (rr) {
|
|
case let v: i64 => got = v;
|
|
case let e: os.oserror => abort("read failed");
|
|
};
|
|
assert(got == n);
|
|
let ni: i32 = n: i32;
|
|
b[ni] = 0u8;
|
|
let out: str;
|
|
out.ptr = b.ptr;
|
|
out.len = n: i32;
|
|
return out;
|
|
};
|
|
|
|
export fn writefile(path: str, content: str) void = {
|
|
let fd: i32 = os.open(path,
|
|
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384i32);
|
|
assert(fd >= 0);
|
|
match (os.writeall(fd, content.ptr, content.len: u64)) {
|
|
case let n: i64 => assert(n == content.len: i64);
|
|
case let e: os.oserror => abort("write failed");
|
|
};
|
|
assert(os.close(fd) == 0);
|
|
};
|
|
|
|
export fn exists(path: str) bool = {
|
|
return os.access(path, 0i32) == 0;
|
|
};
|
|
|
|
export fn isdir(path: str) bool = {
|
|
let st: os.filestat;
|
|
match (os.stat(&st, path)) {
|
|
case void => {
|
|
return ((st.mode: u32) & 61440u32) == (os.mode.DIR: u32);
|
|
};
|
|
case let e: os.oserror => return false;
|
|
};
|
|
};
|
|
|
|
// Run argv[0] with `dir` as the working directory, capturing stdout and
|
|
// stderr into `<root>/<name>.{stdout,stderr}`. Launch and cleanup
|
|
// failures abort; termination and exit code are the caller's verdict.
|
|
export fn runcommand(dir: str, root: str, name: str, argv: []str,
|
|
lifetime: time.duration, out: *commandout) void = {
|
|
runcommandenv(dir, root, name, argv, os.getenvs(), lifetime, out);
|
|
};
|
|
|
|
// Explicit-env variant of [[runcommand]] for arranger observers that
|
|
// must control the child's environment per row (exec.command.env is
|
|
// caller-built; ww ships no setenv primitive, deliberately).
|
|
export fn runcommandenv(dir: str, root: str, name: str, argv: []str,
|
|
env: []str, lifetime: time.duration, out: *commandout) void = {
|
|
let c: exec.command;
|
|
c.path = argv[0];
|
|
c.argv = argv;
|
|
c.env = env;
|
|
c.dir = dir;
|
|
c.stdoutpath = strings.concat(root, "/", name, ".stdout");
|
|
c.stderrpath = strings.concat(root, "/", name, ".stderr");
|
|
c.deadline = time.add(time.now(time.clock.monotonic), lifetime);
|
|
c.grace = (100i64 * (time.millisecond: i64)): time.duration;
|
|
let r: exec.result;
|
|
exec.run(&c, &r);
|
|
assert(r.errno == 0 && r.cleanuperrno == 0);
|
|
out.termination = r.termination;
|
|
out.code = r.code;
|
|
out.stdout = readfile(c.stdoutpath);
|
|
out.stderr = readfile(c.stderrpath);
|
|
};
|
|
|
|
export fn clean(root: str) void = {
|
|
let av: []str = ["/bin/rm", "-rf", "--", root];
|
|
let c: exec.command;
|
|
c.path = av[0];
|
|
c.argv = av;
|
|
c.env = os.getenvs();
|
|
c.dir = "/";
|
|
c.stdoutpath = strings.concat(root, ".cleanup.stdout");
|
|
c.stderrpath = strings.concat(root, ".cleanup.stderr");
|
|
c.deadline = time.add(time.now(time.clock.monotonic), time.second);
|
|
c.grace = (50i64 * (time.millisecond: i64)): time.duration;
|
|
let r: exec.result;
|
|
exec.run(&c, &r);
|
|
assert(r.termination == exec.termination.EXIT && r.code == 0);
|
|
assert(os.remove(c.stdoutpath) == 0);
|
|
assert(os.remove(c.stderrpath) == 0);
|
|
};
|
|
|
|
export fn pos(haystack: str, needle: str) i32 = {
|
|
match (strings.index(haystack, needle)) {
|
|
case let n: i32 => return n;
|
|
case void => return -1;
|
|
};
|
|
};
|
|
|
|
export fn has(haystack: str, needle: str) bool = {
|
|
return pos(haystack, needle) >= 0;
|
|
};
|
|
|
|
export fn same(a: str, b: str) bool = {
|
|
if (a.len != b.len) { return false; };
|
|
let i: i32 = 0;
|
|
for (i < a.len) {
|
|
if (a[i] != b[i]) { return false; };
|
|
i += 1;
|
|
};
|
|
return true;
|
|
};
|
|
|
|
// The ELF observers under test/object read header fields straight out
|
|
// of readfile's byte-carrying str; these getters reinterpret it as
|
|
// bytes and route through endian's canonical little-endian readers
|
|
// instead of re-deriving the bit math. u64 uniformly so one call
|
|
// shape covers u16/u32/u64 fields; callers narrow for offset math.
|
|
export fn leu16(s: str, off: i32) u64 = {
|
|
let b: []u8;
|
|
b.ptr = &s.ptr[off];
|
|
b.len = 2;
|
|
b.cap = 2;
|
|
return endian.legetu16(b): u64;
|
|
};
|
|
|
|
export fn leu32(s: str, off: i32) u64 = {
|
|
let b: []u8;
|
|
b.ptr = &s.ptr[off];
|
|
b.len = 4;
|
|
b.cap = 4;
|
|
return endian.legetu32(b): u64;
|
|
};
|
|
|
|
export fn leu64(s: str, off: i32) u64 = {
|
|
let b: []u8;
|
|
b.ptr = &s.ptr[off];
|
|
b.len = 8;
|
|
b.cap = 8;
|
|
return endian.legetu64(b);
|
|
};
|
|
|
|
export fn occurrences(haystack: str, needle: str) i32 = {
|
|
if (needle.len == 0 || haystack.len < needle.len) { return 0; };
|
|
let count: i32 = 0;
|
|
let i: i32 = 0;
|
|
for (i + needle.len <= haystack.len) {
|
|
let j: i32 = 0;
|
|
let hit: bool = true;
|
|
for (j < needle.len) {
|
|
if (haystack[i + j] != needle[j]) { hit = false; j = needle.len; }
|
|
else { j += 1; };
|
|
};
|
|
if (hit) { count += 1; };
|
|
i += 1;
|
|
};
|
|
return count;
|
|
};
|
|
|
|
// Byte-lexicographic order, so directory listings sort identically on
|
|
// every host regardless of locale.
|
|
export fn lexless(a: str, b: str) bool = {
|
|
let n: i32 = a.len;
|
|
if (b.len < n) { n = b.len; };
|
|
let i: i32 = 0;
|
|
for (i < n) {
|
|
if (a[i] != b[i]) { return a[i] < b[i]; };
|
|
i += 1;
|
|
};
|
|
return a.len < b.len;
|
|
};
|
|
|
|
def DIRBUF: i32 = 16384;
|
|
|
|
// Entry names of `path` (excluding . and ..), byte-sorted. Aborts on an
|
|
// unreadable directory or a malformed dirent stream.
|
|
export fn listdir(path: str) []str = {
|
|
let names: []str = alloc([], 16u64)!;
|
|
let fd: i32 = os.open(path, os.flag.RDONLY, 0i32);
|
|
assert(fd >= 0);
|
|
let buf: []u8 = alloc([], DIRBUF: u64)!;
|
|
buf.len = DIRBUF;
|
|
let n: i64 = os.getdents64(fd, buf.ptr, buf.len: u64);
|
|
for (n > 0i64) {
|
|
let off: i32 = 0;
|
|
for (off < n: i32) {
|
|
// linux_dirent64: d_reclen at 16..17, name at 19.
|
|
let reclen: i32 = (buf[off + 16]: i32)
|
|
+ ((buf[off + 17]: i32) * 256);
|
|
assert(reclen >= 20 && off + reclen <= n: i32);
|
|
let nb: []u8 = alloc([], 256u64)!;
|
|
let k: i32 = 0;
|
|
for (buf[off + 19 + k] != 0u8) {
|
|
append(nb, buf[off + 19 + k]);
|
|
k += 1;
|
|
};
|
|
let name: str = strings.frombytes(nb);
|
|
if (!same(name, ".") && !same(name, "..")) {
|
|
append(names, name);
|
|
};
|
|
off += reclen;
|
|
};
|
|
n = os.getdents64(fd, buf.ptr, buf.len: u64);
|
|
};
|
|
assert(n == 0i64);
|
|
assert(os.close(fd) == 0);
|
|
// insertion sort; listings are small
|
|
let i: i32 = 1;
|
|
for (i < names.len) {
|
|
let v: str = names[i];
|
|
let j: i32 = i;
|
|
for (j > 0) {
|
|
if (lexless(v, names[j - 1])) {
|
|
names[j] = names[j - 1];
|
|
j -= 1;
|
|
} else { break; };
|
|
};
|
|
names[j] = v;
|
|
i += 1;
|
|
};
|
|
return names;
|
|
};
|