Skip to content

Commit

Permalink
feat(javascript-mutator): allow to override babel plugins (#1764)
Browse files Browse the repository at this point in the history
Allow to override babel plugins when mutating code. For example:

```ts
// stryker.conf.js
module.exports = function(config) {
  config.set({
    mutator: {
       name: 'javascript',
       plugins: ['optionalChaining'] 
    }
  });
}
```

Please see https://github.com/stryker-mutator/stryker/tree/master/packages/javascript-mutator#config for more details.
  • Loading branch information
Bartosz Leoniak authored and nicojs committed Nov 5, 2019
1 parent ca56200 commit ddb3d60
Show file tree
Hide file tree
Showing 33 changed files with 1,224 additions and 793 deletions.
1,587 changes: 930 additions & 657 deletions e2e/package-lock.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
"version": "0.0.0",
"private": true,
"devDependencies": {
"@babel/cli": "^7.6.4",
"@babel/core": "^7.2.2",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-pipeline-operator": "^7.5.0",
"@babel/preset-env": "^7.3.1",
"@types/node": "^10.12.18",
"@types/semver": "^5.5.0",
"chai": "~4.1.2",
"chai-as-promised": "~7.1.1",
"cross-env": "~5.2.0",
"grunt": "~1.0.3",
"grunt": "^1.0.4",
"jasmine-core": "~3.1.0",
"karma": "^4.0.0",
"karma": "^4.4.1",
"karma-chai": "~0.1.0",
"karma-chrome-launcher": "~2.2.0",
"karma-jasmine": "~1.1.2",
Expand Down
5 changes: 2 additions & 3 deletions e2e/test/babel-transpiling/.babelrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"presets": [
"@babel/env"
]
"presets": ["@babel/env"],
"plugins": [["@babel/plugin-proposal-pipeline-operator", { "proposal": "minimal" }]]
}
4 changes: 2 additions & 2 deletions e2e/test/babel-transpiling/package-lock.json

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

3 changes: 2 additions & 1 deletion e2e/test/babel-transpiling/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@
},
"keywords": [],
"author": "",
"license": "ISC"
"license": "ISC",
"devDependencies": {}
}
11 changes: 9 additions & 2 deletions e2e/test/babel-transpiling/src/Casino.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
var Bank = require('./Bank').default;
// We use a require statement to see if stryker will work if users use require statements
var Bank = require('./Bank').default;

// Use a pipeline operator to test the `mutator.plugins` option
function capitalize (str) {
return str[0].toUpperCase() + str.substring(1);
}
let result = "hello"
|> capitalize;

