Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(in place): support in-place mutation #2706

Merged
merged 23 commits into from Jan 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
72479b2
feat(in place): support in-place mutation
nicojs Jan 21, 2021
7f338e3
revert accidental change
nicojs Jan 21, 2021
18afb96
Use stryker-tmp dir
nicojs Jan 21, 2021
6599842
refactor: provide stryker register via di
nicojs Jan 21, 2021
a340169
Add unit tests for unexpected-exit-handler
nicojs Jan 21, 2021
13f1822
Add unit tests for sandbox dir
nicojs Jan 21, 2021
77ed18c
Add integration tests for `moveDirectoryRecursiveSync`
nicojs Jan 22, 2021
9d4bf46
Remove mension of 'sandbox' in message
nicojs Jan 22, 2021
62334db
Support `--inPlace` in the ts-config-preprossor
nicojs Jan 22, 2021
bb0821e
Fix normalized path names
nicojs Jan 22, 2021
acfae85
Support --inPlace from cli
nicojs Jan 22, 2021
7c3b335
Remove unused variable
nicojs Jan 22, 2021
f1ee9b0
Document --inPlace
nicojs Jan 22, 2021
6794675
Remove unused operator
nicojs Jan 22, 2021
218b344
test(e2e): add in place test for karma
nicojs Jan 22, 2021
ff2edc6
feat(in place): support in-place mode in the karma-runner
nicojs Jan 22, 2021
41e3149
Merge branch 'feat/in-place' of github.com:stryker-mutator/stryker in…
nicojs Jan 22, 2021
506251e
Reset launch.json
nicojs Jan 22, 2021
381caef
Remove unused interface
nicojs Jan 22, 2021
8901385
feat(in place): support in place mode in karma-runner
nicojs Jan 22, 2021
dad29af
Merge branch 'feat/in-place' of github.com:stryker-mutator/stryker in…
nicojs Jan 22, 2021
e4a6c36
Fix merge error
nicojs Jan 22, 2021
31314af
Use relative file names in log messages
nicojs Jan 22, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
65 changes: 0 additions & 65 deletions .vscode/launch.json
Expand Up @@ -4,71 +4,6 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${file}",
"skipFiles": [
"<node_internals>/**"
]
},
{
"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"
],
"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"
},
{
"type": "node",
"request": "attach",
Expand Down
13 changes: 13 additions & 0 deletions docs/configuration.md
Expand Up @@ -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`<br />
Command line: `--inPlace`<br />
Config file: `"inPlace": true`<br />

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`<br />
Expand Down
4 changes: 4 additions & 0 deletions e2e/test/in-place/.mocharc.jsonc
@@ -0,0 +1,4 @@
{
"require": "./test/helpers/testSetup.js",
"spec": ["test/unit/*.js"]
}
5 changes: 5 additions & 0 deletions e2e/test/in-place/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions 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"
}
24 changes: 24 additions & 0 deletions 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;
};
15 changes: 15 additions & 0 deletions 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
}
5 changes: 5 additions & 0 deletions e2e/test/in-place/test/helpers/testSetup.js
@@ -0,0 +1,5 @@
exports.mochaHooks = {
beforeAll() {
global.expect = require('chai').expect;
}
}
52 changes: 52 additions & 0 deletions e2e/test/in-place/test/unit/Add.spec.js
@@ -0,0 +1,52 @@
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;
});
});
13 changes: 13 additions & 0 deletions e2e/test/in-place/test/unit/Wait.spec.js
@@ -0,0 +1,13 @@
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));
}
69 changes: 69 additions & 0 deletions 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<string> {
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
await expect(onGoingStrykerRun).rejected;

// Assert
expect(await readAddJS()).eq(originalAddJSContent);
expect(addJSMutatedContent).not.eq(originalAddJSContent);
});
});
8 changes: 6 additions & 2 deletions e2e/test/karma-mocha/package.json
Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions packages/api/schema/stryker-core.json
Expand Up @@ -294,6 +294,11 @@
"type": "string"
}
},
"inPlace": {
"type": "boolean",
"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": {
"description": "Set the log level that Stryker uses to write to the console.",
"$ref": "#/definitions/logLevel",
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/di/build-main-injector.ts
Expand Up @@ -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 { UnexpectedExitHandler } from '../unexpected-exit-handler';

import { pluginResolverFactory } from './factory-methods';

Expand All @@ -21,11 +22,13 @@ export interface MainContext extends PluginContext {
[coreTokens.timer]: I<Timer>;
[coreTokens.temporaryDirectory]: I<TemporaryDirectory>;
[coreTokens.execa]: typeof execa;
[coreTokens.process]: NodeJS.Process;
[coreTokens.unexpectedExitRegistry]: I<UnexpectedExitHandler>;
}

type PluginResolverProvider = Injector<PluginContext>;
export type CliOptionsProvider = Injector<Pick<MainContext, 'logger' | 'getLogger'> & { [coreTokens.cliOptions]: PartialStrykerOptions }>;

export type CliOptionsProvider = Injector<Pick<MainContext, 'logger' | 'getLogger'> & { [coreTokens.cliOptions]: PartialStrykerOptions }>;
buildMainInjector.inject = tokens(commonTokens.injector);
export function buildMainInjector(injector: CliOptionsProvider): Injector<MainContext> {
const pluginResolverProvider = createPluginResolverProvider(injector);
Expand All @@ -35,7 +38,9 @@ export function buildMainInjector(injector: CliOptionsProvider): Injector<MainCo
.provideClass(coreTokens.reporter, BroadcastReporter)
.provideClass(coreTokens.temporaryDirectory, TemporaryDirectory)
.provideClass(coreTokens.timer, Timer)
.provideValue(coreTokens.execa, execa);
.provideValue(coreTokens.execa, execa)
.provideValue(coreTokens.process, process)
.provideClass(coreTokens.unexpectedExitRegistry, UnexpectedExitHandler);
}

export function createPluginResolverProvider(parent: CliOptionsProvider): PluginResolverProvider {
Expand Down
6 changes: 2 additions & 4 deletions packages/core/src/di/core-tokens.ts
Expand Up @@ -5,21 +5,19 @@ 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 process = 'process';
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';
Expand Down