mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +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.
|
* the parse worker, and a stale dist is a spurious red.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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 path from 'path';
|
||||||
import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js';
|
import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js';
|
||||||
import type { PipelineResult } from '../../../src/types/pipeline.js';
|
import type { PipelineResult } from '../../../src/types/pipeline.js';
|
||||||
import { decodeTaintPath } from '../../../src/core/ingestion/taint/path-codec.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 FIXTURE = path.join(__dirname, 'fixtures', 'interproc-repo');
|
||||||
|
|
||||||
const tmpDirs: string[] = [];
|
const repos = createTempDirPool('gn-interproc-');
|
||||||
function freshRepo(): string {
|
const freshRepo = (): string => repos.fromFixture(FIXTURE);
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-interproc-'));
|
|
||||||
fs.cpSync(FIXTURE, dir, { recursive: true });
|
|
||||||
tmpDirs.push(dir);
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
function taintPaths(result: PipelineResult) {
|
function taintPaths(result: PipelineResult) {
|
||||||
return [...result.graph.iterRelationships()].filter((r) => r.type === 'TAINT_PATH');
|
return [...result.graph.iterRelationships()].filter((r) => r.type === 'TAINT_PATH');
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('U9 — end-to-end interprocedural taint (--pdg)', () => {
|
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 () => {
|
it('with --pdg: composes a cross-file source→sink into a TAINT_PATH edge', async () => {
|
||||||
const result = await runPipelineFromRepo(freshRepo(), () => {}, { pdg: true });
|
const result = await runPipelineFromRepo(freshRepo(), () => {}, { pdg: true });
|
||||||
const paths = taintPaths(result);
|
const paths = taintPaths(result);
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,11 @@
|
||||||
* fixture is shared by eight suites including a snapshot test, so growing it to
|
* fixture is shared by eight suites including a snapshot test, so growing it to
|
||||||
* cover one seam churns unrelated expectations.
|
* 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 fs from 'fs';
|
||||||
import os from 'os';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js';
|
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`
|
// The PRODUCTION reader of the cell: splits on `CALLEE_ID_SEP`
|
||||||
// (src/core/ingestion/cfg/emit.ts) and drops the truncation sentinel. Both the
|
// (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
|
// statement-precise bridge and the inter-procedural descent go through it, so
|
||||||
|
|
@ -225,7 +225,7 @@ interface BlockCell {
|
||||||
readonly ids: readonly string[];
|
readonly ids: readonly string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const tmpDirs: string[] = [];
|
const repos = createTempDirPool('gn-pdg-chain-');
|
||||||
let blocks: readonly BlockCell[] = [];
|
let blocks: readonly BlockCell[] = [];
|
||||||
|
|
||||||
function blocksFor(marker: string): 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)', () => {
|
describe('PDG calleeIds — chained receiver calls by receiver form (#2802 follow-up)', () => {
|
||||||
beforeAll(async () => {
|
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.mkdirSync(path.join(dir, path.dirname(FIXTURE_PATH)));
|
||||||
fs.writeFileSync(path.join(dir, FIXTURE_PATH), CHAINED_SOURCE);
|
fs.writeFileSync(path.join(dir, FIXTURE_PATH), CHAINED_SOURCE);
|
||||||
tmpDirs.push(dir);
|
|
||||||
|
|
||||||
const result = await runPipelineFromRepo(dir, () => {}, { pdg: true });
|
const result = await runPipelineFromRepo(dir, () => {}, { pdg: true });
|
||||||
const collected: BlockCell[] = [];
|
const collected: BlockCell[] = [];
|
||||||
|
|
@ -268,10 +267,6 @@ describe('PDG calleeIds — chained receiver calls by receiver form (#2802 follo
|
||||||
blocks = collected;
|
blocks = collected;
|
||||||
}, 180000);
|
}, 180000);
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
for (const d of tmpDirs) fs.rmSync(d, { recursive: true, force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('every receiver shape contributes exactly one chained-call block', () => {
|
it('every receiver shape contributes exactly one chained-call block', () => {
|
||||||
const counts = Object.fromEntries(
|
const counts = Object.fromEntries(
|
||||||
RECEIVER_SHAPES.map((s) => [s.name, blocksFor(s.marker).length]),
|
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
|
* for its CSV dir), differing ONLY in `streamPdgEmit`, so streaming is the only
|
||||||
* variable.
|
* variable.
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, afterAll } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import os from 'os';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js';
|
import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js';
|
||||||
import { loadParseCache } from '../../../src/storage/parse-cache.js';
|
import { loadParseCache } from '../../../src/storage/parse-cache.js';
|
||||||
import type { PipelineResult } from '../../../src/types/pipeline.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');
|
const FIXTURE = path.join(__dirname, 'fixtures', 'pdg-repo');
|
||||||
// A `.vue` SFC importing a `.ts` module: the TS module is PDG-emitted in BOTH
|
// 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',
|
'SANITIZES',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const tmpDirs: string[] = [];
|
const repos = createTempDirPool('gn-pdg-stream-');
|
||||||
function freshRepo(fixture: string = FIXTURE): string {
|
const storages = createTempDirPool('gn-pdg-store-');
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-pdg-stream-'));
|
const freshRepo = (fixture: string = FIXTURE): string => repos.fromFixture(fixture);
|
||||||
fs.cpSync(fixture, dir, { recursive: true });
|
const freshStorage = (): string => storages.dir();
|
||||||
tmpDirs.push(dir);
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
function freshStorage(): string {
|
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-pdg-store-'));
|
|
||||||
tmpDirs.push(dir);
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pdgCounts(result: PipelineResult): { basicBlocks: number; pdgEdges: number } {
|
function pdgCounts(result: PipelineResult): { basicBlocks: number; pdgEdges: number } {
|
||||||
let basicBlocks = 0;
|
let basicBlocks = 0;
|
||||||
|
|
@ -62,10 +54,6 @@ function pdgCounts(result: PipelineResult): { basicBlocks: number; pdgEdges: num
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('#2202 — streaming PDG emit end-to-end', () => {
|
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 () => {
|
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) ──
|
// ── Baseline: --pdg on, streaming OFF (durable cache, same as streamed) ──
|
||||||
const baseStorage = freshStorage();
|
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 fs from 'fs';
|
||||||
import os from 'os';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js';
|
import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js';
|
||||||
import type { PipelineResult } from '../../../src/types/pipeline.js';
|
import type { PipelineResult } from '../../../src/types/pipeline.js';
|
||||||
import { decodeTaintPath } from '../../../src/core/ingestion/taint/path-codec.js';
|
import { decodeTaintPath } from '../../../src/core/ingestion/taint/path-codec.js';
|
||||||
import { fixtureTaintTotals } from '../../helpers/taint-fixture.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 { isLanguageAvailable } from '../../../src/core/tree-sitter/parser-loader.js';
|
||||||
import { SupportedLanguages } from '../../../src/config/supported-languages.js';
|
import { SupportedLanguages } from '../../../src/config/supported-languages.js';
|
||||||
|
|
||||||
|
|
@ -45,19 +45,10 @@ function counts(result: PipelineResult): {
|
||||||
return { basicBlocks, cfgEdges, reachingDefs, tainted, sanitizes, cdg };
|
return { basicBlocks, cfgEdges, reachingDefs, tainted, sanitizes, cdg };
|
||||||
}
|
}
|
||||||
|
|
||||||
const tmpDirs: string[] = [];
|
const repos = createTempDirPool('gn-pdg-');
|
||||||
function freshRepo(): string {
|
const freshRepo = (): string => repos.fromFixture(FIXTURE);
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-pdg-'));
|
|
||||||
fs.cpSync(FIXTURE, dir, { recursive: true });
|
|
||||||
tmpDirs.push(dir);
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('U7 — end-to-end --pdg pipeline', () => {
|
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 () => {
|
it('with --pdg on: emits BasicBlock nodes + CFG edges into the graph', async () => {
|
||||||
const result = await runPipelineFromRepo(freshRepo(), () => {}, { pdg: true });
|
const result = await runPipelineFromRepo(freshRepo(), () => {}, { pdg: true });
|
||||||
const { basicBlocks, cfgEdges, reachingDefs } = counts(result);
|
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)
|
{ 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 {
|
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));
|
fs.copyFileSync(path.join(C_FAMILY_FIXTURES, fixture), path.join(dir, fixture));
|
||||||
cFamilyTmpDirs.push(dir);
|
|
||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -396,10 +387,6 @@ function cdgSourcedInHazardFunction(result: PipelineResult, hazardMarker: string
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('U7 — C-family worker-mode --pdg pipeline', () => {
|
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) {
|
for (const { lang, fixture, hazard } of C_FAMILY) {
|
||||||
it(`${lang}: --pdg on emits BasicBlock + CFG + REACHING_DEF + CDG (> 0) via the worker`, async () => {
|
it(`${lang}: --pdg on emits BasicBlock + CFG + REACHING_DEF + CDG (> 0) via the worker`, async () => {
|
||||||
const result = await runPipelineFromRepo(freshLangRepo(fixture), () => {}, WORKER_PDG);
|
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)', () => {
|
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) {
|
for (const { lang, fixture, hazard, vendored } of REMAINING_LANGS) {
|
||||||
// Vendored grammars (Swift/Kotlin/Dart) may lack a prebuild on the CI
|
// 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).
|
// platform — skip rather than fail when the grammar can't load (#2197 U4).
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue