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 writeexecutable(path: str, content: str) void = { let fd: i32 = os.open(path, os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 448i32); 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 `/.{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; }; // Expected semantic identity for a canonical absolute directory that is not // representable below an active source root. Keep this independent observer // byte-for-byte aligned with the documented driver protocol. export fn localpackageidentity(dir: str, leaf: str) str = { let out: []u8 = alloc([], (dir.len * 4 + leaf.len + 16): u64)!; let prefix: str = "__wwlocal.p"; let i: i32 = 0; for (i < prefix.len) { append(out, prefix[i]); i += 1; }; let hex: str = "0123456789abcdef"; i = 0; for (i < dir.len) { let c: u8 = dir[i]; if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { append(out, c); } else if (c == '_') { append(out, '_'); append(out, 'u'); } else if (c == '/') { append(out, '_'); append(out, 's'); } else { let high: i32 = (c / 16u8): i32; let low: i32 = (c % 16u8): i32; append(out, '_'); append(out, 'x'); append(out, hex[high]); append(out, hex[low]); }; i += 1; }; append(out, '.'); i = 0; for (i < leaf.len) { append(out, leaf[i]); i += 1; }; return strings.frombytes(out); }; // 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; };