ww: clean per-pid sep-build scratch dir on run/test (#59)
ww run / ww test created /tmp/ww_{run,test}_<pid>.sepwork/ but removed only
the built binary, leaking the scratch DIR every invocation — the tmpfs filler
(98,811 entries blocked the gate twice). Add a keepscratch param to
build_one_sep: a thin wrapper rm -rfs the impl scratch at the single
choke-point when keepscratch==0 AND the path ends ".sepwork" (covers every
return, success+error; fires post-link, pre-run). do_build keeps it (the
byte-id gates read <stem>.sepwork from ww build -o); do_run/do_test clean;
do_test no-o redirects scratch into /tmp. Both stages symmetric; reuses the
existing shell rm -rf idiom (lib/os.removeall = #109). Test 989_sepscratch_run
is self-scoped by child pid (non-flaky) with a KEEP control + revert-verified
non-vacuity. Full gate: 448 pass, zero new run/test leaks. (ww_d_* relic +
historical bulk = one-time sweep + agent-probe discipline, not code.)
This commit is contained in:
152
test/wcc/989_sepscratch_run.c
Normal file
152
test/wcc/989_sepscratch_run.c
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 989_sepscratch_run (#59) — the driver must not LEAK its per-build
|
||||
* `<stem>.sepwork` scratch dir. Pre-fix, `ww run` / `ww test` removed only
|
||||
* the built binary (unlink) and left `/tmp/ww_run_<pid>.sepwork/` behind
|
||||
* every invocation — the tmpfs filler. The fix (build_one_sep keepscratch
|
||||
* param + guarded rm at the wrapper choke-point) removes run/test scratch
|
||||
* while KEEPING build -o scratch (the byte-id gates read it).
|
||||
*
|
||||
* Two deterministic, self-scoped checks per driver stage (ww + ww_ww), so
|
||||
* they never flake under parallel phase-2 (no global /tmp glob):
|
||||
* A. KEEP control: `<drv> build -o <td>/prog <root.ww>` → assert
|
||||
* `<td>/prog.sepwork` STILL EXISTS (keepscratch 1). Proves the test is
|
||||
* non-vacuous AND that the gate-read build scratch survives.
|
||||
* B. CLEAN: fork a child that exec's `<drv> run <root.ww>`; the child's
|
||||
* pid P fixes the driver temp at `/tmp/ww_run_<P>.sepwork` EXACTLY
|
||||
* (do_run / makeruntmp both key on getpid()). After the child exits,
|
||||
* assert that dir is GONE (keepscratch 0). Self-scoped to P → immune to
|
||||
* concurrent builds.
|
||||
*
|
||||
* run/test share ONE cleanup choke-point (the build_one_sep wrapper), so
|
||||
* proving `run` proves the `test` path by construction. Revert the wrapper
|
||||
* rm → check B reddens (the mandatory non-vacuity revert-experiment).
|
||||
*
|
||||
* Light wwstage-driver test (CLAUDE.md rule 14): the trivial no-import root
|
||||
* builds fast; all artifacts live under a per-pid /tmp td or the driver's
|
||||
* own /tmp temp, so it is phase-1 parallel-safe. Models 989_sepbuild_run.c.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
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
|
||||
write_file(const char *path, const char *body)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
fputs(body, f);
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* fork+exec `<bin>/<drv> run <root>` with output muted; return the child's
|
||||
* pid via *outpid so the caller can name the driver's /tmp scratch. */
|
||||
static int
|
||||
run_child(const char *bin, const char *drv, const char *root, pid_t *outpid)
|
||||
{
|
||||
char drvpath[2048];
|
||||
snprintf(drvpath, sizeof drvpath, "%s/%s", bin, drv);
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return -1;
|
||||
if (pid == 0) {
|
||||
int dn = open("/dev/null", O_WRONLY);
|
||||
if (dn >= 0) { dup2(dn, 1); dup2(dn, 2); }
|
||||
/* argv[0] MUST be the full path: the driver derives self_dir
|
||||
* (to locate w6c/w6a/w6l/libwwrt) from argv[0]. */
|
||||
execl(drvpath, drvpath, "run", root, (char *)NULL);
|
||||
_exit(127);
|
||||
}
|
||||
*outpid = pid;
|
||||
int status = 0;
|
||||
waitpid(pid, &status, 0);
|
||||
if (WIFEXITED(status)) return WEXITSTATUS(status);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* A no-import root: builds fast, still produces a real `__root` scratch. */
|
||||
static const char *root_src =
|
||||
"package main;\n"
|
||||
"fn main() i32 = { return 0; };\n";
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
const char *bin = absbin();
|
||||
if (!bin) return 1;
|
||||
char td[64], cmd[4096], rootww[1024];
|
||||
int fail = 0;
|
||||
|
||||
snprintf(td, sizeof td, "/tmp/wwscratch_%d", getpid());
|
||||
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
||||
if (system(cmd) == -1) return 1;
|
||||
mkdir(td, 0755);
|
||||
|
||||
snprintf(rootww, sizeof rootww, "%s/root.ww", td);
|
||||
if (write_file(rootww, root_src)) { fail++; goto out; }
|
||||
|
||||
const char *drvs[] = { "ww", "ww_ww" };
|
||||
for (int s = 0; s < 2; s++) {
|
||||
/* A. KEEP control — build -o scratch must SURVIVE. */
|
||||
char prog[1024], buildscr[1100];
|
||||
snprintf(prog, sizeof prog, "%s/prog_%s", td, drvs[s]);
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"timeout 240 %s/%s build -o %s %s >/dev/null 2>&1",
|
||||
bin, drvs[s], prog, rootww);
|
||||
int brc = system(cmd);
|
||||
if (brc == -1 || (WIFEXITED(brc) && WEXITSTATUS(brc) != 0)) {
|
||||
fprintf(stderr, "sepscratch FAIL: %s build -o\n", drvs[s]);
|
||||
fail++;
|
||||
}
|
||||
snprintf(buildscr, sizeof buildscr, "%s.sepwork", prog);
|
||||
if (access(buildscr, 0) != 0) {
|
||||
fprintf(stderr, "sepscratch FAIL: %s build -o scratch %s was "
|
||||
"removed (gates read it; keepscratch must be 1)\n",
|
||||
drvs[s], buildscr);
|
||||
fail++;
|
||||
}
|
||||
|
||||
/* B. CLEAN — run scratch must be GONE post-exit (self-scoped P). */
|
||||
pid_t p = 0;
|
||||
int rrc = run_child(bin, drvs[s], rootww, &p);
|
||||
if (rrc != 0) {
|
||||
fprintf(stderr, "sepscratch FAIL: %s run exit=%d (expected 0; "
|
||||
"build must have created+run, proving non-vacuity)\n",
|
||||
drvs[s], rrc);
|
||||
fail++;
|
||||
}
|
||||
char runscr[64];
|
||||
snprintf(runscr, sizeof runscr, "/tmp/ww_run_%d.sepwork", (int)p);
|
||||
if (access(runscr, 0) == 0) {
|
||||
fprintf(stderr, "sepscratch FAIL: %s LEAKED %s (keepscratch 0 "
|
||||
"must rm run scratch)\n", drvs[s], runscr);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
out:
|
||||
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
||||
if (system(cmd) == -1) { /* best-effort cleanup */ }
|
||||
if (fail) {
|
||||
fprintf(stderr, "sepscratch: %d check(s) failed\n", fail);
|
||||
return 1;
|
||||
}
|
||||
printf("sepscratch: ww+ww_ww — build -o scratch kept, run scratch "
|
||||
"removed (no /tmp .sepwork leak)\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user