From a560711023990dca950700da18269e78249b5c49 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Sun, 4 Oct 2020 20:57:03 +0200 Subject: [PATCH] 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. --- .../instrumenter/src/mutant-placers/index.ts | 3 +- .../switch-case-mutant-placer.ts | 44 ++++++++ .../test/integration/instrumenter.it.spec.ts | 3 + .../switch-case-mutant-placer.spec.ts | 103 ++++++++++++++++++ .../testResources/instrumenter/switch-case.js | 9 ++ .../instrumenter/switch-case.js.out.snap | 74 +++++++++++++ 6 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 packages/instrumenter/src/mutant-placers/switch-case-mutant-placer.ts create mode 100644 packages/instrumenter/test/unit/mutant-placers/switch-case-mutant-placer.spec.ts create mode 100644 packages/instrumenter/testResources/instrumenter/switch-case.js create mode 100644 packages/instrumenter/testResources/instrumenter/switch-case.js.out.snap diff --git a/packages/instrumenter/src/mutant-placers/index.ts b/packages/instrumenter/src/mutant-placers/index.ts index a92f4a6564..a6145d18df 100644 --- a/packages/instrumenter/src/mutant-placers/index.ts +++ b/packages/instrumenter/src/mutant-placers/index.ts @@ -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 diff --git a/packages/instrumenter/src/mutant-placers/switch-case-mutant-placer.ts b/packages/instrumenter/src/mutant-placers/switch-case-mutant-placer.ts new file mode 100644 index 0000000000..3642add683 --- /dev/null +++ b/packages/instrumenter/src/mutant-placers/switch-case-mutant-placer.ts @@ -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 }; diff --git a/packages/instrumenter/test/integration/instrumenter.it.spec.ts b/packages/instrumenter/test/integration/instrumenter.it.spec.ts index 95125954dc..52ad5d1d05 100644 --- a/packages/instrumenter/test/integration/instrumenter.it.spec.ts +++ b/packages/instrumenter/test/integration/instrumenter.it.spec.ts @@ -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 () => { diff --git a/packages/instrumenter/test/unit/mutant-placers/switch-case-mutant-placer.spec.ts b/packages/instrumenter/test/unit/mutant-placers/switch-case-mutant-placer.spec.ts new file mode 100644 index 0000000000..013d88ef60 --- /dev/null +++ b/packages/instrumenter/test/unit/mutant-placers/switch-case-mutant-placer.spec.ts @@ -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)`) + ); + }); + }); +}); diff --git a/packages/instrumenter/testResources/instrumenter/switch-case.js b/packages/instrumenter/testResources/instrumenter/switch-case.js new file mode 100644 index 0000000000..ea7418ea69 --- /dev/null +++ b/packages/instrumenter/testResources/instrumenter/switch-case.js @@ -0,0 +1,9 @@ +switch(foo){ + case 'bar': + console.log('bar'); + break; + case 'baz': + console.log('baz'); + break; + +} diff --git a/packages/instrumenter/testResources/instrumenter/switch-case.js.out.snap b/packages/instrumenter/testResources/instrumenter/switch-case.js.out.snap new file mode 100644 index 0000000000..4fa31ea6dc --- /dev/null +++ b/packages/instrumenter/testResources/instrumenter/switch-case.js.out.snap @@ -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; + } + +}" +`;