Skip to content

Commit

Permalink
more mutators; groundwork for multi-mutators
Browse files Browse the repository at this point in the history
  • Loading branch information
bttmly committed Jul 14, 2016
1 parent 795c6d8 commit 474ecfd
Show file tree
Hide file tree
Showing 19 changed files with 166 additions and 88 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ dogfood:
node ./run.js dogfood

build:
./node_modules/.bin/tsc
./node_modules/.bin/tsc --strictNullChecks

.PHONY: test example
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
"object-assign": "^2.0.0",
"ramda": "^0.18.0",
"ramda-fantasy": "^0.4.1",
"through2": "^2.0.0"
"through2": "^2.0.0",
"typescript": "^2.0.0"
},
"devDependencies": {
"babel-eslint": "^4.1.1",
Expand Down
12 changes: 10 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ module.exports = function perturb (_cfg: PerturbConfig) {

console.log("init with config\n", cfg);

const {setup, teardown, paths} = fileSystem(cfg);
const { setup, teardown, paths } = fileSystem(cfg);

const matcher = getMatcher(cfg);
const runner: RunnerPlugin = getRunner(cfg.runner);
Expand Down Expand Up @@ -77,7 +77,8 @@ module.exports = function perturb (_cfg: PerturbConfig) {
.catch(err => {
console.log("ERROR IN PERTURB MAIN CHAIN", err);
throw err;
});
})
.finally(teardown)
}

function makeMutantHandler (runner: RunnerPlugin, reporter: ReporterPlugin) {
Expand Down Expand Up @@ -119,4 +120,11 @@ function sanityCheckAndSideEffects (ms: Mutant[]) {
assert.notEqual(m.mutatedSourceCode, "", "Mutated source code should not be empty.");
});
return ms;
}

Promise.prototype.finally = function (cb) {
return this.then(
value => this.constructor.resolve(cb()).then(() => value),
reason => this.constructor.resolve(cb()).then(() => { throw reason; })
);
}
34 changes: 13 additions & 21 deletions src/make-mutants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ const FS_SETTINGS = {
};

module.exports = function makeMutants (match: Match): Mutant[] {
const {source, tests} = match;
const {ast, code} = parse(source);
const { source, tests } = match;
const { ast, code } = parse(source);
const paths: Path[] = getMutationPaths(ast).map(p => p.map(String));

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

function mutationsFromPath (path: Path): Mutant[] {
function mutantsFromPath (path: Path): Mutant[] {
const node = <ESTree.Node>R.path(path, ast);
return getMutatorsForNode(node)
.filter(mutatorFilterFromNode(node))
.map(function (m: MutatorPlugin) {
return R.pipe(
R.filter(mutatorFilterFromNode(node)),
R.chain(function (m: MutatorPlugin) {
// this can be done more elegantly with Ramda lenses, probably
const newNode = m.mutator(node);

Expand All @@ -63,7 +63,8 @@ module.exports = function makeMutants (match: Match): Mutant[] {
originalSourceCode: originalSourceCode,
mutatedSourceCode: mutatedSourceCode,
};
});
})
)(getMutatorsForNode(node));
}
}

Expand Down Expand Up @@ -94,24 +95,15 @@ function mutatorFilterFromNode (node: ESTree.Node) {
};
}

interface _ParseResult {
ast: ESTree.Node;
code: string;
}

