-
Notifications
You must be signed in to change notification settings - Fork 250
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(instrumenter): switch case mutant placer (#2518)
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
Showing
6 changed files
with
235 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
44 changes: 44 additions & 0 deletions
44
packages/instrumenter/src/mutant-placers/switch-case-mutant-placer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
103 changes: 103 additions & 0 deletions
103
packages/instrumenter/test/unit/mutant-placers/switch-case-mutant-placer.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)`) | ||
); | ||
}); | ||
}); | ||
}); |
9 changes: 9 additions & 0 deletions
9
packages/instrumenter/testResources/instrumenter/switch-case.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
switch(foo){ | ||
case 'bar': | ||
console.log('bar'); | ||
break; | ||
case 'baz': | ||
console.log('baz'); | ||
break; | ||
|
||
} |
74 changes: 74 additions & 0 deletions
74
packages/instrumenter/testResources/instrumenter/switch-case.js.out.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
}" | ||
`; |