Skip to content

Commit

Permalink
major refactor of mutation paths/AST traversal
Browse files Browse the repository at this point in the history
  • Loading branch information
bttmly committed Aug 5, 2016
1 parent 1673ba1 commit a40ebd5
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 138 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ test-bail: compile

compile:
rm -rf ./built
./node_modules/.bin/tsc
./node_modules/.bin/tsc --strictNullChecks

example-events: compile
rm -rf ./.perturb
Expand Down
2 changes: 1 addition & 1 deletion TODO
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
- get CLI e2e test working
- `--strict` mode with stuff to ensure no mutation happens
- `--strict` mode with stuff to ensure no mutation happens in plugins (mutators in particular)
-
45 changes: 45 additions & 0 deletions src/ast-paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import fs = require("fs-extra");
import R = require("ramda");
import updateIn = require("./util/update-in");

import estraverse = require("estraverse");
import CommentManager = require("./comments");

// TODO this should be injected somehow
import shouldSkip = require("./skippers");

// the nice thing about this is that the plugins that are returned
// originate with the MutatorFinder argument, which simplifies testing
function getMutantLocations (locator: MutatorFinder, ast: ESTree.Node): MutantLocation[] {
const mutantLocations: MutantLocation[] = [];
const manager = new CommentManager();

estraverse.traverse(ast, {
enter: function (node: ESTree.Node) {
const locs: MutantLocation[] = applyVisitor(node, this, manager, locator);
mutantLocations.push(...locs);
},
});
return mutantLocations;
}

// No typings for estraverse.Controller :/
function applyVisitor (node: ESTree.Node, controller: any, manager: CommentManager, locator: MutatorFinder): MutantLocation[] {
const path : string[] = controller.path();

// TODO -- `shouldSkip` is the last part here which is in local module state rather than coming from an
// argument
if (shouldSkip(node, path)) {
controller.skip();
return [];
}

manager.applyNode(node);

return locator(node)
.filter(plugin => !manager.hasName(plugin.name))
.filter(m => m.filter == null || m.filter(node))
.map(plugin => ({node, path, mutator: plugin}))
}

export = R.curry(getMutantLocations);
40 changes: 31 additions & 9 deletions src/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,38 @@ interface CommentedNode extends ESTree.Node {
trailingComments?: Comment[];
}