function parse (source: string): _ParseResult {
console.log("reading", source, "...");
function parse (source: string) {
const originalSource = fs.readFileSync(source).toString();

let ast : ESTree.Node;
try {
ast = esprima.parse(originalSource, ESPRIMA_SETTINGS);
const ast: ESTree.Node = esprima.parse(originalSource, ESPRIMA_SETTINGS);
const code: string = escodegen.generate(ast);
return { ast, code };
} catch (err) {
// TODO -- better error handling here
console.log("ERROR PARSING SOURCE FILE", source);
throw err;
}

const code = escodegen.generate(ast);
return { ast, code }
}
12 changes: 6 additions & 6 deletions src/mutators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ const coreMutators: MutatorPlugin[] = [
require("./tweak-object-literal"),
require("./tweak-boolean-literal"),
require("./tweak-number-literal"),
require("./tweak-string-literal"),
// require("./tweak-string-literal"),
require("./tweak-arguments"),
require("./tweak-switch"),
];

let mutatorIndex = {}
let activeMutators = []
let activeMutators: MutatorPlugin[] = [];

// temporary stub -- this function will return false for disabled mutators (based on config)
function isMutatorEnabled (m: MutatorPlugin): boolean {
Expand Down Expand Up @@ -62,10 +64,8 @@ function makeMutatorIndex (names: string[]) {

function locateMutatorPlugins (names: string[]): MutatorPlugin[] {
return names.map(function (name: string): MutatorPlugin {
let plugin: MutatorPlugin;
try {
plugin = require(`perturb-plugin-mutator-${name}`);
return plugin;
return <MutatorPlugin>require(`perturb-plugin-mutator-${name}`)
} catch (err) {
// any way to recover? other locate strategy? something with local path resolution?
console.log(`unable to locate -MUTATOR- plugin "${name}" -- fatal error, exiting`);
Expand All @@ -88,7 +88,7 @@ exports.getMutatorsForNode = function (node: ESTree.Node): MutatorPlugin[] {
return R.propOr([], node.type, mutatorIndex);
}

exports.getMutatorByName = function (name: string): MutatorPlugin {
exports.getMutatorByName = function (name: string): MutatorPlugin | undefined {
return activeMutators.find(m => m.name === name);
}

Expand Down
19 changes: 19 additions & 0 deletions src/mutators/tweak-arguments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const R = require("ramda");
const { Syntax } = require("estraverse");

const dropItem = require("../util/drop-item");

import { MutatorPlugin } from "../types";

module.exports = <MutatorPlugin>{
// drops the first declared element in an array literal
// `['a', 'b']` => `['a']`
name: "tweak-arguments",
nodeTypes: [Syntax.CallExpression],
filter: function (node) {
return R.path(["arguments", "length"], node) !== 0;
},
mutator: function (node) {
return dropItem(node, "arguments", "first");
},
};
27 changes: 3 additions & 24 deletions src/mutators/tweak-array-literal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const R = require("ramda");
const { Syntax } = require("estraverse");

const dropItem = require("../util/drop-item");

import { MutatorPlugin } from "../types";

module.exports = <MutatorPlugin>{
Expand All @@ -12,29 +14,6 @@ module.exports = <MutatorPlugin>{
return R.path(["elements", "length"], node) !== 0;
},
mutator: function (node) {
return strategies.dropFirst(<ESTree.ArrayExpression>node);
return dropItem(<ESTree.ArrayExpression>node, "elements", "first");
},
};

const strategies = {
dropFirst: function (node: ESTree.ArrayExpression) {
return R.assoc("elements", node.elements.slice(1), node);
},
dropLast: function (node: ESTree.ArrayExpression) {
return R.assoc("elements", node.elements.slice(0, -1), node);
},
dropRandom: function (node: ESTree.ArrayExpression) {
return R.assoc("elements", dropRandom(node.elements), node);
}
}

function dropRandom(arr) {
var i = getRandomIndex(arr);
var out = arr.slice();
out.splice(i, 1);
return out;
}

function getRandomIndex(arr) {
return Math.floor(Math.random() * (arr.length))
}
9 changes: 5 additions & 4 deletions src/mutators/tweak-number-literal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ interface NumberLiteral extends ESTree.Literal {
value: number;
}

module.exports = {
module.exports = <MutatorPlugin>{
// adds 1 to any number literal OR replaces 1 with 0
// var num = 0; => var num = 1;
// var x = 735; => var x = 736;
// `var num = 735;` => `var num = 736;`
// `var num = 1;` => `var num = 0;`
name: "tweak-number-literal",
nodeTypes: [Syntax.Literal],
filter: function (node) {
return typeof node.value === "number";
const {value} = (<ESTree.Literal>node);
return typeof value === "number";
},
mutator: function (node) {
const {value} = <NumberLiteral>node;
Expand Down
23 changes: 3 additions & 20 deletions src/mutators/tweak-object-literal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const { Syntax } = require("estraverse");
const R = require("ramda");

const dropItem = require("../util/drop-item");

import { MutatorPlugin } from "../types";

module.exports = <MutatorPlugin>{
Expand All @@ -12,25 +14,6 @@ module.exports = <MutatorPlugin>{
return R.path(["properties", "length"], node) !== 0;
},
mutator: function (node) {
return strategies.dropFirst(<ESTree.ObjectExpression>node);
return dropItem(<ESTree.ArrayExpression>node, "properties", "first");
},
};

// TODO -- DRY this up w/ array literal tweak mutator and string mutator

const strategies = {
dropFirst: function(node: ESTree.ObjectExpression) {
return R.assoc("properties", node.properties.slice(1), node);
},
dropLast: function(node: ESTree.ObjectExpression) {
return R.assoc("properties", node.properties.slice(0, -1), node);
},
dropRandom: function(node: ESTree.ObjectExpression) {
return R.assoc("properties", dropRandom(node.properties), node);
}
}

function dropRandom (s) {
const pos = Math.round(Math.random() * s.length - 1);
return s.slice(0, pos) + s.slice(pos + 1);
}
17 changes: 17 additions & 0 deletions src/mutators/tweak-switch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const { Syntax } = require("estraverse");
const R = require("ramda");

const dropItem = require("../util/drop-item");

import { MutatorPlugin } from "../types";

module.exports = <MutatorPlugin>{
name: "tweak-switch",
nodeTypes: [Syntax.SwitchStatement],
filter: function (node) {
return R.path(["cases", "length"], node) !== 0;
},
mutator: function (node) {
return dropItem(node, "cases", "first");
},
};
4 changes: 3 additions & 1 deletion src/reporters/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ function generateReport (r: RunnerResult): string {
// return chalk.gray(id);
// }

const title = chalk.red.underline(alive + id);
const title = r.error ? chalk.gray(dead + id) :
chalk.red.underline(alive + id);

const diff = generateDiff(r);

return [
Expand Down
36 changes: 36 additions & 0 deletions src/util/drop-item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
///<reference path="../../typings/modules/ramda/index.d.ts"/>

// extra block-scope hack to account for "error TS2451: Cannot redeclare block-scoped variable 'R'" (???)
{

const R = require("ramda");

type DropStrategy = "first" | "last" | "random"

const strategyMap = {
first: function(node: ESTree.Node, key: string) {
return R.assoc(key, node[key].slice(1), node);
},
last: function(node: ESTree.Node, key: string) {
return R.assoc(key, node[key].slice(0, -1), node);
},
random: function(node: ESTree.Node, key: string) {
return R.assoc(key, dropRandom(node[key]), node);
},
}

function getRandomIndex (arr: any[]) {
return Math.floor(Math.random() * (arr.length))
}

function dropRandom (arr: any[]) {
return R.remove(getRandomIndex(arr), 1, arr);
}

function dropItemOfKey (node: ESTree.Node, key: string, strategy: DropStrategy) {
return strategyMap[strategy](node, key);
}

module.exports = dropItemOfKey;

}
15 changes: 15 additions & 0 deletions src/util/remove-each.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
///<reference path="../../typings/modules/ramda/index.d.ts"/>

{

const R = require("ramda");

function mapRemoveKeyItem (key, obj) {
return obj[key].map(function (_, i) {
return R.assoc(key, R.remove(i, 1, obj[key]), obj);
});
}

module.exports = R.curry(mapRemoveKeyItem);

}
4 changes: 4 additions & 0 deletions src/util/update-in.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
///<reference path="../../typings/modules/ramda/index.d.ts"/>

// copied and (slightly) moddified from Ramda source code

const R = require("ramda");

type Prop = string | number;
Expand Down
2 changes: 1 addition & 1 deletion test/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function mutatorByName (name) {
}

function applyMutation (node, name) {
return mutatorByName("tweak-object-literal").mutator(node);
return mutatorByName(name).mutator(node);
}

module.exports = {
Expand Down
8 changes: 8 additions & 0 deletions test/mutators/_notes
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
mutators
- drop last argument of function
- drop a case from a switch statement


API
- how to skip or mark OK? i.e. "reverse-function-parameters" applied to (a, b) => a + b will not report a failure, but it is ok

Empty file removed test/mutators/drop-node.js
Empty file.
Loading

0 comments on commit 474ecfd

Please sign in to comment.