From 72479b2924b607693d30f993397fc82591e697e8 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Thu, 21 Jan 2021 16:38:29 +0100 Subject: [PATCH 01/21] feat(in place): support in-place mutation Add `--inPlace` flag. When `inPlace` mode is active, mutations will be written in-place, overriding any local changes. --- .vscode/launch.json | 65 ++-------- e2e/test/in-place/.mocharc.jsonc | 4 + e2e/test/in-place/package-lock.json | 5 + e2e/test/in-place/package.json | 15 +++ e2e/test/in-place/src/Add.js | 24 ++++ e2e/test/in-place/stryker.conf.json | 15 +++ e2e/test/in-place/test/helpers/testSetup.js | 13 ++ e2e/test/in-place/test/unit/Add.spec.js | 60 ++++++++++ e2e/test/in-place/test/unit/Wait.spec.js | 19 +++ e2e/test/in-place/verify/verify.ts | 69 +++++++++++ e2e/test/mocha-javascript/stryker.conf.json | 5 +- packages/api/schema/stryker-core.json | 5 + packages/core/src/di/build-main-injector.ts | 2 +- packages/core/src/di/core-tokens.ts | 5 +- .../process/2-mutant-instrumenter-executor.ts | 12 +- .../core/src/process/3-dry-run-executor.ts | 2 +- packages/core/src/sandbox/sandbox.ts | 113 ++++++++++-------- packages/core/src/stryker-registry.ts | 35 ++++++ packages/core/src/stryker.ts | 10 +- packages/core/src/test-runner/index.ts | 2 +- packages/core/src/utils/file-utils.ts | 46 +++++-- packages/core/src/utils/object-utils.ts | 8 ++ .../core/src/utils/temporary-directory.ts | 11 +- .../create-test-runner-factory.it.spec.ts | 2 +- .../unit/config/options-validator.spec.ts | 1 + .../2-mutant-instrumenter-executor.spec.ts | 15 ++- .../core/test/unit/sandbox/sandbox.spec.ts | 75 +++++++----- packages/core/test/unit/stryker.spec.ts | 5 + .../unit/utils/temporary-directory.spec.ts | 3 +- packages/core/tsconfig.src.json | 1 + 30 files changed, 469 insertions(+), 178 deletions(-) create mode 100644 e2e/test/in-place/.mocharc.jsonc create mode 100644 e2e/test/in-place/package-lock.json create mode 100644 e2e/test/in-place/package.json create mode 100644 e2e/test/in-place/src/Add.js create mode 100644 e2e/test/in-place/stryker.conf.json create mode 100644 e2e/test/in-place/test/helpers/testSetup.js create mode 100644 e2e/test/in-place/test/unit/Add.spec.js create mode 100644 e2e/test/in-place/test/unit/Wait.spec.js create mode 100644 e2e/test/in-place/verify/verify.ts create mode 100644 packages/core/src/stryker-registry.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 53d2180992..ee92f2bc48 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,69 +5,20 @@ "version": "0.2.0", "configurations": [ { - "type": "node", - "request": "launch", "name": "Launch Program", - "program": "${file}", + "program": "${workspaceFolder}/packages/core/bin/stryker", + "request": "launch", + "args": ["run"], "skipFiles": [ "/**" - ] - }, - { - "type": "node", - "request": "launch", - "name": "Stryker unit tests", - "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", - "args": [ - "--timeout", - "999999", - "--colors", - "${workspaceRoot}/packages/stryker/test/helpers", - "${workspaceRoot}/packages/stryker/test/unit/**/*.js" ], - "internalConsoleOptions": "openOnSessionStart" - }, - { - "type": "node", - "request": "launch", - "name": "Stryker-mocha-framework unit tests", - "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", - "args": [ - "--timeout", - "999999", - "--colors", - "${workspaceRoot}/packages/stryker-mocha-framework/test/helpers", - "${workspaceRoot}/packages/stryker-mocha-framework/test/unit/**/*.js" - ], - "internalConsoleOptions": "openOnSessionStart" - }, - { - "type": "node", - "request": "launch", - "name": "babel-transpiler unit tests", - "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", - "args": [ - "--timeout", - "999999", - "--colors", - "${workspaceRoot}/packages/stryker-babel-transpiler/test/helpers", - "${workspaceRoot}/packages/stryker-babel-transpiler/test/unit/**/*.js" + "outFiles": [ + "${workspaceFolder}/packages/**/*.js", + "!**/node_modules/**" ], - "internalConsoleOptions": "openOnSessionStart" - }, - { "type": "node", - "request": "launch", - "name": "javascript-mutator unit tests", - "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", - "args": [ - "--timeout", - "999999", - "--colors", - "${workspaceRoot}/packages/stryker-javascript-mutator/test/helpers", - "${workspaceRoot}/packages/stryker-javascript-mutator/test/unit/**/*.js" - ], - "internalConsoleOptions": "openOnSessionStart" + "cwd": "${workspaceFolder}/e2e/test/mocha-javascript", + "outputCapture": "console" }, { "type": "node", diff --git a/e2e/test/in-place/.mocharc.jsonc b/e2e/test/in-place/.mocharc.jsonc new file mode 100644 index 0000000000..3c2e5a6a9d --- /dev/null +++ b/e2e/test/in-place/.mocharc.jsonc @@ -0,0 +1,4 @@ +{ + "require": "./test/helpers/testSetup.js", + "spec": ["test/unit/*.js"] +} \ No newline at end of file diff --git a/e2e/test/in-place/package-lock.json b/e2e/test/in-place/package-lock.json new file mode 100644 index 0000000000..3900182e5c --- /dev/null +++ b/e2e/test/in-place/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "mocha-mocha", + "version": "0.0.0", + "lockfileVersion": 1 +} diff --git a/e2e/test/in-place/package.json b/e2e/test/in-place/package.json new file mode 100644 index 0000000000..fa3e3eb5a7 --- /dev/null +++ b/e2e/test/in-place/package.json @@ -0,0 +1,15 @@ +{ + "name": "in-place", + "version": "0.0.0", + "private": true, + "description": "A module to perform an integration test", + "main": "index.js", + "scripts": { + "pretest": "rimraf \"reports\"", + "test": "mocha --no-config --require ../../tasks/ts-node-register.js --no-timeout verify/*.ts", + "test:unit": "mocha", + "test:mutation": "stryker run" + }, + "author": "", + "license": "ISC" +} diff --git a/e2e/test/in-place/src/Add.js b/e2e/test/in-place/src/Add.js new file mode 100644 index 0000000000..d7c5f6baae --- /dev/null +++ b/e2e/test/in-place/src/Add.js @@ -0,0 +1,24 @@ +module.exports.add = function(num1, num2) { + return num1 + num2; +}; + +module.exports.addOne = function(number) { + number++; + return number; +}; + +module.exports.negate = function(number) { + return -number; +}; + +module.exports.notCovered = function(number) { + return number > 10; +}; + +module.exports.isNegativeNumber = function(number) { + var isNegative = false; + if(number < 0){ + isNegative = true; + } + return isNegative; +}; diff --git a/e2e/test/in-place/stryker.conf.json b/e2e/test/in-place/stryker.conf.json new file mode 100644 index 0000000000..4b88ff44b4 --- /dev/null +++ b/e2e/test/in-place/stryker.conf.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "testRunner": "mocha", + "concurrency": 2, + "coverageAnalysis": "perTest", + "reporters": [ + "clear-text", + "html", + "event-recorder" + ], + "plugins": [ + "@stryker-mutator/mocha-runner" + ], + "inPlace": true +} diff --git a/e2e/test/in-place/test/helpers/testSetup.js b/e2e/test/in-place/test/helpers/testSetup.js new file mode 100644 index 0000000000..ef2fd68a33 --- /dev/null +++ b/e2e/test/in-place/test/helpers/testSetup.js @@ -0,0 +1,13 @@ +// @ts-nocheck +// +// +// +// +// +// +// +exports.mochaHooks = { + beforeAll() { + global.expect = require('chai').expect; + } +} diff --git a/e2e/test/in-place/test/unit/Add.spec.js b/e2e/test/in-place/test/unit/Add.spec.js new file mode 100644 index 0000000000..dbb7d630e0 --- /dev/null +++ b/e2e/test/in-place/test/unit/Add.spec.js @@ -0,0 +1,60 @@ +// @ts-nocheck +// +// +// +// +// +// +// +var addModule = require('../../src/Add'); +var add = addModule.add; +var addOne = addModule.addOne; +var isNegativeNumber = addModule.isNegativeNumber; +var negate = addModule.negate; +var notCovered = addModule.notCovered; + +describe('Add', function() { + it('should be able to add two numbers', function() { + var num1 = 2; + var num2 = 5; + var expected = num1 + num2; + + var actual = add(num1, num2); + + expect(actual).to.be.equal(expected); + }); + + it('should be able 1 to a number', function() { + var number = 2; + var expected = 3; + + var actual = addOne(number); + + expect(actual).to.be.equal(expected); + }); + + it('should be able negate a number', function() { + var number = 2; + var expected = -2; + + var actual = negate(number); + + expect(actual).to.be.equal(expected); + }); + + it('should be able to recognize a negative number', function() { + var number = -2; + + var isNegative = isNegativeNumber(number); + + expect(isNegative).to.be.true; + }); + + it('should be able to recognize that 0 is not a negative number', function() { + var number = 0; + + var isNegative = isNegativeNumber(number); + + expect(isNegative).to.be.false; + }); +}); diff --git a/e2e/test/in-place/test/unit/Wait.spec.js b/e2e/test/in-place/test/unit/Wait.spec.js new file mode 100644 index 0000000000..9c9ff614ff --- /dev/null +++ b/e2e/test/in-place/test/unit/Wait.spec.js @@ -0,0 +1,19 @@ +// @ts-nocheck +// +// +// +// +// +const { existsSync } = require('fs'); +const path = require('path'); + +describe('wait', () =>{ + it('should wait until `.lock` is removed', async () => { + while(existsSync(path.resolve(__dirname, '..', '..', '.lock'))){ + await sleep(10); + } + }); +}) +async function sleep(n) { + return new Promise(res => setTimeout(res, n)); +} diff --git a/e2e/test/in-place/verify/verify.ts b/e2e/test/in-place/verify/verify.ts new file mode 100644 index 0000000000..35e6c7763d --- /dev/null +++ b/e2e/test/in-place/verify/verify.ts @@ -0,0 +1,69 @@ +import { expectMetrics } from '../../../helpers'; +import { promises as fsPromises } from 'fs'; +import chai from 'chai'; +import execa from 'execa'; +import rimraf from 'rimraf'; +import path from 'path'; +import { promisify } from 'util'; +import { it } from 'mocha'; +import chaiAsPromised from 'chai-as-promised'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +const rm = promisify(rimraf); + +const rootResolve: typeof path.resolve = path.resolve.bind(path, __dirname, '..'); + +describe('in place', () => { + + let originalAddJSContent: string; + + + function readAddJS(): Promise { + return fsPromises.readFile(rootResolve('src', 'Add.js'), 'utf-8'); + } + + before(async () => { + originalAddJSContent = await readAddJS(); + }) + + + afterEach(async () => { + await rm(rootResolve('reports')); + await rm(rootResolve('.lock')); + }) + + it('should reset files after a successful run', async () => { + execa.sync('stryker', ['run']); + const addJSContent = await fsPromises.readFile(rootResolve('src', 'Add.js'), 'utf-8'); + expect(addJSContent).eq(originalAddJSContent); + }); + + it('should report correct score', async () => { + execa.sync('stryker', ['run']); + await expectMetrics({ mutationScore: 73.68 }); + }); + + it('should also reset the files if Stryker exits unexpectedly', async () => { + // Arrange + let addJSMutatedContent: string; + await fsPromises.writeFile(rootResolve('.lock'), ''); // this will lock the test run completion + const onGoingStrykerRun = execa('node', [path.resolve('..', '..', 'node_modules', '.bin', 'stryker'), 'run']); + onGoingStrykerRun.stdout.on('data', async (data) => { + if (data.toString().includes('Starting initial test run')) { + addJSMutatedContent = await readAddJS(); + + // Now, mr bond, it is time to die! + onGoingStrykerRun.kill(); + } + }); + + // Act + const exit = await expect(onGoingStrykerRun).rejected; + + // Assert + expect(await readAddJS()).eq(originalAddJSContent); + expect(addJSMutatedContent).not.eq(originalAddJSContent); + }); +}); diff --git a/e2e/test/mocha-javascript/stryker.conf.json b/e2e/test/mocha-javascript/stryker.conf.json index d864973ed7..ca0cbbbefc 100644 --- a/e2e/test/mocha-javascript/stryker.conf.json +++ b/e2e/test/mocha-javascript/stryker.conf.json @@ -5,6 +5,7 @@ "coverageAnalysis": "perTest", "reporters": ["clear-text", "html", "event-recorder"], "plugins": [ - "@stryker-mutator/mocha-runner" - ] + "/home/nicojs/github/stryker/packages/mocha-runner" + ], + "inPlace": true } diff --git a/packages/api/schema/stryker-core.json b/packages/api/schema/stryker-core.json index a07e5a68d3..9ba70603f5 100644 --- a/packages/api/schema/stryker-core.json +++ b/packages/api/schema/stryker-core.json @@ -294,6 +294,11 @@ "type": "string" } }, + "inPlace": { + "type": "boolean", + "description": "Determines whether or not Stryker should mutate your files in-place. Note: mutating files in place is generally not needed, unless you have code in your project that is really dependent on the file locations.\n\nWhen true, Stryker will override your files, but it will keep a copy of the originals in a temp directory. It will place the originals back after it is done.\n\nWhen false (default) Stryker will work in a copy in a sandbox directory.", + "default": false + }, "logLevel": { "description": "Set the log level that Stryker uses to write to the console.", "$ref": "#/definitions/logLevel", diff --git a/packages/core/src/di/build-main-injector.ts b/packages/core/src/di/build-main-injector.ts index a36c5cab90..fe67a193bf 100644 --- a/packages/core/src/di/build-main-injector.ts +++ b/packages/core/src/di/build-main-injector.ts @@ -24,8 +24,8 @@ export interface MainContext extends PluginContext { } type PluginResolverProvider = Injector; -export type CliOptionsProvider = Injector & { [coreTokens.cliOptions]: PartialStrykerOptions }>; +export type CliOptionsProvider = Injector & { [coreTokens.cliOptions]: PartialStrykerOptions }>; buildMainInjector.inject = tokens(commonTokens.injector); export function buildMainInjector(injector: CliOptionsProvider): Injector { const pluginResolverProvider = createPluginResolverProvider(injector); diff --git a/packages/core/src/di/core-tokens.ts b/packages/core/src/di/core-tokens.ts index 005d2b9277..a1854be93c 100644 --- a/packages/core/src/di/core-tokens.ts +++ b/packages/core/src/di/core-tokens.ts @@ -5,21 +5,18 @@ export const disableTypeChecksHelper = 'disableTypeChecksHelper'; export const execa = 'execa'; export const cliOptions = 'cliOptions'; export const configReader = 'configReader'; -export const configOptionsApplier = 'configOptionsApplier'; // TODO: Remove if unused post mutation switching export const inputFiles = 'inputFiles'; export const dryRunResult = 'dryRunResult'; -export const transpiledFiles = 'transpiledFiles'; // TODO: Remove if unused post mutation switching export const files = 'files'; export const mutants = 'mutants'; export const mutantsWithTestCoverage = 'mutantsWithTestCoverage'; -export const mutantTranspileScheduler = 'mutantTranspileScheduler'; // TODO: Remove if unused post mutation switching export const temporaryDirectory = 'temporaryDirectory'; +export const unexpectedExitRegistry = 'unexpectedExitRegistry'; export const timer = 'timer'; export const timeOverheadMS = 'timeOverheadMS'; export const loggingContext = 'loggingContext'; export const mutationTestReportHelper = 'mutationTestReportHelper'; export const sandbox = 'sandbox'; -export const sandboxWorkingDirectory = 'sandboxWorkingDirectory'; // TODO: Remove if unused post mutation switching export const concurrencyTokenProvider = 'concurrencyTokenProvider'; export const testRunnerFactory = 'testRunnerFactory'; export const testRunnerPool = 'testRunnerPool'; diff --git a/packages/core/src/process/2-mutant-instrumenter-executor.ts b/packages/core/src/process/2-mutant-instrumenter-executor.ts index 738cdaab3c..d7faf56513 100644 --- a/packages/core/src/process/2-mutant-instrumenter-executor.ts +++ b/packages/core/src/process/2-mutant-instrumenter-executor.ts @@ -11,11 +11,14 @@ import { ConcurrencyTokenProvider, createCheckerPool } from '../concurrent'; import { createCheckerFactory } from '../checker/checker-facade'; import { createPreprocessor } from '../sandbox'; +import { StrykerRegistry } from '../stryker-registry'; + import { DryRunContext } from './3-dry-run-executor'; export interface MutantInstrumenterContext extends MainContext { [coreTokens.inputFiles]: InputFileCollection; [coreTokens.loggingContext]: LoggingClientContext; + [coreTokens.unexpectedExitRegistry]: StrykerRegistry; } export class MutantInstrumenterExecutor { @@ -49,8 +52,13 @@ export class MutantInstrumenterExecutor { await checkerPool.init(); // Feed the sandbox - const sandbox = await this.injector.provideValue(coreTokens.files, files).injectFunction(Sandbox.create); - return checkerPoolProvider.provideValue(coreTokens.sandbox, sandbox).provideValue(coreTokens.mutants, instrumentResult.mutants); + const dryRunProvider = checkerPoolProvider + .provideValue(coreTokens.files, files) + .provideClass(coreTokens.sandbox, Sandbox) + .provideValue(coreTokens.mutants, instrumentResult.mutants); + const sandbox = dryRunProvider.resolve(coreTokens.sandbox); + await sandbox.init(); + return dryRunProvider; } private replaceInstrumentedFiles(instrumentResult: InstrumentResult): File[] { diff --git a/packages/core/src/process/3-dry-run-executor.ts b/packages/core/src/process/3-dry-run-executor.ts index 18ca0ff3cc..d7e9ef5b44 100644 --- a/packages/core/src/process/3-dry-run-executor.ts +++ b/packages/core/src/process/3-dry-run-executor.ts @@ -35,7 +35,7 @@ import { MutantInstrumenterContext } from './2-mutant-instrumenter-executor'; const INITIAL_TEST_RUN_MARKER = 'Initial test run'; export interface DryRunContext extends MutantInstrumenterContext { - [coreTokens.sandbox]: Sandbox; + [coreTokens.sandbox]: I; [coreTokens.mutants]: readonly Mutant[]; [coreTokens.checkerPool]: I>; [coreTokens.concurrencyTokenProvider]: I; diff --git a/packages/core/src/sandbox/sandbox.ts b/packages/core/src/sandbox/sandbox.ts index 8dc4ad29e5..c3a65aea85 100644 --- a/packages/core/src/sandbox/sandbox.ts +++ b/packages/core/src/sandbox/sandbox.ts @@ -1,77 +1,62 @@ import path = require('path'); +import { promises as fsPromises } from 'fs'; +import os = require('os'); import execa = require('execa'); import npmRunPath = require('npm-run-path'); import { StrykerOptions } from '@stryker-mutator/api/core'; import { File } from '@stryker-mutator/api/core'; import { normalizeWhitespaces, I } from '@stryker-mutator/util'; -import * as mkdirp from 'mkdirp'; -import { Logger, LoggerFactoryMethod } from '@stryker-mutator/api/logging'; -import { tokens, commonTokens } from '@stryker-mutator/api/plugin'; +import { Logger } from '@stryker-mutator/api/logging'; +import { tokens, commonTokens, Disposable } from '@stryker-mutator/api/plugin'; import { mergeMap, toArray } from 'rxjs/operators'; import { from } from 'rxjs'; import { TemporaryDirectory } from '../utils/temporary-directory'; -import { findNodeModules, MAX_CONCURRENT_FILE_IO, symlinkJunction, writeFile } from '../utils/file-utils'; +import { findNodeModules, MAX_CONCURRENT_FILE_IO, moveDirectoryRecursiveSync, symlinkJunction, writeFile, mkdirp } from '../utils/file-utils'; import { coreTokens } from '../di'; +import { random } from '../utils/object-utils'; +import { UnexpectedExitRegister } from '../stryker-registry'; -interface SandboxFactory { - ( - options: StrykerOptions, - getLogger: LoggerFactoryMethod, - files: readonly File[], - tempDir: I, - exec: typeof execa - ): Promise; - inject: [ - typeof commonTokens.options, - typeof commonTokens.getLogger, - typeof coreTokens.files, - typeof coreTokens.temporaryDirectory, - typeof coreTokens.execa - ]; -} - -export class Sandbox { +export class Sandbox implements Disposable { private readonly fileMap = new Map(); public readonly workingDirectory: string; + private readonly backupDirectory: string = ''; + + public static readonly inject = tokens( + commonTokens.options, + commonTokens.logger, + coreTokens.temporaryDirectory, + coreTokens.files, + coreTokens.execa, + coreTokens.unexpectedExitRegistry + ); - private constructor( + constructor( private readonly options: StrykerOptions, private readonly log: Logger, temporaryDirectory: I, private readonly files: readonly File[], - private readonly exec: typeof execa + private readonly exec: typeof execa, + unexpectedExitRegistry: UnexpectedExitRegister ) { - this.workingDirectory = temporaryDirectory.createRandomDirectory('sandbox'); - this.log.debug('Creating a sandbox for files in %s', this.workingDirectory); + if (options.inPlace) { + this.workingDirectory = process.cwd(); + this.backupDirectory = path.join(os.tmpdir(), `stryker-backup-${random()}`); + this.log.info('In place mode detected. Stryker will be overriding YOUR files. Find your backup at: %s', this.backupDirectory); + unexpectedExitRegistry.registerUnexpectedExitHandler(this.dispose.bind(this, true)); + } else { + this.workingDirectory = temporaryDirectory.createRandomDirectory('sandbox'); + this.log.debug('Creating a sandbox for files in %s', this.workingDirectory); + } } - private async initialize(): Promise { + public async init(): Promise { await this.fillSandbox(); await this.runBuildCommand(); await this.symlinkNodeModulesIfNeeded(); } - public static create: SandboxFactory = Object.assign( - async ( - options: StrykerOptions, - getLogger: LoggerFactoryMethod, - files: readonly File[], - tempDir: I, - exec: typeof execa - ): Promise => { - const sandbox = new Sandbox(options, getLogger(Sandbox.name), tempDir, files, exec); - await sandbox.initialize(); - return sandbox; - }, - { inject: tokens(commonTokens.options, commonTokens.getLogger, coreTokens.files, coreTokens.temporaryDirectory, coreTokens.execa) } - ); - - public get sandboxFileNames(): string[] { - return [...this.fileMap.entries()].map(([, to]) => to); - } - public sandboxFileFor(fileName: string): string { const sandboxFileName = this.fileMap.get(fileName); if (sandboxFileName === undefined) { @@ -99,7 +84,7 @@ export class Sandbox { } private async symlinkNodeModulesIfNeeded(): Promise { - if (this.options.symlinkNodeModules) { + if (this.options.symlinkNodeModules && !this.options.inPlace) { // TODO: Change with this.options.basePath when we have it const basePath = process.cwd(); const nodeModules = await findNodeModules(basePath); @@ -123,10 +108,34 @@ export class Sandbox { private async fillFile(file: File): Promise { const relativePath = path.relative(process.cwd(), file.name); - const folderName = path.join(this.workingDirectory, path.dirname(relativePath)); - mkdirp.sync(folderName); - const targetFileName = path.join(folderName, path.basename(relativePath)); - this.fileMap.set(file.name, targetFileName); - await writeFile(targetFileName, file.content); + if (this.options.inPlace) { + this.fileMap.set(file.name, file.name); + const originalContent = await fsPromises.readFile(file.name); + if (originalContent.compare(file.content) !== 0) { + // difference + const backupFileName = path.join(this.backupDirectory, relativePath); + await mkdirp(path.dirname(backupFileName)); + await fsPromises.writeFile(backupFileName, originalContent); + this.log.debug(`Stored backup file at ${backupFileName}`); + await writeFile(file.name, file.content); + } + } else { + const folderName = path.join(this.workingDirectory, path.dirname(relativePath)); + await mkdirp(folderName); + const targetFileName = path.join(folderName, path.basename(relativePath)); + this.fileMap.set(file.name, targetFileName); + await writeFile(targetFileName, file.content); + } + } + + public dispose(unexpected = false): void { + if (this.backupDirectory) { + if (unexpected) { + console.error(`Detecting unexpected exit, recovering original files from ${this.backupDirectory}`); + } else { + this.log.info(`Resetting your original files from ${this.backupDirectory}.`); + } + moveDirectoryRecursiveSync(this.backupDirectory, this.workingDirectory); + } } } diff --git a/packages/core/src/stryker-registry.ts b/packages/core/src/stryker-registry.ts new file mode 100644 index 0000000000..9c3cbcc1c2 --- /dev/null +++ b/packages/core/src/stryker-registry.ts @@ -0,0 +1,35 @@ +export type ExitHandler = () => void; + +export interface UnexpectedExitRegister { + registerUnexpectedExitHandler(handler: ExitHandler): void; +} + +const signals = Object.freeze(['SIGABRT', 'SIGINT', 'SIGHUP', 'SIGTERM']); + +export class StrykerRegistry implements UnexpectedExitRegister { + private readonly unexpectedExitHandlers: ExitHandler[] = []; + + public startHandleExit() { + process.on('exit', this.handleExit); + signals.forEach((signal) => process.on(signal, this.processSignal)); + } + + public stopHandleExit() { + process.off('exit', this.handleExit); + signals.forEach((signal) => process.off(signal, this.processSignal)); + } + + private processSignal(_signal: string, signalNumber: number) { + // Just call 'exit' with correct exitCode. + // See https://nodejs.org/api/process.html#process_signal_events, we should exit with 128 + signal number + process.exit(128 + signalNumber); + } + + public handleExit = () => { + this.unexpectedExitHandlers.forEach((handler) => handler()); + }; + + public registerUnexpectedExitHandler(handler: ExitHandler) { + this.unexpectedExitHandlers.push(handler); + } +} diff --git a/packages/core/src/stryker.ts b/packages/core/src/stryker.ts index efdf023106..a15599bc5e 100644 --- a/packages/core/src/stryker.ts +++ b/packages/core/src/stryker.ts @@ -8,6 +8,7 @@ import { LogConfigurator } from './logging'; import { PrepareExecutor, MutantInstrumenterExecutor, DryRunExecutor, MutationTestExecutor } from './process'; import { coreTokens, provideLogger } from './di'; import { retrieveCause, ConfigError } from './errors'; +import { StrykerRegistry } from './stryker-registry'; /** * The main Stryker class. @@ -23,9 +24,13 @@ export default class Stryker { public async runMutationTest(): Promise { const rootInjector = this.injectorFactory(); - const loggerProvider = provideLogger(rootInjector); + const strykerRegistry = rootInjector.injectClass(StrykerRegistry); + const loggerProvider = provideLogger(rootInjector).provideValue(coreTokens.unexpectedExitRegistry, strykerRegistry); try { + // Register a global exit handler, this will allow the Sandbox class to use `strykerRegistry.registerUnexpectedExitHandler` to cleanup on unexpected exit + strykerRegistry.startHandleExit(); + // 1. Prepare. Load Stryker configuration, load the input files and starts the logging server const prepareExecutor = loggerProvider.provideValue(coreTokens.cliOptions, this.cliOptions).injectClass(PrepareExecutor); const mutantInstrumenterInjector = await prepareExecutor.execute(); @@ -65,6 +70,9 @@ export default class Stryker { } finally { await rootInjector.dispose(); await LogConfigurator.shutdown(); + + // Remove our grip on the exit handlers + strykerRegistry.stopHandleExit(); } } } diff --git a/packages/core/src/test-runner/index.ts b/packages/core/src/test-runner/index.ts index e4fbb3b958..bce828a470 100644 --- a/packages/core/src/test-runner/index.ts +++ b/packages/core/src/test-runner/index.ts @@ -15,7 +15,7 @@ import MaxTestRunnerReuseDecorator from './max-test-runner-reuse-decorator'; createTestRunnerFactory.inject = tokens(commonTokens.options, coreTokens.sandbox, coreTokens.loggingContext); export function createTestRunnerFactory( options: StrykerOptions, - sandbox: Pick, + sandbox: Pick, loggingContext: LoggingClientContext ): () => Required { if (CommandTestRunner.is(options.testRunner)) { diff --git a/packages/core/src/utils/file-utils.ts b/packages/core/src/utils/file-utils.ts index c25416b479..056053b4ad 100644 --- a/packages/core/src/utils/file-utils.ts +++ b/packages/core/src/utils/file-utils.ts @@ -1,14 +1,16 @@ import * as path from 'path'; -import { promises as fs } from 'fs'; +import fs = require('fs'); import { promisify } from 'util'; import * as nodeGlob from 'glob'; -import * as mkdirp from 'mkdirp'; +import mkdirpModule = require('mkdirp'); import * as rimraf from 'rimraf'; export const MAX_CONCURRENT_FILE_IO = 256; +export const mkdirp = mkdirpModule; + export function glob(expression: string): Promise { return new Promise((resolve, reject) => { nodeGlob(expression, { nodir: true }, (error, matches) => { @@ -21,11 +23,11 @@ export const deleteDir = promisify(rimraf); export async function cleanFolder(folderName: string) { try { - await fs.lstat(folderName); + await fs.promises.lstat(folderName); await deleteDir(folderName); - return mkdirp.sync(folderName); + return mkdirp(folderName); } catch (e) { - return mkdirp.sync(folderName); + return mkdirp(folderName); } } @@ -44,10 +46,36 @@ export function importModule(moduleName: string): unknown { */ export function writeFile(fileName: string, data: string | Buffer): Promise { if (Buffer.isBuffer(data)) { - return fs.writeFile(fileName, data); + return fs.promises.writeFile(fileName, data); } else { - return fs.writeFile(fileName, data, 'utf8'); + return fs.promises.writeFile(fileName, data, 'utf8'); + } +} + +/** + * Recursively walks the from directory and copy the content to the target directory synchronously + * @param from The source directory to move from + * @param to The target directory to move to + */ +export function moveDirectoryRecursiveSync(from: string, to: string) { + if (!fs.existsSync(from)) { + return; + } + if (!fs.existsSync(to)) { + fs.mkdirSync(to); + } + const files = fs.readdirSync(from); + for (const file of files) { + const fromFileName = path.join(from, file); + const toFileName = path.join(to, file); + const stats = fs.lstatSync(fromFileName); + if (stats.isFile()) { + fs.renameSync(fromFileName, toFileName); + } else { + moveDirectoryRecursiveSync(fromFileName, toFileName); + } } + fs.rmdirSync(from); } /** @@ -56,7 +84,7 @@ export function writeFile(fileName: string, data: string | Buffer): Promise basePath = path.resolve(basePath); const nodeModules = path.resolve(basePath, 'node_modules'); try { - await fs.stat(nodeModules); + await fs.promises.stat(nodeModules); return nodeModules; } catch (e) { const parent = path.dirname(basePath); diff --git a/packages/core/src/utils/object-utils.ts b/packages/core/src/utils/object-utils.ts index c7155ab266..416d9776c9 100644 --- a/packages/core/src/utils/object-utils.ts +++ b/packages/core/src/utils/object-utils.ts @@ -70,3 +70,11 @@ export function padLeft(input: string): string { .map((str) => '\t' + str) .join('\n'); } + +/** + * Creates a random integer number. + * @returns A random integer. + */ +export function 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 9df878a8c5..65c1fb37f8 100644 --- a/packages/core/src/utils/temporary-directory.ts +++ b/packages/core/src/utils/temporary-directory.ts @@ -8,6 +8,7 @@ import * as mkdirp from 'mkdirp'; import { Disposable } from 'typed-inject'; import { deleteDir } from './file-utils'; +import { random } from './object-utils'; export class TemporaryDirectory implements Disposable { private readonly temporaryDirectory: string; @@ -35,7 +36,7 @@ export class TemporaryDirectory implements Disposable { if (!this.isInitialized) { throw new Error('initialize() was not called!'); } - const dir = path.resolve(this.temporaryDirectory, `${prefix}${this.random()}`); + const dir = path.resolve(this.temporaryDirectory, `${prefix}${random()}`); mkdirp.sync(dir); return dir; } @@ -77,12 +78,4 @@ export class TemporaryDirectory implements Disposable { } } } - - /** - * Creates a random integer number. - * @returns A random integer. - */ - public random(): number { - return Math.ceil(Math.random() * 10000000); - } } diff --git a/packages/core/test/integration/test-runner/create-test-runner-factory.it.spec.ts b/packages/core/test/integration/test-runner/create-test-runner-factory.it.spec.ts index f38bc611eb..02a21a622d 100644 --- a/packages/core/test/integration/test-runner/create-test-runner-factory.it.spec.ts +++ b/packages/core/test/integration/test-runner/create-test-runner-factory.it.spec.ts @@ -36,7 +36,7 @@ describe(`${createTestRunnerFactory.name} integration`, () => { testInjector.options.maxTestRunnerReuse = 0; alreadyDisposed = false; createSut = testInjector.injector - .provideValue(coreTokens.sandbox, { sandboxFileNames: ['foo.js'], workingDirectory: __dirname }) + .provideValue(coreTokens.sandbox, { workingDirectory: __dirname }) .provideValue(coreTokens.loggingContext, loggingContext) .injectFunction(createTestRunnerFactory); diff --git a/packages/core/test/unit/config/options-validator.spec.ts b/packages/core/test/unit/config/options-validator.spec.ts index 1138870a6c..e1f5fe040b 100644 --- a/packages/core/test/unit/config/options-validator.spec.ts +++ b/packages/core/test/unit/config/options-validator.spec.ts @@ -30,6 +30,7 @@ describe(OptionsValidator.name, () => { appendPlugins: [], checkers: [], cleanTempDir: true, + inPlace: false, clearTextReporter: { allowColor: true, logTests: true, diff --git a/packages/core/test/unit/process/2-mutant-instrumenter-executor.spec.ts b/packages/core/test/unit/process/2-mutant-instrumenter-executor.spec.ts index 70d9901a62..6dcba66d32 100644 --- a/packages/core/test/unit/process/2-mutant-instrumenter-executor.spec.ts +++ b/packages/core/test/unit/process/2-mutant-instrumenter-executor.spec.ts @@ -52,7 +52,7 @@ describe(MutantInstrumenterExecutor.name, () => { sut = new MutantInstrumenterExecutor(injectorMock as Injector, inputFiles, testInjector.options); injectorMock.injectClass.withArgs(Instrumenter).returns(instrumenterMock); injectorMock.injectFunction.withArgs(createPreprocessor).returns(sandboxFilePreprocessorMock); - injectorMock.injectFunction.withArgs(Sandbox.create).returns(sandboxMock); + injectorMock.resolve.withArgs(coreTokens.sandbox).returns(sandboxMock); injectorMock.resolve .withArgs(coreTokens.concurrencyTokenProvider) .returns(concurrencyTokenProviderMock) @@ -77,13 +77,13 @@ describe(MutantInstrumenterExecutor.name, () => { it('should preprocess files before initializing the sandbox', async () => { await sut.execute(); expect(sandboxFilePreprocessorMock.preprocess).calledWithExactly([mutatedFile, testFile]); - expect(sandboxFilePreprocessorMock.preprocess).calledBefore(injectorMock.injectFunction); + expect(sandboxFilePreprocessorMock.preprocess).calledBefore(sandboxMock.init); }); it('should provide the mutated files to the sandbox', async () => { await sut.execute(); expect(injectorMock.provideValue).calledWithExactly(coreTokens.files, [mutatedFile, testFile]); - expect(injectorMock.provideValue.withArgs(coreTokens.files, sinon.match.any)).calledBefore(injectorMock.injectFunction); + expect(injectorMock.provideValue.withArgs(coreTokens.files, sinon.match.any)).calledBefore(sandboxMock.init); }); it('should provide checkerToken$ to the checker pool', async () => { @@ -101,7 +101,7 @@ describe(MutantInstrumenterExecutor.name, () => { it('should initialize the CheckerPool before creating the sandbox', async () => { // This is important for in-place mutation. We need to initialize the typescript checker(s) before we write mutated files to disk. await sut.execute(); - expect(checkerPoolMock.init).calledBefore(injectorMock.injectFunction.withArgs(Sandbox.create)); + expect(checkerPoolMock.init).calledBefore(injectorMock.provideClass.withArgs(coreTokens.sandbox, Sandbox)); }); it('should provide mutants in the result', async () => { @@ -111,6 +111,11 @@ describe(MutantInstrumenterExecutor.name, () => { it('should provide the sandbox in the result', async () => { await sut.execute(); - expect(injectorMock.provideValue).calledWithExactly(coreTokens.sandbox, sandboxMock); + expect(injectorMock.provideClass).calledWithExactly(coreTokens.sandbox, Sandbox); + }); + + it('should initialize the sandbox', async () => { + await sut.execute(); + expect(sandboxMock.init).calledOnce; }); }); diff --git a/packages/core/test/unit/sandbox/sandbox.spec.ts b/packages/core/test/unit/sandbox/sandbox.spec.ts index 89c44514b6..33d3db295b 100644 --- a/packages/core/test/unit/sandbox/sandbox.spec.ts +++ b/packages/core/test/unit/sandbox/sandbox.spec.ts @@ -4,7 +4,6 @@ import execa = require('execa'); import npmRunPath = require('npm-run-path'); import { expect } from 'chai'; import sinon = require('sinon'); -import * as mkdirp from 'mkdirp'; import { testInjector, tick } from '@stryker-mutator/test-helpers'; import { File } from '@stryker-mutator/api/core'; import { fileAlreadyExistsError } from '@stryker-mutator/test-helpers/src/factory'; @@ -14,21 +13,23 @@ import { Sandbox } from '../../../src/sandbox/sandbox'; import { coreTokens } from '../../../src/di'; import { TemporaryDirectory } from '../../../src/utils/temporary-directory'; import * as fileUtils from '../../../src/utils/file-utils'; +import { UnexpectedExitRegister } from '../../../src/stryker-registry'; describe(Sandbox.name, () => { let temporaryDirectoryMock: sinon.SinonStubbedInstance; let files: File[]; - let mkdirpSyncStub: sinon.SinonStub; + let mkdirpStub: sinon.SinonStub; let writeFileStub: sinon.SinonStub; let symlinkJunctionStub: sinon.SinonStub; let findNodeModulesStub: sinon.SinonStub; let execaMock: sinon.SinonStubbedInstance; + let unexpectedExitRegisterMock: sinon.SinonStubbedInstance; const SANDBOX_WORKING_DIR = 'sandbox-123'; beforeEach(() => { temporaryDirectoryMock = sinon.createStubInstance(TemporaryDirectory); temporaryDirectoryMock.createRandomDirectory.returns(SANDBOX_WORKING_DIR); - mkdirpSyncStub = sinon.stub(mkdirp, 'sync'); + mkdirpStub = sinon.stub(fileUtils, 'mkdirp'); writeFileStub = sinon.stub(fileUtils, 'writeFile'); symlinkJunctionStub = sinon.stub(fileUtils, 'symlinkJunction'); findNodeModulesStub = sinon.stub(fileUtils, 'findNodeModules'); @@ -38,22 +39,27 @@ describe(Sandbox.name, () => { node: sinon.stub(), sync: sinon.stub(), }; + unexpectedExitRegisterMock = { + registerUnexpectedExitHandler: sinon.stub(), + }; symlinkJunctionStub.resolves(); findNodeModulesStub.resolves('node_modules'); files = []; }); - function createSut(): Promise { + function createSut(): Sandbox { return testInjector.injector .provideValue(coreTokens.files, files) .provideValue(coreTokens.temporaryDirectory, temporaryDirectoryMock) .provideValue(coreTokens.execa, (execaMock as unknown) as typeof execa) - .injectFunction(Sandbox.create); + .provideValue(coreTokens.unexpectedExitRegistry, unexpectedExitRegisterMock) + .injectClass(Sandbox); } - describe('create()', () => { + describe('init()', () => { it('should have created a sandbox folder', async () => { - await createSut(); + const sut = createSut(); + await sut.init(); expect(temporaryDirectoryMock.createRandomDirectory).calledWith('sandbox'); }); @@ -62,7 +68,8 @@ describe(Sandbox.name, () => { const fileE = new File(path.resolve('c', 'd', 'e.log'), 'e content'); files.push(fileB); files.push(fileE); - await createSut(); + const sut = createSut(); + await sut.init(); expect(writeFileStub).calledWith(path.join(SANDBOX_WORKING_DIR, 'a', 'b.txt'), fileB.content); expect(writeFileStub).calledWith(path.join(SANDBOX_WORKING_DIR, 'c', 'd', 'e.log'), fileE.content); }); @@ -70,15 +77,17 @@ describe(Sandbox.name, () => { it('should make the dir before copying the file', async () => { files.push(new File(path.resolve('a', 'b.js'), 'b content')); files.push(new File(path.resolve('c', 'd', 'e.js'), 'e content')); - await createSut(); - expect(mkdirpSyncStub).calledTwice; - expect(mkdirpSyncStub).calledWithExactly(path.join(SANDBOX_WORKING_DIR, 'a')); - expect(mkdirpSyncStub).calledWithExactly(path.join(SANDBOX_WORKING_DIR, 'c', 'd')); + const sut = createSut(); + await sut.init(); + expect(mkdirpStub).calledTwice; + expect(mkdirpStub).calledWithExactly(path.join(SANDBOX_WORKING_DIR, 'a')); + expect(mkdirpStub).calledWithExactly(path.join(SANDBOX_WORKING_DIR, 'c', 'd')); }); it('should be able to copy a local file', async () => { files.push(new File('localFile.txt', 'foobar')); - await createSut(); + const sut = createSut(); + await sut.init(); expect(writeFileStub).calledWith(path.join(SANDBOX_WORKING_DIR, 'localFile.txt'), Buffer.from('foobar')); }); @@ -94,7 +103,8 @@ describe(Sandbox.name, () => { } // Act - const onGoingWork = createSut(); + const sut = createSut(); + const initPromise = sut.init(); await tick(); expect(writeFileStub).callCount(maxFileIO); fileHandles[0].task.resolve(); @@ -103,18 +113,20 @@ describe(Sandbox.name, () => { // Assert expect(writeFileStub).callCount(maxFileIO + 1); fileHandles.forEach(({ task }) => task.resolve()); - await onGoingWork; + await initPromise; }); it('should symlink node modules in sandbox directory if exists', async () => { - await createSut(); + const sut = createSut(); + await sut.init(); expect(findNodeModulesStub).calledWith(process.cwd()); expect(symlinkJunctionStub).calledWith('node_modules', path.join(SANDBOX_WORKING_DIR, 'node_modules')); }); it('should not symlink node modules in sandbox directory if no node_modules exist', async () => { findNodeModulesStub.resolves(null); - await createSut(); + const sut = createSut(); + await sut.init(); expect(testInjector.logger.warn).calledWithMatch('Could not find a node_modules'); expect(testInjector.logger.warn).calledWithMatch(process.cwd()); expect(symlinkJunctionStub).not.called; @@ -123,7 +135,8 @@ describe(Sandbox.name, () => { it('should log a warning if "node_modules" already exists in the working folder', async () => { findNodeModulesStub.resolves('node_modules'); symlinkJunctionStub.rejects(fileAlreadyExistsError()); - await createSut(); + const sut = createSut(); + await sut.init(); expect(testInjector.logger.warn).calledWithMatch( normalizeWhitespaces( `Could not symlink "node_modules" in sandbox directory, it is already created in the sandbox. @@ -137,7 +150,8 @@ describe(Sandbox.name, () => { findNodeModulesStub.resolves('basePath/node_modules'); const error = new Error('unknown'); symlinkJunctionStub.rejects(error); - await createSut(); + const sut = createSut(); + await sut.init(); expect(testInjector.logger.warn).calledWithMatch( normalizeWhitespaces('Unexpected error while trying to symlink "basePath/node_modules" in sandbox directory.'), error @@ -146,43 +160,40 @@ describe(Sandbox.name, () => { it('should symlink node modules in sandbox directory if `symlinkNodeModules` is `false`', async () => { testInjector.options.symlinkNodeModules = false; - await createSut(); + const sut = createSut(); + await sut.init(); expect(symlinkJunctionStub).not.called; expect(findNodeModulesStub).not.called; }); it('should execute the buildCommand in the sandbox', async () => { testInjector.options.buildCommand = 'npm run build'; - await createSut(); + const sut = createSut(); + await sut.init(); expect(execaMock.command).calledWith('npm run build', { cwd: SANDBOX_WORKING_DIR, env: npmRunPath.env() }); expect(testInjector.logger.info).calledWith('Running build command "%s" in the sandbox at "%s".', 'npm run build', SANDBOX_WORKING_DIR); }); it('should not execute a build command when non is configured', async () => { testInjector.options.buildCommand = undefined; - await createSut(); + const sut = createSut(); + await sut.init(); expect(execaMock.command).not.called; }); it('should execute the buildCommand before the node_modules are symlinked', async () => { // It is important to first run the buildCommand, otherwise the build dependencies are not correctly resolved testInjector.options.buildCommand = 'npm run build'; - await createSut(); + const sut = createSut(); + await sut.init(); expect(execaMock.command).calledBefore(symlinkJunctionStub); }); }); - describe('get sandboxFileNames()', () => { - it('should retrieve all files', async () => { - files.push(new File('a.js', '')); - files.push(new File(path.resolve('b', 'c', 'e.js'), '')); - const sut = await createSut(); - expect(sut.sandboxFileNames).deep.eq([path.join(SANDBOX_WORKING_DIR, 'a.js'), path.join(SANDBOX_WORKING_DIR, 'b', 'c', 'e.js')]); - }); - }); describe('workingDirectory', () => { it('should retrieve the sandbox directory', async () => { - const sut = await createSut(); + const sut = createSut(); + await sut.init(); expect(sut.workingDirectory).eq(SANDBOX_WORKING_DIR); }); }); diff --git a/packages/core/test/unit/stryker.spec.ts b/packages/core/test/unit/stryker.spec.ts index 82efc953c8..6a94aa08c3 100644 --- a/packages/core/test/unit/stryker.spec.ts +++ b/packages/core/test/unit/stryker.spec.ts @@ -13,6 +13,7 @@ import { PrepareExecutor, MutantInstrumenterExecutor, DryRunExecutor, MutationTe import { coreTokens } from '../../src/di'; import { ConfigError } from '../../src/errors'; import { TemporaryDirectory } from '../../src/utils/temporary-directory'; +import { StrykerRegistry } from '../../src/stryker-registry'; describe(Stryker.name, () => { let sut: Stryker; @@ -23,6 +24,7 @@ describe(Stryker.name, () => { let loggerMock: sinon.SinonStubbedInstance; let temporaryDirectoryMock: sinon.SinonStubbedInstance; let getLoggerStub: sinon.SinonStub; + let strykerRegistryMock: sinon.SinonStubbedInstance; let prepareExecutorMock: sinon.SinonStubbedInstance; let mutantInstrumenterExecutorMock: sinon.SinonStubbedInstance; @@ -39,7 +41,10 @@ describe(Stryker.name, () => { mutantInstrumenterExecutorMock = sinon.createStubInstance(MutantInstrumenterExecutor); dryRunExecutorMock = sinon.createStubInstance(DryRunExecutor); mutationTestExecutorMock = sinon.createStubInstance(MutationTestExecutor); + strykerRegistryMock = sinon.createStubInstance(StrykerRegistry); injectorMock.injectClass + .withArgs(StrykerRegistry) + .returns(strykerRegistryMock) .withArgs(PrepareExecutor) .returns(prepareExecutorMock) .withArgs(MutantInstrumenterExecutor) diff --git a/packages/core/test/unit/utils/temporary-directory.spec.ts b/packages/core/test/unit/utils/temporary-directory.spec.ts index 73cdfd04fe..8fa1acfb3e 100644 --- a/packages/core/test/unit/utils/temporary-directory.spec.ts +++ b/packages/core/test/unit/utils/temporary-directory.spec.ts @@ -10,6 +10,7 @@ import * as sinon from 'sinon'; import { StrykerOptions } from '@stryker-mutator/api/core'; import * as fileUtils from '../../../src/utils/file-utils'; +import * as objectUtils from '../../../src/utils/object-utils'; import { TemporaryDirectory } from '../../../src/utils/temporary-directory'; describe(TemporaryDirectory.name, () => { @@ -25,7 +26,7 @@ describe(TemporaryDirectory.name, () => { sut = createSut(); - randomStub = sinon.stub(sut, 'random'); + randomStub = sinon.stub(objectUtils, 'random'); randomStub.returns('rand'); }); diff --git a/packages/core/tsconfig.src.json b/packages/core/tsconfig.src.json index 4817489634..aaae7cde8d 100644 --- a/packages/core/tsconfig.src.json +++ b/packages/core/tsconfig.src.json @@ -3,6 +3,7 @@ "compilerOptions": { "rootDir": ".", "noImplicitThis": true, + "strictBindCallApply": true, "resolveJsonModule": true, "types": [] // Exclude global mocha functions for the sources }, From 7f338e34c91b9c313028ebdb28b0fca72ce1092b Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Thu, 21 Jan 2021 16:52:58 +0100 Subject: [PATCH 02/21] revert accidental change --- e2e/test/mocha-javascript/stryker.conf.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/e2e/test/mocha-javascript/stryker.conf.json b/e2e/test/mocha-javascript/stryker.conf.json index ca0cbbbefc..d864973ed7 100644 --- a/e2e/test/mocha-javascript/stryker.conf.json +++ b/e2e/test/mocha-javascript/stryker.conf.json @@ -5,7 +5,6 @@ "coverageAnalysis": "perTest", "reporters": ["clear-text", "html", "event-recorder"], "plugins": [ - "/home/nicojs/github/stryker/packages/mocha-runner" - ], - "inPlace": true + "@stryker-mutator/mocha-runner" + ] } From 18afb96c0debbf233eb1ea41bfa77fe747cd4a12 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Thu, 21 Jan 2021 18:11:36 +0100 Subject: [PATCH 03/21] Use stryker-tmp dir --- e2e/test/in-place/test/helpers/testSetup.js | 8 -------- e2e/test/in-place/test/unit/Add.spec.js | 8 -------- e2e/test/in-place/test/unit/Wait.spec.js | 6 ------ packages/core/src/sandbox/sandbox.ts | 6 ++---- 4 files changed, 2 insertions(+), 26 deletions(-) diff --git a/e2e/test/in-place/test/helpers/testSetup.js b/e2e/test/in-place/test/helpers/testSetup.js index ef2fd68a33..993b68e6fc 100644 --- a/e2e/test/in-place/test/helpers/testSetup.js +++ b/e2e/test/in-place/test/helpers/testSetup.js @@ -1,11 +1,3 @@ -// @ts-nocheck -// -// -// -// -// -// -// exports.mochaHooks = { beforeAll() { global.expect = require('chai').expect; diff --git a/e2e/test/in-place/test/unit/Add.spec.js b/e2e/test/in-place/test/unit/Add.spec.js index dbb7d630e0..d37ba16e88 100644 --- a/e2e/test/in-place/test/unit/Add.spec.js +++ b/e2e/test/in-place/test/unit/Add.spec.js @@ -1,11 +1,3 @@ -// @ts-nocheck -// -// -// -// -// -// -// var addModule = require('../../src/Add'); var add = addModule.add; var addOne = addModule.addOne; diff --git a/e2e/test/in-place/test/unit/Wait.spec.js b/e2e/test/in-place/test/unit/Wait.spec.js index 9c9ff614ff..c40cd26a55 100644 --- a/e2e/test/in-place/test/unit/Wait.spec.js +++ b/e2e/test/in-place/test/unit/Wait.spec.js @@ -1,9 +1,3 @@ -// @ts-nocheck -// -// -// -// -// const { existsSync } = require('fs'); const path = require('path'); diff --git a/packages/core/src/sandbox/sandbox.ts b/packages/core/src/sandbox/sandbox.ts index c3a65aea85..13f2077421 100644 --- a/packages/core/src/sandbox/sandbox.ts +++ b/packages/core/src/sandbox/sandbox.ts @@ -1,6 +1,5 @@ import path = require('path'); import { promises as fsPromises } from 'fs'; -import os = require('os'); import execa = require('execa'); import npmRunPath = require('npm-run-path'); @@ -15,7 +14,6 @@ import { from } from 'rxjs'; import { TemporaryDirectory } from '../utils/temporary-directory'; import { findNodeModules, MAX_CONCURRENT_FILE_IO, moveDirectoryRecursiveSync, symlinkJunction, writeFile, mkdirp } from '../utils/file-utils'; import { coreTokens } from '../di'; -import { random } from '../utils/object-utils'; import { UnexpectedExitRegister } from '../stryker-registry'; export class Sandbox implements Disposable { @@ -42,8 +40,8 @@ export class Sandbox implements Disposable { ) { if (options.inPlace) { this.workingDirectory = process.cwd(); - this.backupDirectory = path.join(os.tmpdir(), `stryker-backup-${random()}`); - this.log.info('In place mode detected. Stryker will be overriding YOUR files. Find your backup at: %s', this.backupDirectory); + this.backupDirectory = temporaryDirectory.createRandomDirectory('backup'); + this.log.info('InPlace is enabled, Stryker will be overriding YOUR files. Find your backup at: %s', this.backupDirectory); unexpectedExitRegistry.registerUnexpectedExitHandler(this.dispose.bind(this, true)); } else { this.workingDirectory = temporaryDirectory.createRandomDirectory('sandbox'); From 659984261b7b70b411a2cbc34123156bfbe49cf5 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Thu, 21 Jan 2021 20:28:10 +0100 Subject: [PATCH 04/21] refactor: provide stryker register via di --- packages/core/src/di/build-main-injector.ts | 5 ++++- .../process/2-mutant-instrumenter-executor.ts | 3 --- packages/core/src/stryker-registry.ts | 17 +++++++++-------- packages/core/src/stryker.ts | 10 +--------- .../test/unit/di/build-main-injector.spec.ts | 10 ++++++++++ packages/core/test/unit/stryker.spec.ts | 5 ----- 6 files changed, 24 insertions(+), 26 deletions(-) diff --git a/packages/core/src/di/build-main-injector.ts b/packages/core/src/di/build-main-injector.ts index fe67a193bf..3cd82ac437 100644 --- a/packages/core/src/di/build-main-injector.ts +++ b/packages/core/src/di/build-main-injector.ts @@ -9,6 +9,7 @@ import ConfigReader from '../config/config-reader'; import BroadcastReporter from '../reporters/broadcast-reporter'; import { TemporaryDirectory } from '../utils/temporary-directory'; import Timer from '../utils/timer'; +import { StrykerRegistry } from '../stryker-registry'; import { pluginResolverFactory } from './factory-methods'; @@ -21,6 +22,7 @@ export interface MainContext extends PluginContext { [coreTokens.timer]: I; [coreTokens.temporaryDirectory]: I; [coreTokens.execa]: typeof execa; + [coreTokens.unexpectedExitRegistry]: StrykerRegistry; } type PluginResolverProvider = Injector; @@ -35,7 +37,8 @@ export function buildMainInjector(injector: CliOptionsProvider): Injector void; export interface UnexpectedExitRegister { @@ -6,19 +8,13 @@ export interface UnexpectedExitRegister { const signals = Object.freeze(['SIGABRT', 'SIGINT', 'SIGHUP', 'SIGTERM']); -export class StrykerRegistry implements UnexpectedExitRegister { +export class StrykerRegistry implements UnexpectedExitRegister, Disposable { private readonly unexpectedExitHandlers: ExitHandler[] = []; - public startHandleExit() { + constructor() { process.on('exit', this.handleExit); signals.forEach((signal) => process.on(signal, this.processSignal)); } - - public stopHandleExit() { - process.off('exit', this.handleExit); - signals.forEach((signal) => process.off(signal, this.processSignal)); - } - private processSignal(_signal: string, signalNumber: number) { // Just call 'exit' with correct exitCode. // See https://nodejs.org/api/process.html#process_signal_events, we should exit with 128 + signal number @@ -32,4 +28,9 @@ export class StrykerRegistry implements UnexpectedExitRegister { public registerUnexpectedExitHandler(handler: ExitHandler) { this.unexpectedExitHandlers.push(handler); } + + public dispose(): void { + process.off('exit', this.handleExit); + signals.forEach((signal) => process.off(signal, this.processSignal)); + } } diff --git a/packages/core/src/stryker.ts b/packages/core/src/stryker.ts index a15599bc5e..efdf023106 100644 --- a/packages/core/src/stryker.ts +++ b/packages/core/src/stryker.ts @@ -8,7 +8,6 @@ import { LogConfigurator } from './logging'; import { PrepareExecutor, MutantInstrumenterExecutor, DryRunExecutor, MutationTestExecutor } from './process'; import { coreTokens, provideLogger } from './di'; import { retrieveCause, ConfigError } from './errors'; -import { StrykerRegistry } from './stryker-registry'; /** * The main Stryker class. @@ -24,13 +23,9 @@ export default class Stryker { public async runMutationTest(): Promise { const rootInjector = this.injectorFactory(); - const strykerRegistry = rootInjector.injectClass(StrykerRegistry); - const loggerProvider = provideLogger(rootInjector).provideValue(coreTokens.unexpectedExitRegistry, strykerRegistry); + const loggerProvider = provideLogger(rootInjector); try { - // Register a global exit handler, this will allow the Sandbox class to use `strykerRegistry.registerUnexpectedExitHandler` to cleanup on unexpected exit - strykerRegistry.startHandleExit(); - // 1. Prepare. Load Stryker configuration, load the input files and starts the logging server const prepareExecutor = loggerProvider.provideValue(coreTokens.cliOptions, this.cliOptions).injectClass(PrepareExecutor); const mutantInstrumenterInjector = await prepareExecutor.execute(); @@ -70,9 +65,6 @@ export default class Stryker { } finally { await rootInjector.dispose(); await LogConfigurator.shutdown(); - - // Remove our grip on the exit handlers - strykerRegistry.stopHandleExit(); } } } diff --git a/packages/core/test/unit/di/build-main-injector.spec.ts b/packages/core/test/unit/di/build-main-injector.spec.ts index 2df9a252f5..9ce5b6b0f7 100644 --- a/packages/core/test/unit/di/build-main-injector.spec.ts +++ b/packages/core/test/unit/di/build-main-injector.spec.ts @@ -13,6 +13,7 @@ import { PluginCreator, PluginLoader, coreTokens, provideLogger } from '../../.. import { buildMainInjector, CliOptionsProvider } from '../../../src/di/build-main-injector'; import * as broadcastReporterModule from '../../../src/reporters/broadcast-reporter'; import currentLogMock from '../../helpers/log-mock'; +import { StrykerRegistry } from '../../../src/stryker-registry'; describe(buildMainInjector.name, () => { let pluginLoaderMock: sinon.SinonStubbedInstance; @@ -45,6 +46,10 @@ describe(buildMainInjector.name, () => { stubInjectable(broadcastReporterModule, 'default').returns(broadcastReporterMock); }); + afterEach(async () => { + await injector.dispose(); + }); + function stubInjectable(obj: T, method: keyof T) { const inject = (obj[method] as any).inject; const stub = sinon.stub(obj, method); @@ -104,4 +109,9 @@ describe(buildMainInjector.name, () => { expect(actualInjector.resolve(coreTokens.pluginCreatorReporter)).eq(pluginCreatorMock); expect(actualInjector.resolve(coreTokens.pluginCreatorChecker)).eq(pluginCreatorMock); }); + + it('should be able to supply a UnexpectedExitRegister', () => { + const actualInjector = buildMainInjector(injector); + expect(actualInjector.resolve(coreTokens.unexpectedExitRegistry)).instanceOf(StrykerRegistry); + }); }); diff --git a/packages/core/test/unit/stryker.spec.ts b/packages/core/test/unit/stryker.spec.ts index 6a94aa08c3..82efc953c8 100644 --- a/packages/core/test/unit/stryker.spec.ts +++ b/packages/core/test/unit/stryker.spec.ts @@ -13,7 +13,6 @@ import { PrepareExecutor, MutantInstrumenterExecutor, DryRunExecutor, MutationTe import { coreTokens } from '../../src/di'; import { ConfigError } from '../../src/errors'; import { TemporaryDirectory } from '../../src/utils/temporary-directory'; -import { StrykerRegistry } from '../../src/stryker-registry'; describe(Stryker.name, () => { let sut: Stryker; @@ -24,7 +23,6 @@ describe(Stryker.name, () => { let loggerMock: sinon.SinonStubbedInstance; let temporaryDirectoryMock: sinon.SinonStubbedInstance; let getLoggerStub: sinon.SinonStub; - let strykerRegistryMock: sinon.SinonStubbedInstance; let prepareExecutorMock: sinon.SinonStubbedInstance; let mutantInstrumenterExecutorMock: sinon.SinonStubbedInstance; @@ -41,10 +39,7 @@ describe(Stryker.name, () => { mutantInstrumenterExecutorMock = sinon.createStubInstance(MutantInstrumenterExecutor); dryRunExecutorMock = sinon.createStubInstance(DryRunExecutor); mutationTestExecutorMock = sinon.createStubInstance(MutationTestExecutor); - strykerRegistryMock = sinon.createStubInstance(StrykerRegistry); injectorMock.injectClass - .withArgs(StrykerRegistry) - .returns(strykerRegistryMock) .withArgs(PrepareExecutor) .returns(prepareExecutorMock) .withArgs(MutantInstrumenterExecutor) From a34016926a313aa7b7ee95bb5c78187d0b07d8ca Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Thu, 21 Jan 2021 20:57:11 +0100 Subject: [PATCH 05/21] Add unit tests for unexpected-exit-handler --- packages/core/src/di/build-main-injector.ts | 8 ++- packages/core/src/di/core-tokens.ts | 1 + packages/core/src/sandbox/sandbox.ts | 6 +- packages/core/src/stryker-registry.ts | 36 ---------- packages/core/src/unexpected-exit-handler.ts | 39 ++++++++++ .../test/unit/di/build-main-injector.spec.ts | 4 +- .../core/test/unit/sandbox/sandbox.spec.ts | 7 +- .../test/unit/unexpected-exit-handler.spec.ts | 72 +++++++++++++++++++ 8 files changed, 126 insertions(+), 47 deletions(-) delete mode 100644 packages/core/src/stryker-registry.ts create mode 100644 packages/core/src/unexpected-exit-handler.ts create mode 100644 packages/core/test/unit/unexpected-exit-handler.spec.ts diff --git a/packages/core/src/di/build-main-injector.ts b/packages/core/src/di/build-main-injector.ts index 3cd82ac437..641773ecc5 100644 --- a/packages/core/src/di/build-main-injector.ts +++ b/packages/core/src/di/build-main-injector.ts @@ -9,7 +9,7 @@ import ConfigReader from '../config/config-reader'; import BroadcastReporter from '../reporters/broadcast-reporter'; import { TemporaryDirectory } from '../utils/temporary-directory'; import Timer from '../utils/timer'; -import { StrykerRegistry } from '../stryker-registry'; +import { UnexpectedExitHandler } from '../unexpected-exit-handler'; import { pluginResolverFactory } from './factory-methods'; @@ -22,7 +22,8 @@ export interface MainContext extends PluginContext { [coreTokens.timer]: I; [coreTokens.temporaryDirectory]: I; [coreTokens.execa]: typeof execa; - [coreTokens.unexpectedExitRegistry]: StrykerRegistry; + [coreTokens.process]: NodeJS.Process; + [coreTokens.unexpectedExitRegistry]: I; } type PluginResolverProvider = Injector; @@ -38,7 +39,8 @@ export function buildMainInjector(injector: CliOptionsProvider): Injector(); @@ -36,13 +36,13 @@ export class Sandbox implements Disposable { temporaryDirectory: I, private readonly files: readonly File[], private readonly exec: typeof execa, - unexpectedExitRegistry: UnexpectedExitRegister + unexpectedExitRegistry: I ) { if (options.inPlace) { this.workingDirectory = process.cwd(); this.backupDirectory = temporaryDirectory.createRandomDirectory('backup'); this.log.info('InPlace is enabled, Stryker will be overriding YOUR files. Find your backup at: %s', this.backupDirectory); - unexpectedExitRegistry.registerUnexpectedExitHandler(this.dispose.bind(this, true)); + unexpectedExitRegistry.registerHandler(this.dispose.bind(this, true)); } else { this.workingDirectory = temporaryDirectory.createRandomDirectory('sandbox'); this.log.debug('Creating a sandbox for files in %s', this.workingDirectory); diff --git a/packages/core/src/stryker-registry.ts b/packages/core/src/stryker-registry.ts deleted file mode 100644 index 0fe06c8a63..0000000000 --- a/packages/core/src/stryker-registry.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Disposable } from '@stryker-mutator/api/plugin'; - -export type ExitHandler = () => void; - -export interface UnexpectedExitRegister { - registerUnexpectedExitHandler(handler: ExitHandler): void; -} - -const signals = Object.freeze(['SIGABRT', 'SIGINT', 'SIGHUP', 'SIGTERM']); - -export class StrykerRegistry implements UnexpectedExitRegister, Disposable { - private readonly unexpectedExitHandlers: ExitHandler[] = []; - - constructor() { - process.on('exit', this.handleExit); - signals.forEach((signal) => process.on(signal, this.processSignal)); - } - private processSignal(_signal: string, signalNumber: number) { - // Just call 'exit' with correct exitCode. - // See https://nodejs.org/api/process.html#process_signal_events, we should exit with 128 + signal number - process.exit(128 + signalNumber); - } - - public handleExit = () => { - this.unexpectedExitHandlers.forEach((handler) => handler()); - }; - - public registerUnexpectedExitHandler(handler: ExitHandler) { - this.unexpectedExitHandlers.push(handler); - } - - public dispose(): void { - process.off('exit', this.handleExit); - signals.forEach((signal) => process.off(signal, this.processSignal)); - } -} diff --git a/packages/core/src/unexpected-exit-handler.ts b/packages/core/src/unexpected-exit-handler.ts new file mode 100644 index 0000000000..875c2ea82e --- /dev/null +++ b/packages/core/src/unexpected-exit-handler.ts @@ -0,0 +1,39 @@ +import { Disposable } from '@stryker-mutator/api/plugin'; + +import { coreTokens } from './di'; + +export type ExitHandler = () => void; + +export interface UnexpectedExitHandler { + registerHandler(handler: ExitHandler): void; +} + +const signals = Object.freeze(['SIGABRT', 'SIGINT', 'SIGHUP', 'SIGTERM']); + +export class UnexpectedExitHandler implements Disposable { + private readonly unexpectedExitHandlers: ExitHandler[] = []; + + public static readonly inject = [coreTokens.process] as const; + constructor(private readonly process: Pick) { + process.on('exit', this.handleExit); + signals.forEach((signal) => process.on(signal, this.processSignal)); + } + private readonly processSignal = (_signal: string, signalNumber: number) => { + // Just call 'exit' with correct exitCode. + // See https://nodejs.org/api/process.html#process_signal_events, we should exit with 128 + signal number + this.process.exit(128 + signalNumber); + }; + + private readonly handleExit = () => { + this.unexpectedExitHandlers.forEach((handler) => handler()); + }; + + public registerHandler(handler: ExitHandler) { + this.unexpectedExitHandlers.push(handler); + } + + public dispose(): void { + this.process.off('exit', this.handleExit); + signals.forEach((signal) => this.process.off(signal, this.processSignal)); + } +} diff --git a/packages/core/test/unit/di/build-main-injector.spec.ts b/packages/core/test/unit/di/build-main-injector.spec.ts index 9ce5b6b0f7..24b1ce7098 100644 --- a/packages/core/test/unit/di/build-main-injector.spec.ts +++ b/packages/core/test/unit/di/build-main-injector.spec.ts @@ -13,7 +13,7 @@ import { PluginCreator, PluginLoader, coreTokens, provideLogger } from '../../.. import { buildMainInjector, CliOptionsProvider } from '../../../src/di/build-main-injector'; import * as broadcastReporterModule from '../../../src/reporters/broadcast-reporter'; import currentLogMock from '../../helpers/log-mock'; -import { StrykerRegistry } from '../../../src/stryker-registry'; +import { UnexpectedExitHandler } from '../../../src/unexpected-exit-handler'; describe(buildMainInjector.name, () => { let pluginLoaderMock: sinon.SinonStubbedInstance; @@ -112,6 +112,6 @@ describe(buildMainInjector.name, () => { it('should be able to supply a UnexpectedExitRegister', () => { const actualInjector = buildMainInjector(injector); - expect(actualInjector.resolve(coreTokens.unexpectedExitRegistry)).instanceOf(StrykerRegistry); + expect(actualInjector.resolve(coreTokens.unexpectedExitRegistry)).instanceOf(UnexpectedExitHandler); }); }); diff --git a/packages/core/test/unit/sandbox/sandbox.spec.ts b/packages/core/test/unit/sandbox/sandbox.spec.ts index 33d3db295b..815e7222d6 100644 --- a/packages/core/test/unit/sandbox/sandbox.spec.ts +++ b/packages/core/test/unit/sandbox/sandbox.spec.ts @@ -13,7 +13,7 @@ import { Sandbox } from '../../../src/sandbox/sandbox'; import { coreTokens } from '../../../src/di'; import { TemporaryDirectory } from '../../../src/utils/temporary-directory'; import * as fileUtils from '../../../src/utils/file-utils'; -import { UnexpectedExitRegister } from '../../../src/stryker-registry'; +import { UnexpectedExitHandler } from '../../../src/unexpected-exit-handler'; describe(Sandbox.name, () => { let temporaryDirectoryMock: sinon.SinonStubbedInstance; @@ -23,7 +23,7 @@ describe(Sandbox.name, () => { let symlinkJunctionStub: sinon.SinonStub; let findNodeModulesStub: sinon.SinonStub; let execaMock: sinon.SinonStubbedInstance; - let unexpectedExitRegisterMock: sinon.SinonStubbedInstance; + let unexpectedExitRegisterMock: sinon.SinonStubbedInstance; const SANDBOX_WORKING_DIR = 'sandbox-123'; beforeEach(() => { @@ -40,7 +40,8 @@ describe(Sandbox.name, () => { sync: sinon.stub(), }; unexpectedExitRegisterMock = { - registerUnexpectedExitHandler: sinon.stub(), + registerHandler: sinon.stub(), + dispose: sinon.stub(), }; symlinkJunctionStub.resolves(); findNodeModulesStub.resolves('node_modules'); diff --git a/packages/core/test/unit/unexpected-exit-handler.spec.ts b/packages/core/test/unit/unexpected-exit-handler.spec.ts new file mode 100644 index 0000000000..2ee93f821c --- /dev/null +++ b/packages/core/test/unit/unexpected-exit-handler.spec.ts @@ -0,0 +1,72 @@ +import { EventEmitter } from 'events'; + +import { testInjector } from '@stryker-mutator/test-helpers'; +import sinon = require('sinon'); +import { expect } from 'chai'; + +import { coreTokens } from '../../src/di'; +import { UnexpectedExitHandler } from '../../src/unexpected-exit-handler'; + +class ProcessMock extends EventEmitter { + public exit = sinon.stub(); +} +const signals = Object.freeze(['SIGABRT', 'SIGINT', 'SIGHUP', 'SIGTERM']); + +describe(UnexpectedExitHandler.name, () => { + let processMock: ProcessMock; + + beforeEach(() => { + processMock = new ProcessMock(); + }); + + function createSut() { + return testInjector.injector + .provideValue(coreTokens.process, (processMock as unknown) as Pick) + .injectClass(UnexpectedExitHandler); + } + + describe('constructor', () => { + it('should register an exit handler', () => { + createSut(); + expect(processMock.listenerCount('exit')).eq(1); + }); + + signals.forEach((signal) => { + it(`should register a "${signal}" signal handler`, () => { + createSut(); + expect(processMock.listenerCount(signal)).eq(1); + }); + }); + }); + + describe('dispose', () => { + it('should remove the "exit" handler', () => { + createSut().dispose(); + expect(processMock.listenerCount('exit')).eq(0); + }); + signals.forEach((signal) => { + it(`should remove the "${signal}" signal handler`, () => { + createSut().dispose(); + expect(processMock.listenerCount(signal)).eq(0); + }); + }); + }); + + signals.forEach((signal) => { + it(`should call process.exit on "${signal}" signal`, () => { + createSut(); + processMock.emit(signal, [signal, 4]); + expect(processMock.exit).calledWith(132); + }); + }); + + describe(UnexpectedExitHandler.prototype.registerHandler.name, () => { + it('should call the provided handler on exit', () => { + const exitHandler = sinon.stub(); + const sut = createSut(); + sut.registerHandler(exitHandler); + processMock.emit('exit'); + expect(exitHandler).called; + }); + }); +}); From 13f1822c7d278969fe140095cb529fe2e79b4f22 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Thu, 21 Jan 2021 22:09:06 +0100 Subject: [PATCH 06/21] Add unit tests for sandbox dir --- packages/core/src/sandbox/sandbox.ts | 16 +- packages/core/src/utils/file-utils.ts | 14 -- .../core/test/unit/sandbox/sandbox.spec.ts | 208 ++++++++++++++---- .../test/unit/unexpected-exit-handler.spec.ts | 2 +- .../core/test/unit/utils/file-utils.spec.ts | 7 - 5 files changed, 176 insertions(+), 71 deletions(-) diff --git a/packages/core/src/sandbox/sandbox.ts b/packages/core/src/sandbox/sandbox.ts index 203d83ed2c..8767dba215 100644 --- a/packages/core/src/sandbox/sandbox.ts +++ b/packages/core/src/sandbox/sandbox.ts @@ -12,7 +12,7 @@ import { mergeMap, toArray } from 'rxjs/operators'; import { from } from 'rxjs'; import { TemporaryDirectory } from '../utils/temporary-directory'; -import { findNodeModules, MAX_CONCURRENT_FILE_IO, moveDirectoryRecursiveSync, symlinkJunction, writeFile, mkdirp } from '../utils/file-utils'; +import { findNodeModules, MAX_CONCURRENT_FILE_IO, moveDirectoryRecursiveSync, symlinkJunction, mkdirp } from '../utils/file-utils'; import { coreTokens } from '../di'; import { UnexpectedExitHandler } from '../unexpected-exit-handler'; @@ -36,13 +36,13 @@ export class Sandbox implements Disposable { temporaryDirectory: I, private readonly files: readonly File[], private readonly exec: typeof execa, - unexpectedExitRegistry: I + unexpectedExitHandler: I ) { if (options.inPlace) { this.workingDirectory = process.cwd(); this.backupDirectory = temporaryDirectory.createRandomDirectory('backup'); - this.log.info('InPlace is enabled, Stryker will be overriding YOUR files. Find your backup at: %s', this.backupDirectory); - unexpectedExitRegistry.registerHandler(this.dispose.bind(this, true)); + this.log.info('In place mode is enabled, Stryker will be overriding YOUR files. Find your backup at: %s', this.backupDirectory); + unexpectedExitHandler.registerHandler(this.dispose.bind(this, true)); } else { this.workingDirectory = temporaryDirectory.createRandomDirectory('sandbox'); this.log.debug('Creating a sandbox for files in %s', this.workingDirectory); @@ -110,19 +110,19 @@ export class Sandbox implements Disposable { this.fileMap.set(file.name, file.name); const originalContent = await fsPromises.readFile(file.name); if (originalContent.compare(file.content) !== 0) { - // difference + // File is changed (either mutated or by a preprocessor), make a backup and replace in-place const backupFileName = path.join(this.backupDirectory, relativePath); await mkdirp(path.dirname(backupFileName)); await fsPromises.writeFile(backupFileName, originalContent); - this.log.debug(`Stored backup file at ${backupFileName}`); - await writeFile(file.name, file.content); + this.log.debug('Stored backup file at %s', backupFileName); + await fsPromises.writeFile(file.name, file.content); } } else { const folderName = path.join(this.workingDirectory, path.dirname(relativePath)); await mkdirp(folderName); const targetFileName = path.join(folderName, path.basename(relativePath)); this.fileMap.set(file.name, targetFileName); - await writeFile(targetFileName, file.content); + await fsPromises.writeFile(targetFileName, file.content); } } diff --git a/packages/core/src/utils/file-utils.ts b/packages/core/src/utils/file-utils.ts index 056053b4ad..850ffe480b 100644 --- a/packages/core/src/utils/file-utils.ts +++ b/packages/core/src/utils/file-utils.ts @@ -38,20 +38,6 @@ export function importModule(moduleName: string): unknown { return require(moduleName); } -/** - * Writes data to a specified file. - * @param fileName The path to the file. - * @param data The content of the file. - * @returns A promise to eventually save the file. - */ -export function writeFile(fileName: string, data: string | Buffer): Promise { - if (Buffer.isBuffer(data)) { - return fs.promises.writeFile(fileName, data); - } else { - return fs.promises.writeFile(fileName, data, 'utf8'); - } -} - /** * Recursively walks the from directory and copy the content to the target directory synchronously * @param from The source directory to move from diff --git a/packages/core/test/unit/sandbox/sandbox.spec.ts b/packages/core/test/unit/sandbox/sandbox.spec.ts index 815e7222d6..46c1b80eb1 100644 --- a/packages/core/test/unit/sandbox/sandbox.spec.ts +++ b/packages/core/test/unit/sandbox/sandbox.spec.ts @@ -1,4 +1,5 @@ import path = require('path'); +import { promises as fsPromises } from 'fs'; import execa = require('execa'); import npmRunPath = require('npm-run-path'); @@ -23,23 +24,28 @@ describe(Sandbox.name, () => { let symlinkJunctionStub: sinon.SinonStub; let findNodeModulesStub: sinon.SinonStub; let execaMock: sinon.SinonStubbedInstance; - let unexpectedExitRegisterMock: sinon.SinonStubbedInstance; + let unexpectedExitHandlerMock: sinon.SinonStubbedInstance; + let readFile: sinon.SinonStub; + let moveDirectoryRecursiveSyncStub: sinon.SinonStub; const SANDBOX_WORKING_DIR = 'sandbox-123'; + const BACKUP_DIR = 'backup-123'; beforeEach(() => { temporaryDirectoryMock = sinon.createStubInstance(TemporaryDirectory); - temporaryDirectoryMock.createRandomDirectory.returns(SANDBOX_WORKING_DIR); + temporaryDirectoryMock.createRandomDirectory.withArgs('sandbox').returns(SANDBOX_WORKING_DIR).withArgs('backup').returns(BACKUP_DIR); mkdirpStub = sinon.stub(fileUtils, 'mkdirp'); - writeFileStub = sinon.stub(fileUtils, 'writeFile'); + writeFileStub = sinon.stub(fsPromises, 'writeFile'); symlinkJunctionStub = sinon.stub(fileUtils, 'symlinkJunction'); findNodeModulesStub = sinon.stub(fileUtils, 'findNodeModules'); + moveDirectoryRecursiveSyncStub = sinon.stub(fileUtils, 'moveDirectoryRecursiveSync'); + readFile = sinon.stub(fsPromises, 'readFile'); execaMock = { command: sinon.stub(), commandSync: sinon.stub(), node: sinon.stub(), sync: sinon.stub(), }; - unexpectedExitRegisterMock = { + unexpectedExitHandlerMock = { registerHandler: sinon.stub(), dispose: sinon.stub(), }; @@ -53,43 +59,139 @@ describe(Sandbox.name, () => { .provideValue(coreTokens.files, files) .provideValue(coreTokens.temporaryDirectory, temporaryDirectoryMock) .provideValue(coreTokens.execa, (execaMock as unknown) as typeof execa) - .provideValue(coreTokens.unexpectedExitRegistry, unexpectedExitRegisterMock) + .provideValue(coreTokens.unexpectedExitRegistry, unexpectedExitHandlerMock) .injectClass(Sandbox); } describe('init()', () => { - it('should have created a sandbox folder', async () => { - const sut = createSut(); - await sut.init(); - expect(temporaryDirectoryMock.createRandomDirectory).calledWith('sandbox'); - }); + describe('with inPlace = false', () => { + beforeEach(() => { + testInjector.options.inPlace = false; + }); - it('should copy regular input files', async () => { - const fileB = new File(path.resolve('a', 'b.txt'), 'b content'); - const fileE = new File(path.resolve('c', 'd', 'e.log'), 'e content'); - files.push(fileB); - files.push(fileE); - const sut = createSut(); - await sut.init(); - expect(writeFileStub).calledWith(path.join(SANDBOX_WORKING_DIR, 'a', 'b.txt'), fileB.content); - expect(writeFileStub).calledWith(path.join(SANDBOX_WORKING_DIR, 'c', 'd', 'e.log'), fileE.content); - }); + it('should have created a sandbox folder', async () => { + const sut = createSut(); + await sut.init(); + expect(temporaryDirectoryMock.createRandomDirectory).calledWith('sandbox'); + }); - it('should make the dir before copying the file', async () => { - files.push(new File(path.resolve('a', 'b.js'), 'b content')); - files.push(new File(path.resolve('c', 'd', 'e.js'), 'e content')); - const sut = createSut(); - await sut.init(); - expect(mkdirpStub).calledTwice; - expect(mkdirpStub).calledWithExactly(path.join(SANDBOX_WORKING_DIR, 'a')); - expect(mkdirpStub).calledWithExactly(path.join(SANDBOX_WORKING_DIR, 'c', 'd')); + it('should copy regular input files', async () => { + const fileB = new File(path.resolve('a', 'b.txt'), 'b content'); + const fileE = new File(path.resolve('c', 'd', 'e.log'), 'e content'); + files.push(fileB); + files.push(fileE); + const sut = createSut(); + await sut.init(); + expect(writeFileStub).calledWith(path.join(SANDBOX_WORKING_DIR, 'a', 'b.txt'), fileB.content); + expect(writeFileStub).calledWith(path.join(SANDBOX_WORKING_DIR, 'c', 'd', 'e.log'), fileE.content); + }); + + it('should make the dir before copying the file', async () => { + files.push(new File(path.resolve('a', 'b.js'), 'b content')); + files.push(new File(path.resolve('c', 'd', 'e.js'), 'e content')); + const sut = createSut(); + await sut.init(); + expect(mkdirpStub).calledTwice; + expect(mkdirpStub).calledWithExactly(path.join(SANDBOX_WORKING_DIR, 'a')); + expect(mkdirpStub).calledWithExactly(path.join(SANDBOX_WORKING_DIR, 'c', 'd')); + }); + + it('should be able to copy a local file', async () => { + files.push(new File('localFile.txt', 'foobar')); + const sut = createSut(); + await sut.init(); + expect(writeFileStub).calledWith(path.join(SANDBOX_WORKING_DIR, 'localFile.txt'), Buffer.from('foobar')); + }); + + it('should symlink node modules in sandbox directory if exists', async () => { + const sut = createSut(); + await sut.init(); + expect(findNodeModulesStub).calledWith(process.cwd()); + expect(symlinkJunctionStub).calledWith('node_modules', path.join(SANDBOX_WORKING_DIR, 'node_modules')); + }); }); - it('should be able to copy a local file', async () => { - files.push(new File('localFile.txt', 'foobar')); - const sut = createSut(); - await sut.init(); - expect(writeFileStub).calledWith(path.join(SANDBOX_WORKING_DIR, 'localFile.txt'), Buffer.from('foobar')); + describe('with inPlace = true', () => { + beforeEach(() => { + testInjector.options.inPlace = true; + }); + + it('should have created a backup directory', async () => { + const sut = createSut(); + await sut.init(); + expect(temporaryDirectoryMock.createRandomDirectory).calledWith('backup'); + }); + + it('should not override the current file if no changes were detected', async () => { + const fileB = new File(path.resolve('a', 'b.txt'), 'b content'); + readFile.withArgs(path.resolve('a', 'b.txt')).resolves(Buffer.from('b content')); + files.push(fileB); + const sut = createSut(); + await sut.init(); + expect(writeFileStub).not.called; + }); + + it('should override original file if changes were detected', async () => { + // Arrange + const fileName = path.resolve('a', 'b.js'); + const originalContent = Buffer.from('b content'); + const fileB = new File(fileName, 'b mutated content'); + readFile.withArgs(fileName).resolves(originalContent); + files.push(fileB); + + // Act + const sut = createSut(); + await sut.init(); + + // Assert + expect(writeFileStub).calledWith(fileB.name, fileB.content); + }); + + it('should override backup the original before overriding it', async () => { + // Arrange + const fileName = path.resolve('a', 'b.js'); + const originalContent = Buffer.from('b content'); + const fileB = new File(fileName, 'b mutated content'); + readFile.withArgs(fileName).resolves(originalContent); + files.push(fileB); + const expectedBackupDirectory = path.join(BACKUP_DIR, 'a'); + const expectedBackupFileName = path.join(expectedBackupDirectory, 'b.js'); + + // Act + const sut = createSut(); + await sut.init(); + + // Assert + expect(mkdirpStub).calledWith(expectedBackupDirectory); + expect(writeFileStub).calledWith(expectedBackupFileName, originalContent); + expect(writeFileStub.withArgs(expectedBackupFileName)).calledBefore(writeFileStub.withArgs(fileB.name)); + }); + + it('should log the backup file location', async () => { + // Arrange + const fileName = path.resolve('a', 'b.js'); + const originalContent = Buffer.from('b content'); + const fileB = new File(fileName, 'b mutated content'); + readFile.withArgs(fileName).resolves(originalContent); + files.push(fileB); + const expectedBackupFileName = path.join(BACKUP_DIR, 'a', 'b.js'); + + // Act + const sut = createSut(); + await sut.init(); + + // Assert + expect(testInjector.logger.debug).calledWith('Stored backup file at %s', expectedBackupFileName); + }); + + it('should register an unexpected exit handler', async () => { + // Act + const sut = createSut(); + await sut.init(); + + // Assert + expect(unexpectedExitHandlerMock.registerHandler).called; + }); }); it('should not open too many file handles', async () => { @@ -117,13 +219,6 @@ describe(Sandbox.name, () => { await initPromise; }); - it('should symlink node modules in sandbox directory if exists', async () => { - const sut = createSut(); - await sut.init(); - expect(findNodeModulesStub).calledWith(process.cwd()); - expect(symlinkJunctionStub).calledWith('node_modules', path.join(SANDBOX_WORKING_DIR, 'node_modules')); - }); - it('should not symlink node modules in sandbox directory if no node_modules exist', async () => { findNodeModulesStub.resolves(null); const sut = createSut(); @@ -191,11 +286,42 @@ describe(Sandbox.name, () => { }); }); + describe('dispose', () => { + it("shouldn't do anything when inPlace = false", () => { + const sut = createSut(); + sut.dispose(); + expect(moveDirectoryRecursiveSyncStub).not.called; + }); + + it('should recover from the backup dir synchronously if inPlace = true', () => { + testInjector.options.inPlace = true; + const sut = createSut(); + sut.dispose(); + expect(moveDirectoryRecursiveSyncStub).calledWith(BACKUP_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}`); + }); + }); + describe('workingDirectory', () => { - it('should retrieve the sandbox directory', async () => { + it('should retrieve the sandbox directory when inPlace = false', async () => { const sut = createSut(); await sut.init(); expect(sut.workingDirectory).eq(SANDBOX_WORKING_DIR); }); + + it('should retrieve the cwd directory when inPlace = true', async () => { + testInjector.options.inPlace = true; + const sut = createSut(); + await sut.init(); + expect(sut.workingDirectory).eq(process.cwd()); + }); }); }); diff --git a/packages/core/test/unit/unexpected-exit-handler.spec.ts b/packages/core/test/unit/unexpected-exit-handler.spec.ts index 2ee93f821c..dc1cf08097 100644 --- a/packages/core/test/unit/unexpected-exit-handler.spec.ts +++ b/packages/core/test/unit/unexpected-exit-handler.spec.ts @@ -55,7 +55,7 @@ describe(UnexpectedExitHandler.name, () => { signals.forEach((signal) => { it(`should call process.exit on "${signal}" signal`, () => { createSut(); - processMock.emit(signal, [signal, 4]); + processMock.emit(signal, signal, 4); expect(processMock.exit).calledWith(132); }); }); diff --git a/packages/core/test/unit/utils/file-utils.spec.ts b/packages/core/test/unit/utils/file-utils.spec.ts index 2d1fa259c0..b73c70ee54 100644 --- a/packages/core/test/unit/utils/file-utils.spec.ts +++ b/packages/core/test/unit/utils/file-utils.spec.ts @@ -15,13 +15,6 @@ describe('fileUtils', () => { statStub = sinon.stub(fs.promises, 'stat'); }); - describe('writeFile', () => { - it('should call fs.writeFile', () => { - fileUtils.writeFile('filename', 'data'); - expect(fs.promises.writeFile).calledWith('filename', 'data', 'utf8'); - }); - }); - describe('symlinkJunction', () => { it('should call fs.symlink', async () => { await fileUtils.symlinkJunction('a', 'b'); From 77ed18c1e0c49a53edfa61018b47a84453e90713 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 22 Jan 2021 08:52:38 +0100 Subject: [PATCH 07/21] Add integration tests for `moveDirectoryRecursiveSync` --- .../test/integration/utils/file-utils.spec.ts | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/packages/core/test/integration/utils/file-utils.spec.ts b/packages/core/test/integration/utils/file-utils.spec.ts index 6eb0b4d243..504922cf21 100644 --- a/packages/core/test/integration/utils/file-utils.spec.ts +++ b/packages/core/test/integration/utils/file-utils.spec.ts @@ -1,4 +1,13 @@ +import os = require('os'); +import path = require('path'); +import { promises as fsPromises } from 'fs'; + +import mkdirp = require('mkdirp'); + import { expect } from 'chai'; +import { File } from '@stryker-mutator/api/core'; +import nodeGlob = require('glob'); +import { assertions } from '@stryker-mutator/test-helpers'; import * as fileUtils from '../../../src/utils/file-utils'; @@ -9,4 +18,88 @@ describe('fileUtils', () => { it('should not resolve to directories', () => expect(fileUtils.glob('testResources/globTestFiles/notResolveDirs/**/*.js')).to.eventually.have.length(1)); }); + + describe('moveDirectoryRecursiveSync', () => { + const from = path.resolve(os.tmpdir(), 'moveDirectoryRecursiveSyncFrom'); + const to = path.resolve(os.tmpdir(), 'moveDirectoryRecursiveSyncTo'); + + afterEach(async () => { + await Promise.all([fileUtils.deleteDir(from), fileUtils.deleteDir(to)]); + }); + + it('should override target files', async () => { + // Arrange + const fromFileA = new File(path.resolve(from, 'a.js'), 'original a'); + const fromFileB = new File(path.resolve(from, 'b', 'b.js'), 'original b'); + const toFileA = new File(path.resolve(to, 'a.js'), 'mutated a'); + const toFileB = new File(path.resolve(to, 'b', 'b.js'), 'mutated b'); + await writeAll(fromFileA, fromFileB, toFileA, toFileB); + + // Act + fileUtils.moveDirectoryRecursiveSync(from, to); + + // Assert + const files = await readDirRecursive(to); + assertions.expectTextFilesEqual(files, [new File(toFileA.name, fromFileA.content), new File(toFileB.name, fromFileB.content)]); + }); + + it("should create dirs that don't exist", async () => { + // Arrange + const fromFileA = new File(path.resolve(from, 'a.js'), 'original a'); + const fromFileB = new File(path.resolve(from, 'b', 'b.js'), 'original b'); + await writeAll(fromFileA, fromFileB); + + // Act + fileUtils.moveDirectoryRecursiveSync(from, to); + + // Assert + const files = await readDirRecursive(to); + assertions.expectTextFilesEqual(files, [ + new File(path.resolve(to, 'a.js'), fromFileA.content), + new File(path.resolve(to, 'b', 'b.js'), fromFileB.content), + ]); + }); + + it('should remove the from directory', async () => { + // Arrange + const fromFileA = new File(path.resolve(from, 'a.js'), 'original a'); + const fromFileB = new File(path.resolve(from, 'b', 'b.js'), 'original b'); + await writeAll(fromFileA, fromFileB); + + // Act + fileUtils.moveDirectoryRecursiveSync(from, to); + + // Assert + await expect(fsPromises.access(from)).rejected; + }); + }); + + async function readDirRecursive(dir: string): Promise { + return new Promise((res, rej) => { + nodeGlob(path.join(dir, '**/*'), { nodir: true }, (err, matches) => { + if (err) { + rej(err); + } + // eslint-disable-next-line @typescript-eslint/require-array-sort-compare + matches.sort(); + res( + Promise.all( + matches.map(async (fileName) => { + const content = await fsPromises.readFile(fileName); + return new File(fileName, content); + }) + ) + ); + }); + }); + } + + async function writeAll(...files: File[]): Promise { + await Promise.all( + files.map(async (file) => { + await mkdirp(path.dirname(file.name)); + await fsPromises.writeFile(file.name, file.content); + }) + ); + } }); From 9d4bf462a0c53330d2260df6c6538ae2fc5821a6 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 22 Jan 2021 09:17:44 +0100 Subject: [PATCH 08/21] Remove mension of 'sandbox' in message --- packages/core/src/sandbox/sandbox.ts | 4 ++-- packages/core/test/unit/sandbox/sandbox.spec.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/sandbox/sandbox.ts b/packages/core/src/sandbox/sandbox.ts index 8767dba215..2debf22d5d 100644 --- a/packages/core/src/sandbox/sandbox.ts +++ b/packages/core/src/sandbox/sandbox.ts @@ -75,7 +75,7 @@ export class Sandbox implements Disposable { private async runBuildCommand() { if (this.options.buildCommand) { const env = npmRunPath.env(); - this.log.info('Running build command "%s" in the sandbox at "%s".', this.options.buildCommand, this.workingDirectory); + this.log.info('Running build command "%s" in "%s".', this.options.buildCommand, this.workingDirectory); this.log.debug('(using PATH: %s)', env.PATH); await this.exec.command(this.options.buildCommand, { cwd: this.workingDirectory, env }); } @@ -109,7 +109,7 @@ export class Sandbox implements Disposable { if (this.options.inPlace) { this.fileMap.set(file.name, file.name); const originalContent = await fsPromises.readFile(file.name); - if (originalContent.compare(file.content) !== 0) { + if (!originalContent.equals(file.content)) { // File is changed (either mutated or by a preprocessor), make a backup and replace in-place const backupFileName = path.join(this.backupDirectory, relativePath); await mkdirp(path.dirname(backupFileName)); diff --git a/packages/core/test/unit/sandbox/sandbox.spec.ts b/packages/core/test/unit/sandbox/sandbox.spec.ts index 46c1b80eb1..19d5276ef7 100644 --- a/packages/core/test/unit/sandbox/sandbox.spec.ts +++ b/packages/core/test/unit/sandbox/sandbox.spec.ts @@ -267,7 +267,7 @@ describe(Sandbox.name, () => { const sut = createSut(); await sut.init(); expect(execaMock.command).calledWith('npm run build', { cwd: SANDBOX_WORKING_DIR, env: npmRunPath.env() }); - expect(testInjector.logger.info).calledWith('Running build command "%s" in the sandbox at "%s".', 'npm run build', SANDBOX_WORKING_DIR); + expect(testInjector.logger.info).calledWith('Running build command "%s" in "%s".', 'npm run build', SANDBOX_WORKING_DIR); }); it('should not execute a build command when non is configured', async () => { From 62334dbc0122b0c5c61bc5694a313833a123b2ac Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 22 Jan 2021 09:23:09 +0100 Subject: [PATCH 09/21] Support `--inPlace` in the ts-config-preprossor --- .../src/sandbox/ts-config-preprocessor.ts | 23 +++++++++++-------- .../sandbox/ts-config-preprocessor.it.spec.ts | 23 +++++++++++++------ 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/packages/core/src/sandbox/ts-config-preprocessor.ts b/packages/core/src/sandbox/ts-config-preprocessor.ts index d72e7e347d..34d11488f2 100644 --- a/packages/core/src/sandbox/ts-config-preprocessor.ts +++ b/packages/core/src/sandbox/ts-config-preprocessor.ts @@ -30,16 +30,21 @@ export class TSConfigPreprocessor implements FilePreprocessor { constructor(private readonly log: Logger, private readonly options: StrykerOptions) {} public async preprocess(input: File[]): Promise { - const tsconfigFile = path.resolve(this.options.tsconfigFile); - if (input.find((file) => file.name === tsconfigFile)) { - this.fs.clear(); - input.forEach((file) => { - this.fs.set(file.name, file); - }); - await this.rewriteTSConfigFile(tsconfigFile); - return [...this.fs.values()]; - } else { + if (this.options.inPlace) { + // If stryker is running 'inPlace', we don't have to change the tsconfig file return input; + } else { + const tsconfigFile = path.resolve(this.options.tsconfigFile); + if (input.find((file) => file.name === tsconfigFile)) { + this.fs.clear(); + input.forEach((file) => { + this.fs.set(file.name, file); + }); + await this.rewriteTSConfigFile(tsconfigFile); + return [...this.fs.values()]; + } else { + return input; + } } } diff --git a/packages/core/test/unit/sandbox/ts-config-preprocessor.it.spec.ts b/packages/core/test/unit/sandbox/ts-config-preprocessor.it.spec.ts index df116235ed..c5fca58ead 100644 --- a/packages/core/test/unit/sandbox/ts-config-preprocessor.it.spec.ts +++ b/packages/core/test/unit/sandbox/ts-config-preprocessor.it.spec.ts @@ -2,7 +2,7 @@ import path = require('path'); import { expect } from 'chai'; import { File } from '@stryker-mutator/api/core'; -import { testInjector } from '@stryker-mutator/test-helpers'; +import { assertions, testInjector } from '@stryker-mutator/test-helpers'; import { TSConfigPreprocessor } from '../../../src/sandbox/ts-config-preprocessor'; @@ -24,19 +24,26 @@ describe(TSConfigPreprocessor.name, () => { it('should ignore missing "extends"', async () => { files.push(tsconfigFile('tsconfig.json', { references: [{ path: './tsconfig.src.json' }] })); const output = await sut.preprocess(files); - expect(output).deep.eq(files); + assertions.expectTextFilesEqual(output, files); }); it('should ignore missing "references"', async () => { files.push(tsconfigFile('tsconfig.json', { extends: './tsconfig.settings.json' })); const output = await sut.preprocess(files); - expect(output).deep.eq(files); + assertions.expectTextFilesEqual(output, files); }); it('should rewrite "extends" if it falls outside of sandbox', async () => { files.push(tsconfigFile('tsconfig.json', { extends: '../tsconfig.settings.json' })); const output = await sut.preprocess(files); - expect(output).deep.eq([tsconfigFile('tsconfig.json', { extends: '../../../tsconfig.settings.json' })]); + assertions.expectTextFilesEqual(output, [tsconfigFile('tsconfig.json', { extends: '../../../tsconfig.settings.json' })]); + }); + + it('should not do anything when inPlace = true', async () => { + testInjector.options.inPlace = true; + files.push(tsconfigFile('tsconfig.json', { extends: '../tsconfig.settings.json' })); + const output = await sut.preprocess(files); + assertions.expectTextFilesEqual(output, files); }); it('should support comments and other settings', async () => { @@ -53,13 +60,15 @@ describe(TSConfigPreprocessor.name, () => { ) ); const output = await sut.preprocess(files); - expect(output).deep.eq([tsconfigFile('tsconfig.json', { extends: '../../../tsconfig.settings.json', compilerOptions: { target: 'es5' } })]); + assertions.expectTextFilesEqual(output, [ + tsconfigFile('tsconfig.json', { extends: '../../../tsconfig.settings.json', compilerOptions: { target: 'es5' } }), + ]); }); it('should rewrite "references" if it falls outside of sandbox', async () => { files.push(tsconfigFile('tsconfig.json', { references: [{ path: '../model' }] })); const output = await sut.preprocess(files); - expect(output).deep.eq([tsconfigFile('tsconfig.json', { references: [{ path: '../../../model/tsconfig.json' }] })]); + assertions.expectTextFilesEqual(output, [tsconfigFile('tsconfig.json', { references: [{ path: '../../../model/tsconfig.json' }] })]); }); it('should rewrite referenced tsconfig files that are also located in the sandbox', async () => { @@ -67,7 +76,7 @@ describe(TSConfigPreprocessor.name, () => { files.push(tsconfigFile('tsconfig.settings.json', { extends: '../../tsconfig.root-settings.json' })); files.push(tsconfigFile('src/tsconfig.json', { references: [{ path: '../../model' }] })); const output = await sut.preprocess(files); - expect(output).deep.eq([ + assertions.expectTextFilesEqual(output, [ tsconfigFile('tsconfig.json', { extends: './tsconfig.settings.json', references: [{ path: './src' }] }), tsconfigFile('tsconfig.settings.json', { extends: '../../../../tsconfig.root-settings.json' }), tsconfigFile('src/tsconfig.json', { references: [{ path: '../../../../model/tsconfig.json' }] }), From bb0821ec42cd3869ed0d305d232a14b16ecc4741 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 22 Jan 2021 09:28:31 +0100 Subject: [PATCH 10/21] Fix normalized path names --- .../utils/{file-utils.spec.ts => file-utils.it.spec.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename packages/core/test/integration/utils/{file-utils.spec.ts => file-utils.it.spec.ts} (98%) diff --git a/packages/core/test/integration/utils/file-utils.spec.ts b/packages/core/test/integration/utils/file-utils.it.spec.ts similarity index 98% rename from packages/core/test/integration/utils/file-utils.spec.ts rename to packages/core/test/integration/utils/file-utils.it.spec.ts index 504922cf21..020d7cd33b 100644 --- a/packages/core/test/integration/utils/file-utils.spec.ts +++ b/packages/core/test/integration/utils/file-utils.it.spec.ts @@ -86,7 +86,7 @@ describe('fileUtils', () => { Promise.all( matches.map(async (fileName) => { const content = await fsPromises.readFile(fileName); - return new File(fileName, content); + return new File(path.normalize(fileName), content); }) ) ); From acfae85d940c445caffc5ac331732a1cb4922613 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 22 Jan 2021 10:46:33 +0100 Subject: [PATCH 11/21] Support --inPlace from cli --- packages/core/src/stryker-cli.ts | 4 ++++ packages/core/test/unit/stryker-cli.spec.ts | 9 +-------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/core/src/stryker-cli.ts b/packages/core/src/stryker-cli.ts index 605f8ccfcb..5ae1ea6f92 100644 --- a/packages/core/src/stryker-cli.ts +++ b/packages/core/src/stryker-cli.ts @@ -139,6 +139,10 @@ export default class StrykerCli { `Send a full report (inc. source code and mutant results) or only the mutation score. Default: ${defaultValues.dashboard.reportType}`, deepOption(dashboard, 'reportType') ) + .option( + '--inPlace', + 'Enable Stryker to mutate your files in place and put back the originals after its done. Note: mutating your files in place is generally not needed for mutation testing.' + ) .option( '--tempDirName ', 'Set the name of the directory that is used by Stryker as a working directory. This directory will be cleaned after a successful run' diff --git a/packages/core/test/unit/stryker-cli.spec.ts b/packages/core/test/unit/stryker-cli.spec.ts index af7e300ed7..ade7c361f7 100644 --- a/packages/core/test/unit/stryker-cli.spec.ts +++ b/packages/core/test/unit/stryker-cli.spec.ts @@ -42,6 +42,7 @@ describe(StrykerCli.name, () => { [['--testRunner', 'foo-running'], { testRunner: 'foo-running' }], [['--testRunnerNodeArgs', '--inspect=1337 --gc'], { testRunnerNodeArgs: ['--inspect=1337', '--gc'] }], [['--coverageAnalysis', 'all'], { coverageAnalysis: 'all' }], + [['--inPlace'], { inPlace: true }], [['--concurrency', '5'], { concurrency: 5 }], [['--cleanTempDir', 'false'], { cleanTempDir: false }], [['-c', '6'], { concurrency: 6 }], @@ -100,14 +101,6 @@ describe(StrykerCli.name, () => { runMutationTestingStub.resolves(); actRun(args); expect(runMutationTestingStub).called; - const actualOptions: PartialStrykerOptions = runMutationTestingStub.getCall(0).args[0]; - for (const option in actualOptions) { - // Unfortunately, commander leaves all unspecified options as `undefined` on the object. - // This is not a problem for stryker, so let's clean them for this test. - if (actualOptions[option] === undefined) { - delete actualOptions[option]; - } - } expect(runMutationTestingStub).calledWith(expectedOptions); } }); From 7c3b3351b1828c3633e335d9b86613e9f536b799 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 22 Jan 2021 10:46:54 +0100 Subject: [PATCH 12/21] Remove unused variable --- e2e/test/in-place/verify/verify.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/test/in-place/verify/verify.ts b/e2e/test/in-place/verify/verify.ts index 35e6c7763d..2c86ff4e62 100644 --- a/e2e/test/in-place/verify/verify.ts +++ b/e2e/test/in-place/verify/verify.ts @@ -60,7 +60,7 @@ describe('in place', () => { }); // Act - const exit = await expect(onGoingStrykerRun).rejected; + await expect(onGoingStrykerRun).rejected; // Assert expect(await readAddJS()).eq(originalAddJSContent); From f1ee9b0edf7f018c3dac036ce8e879a711aa1152 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 22 Jan 2021 10:47:05 +0100 Subject: [PATCH 13/21] Document --inPlace --- docs/configuration.md | 13 +++++++++++++ packages/api/schema/stryker-core.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 4e1523c524..8893096a4f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -145,6 +145,19 @@ When using the config file you can provide an array with `string`s You can *ignore* files by adding an exclamation mark (`!`) at the start of an expression. +### `inPlace` [`boolean`] + +Default: `false`
+Command line: `--inPlace`
+Config file: `"inPlace": true`
+ +Determines whether or not Stryker should mutate your files in place. +Note: mutating your files in place is generally not needed for mutation testing, unless you have a dependency in your project that is really dependent on the file locations (like "app-root-path" for example). + +When `true`, Stryker will override your files, but it will keep a copy of the originals in the temp directory (using `tempDirName`) and it will place the originals back after it is done. + +When `false` (default) Stryker will work in the copy of your code inside the temp directory. + ### `logLevel` [`string`] Default: `info`
diff --git a/packages/api/schema/stryker-core.json b/packages/api/schema/stryker-core.json index 9ba70603f5..b90ae5a51e 100644 --- a/packages/api/schema/stryker-core.json +++ b/packages/api/schema/stryker-core.json @@ -296,7 +296,7 @@ }, "inPlace": { "type": "boolean", - "description": "Determines whether or not Stryker should mutate your files in-place. Note: mutating files in place is generally not needed, unless you have code in your project that is really dependent on the file locations.\n\nWhen true, Stryker will override your files, but it will keep a copy of the originals in a temp directory. It will place the originals back after it is done.\n\nWhen false (default) Stryker will work in a copy in a sandbox directory.", + "description": "Determines whether or not Stryker should mutate your files in place. Note: mutating your files in place is generally not needed for mutation testing, unless you have a dependency in your project that is really dependent on the file locations (like \"app-root-path\" for example).\n\nWhen `true`, Stryker will override your files, but it will keep a copy of the originals in the temp directory (using `tempDirName`) and it will place the originals back after it is done.\n\nWhen `false` (default) Stryker will work in the copy of your code inside the temp directory.", "default": false }, "logLevel": { From 679467539384ecc8bb3a076ce2cf931ebd4c5ec0 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 22 Jan 2021 10:47:16 +0100 Subject: [PATCH 14/21] Remove unused operator --- packages/core/src/process/3-dry-run-executor.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/core/src/process/3-dry-run-executor.ts b/packages/core/src/process/3-dry-run-executor.ts index d7e9ef5b44..e6f095ce47 100644 --- a/packages/core/src/process/3-dry-run-executor.ts +++ b/packages/core/src/process/3-dry-run-executor.ts @@ -16,7 +16,6 @@ import { ErrorDryRunResult, } from '@stryker-mutator/api/test-runner'; import { of } from 'rxjs'; -import { first } from 'rxjs/operators'; import { Checker } from '@stryker-mutator/api/check'; import { coreTokens } from '../di'; @@ -90,10 +89,7 @@ export class DryRunExecutor { .provideValue(coreTokens.testRunnerConcurrencyTokens, this.concurrencyTokenProvider.testRunnerToken$) .provideFactory(coreTokens.testRunnerPool, createTestRunnerPool); const testRunnerPool = testRunnerInjector.resolve(coreTokens.testRunnerPool); - const { dryRunResult, timing } = await testRunnerPool - .schedule(of(0), (testRunner) => this.timeDryRun(testRunner)) - .pipe(first()) - .toPromise(); + const { dryRunResult, timing } = await testRunnerPool.schedule(of(0), (testRunner) => this.timeDryRun(testRunner)).toPromise(); this.logInitialTestRunSucceeded(dryRunResult.tests, timing); if (!dryRunResult.tests.length) { From 218b344f83fafb5e42fdc51f8a390924ed7dc2a0 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 22 Jan 2021 12:37:35 +0100 Subject: [PATCH 15/21] test(e2e): add in place test for karma --- e2e/test/karma-mocha/package.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/e2e/test/karma-mocha/package.json b/e2e/test/karma-mocha/package.json index df972b3668..025364661e 100644 --- a/e2e/test/karma-mocha/package.json +++ b/e2e/test/karma-mocha/package.json @@ -6,8 +6,12 @@ "main": "index.js", "scripts": { "pretest": "rimraf \"reports\"", - "test": "stryker run stryker.conf.js", - "posttest": "mocha --require ../../tasks/ts-node-register.js verify/*.ts" + "test": "npm run test:not-in-place && npm run test:in-place", + "test:not-in-place": "stryker run stryker.conf.js", + "posttest:not-in-place": "npm run verify", + "test:in-place": "stryker run stryker.conf.js --inPlace", + "posttest:in-place": "npm run verify", + "verify": "mocha --require ../../tasks/ts-node-register.js verify/*.ts" }, "author": "", "license": "ISC" From ff2edc63966515a2e00bb4fa77d6398612d43100 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 22 Jan 2021 12:51:36 +0100 Subject: [PATCH 16/21] feat(in place): support in-place mode in the karma-runner --- ...t-hooks-middleware-21f23d35-a4c9-4b01-aeff-da9c99c3ffc0.ts | 1 + .../karma-runner/src/karma-plugins/test-hooks-middleware.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 packages/karma-runner/src/karma-plugins/test-hooks-middleware-21f23d35-a4c9-4b01-aeff-da9c99c3ffc0.ts diff --git a/packages/karma-runner/src/karma-plugins/test-hooks-middleware-21f23d35-a4c9-4b01-aeff-da9c99c3ffc0.ts b/packages/karma-runner/src/karma-plugins/test-hooks-middleware-21f23d35-a4c9-4b01-aeff-da9c99c3ffc0.ts new file mode 100644 index 0000000000..486f81e385 --- /dev/null +++ b/packages/karma-runner/src/karma-plugins/test-hooks-middleware-21f23d35-a4c9-4b01-aeff-da9c99c3ffc0.ts @@ -0,0 +1 @@ +// Empty, replaced at runtime diff --git a/packages/karma-runner/src/karma-plugins/test-hooks-middleware.ts b/packages/karma-runner/src/karma-plugins/test-hooks-middleware.ts index 88584289bb..75a375ab12 100644 --- a/packages/karma-runner/src/karma-plugins/test-hooks-middleware.ts +++ b/packages/karma-runner/src/karma-plugins/test-hooks-middleware.ts @@ -6,7 +6,7 @@ import { CoverageAnalysis, INSTRUMENTER_CONSTANTS } from '@stryker-mutator/api/c import { MutantRunOptions } from '@stryker-mutator/api/test-runner'; import { escapeRegExpLiteral } from '@stryker-mutator/util'; -export const TEST_HOOKS_FILE_NAME = __filename; +export const TEST_HOOKS_FILE_NAME = require.resolve('./test-hooks-middleware-21f23d35-a4c9-4b01-aeff-da9c99c3ffc0'); const SUPPORTED_FRAMEWORKS = Object.freeze(['mocha', 'jasmine'] as const); @@ -102,7 +102,7 @@ export default class TestHooksMiddleware { public handler: RequestHandler = (request, response, next) => { const pathName = url.parse(request.url).pathname; - if (pathName && path.normalize(pathName).endsWith(TEST_HOOKS_FILE_NAME)) { + if (pathName?.endsWith(path.basename(TEST_HOOKS_FILE_NAME))) { response.writeHead(200, { 'Cache-Control': 'no-cache', 'Content-Type': 'application/javascript', From 506251e1947500cb3276038c1f8c3a15f7100295 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 22 Jan 2021 12:54:27 +0100 Subject: [PATCH 17/21] Reset launch.json --- .vscode/launch.json | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index ee92f2bc48..701aa9df9b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,22 +4,6 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { - "name": "Launch Program", - "program": "${workspaceFolder}/packages/core/bin/stryker", - "request": "launch", - "args": ["run"], - "skipFiles": [ - "/**" - ], - "outFiles": [ - "${workspaceFolder}/packages/**/*.js", - "!**/node_modules/**" - ], - "type": "node", - "cwd": "${workspaceFolder}/e2e/test/mocha-javascript", - "outputCapture": "console" - }, { "type": "node", "request": "attach", From 381caefafb51cc897d266d84d2c22639fadca3c9 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 22 Jan 2021 12:57:43 +0100 Subject: [PATCH 18/21] Remove unused interface --- packages/core/src/unexpected-exit-handler.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/core/src/unexpected-exit-handler.ts b/packages/core/src/unexpected-exit-handler.ts index 875c2ea82e..f2242d730f 100644 --- a/packages/core/src/unexpected-exit-handler.ts +++ b/packages/core/src/unexpected-exit-handler.ts @@ -4,12 +4,7 @@ import { coreTokens } from './di'; export type ExitHandler = () => void; -export interface UnexpectedExitHandler { - registerHandler(handler: ExitHandler): void; -} - const signals = Object.freeze(['SIGABRT', 'SIGINT', 'SIGHUP', 'SIGTERM']); - export class UnexpectedExitHandler implements Disposable { private readonly unexpectedExitHandlers: ExitHandler[] = []; From 89013859ab8066dd841c48640bb0a11ef7d5c4a9 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 22 Jan 2021 12:41:00 +0100 Subject: [PATCH 19/21] feat(in place): support in place mode in karma-runner --- .../src/karma-plugins/test-hooks-middleware.ts | 8 ++++++++ .../test/unit/karma-plugins/test-hooks-middleware.spec.ts | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/karma-runner/src/karma-plugins/test-hooks-middleware.ts b/packages/karma-runner/src/karma-plugins/test-hooks-middleware.ts index 75a375ab12..392019ff80 100644 --- a/packages/karma-runner/src/karma-plugins/test-hooks-middleware.ts +++ b/packages/karma-runner/src/karma-plugins/test-hooks-middleware.ts @@ -7,6 +7,10 @@ import { MutantRunOptions } from '@stryker-mutator/api/test-runner'; import { escapeRegExpLiteral } from '@stryker-mutator/util'; export const TEST_HOOKS_FILE_NAME = require.resolve('./test-hooks-middleware-21f23d35-a4c9-4b01-aeff-da9c99c3ffc0'); +<<<<<<< HEAD +======= +const TEST_HOOKS_FILE_BASE_NAME = path.basename(TEST_HOOKS_FILE_NAME); +>>>>>>> fb4641c3 (feat(in place): support in place mode in karma-runner) const SUPPORTED_FRAMEWORKS = Object.freeze(['mocha', 'jasmine'] as const); @@ -102,7 +106,11 @@ export default class TestHooksMiddleware { public handler: RequestHandler = (request, response, next) => { const pathName = url.parse(request.url).pathname; +<<<<<<< HEAD if (pathName?.endsWith(path.basename(TEST_HOOKS_FILE_NAME))) { +======= + if (pathName?.endsWith(TEST_HOOKS_FILE_BASE_NAME)) { +>>>>>>> fb4641c3 (feat(in place): support in place mode in karma-runner) response.writeHead(200, { 'Cache-Control': 'no-cache', 'Content-Type': 'application/javascript', diff --git a/packages/karma-runner/test/unit/karma-plugins/test-hooks-middleware.spec.ts b/packages/karma-runner/test/unit/karma-plugins/test-hooks-middleware.spec.ts index 18e0f12136..ac20fea0e9 100644 --- a/packages/karma-runner/test/unit/karma-plugins/test-hooks-middleware.spec.ts +++ b/packages/karma-runner/test/unit/karma-plugins/test-hooks-middleware.spec.ts @@ -1,7 +1,8 @@ +import path = require('path'); + import { expect } from 'chai'; import { factory } from '@stryker-mutator/test-helpers'; import { Request, NextFunction, Response } from 'express'; - import sinon = require('sinon'); import TestHooksMiddleware, { TEST_HOOKS_FILE_NAME } from '../../../src/karma-plugins/test-hooks-middleware'; @@ -127,7 +128,7 @@ describe(TestHooksMiddleware.name, () => { it('should pass serve "currentTestHooks" when called with the correct url', () => { sut.currentTestHooks = 'foo test hooks'; - request.url = `/absolute${TEST_HOOKS_FILE_NAME}?foo=bar`; + request.url = `/absolute${path.basename(TEST_HOOKS_FILE_NAME)}?foo=bar`; sut.handler(request, response, next); expect(next).not.called; expect(response.writeHead).calledWith(200, { From e4a6c3631d5d13354d851660fa0ea7cae729156f Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 22 Jan 2021 13:01:01 +0100 Subject: [PATCH 20/21] Fix merge error --- .../src/karma-plugins/test-hooks-middleware.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/karma-runner/src/karma-plugins/test-hooks-middleware.ts b/packages/karma-runner/src/karma-plugins/test-hooks-middleware.ts index 392019ff80..f0d62e2088 100644 --- a/packages/karma-runner/src/karma-plugins/test-hooks-middleware.ts +++ b/packages/karma-runner/src/karma-plugins/test-hooks-middleware.ts @@ -7,10 +7,7 @@ import { MutantRunOptions } from '@stryker-mutator/api/test-runner'; import { escapeRegExpLiteral } from '@stryker-mutator/util'; export const TEST_HOOKS_FILE_NAME = require.resolve('./test-hooks-middleware-21f23d35-a4c9-4b01-aeff-da9c99c3ffc0'); -<<<<<<< HEAD -======= const TEST_HOOKS_FILE_BASE_NAME = path.basename(TEST_HOOKS_FILE_NAME); ->>>>>>> fb4641c3 (feat(in place): support in place mode in karma-runner) const SUPPORTED_FRAMEWORKS = Object.freeze(['mocha', 'jasmine'] as const); @@ -106,11 +103,7 @@ export default class TestHooksMiddleware { public handler: RequestHandler = (request, response, next) => { const pathName = url.parse(request.url).pathname; -<<<<<<< HEAD - if (pathName?.endsWith(path.basename(TEST_HOOKS_FILE_NAME))) { -======= if (pathName?.endsWith(TEST_HOOKS_FILE_BASE_NAME)) { ->>>>>>> fb4641c3 (feat(in place): support in place mode in karma-runner) response.writeHead(200, { 'Cache-Control': 'no-cache', 'Content-Type': 'application/javascript', From 31314af83b72ce7866a4ab73830e73d612ee4279 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 22 Jan 2021 13:04:28 +0100 Subject: [PATCH 21/21] Use relative file names in log messages --- packages/core/src/sandbox/sandbox.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/core/src/sandbox/sandbox.ts b/packages/core/src/sandbox/sandbox.ts index 2debf22d5d..8b3f901f4b 100644 --- a/packages/core/src/sandbox/sandbox.ts +++ b/packages/core/src/sandbox/sandbox.ts @@ -41,7 +41,10 @@ export class Sandbox implements Disposable { if (options.inPlace) { this.workingDirectory = process.cwd(); this.backupDirectory = temporaryDirectory.createRandomDirectory('backup'); - this.log.info('In place mode is enabled, Stryker will be overriding YOUR files. Find your backup at: %s', this.backupDirectory); + 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)); } else { this.workingDirectory = temporaryDirectory.createRandomDirectory('sandbox'); @@ -129,9 +132,9 @@ export class Sandbox implements Disposable { public dispose(unexpected = false): void { if (this.backupDirectory) { if (unexpected) { - console.error(`Detecting unexpected exit, recovering original files from ${this.backupDirectory}`); + console.error(`Detecting unexpected exit, recovering original files from ${path.relative(process.cwd(), this.backupDirectory)}`); } else { - this.log.info(`Resetting your original files from ${this.backupDirectory}.`); + this.log.info(`Resetting your original files from ${path.relative(process.cwd(), this.backupDirectory)}.`); } moveDirectoryRecursiveSync(this.backupDirectory, this.workingDirectory); }