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:
2026-08-07 23:01:03 +09:00
parent ff042df937
commit 7a2c21acfb
34 changed files with 1598 additions and 0 deletions

View 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.

View 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);
};

View File

@@ -0,0 +1,7 @@
package runtime_expected_abort_return_test;
import test;
@test fn expected_abort_must_happen() void = {
test.expectabort();
};

View File

@@ -0,0 +1,5 @@
package runtime_fail_test;
@test fn assertion_failure() void = {
assert(false, "synthetic assertion failure");
};

View 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);
};

View File

@@ -0,0 +1,9 @@
package runtime_multiple_fail_test;
@test fn first_failure() void = {
assert(false);
};
@test fn second_failure() void = {
assert(false);
};

View File

@@ -0,0 +1,7 @@
package runtime_premature_exit_test;
import os;
@test fn clean_exit_without_completion() void = {
os.exit(0);
};

View File

@@ -0,0 +1,7 @@
package runtime_signal_test;
import os;
@test fn signal_is_not_exit() void = {
os.kill(os.getpid(), os.SIGTERM);
};

View 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);
};

View File

@@ -0,0 +1,5 @@
package runtime_timeout_test;
@test fn hang_times_out() void = {
for (true) { };
};