diff --git a/gitnexus/test/helpers/temp-dir-pool.ts b/gitnexus/test/helpers/temp-dir-pool.ts new file mode 100644 index 000000000..cd6749c82 --- /dev/null +++ b/gitnexus/test/helpers/temp-dir-pool.ts @@ -0,0 +1,62 @@ +/** + * Temp-directory lifecycle for pipeline-level integration tests. + * + * The pipeline mutates the repo it is handed (parse caches, `.gitnexus/`), so + * these tests each run against a throwaway copy of a fixture. Every consumer + * had hand-rolled the SAME three parts — a `string[]` of created dirs, a + * `mkdtempSync` that pushes onto it, and an `afterAll` that `rmSync`s the lot. + * Extracted at the fourth consumer (`pipeline-pdg`, `pipeline-pdg-streaming`, + * `interproc-taint`, `pdg-chained-receiver-callees`); the copies had already + * drifted — `pipeline-pdg` registered two cleanup hooks over one array. + * + * Only the LIFECYCLE is shared, deliberately: seeding differs per test (a + * recursive fixture copy, a single file, an inline-written source, or nothing + * at all), so `dir()` hands back an empty registered directory and the caller + * fills it however it likes. `fromFixture()` is the common case. + * + * `createTempDirPool` calls `afterAll` itself, so it must be called from a + * test file's module scope (not from this module's top level — ESM caching + * would register the hook once, for whichever file imported it first). + * Directories are registered at creation, before any seeding runs, so a + * fixture copy or a pipeline run that throws still leaves them cleaned up. + */ + +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { afterAll } from 'vitest'; + +export interface TempDirPool { + /** A fresh empty temp dir, registered for cleanup. Seed it yourself. */ + dir(): string; + /** A fresh temp dir seeded with a recursive copy of `fixture`. */ + fromFixture(fixture: string): string; +} + +/** + * Create a pool of temp directories that are removed after the calling test + * file finishes. `prefix` is the `mkdtemp` prefix (e.g. `'gn-pdg-'`), kept + * per-pool so a leaked directory still names the suite that made it. + */ +export function createTempDirPool(prefix: string): TempDirPool { + const created: string[] = []; + + const dir = (): string => { + const made = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + created.push(made); + return made; + }; + + afterAll(() => { + for (const d of created) fs.rmSync(d, { recursive: true, force: true }); + }); + + return { + dir, + fromFixture(fixture: string): string { + const made = dir(); + fs.cpSync(fixture, made, { recursive: true }); + return made; + }, + }; +} diff --git a/gitnexus/test/integration/cfg/interproc-taint.test.ts b/gitnexus/test/integration/cfg/interproc-taint.test.ts index 1e826c3eb..c009da7fe 100644 --- a/gitnexus/test/integration/cfg/interproc-taint.test.ts +++ b/gitnexus/test/integration/cfg/interproc-taint.test.ts @@ -12,33 +12,23 @@ * the parse worker, and a stale dist is a spurious red. */ -import { describe, it, expect, afterAll } from 'vitest'; -import fs from 'fs'; -import os from 'os'; +import { describe, it, expect } from 'vitest'; import path from 'path'; import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js'; import type { PipelineResult } from '../../../src/types/pipeline.js'; import { decodeTaintPath } from '../../../src/core/ingestion/taint/path-codec.js'; +import { createTempDirPool } from '../../helpers/temp-dir-pool.js'; const FIXTURE = path.join(__dirname, 'fixtures', 'interproc-repo'); -const tmpDirs: string[] = []; -function freshRepo(): string { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-interproc-')); - fs.cpSync(FIXTURE, dir, { recursive: true }); - tmpDirs.push(dir); - return dir; -} +const repos = createTempDirPool('gn-interproc-'); +const freshRepo = (): string => repos.fromFixture(FIXTURE); function taintPaths(result: PipelineResult) { return [...result.graph.iterRelationships()].filter((r) => r.type === 'TAINT_PATH'); } describe('U9 — end-to-end interprocedural taint (--pdg)', () => { - afterAll(() => { - for (const d of tmpDirs) fs.rmSync(d, { recursive: true, force: true }); - }); - it('with --pdg: composes a cross-file source→sink into a TAINT_PATH edge', async () => { const result = await runPipelineFromRepo(freshRepo(), () => {}, { pdg: true }); const paths = taintPaths(result); diff --git a/gitnexus/test/integration/cfg/pdg-chained-receiver-callees.test.ts b/gitnexus/test/integration/cfg/pdg-chained-receiver-callees.test.ts index 2017e476a..2305239f8 100644 --- a/gitnexus/test/integration/cfg/pdg-chained-receiver-callees.test.ts +++ b/gitnexus/test/integration/cfg/pdg-chained-receiver-callees.test.ts @@ -47,11 +47,11 @@ * fixture is shared by eight suites including a snapshot test, so growing it to * cover one seam churns unrelated expectations. */ -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { describe, it, expect, beforeAll } from 'vitest'; import fs from 'fs'; -import os from 'os'; import path from 'path'; import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js'; +import { createTempDirPool } from '../../helpers/temp-dir-pool.js'; // The PRODUCTION reader of the cell: splits on `CALLEE_ID_SEP` // (src/core/ingestion/cfg/emit.ts) and drops the truncation sentinel. Both the // statement-precise bridge and the inter-procedural descent go through it, so @@ -225,7 +225,7 @@ interface BlockCell { readonly ids: readonly string[]; } -const tmpDirs: string[] = []; +const repos = createTempDirPool('gn-pdg-chain-'); let blocks: readonly BlockCell[] = []; function blocksFor(marker: string): readonly BlockCell[] { @@ -251,10 +251,9 @@ function assertChainReachesPdg(shape: ReceiverShape): void { describe('PDG calleeIds — chained receiver calls by receiver form (#2802 follow-up)', () => { beforeAll(async () => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-pdg-chain-')); + const dir = repos.dir(); fs.mkdirSync(path.join(dir, path.dirname(FIXTURE_PATH))); fs.writeFileSync(path.join(dir, FIXTURE_PATH), CHAINED_SOURCE); - tmpDirs.push(dir); const result = await runPipelineFromRepo(dir, () => {}, { pdg: true }); const collected: BlockCell[] = []; @@ -268,10 +267,6 @@ describe('PDG calleeIds — chained receiver calls by receiver form (#2802 follo blocks = collected; }, 180000); - afterAll(() => { - for (const d of tmpDirs) fs.rmSync(d, { recursive: true, force: true }); - }); - it('every receiver shape contributes exactly one chained-call block', () => { const counts = Object.fromEntries( RECEIVER_SHAPES.map((s) => [s.name, blocksFor(s.marker).length]), diff --git a/gitnexus/test/integration/cfg/pipeline-pdg-streaming.test.ts b/gitnexus/test/integration/cfg/pipeline-pdg-streaming.test.ts index ddce07071..a474c0d81 100644 --- a/gitnexus/test/integration/cfg/pipeline-pdg-streaming.test.ts +++ b/gitnexus/test/integration/cfg/pipeline-pdg-streaming.test.ts @@ -15,13 +15,13 @@ * for its CSV dir), differing ONLY in `streamPdgEmit`, so streaming is the only * variable. */ -import { describe, it, expect, afterAll } from 'vitest'; +import { describe, it, expect } from 'vitest'; import fs from 'fs'; -import os from 'os'; import path from 'path'; import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js'; import { loadParseCache } from '../../../src/storage/parse-cache.js'; import type { PipelineResult } from '../../../src/types/pipeline.js'; +import { createTempDirPool } from '../../helpers/temp-dir-pool.js'; const FIXTURE = path.join(__dirname, 'fixtures', 'pdg-repo'); // A `.vue` SFC importing a `.ts` module: the TS module is PDG-emitted in BOTH @@ -36,18 +36,10 @@ const PDG_EDGE_TYPES = new Set([ 'SANITIZES', ]); -const tmpDirs: string[] = []; -function freshRepo(fixture: string = FIXTURE): string { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-pdg-stream-')); - fs.cpSync(fixture, dir, { recursive: true }); - tmpDirs.push(dir); - return dir; -} -function freshStorage(): string { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-pdg-store-')); - tmpDirs.push(dir); - return dir; -} +const repos = createTempDirPool('gn-pdg-stream-'); +const storages = createTempDirPool('gn-pdg-store-'); +const freshRepo = (fixture: string = FIXTURE): string => repos.fromFixture(fixture); +const freshStorage = (): string => storages.dir(); function pdgCounts(result: PipelineResult): { basicBlocks: number; pdgEdges: number } { let basicBlocks = 0; @@ -62,10 +54,6 @@ function pdgCounts(result: PipelineResult): { basicBlocks: number; pdgEdges: num } describe('#2202 — streaming PDG emit end-to-end', () => { - afterAll(() => { - for (const d of tmpDirs) fs.rmSync(d, { recursive: true, force: true }); - }); - it('streams the PDG layer out of the graph while preserving the emitted set', async () => { // ── Baseline: --pdg on, streaming OFF (durable cache, same as streamed) ── const baseStorage = freshStorage(); diff --git a/gitnexus/test/integration/cfg/pipeline-pdg.test.ts b/gitnexus/test/integration/cfg/pipeline-pdg.test.ts index c3759570c..4fccc107f 100644 --- a/gitnexus/test/integration/cfg/pipeline-pdg.test.ts +++ b/gitnexus/test/integration/cfg/pipeline-pdg.test.ts @@ -1,12 +1,12 @@ -import { describe, it, expect, afterAll } from 'vitest'; +import { describe, it, expect } from 'vitest'; import fs from 'fs'; -import os from 'os'; import path from 'path'; import crypto from 'crypto'; import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js'; import type { PipelineResult } from '../../../src/types/pipeline.js'; import { decodeTaintPath } from '../../../src/core/ingestion/taint/path-codec.js'; import { fixtureTaintTotals } from '../../helpers/taint-fixture.js'; +import { createTempDirPool } from '../../helpers/temp-dir-pool.js'; import { isLanguageAvailable } from '../../../src/core/tree-sitter/parser-loader.js'; import { SupportedLanguages } from '../../../src/config/supported-languages.js'; @@ -45,19 +45,10 @@ function counts(result: PipelineResult): { return { basicBlocks, cfgEdges, reachingDefs, tainted, sanitizes, cdg }; } -const tmpDirs: string[] = []; -function freshRepo(): string { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-pdg-')); - fs.cpSync(FIXTURE, dir, { recursive: true }); - tmpDirs.push(dir); - return dir; -} +const repos = createTempDirPool('gn-pdg-'); +const freshRepo = (): string => repos.fromFixture(FIXTURE); describe('U7 — end-to-end --pdg pipeline', () => { - afterAll(() => { - for (const d of tmpDirs) fs.rmSync(d, { recursive: true, force: true }); - }); - it('with --pdg on: emits BasicBlock nodes + CFG edges into the graph', async () => { const result = await runPipelineFromRepo(freshRepo(), () => {}, { pdg: true }); const { basicBlocks, cfgEdges, reachingDefs } = counts(result); @@ -288,11 +279,11 @@ const REMAINING_LANGS: ReadonlyArray<{ { lang: 'Vue', fixture: 'vue-hazards.vue', hazard: 'shouldStop' }, // eventLoop: while(true) ]; -const cFamilyTmpDirs: string[] = []; +// Single-file seeding, so this pool uses `dir()` rather than `fromFixture()`. +const langRepos = createTempDirPool('gn-pdg-lang-'); function freshLangRepo(fixture: string): string { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-pdg-lang-')); + const dir = langRepos.dir(); fs.copyFileSync(path.join(C_FAMILY_FIXTURES, fixture), path.join(dir, fixture)); - cFamilyTmpDirs.push(dir); return dir; } @@ -396,10 +387,6 @@ function cdgSourcedInHazardFunction(result: PipelineResult, hazardMarker: string } describe('U7 — C-family worker-mode --pdg pipeline', () => { - afterAll(() => { - for (const d of cFamilyTmpDirs) fs.rmSync(d, { recursive: true, force: true }); - }); - for (const { lang, fixture, hazard } of C_FAMILY) { it(`${lang}: --pdg on emits BasicBlock + CFG + REACHING_DEF + CDG (> 0) via the worker`, async () => { const result = await runPipelineFromRepo(freshLangRepo(fixture), () => {}, WORKER_PDG); @@ -478,10 +465,6 @@ describe('U7 — C-family worker-mode --pdg pipeline', () => { }); describe('U7 — remaining languages worker-mode --pdg pipeline (#2195 capstone)', () => { - afterAll(() => { - for (const d of cFamilyTmpDirs) fs.rmSync(d, { recursive: true, force: true }); - }); - for (const { lang, fixture, hazard, vendored } of REMAINING_LANGS) { // Vendored grammars (Swift/Kotlin/Dart) may lack a prebuild on the CI // platform — skip rather than fail when the grammar can't load (#2197 U4).