Skip to content

Commit

Permalink
fix(instrumenter): place mutants in if statements
Browse files Browse the repository at this point in the history
Place mutants in a statement with a `if` statement instead of a `switch` statement.

Fixes #2469
  • Loading branch information
nicojs committed Sep 16, 2020
1 parent 632f030 commit 848eb51
Show file tree
Hide file tree
Showing 10 changed files with 246 additions and 328 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function isValidParent(child: NodePath<types.Expression>) {
/**
* Places the mutants with a conditional expression: `global.activeMutant === 1? mutatedCode : regularCode`;
*/
const conditionalExpressionMutantPlacer: MutantPlacer = (path: NodePath, mutants: Mutant[]): boolean => {
const expressionMutantPlacer: MutantPlacer = (path: NodePath, mutants: Mutant[]): boolean => {
if (path.isExpression() && isValidParent(path)) {
// First calculated the mutated ast before we start to apply mutants.
const appliedMutants = mutants.map((mutant) => ({
Expand All @@ -75,4 +75,4 @@ const conditionalExpressionMutantPlacer: MutantPlacer = (path: NodePath, mutants
};

// Export it after initializing so `fn.name` is properly set
export { conditionalExpressionMutantPlacer };
export { expressionMutantPlacer };
6 changes: 3 additions & 3 deletions packages/instrumenter/src/mutant-placers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { NodePath } from '@babel/core';
import { Mutant } from '../mutant';

import { MutantPlacer } from './mutant-placer';
import { switchCaseMutantPlacer } from './switch-case-mutant-placer';
import { conditionalExpressionMutantPlacer } from './conditional-expression-mutant-placer';
import { statementMutantPlacer } from './statement-mutant-placer';
import { expressionMutantPlacer } from './expression-mutant-placer';

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

/**
* 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,58 @@
import { types } from '@babel/core';

import { memberExpressionChain, createMutatedAst, mutationCoverageSequenceExpression, ID } from '../util/syntax-helpers';

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

/**
* Mutant placer that places mutants in statements that allow it.
* It uses an `if` statement to do so
*/
const statementMutantPlacer: MutantPlacer = (path, mutants) => {
if (path.isStatement()) {
// First transform the mutated ast before we start to apply mutants.
const appliedMutants = mutants.map((mutant) => ({
mutant,
ast: createMutatedAst(path, mutant),
}));

// path.replaceWith(
// types.blockStatement([
// types.switchStatement(memberExpressionChain(ID.GLOBAL, ID.ACTIVE_MUTANT), [
// ...appliedMutants.map(({ ast, mutant }) => types.switchCase(types.numericLiteral(mutant.id), [ast, types.breakStatement()])),
// types.switchCase(null, [
// // Add mutation covering statement
// types.expressionStatement(mutationCoverageSequenceExpression(mutants)),
// path.node,
// types.breakStatement(),
// ]),
// ]),
// ])
// );

const instrumentedAst = appliedMutants.reduce(
// Add if statements per mutant
(prev: types.Statement, { ast, mutant }) =>
types.ifStatement(
types.binaryExpression('===', memberExpressionChain(ID.GLOBAL, ID.ACTIVE_MUTANT), types.numericLiteral(mutant.id)),
types.blockStatement([ast]),
prev
),
path.isBlockStatement()
? types.blockStatement([types.expressionStatement(mutationCoverageSequenceExpression(mutants)), ...path.node.body])
: types.blockStatement([types.expressionStatement(mutationCoverageSequenceExpression(mutants)), path.node])
);
if (path.isBlockStatement()) {
path.replaceWith(types.blockStatement([instrumentedAst]));
} else {
path.replaceWith(instrumentedAst);
}

return true;
} else {
return false;
}
};

// Export it after initializing so `fn.name` is properly set
export { statementMutantPlacer };

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ import { types, NodePath } from '@babel/core';
import { normalizeWhitespaces } from '@stryker-mutator/util';
import generate from '@babel/generator';

import { conditionalExpressionMutantPlacer } from '../../../src/mutant-placers/conditional-expression-mutant-placer';
import { expressionMutantPlacer } from '../../../src/mutant-placers/expression-mutant-placer';
import { findNodePath, parseJS } from '../../helpers/syntax-test-helpers';
import { Mutant } from '../../../src/mutant';
import { createMutant } from '../../helpers/factories';

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

it('should not place when the parent is tagged template expression', () => {
// A templateLiteral is considered an expression, while it is not save to place a mutant there!
const templateLiteral = findNodePath(parseJS('html`<p></p>`'), (p) => p.isTemplateLiteral());
expect(conditionalExpressionMutantPlacer(templateLiteral, [])).false;
expect(expressionMutantPlacer(templateLiteral, [])).false;
});

function arrangeSingleMutant() {
Expand All @@ -37,7 +37,7 @@ describe(conditionalExpressionMutantPlacer.name, () => {
const { binaryExpression, mutant, ast } = arrangeSingleMutant();

// Act
const actual = conditionalExpressionMutantPlacer(binaryExpression, [mutant]);
const actual = expressionMutantPlacer(binaryExpression, [mutant]);
const actualCode = normalizeWhitespaces(generate(ast).code);

// Assert
Expand All @@ -47,15 +47,15 @@ describe(conditionalExpressionMutantPlacer.name, () => {

it('should place the original code as the alternative', () => {
const { binaryExpression, mutant, ast } = arrangeSingleMutant();
conditionalExpressionMutantPlacer(binaryExpression, [mutant]);
expressionMutantPlacer(binaryExpression, [mutant]);
const actualAlternative = findNodePath<types.ConditionalExpression>(ast, (p) => p.isConditionalExpression()).node.alternate;
const actualAlternativeCode = generate(actualAlternative).code;
expect(actualAlternativeCode.endsWith('a + b'), `${actualAlternativeCode} did not end with "a + b"`).true;
});

it('should add mutant coverage syntax', () => {
const { binaryExpression, mutant, ast } = arrangeSingleMutant();
conditionalExpressionMutantPlacer(binaryExpression, [mutant]);
expressionMutantPlacer(binaryExpression, [mutant]);
const actualAlternative = findNodePath<types.ConditionalExpression>(ast, (p) => p.isConditionalExpression()).node.alternate;
const actualAlternativeCode = generate(actualAlternative).code;
const expected = '__global_69fa48.__coverMutant__(1), ';
Expand All @@ -80,7 +80,7 @@ describe(conditionalExpressionMutantPlacer.name, () => {
];

// Act
conditionalExpressionMutantPlacer(binaryExpression, mutants);
expressionMutantPlacer(binaryExpression, mutants);
const actualCode = normalizeWhitespaces(generate(ast).code);

// Assert
Expand All @@ -91,13 +91,13 @@ describe(conditionalExpressionMutantPlacer.name, () => {
it('should not place when the expression is a key', () => {
// A stringLiteral is considered an expression, while it is not save to place a mutant there!
const stringLiteral = findNodePath(parseJS("const foo = { 'foo': bar }"), (p) => p.isStringLiteral());
expect(conditionalExpressionMutantPlacer(stringLiteral, [])).false;
expect(expressionMutantPlacer(stringLiteral, [])).false;
});

it('should place when the expression is the value', () => {
// A stringLiteral is considered an expression, while it is not save to place a mutant there!
const stringLiteral = findNodePath(parseJS("const foo = { 'foo': bar }"), (p) => p.isIdentifier() && p.node.name === 'bar');
expect(conditionalExpressionMutantPlacer(stringLiteral, [])).true;
expect(expressionMutantPlacer(stringLiteral, [])).true;
});
});

Expand All @@ -116,7 +116,7 @@ describe(conditionalExpressionMutantPlacer.name, () => {
];

// Act
conditionalExpressionMutantPlacer(expression, mutants);
expressionMutantPlacer(expression, mutants);
const actualCode = normalizeWhitespaces(generate(ast).code);

// Assert
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { types } from '@babel/core';
import generate from '@babel/generator';
import { normalizeWhitespaces } from '@stryker-mutator/util';

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

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

it("shouldn't place mutants on anything but a statement", () => {
Expand All @@ -18,7 +19,7 @@ describe(switchCaseMutantPlacer.name, () => {
findNodePath(parseJS('foo = bar'), (p) => p.isAssignmentExpression()),
findNodePath(parseJS('foo.bar()'), (p) => p.isCallExpression()),
].forEach((node) => {
expect(switchCaseMutantPlacer(node, [])).false;
expect(statementMutantPlacer(node, [])).false;
});
});

Expand All @@ -41,33 +42,45 @@ describe(switchCaseMutantPlacer.name, () => {
const { statement, mutant, ast } = arrangeSingleMutant();

// Act
const actual = switchCaseMutantPlacer(statement, [mutant]);
const actual = statementMutantPlacer(statement, [mutant]);
const actualCode = normalizeWhitespaces(generate(ast).code);

// Assert
expect(actual).true;
expect(actualCode).contains(
normalizeWhitespaces(`{
switch (__global_69fa48.__activeMutant__) {
case 1:
const foo = bar >>> baz;
break;
`)
);
expect(actualCode).contains(normalizeWhitespaces('if (__global_69fa48.__activeMutant__ === 1) { const foo = bar >>> baz; } else '));
});

it('should keep block statements in tact', () => {
// Arrange
const ast = parseJS('function add(a, b) { return a + b; }');
const statement = findNodePath(ast, (p) => p.isBlockStatement());
const originalNodePath = findNodePath<types.BinaryExpression>(ast, (p) => p.isBinaryExpression());
const mutant = createMutant({
original: originalNodePath.node,
replacement: types.binaryExpression('>>>', types.identifier('a'), types.identifier('b')),
});

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

// Assert
expect(actual).true;
expect(actualCode).matches(/function\s*add\s*\(a,\s*b\)\s*{.*}/);
});

it('should place the original code as default case', () => {
it('should place the original code as alternative (inside `else`)', () => {
const { ast, mutant, statement } = arrangeSingleMutant();
switchCaseMutantPlacer(statement, [mutant]);
statementMutantPlacer(statement, [mutant]);
const actualCode = normalizeWhitespaces(generate(ast).code);
expect(actualCode).matches(/default:.*const foo = a \+ b;\s*break;/);
expect(actualCode).matches(/else\s*{.*const foo = a \+ b;\s*\}/);
});

it('should add mutant coverage syntax', () => {
const { ast, mutant, statement } = arrangeSingleMutant();
switchCaseMutantPlacer(statement, [mutant]);
statementMutantPlacer(statement, [mutant]);
const actualCode = normalizeWhitespaces(generate(ast).code);
expect(actualCode).matches(/default:\s*__global_69fa48\.__coverMutant__\(1\)/);
expect(actualCode).matches(/else\s*{\s*__global_69fa48\.__coverMutant__\(1\)/);
});

it('should be able to place multiple mutants', () => {
Expand All @@ -82,19 +95,17 @@ describe(switchCaseMutantPlacer.name, () => {
];

// Act
switchCaseMutantPlacer(statement, mutants);
statementMutantPlacer(statement, mutants);
const actualCode = normalizeWhitespaces(generate(ast).code);

// Assert
expect(actualCode).contains(
normalizeWhitespaces(`{
switch (__global_69fa48.__activeMutant__) {
case 52:
const foo = bar >>> baz;
break;
case 659:
const bar = a + b;
break;`)
normalizeWhitespaces(`if (__global_69fa48.__activeMutant__ === 659) {
const bar = a + b;
} else if (__global_69fa48.__activeMutant__ === 52) {
const foo = bar >>> baz;
} else {
__global_69fa48.__coverMutant__(52, 659)`)
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,11 @@ var __global_69fa48 = function (g) {
}(new Function(\\"return this\\")());
function add(a, b) {
switch (__global_69fa48.__activeMutant__) {
case 0:
{}
break;
default:
__global_69fa48.__coverMutant__(0);
{
return __global_69fa48.__activeMutant__ === 1 ? a - b : (__global_69fa48.__coverMutant__(1), a + b);
}
break;
if (__global_69fa48.__activeMutant__ === 0) {
{}
} else {
__global_69fa48.__coverMutant__(0);
return __global_69fa48.__activeMutant__ === 1 ? a - b : (__global_69fa48.__coverMutant__(1), a + b);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,11 @@ exports[`instrumenter integration should be able to instrument a simple js file
}(new Function(\\"return this\\")());
function add(a, b) {
switch (__global_69fa48.__activeMutant__) {
case 0:
{}
break;
default:
__global_69fa48.__coverMutant__(0);
{
return __global_69fa48.__activeMutant__ === 1 ? a - b : (__global_69fa48.__coverMutant__(1), a + b);
}
break;
if (__global_69fa48.__activeMutant__ === 0) {
{}
} else {
__global_69fa48.__coverMutant__(0);
return __global_69fa48.__activeMutant__ === 1 ? a - b : (__global_69fa48.__coverMutant__(1), a + b);
}
}
Expand Down
Loading

0 comments on commit 848eb51

Please sign in to comment.