Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(temp-dir): allow multiple instances to share the same temp-dir #5120

Merged
merged 2 commits into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/src/reporters/event-recorder-reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
22 changes: 9 additions & 13 deletions packages/core/src/sandbox/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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>,
temporaryDirectory: I<TemporaryDirectory>,
private readonly project: Project,
private readonly execCommand: typeof execaCommand,
unexpectedExitHandler: I<UnexpectedExitHandler>,
) {
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<void> {
await this.temporaryDirectory.createDirectory(this.tempDirectory);
await this.fillSandbox();
await this.runBuildCommand();
await this.symlinkNodeModulesIfNeeded();
Expand All @@ -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<void> {
Expand Down
17 changes: 8 additions & 9 deletions packages/core/src/utils/file-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@ import fs from 'fs';
import { isErrnoException } from '@stryker-mutator/util';

export const fileUtils = {
deleteDir(dir: string): Promise<void> {
return fs.promises.rm(dir, { recursive: true, force: true });
},

async cleanFolder(folderName: string): Promise<string | undefined> {
/**
* Cleans the dir by creating it.
*/
async cleanDir(dirName: string): Promise<string | undefined> {
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 });
}
},

Expand Down
8 changes: 0 additions & 8 deletions packages/core/src/utils/object-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,4 @@ export const objectUtils = {
}
});
},

/**
* Creates a random integer number.
* @returns A random integer.
*/
random(): number {
return Math.ceil(Math.random() * 10000000);
},
};
54 changes: 26 additions & 28 deletions packages/core/src/utils/temporary-directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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<void> {
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<void> {
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);
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/integration/utils/file-utils.it.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
});

Expand Down
48 changes: 7 additions & 41 deletions packages/core/test/unit/sandbox/sandbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,11 @@ describe(Sandbox.name, () => {
let unexpectedExitHandlerMock: sinon.SinonStubbedInstance<I<UnexpectedExitHandler>>;
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');
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -280,16 +265,16 @@ 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', () => {
testInjector.options.inPlace = true;
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}`);
});
});

Expand All @@ -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();
Expand Down
Loading