mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
test(helpers): extract the temp-repo lifecycle, collapsing five hand-rolled cleanups into one
Four cfg integration tests each hand-rolled a `tmpDirs` array, a mkdtemp-and-register step, and an `afterAll` rmSync. It is actually five registrations across six creation sites — `pipeline-pdg.test.ts` keeps a second pool for its C-family fixtures. Seeding genuinely varies four ways (recursive cpSync, single copyFileSync, inline mkdir+writeFile, and nothing at all), so a fixture-copier helper would have fitted about half the sites and made things worse. Extracted the LIFECYCLE instead — mkdtemp, register, afterAll cleanup — which is byte-identical at all five registrations and is the correctness-critical part. `dir()` returns an empty registered directory for callers that seed themselves; `fromFixture()` covers the common case. That fits 6/6. The duplication had already produced a latent defect: `cFamilyTmpDirs` was cleaned by TWO `afterAll` blocks, harmless only because `rmSync` was called with `force: true`. Now one hook. `createTempDirPool` is a function called from each test file's module scope rather than a top-level hook in the helper, because under ESM caching a module-level `afterAll` would register once, against whichever file imported it first. That hazard is documented in the helper. Raw line count is roughly neutral (-44 across the tests, +62 for the helper, 29 of which are the rationale). The win is that a cleanup invariant went from five copies to one. Cleanup verified empirically, including the failure path: a throwaway suite whose `beforeAll` throws still has its directory removed, and every temp directory created by the four migrated files is gone after a run. 46 tests pass across the four files. Refs #2802 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a188d6457d
commit
ec36c6dda3
5 changed files with 83 additions and 65 deletions
62
gitnexus/test/helpers/temp-dir-pool.ts
Normal file
62
gitnexus/test/helpers/temp-dir-pool.ts
Normal file
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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]),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue