mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-29 14:49:36 +08:00
test: stop test runs leaving temp dirs behind (#2622)
Every full test run left about 40 directories in the temp dir, even when it passed. On the Windows dev machine about 16,000 had piled up by mid-October. A full run with TEMP pointed at an empty folder showed where each one came from: - function-ref (20) and ts-field-classification (3): each case's `finally` cleared `tmpDir` right after `cg.destroy()`, so the `afterEach` that removes the dir found nothing to remove. - npm-sdk (3) and liveness-watchdog (3) never removed the dirs they made. - extraction's #1502 suite (3) made a dir in `beforeEach` and had no `afterEach`; pr19-improvements' schema-version case (1) made one inline. - setup-home-sandbox (6 on Windows): Vitest runs no hooks for a file whose every test is skipped, so each platform-gated file left its home sandbox. Each sandbox now records itself in a per-run registry, and a global teardown removes whatever is still listed and names any dir still in use. With the fix, the same full run leaves nothing behind but Node's own reused compile cache. test-home-sandbox gains two tests: a file's home is listed in the run's registry, and the teardown removes every listed home, then the registry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1981354476
commit
2344ce0722
@@ -13721,6 +13721,10 @@ describe('Unsupported-language projects report what they skipped (#1502)', () =>
|
||||
tempDir = createTempDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTempDir(tempDir);
|
||||
});
|
||||
|
||||
it('counts files it could not index, by extension, on the git path', async () => {
|
||||
const runGit = (...args: string[]) =>
|
||||
execFileSync('git', args, { cwd: tempDir, stdio: 'pipe' });
|
||||
|
||||
@@ -108,7 +108,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
expect(sourceNames(cg, callEdges)).toEqual(['direct_caller']);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -152,7 +151,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'handleClick'))).toEqual(['wire']);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -182,7 +180,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
expect(target?.filePath.endsWith('handlers.ts')).toBe(true);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -207,7 +204,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
expect(sourceNames(cg, edges)).not.toContain('wire');
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -235,7 +231,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
expect(target?.filePath.endsWith('real.c')).toBe(true);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -260,7 +255,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
expect(fnRef).toHaveLength(0);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -284,7 +278,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
expect(selfLoops).toHaveLength(0);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -349,7 +342,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
}
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -406,7 +398,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
]);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -456,7 +447,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
}
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -496,7 +486,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
).toHaveLength(0);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -543,7 +532,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'run0'))).toEqual(['thisRef']);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -588,7 +576,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
expect(cg.getIncomingEdges(decoy.id).filter((e) => e.metadata?.fnRef === true)).toHaveLength(0);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -617,7 +604,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
expect(sourceNames(cg, edges)).toEqual(['wire']);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -661,7 +647,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
expect(fnRefEdgesInto(cg, 'dupCmd')).toHaveLength(0);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -702,7 +687,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'load'))).toEqual(['sorter']);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -743,7 +727,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
expect(fnRefEdgesInto(cg, 'title')).toHaveLength(0);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -823,7 +806,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
expect(callers.some((c) => c.node.name === 'get_serializer_class')).toBe(true);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -856,7 +838,6 @@ describe('Function-as-value capture (#756)', () => {
|
||||
expect(fnRefEdgesInto(cg, 'unknown_thing')).toHaveLength(0);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1598,7 +1579,6 @@ func Unknown(obj interface{}) { Submit(obj.Fetch) }
|
||||
expect(sourceNames(cg, fnRefEdgesInto(cg, 'cb_a'))).toEqual(['wire']);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Remove the home sandboxes `setup-home-sandbox.ts` could not.
|
||||
*
|
||||
* Each test file removes its own sandbox in an `afterAll`, but Vitest runs no
|
||||
* hooks at all for a file whose every test is skipped — a platform-gated suite
|
||||
* skips that way, and each one left its home dir behind on every run — and a
|
||||
* straggling child can still hold a file there when `afterAll` runs. So every
|
||||
* sandbox records its path in this run's registry dir, and the end of the run
|
||||
* removes whatever is still listed.
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import type { GlobalSetupContext } from 'vitest/node';
|
||||
|
||||
declare module 'vitest' {
|
||||
export interface ProvidedContext {
|
||||
/** The dir each test file's home sandbox records its path in. */
|
||||
homeSandboxRegistry: string;
|
||||
}
|
||||
}
|
||||
|
||||
export default function setup({ provide }: GlobalSetupContext): () => void {
|
||||
const registry = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-test-homes-'));
|
||||
provide('homeSandboxRegistry', registry);
|
||||
return () => {
|
||||
const held: string[] = [];
|
||||
for (const entry of fs.readdirSync(registry)) {
|
||||
const home = fs.readFileSync(path.join(registry, entry), 'utf8');
|
||||
try {
|
||||
fs.rmSync(home, { recursive: true, force: true, maxRetries: 3 });
|
||||
} catch {
|
||||
held.push(home);
|
||||
}
|
||||
}
|
||||
fs.rmSync(registry, { recursive: true, force: true });
|
||||
if (held.length > 0) {
|
||||
process.stderr.write(`[test teardown] ${held.length} home sandbox dir(s) still in use, left behind:\n`
|
||||
+ held.map((home) => ` ${home}\n`).join(''));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, afterEach } from 'vitest';
|
||||
import { spawn } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
@@ -140,6 +140,19 @@ describe('liveness watchdog (spawned, real watchdog process)', () => {
|
||||
// when the watched DB files keep advancing (a slow synchronous SQLite
|
||||
// statement on degraded storage). ---
|
||||
|
||||
// The dirs holding each case's watched file, removed after it.
|
||||
const progressDirs: string[] = [];
|
||||
afterEach(() => {
|
||||
for (const dir of progressDirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
/** A fresh path for a case's watchdog to watch, in its own temp dir. */
|
||||
function progressFile(): string {
|
||||
const dir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'cg-wd-'));
|
||||
progressDirs.push(dir);
|
||||
return path.join(dir, 'db-wal');
|
||||
}
|
||||
|
||||
/** Grow `file` every 150ms for `forMs`; resolves when done. */
|
||||
function growFile(file: string, forMs: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
@@ -149,7 +162,7 @@ describe('liveness watchdog (spawned, real watchdog process)', () => {
|
||||
}
|
||||
|
||||
it('does NOT kill a blocked loop while the watched files advance (slow store, not a wedge)', async () => {
|
||||
const tmp = path.join(fs.mkdtempSync(path.join(require('os').tmpdir(), 'cg-wd-')), 'db-wal');
|
||||
const tmp = progressFile();
|
||||
fs.writeFileSync(tmp, 'seed');
|
||||
// Base timeout 500ms; the loop blocks for 2.5s (5 timeouts, under the 10×
|
||||
// cap) while the test process grows the watched file. Old behavior: killed
|
||||
@@ -168,7 +181,7 @@ describe('liveness watchdog (spawned, real watchdog process)', () => {
|
||||
}, 15000);
|
||||
|
||||
it('still kills a blocked loop when the watched files do NOT advance (a true wedge)', async () => {
|
||||
const tmp = path.join(fs.mkdtempSync(path.join(require('os').tmpdir(), 'cg-wd-')), 'db-wal');
|
||||
const tmp = progressFile();
|
||||
fs.writeFileSync(tmp, 'seed');
|
||||
// Same blocked loop, nobody grows the file: the base timeout kills it long
|
||||
// before its own exit(5) at 2.5s.
|
||||
@@ -182,7 +195,7 @@ describe('liveness watchdog (spawned, real watchdog process)', () => {
|
||||
}, 15000);
|
||||
|
||||
it('kills at the hard cap even with ongoing file activity (bounded deferral)', async () => {
|
||||
const tmp = path.join(fs.mkdtempSync(path.join(require('os').tmpdir(), 'cg-wd-')), 'db-wal');
|
||||
const tmp = progressFile();
|
||||
fs.writeFileSync(tmp, 'seed');
|
||||
// Base timeout 300ms ⇒ cap 3s. The loop blocks for 8s with continuous file
|
||||
// growth: deferral carries it past 300ms but the cap kills it around ~3s,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* hermetically with no real bundle, network, or registry.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
@@ -23,8 +23,13 @@ const SDK_SRC = path.join(__dirname, '..', 'scripts', 'npm-sdk.js');
|
||||
const target = `${process.platform}-${process.arch}`;
|
||||
const VERSION = '9.9.9-test';
|
||||
|
||||
// Every temp dir a case makes, removed after it.
|
||||
const tmpDirs: string[] = [];
|
||||
|
||||
function mkTmp(label: string): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), `cg-sdk-${label}-`));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `cg-sdk-${label}-`));
|
||||
tmpDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
// A temp node_modules with the main package (npm-sdk.js + package.json). The
|
||||
@@ -75,6 +80,10 @@ function requireSdk(mainPkg: string, env: Record<string, string> = {}) {
|
||||
}
|
||||
|
||||
describe('npm-sdk programmatic entry', () => {
|
||||
afterEach(() => {
|
||||
for (const dir of tmpDirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('re-exports the installed platform bundle library', () => {
|
||||
const { root, mainPkg } = makeConsumer();
|
||||
installPlatformPackage(root, 'platform-lib');
|
||||
|
||||
@@ -305,12 +305,13 @@ describe('Schema v2 Migration', () => {
|
||||
// just makes every schema change edit this test (v9/#1500 was the latest).
|
||||
// A fresh database records the current version, so nothing is pending;
|
||||
// ask a version-0 database instead to see the full migration list.
|
||||
const dbPath = path.join(createTempDir(), 'schema-version.db');
|
||||
const conn = DatabaseConnection.initialize(dbPath);
|
||||
const dir = createTempDir();
|
||||
const conn = DatabaseConnection.initialize(path.join(dir, 'schema-version.db'));
|
||||
const raw = conn.getDb();
|
||||
raw.prepare('DELETE FROM schema_versions').run();
|
||||
const highest = Math.max(...getPendingMigrations(raw).map((m) => m.version));
|
||||
conn.close();
|
||||
cleanupTempDir(dir);
|
||||
|
||||
expect(CURRENT_SCHEMA_VERSION).toBe(highest);
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* Tests that redirect the home dir themselves (spying on `os.homedir`, or
|
||||
* setting HOME and restoring it) keep working: they restore to this sandbox.
|
||||
*/
|
||||
import { afterAll } from 'vitest';
|
||||
import { afterAll, inject } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
@@ -30,6 +30,13 @@ import * as path from 'path';
|
||||
// `/private/var`) sees the same home everything else does.
|
||||
const home = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-test-home-')));
|
||||
|
||||
// Recorded for the end of the run, which removes the home if the afterAll
|
||||
// below never runs (a file whose every test is skipped runs no hooks) or
|
||||
// cannot (global-setup-home-sandbox.ts).
|
||||
const registry = inject('homeSandboxRegistry');
|
||||
const registryEntry = registry ? path.join(registry, path.basename(home)) : undefined;
|
||||
if (registryEntry) fs.writeFileSync(registryEntry, home);
|
||||
|
||||
process.env.HOME = home;
|
||||
process.env.USERPROFILE = home;
|
||||
if (process.platform === 'win32') {
|
||||
@@ -60,7 +67,8 @@ process.env.GIT_CONFIG_GLOBAL = gitConfig;
|
||||
afterAll(() => {
|
||||
try {
|
||||
fs.rmSync(home, { recursive: true, force: true, maxRetries: 3 });
|
||||
if (registryEntry) fs.rmSync(registryEntry, { force: true });
|
||||
} catch {
|
||||
/* a straggling child still holding a file there; it's in the temp dir */
|
||||
/* a straggling child still holding a file there; the end of the run retries */
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
* nothing a test runs — in-process or spawned — resolves the developer's real
|
||||
* home, Claude profile, daemon registry or git identity.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, inject } from 'vitest';
|
||||
import type { GlobalSetupContext } from 'vitest/node';
|
||||
import { execFileSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { claudeTarget } from '../src/installer/targets/claude';
|
||||
import { getRegistryDir } from '../src/mcp/daemon-registry';
|
||||
import setupHomeSandboxRun from './global-setup-home-sandbox';
|
||||
|
||||
const tmpRoot = fs.realpathSync(os.tmpdir());
|
||||
const under = (p: string, dir: string): boolean => path.resolve(p).startsWith(dir + path.sep);
|
||||
@@ -62,4 +64,28 @@ describe('test home sandbox', () => {
|
||||
expect(under(process.env.APPDATA ?? '', home)).toBe(true);
|
||||
expect(under(process.env.LOCALAPPDATA ?? '', home)).toBe(true);
|
||||
});
|
||||
|
||||
// A file whose every test is skipped runs no hooks, so its own afterAll never
|
||||
// removes its home: the end of the run removes every home still listed.
|
||||
it("records this file's home in the run's registry", () => {
|
||||
const home = sandboxHome();
|
||||
const entry = path.join(inject('homeSandboxRegistry'), path.basename(home));
|
||||
expect(fs.readFileSync(entry, 'utf8')).toBe(home);
|
||||
});
|
||||
|
||||
it('the end of the run removes every home still listed, then the registry', () => {
|
||||
let registry = '';
|
||||
const teardown = setupHomeSandboxRun({
|
||||
provide(_key: string, value: string) { registry = value; },
|
||||
} as unknown as GlobalSetupContext);
|
||||
const homes = ['skipped', 'held'].map((label) => {
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), `codegraph-test-home-${label}-`));
|
||||
fs.writeFileSync(path.join(home, '.gitconfig'), '');
|
||||
fs.writeFileSync(path.join(registry, path.basename(home)), home);
|
||||
return home;
|
||||
});
|
||||
teardown();
|
||||
expect(homes.filter((home) => fs.existsSync(home))).toEqual([]);
|
||||
expect(fs.existsSync(registry)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,7 +99,6 @@ describe('TS/JS class field classification (#808)', () => {
|
||||
expect(fontsProp.signature).toBe('Fonts fonts');
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -126,7 +125,6 @@ describe('TS/JS class field classification (#808)', () => {
|
||||
expect(cg.getNodesByName('onClick')[0]?.kind).toBe('method');
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -153,7 +151,6 @@ describe('TS/JS class field classification (#808)', () => {
|
||||
expect(fnRefs.length).toBeGreaterThan(0);
|
||||
} finally {
|
||||
cg.destroy();
|
||||
tmpDir = undefined;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+3
-2
@@ -12,8 +12,9 @@ export default defineConfig({
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['__tests__/**/*.test.ts'],
|
||||
// Suites that spawn the built CLI need a current dist/ (#1879).
|
||||
globalSetup: ['./__tests__/global-setup-dist.ts'],
|
||||
// Suites that spawn the built CLI need a current dist/ (#1879), and the end
|
||||
// of the run removes any home sandbox its own test file could not.
|
||||
globalSetup: ['./__tests__/global-setup-dist.ts', './__tests__/global-setup-home-sandbox.ts'],
|
||||
// A throwaway home dir (and git global config) per test file, so nothing
|
||||
// the suite runs can write to the developer's real one (#2275).
|
||||
setupFiles: ['./__tests__/setup-home-sandbox.ts'],
|
||||
|
||||
Reference in New Issue
Block a user