Skip to content

Commit

Permalink
fix(instrumenter): switch case mutant placer (#2518)
Browse files Browse the repository at this point in the history
Add a `MutantPlacer` for `SwitchCase` nodes. It places the mutants in the `consequence` part of the switch case. This can dramatically improve the performance of instrumenting large switch-case statements.

This:

```js
switch(foo){
  case 'bar':
    console.log('bar');
    break;
  case 'baz':
    console.log('baz');
    break;

}
```

Is now instrumented as:

```js
switch (foo) {
  case stryMutAct_9fa48(1) ? \\"\\" : (stryCov_9fa48(1), 'bar'):
    if (stryMutAct_9fa48(0)) {} else {
      stryCov_9fa48(0);
      console.log(stryMutAct_9fa48(2) ? \\"\\" : (stryCov_9fa48(2), 'bar'));
      break;
    }

  case stryMutAct_9fa48(4) ? \\"\\" : (stryCov_9fa48(4), 'baz'):
    if (stryMutAct_9fa48(3)) {} else {
      stryCov_9fa48(3);
      console.log(stryMutAct_9fa48(5) ? \\"\\" : (stryCov_9fa48(5), 'baz'));
      break;
    }
}
```

While previously the entire `SwitchCaseExpression` was duplicated for each empty switch case mutation.
  • Loading branch information
nicojs committed Oct 4, 2020
1 parent 3eef8aa commit a560711
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 1 deletion.
3 changes: 2 additions & 1 deletion packages/instrumenter/src/mutant-placers/index.ts
Expand Up @@ -8,8 +8,9 @@ import { Mutant } from '../mutant';
import { MutantPlacer } from './mutant-placer';
import { statementMutantPlacer } from './statement-mutant-placer';
import { expressionMutantPlacer } from './expression-mutant-placer';
import { switchCaseMutantPlacer } from './switch-case-mutant-placer';

export const MUTANT_PLACERS = Object.freeze([expressionMutantPlacer, statementMutantPlacer]);
export const MUTANT_PLACERS = Object.freeze([expressionMutantPlacer, statementMutantPlacer, switchCaseMutantPlacer]);

/**
* Represents a mutant placer, tries to place a mutant in the AST with corresponding mutation switch and mutant covering expression
Expand Down
@@ -0,0 +1,44 @@
import { types } from '@babel/core';

import { createMutatedAst, mutantTestExpression, mutationCoverageSequenceExpression } from '../util';

import { MutantPlacer } from './mutant-placer';

/**
* Places the mutants with consequent of a SwitchCase node. Uses an if-statement to do so.
* @example
* case 'foo':
* if (stryMutAct_9fa48(0)) {} else {
* stryCov_9fa48(0);
* console.log('bar');
* break;
* }
*/
const switchCaseMutantPlacer: MutantPlacer = (path, mutants): boolean => {
if (path.isSwitchCase()) {
// First transform the mutated ast before we start to apply mutants.
const appliedMutants = mutants.map((mutant) => {
const ast = createMutatedAst(path, mutant);
if (!types.isSwitchCase(ast)) {
throw new Error(`${switchCaseMutantPlacer.name} can only place SwitchCase syntax`);
}
return {
ast,
mutant,
};
});

const instrumentedConsequent = appliedMutants.reduce(
// Add if statements per mutant
(prev: types.Statement, { ast, mutant }) => types.ifStatement(mutantTestExpression(mutant.id), types.blockStatement(ast.consequent), prev),
types.blockStatement([types.expressionStatement(mutationCoverageSequenceExpression(mutants)), ...path.node.consequent])
);
path.replaceWith(types.switchCase(path.node.test, [instrumentedConsequent]));
return true;
}

return false;
};

// Export it after initializing so `fn.name` is properly set
export { switchCaseMutantPlacer };
Expand Up @@ -49,6 +49,9 @@ describe('instrumenter integration', () => {
it('should be able to instrument js files with a shebang in them', async () => {
await arrangeAndActAssert('shebang.js');
});
it('should be able to instrument switch case statements (using the switchCaseMutantPlacer)', async () => {
await arrangeAndActAssert('switch-case.js');
});

describe('type declarations', () => {
it('should not produce mutants for TS type definitions', async () => {
Expand Down
@@ -0,0 +1,103 @@
import { NodePath, types } from '@babel/core';
import generate from '@babel/generator';
import { normalizeWhitespaces } from '@stryker-mutator/util';
import { expect } from 'chai';

import { switchCaseMutantPlacer as sut } from '../../../src/mutant-placers/switch-case-mutant-placer';
import { createMutant } from '../../helpers/factories';
import { findNodePath, parseJS } from '../../helpers/syntax-test-helpers';

describe(sut.name, () => {
it('should have the correct name', () => {
expect(sut.name).eq('switchCaseMutantPlacer');
});

it('should not place mutants on non-switch-case nodes', () => {
[
findNodePath(parseJS('foo + bar'), (p) => p.isBinaryExpression()),
findNodePath(parseJS('switch(foo) { }'), (p) => p.isSwitchStatement()),
].forEach((node) => {
expect(sut(node, [])).false;
});
});

it('should only place SwitchCase nodes', () => {
const switchCase = findNodePath(parseJS('switch(foo) { case "bar": console.log("bar"); break; }'), (p) => p.isSwitchCase());
const mutant = createMutant({ original: switchCase.node, replacement: types.stringLiteral('foo') });
expect(() => sut(switchCase, [mutant])).throws('switchCaseMutantPlacer can only place SwitchCase syntax');
});

describe('given a SwitchCase node', () => {
let ast: types.File;
let switchCase: NodePath;

beforeEach(() => {
ast = parseJS('switch(foo) { case "bar": console.log("bar"); break; }');
switchCase = findNodePath(ast, (p) => p.isSwitchCase());
});

it('should place a mutant in the "consequent" part of a switch-case', () => {
// Arrange
const mutant = createMutant({ id: 42, original: switchCase.node, replacement: types.switchCase(types.stringLiteral('bar'), []) });

// Act
const actual = sut(switchCase, [mutant]);
const actualCode = normalizeWhitespaces(generate(ast).code);

// Assert
expect(actual).true;
expect(actualCode).contains(normalizeWhitespaces('switch (foo) { case "bar": if (stryMutAct_9fa48(42))'));
});

it('should place the original code as alternative (inside `else`)', () => {
// Arrange
const mutant = createMutant({ id: 42, original: switchCase.node, replacement: types.switchCase(types.stringLiteral('bar'), []) });

// Act
const actual = sut(switchCase, [mutant]);
const actualCode = normalizeWhitespaces(generate(ast).code);

// Assert
expect(actual).true;
expect(actualCode).matches(/else {.* console\.log\("bar"\); break; }/);
});

it('should add mutant coverage syntax', () => {
// Arrange
const mutant = createMutant({ id: 42, original: switchCase.node, replacement: types.switchCase(types.stringLiteral('bar'), []) });

// Act
const actual = sut(switchCase, [mutant]);
const actualCode = normalizeWhitespaces(generate(ast).code);

// Assert
expect(actual).true;
expect(actualCode).matches(/else\s*{\s*stryCov_9fa48\(42\)/);
});

it('should be able to place multiple mutants', () => {
// Arrange
const mutants = [
createMutant({ id: 42, original: switchCase.node, replacement: types.switchCase(types.stringLiteral('bar'), []) }),
createMutant({
id: 156,
original: switchCase.node,
replacement: types.switchCase(types.stringLiteral('bar'), [types.expressionStatement(types.callExpression(types.identifier('foo'), []))]),
}),
];

// Act
sut(switchCase, mutants);
const actualCode = normalizeWhitespaces(generate(ast).code);

// Assert
expect(actualCode).contains(
normalizeWhitespaces(`if (stryMutAct_9fa48(156)) {
foo();
} else if (stryMutAct_9fa48(42)) {}
else {
stryCov_9fa48(42, 156)`)
);
});
});
});
@@ -0,0 +1,9 @@
switch(foo){
case 'bar':
console.log('bar');
break;
case 'baz':
console.log('baz');
break;

}
@@ -0,0 +1,74 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`instrumenter integration should be able to instrument switch case statements (using the switchCaseMutantPlacer) 1`] = `
"function stryNS_9fa48() {
var g = new Function(\\"return this\\")();
var ns = g.__stryker__ || (g.__stryker__ = {});
if (ns.activeMutant === undefined && g.process && g.process.env && g.process.env.__STRYKER_ACTIVE_MUTANT__) {
ns.activeMutant = Number(g.process.env.__STRYKER_ACTIVE_MUTANT__);
}
function retrieveNS() {
return ns;
}
stryNS_9fa48 = retrieveNS;
return retrieveNS();
}
stryNS_9fa48();
function stryCov_9fa48() {
var ns = stryNS_9fa48();
var cov = ns.mutantCoverage || (ns.mutantCoverage = {
static: {},
perTest: {}
});
function cover() {
var c = cov.static;
if (ns.currentTestId) {
c = cov.perTest[ns.currentTestId] = cov.perTest[ns.currentTestId] || {};
}
var a = arguments;
for (var i = 0; i < a.length; i++) {
c[a[i]] = (c[a[i]] || 0) + 1;
}
}
stryCov_9fa48 = cover;
cover.apply(null, arguments);
}
function stryMutAct_9fa48(id) {
var ns = stryNS_9fa48();
function isActive(id) {
return ns.activeMutant === id;
}
stryMutAct_9fa48 = isActive;
return isActive(id);
}
switch (foo) {
case stryMutAct_9fa48(1) ? \\"\\" : (stryCov_9fa48(1), 'bar'):
if (stryMutAct_9fa48(0)) {} else {
stryCov_9fa48(0);
console.log(stryMutAct_9fa48(2) ? \\"\\" : (stryCov_9fa48(2), 'bar'));
break;
}
case stryMutAct_9fa48(4) ? \\"\\" : (stryCov_9fa48(4), 'baz'):
if (stryMutAct_9fa48(3)) {} else {
stryCov_9fa48(3);
console.log(stryMutAct_9fa48(5) ? \\"\\" : (stryCov_9fa48(5), 'baz'));
break;
}
}"
`;

0 comments on commit a560711

Please sign in to comment.