Skip to content

Commit

Permalink
feat(instrumenter): add mutant placers (#2224)
Browse files Browse the repository at this point in the history
Add mutant placers.

* `switchCaseMutantPlacer`: places a mutant in a switch case
* `conditionalExpressionMutantPacer`: places a mutant in a conditional expression
  • Loading branch information
nicojs authored May 28, 2020
1 parent 78a021c commit 0e05025
Show file tree
Hide file tree
Showing 13 changed files with 538 additions and 136 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { NodePath, types } from '@babel/core';

import { Mutant } from '../mutant';
import { createMutatedAst, mutantTestExpression, mutationCoverageSequenceExpression } from '../util/syntax-helpers';

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

/**
* Places the mutants with a conditional expression: `global.activeMutant === 1? mutatedCode : regularCode`;
*/
const conditionalExpressionMutantPlacer: MutantPlacer = (path: NodePath, mutants: Mutant[]): boolean => {
if (path.isExpression() && !path.parentPath.isObjectProperty() && !path.parentPath.isTaggedTemplateExpression()) {
// First calculated the mutated ast before we start to apply mutants.
const appliedMutants = mutants.map((mutant) => ({
mutant,
ast: createMutatedAst<types.BinaryExpression>(path as NodePath<types.BinaryExpression>, mutant),
}));

// Second add the mutation coverage expression
path.replaceWith(mutationCoverageSequenceExpression(mutants, path.node));

// Now apply the mutants
for (const appliedMutant of appliedMutants) {
path.replaceWith(types.conditionalExpression(mutantTestExpression(appliedMutant.mutant.id), appliedMutant.ast, path.node));
}
return true;
} else {
return false;
}
};

// Export it after initializing so `fn.name` is properly set
export { conditionalExpressionMutantPlacer };
33 changes: 33 additions & 0 deletions packages/instrumenter/src/mutant-placers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export * from './mutant-placer';
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';

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

/**
* Represents a mutant placer, tries to place a mutant in the AST with corresponding mutation switch and mutant covering expression
* @see https://github.com/stryker-mutator/stryker/issues/1514
* @param node The ast node to try and replace with a mutated
* @param mutants The mutants to place in the AST node
*/
export function placeMutant(node: NodePath, mutants: Mutant[], mutantPlacers: readonly MutantPlacer[] = MUTANT_PLACERS) {
if (mutants.length) {
for (const placer of mutantPlacers) {
try {
if (placer(node, mutants)) {
return true;
}
} catch (error) {
throw new Error(
`Error while placing mutants on ${node.node.loc?.start.line}:${node.node.loc?.start.column} with ${placer.name}. ${error.stack}`
);
}
}
}
return false;
}
5 changes: 5 additions & 0 deletions packages/instrumenter/src/mutant-placers/mutant-placer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { NodePath } from '@babel/core';

import { Mutant } from '../mutant';

export type MutantPlacer = (node: NodePath, mutants: Mutant[]) => boolean;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { types } from '@babel/core';

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

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

const switchCaseMutantPlacer: MutantPlacer = (path, mutants) => {
if (path.isStatement()) {
// First calculate the mutated ast before we start to apply mutants.
const appliedMutants = mutants.map((mutant) => ({
mutant,
ast: createMutatedAst(path, mutant),
}));

// Add switch statement
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(),
]),
]),
])
);
return true;
} else {
return false;
}
};

// Export it after initializing so `fn.name` is properly set
export { switchCaseMutantPlacer };
214 changes: 110 additions & 104 deletions packages/instrumenter/src/util/syntax-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,90 +1,76 @@
import { types } from '@babel/core';
import { types, NodePath } from '@babel/core';
import traverse from '@babel/traverse';
import { parse } from '@babel/parser';

// export const GLOBAL = '__global_69fa48';
// export const MUTATION_COVERAGE_OBJECT = '__mutationCoverage__';
// export const COVER_MUTANT_HELPER_METHOD = '__coverMutant__';
// export const ACTIVE_MUTANT = 'activeMutant';
import { Mutant } from '../mutant';

