Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 37 additions & 15 deletions packages/qa/dogfood/test/per-file-cwd.global-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,34 @@
// left by a developer's earlier run on a tree without this isolation, or by
// a crashed run. The guard judges only what THIS run leaves, so an old
// leftover never reds a run that wrote nothing.
// 2. It creates ONE temporary root for the run and hands it to every worker
// through `provide` / `inject`. Each test file makes its own working
// directory under that root.
// 2. It reserves a TAG for the run, `os-dogfood-run-XXXXXX`, as a directory
// `mkdtempSync` creates under the system temp directory, and hands the tag
// (a name, never a path) to every worker through `provide` / `inject`.
// Each test file makes its own working directory directly under the system
// temp directory, named `<tag>-file-XXXXXX`.
//
// At the END of the run it removes that root, and with it every per-file
// directory. The removal is run-level, not per-file: on the `shared-showcase`
// project (`isolate: false`) one memoized boot serves every file on a worker,
// and its SQLite handles stay open in the directory of the file that booted it.
// At the END of the run it removes every directory whose name starts with this
// run's `<tag>-file-`, then the reservation itself. Another run's directories
// carry another tag, so a concurrent run on the same machine is never touched.
// The removal is run-level, not per-file: on the `shared-showcase` project
// (`isolate: false`) one memoized boot serves every file on a worker, and its
// SQLite handles stay open in the directory of the file that booted it.
//
// Why a tag and not a shared parent path (#21924): every `mkdtempSync` base in
// this tree must be one the tree's scratch-directory scan can read, so that an
// in-tree fixture root can never hide behind an expression
// (`scripts/pm/dispatch-gates.mjs`, "no mkdtempSync site in this tree takes a
// base the scan cannot read"). A path handed over through `inject()` is such an
// expression. `join(tmpdir(), ...)` is not: it is outside the tree by
// construction, whatever name follows it.
//
// ⛔ This teardown never JUDGES anything. On vitest 4.1.11 an error thrown from
// a globalSetup teardown is printed as `error during close` and the run still
// exits 0 (measured), so a guard placed here would be a false green. The guard
// is a throwing `afterAll` in the per-file module, which fails a test file.
import { mkdtempSync, rmSync } from 'node:fs';
import { mkdtempSync, readdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { basename, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { TestProject } from 'vitest/node';

Expand All @@ -34,19 +46,29 @@ const PACKAGE_ROOT = fileURLToPath(new URL('..', import.meta.url));

declare module 'vitest' {
export interface ProvidedContext {
/** The run's temporary root; each test file makes its working directory under it. */
dogfoodCwdRoot: string;
/** The run's tag; each test file makes its working directory as `join(tmpdir(), '<tag>-file-')`. */
dogfoodRunTag: string;
}
}

let runRoot: string | undefined;
/** The prefix of every per-file directory a run tagged `tag` creates under the system temp directory. */
export function perFileDirPrefix(tag: string): string {
return `${tag}-file-`;
}

let reservation: string | undefined;

export function setup(project: TestProject): void {
rmSync(join(PACKAGE_ROOT, '.objectstack'), { recursive: true, force: true });
runRoot = mkdtempSync(join(tmpdir(), 'os-dogfood-run-'));
project.provide('dogfoodCwdRoot', runRoot);
reservation = mkdtempSync(join(tmpdir(), 'os-dogfood-run-'));
project.provide('dogfoodRunTag', basename(reservation));
}

export function teardown(): void {
if (runRoot) rmSync(runRoot, { recursive: true, force: true });
if (!reservation) return;
const prefix = perFileDirPrefix(basename(reservation));
for (const name of readdirSync(tmpdir())) {
if (name.startsWith(prefix)) rmSync(join(tmpdir(), name), { recursive: true, force: true });
}
rmSync(reservation, { recursive: true, force: true });
}
29 changes: 21 additions & 8 deletions packages/qa/dogfood/test/per-file-cwd.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,18 @@
// ## What it does
//
// At module top level, which runs before the test file's own imports, it makes
// a directory under the run's temporary root and `chdir`s into it. `afterAll`
// restores the previous working directory. The directories are removed at the
// end of the run by the globalSetup, not here: the memoized `shared-showcase`
// boot keeps its SQLite handles open in the first file's directory.
// a directory directly under the system temp directory, named with the run's
// tag (`<tag>-file-XXXXXX`), and `chdir`s into it. `afterAll` restores the
// previous working directory. The directories are removed at the end of the
// run by the globalSetup, which sweeps its own tag, not here: the memoized
// `shared-showcase` boot keeps its SQLite handles open in the first file's
// directory.
//
// The base is spelled `join(tmpdir(), ...)` on purpose (#21924): the tree's
// scratch-directory scan must be able to read every `mkdtempSync` base, and a
// path received through `inject()` is one it cannot read. Only the run's TAG
// comes through `inject()`, as a name component, and it is refused below if
// it could carry a separator.
//
// The invariant for every dogfood author: a file runs in its own temporary
// cwd, so anything cwd-relative it writes is its own and disappears with the
Expand All @@ -38,26 +46,31 @@
// what this run leaves.
import { afterAll, inject } from 'vitest';
import { existsSync, mkdtempSync, readdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { perFileDirPrefix } from './per-file-cwd.global-setup.js';

/** `packages/qa/dogfood`, resolved from this module's own location. */
const PACKAGE_ROOT = fileURLToPath(new URL('..', import.meta.url));
/** What a file must never leave in the package directory. */
const LEFTOVER = join(PACKAGE_ROOT, '.objectstack', 'data');

const runRoot = inject('dogfoodCwdRoot');
if (!runRoot) {
const runTag = inject('dogfoodRunTag');
if (!runTag) {
throw new Error(
'per-file-cwd.setup.ts: no run root was provided. The globalSetup ' +
'per-file-cwd.setup.ts: no run tag was provided. The globalSetup ' +
'`test/per-file-cwd.global-setup.ts` must be wired in packages/qa/dogfood/vitest.config.ts; ' +
'without it this file would run in the package directory.',
);
}
if (/[\\/]|\.\./.test(runTag)) {
throw new Error(`per-file-cwd.setup.ts: the run tag ${JSON.stringify(runTag)} is not a plain directory name.`);
}

const previousCwd = process.cwd();
const presentAtStart = existsSync(LEFTOVER);
process.chdir(mkdtempSync(join(runRoot, 'file-')));
process.chdir(mkdtempSync(join(tmpdir(), perFileDirPrefix(runTag))));

afterAll(() => {
process.chdir(previousCwd);
Expand Down
2 changes: 1 addition & 1 deletion packages/qa/dogfood/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ runProjectCliOverridePreflight({
// `.objectstack/data` exists in the package directory: that throw is the guard.
// - The `globalSetup` below is ROOT-level: one run, one call, covering both
// projects and each `OS_TEST_SHARD` slice (measured). It clears a stale
// `.objectstack` at the start and removes the run's temporary root at the end.
// `.objectstack` at the start and removes the run's per-file directories at the end.
// Its teardown judges nothing, because a throw there exits 0 on vitest 4.1.11.
// Both modules' headers carry the rest, including what a dogfood author owes.
const PER_FILE_CWD = './test/per-file-cwd.setup.ts';
Expand Down
Loading