wwtest: native package-test coordinator
Single-directory package-test planner: discovers *.ww, groups white-box and external <pkg>_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.
This commit is contained in:
14
cmd/wwtest/wwtest.ww
Normal file
14
cmd/wwtest/wwtest.ww
Normal file
@@ -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]);
|
||||||
|
};
|
||||||
838
internal/wwpackage/package.ww
Normal file
838
internal/wwpackage/package.ww
Normal file
@@ -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 <package>_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;
|
||||||
|
};
|
||||||
3
test/package/bad_external/bad_external.ww
Normal file
3
test/package/bad_external/bad_external.ww
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package bad_external;
|
||||||
|
|
||||||
|
export fn value() int = { return 1; };
|
||||||
3
test/package/bad_external/wrong_test.ww
Normal file
3
test/package/bad_external/wrong_test.ww
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package unrelated_test;
|
||||||
|
|
||||||
|
@test fn wrong_external_name() void = { assert(true); };
|
||||||
5
test/package/cases/assert_failure/assert_failure_test.ww
Normal file
5
test/package/cases/assert_failure/assert_failure_test.ww
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package assert_failure_test;
|
||||||
|
|
||||||
|
@test fn assertion_failure() void = {
|
||||||
|
assert(false, "synthetic assertion failure");
|
||||||
|
};
|
||||||
25
test/package/cases/descendant/descendant_test.ww
Normal file
25
test/package/cases/descendant/descendant_test.ww
Normal file
@@ -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]);
|
||||||
|
};
|
||||||
5
test/package/cases/multi_fail/multi_fail_test.ww
Normal file
5
test/package/cases/multi_fail/multi_fail_test.ww
Normal file
@@ -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); };
|
||||||
5
test/package/cases/nonzero/nonzero_test.ww
Normal file
5
test/package/cases/nonzero/nonzero_test.ww
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package nonzero_test;
|
||||||
|
|
||||||
|
import os;
|
||||||
|
|
||||||
|
@test fn deliberate_nonzero_exit() void = { os.exit(7); };
|
||||||
18
test/package/cases/pass/pass_test.ww
Normal file
18
test/package/cases/pass/pass_test.ww
Normal file
@@ -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); };
|
||||||
5
test/package/cases/premature/premature_test.ww
Normal file
5
test/package/cases/premature/premature_test.ww
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package premature_test;
|
||||||
|
|
||||||
|
import os;
|
||||||
|
|
||||||
|
@test fn clean_exit_without_completion() void = { os.exit(0); };
|
||||||
7
test/package/cases/signal/signal_test.ww
Normal file
7
test/package/cases/signal/signal_test.ww
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package signal_test;
|
||||||
|
|
||||||
|
import os;
|
||||||
|
|
||||||
|
@test fn signal_is_not_exit() void = {
|
||||||
|
os.kill(os.getpid(), os.SIGTERM);
|
||||||
|
};
|
||||||
16
test/package/cases/timeout/timeout_test.ww
Normal file
16
test/package/cases/timeout/timeout_test.ww
Normal file
@@ -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) { };
|
||||||
|
};
|
||||||
4
test/package/dependency/dep/contest.ww
Normal file
4
test/package/dependency/dep/contest.ww
Normal file
@@ -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; };
|
||||||
3
test/package/dependency/dep/dep.ww
Normal file
3
test/package/dependency/dep/dep.ww
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package dep;
|
||||||
|
|
||||||
|
export fn value() int = { return 40; };
|
||||||
7
test/package/dependency/dep/deptest.ww
Normal file
7
test/package/dependency/dep/deptest.ww
Normal file
@@ -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);
|
||||||
|
};
|
||||||
5
test/package/dependency/root/root.ww
Normal file
5
test/package/dependency/root/root.ww
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package root;
|
||||||
|
|
||||||
|
import dep;
|
||||||
|
|
||||||
|
fn answer() int = { return dep.value() + dep.bonus(); };
|
||||||
5
test/package/dependency/root/root_test.ww
Normal file
5
test/package/dependency/root/root_test.ww
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package root;
|
||||||
|
|
||||||
|
@test fn imported_production_only() void = {
|
||||||
|
assert(answer() == 42);
|
||||||
|
};
|
||||||
3
test/package/no_tests/no_tests.ww
Normal file
3
test/package/no_tests/no_tests.ww
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package no_tests;
|
||||||
|
|
||||||
|
export fn value() int = { return 1; };
|
||||||
444
test/package/package_test.ww
Normal file
444
test/package/package_test.ww
Normal file
@@ -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 <package>_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);
|
||||||
|
};
|
||||||
5
test/package/routing/a_helper_test.ww
Normal file
5
test/package/routing/a_helper_test.ww
Normal file
@@ -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; };
|
||||||
5
test/package/routing/b_white_test.ww
Normal file
5
test/package/routing/b_white_test.ww
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package routing;
|
||||||
|
|
||||||
|
@test fn private_helper_first() void = {
|
||||||
|
assert(privatevalue() == 8);
|
||||||
|
};
|
||||||
7
test/package/routing/external_test.ww
Normal file
7
test/package/routing/external_test.ww
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package routing_test;
|
||||||
|
|
||||||
|
import routing;
|
||||||
|
|
||||||
|
@test fn external_package() void = {
|
||||||
|
assert(routing.value() == 7);
|
||||||
|
};
|
||||||
3
test/package/routing/routing.ww
Normal file
3
test/package/routing/routing.ww
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package routing;
|
||||||
|
|
||||||
|
export fn value() int = { return 7; };
|
||||||
6
test/package/routing/routing_test.ww
Normal file
6
test/package/routing/routing_test.ww
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package routing;
|
||||||
|
|
||||||
|
@test fn same_package() void = {
|
||||||
|
assert(value() == 7);
|
||||||
|
assert(privatevalue() == 8);
|
||||||
|
};
|
||||||
45
test/package/runtime/README.md
Normal file
45
test/package/runtime/README.md
Normal file
@@ -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.
|
||||||
26
test/package/runtime/escaped_descendant_test.ww
Normal file
26
test/package/runtime/escaped_descendant_test.ww
Normal file
@@ -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);
|
||||||
|
};
|
||||||
7
test/package/runtime/expected_abort_return_test.ww
Normal file
7
test/package/runtime/expected_abort_return_test.ww
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package runtime_expected_abort_return_test;
|
||||||
|
|
||||||
|
import test;
|
||||||
|
|
||||||
|
@test fn expected_abort_must_happen() void = {
|
||||||
|
test.expectabort();
|
||||||
|
};
|
||||||
5
test/package/runtime/fail_test.ww
Normal file
5
test/package/runtime/fail_test.ww
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package runtime_fail_test;
|
||||||
|
|
||||||
|
@test fn assertion_failure() void = {
|
||||||
|
assert(false, "synthetic assertion failure");
|
||||||
|
};
|
||||||
26
test/package/runtime/lingering_descendant_test.ww
Normal file
26
test/package/runtime/lingering_descendant_test.ww
Normal file
@@ -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);
|
||||||
|
};
|
||||||
9
test/package/runtime/multiple_fail_test.ww
Normal file
9
test/package/runtime/multiple_fail_test.ww
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package runtime_multiple_fail_test;
|
||||||
|
|
||||||
|
@test fn first_failure() void = {
|
||||||
|
assert(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
@test fn second_failure() void = {
|
||||||
|
assert(false);
|
||||||
|
};
|
||||||
7
test/package/runtime/premature_exit_test.ww
Normal file
7
test/package/runtime/premature_exit_test.ww
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package runtime_premature_exit_test;
|
||||||
|
|
||||||
|
import os;
|
||||||
|
|
||||||
|
@test fn clean_exit_without_completion() void = {
|
||||||
|
os.exit(0);
|
||||||
|
};
|
||||||
7
test/package/runtime/signal_test.ww
Normal file
7
test/package/runtime/signal_test.ww
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package runtime_signal_test;
|
||||||
|
|
||||||
|
import os;
|
||||||
|
|
||||||
|
@test fn signal_is_not_exit() void = {
|
||||||
|
os.kill(os.getpid(), os.SIGTERM);
|
||||||
|
};
|
||||||
20
test/package/runtime/success_test.ww
Normal file
20
test/package/runtime/success_test.ww
Normal file
@@ -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);
|
||||||
|
};
|
||||||
5
test/package/runtime/timeout_test.ww
Normal file
5
test/package/runtime/timeout_test.ww
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package runtime_timeout_test;
|
||||||
|
|
||||||
|
@test fn hang_times_out() void = {
|
||||||
|
for (true) { };
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user