test: port the ww-test sep observer to ww; retire 989_septest
septest_test.ww keeps all five rows (pass/fail run-exits, the userman + badsig @test rejects, the collide_run user-fn-run coexist) plus the full-sepwork .s/.wwi byte-id sweep and the -T synth TEXT-main / CALL-test.run presence needles, both driver stages. The pass/fail run-exit rows overlap package_test ownership but stay: retiring them would drop audit-listed assertions from this gate. The attest_* data fixtures remain in test/wcc/data (also consumed by the 911_attest_* carriers).
This commit is contained in:
155
test/sep/septest_test.ww
Normal file
155
test/sep/septest_test.ww
Normal file
@@ -0,0 +1,155 @@
|
||||
package septest_test;
|
||||
|
||||
// `ww test` sep-driver gate (M4 E1, #79-B). Port of the retired native
|
||||
// carrier test/wcc/989_septest_run.c; every assertion preserved.
|
||||
//
|
||||
// Rows (each drives `<drv> test -o` on BOTH stages):
|
||||
// pass — two passing inline @tests -> run-exit 0; the byte-id
|
||||
// + synth anchor.
|
||||
// fail — one failing inline @test -> run-exit 1: proves the
|
||||
// @tests genuinely RUN under sep (non-vacuity teeth).
|
||||
// userman — explicit user `main` collides with the -T synth
|
||||
// entry -> reject (exit != 0; exact code is a driver
|
||||
// detail, deliberately unasserted).
|
||||
// badsig — non-`fn() void` @test signature -> reject.
|
||||
// collide_run — user `fn run` coexists with lib/test's bound runner
|
||||
// (distinct units: main.run in __root.s, test.run in
|
||||
// test.s) -> exit 0; a regressed collision re-mangles
|
||||
// to duplicate test.run -> w6l dup-symbol -> non-zero.
|
||||
//
|
||||
// byteid rows (pass, collide_run): EVERY .s/.wwi in the cs sepwork
|
||||
// byte-equals the wwstage same-named file, with seen>0 (an empty
|
||||
// sepwork fails loudly); __root.s carries "TEXT main" AND
|
||||
// "CALL\ttest.run(SB)" — the qualified -T synth path fired under sep.
|
||||
//
|
||||
// The three on-disk attest fixtures stay in test/wcc/data/ (also
|
||||
// consumed by the 911_attest_* carriers).
|
||||
|
||||
import os;
|
||||
import os.exec;
|
||||
import strings;
|
||||
import testenv;
|
||||
import time;
|
||||
|
||||
fn fail(label: str, why: str) void = {
|
||||
let m: str = strings.concat("septest FAIL: ", label, " -- ", why,
|
||||
"\n");
|
||||
os.write(2, m.ptr, m.len: u64);
|
||||
assert(false);
|
||||
};
|
||||
|
||||
fn tmo() time.duration = {
|
||||
return (180i64 * (time.second: i64)): time.duration;
|
||||
};
|
||||
|
||||
fn passsrc() str = {
|
||||
return strings.concat(
|
||||
"package septest;\n",
|
||||
"@test fn t_arith() void = {\n",
|
||||
" let a: i32 = 2;\n",
|
||||
" if (a + 3 != 5) { abort(); };\n",
|
||||
"};\n",
|
||||
"@test fn t_again() void = {\n",
|
||||
" let s: str = \"ok\";\n",
|
||||
" if (len(s) != 2) { abort(); };\n",
|
||||
"};\n");
|
||||
};
|
||||
|
||||
fn failsrc() str = {
|
||||
return strings.concat(
|
||||
"package septest;\n",
|
||||
"@test fn t_bad() void = {\n",
|
||||
" if (1 + 1 == 2) { abort(); };\n",
|
||||
"};\n");
|
||||
};
|
||||
|
||||
// -1 encodes an abnormal (non-EXIT) termination, never a valid code.
|
||||
fn teststage(td: str, label: str, drv: str, tag: str, fixture: str) i32 = {
|
||||
let prog: str = strings.concat(td, "/", label, ".", tag, ".bin");
|
||||
let av: []str = [testenv.driver(drv), "test", "-o", prog, fixture];
|
||||
let co: testenv.commandout;
|
||||
testenv.runcommand(td, td, strings.concat(label, "_", tag), av,
|
||||
tmo(), &co);
|
||||
if (co.termination != exec.termination.EXIT) { return -1; };
|
||||
return co.code;
|
||||
};
|
||||
|
||||
fn cmpsepwork(label: str, csdir: str, wwdir: str) void = {
|
||||
if (!testenv.isdir(csdir)) { fail(label, "no cs sepwork"); };
|
||||
let names: []str = testenv.listdir(csdir);
|
||||
let seen: i32 = 0;
|
||||
let i: i32 = 0;
|
||||
for (i < names.len) {
|
||||
if (strings.hassuffix(names[i], ".s")
|
||||
|| strings.hassuffix(names[i], ".wwi")) {
|
||||
seen += 1;
|
||||
if (!testenv.same(
|
||||
testenv.readfile(strings.concat(csdir, "/", names[i])),
|
||||
testenv.readfile(strings.concat(wwdir, "/", names[i])))) {
|
||||
fail(label, strings.concat("cs!=ww for ", names[i],
|
||||
" (rule 10)"));
|
||||
};
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
// an existing-but-empty sepwork would pass the loop vacuously
|
||||
if (seen == 0) { fail(label, "no .s/.wwi in cs sepwork"); };
|
||||
};
|
||||
|
||||
fn runrow(label: str, src: str, path: str, expect: i32, byteid: bool,
|
||||
reject: bool) void = {
|
||||
let td: str = testenv.fresh();
|
||||
let fixture: str = "";
|
||||
if (path.len != 0) {
|
||||
fixture = strings.concat(testenv.repo(), "/", path);
|
||||
} else {
|
||||
fixture = strings.concat(td, "/", label, ".ww");
|
||||
testenv.writefile(fixture, src);
|
||||
};
|
||||
let cs: i32 = teststage(td, label, "ww", "cs", fixture);
|
||||
let ws: i32 = teststage(td, label, "ww_ww", "ww", fixture);
|
||||
if (reject) {
|
||||
if (cs == 0 || ws == 0) {
|
||||
fail(label, "accepted (expected reject on both stages)");
|
||||
};
|
||||
} else {
|
||||
if (cs != expect || ws != expect) {
|
||||
fail(label, "run-exit differs from expected on a stage");
|
||||
};
|
||||
};
|
||||
if (byteid) {
|
||||
let csdir: str = strings.concat(td, "/", label, ".cs.bin.sepwork");
|
||||
let wwdir: str = strings.concat(td, "/", label, ".ww.bin.sepwork");
|
||||
cmpsepwork(label, csdir, wwdir);
|
||||
let rs: str = testenv.readfile(strings.concat(csdir, "/__root.s"));
|
||||
if (!testenv.has(rs, "TEXT main")) {
|
||||
fail(label, "__root.s lacks `TEXT main` (synth -T main missing)");
|
||||
};
|
||||
if (!testenv.has(rs, "CALL\ttest.run(SB)")) {
|
||||
fail(label, strings.concat("__root.s lacks `CALL test.run(SB)` ",
|
||||
"(qualified synth call missing)"));
|
||||
};
|
||||
};
|
||||
testenv.clean(td);
|
||||
};
|
||||
|
||||
@test fn pass() void = {
|
||||
runrow("pass", passsrc(), "", 0, true, false);
|
||||
};
|
||||
|
||||
@test fn failing() void = {
|
||||
runrow("fail", failsrc(), "", 1, false, false);
|
||||
};
|
||||
|
||||
@test fn userman() void = {
|
||||
runrow("userman", "", "test/wcc/data/attest_userman.ww", 0, false, true);
|
||||
};
|
||||
|
||||
@test fn badsig() void = {
|
||||
runrow("badsig", "", "test/wcc/data/attest_badsig.ww", 0, false, true);
|
||||
};
|
||||
|
||||
@test fn colliderun() void = {
|
||||
runrow("collide_run", "", "test/wcc/data/attest_userrun.ww", 0, true,
|
||||
false);
|
||||
};
|
||||
@@ -1,319 +0,0 @@
|
||||
/*
|
||||
* 989_septest_run — M4 E1 (#79-B) regression gate: `ww test`.
|
||||
*
|
||||
* `ww test` uses the separate-compilation producer. The synthesized
|
||||
* test-main emits `test.run(__wwtests)` (N_DOT, qualified) + injects a
|
||||
* synthetic `use test;`; the driver injects `test` as a graph dep of the
|
||||
* root, passes `-T` to the root producer only, and routes a single-file
|
||||
* `ww test` through build_one_sep(is_test=1). This gate is the standing
|
||||
* regression for that capability.
|
||||
*
|
||||
* Asserts (all COLD — direct per-(case,stage) builds):
|
||||
* 1. RUN-EXIT: `ww test <fixture>` exits with the expected code for
|
||||
* BOTH driver stages — 0 when every @test passes, non-zero when one
|
||||
* fails. The failing row is the non-vacuity teeth: it proves the @tests
|
||||
* genuinely run (a vacuous path that linked an empty main would
|
||||
* exit 0 on the failing fixture and the gate would catch it).
|
||||
* This same RUN-EXIT path also hosts the @test user-facing reject +
|
||||
* coexist legs ported from 910/997 (C2a, #83), which die at the M4 flip
|
||||
* that deletes 910/997: `userman` (explicit user main collides with the
|
||||
* -T synth entry) and `badsig` (non-`fn() void` @test) must reject
|
||||
* (exit != 0) on both stages; `collide_run` (user `fn run` coexists with
|
||||
* lib/test's bound runner) must accept and run to exit 0 — the
|
||||
* two land in distinct units, so a regressed collision fails the link.
|
||||
* 2. cs==ww (rule 10): the cstage `ww` and wwstage `ww_ww` sep-drivers emit
|
||||
* byte-identical per-package .s/.wwi for the passing fixture — including
|
||||
* __root.s, which carries the synth `CALL test.run`.
|
||||
* 3. SYNTH PRESENCE (non-vacuity on the -T path): the passing fixture's
|
||||
* __root.s defines `TEXT main` and emits `CALL test.run(SB)` — proving
|
||||
* the qualified-synth -T path fired.
|
||||
*
|
||||
* Every intermediate is `-o`-redirected to /tmp for isolation. Models
|
||||
* 989_c6soak_run.c conventions; 989 prefix per the sep-gate precedent.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/stat.h>
|
||||
#include <dirent.h>
|
||||
#include "wwtestpkg.h"
|
||||
|
||||
static int
|
||||
runwait(const char *cmd)
|
||||
{
|
||||
int rc = system(cmd);
|
||||
if (rc == -1) return -1;
|
||||
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static const char *
|
||||
absbin(void)
|
||||
{
|
||||
const char *b = getenv("BIN");
|
||||
if (!b) b = "out/bin";
|
||||
if (b[0] == '/') return b;
|
||||
static char buf[2048];
|
||||
char cwd[1024];
|
||||
if (getcwd(cwd, sizeof cwd) == NULL) return NULL;
|
||||
snprintf(buf, sizeof buf, "%s/%s", cwd, b);
|
||||
return buf;
|
||||
}
|
||||
|
||||
static int
|
||||
slurp(const char *path, char **outbuf, size_t *outlen)
|
||||
{
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return -1;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long n = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (n < 0) { fclose(f); return -1; }
|
||||
char *b = malloc((size_t)n + 1);
|
||||
if (!b) { fclose(f); return -1; }
|
||||
if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; }
|
||||
b[n] = '\0';
|
||||
fclose(f);
|
||||
*outbuf = b;
|
||||
*outlen = (size_t)n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
files_eq(const char *a, const char *b)
|
||||
{
|
||||
char *ba = NULL, *bb = NULL;
|
||||
size_t na = 0, nb = 0;
|
||||
if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) {
|
||||
free(ba); free(bb);
|
||||
return -1;
|
||||
}
|
||||
int eq = (na == nb && memcmp(ba, bb, na) == 0);
|
||||
free(ba); free(bb);
|
||||
return eq ? 0 : 1;
|
||||
}
|
||||
|
||||
/* 0 if `needle` occurs in the file at `path`, 1 if absent, -1 on read err. */
|
||||
static int
|
||||
file_contains(const char *path, const char *needle)
|
||||
{
|
||||
char *b = NULL;
|
||||
size_t n = 0;
|
||||
if (slurp(path, &b, &n) < 0) return -1;
|
||||
int found = (strstr(b, needle) != NULL);
|
||||
free(b);
|
||||
return found ? 0 : 1;
|
||||
}
|
||||
|
||||
static int
|
||||
write_file(const char *path, const char *body)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
wwtest_fputs(body, f);
|
||||
return fclose(f);
|
||||
}
|
||||
|
||||
/* cs==ww over a sep test build's per-package output: every .s/.wwi the
|
||||
* cstage driver produced in `csdir` must be byte-identical to the wwstage
|
||||
* driver's same-named file in `wwdir`. Returns the count of mismatches. */
|
||||
static int
|
||||
cmp_sepwork(const char *csdir, const char *wwdir, const char *label)
|
||||
{
|
||||
DIR *d = opendir(csdir);
|
||||
if (!d) {
|
||||
fprintf(stderr, "septest FAIL: %s — no cs sepwork %s\n", label, csdir);
|
||||
return 1;
|
||||
}
|
||||
int bad = 0, seen = 0;
|
||||
struct dirent *ent;
|
||||
while ((ent = readdir(d)) != NULL) {
|
||||
const char *nm = ent->d_name;
|
||||
size_t nl = strlen(nm);
|
||||
int is_s = (nl > 2 && strcmp(nm + nl - 2, ".s") == 0);
|
||||
int is_wwi = (nl > 4 && strcmp(nm + nl - 4, ".wwi") == 0);
|
||||
if (!is_s && !is_wwi) continue;
|
||||
seen++;
|
||||
char a[2048], b[2048];
|
||||
snprintf(a, sizeof a, "%s/%s", csdir, nm);
|
||||
snprintf(b, sizeof b, "%s/%s", wwdir, nm);
|
||||
if (files_eq(a, b) != 0) {
|
||||
fprintf(stderr, "septest FAIL: %s — cs!=ww for %s (rule 10)\n",
|
||||
label, nm);
|
||||
bad++;
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
/* An existing-but-empty sepwork would pass the cs==ww loop vacuously. */
|
||||
if (seen == 0) {
|
||||
fprintf(stderr, "septest FAIL: %s — no .s/.wwi in %s\n", label, csdir);
|
||||
bad++;
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
|
||||
struct tcase {
|
||||
const char *label;
|
||||
const char *src; /* inline fixture written to <td>/<label>.ww */
|
||||
const char *path; /* when set, use this on-disk fixture instead of src */
|
||||
int expect; /* expected `ww test` run-exit (when !reject) */
|
||||
int byteid; /* run the cs==ww + synth-presence checks */
|
||||
int reject; /* @test user-facing reject: assert run-exit != 0 */
|
||||
};
|
||||
|
||||
static struct tcase cases[] = {
|
||||
/* every @test passes → run-exit 0; the byte-id + synth anchor. */
|
||||
{ "pass",
|
||||
"package septest;\n"
|
||||
"@test fn t_arith() void = {\n"
|
||||
" let a: i32 = 2;\n"
|
||||
" if (a + 3 != 5) { abort(); };\n"
|
||||
"};\n"
|
||||
"@test fn t_again() void = {\n"
|
||||
" let s: str = \"ok\";\n"
|
||||
" if (len(s) != 2) { abort(); };\n"
|
||||
"};\n", NULL, 0, 1, 0 },
|
||||
/* a failing @test → run-exit 1: proves the tests actually RUN under
|
||||
* sep (the non-vacuity teeth). */
|
||||
{ "fail",
|
||||
"package septest;\n"
|
||||
"@test fn t_bad() void = {\n"
|
||||
" if (1 + 1 == 2) { abort(); };\n"
|
||||
"};\n", NULL, 1, 0, 0 },
|
||||
/* C2a (#83): the @test user-facing reject + coexist paths, re-hosted
|
||||
* from 910/997 (deleted at the M4 flip) onto the live `ww test`
|
||||
* gate. Fixtures are the unchanged 910/997 flat @test fixtures, reused
|
||||
* in place (single source of truth) — they survive the flip; only the
|
||||
* .c gates move. */
|
||||
|
||||
/* an explicit user `main` collides with the -T synth entry → reject. */
|
||||
{ "userman", NULL, "test/wcc/data/attest_userman.ww", 0, 0, 1 },
|
||||
/* a non-`fn() void` @test signature must loud-reject, not be coerced. */
|
||||
{ "badsig", NULL, "test/wcc/data/attest_badsig.ww", 0, 0, 1 },
|
||||
/* a user `fn run` COEXISTS with lib/test's bound runner (post-#80 synth
|
||||
* `use test;` keys the runner under "test"; under strict-package #24a the
|
||||
* user file's `package main;` mangles its `run` to `main.run`) → accept
|
||||
* and run to exit 0. Under sep the two land in distinct units (`main.run`
|
||||
* in __root.s, `test.run` in test.s), so a regressed collision re-mangles
|
||||
* the user `run` to `test.run` and the force-loaded root `.o` + test's
|
||||
* `.a` both define `test.run` → w6l duplicate-symbol → non-zero; exit 0
|
||||
* is the teeth (empirically confirmed). byteid=1 also
|
||||
* pins this coexist path's cs==ww per-pkg asm + synth presence — the
|
||||
* rule-10 leg the ported 997 `coexist_run` carried over the #84 mangle. */
|
||||
{ "collide_run", NULL, "test/wcc/data/attest_userrun.ww", 0, 1, 0 },
|
||||
{ NULL, NULL, NULL, 0, 0, 0 },
|
||||
};
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
const char *bin = absbin();
|
||||
if (!bin) return 1;
|
||||
|
||||
char td[] = "/tmp/wwseptest_XXXXXX";
|
||||
char cmd[8192];
|
||||
int fail = 0, cleanup_fail = 0;
|
||||
if (mkdtemp(td) == NULL) return 1;
|
||||
|
||||
for (int i = 0; cases[i].label; i++) {
|
||||
struct tcase *t = &cases[i];
|
||||
char rootww[1024];
|
||||
if (t->path) {
|
||||
snprintf(rootww, sizeof rootww, "%s", t->path);
|
||||
} else {
|
||||
snprintf(rootww, sizeof rootww, "%s/%s.ww", td, t->label);
|
||||
if (write_file(rootww, t->src)) { fail++; continue; }
|
||||
}
|
||||
|
||||
struct { const char *drv, *tag; int rc; }
|
||||
stg[] = { { "ww", "cs", -1 }, { "ww_ww", "ww", -1 } };
|
||||
for (int s = 0; s < 2; s++) {
|
||||
char prog[1024];
|
||||
/* `.bin` infix keeps the test binary distinct from the
|
||||
* `<label>.ww` SOURCE; -o leaves <prog>.sepwork for the
|
||||
* byte-id compare and runs the binary (exit = test result). */
|
||||
snprintf(prog, sizeof prog, "%s/%s.%s.bin", td, t->label, stg[s].tag);
|
||||
/* Each direct build produces the per-package .s/.wwi artifacts
|
||||
* this gate inspects. */
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"%s/%s test -o %s %s "
|
||||
">/dev/null 2>&1",
|
||||
bin, stg[s].drv, prog, rootww);
|
||||
stg[s].rc = runwait(cmd);
|
||||
}
|
||||
|
||||
/* 1. RUN-EXIT: a reject row must exit non-zero on BOTH stages (the
|
||||
* user-facing path is how `ww test` surfaces the diagnostic; the
|
||||
* exact code is a driver detail, so assert != 0 not == N). An
|
||||
* accept row exits with the exact expected code. */
|
||||
if (t->reject) {
|
||||
if (stg[0].rc == 0 || stg[1].rc == 0) {
|
||||
fprintf(stderr, "septest FAIL: %s accepted cs=%d ww=%d "
|
||||
"(expected reject)\n", t->label, stg[0].rc, stg[1].rc);
|
||||
fail++;
|
||||
}
|
||||
} else if (stg[0].rc != t->expect || stg[1].rc != t->expect) {
|
||||
fprintf(stderr, "septest FAIL: %s exits cs=%d ww=%d (expected %d)\n",
|
||||
t->label, stg[0].rc, stg[1].rc, t->expect);
|
||||
fail++;
|
||||
}
|
||||
|
||||
if (!t->byteid) continue;
|
||||
|
||||
char csdir[1024], wwdir[1024];
|
||||
snprintf(csdir, sizeof csdir, "%s/%s.cs.bin.sepwork", td, t->label);
|
||||
snprintf(wwdir, sizeof wwdir, "%s/%s.ww.bin.sepwork", td, t->label);
|
||||
|
||||
/* 2. cs==ww (rule 10): per-package .s/.wwi byte-identical. */
|
||||
fail += cmp_sepwork(csdir, wwdir, t->label);
|
||||
|
||||
/* 3. SYNTH PRESENCE: the -T synth fired under sep. */
|
||||
char rs[2048];
|
||||
snprintf(rs, sizeof rs, "%s/__root.s", csdir);
|
||||
if (file_contains(rs, "TEXT main") != 0) {
|
||||
fprintf(stderr, "septest FAIL: %s — __root.s lacks `TEXT main` "
|
||||
"(synth -T main missing)\n", t->label);
|
||||
fail++;
|
||||
}
|
||||
if (file_contains(rs, "CALL\ttest.run(SB)") != 0) {
|
||||
fprintf(stderr, "septest FAIL: %s — __root.s lacks "
|
||||
"`CALL test.run(SB)` (qualified synth call missing)\n",
|
||||
t->label);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; cases[i].label; i++) {
|
||||
for (int s = 0; s < 2; s++) {
|
||||
const char *tag = s == 0 ? "cs" : "ww";
|
||||
char path[1200];
|
||||
snprintf(path, sizeof path, "%s/%s.%s.bin.sepwork",
|
||||
td, cases[i].label, tag);
|
||||
snprintf(cmd, sizeof cmd, "rm -rf -- '%s'", path);
|
||||
if (runwait(cmd) != 0) cleanup_fail = 1;
|
||||
snprintf(path, sizeof path, "%s/%s.%s.bin",
|
||||
td, cases[i].label, tag);
|
||||
if (unlink(path) != 0 && errno != ENOENT) cleanup_fail = 1;
|
||||
}
|
||||
if (cases[i].src) {
|
||||
char path[1200];
|
||||
snprintf(path, sizeof path, "%s/%s.ww", td, cases[i].label);
|
||||
if (unlink(path) != 0 && errno != ENOENT) cleanup_fail = 1;
|
||||
}
|
||||
}
|
||||
if (rmdir(td) != 0) cleanup_fail = 1;
|
||||
if (cleanup_fail) {
|
||||
fprintf(stderr, "septest FAIL: cleanup incomplete under %s\n", td);
|
||||
fail++;
|
||||
}
|
||||
if (fail) {
|
||||
fprintf(stderr, "septest: %d check(s) failed\n", fail);
|
||||
return 1;
|
||||
}
|
||||
printf("septest: `ww test` run-exit (pass=0, fail=1) on both driver "
|
||||
"stages + cs==ww per-pkg .s/.wwi + synth `CALL test.run` in __root.s\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user