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

feat(instrumenter): add mutant placers #2224

Merged
merged 2 commits into from
May 28, 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
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