diff --git a/src/managers/BreakpointManager.spec.ts b/src/managers/BreakpointManager.spec.ts index 6410dc57..3ce56940 100644 --- a/src/managers/BreakpointManager.spec.ts +++ b/src/managers/BreakpointManager.spec.ts @@ -10,7 +10,7 @@ let n = fileUtils.standardizePath.bind(fileUtils); import type { SourceLocation } from '../managers/LocationManager'; import { LocationManager } from '../managers/LocationManager'; import { SourceMapManager } from './SourceMapManager'; -import { expectPickEquals, pickArray } from '../testHelpers.spec'; +import { expectPickEquals, pickArray, removeTempDir } from '../testHelpers.spec'; import { createSandbox } from 'sinon'; const sinon = createSandbox(); @@ -103,7 +103,7 @@ describe('BreakpointManager', () => { afterEach(() => { sinon.restore(); - fsExtra.removeSync(tmpDir); + removeTempDir(tmpDir, 'BreakpointManager'); }); describe('reset', () => { @@ -415,7 +415,7 @@ describe('BreakpointManager', () => { }); afterEach(() => { - fsExtra.removeSync(tmpDir); + removeTempDir(tmpDir, 'BreakpointManager'); }); it('works with normal flow', async () => { diff --git a/src/managers/LocationManager.spec.ts b/src/managers/LocationManager.spec.ts index ca2294ac..29cd43f4 100644 --- a/src/managers/LocationManager.spec.ts +++ b/src/managers/LocationManager.spec.ts @@ -4,6 +4,7 @@ import { SourceMapConsumer, SourceNode } from 'source-map'; import { standardizePath as s } from '../FileUtils'; import { LocationManager } from './LocationManager'; import { SourceMapManager } from './SourceMapManager'; +import { removeTempDir } from '../testHelpers.spec'; let tempDir = s`${process.cwd()}/.tmp`; const rootDir = s`${tempDir}/rootDir`; @@ -20,7 +21,7 @@ describe('LocationManager', () => { beforeEach(() => { sourceMapManager = new SourceMapManager(); locationManager = new LocationManager(sourceMapManager); - fsExtra.removeSync(tempDir); + removeTempDir(tempDir, 'LocationManager'); fsExtra.ensureDirSync(`${rootDir}/source`); fsExtra.ensureDirSync(`${stagingDir}/source`); for (let sourceDir of sourceDirs) { @@ -28,7 +29,7 @@ describe('LocationManager', () => { } }); afterEach(() => { - fsExtra.removeSync(tempDir); + removeTempDir(tempDir, 'LocationManager'); }); describe('getSourceLocation', () => { diff --git a/src/managers/ProjectManager.spec.ts b/src/managers/ProjectManager.spec.ts index 735f2188..35a6a636 100644 --- a/src/managers/ProjectManager.spec.ts +++ b/src/managers/ProjectManager.spec.ts @@ -11,6 +11,7 @@ import { BreakpointManager } from './BreakpointManager'; import { SourceMapManager } from './SourceMapManager'; import { LocationManager } from './LocationManager'; import * as decompress from 'decompress'; +import { removeTempDir, emptyTempDir } from '../testHelpers.spec'; let sinon = sinonActual.createSandbox(); let n = fileUtils.standardizePath.bind(fileUtils); @@ -25,12 +26,12 @@ let compLibstagingDir = s`${rootDir}/component-libraries/CompLibA`; beforeEach(() => { fsExtra.ensureDirSync(tempPath); - fsExtra.emptyDirSync(tempPath); + emptyTempDir(tempPath, 'ProjectManager'); sinon.restore(); }); afterEach(() => { fsExtra.ensureDirSync(tempPath); - fsExtra.emptyDirSync(tempPath); + emptyTempDir(tempPath, 'ProjectManager'); }); describe('ProjectManager', () => { @@ -432,7 +433,7 @@ describe('Project', () => { describe('stage', () => { afterEach(() => { try { - fsExtra.removeSync(tempPath); + removeTempDir(tempPath, 'ProjectManager'); } catch (e) { } }); it('actually stages the project', async () => { @@ -542,7 +543,7 @@ describe('Project', () => { describe('preprocessStagingFiles', () => { afterEach(() => { try { - fsExtra.removeSync(tempPath); + removeTempDir(tempPath, 'ProjectManager'); } catch (e) { } }); @@ -1324,7 +1325,7 @@ describe('Project', () => { describe('scriptReferencedFiles', () => { afterEach(() => { try { - fsExtra.removeSync(tempPath); + removeTempDir(tempPath, 'ProjectManager'); } catch (e) { } }); @@ -1488,16 +1489,16 @@ describe('Project', () => { fsExtra.writeFileSync(raleTrackerTaskFileLocation, ``); }); after(() => { - fsExtra.removeSync(tempPath); + removeTempDir(tempPath, 'ProjectManager'); fsExtra.removeSync(raleTrackerTaskFileLocation); }); afterEach(() => { - fsExtra.emptyDirSync(tempPath); + emptyTempDir(tempPath, 'ProjectManager'); fsExtra.rmdirSync(tempPath); }); async function doTest(fileContents: string, expectedContents: string, fileExt = 'brs') { - fsExtra.emptyDirSync(tempPath); + emptyTempDir(tempPath, 'ProjectManager'); let folder = s`${tempPath}/findMainFunctionTests/`; fsExtra.mkdirSync(folder); @@ -1600,17 +1601,17 @@ describe('Project', () => { fsExtra.writeFileSync(componentsFilePath, `' ${componentsFilePath}`); }); after(() => { - fsExtra.removeSync(tempPath); + removeTempDir(tempPath, 'ProjectManager'); fsExtra.emptyDirSync(rdbFilesBasePath); fsExtra.rmdirSync(rdbFilesBasePath); }); afterEach(() => { - fsExtra.emptyDirSync(tempPath); + emptyTempDir(tempPath, 'ProjectManager'); fsExtra.rmdirSync(tempPath); }); async function doTest(fileContents: string, expectedContents: string, fileExt = 'brs', injectRdbOnDeviceComponent = true) { - fsExtra.emptyDirSync(tempPath); + emptyTempDir(tempPath, 'ProjectManager'); let folder = s`${tempPath}/findMainFunctionTests/`; fsExtra.mkdirSync(folder); @@ -1636,7 +1637,7 @@ describe('Project', () => { //which fast-glob treats as escape characters. Without normalization, the glob //matches nothing and no files get copied. it('copies the RDB files when rdbFilesBasePath is an absolute path with native separators', async () => { - fsExtra.emptyDirSync(tempPath); + emptyTempDir(tempPath, 'ProjectManager'); let folder = s`${tempPath}/copyAndTransformRDBTests/`; fsExtra.mkdirSync(folder); let filePath = s`${folder}/main.brs`; @@ -1658,7 +1659,7 @@ describe('Project', () => { }); it('does not copy files when injectRdbOnDeviceComponent is false', async () => { - fsExtra.emptyDirSync(tempPath); + emptyTempDir(tempPath, 'ProjectManager'); let folder = s`${tempPath}/copyAndTransformRDBTests/`; fsExtra.mkdirSync(folder); let filePath = s`${folder}/main.brs`; @@ -1680,7 +1681,7 @@ describe('Project', () => { }); it('does not copy files when rdbFilesBasePath is not set', async () => { - fsExtra.emptyDirSync(tempPath); + emptyTempDir(tempPath, 'ProjectManager'); let folder = s`${tempPath}/copyAndTransformRDBTests/`; fsExtra.mkdirSync(folder); let filePath = s`${folder}/main.brs`; diff --git a/src/managers/SourceMapManager.spec.ts b/src/managers/SourceMapManager.spec.ts index ec8e21b5..9baf23a6 100644 --- a/src/managers/SourceMapManager.spec.ts +++ b/src/managers/SourceMapManager.spec.ts @@ -3,6 +3,7 @@ import * as fsExtra from 'fs-extra'; import * as path from 'path'; import { standardizePath as s } from '../FileUtils'; import { SourceMapManager } from './SourceMapManager'; +import { removeTempDir } from '../testHelpers.spec'; let tmpPath = s`${process.cwd()}/.tmp`; describe('SourceMapManager', () => { let manager: SourceMapManager; @@ -13,7 +14,7 @@ describe('SourceMapManager', () => { manager = new SourceMapManager(); }); afterEach(() => { - fsExtra.removeSync(tmpPath); + removeTempDir(tmpPath, 'SourceMapManager'); }); it('constructs', () => { diff --git a/src/testHelpers.spec.ts b/src/testHelpers.spec.ts index 5c4a651d..5c073d4a 100644 --- a/src/testHelpers.spec.ts +++ b/src/testHelpers.spec.ts @@ -9,12 +9,73 @@ export const tempDir = s`${__dirname}/../.tmp`; export const rootDir = s`${tempDir}/rootDir`; export const stagingDir = s`${tempDir}/stagingDir`; +/** + * List every path remaining inside `dir` (recursively). + */ +function listTempLeftovers(dir: string): string[] { + const result: string[] = []; + const walk = (current: string) => { + let entries: string[]; + try { + entries = fsExtra.readdirSync(current); + } catch { + return; + } + for (const entry of entries) { + const fullPath = `${current}/${entry}`; + result.push(fullPath); + try { + if (fsExtra.statSync(fullPath).isDirectory()) { + walk(fullPath); + } + } catch { + //entry may have been removed concurrently; ignore + } + } + }; + walk(dir); + return result.sort(); +} + +/** + * Run a temp-dir cleanup operation. If it throws (most often Windows `ENOTEMPTY`, which happens when an + * earlier test left a file or handle open in the shared temp dir), log exactly what is still present so CI + * tells us which leftovers locked the dir, then re-throw. This diagnoses the failure; it does not hide it. + */ +function cleanupTempDir(label: string, dir: string, action: () => void) { + try { + action(); + } catch (error) { + const leftovers = listTempLeftovers(dir); + console.error(`[temp-teardown] ${label}: ${(error as Error).message}`); + console.error(`[temp-teardown] ${label}: ${leftovers.length} path(s) still present under ${dir}:`); + for (const leftover of leftovers) { + console.error(`[temp-teardown] ${leftover}`); + } + throw error; + } +} + +/** + * `fsExtra.removeSync` for a temp dir, with diagnostics logged if the removal fails. + */ +export function removeTempDir(dir: string, label: string) { + cleanupTempDir(label, dir, () => fsExtra.removeSync(dir)); +} + +/** + * `fsExtra.emptyDirSync` for a temp dir, with diagnostics logged if the empty fails. + */ +export function emptyTempDir(dir: string, label: string) { + cleanupTempDir(label, dir, () => fsExtra.emptyDirSync(dir)); +} + beforeEach(() => { - fsExtra.emptyDirSync(tempDir); + emptyTempDir(tempDir, 'global beforeEach'); }); afterEach(() => { - fsExtra.removeSync(tempDir); + removeTempDir(tempDir, 'global afterEach'); }); /**