// /**
// * Returns syntax for the global header
// */
// export function declareGlobal(): types.VariableDeclaration {
// return parse(`var ${GLOBAL} = (function(g){
// g.${MUTATION_COVERAGE_OBJECT} = g.${MUTATION_COVERAGE_OBJECT} || { static: {} };
// g.${COVER_MUTANT_HELPER_METHOD} = g.${COVER_MUTANT_HELPER_METHOD} || function () {
// var c = g.${MUTATION_COVERAGE_OBJECT}.static;
// if (g.__currentTestId__) {
// c = g.${MUTATION_COVERAGE_OBJECT}[g.__currentTestId__];
// }
// var a = arguments;
// for(var i=0; i < a.length; i++){
// c[a[i]] = (c[a[i]] || 0) + 1;
// }
// };
// return g;
// })(new Function("return this")())`).program.body[0] as types.VariableDeclaration;
// }
/**
* Identifiers used when instrumenting the code
*/
export const ID = Object.freeze({
GLOBAL: '__global_69fa48',
MUTATION_COVERAGE_OBJECT: '__mutationCoverage__',
COVER_MUTANT_HELPER_METHOD: '__coverMutant__',
ACTIVE_MUTANT: 'activeMutant',
} as const);

// /**
// * returns syntax for `global.activeMutant === $mutantId`
// * @param mutantId The id of the mutant to switch
// */
// export function mutantTestExpression(mutantId: number): types.BinaryExpression {
// return types.binaryExpression('===', memberExpressionChain(GLOBAL, ACTIVE_MUTANT), types.numericLiteral(mutantId));
// }
/**
* Returns syntax for the global header
*/
export const declareGlobal = parse(`var ${ID.GLOBAL} = (function(g){
g.${ID.MUTATION_COVERAGE_OBJECT} = g.${ID.MUTATION_COVERAGE_OBJECT} || { static: {} };
g.${ID.COVER_MUTANT_HELPER_METHOD} = g.${ID.COVER_MUTANT_HELPER_METHOD} || function () {
var c = g.${ID.MUTATION_COVERAGE_OBJECT}.static;
if (g.__currentTestId__) {
c = g.${ID.MUTATION_COVERAGE_OBJECT}[g.__currentTestId__];
}
var a = arguments;
for(var i=0; i < a.length; i++){
c[a[i]] = (c[a[i]] || 0) + 1;
}
};
return g;
})(new Function("return this")())`).program.body[0] as types.VariableDeclaration;

// /**
// * Creates a member expression chain: `memberExpressionChain('a', 'b', 'c', 4)` => `a.b.c[4]`
// */
// export function memberExpressionChain(...identifiers: Array<string | number>): types.Identifier | types.MemberExpression {
// const currentIdentifier = identifiers[identifiers.length - 1];
// if (identifiers.length === 1) {
// return types.identifier(currentIdentifier.toString());
// } else {
// return types.memberExpression(
// memberExpressionChain(...identifiers.slice(0, identifiers.length - 1)),
// types.identifier(currentIdentifier.toString()),
// /* computed */ typeof currentIdentifier === 'number'
// );
// }
// }
/**
* returns syntax for `global.activeMutant === $mutantId`
* @param mutantId The id of the mutant to switch
*/
export function mutantTestExpression(mutantId: number): types.BinaryExpression {
return types.binaryExpression('===', memberExpressionChain(ID.GLOBAL, ID.ACTIVE_MUTANT), types.numericLiteral(mutantId));
}

// /**
// * Returns a sequence of mutation coverage counters with an optional last expression.
// *
// * @example (global.__coverMutant__(0), global.__coverMutant__(1), 40 + 2)
// * @param mutants The mutant ids for which covering syntax needs to be generated
// * @param targetExpression The original expression
// */
// export function mutationCoverageSequenceExpression(mutants: Mutant[], targetExpression?: types.Expression): types.Expression {
// const sequence: types.Expression[] = [
// types.callExpression(
// memberExpressionChain(GLOBAL, COVER_MUTANT_HELPER_METHOD),
// mutants.map((mutant) => types.numericLiteral(mutant.id))
// ),
// ];
// if (targetExpression) {
// sequence.push(targetExpression);
// }
// return types.sequenceExpression(sequence);
// }
/**
* Creates a member expression chain: `memberExpressionChain('a', 'b', 'c', 4)` => `a.b.c[4]`
*/
export function memberExpressionChain(...identifiers: Array<string | number>): types.Identifier | types.MemberExpression {
const currentIdentifier = identifiers[identifiers.length - 1];
if (identifiers.length === 1) {
return types.identifier(currentIdentifier.toString());
} else {
return types.memberExpression(
memberExpressionChain(...identifiers.slice(0, identifiers.length - 1)),
types.identifier(currentIdentifier.toString()),
/* computed */ typeof currentIdentifier === 'number'
);
}
}

