From 144eb8d707c6118da3c5e12ff3116b2473b15cef Mon Sep 17 00:00:00 2001 From: Ian Pascoe Date: Wed, 11 Nov 2026 14:58:54 -0500 Subject: [PATCH] test(sandbox-image): remove the temporary directories tests create --- apps/sandbox-image/src/builds.test.ts | 9 +++++- apps/sandbox-image/src/changes.test.ts | 10 ++++++- apps/sandbox-image/src/comparison.test.ts | 20 +++++++++++-- apps/sandbox-image/src/docker.test.ts | 12 ++++++-- apps/sandbox-image/src/memory-guard.test.ts | 20 +++++++++++-- apps/sandbox-image/src/notify.test.ts | 23 +++++++++++++-- apps/sandbox-image/src/supervisor.test.ts | 31 +++++++++++++++------ apps/sandbox-image/src/toolchains.test.ts | 14 +++++++++- apps/sandbox-image/src/workspaces.test.ts | 12 +++++++- 9 files changed, 128 insertions(+), 23 deletions(-) diff --git a/apps/sandbox-image/src/builds.test.ts b/apps/sandbox-image/src/builds.test.ts index 02f691d08..93cfc4e4d 100644 --- a/apps/sandbox-image/src/builds.test.ts +++ b/apps/sandbox-image/src/builds.test.ts @@ -8,6 +8,7 @@ import { mkdirSync, mkdtempSync, readFileSync, + rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; @@ -30,8 +31,9 @@ import { setupScriptRunIfDue, } from "@cloudpid/sandbox-image/toolchains"; -/** Processes a test started, killed after it whatever became of it. */ +/** Processes a test started, killed after it whatever became of it; then its directories are removed. */ const started: number[] = []; +const roots: string[] = []; afterEach(() => { for (const pid of started.splice(0)) { @@ -41,11 +43,16 @@ afterEach(() => { // Gone already. } } + + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); /** A scratch root, a Setup Script state, a fake `mise`, and bare remotes `app` and `api` whose default branch is `main`. */ const container = () => { const base = mkdtempSync(join(tmpdir(), "builds-")); + + roots.push(base); + const root = join(base, "build"); const state = join(base, "state"); const home = join(base, "home"); diff --git a/apps/sandbox-image/src/changes.test.ts b/apps/sandbox-image/src/changes.test.ts index 938de9856..80bebc03c 100644 --- a/apps/sandbox-image/src/changes.test.ts +++ b/apps/sandbox-image/src/changes.test.ts @@ -4,7 +4,7 @@ import { spawnSync } from "node:child_process"; import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import { hasGnuCoreutils } from "./host.fixture.ts"; import { changesScript, @@ -23,11 +23,19 @@ const env = { GIT_CONFIG_NOSYSTEM: "1", }; +const roots: string[] = []; + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + /** A Workspace root with repo `app`, whose files are committed on `main`. */ const workspace = (files: Readonly>) => { const root = mkdtempSync(join(tmpdir(), "changes-")); const dir = join(root, "app"); + roots.push(root); + const sh = (script: string, cwd: string = root) => { const result = spawnSync("bash", ["-c", script], { cwd, encoding: "utf8", env }); diff --git a/apps/sandbox-image/src/comparison.test.ts b/apps/sandbox-image/src/comparison.test.ts index a260f93fa..cbfeaa77c 100644 --- a/apps/sandbox-image/src/comparison.test.ts +++ b/apps/sandbox-image/src/comparison.test.ts @@ -2,10 +2,18 @@ // clones of it in a throwaway root. // @effect-diagnostics nodeBuiltinImport:off preferEffectArray:off preferEffectRecord:off processEnv:off import { spawnSync } from "node:child_process"; -import { existsSync, mkdirSync, mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import { hasGnuCoreutils } from "./host.fixture.ts"; import { blobsScript, @@ -27,6 +35,12 @@ const env = { const lines = (count: number, prefix = "line") => Array.from({ length: count }, (_, index) => `${prefix} ${index}\n`).join(""); +const roots: string[] = []; + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + /** * A Workspace root with a bare remote `origin.git` (main: one commit) and repo `app`, a clone of it on `main`. Other * clones push to the remote from `other`. @@ -35,6 +49,8 @@ const workspace = () => { const root = mkdtempSync(join(tmpdir(), "comparison-")); const dir = join(root, "app"); + roots.push(root); + const sh = (script: string, cwd: string = root) => { const result = spawnSync("bash", ["-c", script], { cwd, encoding: "utf8", env }); diff --git a/apps/sandbox-image/src/docker.test.ts b/apps/sandbox-image/src/docker.test.ts index 3424e77c5..c392bc6c6 100644 --- a/apps/sandbox-image/src/docker.test.ts +++ b/apps/sandbox-image/src/docker.test.ts @@ -2,15 +2,21 @@ // real, under bash, around a fake `dockerd` and `docker`. // @effect-diagnostics nodeBuiltinImport:off preferEffectArray:off processEnv:off asyncFunction:off newPromise:off import { spawn } from "node:child_process"; -import { chmodSync, existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import { hasFlock, hasSetsid } from "./host.fixture.ts"; import { sandboxImageContext } from "@cloudpid/sandbox-image"; const helper = join(sandboxImageContext, "cloudpid-dockerd"); +const roots: string[] = []; + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + /** * A directory with a fake `dockerd`, which records its arguments and `DOCKER_ALLOW_IPV6_ON_IPV4_INTERFACE`, then * answers (`ready`) after `startSeconds` and stays up, and a fake `docker`, whose `info` succeeds once it answers. @@ -18,6 +24,8 @@ const helper = join(sandboxImageContext, "cloudpid-dockerd"); const fakes = (startSeconds: number) => { const dir = mkdtempSync(join(tmpdir(), "cloudpid-dockerd-")); + roots.push(dir); + writeFileSync( join(dir, "dockerd"), `#!/usr/bin/env bash diff --git a/apps/sandbox-image/src/memory-guard.test.ts b/apps/sandbox-image/src/memory-guard.test.ts index cdf33d301..ad2aecccb 100644 --- a/apps/sandbox-image/src/memory-guard.test.ts +++ b/apps/sandbox-image/src/memory-guard.test.ts @@ -10,6 +10,7 @@ import { mkdtempSync, readdirSync, readFileSync, + rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; @@ -27,7 +28,18 @@ import { const guardScript = join(sandboxImageContext, "cloudpid-memory-guard"); const commandBash = join(sandboxImageContext, "command.bash"); -const notesDirectory = () => join(mkdtempSync(join(tmpdir(), "memory-notes-")), "notes"); +const roots: string[] = []; + +/** A new directory under the system's temporary one, removed after the test, once its processes are stopped. */ +const temporaryDirectory = (prefix: string) => { + const dir = mkdtempSync(join(tmpdir(), prefix)); + + roots.push(dir); + + return dir; +}; + +const notesDirectory = () => join(temporaryDirectory("memory-notes-"), "notes"); /** What earlyoom 1.7 prints just before it sends a process the signal. */ const sending = (signal: string, pid: number, name: string, mib: number) => @@ -38,7 +50,7 @@ const sending = (signal: string, pid: number, name: string, mib: number) => * without one), and runs until it is stopped, as earlyoom does. */ const fakeEarlyoom = (lines: readonly string[], go?: string) => { - const dir = mkdtempSync(join(tmpdir(), "earlyoom-")); + const dir = temporaryDirectory("earlyoom-"); const path = join(dir, "earlyoom"); const args = join(dir, "args"); const pid = join(dir, "pid"); @@ -78,6 +90,8 @@ const running: ChildProcess[] = []; afterEach(() => { for (const child of running.splice(0)) child.kill("SIGTERM"); + + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); /** The guard, as the supervisor starts it, with its stdout collected. */ @@ -318,7 +332,7 @@ describe("a command while the guard stops a process", () => { // The guard announces the stop while the command runs; the command ends a little after, as one whose process // was stopped does once the process has died. const notes = notesDirectory(); - const go = join(mkdtempSync(join(tmpdir(), "go-")), "go"); + const go = join(temporaryDirectory("go-"), "go"); const earlyoom = fakeEarlyoom([sending("SIGTERM", 99, "python3", 5108)], go); const guard = startGuard(earlyoom.path, notes); diff --git a/apps/sandbox-image/src/notify.test.ts b/apps/sandbox-image/src/notify.test.ts index fb772df0f..36340f7bc 100644 --- a/apps/sandbox-image/src/notify.test.ts +++ b/apps/sandbox-image/src/notify.test.ts @@ -5,18 +5,33 @@ // FUSE mount was checked by hand in Docker (the doc section). // @effect-diagnostics nodeBuiltinImport:off processEnv:off preferEffectArray:off import { spawnSync } from "node:child_process"; -import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { describe, expect, it } from "vitest"; +import { afterAll, afterEach, describe, expect, it } from "vitest"; import { sandboxImageContext } from "@cloudpid/sandbox-image"; import { NOTIFY_ATTRIBUTE, NOTIFY_ROOT_VARIABLE } from "@cloudpid/sandbox-image/notify"; +/** The helper's build directory, removed after the file; the roots, after each test. */ +const builds: string[] = []; +const roots: string[] = []; + +afterAll(() => { + for (const dir of builds.splice(0)) rmSync(dir, { recursive: true, force: true }); +}); + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + /** The helper built from the image's source, when this machine can build and run it (Linux, with `cc`). */ const built = ((): string | undefined => { if (process.platform !== "linux") return undefined; - const program = join(mkdtempSync(join(tmpdir(), "cloudpid-notify-")), "cloudpid-notify"); + const dir = mkdtempSync(join(tmpdir(), "cloudpid-notify-")); + const program = join(dir, "cloudpid-notify"); + + builds.push(dir); const compiled = spawnSync( "cc", @@ -39,6 +54,8 @@ const built = ((): string | undefined => { const root = () => { const dir = mkdtempSync(join(tmpdir(), "notify-root-")); + roots.push(dir); + mkdirSync(join(dir, "src")); mkdirSync(join(dir, "keep")); writeFileSync(join(dir, "src", "a.js"), "a"); diff --git a/apps/sandbox-image/src/supervisor.test.ts b/apps/sandbox-image/src/supervisor.test.ts index bdb7dae36..79bf523d8 100644 --- a/apps/sandbox-image/src/supervisor.test.ts +++ b/apps/sandbox-image/src/supervisor.test.ts @@ -11,6 +11,7 @@ import { mkdirSync, mkdtempSync, readFileSync, + rmSync, writeFileSync, } from "node:fs"; import { createServer, type IncomingMessage } from "node:http"; @@ -32,11 +33,23 @@ const script = join(sandboxImageContext, "supervise"); type Report = { readonly method: string; readonly path: string; readonly body: string }; const closers: Array<() => void> = []; +const roots: string[] = []; afterEach(() => { for (const close of closers.splice(0)) close(); + + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); +/** A new directory under the system's temporary one, removed after the test. */ +const temporaryDirectory = (prefix: string) => { + const dir = mkdtempSync(join(tmpdir(), prefix)); + + roots.push(dir); + + return dir; +}; + /** A server that records what it is sent, as the Workspace DO's intercept would receive it. */ const reportServer = async () => { const reports: Report[] = []; @@ -63,7 +76,7 @@ const reportServer = async () => { /** A fake computerd: a bash script doing `behaviour`. */ const fakeComputerd = (behaviour: string) => { - const dir = mkdtempSync(join(tmpdir(), "supervise-")); + const dir = temporaryDirectory("supervise-"); const path = join(dir, "computerd"); writeFileSync(path, `#!/bin/bash\n${behaviour}\n`); @@ -96,7 +109,7 @@ type SuperviseOptions = { const supervise = (computerd: string, port: number, options: SuperviseOptions = {}) => new Promise((resolve) => { const { signal, env = {}, gate } = options; - const opened = join(mkdtempSync(join(tmpdir(), "gate-")), "opened"); + const opened = join(temporaryDirectory("gate-"), "opened"); const child = spawn("bash", [script], { env: { @@ -107,8 +120,8 @@ const supervise = (computerd: string, port: number, options: SuperviseOptions = // No memory guard unless a test gives one, whatever the machine has installed. CLOUDPID_MEMORY_GUARD: "/nonexistent/cloudpid-memory-guard", CLOUDPID_EARLYOOM: "/nonexistent/earlyoom", - CLOUDPID_MEMORY_NOTES: join(mkdtempSync(join(tmpdir(), "memory-notes-")), "notes"), - CLOUDPID_START_MARKER: join(mkdtempSync(join(tmpdir(), "start-marker-")), "started"), + CLOUDPID_MEMORY_NOTES: join(temporaryDirectory("memory-notes-"), "notes"), + CLOUDPID_START_MARKER: join(temporaryDirectory("start-marker-"), "started"), TEST_GATE: opened, TEST_GATE_LINE: gate?.trim() ?? "", ...env, @@ -233,7 +246,7 @@ describe("the supervisor", () => { /** A computerd store, with SQLite's write-ahead log and shared memory files beside it, and a computerd that lists them. */ const storeOnDisk = () => { - const dir = mkdtempSync(join(tmpdir(), "computerd-store-")); + const dir = temporaryDirectory("computerd-store-"); const db = join(dir, "state.db"); for (const file of [db, `${db}-wal`, `${db}-shm`]) writeFileSync(file, "sync state"); @@ -269,7 +282,7 @@ describe("the supervisor", () => { it("makes the Container's start marker anew before computerd starts, whatever a snapshot kept of it", async () => { const { port } = await reportServer(); - const dir = mkdtempSync(join(tmpdir(), "start-marker-")); + const dir = temporaryDirectory("start-marker-"); const marker = join(dir, "started"); // A snapshot's marker: an older start's, with a content and times from long ago. @@ -304,7 +317,7 @@ describe("the supervisor", () => { /** A fake earlyoom: it records its arguments and its pid, says it is up, and runs until it is stopped, as earlyoom does. */ const fakeEarlyoom = () => { - const dir = mkdtempSync(join(tmpdir(), "earlyoom-")); + const dir = temporaryDirectory("earlyoom-"); const path = join(dir, "earlyoom"); const args = join(dir, "args"); const pid = join(dir, "pid"); @@ -390,7 +403,7 @@ describe("the supervisor's memory guard", () => { it("clears the notes a snapshot kept before computerd starts: they are not this Container start's", async () => { const { port } = await reportServer(); - const notes = join(mkdtempSync(join(tmpdir(), "memory-notes-")), "notes"); + const notes = join(temporaryDirectory("memory-notes-"), "notes"); mkdirSync(notes); writeFileSync(join(notes, "1700000000000000"), "an old note"); @@ -413,7 +426,7 @@ describe("the supervisor's memory guard", () => { * `launchEnv` is a launch's: the bridge with a token. */ const fakeBridgeNode = () => { - const dir = mkdtempSync(join(tmpdir(), "bridge-")); + const dir = temporaryDirectory("bridge-"); const node = join(dir, "node"); const record = join(dir, "record"); const pid = join(dir, "pid"); diff --git a/apps/sandbox-image/src/toolchains.test.ts b/apps/sandbox-image/src/toolchains.test.ts index c789039e9..3c41e6af4 100644 --- a/apps/sandbox-image/src/toolchains.test.ts +++ b/apps/sandbox-image/src/toolchains.test.ts @@ -15,7 +15,7 @@ import { } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import { hasSetsid } from "./host.fixture.ts"; import { variableNameProblem } from "@cloudpid/config"; import { sandboxImageContext } from "@cloudpid/sandbox-image"; @@ -102,9 +102,18 @@ describe("a command's environment", () => { }); }); +const roots: string[] = []; + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + /** A throwaway Workspace root and HOME, and a `mise` on PATH that logs its calls and the directory they ran in. */ const sandbox = (options: { readonly miseExit?: number; readonly installs?: string } = {}) => { const base = mkdtempSync(join(tmpdir(), "bootstrap-")); + + roots.push(base); + const root = join(base, "workspace"); const home = join(base, "home"); const bin = join(base, "bin"); @@ -750,6 +759,9 @@ describe("git after the bootstrap", () => { /** A Workspace root with repos, a Container state directory, and the two Setup Script commands run for real. */ const setupSandbox = () => { const base = mkdtempSync(join(tmpdir(), "setup-")); + + roots.push(base); + const root = join(base, "workspace"); const state = join(base, "state"); diff --git a/apps/sandbox-image/src/workspaces.test.ts b/apps/sandbox-image/src/workspaces.test.ts index 051ea3858..f76abd845 100644 --- a/apps/sandbox-image/src/workspaces.test.ts +++ b/apps/sandbox-image/src/workspaces.test.ts @@ -8,11 +8,12 @@ import { mkdirSync, mkdtempSync, readFileSync, + rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import { hasFlock } from "./host.fixture.ts"; import { SETUP_SCRIPT, setupRecord, START_SCRIPT } from "@cloudpid/sandbox-image/toolchains"; import { @@ -27,9 +28,18 @@ import { workStateScript, } from "@cloudpid/sandbox-image/workspaces"; +const roots: string[] = []; + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + /** A Workspace root, a Setup Script state, and bare remotes `app` and `api` whose default branch is `main`. */ const container = () => { const base = mkdtempSync(join(tmpdir(), "workspaces-")); + + roots.push(base); + const root = join(base, "workspace"); const state = join(base, "state"); const home = join(base, "home");