Skip to content

Commit

Permalink
miscellaneous improvements
Browse files Browse the repository at this point in the history
- strong types on node type enum
- use native flatMap over R.chain
- specify reporter in CLI flags
- save history of run results in run-record.json
- fix the signature of some unused filter functions
- test isStringRequire
- upgrade to Node 12, TS 3.7, lib es2019
  • Loading branch information
bttmly committed Oct 10, 2019
1 parent d3244c3 commit efe86fd
Show file tree
Hide file tree
Showing 23 changed files with 304 additions and 91 deletions.
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
12.5.0
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ example-toy: compile

dogfood: compile
rm -rf ./.perturb
node ./lib/cli -s lib
node ./lib/cli -s lib --reporter quiet

dogfood-fork: compile
rm -rf ./.perturb
Expand Down
2 changes: 2 additions & 0 deletions TODO
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
- should encourage running with the fork runner, it needs to be faster. probably need to implement worker pooling
- ~run-record should keep a history with a time log~
- reporters should be able to display progress, which means they need to hold state
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
},
"repository": "git@github.com:bttmly/perturb.git",
"dependencies": {
"@types/node": "^12.7.12",
"chalk": "^2.3.2",
"change-case": "^2.2.0",
"commander": "^2.19.0",
Expand All @@ -20,7 +21,7 @@
"glob": "7.1.3",
"p-map-series": "^2.0.0",
"ramda": "^0.18.0",
"typescript": "^3.4.0-rc"
"typescript": "^3.7.0-beta"
},
"devDependencies": {
"@types/bluebird": "^3.5.20",
Expand All @@ -34,7 +35,6 @@
"@types/fs-extra": "^5.0.1",
"@types/glob": "^5.0.35",
"@types/mocha": "^5.0.0",
"@types/node": "^9.6.1",
"@types/ramda": "^0.25.21",
"babel-eslint": "^8.2.2",
"eslint": "^5.16.0",
Expand Down
6 changes: 4 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ program
parseInt,
)
.option("-u, --runner <runner>", "name of runner or runner plugin")
.option("--reporter <reporter>", "which reporter to use")
.parse(process.argv);