export default class Casino extends Bank {
constructor(chips, money) {
Expand All @@ -26,4 +33,4 @@ export default class Casino extends Bank {
sellChips(user, amount) {
this.buyChips(user, -amount);
}
}
}
5 changes: 4 additions & 1 deletion e2e/test/babel-transpiling/stryker.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ module.exports = function (config) {
testFramework: 'mocha',
testRunner: 'mocha',
coverageAnalysis: 'off',
mutator: 'javascript',
mutator: {
name: 'javascript',
plugins: [['pipelineOperator', { proposal: 'minimal' }]]
},
transpilers: [
'babel'
],
Expand Down
18 changes: 9 additions & 9 deletions e2e/test/babel-transpiling/verify/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ describe('Verify stryker has ran correctly', () => {
await expectMetricsResult({
metrics: produceMetrics({
killed: 24,
mutationScore: 58.54,
mutationScoreBasedOnCoveredCode: 58.54,
runtimeErrors: 1,
survived: 17,
totalCovered: 41,
mutationScore: 55.81,
mutationScoreBasedOnCoveredCode: 55.81,
runtimeErrors: 2,
survived: 19,
totalCovered: 43,
totalDetected: 24,
totalInvalid: 1,
totalMutants: 42,
totalUndetected: 17,
totalValid: 41
totalInvalid: 2,
totalMutants: 45,
totalUndetected: 19,
totalValid: 43
})
});
});
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/core/MutatorDescriptor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
interface MutatorDescriptor {
name: string;
excludedMutations: string[];
plugins: string[] | null;
}

export default MutatorDescriptor;
2 changes: 1 addition & 1 deletion packages/api/src/core/StrykerOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ interface StrykerOptions {
* * The `excludedMutations` property is mandatory and contains the names of the specific mutation types to exclude from testing.
* * The values must match the given names of the mutations. For example: 'BinaryExpression', 'BooleanSubstitution', etc.
*/
mutator: string | MutatorDescriptor;
mutator: string | Partial<MutatorDescriptor>;

/**
* The names of the transpilers to use (in order). Default: [].
Expand Down
3 changes: 2 additions & 1 deletion packages/api/src/plugin/Contexts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { StrykerOptions } from '../../core';
import { MutatorDescriptor, StrykerOptions } from '../../core';
import { Logger, LoggerFactoryMethod } from '../../logging';
import { PluginKind } from './PluginKind';
import { PluginResolver } from './Plugins';
Expand All @@ -19,6 +19,7 @@ export interface BaseContext {
*/
export interface OptionsContext extends BaseContext {
[commonTokens.options]: StrykerOptions;
[commonTokens.mutatorDescriptor]: MutatorDescriptor;
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/plugin/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const commonTokens = Object.freeze({
getLogger: stringLiteral('getLogger'),
injector,
logger: stringLiteral('logger'),
mutatorDescriptor: stringLiteral('mutatorDescriptor'),
options: stringLiteral('options'),
pluginResolver: stringLiteral('pluginResolver'),
produceSourceMaps: stringLiteral('produceSourceMaps'),
Expand Down
5 changes: 3 additions & 2 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ This is optional, as you can choose to not mutate any files at all and perform a
### `mutator` [`object` | `string`]
Default: `javascript`
Command line: `--mutator javascript`
Config file: `mutator: 'javascript'` or `mutator: { name: 'javascript', excludedMutations: ['BooleanSubstitution', 'StringLiteral'] }`
Config file: `mutator: 'javascript'` or `mutator: { name: 'javascript', plugins: ['classProperties', 'optionalChaining'], excludedMutations: ['BooleanSubstitution', 'StringLiteral'] }`

With `mutator` you configure which mutator plugin you want to use, and optionally, which mutation types to exclude from the test run.
The mutator plugin name defaults to `javascript` if not specified. Note: this requires you to have the `@stryker-mutator/javascript-mutator` plugin installed. The list of excluded mutation types defaults to an empty array, meaning all mutation types will be included in the test.
Expand All @@ -227,8 +227,9 @@ The full list of mutation types varies slightly between mutators (for example, t
When using the command line, only the mutator name as a string may be provided.
When using the config file, you can provide either a string representing the mutator name, or a `MutatorDescriptor` object, like so:

* `MutatorDescriptor` object: `{ name: 'name', excludedMutations: ['mutationType1', 'mutationType2', ...] }`:
* `MutatorDescriptor` object: `{ name: 'name', plugins: ['classProperties', 'optionalChaining'], excludedMutations: ['mutationType1', 'mutationType2', ...] }`:
* The `name` property is mandatory and contains the name of the mutator plugin to use.
* The `plugins` property is optional and allows you to specify syntax plugins. Please see the README of your mutator to see which plugins are supported.
* The `excludedMutations` property is mandatory and contains the types of mutations to exclude from the test run.

<a name="plugins"></a>
Expand Down
17 changes: 16 additions & 1 deletion packages/core/src/config/ConfigValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ export default class ConfigValidator {
if (typeof mutator === 'object') {
const mutatorDescriptor = mutator;
this.validateIsString('mutator.name', mutatorDescriptor.name);
this.validateIsStringArray('mutator.excludedMutations', mutatorDescriptor.excludedMutations);
this.validateIsOptionalStringArray('mutator.excludedMutations', mutatorDescriptor.excludedMutations);
this.validateIsOptionalArray('mutator.plugins', mutatorDescriptor.plugins);
} else if (typeof mutator !== 'string') {
this.invalidate(`Value "${mutator}" is invalid for \`mutator\`. Expected either a string or an object`);
}
Expand Down Expand Up @@ -152,6 +153,20 @@ export default class ConfigValidator {
}
}

private validateIsArray(fieldName: keyof Config, value: unknown[]) {
if (!Array.isArray(value)) {
this.invalidate(`Value "${value}" is invalid for \`${fieldName}\`. Expected an array`);
}
}

private validateIsOptionalStringArray(fieldName: keyof Config, value: string[] | undefined) {
value === undefined || this.validateIsStringArray(fieldName, value);
}

private validateIsOptionalArray(fieldName: keyof Config, value: unknown[] | undefined | null) {
value === undefined || value === null || this.validateIsArray(fieldName, value);
}

private invalidate(message: string) {
this.log.fatal(message);
this.isValid = false;
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/di/buildChildProcessInjector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@ import { commonTokens, Injector, OptionsContext, Scope, tokens } from '@stryker-
import { getLogger } from 'log4js';
import { rootInjector } from 'typed-inject';
import { coreTokens } from '.';
import { loggerFactory, pluginResolverFactory } from './factoryMethods';
import { loggerFactory, mutatorDescriptorFactory, pluginResolverFactory } from './factoryMethods';

export function buildChildProcessInjector(options: StrykerOptions): Injector<OptionsContext> {
return rootInjector
.provideValue(commonTokens.options, options)
.provideValue(commonTokens.getLogger, getLogger)
.provideFactory(commonTokens.logger, loggerFactory, Scope.Transient)
.provideFactory(coreTokens.pluginDescriptors, pluginDescriptorsFactory)
.provideFactory(commonTokens.pluginResolver, pluginResolverFactory);
.provideFactory(commonTokens.pluginResolver, pluginResolverFactory)
.provideFactory(commonTokens.mutatorDescriptor, mutatorDescriptorFactory);
}

function pluginDescriptorsFactory(options: StrykerOptions): readonly string[] {
return options.plugins;
}

pluginDescriptorsFactory.inject = tokens(commonTokens.options);
3 changes: 2 additions & 1 deletion packages/core/src/di/buildMainInjector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import ConfigReader from '../config/ConfigReader';
import BroadcastReporter from '../reporters/BroadcastReporter';
import { TemporaryDirectory } from '../utils/TemporaryDirectory';
import Timer from '../utils/Timer';
import { loggerFactory, optionsFactory, pluginResolverFactory, testFrameworkFactory } from './factoryMethods';
import { loggerFactory, mutatorDescriptorFactory, optionsFactory, pluginResolverFactory, testFrameworkFactory } from './factoryMethods';

export interface MainContext extends OptionsContext {
[coreTokens.reporter]: Required<Reporter>;
Expand All @@ -36,6 +36,7 @@ export function buildMainInjector(cliOptions: Partial<StrykerOptions>): Injector
.provideFactory(coreTokens.pluginCreatorConfigEditor, PluginCreator.createFactory(PluginKind.ConfigEditor))
.provideClass(coreTokens.configEditorApplier, ConfigEditorApplier)
.provideFactory(commonTokens.options, optionsFactory)
.provideFactory(commonTokens.mutatorDescriptor, mutatorDescriptorFactory)
.provideFactory(coreTokens.pluginCreatorReporter, PluginCreator.createFactory(PluginKind.Reporter))
.provideFactory(coreTokens.pluginCreatorTestFramework, PluginCreator.createFactory(PluginKind.TestFramework))
.provideFactory(coreTokens.pluginCreatorMutator, PluginCreator.createFactory(PluginKind.Mutator))
Expand Down
22 changes: 21 additions & 1 deletion packages/core/src/di/factoryMethods.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Config } from '@stryker-mutator/api/config';
import { StrykerOptions } from '@stryker-mutator/api/core';
import { MutatorDescriptor, StrykerOptions } from '@stryker-mutator/api/core';
import { Logger, LoggerFactoryMethod } from '@stryker-mutator/api/logging';
import { commonTokens, Injector, OptionsContext, PluginKind, PluginResolver, tokens } from '@stryker-mutator/api/plugin';
import { coreTokens, PluginCreator, PluginLoader } from '.';
Expand Down Expand Up @@ -36,3 +36,23 @@ optionsFactory.inject = tokens<[typeof coreTokens.configReadFromConfigFile, type
coreTokens.configReadFromConfigFile,
coreTokens.configEditorApplier
);

export function mutatorDescriptorFactory(options: StrykerOptions): MutatorDescriptor {
const defaults: MutatorDescriptor = {
plugins: null,
name: 'javascript',
excludedMutations: []
};
if (typeof options.mutator === 'string') {
return {
...defaults,
name: options.mutator
};
}

return {
...defaults,
...options.mutator
};
}
mutatorDescriptorFactory.inject = tokens(commonTokens.options);
15 changes: 7 additions & 8 deletions packages/core/src/mutants/MutatorFacade.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
import { File, MutatorDescriptor, StrykerOptions } from '@stryker-mutator/api/core';
import { File, MutatorDescriptor } from '@stryker-mutator/api/core';
import { Logger } from '@stryker-mutator/api/logging';
import { Mutant, Mutator } from '@stryker-mutator/api/mutant';
import { commonTokens, PluginKind, tokens } from '@stryker-mutator/api/plugin';
import { coreTokens, PluginCreator } from '../di';

export class MutatorFacade implements Mutator {
public static inject = tokens(commonTokens.options, coreTokens.pluginCreatorMutator, commonTokens.logger);
public static inject = tokens(commonTokens.mutatorDescriptor, coreTokens.pluginCreatorMutator, commonTokens.logger);
constructor(
private readonly options: StrykerOptions,
private readonly mutatorDescriptor: MutatorDescriptor,
private readonly pluginCreator: PluginCreator<PluginKind.Mutator>,
private readonly log: Logger
) {}

public mutate(inputFiles: readonly File[]): readonly Mutant[] {
const allMutants = this.pluginCreator.create(this.getMutatorName(this.options.mutator)).mutate(inputFiles);
const allMutants = this.pluginCreator.create(this.getMutatorName(this.mutatorDescriptor.name)).mutate(inputFiles);
const includedMutants = this.removeExcludedMutants(allMutants);
this.logMutantCount(includedMutants.length, allMutants.length);
return includedMutants;
}

private removeExcludedMutants(mutants: readonly Mutant[]): readonly Mutant[] {
if (typeof this.options.mutator === 'string') {
return mutants;
if (this.mutatorDescriptor.excludedMutations.length) {
return mutants.filter(mutant => !this.mutatorDescriptor.excludedMutations.includes(mutant.mutatorName));
} else {
const mutatorDescriptor = this.options.mutator;
return mutants.filter(mutant => !mutatorDescriptor.excludedMutations.includes(mutant.mutatorName));
return mutants;
}
}

Expand Down
27 changes: 25 additions & 2 deletions packages/core/test/unit/config/ConfigValidator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,24 +114,47 @@ describe('ConfigValidator', () => {
});

describe('as an object', () => {
it('should be valid with string mutator name and string array excluded mutations', () => {
it('should be valid with all options', () => {
breakConfig('mutator', {
excludedMutations: ['BooleanSubstitution'],
name: 'javascript',
plugins: ['objectRestSpread', ['decorators', { decoratorsBeforeExport: true }]]
});
sut.validate();
expect(testInjector.logger.fatal).not.called;
});

it('should be valid with minimal options', () => {
breakConfig('mutator', {
name: 'javascript'
});
sut.validate();
expect(testInjector.logger.fatal).not.called;
});

it('should be invalid without name', () => {
breakConfig('mutator', {});
actValidationError();
expect(testInjector.logger.fatal).calledWith('Value "undefined" is invalid for `mutator.name`. Expected a string');
});

it('should be invalid with non-string mutator name', () => {
breakConfig('mutator', {
excludedMutations: [],
name: 0
});
actValidationError();
expect(testInjector.logger.fatal).calledWith('Value "0" is invalid for `mutator.name`. Expected a string');
});

it('should be invalid with non array plugins', () => {
breakConfig('mutator', {
name: 'javascript',
plugins: 'optionalChaining'
});
actValidationError();
expect(testInjector.logger.fatal).calledWith('Value "optionalChaining" is invalid for `mutator.plugins`. Expected an array');
});

it('should be invalid with non-array excluded mutations', () => {
breakConfig('mutator', {
excludedMutations: 'BooleanSubstitution',
Expand Down
11 changes: 11 additions & 0 deletions packages/core/test/unit/di/buildMainInjector.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { buildMainInjector } from '../../../src/di/buildMainInjector';
import * as broadcastReporterModule from '../../../src/reporters/BroadcastReporter';
import TestFrameworkOrchestrator, * as testFrameworkOrchestratorModule from '../../../src/TestFrameworkOrchestrator';
import currentLogMock from '../../helpers/logMock';
import { MutatorDescriptor } from '@stryker-mutator/api/core';

describe(buildMainInjector.name, () => {
let testFrameworkOrchestratorMock: sinon.SinonStubbedInstance<TestFrameworkOrchestrator>;
Expand Down Expand Up @@ -86,6 +87,16 @@ describe(buildMainInjector.name, () => {
});
});

it('should supply mutatorDescriptor', () => {
const expected: MutatorDescriptor = {
name: 'javascript',
plugins: null,
excludedMutations: []
};
const mutatorDescriptor = buildMainInjector({}).resolve(commonTokens.mutatorDescriptor);
expect(mutatorDescriptor).deep.eq(expected);
});

it('should be able to supply the test framework', () => {
const actualTestFramework = buildMainInjector({}).resolve(di.coreTokens.testFramework);
expect(testFrameworkMock).eq(actualTestFramework);
Expand Down
Loading

0 comments on commit ddb3d60

Please sign in to comment.