function applyNodeComments (_node: ESTree.Node, disabledSet: Set<string>) {
R.pipe(
getComments,
R.chain(extractOperators),
R.forEach(applyOperator(disabledSet))
)(<CommentedNode>_node);
interface DisableManger {
apply: (n: ESTree.Node) => void;
has: (name: string) => boolean;
}

// a little class to encapsulate how mutators get enabled/disabled
class CommentManager {
_set: Set<string>;

constructor (set?: Set<string>) {
this._set = set || new Set<string>();
}

applyNode (_node: ESTree.Node) {
R.pipe(
getComments,
R.chain(extractOperators),
R.forEach(applyOperator(this._set))
)(<CommentedNode>_node);
}

hasName (name: string) {
return this._set.has(name);
}

[Symbol.iterator] () {
return [...this._set][Symbol.iterator]();
}
}

const applyOperator = R.curry(function (set: Set<string>, op: Operator) {
// perturb-disable: drop-return
// --perturb-disable: drop-return
switch (op.type) {
case "enable": {
// console.log("ENABLE", op.name)
Expand All @@ -39,7 +61,7 @@ const applyOperator = R.curry(function (set: Set<string>, op: Operator) {
return set.delete(op.name);
}
}
// perturb-enable: drop-return
// --perturb-enable: drop-return
});

function getComments (node: CommentedNode): Comment[] {
Expand Down Expand Up @@ -69,4 +91,4 @@ function extractOperators (c: Comment): Operator[] {
)(value);
}

export = applyNodeComments;
export = CommentManager;
16 changes: 11 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import R = require("ramda");
import Bluebird = require("bluebird");
import { spawn } from "child_process";
import fs = require("fs");
import assert = require("assert");

import getRunner = require("./runners");
Expand All @@ -10,6 +11,8 @@ import makeMutants = require("./make-mutants");
import makeConfig = require("./make-config");
import fileSystem = require("./file-system");
import runMutant = require("./util/run-mutant");
import astPaths = require("./ast-paths");
import mutators = require("./mutators");

function hasTests (m: Match): boolean {
return Boolean(R.path(["tests", "length"], m));
Expand All @@ -23,10 +26,10 @@ function perturb (_cfg: PerturbConfig) {
const { setup, teardown, paths } = fileSystem(cfg);

const matcher = getMatcher(cfg);
// const runner: RunnerPlugin = getRunner(cfg.runner);
const Runner: RunnerPlugin = getRunner(cfg.runner);
const reporter: ReporterPlugin = getReporter(cfg.reporter);
const handler = makeMutantHandler(Runner, reporter);
const runner = getRunner(cfg.runner);
const reporter = getReporter(cfg.reporter);

const handler = makeMutantHandler(runner, reporter);

let start;

Expand All @@ -41,15 +44,18 @@ function perturb (_cfg: PerturbConfig) {
const matches = matcher(sources, tests);

const [tested, untested] = R.partition(hasTests, matches);

// TODO -- surface untested file names somehow
console.log("untested files:", untested.map(m => m.source).join("\n"));

if (tested.length === 0) {
throw new Error("No matched files!");
}

start = Date.now();

return R.chain(makeMutants, tested);
const finder = astPaths(mutators.getMutatorsForNode)
return R.chain(makeMutants(finder), tested);
})
.then(sanityCheckAndSideEffects)
// run the mutatnts and gather the results
Expand Down
120 changes: 40 additions & 80 deletions src/make-mutants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import estraverse = require("estraverse");
import applyNodeComments = require("./comments");
import mutators = require("./mutators");
import shouldSkip = require("./skippers");

import astPaths = require("./ast-paths");
import updateIn = require("./util/update-in");

const PERTURB_ENABLE = "perturb-enable:";
const PERTURB_DISABLE = "perturb-disable:";

const ESPRIMA_SETTINGS = {
loc: true,
Expand All @@ -23,94 +23,54 @@ const FS_SETTINGS = {
encoding: "utf8",
};

// TODO: make this take the mutator plugins (index? finding func?) as an argument
// TODO: this function should not do the file reads, perhaps Match type needs the
// source code.
function makeMutants (match: Match): Mutant[] {
interface MutantLocation {
mutator: MutatorPlugin;
path: Path;
node: ESTree.Node;
}

function makeMutants (getLocations, match: Match): Mutant[] {
const { source, tests } = match;
const { ast, code } = parse(source);
const paths: Path[] = getMutationPaths(ast);
const { ast, code } = parse(match.sourceCode);
const locations: MutantLocation[] = getLocations(ast);

// we regenerate the source code here to make it easy for diffing
const originalSourceCode = escodegen.generate(ast);
return R.chain(mutantsFromPath, paths);

function mutantsFromPath (path: Path): Mutant[] {
const node = <ESTree.Node>R.path(path, ast);
return R.pipe(
R.filter(mutatorFilterFromNode(node)),
R.chain(function (m: MutatorPlugin) {

return toArray(m.mutator(node)).map(function (newNode) {
const updatedAst = updateIn(path, newNode, ast);
const mutatedSourceCode = escodegen.generate(updatedAst);

// both the original source and the mutated source are present here
// to avoid unnecessary extra code generation in mutator prep/teardown,
// and also in reporters

return <Mutant>{
sourceFile: source,
testFiles: tests,
path: path,
mutatorName: m.name,
astAfter: updatedAst,
astBefore: ast,
loc: node.loc,
originalSourceCode: originalSourceCode,
mutatedSourceCode: mutatedSourceCode,
};
});
})
)(mutators.getMutatorsForNode(node));
return R.chain(mutantsFromLocation, locations);

function mutantsFromLocation (location: MutantLocation): Mutant[] {
const {node, mutator, path} = location;

// should rename "mutator" to "mutate"? verb better as function name
return toArray(mutator.mutator(node))
.map(function (newNode) {
const updatedAst = updateIn(path, newNode, ast);
const mutatedSourceCode = escodegen.generate(updatedAst);

// both the original source and the mutated source are present here
// to avoid unnecessary extra code generation in mutator prep/teardown,
// and also in reporters

return <Mutant>{
sourceFile: source,
testFiles: tests,
path: path,
mutatorName: mutator.name,
astAfter: updatedAst,
astBefore: ast,
loc: node.loc,
originalSourceCode: originalSourceCode,
mutatedSourceCode: mutatedSourceCode,
};
});
}
}

type Path = string[];

// TODO: break this into it's own module
// and have it take the plugin index as an argument
function getMutationPaths (ast: ESTree.Node) {
const mutationPaths: Path[] = [];
const disabledMutations = new Set<string>();

estraverse.traverse(ast, {
enter: function (node: ESTree.Node) {
const path = <Path>this.path();
if (shouldSkip(node, path)) {
return this.skip();
}

applyNodeComments(node, disabledMutations);

const plugins = mutators.getMutatorsForNode(node)
const active = plugins.filter(m => !disabledMutations.has(m.name))

// if (plugins.length !== active.length) {
// console.log("some are disabled, what we have is", [...active]);
// console.log(escodegen.generate(node));
// }

if (active.length) {
mutationPaths.push(path)
}
},
});
return mutationPaths;
}

function mutatorFilterFromNode (node: ESTree.Node) {
return function (mutator: MutatorPlugin): boolean {
if (mutator.filter == null) return true;
if (mutator.filter(node)) return true;
return false;
};
}

function parse (source: string) {
const originalSource = fs.readFileSync(source).toString();
try {
const ast: ESTree.Node = esprima.parse(originalSource, ESPRIMA_SETTINGS);
const ast: ESTree.Node = esprima.parse(source, ESPRIMA_SETTINGS);
const code: string = escodegen.generate(ast);
return { ast, code };
} catch (err) {
Expand All @@ -123,4 +83,4 @@ function parse (source: string) {
const last = arr => arr[arr.length - 1];
const toArray = x => Array.isArray(x) ? x : [x];

export = makeMutants;
export = R.curry(makeMutants);
8 changes: 7 additions & 1 deletion src/matchers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as fs from "fs";
import * as R from "ramda";

import baseGenerative = require("./base-generative");
Expand Down Expand Up @@ -39,7 +40,12 @@ function getMatcher (c: PerturbConfig) {
return function findMatches (sources: string[], tests: string[]): Match[] {
const runMatch: runMatcher = type === "generative" ? runGenerative : runComparative;
return sources.map(source => ({
source, tests: runMatch(matcher, source, tests),
source,
tests: runMatch(matcher, source, tests),
// TODO - right now I'm just shuffling this piece of I/O around to make something else
// easier to test. Will have to put it somewhere permanent eventually, but this seems
// best for right now
sourceCode: fs.readFileSync(source).toString(),
}));
}
}
Expand Down
Loading

0 comments on commit a40ebd5

Please sign in to comment.