From 7a2c21acfb405608a93fd9bf440de4397c9165c1 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 7 Aug 2026 23:01:03 +0900 Subject: [PATCH] wwtest: native package-test coordinator Single-directory package-test planner: discovers *.ww, groups white-box and external _test sources, composes the combined test package, builds it through the sibling ww driver, and runs it with the runtime's -package/-list/-timeout-ms contract. test/package holds its E2E corpus and the manual runtime fixture set. --- cmd/wwtest/wwtest.ww | 14 + internal/wwpackage/package.ww | 838 ++++++++++++++++++ test/package/bad_external/bad_external.ww | 3 + test/package/bad_external/wrong_test.ww | 3 + .../assert_failure/assert_failure_test.ww | 5 + .../cases/descendant/descendant_test.ww | 25 + .../cases/multi_fail/multi_fail_test.ww | 5 + test/package/cases/nonzero/nonzero_test.ww | 5 + test/package/cases/pass/pass_test.ww | 18 + .../package/cases/premature/premature_test.ww | 5 + test/package/cases/signal/signal_test.ww | 7 + test/package/cases/timeout/timeout_test.ww | 16 + test/package/dependency/dep/contest.ww | 4 + test/package/dependency/dep/dep.ww | 3 + test/package/dependency/dep/deptest.ww | 7 + test/package/dependency/root/root.ww | 5 + test/package/dependency/root/root_test.ww | 5 + test/package/no_tests/no_tests.ww | 3 + test/package/package_test.ww | 444 ++++++++++ test/package/routing/a_helper_test.ww | 5 + test/package/routing/b_white_test.ww | 5 + test/package/routing/external_test.ww | 7 + test/package/routing/routing.ww | 3 + test/package/routing/routing_test.ww | 6 + test/package/runtime/README.md | 45 + .../runtime/escaped_descendant_test.ww | 26 + .../runtime/expected_abort_return_test.ww | 7 + test/package/runtime/fail_test.ww | 5 + .../runtime/lingering_descendant_test.ww | 26 + test/package/runtime/multiple_fail_test.ww | 9 + test/package/runtime/premature_exit_test.ww | 7 + test/package/runtime/signal_test.ww | 7 + test/package/runtime/success_test.ww | 20 + test/package/runtime/timeout_test.ww | 5 + 34 files changed, 1598 insertions(+) create mode 100644 cmd/wwtest/wwtest.ww create mode 100644 internal/wwpackage/package.ww create mode 100644 test/package/bad_external/bad_external.ww create mode 100644 test/package/bad_external/wrong_test.ww create mode 100644 test/package/cases/assert_failure/assert_failure_test.ww create mode 100644 test/package/cases/descendant/descendant_test.ww create mode 100644 test/package/cases/multi_fail/multi_fail_test.ww create mode 100644 test/package/cases/nonzero/nonzero_test.ww create mode 100644 test/package/cases/pass/pass_test.ww create mode 100644 test/package/cases/premature/premature_test.ww create mode 100644 test/package/cases/signal/signal_test.ww create mode 100644 test/package/cases/timeout/timeout_test.ww create mode 100644 test/package/dependency/dep/contest.ww create mode 100644 test/package/dependency/dep/dep.ww create mode 100644 test/package/dependency/dep/deptest.ww create mode 100644 test/package/dependency/root/root.ww create mode 100644 test/package/dependency/root/root_test.ww create mode 100644 test/package/no_tests/no_tests.ww create mode 100644 test/package/package_test.ww create mode 100644 test/package/routing/a_helper_test.ww create mode 100644 test/package/routing/b_white_test.ww create mode 100644 test/package/routing/external_test.ww create mode 100644 test/package/routing/routing.ww create mode 100644 test/package/routing/routing_test.ww create mode 100644 test/package/runtime/README.md create mode 100644 test/package/runtime/escaped_descendant_test.ww create mode 100644 test/package/runtime/expected_abort_return_test.ww create mode 100644 test/package/runtime/fail_test.ww create mode 100644 test/package/runtime/lingering_descendant_test.ww create mode 100644 test/package/runtime/multiple_fail_test.ww create mode 100644 test/package/runtime/premature_exit_test.ww create mode 100644 test/package/runtime/signal_test.ww create mode 100644 test/package/runtime/success_test.ww create mode 100644 test/package/runtime/timeout_test.ww diff --git a/cmd/wwtest/wwtest.ww b/cmd/wwtest/wwtest.ww new file mode 100644 index 00000000..da18a5ed --- /dev/null +++ b/cmd/wwtest/wwtest.ww @@ -0,0 +1,14 @@ +package main; + +import os; +import wwpackage; + +export fn main() int = { + let args: []str = os.args(); + if (args.len < 2 || args[1] != "package") { + let usage: str = "usage: wwtest package [options] [DIR]\n"; + os.write(os.STDERR_FILENO, usage.ptr, usage.len: u64); + return 2; + }; + return wwpackage.packagecommand(args[2:args.len]); +}; diff --git a/internal/wwpackage/package.ww b/internal/wwpackage/package.ww new file mode 100644 index 00000000..c0ed54cc --- /dev/null +++ b/internal/wwpackage/package.ww @@ -0,0 +1,838 @@ +package wwpackage; + +import os; +import os.exec; +import strconv; +import strings; +import temp; +import time; + +type pkgsource = struct { + path: str, + dir: str, + pkg: str, + test: bool, + attest: bool, +}; + +type pkgfolder = struct { + path: str, + start: i32, + end: i32, + prodpkg: str, + hastests: bool, +}; + +type pkggroup = struct { + dir: str, + pkg: str, + start: i32, + end: i32, + prodpkg: str, + external: bool, + root: str, + combined: str, + bin: str, + buildout: str, + builderr: str, + runout: str, + runerr: str, +}; + +type pkgdiscover = struct { + paths: []str, + errors: i32, +}; + +// Preserve the caller's toolchain environment while pinning the locale and +// temporary directory used by the current package group. +fn toolenv(tmpdir: str) []str = { + let inherited: []str = os.getenvs(); + let env: []str = alloc([], (inherited.len + 2): u64)!; + let i: i32 = 0; + for (i < inherited.len) { + if (!strings.hasprefix(inherited[i], "TMPDIR=") + && !strings.hasprefix(inherited[i], "LC_ALL=")) { + append(env, inherited[i]); + }; + i += 1; + }; + append(env, "LC_ALL=C"); + append(env, strings.concat("TMPDIR=", tmpdir)); + return env; +}; + +fn pkgwrite(fd: i32, s: str) bool = { + let r: (i64 | os.oserror) = os.writeall(fd, s.ptr, s.len: u64); + match (r) { + case let n: i64 => return n == s.len: i64; + case let e: os.oserror => return false; + }; +}; + +fn pkgput(fd: i32, s: str) void = { + pkgwrite(fd, s); +}; + +fn pkgputln(fd: i32, s: str) void = { + pkgput(fd, s); + pkgput(fd, "\n"); +}; + +fn pkgfailpath(path: str, reason: str) void = { + pkgput(os.STDERR_FILENO, "wwtest package: "); + pkgput(os.STDERR_FILENO, path); + pkgput(os.STDERR_FILENO, ": "); + pkgputln(os.STDERR_FILENO, reason); +}; + +fn pkgusage() void = { + let s: str = strings.concat( + "usage: wwtest package [-c] [-list] [-j N] [-I DIR] [-run|-filter GLOB] [-timeout-ms=N] [DIR] [-- GLOB ...]\n", + " *_test.ww is canonical; noncanonical files require an actual @test declaration\n", + " -c retains the compiled package binaries; -j is reserved by sequential v1\n"); + pkgput(os.STDERR_FILENO, s); +}; + +fn pkgread(path: str, out: *str) bool = { + let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + if (fd < 0) { return false; }; + 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 => { os.close(fd); return false; }; + }; + if (n < 0i64) { os.close(fd); return false; }; + 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 => return false; + }; + if (got != n) { return false; }; + let ni: i32 = n: i32; + b[ni] = 0u8; + out.ptr = b.ptr; + out.len = n: i32; + return true; +}; + +fn pkgidentfirst(c: u8) bool = { + if (c >= 'a' && c <= 'z') { return true; }; + if (c >= 'A' && c <= 'Z') { return true; }; + return c == '_'; +}; + +fn pkgident(c: u8) bool = { + if (pkgidentfirst(c)) { return true; }; + return c >= '0' && c <= '9'; +}; + +fn pkgskipspace(src: str, start: i32) i32 = { + let i: i32 = start; + for (i < src.len) { + let c: u8 = src[i]; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + i += 1; + continue; + }; + if (c == '/' && i + 1 < src.len && src[i + 1] == '/') { + i += 2; + for (i < src.len && src[i] != '\n') { i += 1; }; + continue; + }; + break; + }; + return i; +}; + +fn pkgclause(src: str, out: *str) bool = { + let i: i32 = pkgskipspace(src, 0); + let word: str = "package"; + if (i + word.len >= src.len) { return false; }; + let j: i32 = 0; + for (j < word.len) { + if (src[i + j] != word[j]) { return false; }; + j += 1; + }; + i += word.len; + if (i >= src.len) { return false; }; + if (!(src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r')) { + return false; + }; + i = pkgskipspace(src, i); + if (i >= src.len || !pkgidentfirst(src[i])) { return false; }; + let begin: i32 = i; + i += 1; + for (i < src.len && pkgident(src[i])) { i += 1; }; + let end: i32 = i; + i = pkgskipspace(src, i); + if (i >= src.len || src[i] != ';') { return false; }; + out.ptr = src.ptr + (begin: u64); + out.len = end - begin; + return true; +}; + +fn pkgattest(src: str) bool = { + let i: i32 = 0; + for (i < src.len) { + for (i < src.len && (src[i] == ' ' || src[i] == '\t' || + src[i] == '\r')) { i += 1; }; + if (i + 5 < src.len && src[i] == '@' && src[i + 1] == 't' && + src[i + 2] == 'e' && src[i + 3] == 's' && src[i + 4] == 't' && + (src[i + 5] == ' ' || src[i + 5] == '\t')) { + return true; + }; + for (i < src.len && src[i] != '\n') { i += 1; }; + if (i < src.len) { i += 1; }; + }; + return false; +}; + +fn pkgdirname(path: str) str = { + let i: i32 = path.len - 1; + for (i >= 0) { + if (path[i] == '/') { + if (i == 0) { return "/"; }; + let r: str; + r.ptr = path.ptr; + r.len = i; + return r; + }; + i -= 1; + }; + return "."; +}; + +fn pkgbase(path: str) str = { + let i: i32 = path.len - 1; + for (i >= 0) { + if (path[i] == '/') { + let r: str; + r.ptr = path.ptr + ((i + 1): u64); + r.len = path.len - i - 1; + return r; + }; + i -= 1; + }; + return path; +}; + +fn pkgmodeis(m: os.mode, want: os.mode) bool = { + return (((m: u32) & 61440u32) == (want: u32)); +}; + +fn pkgisdir(path: str) bool = { + let fi: os.filestat; + match (os.lstat(&fi, path)) { + case void => return pkgmodeis(fi.mode, os.mode.DIR); + case let e: os.oserror => return false; + }; +}; + +fn pkgkeepfile(name: str) bool = { + if (!strings.hassuffix(name, ".ww")) { return false; }; + if (strings.hassuffix(name, ".combined.ww")) { return false; }; + return true; +}; + +// The first vertical slice owns exactly one explicit package directory. It +// never recurses, and lstat is used both for the root and each candidate so a +// symlink cannot be used to cross that boundary (including DT_UNKNOWN files). +fn pkgdiscoverdir(path: str, st: *pkgdiscover) void = { + let rootstat: os.filestat; + match (os.lstat(&rootstat, path)) { + case void => void; + case let e: os.oserror => { + pkgfailpath(path, "cannot stat package directory"); + st.errors += 1; + return; + }; + }; + if (pkgmodeis(rootstat.mode, os.mode.LINK)) { + pkgfailpath(path, "symlink traversal is not allowed"); + st.errors += 1; + return; + }; + if (!pkgmodeis(rootstat.mode, os.mode.DIR)) { + pkgfailpath(path, "expected one package directory"); + st.errors += 1; + return; + }; + let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + if (fd < 0) { + pkgfailpath(path, "cannot open directory"); + st.errors += 1; + return; + }; + let buf: []u8 = alloc([], 8192u64)!; + buf.len = 8192; + let n: i64 = os.getdents64(fd, buf.ptr, 8192u64); + for (n > 0i64) { + let off: u64 = 0u64; + for (off < n: u64) { + let reclen: u64 = (buf[off + 16u64]: u64) + + (buf[off + 17u64]: u64) * 256u64; + if (reclen < 20u64 || off + reclen > n: u64) { + pkgfailpath(path, "malformed directory entry"); + st.errors += 1; + off = n: u64; + continue; + }; + let np: *u8 = buf.ptr + off + 19u64; + let nl: i32 = 0; + for (nl: u64 + 19u64 < reclen && np[nl] != 0u8) { nl += 1; }; + let name: str; + name.ptr = np; + name.len = nl; + if (pkgkeepfile(name)) { + let child: str = strings.concat(path, "/", name); + let fi: os.filestat; + match (os.lstat(&fi, child)) { + case void => { + if (pkgmodeis(fi.mode, os.mode.LINK)) { + pkgfailpath(child, + "symlink source is not allowed"); + st.errors += 1; + } else if (pkgmodeis(fi.mode, os.mode.REG)) { + append(st.paths, child); + }; + }; + case let e: os.oserror => { + pkgfailpath(child, "cannot stat source"); + st.errors += 1; + }; + }; + }; + off += reclen; + }; + n = os.getdents64(fd, buf.ptr, 8192u64); + }; + if (n < 0i64) { + pkgfailpath(path, "directory read failed"); + st.errors += 1; + }; + os.close(fd); +}; + +fn pkgsort(ss: []str) void = { + let i: i32 = 1; + for (i < ss.len) { + let j: i32 = i; + for (j > 0 && strings.compare(ss[j - 1], ss[j]) > 0) { + let t: str = ss[j]; + ss[j] = ss[j - 1]; + ss[j - 1] = t; + j -= 1; + }; + i += 1; + }; +}; + +fn pkgsortgroups(gs: []pkggroup) void = { + let i: i32 = 1; + for (i < gs.len) { + let j: i32 = i; + for (j > 0) { + let c: int = strings.compare(gs[j - 1].dir, gs[j].dir); + if (c == 0) { c = strings.compare(gs[j - 1].pkg, gs[j].pkg); }; + if (c <= 0) { break; }; + let t: pkggroup = gs[j]; + gs[j] = gs[j - 1]; + gs[j - 1] = t; + j -= 1; + }; + i += 1; + }; +}; + +fn pkgdedup(ss: []str) []str = { + if (ss.len == 0) { return ss; }; + let out: []str = alloc([], ss.len: u64)!; + let i: i32 = 0; + for (i < ss.len) { + if (i == 0 || strings.compare(ss[i - 1], ss[i]) != 0) { + append(out, ss[i]); + }; + i += 1; + }; + return out; +}; + +fn pkgparsedec(s: str, max: i64) bool = { + if (s.len == 0) { return false; }; + let i: i32 = 0; + let n: i64 = 0i64; + for (i < s.len) { + if (s[i] < '0' || s[i] > '9') { return false; }; + let digit: i64 = (s[i] - '0'): i64; + if (n > (max - digit) / 10i64) { return false; }; + n = n * 10i64 + digit; + i += 1; + }; + return n > 0i64; +}; + +fn pkgdefaultbuilder() str = { + let av: []str = os.args(); + if (av.len == 0 || av[0].len == 0) { return "ww"; }; + return strings.concat(pkgdirname(av[0]), "/ww"); +}; + +fn pkgmakedir(path: str) bool = { + return os.mkdir(path, 448i32) == 0; +}; + +fn pkgremoveall(path: str) bool = { + if (!pkgisdir(path)) { return os.remove(path) == 0; }; + let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + if (fd < 0) { return false; }; + let ok: bool = true; + let buf: []u8 = alloc([], 8192u64)!; + buf.len = 8192; + let n: i64 = os.getdents64(fd, buf.ptr, 8192u64); + for (n > 0i64) { + let off: u64 = 0u64; + for (off < n: u64) { + let reclen: u64 = (buf[off + 16u64]: u64) + + (buf[off + 17u64]: u64) * 256u64; + if (reclen < 20u64 || off + reclen > n: u64) { + ok = false; + off = n: u64; + continue; + }; + let np: *u8 = buf.ptr + off + 19u64; + let nl: i32 = 0; + for (nl: u64 + 19u64 < reclen && np[nl] != 0u8) { nl += 1; }; + let name: str; + name.ptr = np; + name.len = nl; + if (!(strings.compare(name, ".") == 0 || strings.compare(name, "..") == 0)) { + let child: str = strings.concat(path, "/", name); + let dtype: u8 = buf[off + 18u64]; + if (dtype == 4u8 || (dtype == 0u8 && pkgisdir(child))) { + if (!pkgremoveall(child)) { ok = false; }; + } else if (os.remove(child) != 0) { ok = false; }; + }; + off += reclen; + }; + n = os.getdents64(fd, buf.ptr, 8192u64); + }; + if (n < 0i64) { ok = false; }; + os.close(fd); + if (os.rmdir(path) != 0) { ok = false; }; + return ok; +}; + +fn pkgemitfile(path: str, fd: i32) bool = { + let s: str; + if (!pkgread(path, &s)) { return false; }; + if (s.len != 0) { + pkgput(fd, s); + if (s[s.len - 1] != '\n') { pkgput(fd, "\n"); }; + }; + return true; +}; + +fn pkgcombined(g: *pkggroup, srcs: []pkgsource) bool = { + let fd: i32 = os.open(g.combined, + os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384i32); + if (fd < 0) { return false; }; + let ok: bool = true; + if (!g.external) { + let i: i32 = g.start; + for (i < g.end) { + if (!srcs[i].test) { + if (!pkgwrite(fd, "//ww:module-reset\n")) { ok = false; }; + let s: str; + if (!pkgread(srcs[i].path, &s) || !pkgwrite(fd, s) + || !pkgwrite(fd, "\n")) { ok = false; }; + }; + i += 1; + }; + }; + let i: i32 = g.start; + for (i < g.end) { + if (srcs[i].test && strings.compare(srcs[i].pkg, g.pkg) == 0) { + if (!pkgwrite(fd, "//ww:module-reset\n")) { ok = false; }; + let s: str; + if (!pkgread(srcs[i].path, &s) || !pkgwrite(fd, s) + || !pkgwrite(fd, "\n")) { ok = false; }; + }; + i += 1; + }; + os.close(fd); + return ok; +}; + +fn pkgsetpaths(g: *pkggroup, root: str, index: i32, + compileonly: bool) bool = { + let num: str = strconv.i32tos(index, strconv.base.DEC); + g.root = strings.concat(root, "/group-", num); + if (!pkgmakedir(g.root)) { return false; }; + g.combined = strings.concat(g.root, "/package.ww"); + if (compileonly) { + g.bin = strings.concat(g.dir, "/", g.pkg, ".test"); + } else { + g.bin = strings.concat(g.root, "/package.test"); + }; + g.buildout = strings.concat(g.root, "/build.stdout"); + g.builderr = strings.concat(g.root, "/build.stderr"); + g.runout = strings.concat(g.root, "/test.stdout"); + g.runerr = strings.concat(g.root, "/test.stderr"); + return true; +}; + +fn pkglabel(g: *pkggroup) void = { + pkgput(os.STDOUT_FILENO, g.dir); + pkgput(os.STDOUT_FILENO, " ["); + pkgput(os.STDOUT_FILENO, g.pkg); + if (g.external) { pkgput(os.STDOUT_FILENO, ", external"); } + else { pkgput(os.STDOUT_FILENO, ", same-package"); }; + pkgput(os.STDOUT_FILENO, "]"); +}; + +fn pkgreportcommand(kind: str, g: *pkggroup, r: *exec.result) void = { + pkgput(os.STDERR_FILENO, "FAIL "); + pkgput(os.STDERR_FILENO, g.dir); + pkgput(os.STDERR_FILENO, " ["); + pkgput(os.STDERR_FILENO, g.pkg); + pkgput(os.STDERR_FILENO, "] ("); + pkgput(os.STDERR_FILENO, kind); + if (r.errno != 0 || r.cleanuperrno != 0 + || r.termination == exec.termination.ERROR) { + pkgput(os.STDERR_FILENO, " harness error "); + let code: i32 = r.errno; + if (code == 0) { code = r.cleanuperrno; }; + pkgput(os.STDERR_FILENO, strconv.i32tos(code, strconv.base.DEC)); + } else if (r.termination == exec.termination.SIGNAL) { + pkgput(os.STDERR_FILENO, " signal "); + pkgput(os.STDERR_FILENO, strconv.i32tos(r.code, strconv.base.DEC)); + } else { + pkgput(os.STDERR_FILENO, " exit "); + pkgput(os.STDERR_FILENO, strconv.i32tos(r.code, strconv.base.DEC)); + }; + pkgputln(os.STDERR_FILENO, ")"); +}; + +fn pkgrungroup(g: *pkggroup, srcs: []pkgsource, builder: str, + filters: []str, includes: []str, timeoutarg: str, + list: bool, compileonly: bool) bool = { + if (!pkgcombined(g, srcs)) { + pkgfailpath(g.combined, "cannot compose package test source"); + return false; + }; + let ba: []str = alloc([], (9 + includes.len * 2): u64)!; + append(ba, builder); + append(ba, "test"); + append(ba, "-c"); + append(ba, "-o"); + append(ba, g.bin); + append(ba, "-I"); + append(ba, g.dir); + let ii: i32 = 0; + for (ii < includes.len) { + append(ba, "-I"); + append(ba, includes[ii]); + ii += 1; + }; + append(ba, g.combined); + let env: []str = toolenv(g.root); + let bcmd: exec.command; + bcmd.path = builder; + bcmd.argv = ba; + bcmd.env = env; + bcmd.dir = ""; + bcmd.stdoutpath = g.buildout; + bcmd.stderrpath = g.builderr; + bcmd.deadline.sec = 0i64; + bcmd.deadline.nsec = 0i64; + bcmd.grace = 0i64: time.duration; + let br: exec.result; + exec.run(&bcmd, &br); + let buildcaptures: bool = pkgemitfile(g.buildout, os.STDOUT_FILENO); + buildcaptures = pkgemitfile(g.builderr, os.STDERR_FILENO) && buildcaptures; + if (!buildcaptures) { + pkgfailpath(g.root, "cannot read build capture"); + return false; + }; + if (br.errno != 0 || br.cleanuperrno != 0 + || br.termination != exec.termination.EXIT || br.code != 0) { + pkgreportcommand("build", g, &br); + return false; + }; + if (compileonly) { + pkgput(os.STDOUT_FILENO, "built "); + pkglabel(g); + pkgput(os.STDOUT_FILENO, " -> "); + pkgputln(os.STDOUT_FILENO, g.bin); + return true; + }; + let ra: []str = alloc([], (filters.len + 4): u64)!; + append(ra, g.bin); + append(ra, strings.concat("-package=", g.pkg)); + if (list) { append(ra, "-list"); }; + if (timeoutarg.len != 0) { append(ra, timeoutarg); }; + let i: i32 = 0; + for (i < filters.len) { append(ra, filters[i]); i += 1; }; + let rcmd: exec.command; + rcmd.path = g.bin; + rcmd.argv = ra; + rcmd.env = env; + rcmd.dir = ""; + rcmd.stdoutpath = g.runout; + rcmd.stderrpath = g.runerr; + rcmd.deadline.sec = 0i64; + rcmd.deadline.nsec = 0i64; + rcmd.grace = 0i64: time.duration; + let rr: exec.result; + exec.run(&rcmd, &rr); + let runcaptures: bool = pkgemitfile(g.runout, os.STDOUT_FILENO); + runcaptures = pkgemitfile(g.runerr, os.STDERR_FILENO) && runcaptures; + if (!runcaptures) { + pkgfailpath(g.root, "cannot read test capture"); + return false; + }; + if (rr.errno != 0 || rr.cleanuperrno != 0 + || rr.termination != exec.termination.EXIT || rr.code != 0) { + pkgreportcommand("test", g, &rr); + return false; + }; + pkgput(os.STDOUT_FILENO, "ok "); + pkglabel(g); + pkgput(os.STDOUT_FILENO, "\n"); + return true; +}; + +export fn packagecommand(args: []str) int = { + let compileonly: bool = false; + let list: bool = false; + let sawj: bool = false; + let afterdash: bool = false; + let roots: []str = alloc([], 2u64)!; + let filters: []str = alloc([], (args.len + 1): u64)!; + let includes: []str = alloc([], (args.len + 1): u64)!; + let timeoutarg: str = ""; + let builder: str = pkgdefaultbuilder(); + let i: i32 = 0; + for (i < args.len) { + let a: str = args[i]; + if (afterdash) { append(filters, a); i += 1; continue; }; + if (strings.compare(a, "--") == 0) { afterdash = true; i += 1; continue; }; + if (strings.compare(a, "-c") == 0) { compileonly = true; i += 1; continue; }; + if (strings.compare(a, "-list") == 0) { list = true; i += 1; continue; }; + if (strings.compare(a, "-I") == 0) { + if (i + 1 >= args.len) { pkgusage(); return 2; }; + append(includes, args[i + 1]); + i += 2; + continue; + }; + if (strings.hasprefix(a, "-I") && a.len > 2) { + append(includes, a[2:a.len]); + i += 1; + continue; + }; + if (strings.hasprefix(a, "-timeout-ms=")) { + if (timeoutarg.len != 0 || a.len == 12 + || !pkgparsedec(a[12:a.len], 3600000i64)) { + pkgusage(); + return 2; + }; + timeoutarg = a; + i += 1; + continue; + }; + if (strings.compare(a, "-j") == 0) { + if (i + 1 >= args.len + || !pkgparsedec(args[i + 1], 2147483647i64)) { + pkgusage(); + return 2; + }; + sawj = true; + i += 2; + continue; + }; + if (strings.compare(a, "-filter") == 0 || strings.compare(a, "-run") == 0) { + if (i + 1 >= args.len) { pkgusage(); return 2; }; + append(filters, args[i + 1]); + i += 2; + continue; + }; + if (strings.compare(a, "--ww-driver") == 0) { + if (i + 1 >= args.len || args[i + 1].len == 0) { + pkgusage(); + return 2; + }; + builder = args[i + 1]; + i += 2; + continue; + }; + if (a.len != 0 && a[0] == '-') { pkgusage(); return 2; }; + append(roots, a); + i += 1; + }; + if (roots.len == 0) { append(roots, "."); }; + if (roots.len != 1) { + pkgputln(os.STDERR_FILENO, + "wwtest package: exactly one package directory is supported"); + return 2; + }; + if (compileonly && (list || filters.len != 0 || timeoutarg.len != 0)) { + pkgusage(); + return 2; + }; + if (sawj) { + pkgputln(os.STDERR_FILENO, + "wwtest package: -j is reserved; sequential package scheduling is active"); + }; + + let ds: pkgdiscover; + let discoveredpaths: []str = alloc([], 64u64)!; + ds.paths = discoveredpaths; + ds.errors = 0; + pkgdiscoverdir(roots[0], &ds); + if (ds.errors != 0) { return 1; }; + pkgsort(ds.paths); + if (ds.paths.len == 0) { + pkgfailpath(roots[0], "directory contains no WW package sources"); + return 1; + }; + + let srcs: []pkgsource = alloc([], ds.paths.len: u64)!; + i = 0; + for (i < ds.paths.len) { + let body: str; + let pn: str; + if (!pkgread(ds.paths[i], &body) || !pkgclause(body, &pn)) { + pkgfailpath(ds.paths[i], "invalid or missing package clause"); + return 1; + }; + let s: pkgsource; + s.path = ds.paths[i]; + s.dir = strings.dup(pkgdirname(ds.paths[i])); + s.pkg = strings.dup(pn); + s.attest = pkgattest(body); + s.test = strings.hassuffix(pkgbase(ds.paths[i]), "_test.ww") + || s.attest; + append(srcs, s); + i += 1; + }; + + let folders: []pkgfolder = alloc([], srcs.len: u64)!; + i = 0; + for (i < srcs.len) { + let f: pkgfolder; + f.path = srcs[i].dir; + f.start = i; + f.end = i; + f.prodpkg = ""; + f.hastests = false; + for (f.end < srcs.len && strings.compare(srcs[f.end].dir, f.path) == 0) { + if (srcs[f.end].test) { + if (srcs[f.end].attest) { f.hastests = true; }; + } else if (f.prodpkg.len == 0) { + f.prodpkg = srcs[f.end].pkg; + } else if (strings.compare(f.prodpkg, srcs[f.end].pkg) != 0) { + pkgfailpath(f.path, "production sources declare conflicting packages"); + return 1; + }; + f.end += 1; + }; + append(folders, f); + i = f.end; + }; + + let groups: []pkggroup = alloc([], srcs.len: u64)!; + i = 0; + for (i < folders.len) { + let f: pkgfolder = folders[i]; + if (!f.hastests) { + pkgput(os.STDOUT_FILENO, "? "); + pkgput(os.STDOUT_FILENO, f.path); + pkgputln(os.STDOUT_FILENO, " [no tests]"); + i += 1; + continue; + }; + let externalpkg: str = ""; + if (f.prodpkg.len != 0) { + externalpkg = strings.concat(f.prodpkg, "_test"); + }; + let standalonepkg: str = ""; + let j: i32 = f.start; + for (j < f.end) { + if (srcs[j].test) { + if (f.prodpkg.len != 0 + && strings.compare(srcs[j].pkg, f.prodpkg) != 0 + && strings.compare(srcs[j].pkg, externalpkg) != 0) { + pkgfailpath(srcs[j].path, + "test package must match production package or _test"); + return 1; + }; + if (f.prodpkg.len == 0) { + if (standalonepkg.len == 0) { + standalonepkg = srcs[j].pkg; + } else if (strings.compare(standalonepkg, + srcs[j].pkg) != 0) { + pkgfailpath(f.path, + "test-only directory declares conflicting packages"); + return 1; + }; + }; + if (!srcs[j].attest) { j += 1; continue; }; + let found: bool = false; + let k: i32 = 0; + for (k < groups.len) { + if (strings.compare(groups[k].dir, f.path) == 0 + && strings.compare(groups[k].pkg, srcs[j].pkg) == 0) { + found = true; + break; + }; + k += 1; + }; + if (!found) { + let g: pkggroup; + g.dir = f.path; + g.pkg = srcs[j].pkg; + g.start = f.start; + g.end = f.end; + g.prodpkg = f.prodpkg; + g.external = f.prodpkg.len != 0 + && strings.compare(f.prodpkg, g.pkg) != 0; + append(groups, g); + }; + }; + j += 1; + }; + i += 1; + }; + if (groups.len == 0) { return 0; }; + pkgsortgroups(groups); + + let borrowed: str = temp.dir(); + let tmproot: str = strings.dup(borrowed); + let failed: i32 = 0; + i = 0; + for (i < groups.len) { + if (!pkgsetpaths(&groups[i], tmproot, i, compileonly)) { + pkgfailpath(tmproot, "cannot create group temporary directory"); + failed += 1; + break; + }; + if (!pkgrungroup(&groups[i], srcs, builder, filters, includes, + timeoutarg, list, compileonly)) { + failed += 1; + }; + i += 1; + }; + if (!pkgremoveall(tmproot)) { + pkgput(os.STDERR_FILENO, "wwtest package: cleanup failed; retained "); + pkgputln(os.STDERR_FILENO, tmproot); + failed += 1; + }; + if (failed != 0) { return 1; }; + return 0; +}; diff --git a/test/package/bad_external/bad_external.ww b/test/package/bad_external/bad_external.ww new file mode 100644 index 00000000..2c32e394 --- /dev/null +++ b/test/package/bad_external/bad_external.ww @@ -0,0 +1,3 @@ +package bad_external; + +export fn value() int = { return 1; }; diff --git a/test/package/bad_external/wrong_test.ww b/test/package/bad_external/wrong_test.ww new file mode 100644 index 00000000..b07dbead --- /dev/null +++ b/test/package/bad_external/wrong_test.ww @@ -0,0 +1,3 @@ +package unrelated_test; + +@test fn wrong_external_name() void = { assert(true); }; diff --git a/test/package/cases/assert_failure/assert_failure_test.ww b/test/package/cases/assert_failure/assert_failure_test.ww new file mode 100644 index 00000000..5056b1e9 --- /dev/null +++ b/test/package/cases/assert_failure/assert_failure_test.ww @@ -0,0 +1,5 @@ +package assert_failure_test; + +@test fn assertion_failure() void = { + assert(false, "synthetic assertion failure"); +}; diff --git a/test/package/cases/descendant/descendant_test.ww b/test/package/cases/descendant/descendant_test.ww new file mode 100644 index 00000000..067eba2b --- /dev/null +++ b/test/package/cases/descendant/descendant_test.ww @@ -0,0 +1,25 @@ +package descendant_test; + +import os; + +@test fn normal_return_clears_descendant() void = { + let ready: [2]i32; + assert(os.pipe(&ready) == 0); + let pid: i32 = os.fork(); + assert(pid >= 0); + if (pid == 0) { + os.close(ready[0]); + let mask: u64 = 1u64 << ((os.SIGTERM - 1): u64); + if (os.sigprocmask(os.SIG_BLOCK, &mask, nil: *u64) != 0) { + os.exit(120); + }; + let b: [1]u8 = [1u8]; + if (os.write(ready[1], &b[0], 1u64) != 1i64) { os.exit(121); }; + os.close(ready[1]); + for (true) { }; + }; + os.close(ready[1]); + let b: [1]u8; + assert(os.read(ready[0], &b[0], 1u64) == 1i64); + os.close(ready[0]); +}; diff --git a/test/package/cases/multi_fail/multi_fail_test.ww b/test/package/cases/multi_fail/multi_fail_test.ww new file mode 100644 index 00000000..da70740b --- /dev/null +++ b/test/package/cases/multi_fail/multi_fail_test.ww @@ -0,0 +1,5 @@ +package multi_fail_test; + +@test fn first_failure() void = { assert(false); }; +@test fn second_failure() void = { assert(false); }; +@test fn continuation_after_failures() void = { assert(true); }; diff --git a/test/package/cases/nonzero/nonzero_test.ww b/test/package/cases/nonzero/nonzero_test.ww new file mode 100644 index 00000000..83438212 --- /dev/null +++ b/test/package/cases/nonzero/nonzero_test.ww @@ -0,0 +1,5 @@ +package nonzero_test; + +import os; + +@test fn deliberate_nonzero_exit() void = { os.exit(7); }; diff --git a/test/package/cases/pass/pass_test.ww b/test/package/cases/pass/pass_test.ww new file mode 100644 index 00000000..48e7aa10 --- /dev/null +++ b/test/package/cases/pass/pass_test.ww @@ -0,0 +1,18 @@ +package pass_test; + +import test; + +@test fn alpha_pass() void = { + assert(test.current() == "pass_test.alpha_pass"); +}; + +@test fn beta_skip() void = { + test.skip("synthetic unavailable feature"); +}; + +@test fn gamma_expected_abort() void = { + test.expectabort(); + abort(); +}; + +@test fn delta_pass() void = { assert(6 * 7 == 42); }; diff --git a/test/package/cases/premature/premature_test.ww b/test/package/cases/premature/premature_test.ww new file mode 100644 index 00000000..6d305034 --- /dev/null +++ b/test/package/cases/premature/premature_test.ww @@ -0,0 +1,5 @@ +package premature_test; + +import os; + +@test fn clean_exit_without_completion() void = { os.exit(0); }; diff --git a/test/package/cases/signal/signal_test.ww b/test/package/cases/signal/signal_test.ww new file mode 100644 index 00000000..83c173aa --- /dev/null +++ b/test/package/cases/signal/signal_test.ww @@ -0,0 +1,7 @@ +package signal_test; + +import os; + +@test fn signal_is_not_exit() void = { + os.kill(os.getpid(), os.SIGTERM); +}; diff --git a/test/package/cases/timeout/timeout_test.ww b/test/package/cases/timeout/timeout_test.ww new file mode 100644 index 00000000..ccccddff --- /dev/null +++ b/test/package/cases/timeout/timeout_test.ww @@ -0,0 +1,16 @@ +package timeout_test; + +import os; + +@test fn timeout_clears_term_resistant_descendant() void = { + let pid: i32 = os.fork(); + assert(pid >= 0); + if (pid == 0) { + let mask: u64 = 1u64 << ((os.SIGTERM - 1): u64); + if (os.sigprocmask(os.SIG_BLOCK, &mask, nil: *u64) != 0) { + os.exit(120); + }; + for (true) { }; + }; + for (true) { }; +}; diff --git a/test/package/dependency/dep/contest.ww b/test/package/dependency/dep/contest.ww new file mode 100644 index 00000000..2ce3c0e6 --- /dev/null +++ b/test/package/dependency/dep/contest.ww @@ -0,0 +1,4 @@ +package dep; + +// A production filename ending in "test.ww" is not a test by itself. +export fn bonus() int = { return 2; }; diff --git a/test/package/dependency/dep/dep.ww b/test/package/dependency/dep/dep.ww new file mode 100644 index 00000000..9fa26805 --- /dev/null +++ b/test/package/dependency/dep/dep.ww @@ -0,0 +1,3 @@ +package dep; + +export fn value() int = { return 40; }; diff --git a/test/package/dependency/dep/deptest.ww b/test/package/dependency/dep/deptest.ww new file mode 100644 index 00000000..a837646c --- /dev/null +++ b/test/package/dependency/dep/deptest.ww @@ -0,0 +1,7 @@ +package dep; + +// Noncanonical migration spelling: the actual line-leading @test makes this +// test-only. It must be excluded when dep is imported by the root package. +@test fn imported_dependency_test_must_not_leak() void = { + assert(false); +}; diff --git a/test/package/dependency/root/root.ww b/test/package/dependency/root/root.ww new file mode 100644 index 00000000..a08b0520 --- /dev/null +++ b/test/package/dependency/root/root.ww @@ -0,0 +1,5 @@ +package root; + +import dep; + +fn answer() int = { return dep.value() + dep.bonus(); }; diff --git a/test/package/dependency/root/root_test.ww b/test/package/dependency/root/root_test.ww new file mode 100644 index 00000000..84a69513 --- /dev/null +++ b/test/package/dependency/root/root_test.ww @@ -0,0 +1,5 @@ +package root; + +@test fn imported_production_only() void = { + assert(answer() == 42); +}; diff --git a/test/package/no_tests/no_tests.ww b/test/package/no_tests/no_tests.ww new file mode 100644 index 00000000..268e8c97 --- /dev/null +++ b/test/package/no_tests/no_tests.ww @@ -0,0 +1,3 @@ +package no_tests; + +export fn value() int = { return 1; }; diff --git a/test/package/package_test.ww b/test/package/package_test.ww new file mode 100644 index 00000000..cbfd4ffc --- /dev/null +++ b/test/package/package_test.ww @@ -0,0 +1,444 @@ +package package_test; + +// Native end-to-end checks for the first single-directory package route. +// Make builds and invokes this one binary; discovery, subprocess ownership, +// output interpretation, and assertions remain WW code. + +import os; +import os.exec; +import strings; +import temp; +import time; + +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 package-test environment"); + }; +}; + +fn repo() str = { return envrequired("WW_PACKAGE_REPO"); }; + +fn driver(name: str) str = { + return strings.concat(repo(), "/out/bin/", name); +}; + +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; +}; + +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); +}; + +fn runcommand(root: str, name: str, argv: []str, + lifetime: time.duration, out: *commandout) void = { + let c: exec.command; + c.path = argv[0]; + c.argv = argv; + c.env = os.getenvs(); + c.dir = repo(); + 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); +}; + +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); +}; + +fn fresh() str = { return strings.dup(temp.dir()); }; + +fn pos(haystack: str, needle: str) i32 = { + match (strings.index(haystack, needle)) { + case let n: i32 => return n; + case void => return -1; + }; +}; + +fn has(haystack: str, needle: str) bool = { + return pos(haystack, needle) >= 0; +}; + +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; +}; + +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 matches: bool = true; + let j: i32 = 0; + for (j < needle.len) { + if (haystack[i + j] != needle[j]) { matches = false; break; }; + j += 1; + }; + if (matches) { count += 1; i += needle.len; } + else { i += 1; }; + }; + return count; +}; + +fn expectexit(out: *commandout, code: i32) void = { + assert(out.termination == exec.termination.EXIT); + assert(out.code == code); +}; + +fn packagepath(relative: str) str = { + return strings.concat(repo(), "/test/package/", relative); +}; + +@test fn deterministic_routing_and_filters() void = { + let root: str = fresh(); + let target: str = packagepath("routing"); + let av: []str = [driver("ww"), "test", "-list", target]; + let out: commandout; + runcommand(root, "list", av, (30i64 * (time.second: i64)): time.duration, + &out); + expectexit(&out, 0); + let first: i32 = pos(out.stdout, "routing.private_helper_first\n"); + let second: i32 = pos(out.stdout, "routing.same_package\n"); + let third: i32 = pos(out.stdout, "routing_test.external_package\n"); + assert(first >= 0 && first < second && second < third); + assert(occurrences(out.stdout, "routing.private_helper_first\n") == 1); + assert(occurrences(out.stdout, "routing.same_package\n") == 1); + assert(occurrences(out.stdout, "routing_test.external_package\n") == 1); + + let rav: []str = [driver("ww"), "test", target]; + runcommand(root, "run", rav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(has(out.stdout, "routing.private_helper_first ... ok\n")); + assert(has(out.stdout, "routing.same_package ... ok\n")); + assert(has(out.stdout, "routing_test.external_package ... ok\n")); + assert(has(out.stdout, + "2 passed, 0 failed, 0 skipped, 0 harness errors\n")); + assert(has(out.stdout, + "1 passed, 0 failed, 0 skipped, 0 harness errors\n")); + + let fav: []str = [driver("ww"), "test", "-run", + "routing.private_helper_first", "-filter", "external_package", + target]; + runcommand(root, "filters", fav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(has(out.stdout, "routing.private_helper_first ... ok\n")); + assert(!has(out.stdout, "routing.same_package ...")); + assert(has(out.stdout, "routing_test.external_package ... ok\n")); + + let nav: []str = [driver("ww"), "test", "-run", "no-such-*", target]; + runcommand(root, "nomatch", nav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(occurrences(out.stdout, "[no matches]\n") == 2); + assert(occurrences(out.stdout, + "1 discovered, 0 selected, 0 started, 0 completed\n") == 1); + assert(occurrences(out.stdout, + "2 discovered, 0 selected, 0 started, 0 completed\n") == 1); + + let lnav: []str = [driver("ww"), "test", "-list", "-run", + "no-such-*", target]; + runcommand(root, "list-nomatch", lnav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(occurrences(out.stdout, "[no matches]\n") == 2); + clean(root); +}; + +@test fn imported_dependency_tests_do_not_leak() void = { + let root: str = fresh(); + let base: str = packagepath("dependency"); + let av: []str = [driver("ww"), "test", "-I", base, + strings.concat(base, "/root")]; + let out: commandout; + runcommand(root, "dependency", av, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(has(out.stdout, "root.imported_production_only ... ok\n")); + assert(!has(out.stdout, "imported_dependency_test_must_not_leak")); + assert(has(out.stdout, + "1 discovered, 1 selected, 1 started, 1 completed\n")); + clean(root); +}; + +@test fn empty_and_invalid_package_classes() void = { + let root: str = fresh(); + let av: []str = [driver("ww"), "test", packagepath("no_tests")]; + let out: commandout; + runcommand(root, "notests", av, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(has(out.stdout, " [no tests]\n")); + assert(!has(out.stdout, " ... ok")); + + let bad: []str = [driver("ww"), "test", packagepath("bad_external")]; + runcommand(root, "badexternal", bad, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, + "test package must match production package or _test")); + + let empty: str = strings.concat(root, "/empty"); + assert(os.mkdir(empty, 448i32) == 0); + let emptyav: []str = [driver("ww"), "test", empty]; + runcommand(root, "emptydir", emptyav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, "directory contains no WW package sources")); + clean(root); +}; + +@test fn symlink_package_is_rejected() void = { + let root: str = fresh(); + let real: str = strings.concat(root, "/real"); + let link: str = strings.concat(root, "/link"); + assert(os.mkdir(real, 448i32) == 0); + writefile(strings.concat(real, "/real.ww"), + "package real;\nexport fn value() int = { return 1; };\n"); + let lav: []str = ["/bin/ln", "-s", real, link]; + let out: commandout; + runcommand(root, "link", lav, time.second, &out); + expectexit(&out, 0); + let av: []str = [driver("ww"), "test", link]; + runcommand(root, "reject", av, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, "symlink traversal is not allowed")); + clean(root); +}; + +@test fn pass_skip_and_expected_abort() void = { + let root: str = fresh(); + let av: []str = [driver("ww"), "test", packagepath("cases/pass")]; + let out: commandout; + runcommand(root, "pass", av, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(has(out.stdout, "pass_test.alpha_pass ... ok\n")); + assert(has(out.stdout, + "pass_test.beta_skip ... SKIP: synthetic unavailable feature\n")); + assert(has(out.stdout, "pass_test.gamma_expected_abort ... ok\n")); + assert(has(out.stdout, "pass_test.delta_pass ... ok\n")); + assert(has(out.stdout, + "3 passed, 0 failed, 1 skipped, 0 harness errors\n")); + assert(has(out.stdout, + "4 discovered, 4 selected, 4 started, 4 completed\n")); + clean(root); +}; + +@test fn failures_are_distinct_and_clamped() void = { + let root: str = fresh(); + let out: commandout; + let av: []str = [driver("ww"), "test", + packagepath("cases/assert_failure")]; + runcommand(root, "assert", av, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stdout, + "assert_failure_test.assertion_failure ... FAIL (exit 1)\n")); + assert(!has(out.stderr, "captures:")); + + let nonzeroav: []str = [driver("ww"), "test", + packagepath("cases/nonzero")]; + runcommand(root, "nonzero", nonzeroav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stdout, + "nonzero_test.deliberate_nonzero_exit ... FAIL (exit 7)\n")); + + let prematureav: []str = [driver("ww"), "test", + packagepath("cases/premature")]; + runcommand(root, "premature", prematureav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stdout, + "premature_test.clean_exit_without_completion ... HARNESS (incomplete result)\n")); + + let signalav: []str = [driver("ww"), "test", + packagepath("cases/signal")]; + runcommand(root, "signal", signalav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stdout, + "signal_test.signal_is_not_exit ... FAIL (signal 15)\n")); + + let multiav: []str = [driver("ww"), "test", + packagepath("cases/multi_fail")]; + runcommand(root, "multifail", multiav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stdout, + "1 passed, 2 failed, 0 skipped, 0 harness errors\n")); + assert(has(out.stdout, + "3 discovered, 3 selected, 3 started, 3 completed\n")); + clean(root); +}; + +@test fn timeout_and_descendant_cleanup() void = { + let root: str = fresh(); + let av: []str = [driver("ww"), "test", "-timeout-ms=100", + packagepath("cases/timeout")]; + let out: commandout; + runcommand(root, "timeout", av, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stdout, + "timeout_test.timeout_clears_term_resistant_descendant ... FAIL(timeout)\n")); + assert(has(out.stdout, + "1 discovered, 1 selected, 1 started, 1 completed\n")); + + let descendantav: []str = [driver("ww"), "test", + packagepath("cases/descendant")]; + runcommand(root, "descendant", descendantav, + (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(has(out.stdout, + "descendant_test.normal_return_clears_descendant ... ok\n")); + assert(!has(out.stdout, "HARNESS")); + clean(root); +}; + +@test fn cstage_wwstage_behavior_and_assembly_match() void = { + let root: str = fresh(); + writefile(strings.concat(root, "/route.ww"), + "package route;\nexport fn value() int = { return 7; };\n"); + writefile(strings.concat(root, "/a_test.ww"), + "package route;\n@test fn white() void = { assert(value() == 7); };\n"); + writefile(strings.concat(root, "/z_test.ww"), + "package route_test;\nimport route;\n@test fn external() void = { assert(route.value() == 7); };\n"); + + let outc: commandout; + let outw: commandout; + let listc: []str = [driver("ww"), "test", "-list", root]; + let listw: []str = [driver("ww_ww"), "test", "-list", root]; + runcommand(root, "list-c", listc, + (30i64 * (time.second: i64)): time.duration, &outc); + runcommand(root, "list-ww", listw, + (30i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outc, 0); + expectexit(&outw, 0); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + + let misusec: []str = [driver("ww"), "test", "-run"]; + let misusew: []str = [driver("ww_ww"), "test", "-run"]; + runcommand(root, "misuse-c", misusec, time.second, &outc); + runcommand(root, "misuse-ww", misusew, time.second, &outw); + expectexit(&outc, 2); + expectexit(&outw, 2); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stderr, "ww test: -run needs an argument\n")); + + let timeoutc: []str = [driver("ww"), "test", + "-timeout-ms=4000000", root]; + let timeoutw: []str = [driver("ww_ww"), "test", + "-timeout-ms=4000000", root]; + runcommand(root, "timeout-misuse-c", timeoutc, time.second, &outc); + runcommand(root, "timeout-misuse-ww", timeoutw, time.second, &outw); + expectexit(&outc, 2); + expectexit(&outw, 2); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + assert(has(outc.stderr, "usage: wwtest package")); + + let cc: []str = [driver("ww"), "test", "-c", root]; + runcommand(root, "compile-c", cc, + (30i64 * (time.second: i64)): time.duration, &outc); + expectexit(&outc, 0); + assert(has(outc.stdout, strings.concat(" -> ", root, "/route.test\n"))); + assert(has(outc.stdout, + strings.concat(" -> ", root, "/route_test.test\n"))); + let cwhite: str = readfile(strings.concat(root, + "/route.test.sepwork/__root.s")); + let cexternal: str = readfile(strings.concat(root, + "/route_test.test.sepwork/__root.s")); + // The explicit package `-c` outputs are caller-owned artifacts. Release + // the two exact C-stage trees before asking the WW driver to acquire the + // same stems; the driver never deletes a pre-existing `.sepwork` path. + clean(strings.concat(root, "/route.test.sepwork")); + clean(strings.concat(root, "/route_test.test.sepwork")); + + let wc: []str = [driver("ww_ww"), "test", "-c", root]; + runcommand(root, "compile-ww", wc, + (30i64 * (time.second: i64)): time.duration, &outw); + expectexit(&outw, 0); + assert(same(outc.stdout, outw.stdout)); + assert(same(outc.stderr, outw.stderr)); + assert(same(cwhite, readfile(strings.concat(root, + "/route.test.sepwork/__root.s")))); + assert(same(cexternal, readfile(strings.concat(root, + "/route_test.test.sepwork/__root.s")))); + clean(root); +}; diff --git a/test/package/routing/a_helper_test.ww b/test/package/routing/a_helper_test.ww new file mode 100644 index 00000000..efa12665 --- /dev/null +++ b/test/package/routing/a_helper_test.ww @@ -0,0 +1,5 @@ +package routing; + +// Canonical test-only helper: no @test declaration in this file. Both +// same-package test files below share it through the single white-box build. +fn privatevalue() int = { return value() + 1; }; diff --git a/test/package/routing/b_white_test.ww b/test/package/routing/b_white_test.ww new file mode 100644 index 00000000..a3318f16 --- /dev/null +++ b/test/package/routing/b_white_test.ww @@ -0,0 +1,5 @@ +package routing; + +@test fn private_helper_first() void = { + assert(privatevalue() == 8); +}; diff --git a/test/package/routing/external_test.ww b/test/package/routing/external_test.ww new file mode 100644 index 00000000..7ada780f --- /dev/null +++ b/test/package/routing/external_test.ww @@ -0,0 +1,7 @@ +package routing_test; + +import routing; + +@test fn external_package() void = { + assert(routing.value() == 7); +}; diff --git a/test/package/routing/routing.ww b/test/package/routing/routing.ww new file mode 100644 index 00000000..f931bd08 --- /dev/null +++ b/test/package/routing/routing.ww @@ -0,0 +1,3 @@ +package routing; + +export fn value() int = { return 7; }; diff --git a/test/package/routing/routing_test.ww b/test/package/routing/routing_test.ww new file mode 100644 index 00000000..b03d4977 --- /dev/null +++ b/test/package/routing/routing_test.ww @@ -0,0 +1,6 @@ +package routing; + +@test fn same_package() void = { + assert(value() == 7); + assert(privatevalue() == 8); +}; diff --git a/test/package/runtime/README.md b/test/package/runtime/README.md new file mode 100644 index 00000000..49275663 --- /dev/null +++ b/test/package/runtime/README.md @@ -0,0 +1,45 @@ +# Native package-runtime fixtures + +These fixtures exercise the compiled package test binary, rather than a +subprocess-per-assertion harness. Build the bootstrap tools once with `make all`, +then run the focused cases from the repository root: + +```sh +out/bin/ww test test/package/runtime/success_test.ww +out/bin/ww test -c -o /tmp/ww-runtime-test test/package/runtime/success_test.ww +/tmp/ww-runtime-test 'alpha*' 'delta*' +/tmp/ww-runtime-test -list +/tmp/ww-runtime-test -list 'gamma*' +/tmp/ww-runtime-test -timeout-ms=50 'alpha*' +out/bin/ww test test/package/runtime/fail_test.ww +out/bin/ww test test/package/runtime/multiple_fail_test.ww +out/bin/ww test test/package/runtime/expected_abort_return_test.ww +out/bin/ww test test/package/runtime/premature_exit_test.ww +out/bin/ww test test/package/runtime/signal_test.ww +out/bin/ww test -c -o /tmp/ww-runtime-timeout test/package/runtime/timeout_test.ww +/tmp/ww-runtime-timeout -timeout-ms=50 +out/bin/ww test -c -o /tmp/ww-runtime-descendant test/package/runtime/lingering_descendant_test.ww +/tmp/ww-runtime-descendant -timeout-ms=250 +out/bin/ww test -c -o /tmp/ww-runtime-escaped test/package/runtime/escaped_descendant_test.ww +/tmp/ww-runtime-escaped -timeout-ms=250 +``` + +The success fixture reports two ordinary passes, one expected-abort pass, and +one skip with its reason. The preserved test binary accepts repeated glob +arguments directly. List mode prints selected names without starting tests. +Each test has a 30-second default timeout; `-timeout-ms=N` selects a positive +per-test timeout up to one hour for a direct test-binary invocation. +The current runtime recognizes `expectabort(); os.exit(nonzero)` as an expected +abort because WW's abort primitive itself terminates with a normal exit status; +the ABI cannot distinguish those two paths without a future abort hook. +The assertion, multiple-failure, unmet-expected-abort, premature-exit, and +signal fixtures fail respectively with a normal exit, two failures whose +process status is still clamped to 1, an expected abort that never happens, an +incomplete completion record, and signal 15; none may be mistaken for a pass. +The timeout fixture fails deterministically as `FAIL(timeout)`. The lingering +descendant fixture passes without waiting on the inherited control-pipe writer: +the descendant blocks SIGTERM, so the runner exercises its grace period and +SIGKILL escalation before draining the record. +The escaped-descendant fixture moves its inherited writer into another process +group for one second. The framed nonblocking protocol returns immediately after +the test leader and owned group complete; it never waits for pipe EOF. diff --git a/test/package/runtime/escaped_descendant_test.ww b/test/package/runtime/escaped_descendant_test.ww new file mode 100644 index 00000000..fe381192 --- /dev/null +++ b/test/package/runtime/escaped_descendant_test.ww @@ -0,0 +1,26 @@ +package runtime_escaped_descendant_test; + +import os; +import time; + +@test fn escaped_writer_does_not_hold_runner() void = { + let fds: [2]i32; + assert(os.pipe(&fds) == 0); + let pid: i32 = os.fork(); + assert(pid >= 0); + if (pid == 0) { + os.close(fds[0]); + if (os.setpgid(0, 0) != 0) { os.exit(120); }; + let ready: u8 = 1u8; + if (os.write(fds[1], &ready, 1u64) != 1i64) { os.exit(121); }; + os.close(fds[1]); + time.sleep((time.second: i64): time.duration, + time.clock.monotonic); + os.exit(0); + }; + os.close(fds[1]); + let ready: u8 = 0u8; + assert(os.read(fds[0], &ready, 1u64) == 1i64); + os.close(fds[0]); + assert(ready == 1u8); +}; diff --git a/test/package/runtime/expected_abort_return_test.ww b/test/package/runtime/expected_abort_return_test.ww new file mode 100644 index 00000000..40c410e9 --- /dev/null +++ b/test/package/runtime/expected_abort_return_test.ww @@ -0,0 +1,7 @@ +package runtime_expected_abort_return_test; + +import test; + +@test fn expected_abort_must_happen() void = { + test.expectabort(); +}; diff --git a/test/package/runtime/fail_test.ww b/test/package/runtime/fail_test.ww new file mode 100644 index 00000000..9dc00b16 --- /dev/null +++ b/test/package/runtime/fail_test.ww @@ -0,0 +1,5 @@ +package runtime_fail_test; + +@test fn assertion_failure() void = { + assert(false, "synthetic assertion failure"); +}; diff --git a/test/package/runtime/lingering_descendant_test.ww b/test/package/runtime/lingering_descendant_test.ww new file mode 100644 index 00000000..ba186034 --- /dev/null +++ b/test/package/runtime/lingering_descendant_test.ww @@ -0,0 +1,26 @@ +package runtime_lingering_descendant_test; + +import os; + +@test fn returning_test_clears_descendant() void = { + let fds: [2]i32; + assert(os.pipe(&fds) == 0); + let pid: i32 = os.fork(); + assert(pid >= 0); + if (pid == 0) { + os.close(fds[0]); + let mask: u64 = 1u64 << ((os.SIGTERM - 1): u64); + if (os.sigprocmask(os.SIG_BLOCK, &mask, nil: *u64) != 0) { + os.exit(120); + }; + let ready: u8 = 1u8; + if (os.write(fds[1], &ready, 1u64) != 1i64) { os.exit(121); }; + os.close(fds[1]); + for (true) { }; + }; + os.close(fds[1]); + let ready: u8 = 0u8; + assert(os.read(fds[0], &ready, 1u64) == 1i64); + os.close(fds[0]); + assert(ready == 1u8); +}; diff --git a/test/package/runtime/multiple_fail_test.ww b/test/package/runtime/multiple_fail_test.ww new file mode 100644 index 00000000..eefb7549 --- /dev/null +++ b/test/package/runtime/multiple_fail_test.ww @@ -0,0 +1,9 @@ +package runtime_multiple_fail_test; + +@test fn first_failure() void = { + assert(false); +}; + +@test fn second_failure() void = { + assert(false); +}; diff --git a/test/package/runtime/premature_exit_test.ww b/test/package/runtime/premature_exit_test.ww new file mode 100644 index 00000000..6ae92c9c --- /dev/null +++ b/test/package/runtime/premature_exit_test.ww @@ -0,0 +1,7 @@ +package runtime_premature_exit_test; + +import os; + +@test fn clean_exit_without_completion() void = { + os.exit(0); +}; diff --git a/test/package/runtime/signal_test.ww b/test/package/runtime/signal_test.ww new file mode 100644 index 00000000..92f2f1cc --- /dev/null +++ b/test/package/runtime/signal_test.ww @@ -0,0 +1,7 @@ +package runtime_signal_test; + +import os; + +@test fn signal_is_not_exit() void = { + os.kill(os.getpid(), os.SIGTERM); +}; diff --git a/test/package/runtime/success_test.ww b/test/package/runtime/success_test.ww new file mode 100644 index 00000000..4d574cef --- /dev/null +++ b/test/package/runtime/success_test.ww @@ -0,0 +1,20 @@ +package runtime_success_test; + +import test; + +@test fn alpha_pass() void = { + assert(test.current() != ""); +}; + +@test fn beta_skip() void = { + test.skip("synthetic unavailable feature"); +}; + +@test fn gamma_expected_abort() void = { + test.expectabort(); + abort(); +}; + +@test fn delta_pass() void = { + assert(6 * 7 == 42); +}; diff --git a/test/package/runtime/timeout_test.ww b/test/package/runtime/timeout_test.ww new file mode 100644 index 00000000..78c65509 --- /dev/null +++ b/test/package/runtime/timeout_test.ww @@ -0,0 +1,5 @@ +package runtime_timeout_test; + +@test fn hang_times_out() void = { + for (true) { }; +};