if (program.rootDir && program.rootDir[0] !== "/") {
Expand All @@ -58,6 +59,7 @@ const args: OptionalPerturbConfig = R.pickBy(R.complement(R.isNil), {
sourceGlob: program.sourceGlob,
testCmd: program.testCmd,
runner: program.runner,
reporter: program.reporter,
killRateMin: program.killRateMin,
});

Expand Down Expand Up @@ -85,14 +87,14 @@ process.on("unhandledRejection", err => {
if (killRate < config.killRateMin) {
console.error(
`❌ Mutant kill rate was ${killRate} which is below minimum acceptable value ${
config.killRateMin
config.killRateMin
}`,
);
process.exitCode = 1;
} else {
console.log(
`✅ Mutant kill rate was ${killRate} which is above minimum acceptable value ${
config.killRateMin
config.killRateMin
}`,
);
}
Expand Down
7 changes: 3 additions & 4 deletions src/comments.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as R from "ramda";
import * as ESTree from "estree";

const ENABLING_COMMENT = "perturb-enable:";
Expand Down Expand Up @@ -39,9 +38,9 @@ export default class CommentManager {
toArray = () => [...this._disabled];

_applyComments(cs: ESTree.Comment[]) {
R.chain(extractOperators, cs).forEach((op: Operator) =>
this._applyOperator(op),
);
for (const op of cs.flatMap(extractOperators)) {
this._applyOperator(op);
}
return null;
}

Expand Down
75 changes: 44 additions & 31 deletions src/filters/filter.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,73 @@
import * as R from "ramda";
import * as escodegen from "escodegen";
import { Node } from "estree";

import S from "../mutators/_syntax";
import { LocationFilter } from "../types";

export const isStringRequire = R.allPass([
R.propEq("type", S.CallExpression),
R.pathEq(["callee", "name"], "require"),
R.pathEq(["arguments", "length"], 1),
R.pathEq(["arguments", "0", "type"], S.Literal),
n => typeof R.path(["arguments", "0", "value"], n) === "string",
]);

// export const isStringRequire = R.allPass([
// R.propEq("type", S.CallExpression),
// R.pathEq(["callee", "name"], "require"),
// R.pathEq(["arguments", "length"], 1),
// R.pathEq(["arguments", "0", "type"], S.Literal),
// n => typeof R.path(["arguments", "0", "value"], n) === "string",
// ]);

export function isStringRequire(node: Node) {
const result = (
R.propEq("type", S.CallExpression, node) &&
R.pathEq(["callee", "name"], "require", node) &&
R.pathEq(["arguments", "length"], 1, node) &&
R.pathEq(["arguments", "0", "type"], S.Literal, node) &&
typeof R.path(["arguments", "0", "value"], node) === "string"
);

// if (result) {
// console.log(`dropped require ${escodegen.generate(node)}`)
// }

return result;
}

export const isUseStrict = R.allPass([
R.propEq("type", S.ExpressionStatement),
R.pathEq(["expression", "value"], "use strict"),
]);

export const isCallOfName = (name: string): LocationFilter => {
return ({ node }) => {
if (!R.propEq("type", S.CallExpression, node)) return false;
if (!R.pathEq(["callee", "type"], S.Identifier, node)) return false;
if (!R.pathEq(["callee", "name"], name, node)) return false;
return true;
export const isCallOfName = (name: string) => {
return (node: Node) => {
const result = (
R.propEq("type", S.CallExpression, node) &&
R.pathEq(["callee", "type"], S.Identifier, node) &&
R.pathEq(["callee", "name"], name, node)
)
return result
};
};

export function nodeSourceIncludes(text: string): LocationFilter {
return ({ node }) => {
return escodegen.generate(node).includes(text);
};
export function nodeSourceIncludes(text: string) {
return (node: Node) => escodegen.generate(node).includes(text);
}

export function sourceNodeIncludesAny(texts: string[]): LocationFilter {
return ({ node }) => {
export function sourceNodeIncludesAny(texts: string[]) {
return (node: Node) => {
const code = escodegen.generate(node);
return texts.some(t => code.includes(t));
};
}

export function nodeSourceMatches(re: RegExp): LocationFilter {
return ({ node }) => {
return re.test(escodegen.generate(node));
};
export function nodeSourceMatches(re: RegExp) {
return (node: Node) => re.test(escodegen.generate(node));
}

export function sourceNodeMatchesAny(res: RegExp[]): LocationFilter {
return ({ node }) => {
export function sourceNodeMatchesAny(res: RegExp[]) {
return (node: Node) => {
const code = escodegen.generate(node);
return res.some(re => re.test(code));
};
}

export function sourceNodeIs(text: string): LocationFilter {
return ({ node }) => {
return escodegen.generate(node).trim() === text;
};
export function sourceNodeIs(text: string) {
return (node: Node) => escodegen.generate(node).trim() === text;
}

export const isESModuleInterop = nodeSourceIncludes(
Expand Down
17 changes: 13 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,21 @@ export default async function perturb(inputCfg: OptionalPerturbConfig) {
console.log("*******************************************");
}

// console.log("matches:", tested.map(t => ({source: t.source, tests: t.tests})));
// let diff = 0;

const parsedMatches = tested.map(parseMatch(locator)).map(pm => {
pm.locations = pm.locations.filter(locationFilter);
const remaining = pm.locations.filter(locationFilter);
// diff += pm.locations.length - remaining.length;
pm.locations = remaining;
return pm;
});

// console.log(`NODE FILTERS REMOVED ${diff} LOCATIONS`)

const start = Date.now();

// create the mutant objects from the matched files
let mutants = await R.chain(makeMutants, parsedMatches);
let mutants = await parsedMatches.flatMap(makeMutants);

// let's just check if everything is okay...
await sanityCheckAndSideEffects(mutants);
Expand Down Expand Up @@ -133,7 +137,12 @@ function makeMutantHandler(
await runner.setup();
const result = await runner.run();
await runner.cleanup();
reporter.onResult(result);
try {
reporter.onResult(result);
} catch (err) {
console.log(reporter);
throw err;
}
return result;
};
}
Expand Down
10 changes: 3 additions & 7 deletions src/make-mutants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as R from "ramda";
import * as escodegen from "escodegen";
import updateIn from "./util/update-in";
import * as ESTree from "estree";
Expand All @@ -14,8 +13,7 @@ export default function makeMutants(pm: ParsedMatch): Mutant[] {
const ast = pm.ast;
const sourceCode = pm.code;

function mapper(location: MutantLocation): Mutant[] {
const { node, mutator, path } = location;
function mapper({ node, mutator, path }: MutantLocation): Mutant[] {
const newNodes = toArray(mutator.mutator(node));

// should rename "mutator" to "mutate" maybe? verb is probably better as function name
Expand All @@ -26,7 +24,7 @@ export default function makeMutants(pm: ParsedMatch): Mutant[] {
// to avoid unnecessary extra code generation in mutator prep/teardown,
// and also in reporters

const m: Mutant = {
return {
sourceFile,
testFiles,
path,
Expand All @@ -37,11 +35,9 @@ export default function makeMutants(pm: ParsedMatch): Mutant[] {
originalSourceCode: sourceCode,
mutatedSourceCode: escodegen.generate(updatedAst),
};
return m;
});
}

return R.chain(mapper, pm.locations);
return pm.locations.flatMap(mapper)
}

const toArray = (x: any) => (Array.isArray(x) ? x : [x]);
86 changes: 81 additions & 5 deletions src/mutators/_syntax.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,89 @@
const S = require("estraverse").Syntax;
// copied from https://github.com/estools/estraverse/blob/54d608c4ce0eb36d9bade685edcc3177e90e9f3c/estraverse.js#L75-L148
// the @types/estraverse package does not include this enum of node types

S.LOOP_NODES = [S.WhileStatement, S.DoWhileStatement, S.ForStatement];
enum S {
AssignmentExpression = "AssignmentExpression",
AssignmentPattern = "AssignmentPattern",
ArrayExpression = "ArrayExpression",
ArrayPattern = "ArrayPattern",
ArrowFunctionExpression = "ArrowFunctionExpression",
AwaitExpression = "AwaitExpression", // CAUTION: It's deferred to ES7.
BlockStatement = "BlockStatement",
BinaryExpression = "BinaryExpression",
BreakStatement = "BreakStatement",
CallExpression = "CallExpression",
CatchClause = "CatchClause",
ClassBody = "ClassBody",
ClassDeclaration = "ClassDeclaration",
ClassExpression = "ClassExpression",
ComprehensionBlock = "ComprehensionBlock", // CAUTION: It's deferred to ES7.
ComprehensionExpression = "ComprehensionExpression", // CAUTION: It's deferred to ES7.
ConditionalExpression = "ConditionalExpression",
ContinueStatement = "ContinueStatement",
DebuggerStatement = "DebuggerStatement",
DirectiveStatement = "DirectiveStatement",
DoWhileStatement = "DoWhileStatement",
EmptyStatement = "EmptyStatement",
ExportAllDeclaration = "ExportAllDeclaration",
ExportDefaultDeclaration = "ExportDefaultDeclaration",
ExportNamedDeclaration = "ExportNamedDeclaration",
ExportSpecifier = "ExportSpecifier",
ExpressionStatement = "ExpressionStatement",
ForStatement = "ForStatement",
ForInStatement = "ForInStatement",
ForOfStatement = "ForOfStatement",
FunctionDeclaration = "FunctionDeclaration",
FunctionExpression = "FunctionExpression",
GeneratorExpression = "GeneratorExpression", // CAUTION: It"s deferred to ES7.
Identifier = "Identifier",
IfStatement = "IfStatement",
ImportExpression = "ImportExpression",
ImportDeclaration = "ImportDeclaration",
ImportDefaultSpecifier = "ImportDefaultSpecifier",
ImportNamespaceSpecifier = "ImportNamespaceSpecifier",
ImportSpecifier = "ImportSpecifier",
Literal = "Literal",
LabeledStatement = "LabeledStatement",
LogicalExpression = "LogicalExpression",
MemberExpression = "MemberExpression",
MetaProperty = "MetaProperty",
MethodDefinition = "MethodDefinition",
ModuleSpecifier = "ModuleSpecifier",
NewExpression = "NewExpression",
ObjectExpression = "ObjectExpression",
ObjectPattern = "ObjectPattern",
Program = "Program",
Property = "Property",
RestElement = "RestElement",
ReturnStatement = "ReturnStatement",
SequenceExpression = "SequenceExpression",
SpreadElement = "SpreadElement",
Super = "Super",
SwitchStatement = "SwitchStatement",
SwitchCase = "SwitchCase",
TaggedTemplateExpression = "TaggedTemplateExpression",
TemplateElement = "TemplateElement",
TemplateLiteral = "TemplateLiteral",
ThisExpression = "ThisExpression",
ThrowStatement = "ThrowStatement",
TryStatement = "TryStatement",
UnaryExpression = "UnaryExpression",
UpdateExpression = "UpdateExpression",
VariableDeclaration = "VariableDeclaration",
VariableDeclarator = "VariableDeclarator",
WhileStatement = "WhileStatement",
WithStatement = "WithStatement",
YieldExpression = "YieldExpression",
};

S.TEST_NODES = [S.IfStatement, S.ConditionalExpression, S.SwitchCase];
export const LOOP_NODES = [S.WhileStatement, S.DoWhileStatement, S.ForStatement];

S.FUNC_NODES = [
export const TEST_NODES = [S.IfStatement, S.ConditionalExpression, S.SwitchCase];

export const FUNC_NODES = [
S.FunctionDeclaration,
S.FunctionExpression,
S.ArrowFunctionExpression,
];

export default S;
export default S;
4 changes: 2 additions & 2 deletions src/mutators/conditional-test-always.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as R from "ramda";
import S from "./_syntax";
import { TEST_NODES } from "./_syntax";
import { TRUE_NODE } from "./_constant-nodes";
import * as util from "./util";
import { MutatorPlugin } from "../types";
Expand All @@ -8,7 +8,7 @@ import { hasProp } from "./_filters";
const plugin: MutatorPlugin = {
type: "mutator",
name: "conditional-test-always",
nodeTypes: S.TEST_NODES,
nodeTypes: TEST_NODES,
filter: hasProp("test"),
mutator: util.update("test", R.always(TRUE_NODE)),
};
Expand Down
Loading

0 comments on commit efe86fd

Please sign in to comment.