From d15453edfdad85b25ff55d46bd7333227226f540 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Sun, 24 Nov 2024 01:50:04 +0100 Subject: [PATCH] feat(temp-dir): allow multiple instances to share the same temp-dir (#5120) * feat(temp-dir): allow multiple instances to share the same temp-dir Allow multiple runs of Stryker to share the same temp dir. This means that Stryker will _no longer remove the entire `.stryker-tmp` directory when it ran successfully (since other `sandbox-xxx` directories could be owned by other processes). Note: this is implemented in preparation for the implementation of the mutation server protocol (#5086) * fix test on windows --- .../src/reporters/event-recorder-reporter.ts | 2 +- packages/core/src/sandbox/sandbox.ts | 22 ++-- packages/core/src/utils/file-utils.ts | 17 ++- packages/core/src/utils/object-utils.ts | 8 -- .../core/src/utils/temporary-directory.ts | 54 +++++---- .../integration/utils/file-utils.it.spec.ts | 2 +- .../reporters/event-recorder-reporter.spec.ts | 10 +- .../core/test/unit/sandbox/sandbox.spec.ts | 48 ++------ .../unit/utils/temporary-directory.spec.ts | 110 ++++++++++++------ 9 files changed, 132 insertions(+), 141 deletions(-) diff --git a/packages/core/src/reporters/event-recorder-reporter.ts b/packages/core/src/reporters/event-recorder-reporter.ts index 9826d0eb34..93daa0892b 100644 --- a/packages/core/src/reporters/event-recorder-reporter.ts +++ b/packages/core/src/reporters/event-recorder-reporter.ts @@ -21,7 +21,7 @@ export class EventRecorderReporter implements StrictReporter { private readonly log: Logger, private readonly options: StrykerOptions, ) { - this.createBaseFolderTask = fileUtils.cleanFolder(this.options.eventReporter.baseDir); + this.createBaseFolderTask = fileUtils.cleanDir(this.options.eventReporter.baseDir); } private writeToFile(methodName: keyof Reporter, data: any) { diff --git a/packages/core/src/sandbox/sandbox.ts b/packages/core/src/sandbox/sandbox.ts index 5d4c0e80bb..c917a7b6ba 100644 --- a/packages/core/src/sandbox/sandbox.ts +++ b/packages/core/src/sandbox/sandbox.ts @@ -23,14 +23,10 @@ export class Sandbox implements Disposable { * Either an actual sandbox directory, or the cwd when running in --inPlace mode */ public readonly workingDirectory: string; - /** + /**11 * The backup directory when running in --inPlace mode */ private readonly backupDirectory: string = ''; - /** - * The sandbox dir or the backup dir when running in `--inPlace` mode - */ - private readonly tempDirectory: string; public static readonly inject = tokens( commonTokens.options, @@ -41,32 +37,32 @@ export class Sandbox implements Disposable { coreTokens.unexpectedExitRegistry, ); + /** + * @param temporaryDirectory The sandbox dir or the backup dir when running in `--inPlace` mode + */ constructor( private readonly options: StrykerOptions, private readonly log: Logger, - private readonly temporaryDirectory: I, + temporaryDirectory: I, private readonly project: Project, private readonly execCommand: typeof execaCommand, unexpectedExitHandler: I, ) { if (options.inPlace) { this.workingDirectory = process.cwd(); - this.backupDirectory = temporaryDirectory.getRandomDirectory('backup'); - this.tempDirectory = this.backupDirectory; + this.backupDirectory = temporaryDirectory.path; this.log.info( 'In place mode is enabled, Stryker will be overriding YOUR files. Find your backup at: %s', path.relative(process.cwd(), this.backupDirectory), ); - unexpectedExitHandler.registerHandler(this.dispose.bind(this, true)); + unexpectedExitHandler.registerHandler(this.dispose.bind(this, /* unexpected */ true)); } else { - this.workingDirectory = temporaryDirectory.getRandomDirectory('sandbox'); - this.tempDirectory = this.workingDirectory; + this.workingDirectory = temporaryDirectory.path; this.log.debug('Creating a sandbox for files in %s', this.workingDirectory); } } public async init(): Promise { - await this.temporaryDirectory.createDirectory(this.tempDirectory); await this.fillSandbox(); await this.runBuildCommand(); await this.symlinkNodeModulesIfNeeded(); @@ -81,7 +77,7 @@ export class Sandbox implements Disposable { } public originalFileFor(sandboxFileName: string): string { - return path.resolve(sandboxFileName).replace(this.workingDirectory, process.cwd()); + return path.resolve(sandboxFileName).replace(path.resolve(this.workingDirectory), process.cwd()); } private async fillSandbox(): Promise { diff --git a/packages/core/src/utils/file-utils.ts b/packages/core/src/utils/file-utils.ts index 16e4307e41..65d0f80dab 100644 --- a/packages/core/src/utils/file-utils.ts +++ b/packages/core/src/utils/file-utils.ts @@ -4,17 +4,16 @@ import fs from 'fs'; import { isErrnoException } from '@stryker-mutator/util'; export const fileUtils = { - deleteDir(dir: string): Promise { - return fs.promises.rm(dir, { recursive: true, force: true }); - }, - - async cleanFolder(folderName: string): Promise { + /** + * Cleans the dir by creating it. + */ + async cleanDir(dirName: string): Promise { try { - await fs.promises.lstat(folderName); - await this.deleteDir(folderName); - return fs.promises.mkdir(folderName, { recursive: true }); + await fs.promises.lstat(dirName); + await fs.promises.rm(dirName, { recursive: true, force: true }); + return fs.promises.mkdir(dirName, { recursive: true }); } catch { - return fs.promises.mkdir(folderName, { recursive: true }); + return fs.promises.mkdir(dirName, { recursive: true }); } }, diff --git a/packages/core/src/utils/object-utils.ts b/packages/core/src/utils/object-utils.ts index eb7eaf1af5..77e1e29519 100644 --- a/packages/core/src/utils/object-utils.ts +++ b/packages/core/src/utils/object-utils.ts @@ -71,12 +71,4 @@ export const objectUtils = { } }); }, - - /** - * Creates a random integer number. - * @returns A random integer. - */ - random(): number { - return Math.ceil(Math.random() * 10000000); - }, }; diff --git a/packages/core/src/utils/temporary-directory.ts b/packages/core/src/utils/temporary-directory.ts index 2a84f9234b..b7e408a1ee 100644 --- a/packages/core/src/utils/temporary-directory.ts +++ b/packages/core/src/utils/temporary-directory.ts @@ -6,57 +6,55 @@ import { Logger } from '@stryker-mutator/api/logging'; import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; import { Disposable } from 'typed-inject'; -import { fileUtils } from './file-utils.js'; -import { objectUtils } from './object-utils.js'; - export class TemporaryDirectory implements Disposable { - private readonly temporaryDirectory: string; - private isInitialized = false; + #temporaryDirectory?: string; public removeDuringDisposal: boolean; public static readonly inject = tokens(commonTokens.logger, commonTokens.options); constructor( private readonly log: Logger, - options: StrykerOptions, + private readonly options: StrykerOptions, ) { - this.temporaryDirectory = path.resolve(options.tempDirName); this.removeDuringDisposal = Boolean(options.cleanTempDir); } public async initialize(): Promise { - this.log.debug('Using temp directory "%s"', this.temporaryDirectory); - await fs.promises.mkdir(this.temporaryDirectory, { recursive: true }); - this.isInitialized = true; + const parent = path.resolve(this.options.tempDirName); + await fs.promises.mkdir(parent, { recursive: true }); + this.#temporaryDirectory = await fs.promises.mkdtemp(path.join(parent, this.options.inPlace ? 'backup-' : 'sandbox-')); + this.log.debug('Using temp directory "%s"', this.#temporaryDirectory); } - public getRandomDirectory(prefix: string): string { - return path.resolve(this.temporaryDirectory, `${prefix}${objectUtils.random()}`); + get path() { + if (!this.#temporaryDirectory) { + this.#throwNotInitialized(); + } + return this.#temporaryDirectory; } - /** - * Creates a new random directory with the specified prefix. - * @returns The path to the directory. - */ - public async createDirectory(name: string): Promise { - if (!this.isInitialized) { - throw new Error('initialize() was not called!'); - } - await fs.promises.mkdir(path.resolve(this.temporaryDirectory, name), { recursive: true }); + #throwNotInitialized(): never { + throw new Error('initialize() was not called!'); } /** * Deletes the Stryker-temp directory */ public async dispose(): Promise { - if (!this.isInitialized) { - throw new Error('initialize() was not called!'); - } - if (this.removeDuringDisposal) { - this.log.debug('Deleting stryker temp directory %s', this.temporaryDirectory); + if (this.removeDuringDisposal && this.#temporaryDirectory) { + this.log.debug('Deleting stryker temp directory %s', this.#temporaryDirectory); try { - await fileUtils.deleteDir(this.temporaryDirectory); + await fs.promises.rm(this.#temporaryDirectory, { recursive: true, force: true }); } catch { - this.log.info(`Failed to delete stryker temp directory ${this.temporaryDirectory}`); + this.log.info(`Failed to delete stryker temp directory ${this.#temporaryDirectory}`); + } + const lingeringDirectories = await fs.promises.readdir(this.options.tempDirName); + if (!lingeringDirectories.length) { + try { + await fs.promises.rmdir(this.options.tempDirName); + } catch (e) { + // It's not THAT important, maybe another StrykerJS process started in the meantime. + this.log.debug(`Failed to clean temp ${path.basename(this.options.tempDirName)}`, e); + } } } } diff --git a/packages/core/test/integration/utils/file-utils.it.spec.ts b/packages/core/test/integration/utils/file-utils.it.spec.ts index a7c2313410..00da34f766 100644 --- a/packages/core/test/integration/utils/file-utils.it.spec.ts +++ b/packages/core/test/integration/utils/file-utils.it.spec.ts @@ -13,7 +13,7 @@ describe('fileUtils', () => { const to = path.resolve(os.tmpdir(), 'moveDirectoryRecursiveSyncTo'); afterEach(async () => { - await Promise.all([fileUtils.deleteDir(from), fileUtils.deleteDir(to)]); + await Promise.all([fsPromises.rm(from, { recursive: true, force: true }), fsPromises.rm(to, { recursive: true, force: true })]); }); it('should override target files', async () => { diff --git a/packages/core/test/unit/reporters/event-recorder-reporter.spec.ts b/packages/core/test/unit/reporters/event-recorder-reporter.spec.ts index 0781e2ca7f..d0f4acdadf 100644 --- a/packages/core/test/unit/reporters/event-recorder-reporter.spec.ts +++ b/packages/core/test/unit/reporters/event-recorder-reporter.spec.ts @@ -11,23 +11,23 @@ import { fileUtils } from '../../../src/utils/file-utils.js'; describe(EventRecorderReporter.name, () => { let sut: StrictReporter; - let cleanFolderStub: sinon.SinonStub; + let cleanDirStub: sinon.SinonStub; let writeFileStub: sinon.SinonStub; beforeEach(() => { - cleanFolderStub = sinon.stub(fileUtils, 'cleanFolder'); writeFileStub = sinon.stub(fs.promises, 'writeFile'); + cleanDirStub = sinon.stub(fileUtils, 'cleanDir'); }); describe('when constructed with empty options', () => { describe('and cleanFolder resolves correctly', () => { beforeEach(() => { - cleanFolderStub.returns(Promise.resolve()); + cleanDirStub.returns(Promise.resolve()); sut = testInjector.injector.injectClass(EventRecorderReporter); }); it('should clean the baseFolder', () => { - expect(fileUtils.cleanFolder).to.have.been.calledWith('reports/mutation/events'); + expect(fileUtils.cleanDir).to.have.been.calledWith('reports/mutation/events'); }); const arrangeActAssertEvent = (eventName: keyof Reporter) => { @@ -68,7 +68,7 @@ describe(EventRecorderReporter.name, () => { let expectedError: Error; beforeEach(() => { expectedError = new Error('Some error 1'); - cleanFolderStub.rejects(expectedError); + cleanDirStub.rejects(expectedError); sut = testInjector.injector.injectClass(EventRecorderReporter); }); diff --git a/packages/core/test/unit/sandbox/sandbox.spec.ts b/packages/core/test/unit/sandbox/sandbox.spec.ts index 706f12cea9..1dc41be0e5 100644 --- a/packages/core/test/unit/sandbox/sandbox.spec.ts +++ b/packages/core/test/unit/sandbox/sandbox.spec.ts @@ -25,12 +25,11 @@ describe(Sandbox.name, () => { let unexpectedExitHandlerMock: sinon.SinonStubbedInstance>; let moveDirectoryRecursiveSyncStub: sinon.SinonStub; let fsTestDouble: FileSystemTestDouble; - const SANDBOX_WORKING_DIR = path.resolve('.stryker-tmp/sandbox-123'); - const BACKUP_DIR = 'backup-123'; + const SANDBOX_WORKING_DIR = path.join('.stryker-tmp', 'sandbox-123'); beforeEach(() => { temporaryDirectoryMock = sinon.createStubInstance(TemporaryDirectory); - temporaryDirectoryMock.getRandomDirectory.withArgs('sandbox').returns(SANDBOX_WORKING_DIR).withArgs('backup').returns(BACKUP_DIR); + sinon.stub(temporaryDirectoryMock, 'path').value(SANDBOX_WORKING_DIR); symlinkJunctionStub = sinon.stub(fileUtils, 'symlinkJunction'); findNodeModulesListStub = sinon.stub(fileUtils, 'findNodeModulesList'); moveDirectoryRecursiveSyncStub = sinon.stub(fileUtils, 'moveDirectoryRecursiveSync'); @@ -68,13 +67,6 @@ describe(Sandbox.name, () => { testInjector.options.inPlace = false; }); - it('should have created a sandbox folder', async () => { - const sut = createSut(); - await sut.init(); - expect(temporaryDirectoryMock.getRandomDirectory).calledWith('sandbox'); - expect(temporaryDirectoryMock.createDirectory).calledWith(SANDBOX_WORKING_DIR); - }); - it('should copy regular input files', async () => { fsTestDouble.files[path.resolve('a', 'main.js')] = 'foo("bar")'; fsTestDouble.files[path.resolve('a', 'b.txt')] = 'b content'; @@ -113,13 +105,6 @@ describe(Sandbox.name, () => { testInjector.options.inPlace = true; }); - it('should have created a backup directory', async () => { - const sut = createSut(); - await sut.init(); - expect(temporaryDirectoryMock.getRandomDirectory).calledWith('backup'); - expect(temporaryDirectoryMock.createDirectory).calledWith(BACKUP_DIR); - }); - it('should not override the current file if no changes were detected', async () => { fsTestDouble.files[path.resolve('a', 'b.txt')] = 'b content'; const sut = createSut(); @@ -150,7 +135,7 @@ describe(Sandbox.name, () => { fsTestDouble.files[fileName] = originalContent; const project = new Project(fsTestDouble, { [fileName]: { mutate: true } }); project.files.get(fileName)!.setContent(mutatedContent); - const expectedBackupFileName = path.join(path.join(BACKUP_DIR, 'a'), 'b.js'); + const expectedBackupFileName = path.join(path.join(SANDBOX_WORKING_DIR, 'a'), 'b.js'); // Act const sut = createSut(project); @@ -169,7 +154,7 @@ describe(Sandbox.name, () => { fsTestDouble.files[fileName] = originalContent; const project = new Project(fsTestDouble, { [fileName]: { mutate: true } }); project.files.get(fileName)!.setContent(mutatedContent); - const expectedBackupFileName = path.join(path.join(BACKUP_DIR, 'a'), 'b.js'); + const expectedBackupFileName = path.join(path.join(SANDBOX_WORKING_DIR, 'a'), 'b.js'); // Act const sut = createSut(project); @@ -280,7 +265,7 @@ describe(Sandbox.name, () => { testInjector.options.inPlace = true; const sut = createSut(); sut.dispose(); - expect(moveDirectoryRecursiveSyncStub).calledWith(BACKUP_DIR, process.cwd()); + sinon.assert.calledWithExactly(moveDirectoryRecursiveSyncStub, SANDBOX_WORKING_DIR, process.cwd()); }); it('should recover from the backup dir if stryker exits unexpectedly while inPlace = true', () => { @@ -288,8 +273,8 @@ describe(Sandbox.name, () => { const errorStub = sinon.stub(console, 'error'); createSut(); unexpectedExitHandlerMock.registerHandler.callArg(0); - expect(moveDirectoryRecursiveSyncStub).calledWith(BACKUP_DIR, process.cwd()); - expect(errorStub).calledWith(`Detecting unexpected exit, recovering original files from ${BACKUP_DIR}`); + expect(moveDirectoryRecursiveSyncStub).calledWith(SANDBOX_WORKING_DIR, process.cwd()); + expect(errorStub).calledWith(`Detecting unexpected exit, recovering original files from ${SANDBOX_WORKING_DIR}`); }); }); @@ -308,25 +293,6 @@ describe(Sandbox.name, () => { }); }); - // describe(Sandbox.prototype.sandboxFileFor.name, () => { - // it('should return the sandbox file if exists', async () => { - // const originalFileName = path.resolve('src/foo.js'); - // fsTestDouble.push(new File(originalFileName, '')); - // const sut = createSut(); - // await sut.init(); - // const actualSandboxFile = sut.sandboxFileFor(originalFileName); - // expect(actualSandboxFile).eq(path.join(SANDBOX_WORKING_DIR, 'src/foo.js')); - // }); - - // it("should throw when the sandbox file doesn't exists", async () => { - // const notExistingFile = 'src/bar.js'; - // fsTestDouble.push(new File(path.resolve('src/foo.js'), '')); - // const sut = createSut(); - // await sut.init(); - // expect(() => sut.sandboxFileFor(notExistingFile)).throws('Cannot find sandbox file for src/bar.js'); - // }); - // }); - describe(Sandbox.prototype.originalFileFor.name, () => { it('should remap the file to the original', async () => { const sut = createSut(); diff --git a/packages/core/test/unit/utils/temporary-directory.spec.ts b/packages/core/test/unit/utils/temporary-directory.spec.ts index d665864534..1c6db22c74 100644 --- a/packages/core/test/unit/utils/temporary-directory.spec.ts +++ b/packages/core/test/unit/utils/temporary-directory.spec.ts @@ -4,80 +4,90 @@ import fs from 'fs'; import { StrykerOptions } from '@stryker-mutator/api/core'; import { commonTokens } from '@stryker-mutator/api/plugin'; import { factory, testInjector } from '@stryker-mutator/test-helpers'; -import { expect } from 'chai'; import sinon from 'sinon'; -import { fileUtils } from '../../../src/utils/file-utils.js'; -import { objectUtils } from '../../../src/utils/object-utils.js'; import { TemporaryDirectory } from '../../../src/utils/temporary-directory.js'; +import { expect } from 'chai'; describe(TemporaryDirectory.name, () => { - let randomStub: sinon.SinonStubbedMember; - let deleteDirStub: sinon.SinonStub; + let mkdtempStub: sinon.SinonStubbedMember; + let rmStub: sinon.SinonStubbedMember; let mkdirStub: sinon.SinonStubbedMember<(typeof fs.promises)['mkdir']>; + let readdirStub: sinon.SinonStubbedMember; + let rmdirStub: sinon.SinonStubbedMember; const tempDirName = '.stryker-tmp'; + const sandboxPath = path.resolve(tempDirName, 'sandbox-123'); beforeEach(() => { mkdirStub = sinon.stub(fs.promises, 'mkdir'); sinon.stub(fs.promises, 'writeFile'); - deleteDirStub = sinon.stub(fileUtils, 'deleteDir'); - randomStub = sinon.stub(objectUtils, 'random'); + rmStub = sinon.stub(fs.promises, 'rm'); + mkdtempStub = sinon.stub(fs.promises, 'mkdtemp'); + readdirStub = sinon.stub(fs.promises, 'readdir'); + rmdirStub = sinon.stub(fs.promises, 'rmdir'); + readdirStub.resolves([]); + mkdtempStub.resolves(path.resolve(tempDirName, 'sandbox-123')); }); function createSut(options?: Partial): TemporaryDirectory { - return testInjector.injector - .provideValue(commonTokens.logger, factory.logger()) - .provideValue(commonTokens.options, factory.strykerOptions(options)) - .injectClass(TemporaryDirectory); + return testInjector.injector.provideValue(commonTokens.options, factory.strykerOptions(options)).injectClass(TemporaryDirectory); } - describe(TemporaryDirectory.prototype.getRandomDirectory.name, () => { - it('should return a random directory with provided prefix', () => { + describe(TemporaryDirectory.prototype.initialize.name, () => { + it('should create the .stryker-tmp directory', async () => { const sut = createSut(); - randomStub.returns(126891); - expect(sut.getRandomDirectory('stryker-prefix-')).eq(path.resolve(tempDirName, 'stryker-prefix-126891')); + await sut.initialize(); + sinon.assert.calledWithExactly(mkdirStub, path.resolve(tempDirName), { recursive: true }); }); - }); - describe(TemporaryDirectory.prototype.createDirectory.name, () => { - it('should create dir with correct path', async () => { + it('should create a sandbox directory', async () => { const sut = createSut(); await sut.initialize(); - await sut.createDirectory('some-dir'); + sinon.assert.calledWithExactly(mkdtempStub, path.resolve(tempDirName, 'sandbox-')); + }); + + it('should create a backup directory when `inPlace` is set', async () => { + const sut = createSut({ inPlace: true }); + await sut.initialize(); + sinon.assert.calledWithExactly(mkdtempStub, path.resolve(tempDirName, 'backup-')); + }); + }); - sinon.assert.calledTwice(mkdirStub); - sinon.assert.calledWith(mkdirStub, path.resolve(tempDirName, 'some-dir')); + describe('path', () => { + it('should return the path when initialized', async () => { + const sut = createSut(); + await sut.initialize(); + expect(sut.path).to.equal(sandboxPath); }); - it('should reject when temp directory is not initialized', async () => { + + it('should throw an error when the path is requested before initialization', () => { const sut = createSut(); - await expect(sut.createDirectory('some-dir')).rejected; + expect(() => sut.path).to.throw('initialize() was not called!'); }); }); describe('dispose', () => { it('should remove the dir if cleanTempDir option is true', async () => { - const expectedPath = path.resolve(tempDirName); - deleteDirStub.resolves(); + rmStub.resolves(); const sut = createSut({ cleanTempDir: true }); await sut.initialize(); await sut.dispose(); - expect(fileUtils.deleteDir).calledWith(expectedPath); + sinon.assert.calledWithExactly(rmStub, sandboxPath, { recursive: true, force: true }); }); it("should remove the dir if cleanTempDir option is 'always'", async () => { - const expectedPath = path.resolve(tempDirName); - deleteDirStub.resolves(); + rmStub.resolves(); const sut = createSut({ cleanTempDir: 'always' }); await sut.initialize(); await sut.dispose(); - expect(fileUtils.deleteDir).calledWith(expectedPath); + sinon.assert.calledWithExactly(rmStub, sandboxPath, { recursive: true, force: true }); }); it('should not remove the dir if cleanTempDir option is enabled', async () => { const sut = createSut({ cleanTempDir: false }); await sut.initialize(); await sut.dispose(); - expect(fileUtils.deleteDir).not.called; + sinon.assert.notCalled(rmStub); }); it('should not remove the dir if `removeDuringDisposal` is set to false', async () => { @@ -85,20 +95,50 @@ describe(TemporaryDirectory.name, () => { await sut.initialize(); sut.removeDuringDisposal = false; await sut.dispose(); - expect(fileUtils.deleteDir).not.called; + sinon.assert.notCalled(rmStub); }); it('should remove the dir by default', async () => { const sut = createSut(); await sut.initialize(); - deleteDirStub.resolves(); + rmStub.resolves(); + await sut.dispose(); + sinon.assert.calledOnce(rmStub); + }); + + it('should also remove the parent directory if it is empty', async () => { + const sut = createSut(); + await sut.initialize(); + rmStub.resolves(); + readdirStub.resolves([]); + await sut.dispose(); + sinon.assert.calledWithExactly(rmdirStub, tempDirName); + }); + + it('should log any errors on debug when failing to remove the parent directory', async () => { + const sut = createSut(); + await sut.initialize(); + rmStub.resolves(); + readdirStub.resolves([]); + const expectedError = new Error('foo bar'); + rmdirStub.rejects(expectedError); await sut.dispose(); - expect(fileUtils.deleteDir).calledOnce; + sinon.assert.calledWithExactly(testInjector.logger.debug, 'Failed to clean temp .stryker-tmp', expectedError); }); - it('should reject when temp directory is not initialized', async () => { + it("should not remove the parent directory if it isn't empty", async () => { const sut = createSut(); - await expect(sut.dispose()).rejected; + await sut.initialize(); + rmStub.resolves(); + readdirStub.resolves(['sandbox-798'] as unknown as fs.Dirent[]); + await sut.dispose(); + sinon.assert.notCalled(rmdirStub); + }); + + it('should do nothing when temp directory was not initialized', async () => { + const sut = createSut(); + await sut.dispose(); + sinon.assert.notCalled(rmStub); }); }); });