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

fix(instrumenter): switch case mutant placer #2518

Merged
merged 2 commits into from
Oct 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/instrumenter/src/mutant-placers/index.ts
Original file line number Diff line number Diff line change
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
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 };
Original file line number Diff line number Diff line change
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
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)`)
);
});
});
});
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;

}
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;
}

}"
`;