|
1 | 1 | import assert from 'node:assert/strict'; |
| 2 | +import crypto from 'node:crypto'; |
2 | 3 | import fs from 'node:fs'; |
3 | 4 | import os from 'node:os'; |
4 | 5 | import path from 'node:path'; |
5 | 6 | import { test } from 'node:test'; |
6 | 7 | import { fileURLToPath } from 'node:url'; |
7 | 8 | import { runCmd } from '../src/utils/exec.ts'; |
8 | | -import { TEST_RUN_TMP_PREFIX, TEST_RUN_TMP_ROOT } from './vitest-tmpdir-global-setup.ts'; |
| 9 | +import { TEST_RUN_TMP_PREFIX, TEST_RUN_TMP_ROOT } from './check-tmpdir-leaks-model.ts'; |
9 | 10 |
|
10 | 11 | const REPOSITORY_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); |
11 | 12 |
|
@@ -66,3 +67,49 @@ test('worker inherits the run-owned temp directory', () => { |
66 | 67 | fs.rmSync(evidenceRoot, { recursive: true, force: true }); |
67 | 68 | } |
68 | 69 | }); |
| 70 | + |
| 71 | +// INT32_MAX exceeds every platform's pid range (Linux pid_max caps at 2^22, |
| 72 | +// macOS at 99999), so kill(pid, 0) is ESRCH by construction — an owner that |
| 73 | +// is dead and can never be reused mid-test, unlike a freshly exited child's pid. |
| 74 | +const NEVER_A_PID = 2_147_483_647; |
| 75 | + |
| 76 | +test('global setup prunes a run directory abandoned by an earlier killed run and keeps a live one', async () => { |
| 77 | + const stamp = crypto.randomUUID(); |
| 78 | + const abandoned = path.join( |
| 79 | + TEST_RUN_TMP_ROOT, |
| 80 | + `${TEST_RUN_TMP_PREFIX}${NEVER_A_PID}-planted-${stamp}`, |
| 81 | + ); |
| 82 | + // Owned by this test process, which is alive for the whole nested run: the |
| 83 | + // same shape as a concurrent run in another worktree, and must survive. |
| 84 | + const live = path.join( |
| 85 | + TEST_RUN_TMP_ROOT, |
| 86 | + `${TEST_RUN_TMP_PREFIX}${process.pid}-planted-${stamp}`, |
| 87 | + ); |
| 88 | + const probeName = `vitest-tmpdir-prune-probe-${process.pid}-${stamp}.test.ts`; |
| 89 | + const probePath = path.join(REPOSITORY_ROOT, 'src', '__tests__', probeName); |
| 90 | + fs.mkdirSync(path.join(abandoned, 'nested'), { recursive: true }); |
| 91 | + fs.writeFileSync(path.join(abandoned, 'nested', 'leftover.txt'), 'from a killed run'); |
| 92 | + fs.mkdirSync(live); |
| 93 | + fs.writeFileSync( |
| 94 | + probePath, |
| 95 | + `import { test } from 'vitest'; |
| 96 | +
|
| 97 | +test('noop probe: the run itself is the subject', () => {}); |
| 98 | +`, |
| 99 | + ); |
| 100 | + |
| 101 | + try { |
| 102 | + const result = await runCmd( |
| 103 | + path.join(REPOSITORY_ROOT, 'node_modules', '.bin', 'vitest'), |
| 104 | + ['run', '--project', 'unit-core', probePath], |
| 105 | + { cwd: REPOSITORY_ROOT, timeoutMs: 30_000 }, |
| 106 | + ); |
| 107 | + assert.equal(result.exitCode, 0, `probe run failed:\n${result.stdout}\n${result.stderr}`); |
| 108 | + assert.equal(fs.existsSync(abandoned), false, 'setup must prune the abandoned run directory'); |
| 109 | + assert.equal(fs.existsSync(live), true, 'setup must never touch a live owner’s run directory'); |
| 110 | + } finally { |
| 111 | + fs.rmSync(abandoned, { recursive: true, force: true }); |
| 112 | + fs.rmSync(live, { recursive: true, force: true }); |
| 113 | + fs.rmSync(probePath, { force: true }); |
| 114 | + } |
| 115 | +}); |
0 commit comments