// interface Position {
// line: number;
// column: number;
// }
interface Position {
line: number;
column: number;
}

// function eqLocation(a: types.SourceLocation, b: types.SourceLocation): boolean {
// function eqPosition(a: Position, b: Position): boolean {
// return a.column === b.column && a.line === b.line;
// }
// return eqPosition(a.start, b.start) && eqPosition(a.end, b.end);
// }
function eqLocation(a: types.SourceLocation, b: types.SourceLocation): boolean {
function eqPosition(a: Position, b: Position): boolean {
return a.column === b.column && a.line === b.line;
}
return eqPosition(a.start, b.start) && eqPosition(a.end, b.end);
}

// export function eqNode<T extends types.Node>(a: T, b: types.Node): b is T {
// return a.type === b.type && !!a.loc && !!b.loc && eqLocation(a.loc, b.loc);
// }
export function eqNode<T extends types.Node>(a: T, b: types.Node): b is T {
return a.type === b.type && !!a.loc && !!b.loc && eqLocation(a.loc, b.loc);
}

export function offsetLocations(file: types.File, { position, line, column }: { position: number; line: number; column: number }): void {
const offsetNode = (node: types.Node): void => {
Expand All @@ -110,33 +96,53 @@ export function offsetLocations(file: types.File, { position, line, column }: {
file.end! += position;
}

// export function createMutatedAst<T extends types.Node>(contextPath: NodePath<T>, mutant: Mutant): T {
// if (eqNode(contextPath.node, mutant.original)) {
// return mutant.replacement as T;
// } else {
// const mutatedAst = types.cloneNode(contextPath.node, /*deep*/ true);
// let isAstMutated = false;
export function createMutatedAst<T extends types.Node>(contextPath: NodePath<T>, mutant: Mutant): T {
if (eqNode(contextPath.node, mutant.original)) {
return mutant.replacement as T;
} else {
const mutatedAst = types.cloneNode(contextPath.node, /*deep*/ true);
let isAstMutated = false;

// traverse(
// mutatedAst,
// {
// noScope: true,
// enter(path) {
// if (eqNode(path.node, mutant.original)) {
// path.replaceWith(mutant.replacement);
// path.stop();
// isAstMutated = true;
// }
// },
// },
// contextPath.scope
// );
// if (!isAstMutated) {
// throw new Error(`Could not apply mutant ${JSON.stringify(mutant.replacement)}.`);
// }
// return mutatedAst;
// }
// }
traverse(
mutatedAst,
{
noScope: true,
enter(path) {
if (eqNode(path.node, mutant.original)) {
path.replaceWith(mutant.replacement);
path.stop();
isAstMutated = true;
}
},
},
contextPath.scope
);
if (!isAstMutated) {
throw new Error(`Could not apply mutant ${JSON.stringify(mutant.replacement)}.`);
}
return mutatedAst;
}
}

/**
* Returns a sequence of mutation coverage counters with an optional last expression.
*
* @example (global.__coverMutant__(0, 1), 40 + 2)
* @param mutants The mutant ids for which covering syntax needs to be generated
* @param targetExpression The original expression
*/
export function mutationCoverageSequenceExpression(mutants: Mutant[], targetExpression?: types.Expression): types.Expression {
const sequence: types.Expression[] = [
types.callExpression(
memberExpressionChain(ID.GLOBAL, ID.COVER_MUTANT_HELPER_METHOD),
mutants.map((mutant) => types.numericLiteral(mutant.id))
),
];
if (targetExpression) {
sequence.push(targetExpression);
}
return types.sequenceExpression(sequence);
}

// export function isTypeAnnotation(path: NodePath): boolean {
// return (
Expand Down
23 changes: 0 additions & 23 deletions packages/instrumenter/test/helpers/expect-ast.ts

This file was deleted.

Loading

0 comments on commit 0e05025

Please sign in to comment.