From efe86fd30d80a5990b7e02f789aad7427dcfbea5 Mon Sep 17 00:00:00 2001 From: Nick Bottomley Date: Thu, 10 Oct 2019 05:33:15 +0000 Subject: [PATCH] miscellaneous improvements - 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 --- .nvmrc | 1 + Makefile | 2 +- TODO | 2 + package.json | 4 +- src/cli.ts | 6 +- src/comments.ts | 7 +- src/filters/filter.ts | 75 ++++++++++-------- src/index.ts | 17 +++- src/make-mutants.ts | 10 +-- src/mutators/_syntax.ts | 86 +++++++++++++++++++-- src/mutators/conditional-test-always.ts | 4 +- src/mutators/conditional-test-invert.ts | 4 +- src/mutators/conditional-test-never.ts | 4 +- src/mutators/drop-parameter-default.ts | 4 +- src/mutators/reverse-function-parameters.ts | 4 +- src/reporters/helpers.ts | 21 +++-- src/reporters/index.ts | 2 + src/reporters/quiet.ts | 20 +++++ src/types.ts | 18 ++++- src/util/timer.ts | 27 +++++++ test/filters/filter.js | 42 ++++++++-- tsconfig.json | 19 +++-- yarn.lock | 16 ++-- 23 files changed, 304 insertions(+), 91 deletions(-) create mode 100644 .nvmrc create mode 100644 src/reporters/quiet.ts create mode 100644 src/util/timer.ts diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..ba247e6 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +12.5.0 \ No newline at end of file diff --git a/Makefile b/Makefile index b9b3f7b..f33bd64 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/TODO b/TODO index 14f61a4..25d359b 100644 --- a/TODO +++ b/TODO @@ -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 \ No newline at end of file diff --git a/package.json b/package.json index 5b0385a..71ef16c 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", @@ -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", diff --git a/src/cli.ts b/src/cli.ts index 398b955..709d3c8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -42,6 +42,7 @@ program parseInt, ) .option("-u, --runner ", "name of runner or runner plugin") + .option("--reporter ", "which reporter to use") .parse(process.argv); if (program.rootDir && program.rootDir[0] !== "/") { @@ -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, }); @@ -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 }`, ); } diff --git a/src/comments.ts b/src/comments.ts index ba7bda6..f07dc2e 100644 --- a/src/comments.ts +++ b/src/comments.ts @@ -1,4 +1,3 @@ -import * as R from "ramda"; import * as ESTree from "estree"; const ENABLING_COMMENT = "perturb-enable:"; @@ -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; } diff --git a/src/filters/filter.ts b/src/filters/filter.ts index eefb910..ed48e67 100644 --- a/src/filters/filter.ts +++ b/src/filters/filter.ts @@ -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( diff --git a/src/index.ts b/src/index.ts index 1ae03d2..f30392c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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); @@ -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; }; } diff --git a/src/make-mutants.ts b/src/make-mutants.ts index 32af4a5..a65a06a 100644 --- a/src/make-mutants.ts +++ b/src/make-mutants.ts @@ -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"; @@ -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 @@ -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, @@ -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]); diff --git a/src/mutators/_syntax.ts b/src/mutators/_syntax.ts index d28eec2..32bc919 100644 --- a/src/mutators/_syntax.ts +++ b/src/mutators/_syntax.ts @@ -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; \ No newline at end of file diff --git a/src/mutators/conditional-test-always.ts b/src/mutators/conditional-test-always.ts index a2982fd..c06efe6 100644 --- a/src/mutators/conditional-test-always.ts +++ b/src/mutators/conditional-test-always.ts @@ -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"; @@ -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)), }; diff --git a/src/mutators/conditional-test-invert.ts b/src/mutators/conditional-test-invert.ts index 67c371d..5c73fa1 100644 --- a/src/mutators/conditional-test-invert.ts +++ b/src/mutators/conditional-test-invert.ts @@ -1,6 +1,6 @@ import * as ESTree from "estree"; -import S from "./_syntax"; +import S, { TEST_NODES } from "./_syntax"; import * as util from "./util"; import { hasProp } from "./_filters"; import { MutatorPlugin } from "../types"; @@ -14,7 +14,7 @@ const BANG = "!"; const mutator: MutatorPlugin = { type: "mutator", name: "conditional-test-invert", - nodeTypes: S.TEST_NODES, + nodeTypes: TEST_NODES, filter: hasProp("test"), mutator: util.update("test", (test: ESTree.Node) => ({ type: S.UnaryExpression, diff --git a/src/mutators/conditional-test-never.ts b/src/mutators/conditional-test-never.ts index 379373b..dbecb41 100644 --- a/src/mutators/conditional-test-never.ts +++ b/src/mutators/conditional-test-never.ts @@ -1,5 +1,5 @@ import * as R from "ramda"; -import S from "./_syntax"; +import { LOOP_NODES, TEST_NODES } from "./_syntax"; import * as util from "./util"; import { FALSE_NODE } from "./_constant-nodes"; import { hasProp } from "./_filters"; @@ -8,7 +8,7 @@ import { MutatorPlugin } from "../types"; const plugin: MutatorPlugin = { type: "mutator", name: "conditional-test-never", - nodeTypes: S.LOOP_NODES.concat(S.TEST_NODES), + nodeTypes: LOOP_NODES.concat(TEST_NODES), filter: hasProp("test"), mutator: util.update("test", R.always(FALSE_NODE)), }; diff --git a/src/mutators/drop-parameter-default.ts b/src/mutators/drop-parameter-default.ts index 6867038..1da39a0 100644 --- a/src/mutators/drop-parameter-default.ts +++ b/src/mutators/drop-parameter-default.ts @@ -1,5 +1,5 @@ import * as R from "ramda"; -import S from "./_syntax"; +import S, { FUNC_NODES } from "./_syntax"; import { VOID_NODE } from "./_constant-nodes"; import { MutatorPlugin } from "../types"; @@ -10,7 +10,7 @@ import { MutatorPlugin } from "../types"; const plugin: MutatorPlugin = { type: "mutator", name: "drop-return", - nodeTypes: S.FUNC_NODES, + nodeTypes: FUNC_NODES, mutator: R.ifElse( node => node.argument == null, R.always(VOID_NODE), diff --git a/src/mutators/reverse-function-parameters.ts b/src/mutators/reverse-function-parameters.ts index 3825e8d..1ecf87a 100644 --- a/src/mutators/reverse-function-parameters.ts +++ b/src/mutators/reverse-function-parameters.ts @@ -1,4 +1,4 @@ -import S from "./_syntax"; +import { FUNC_NODES } from "./_syntax"; import * as util from "./util"; import { MutatorPlugin } from "../types"; @@ -7,7 +7,7 @@ import { MutatorPlugin } from "../types"; const plugin: MutatorPlugin = { type: "mutator", name: "reverse-function-parameters", - nodeTypes: S.FUNC_NODES, + nodeTypes: FUNC_NODES, filter: util.lengthAtPropGreaterThan("params", 1), mutator: util.update("params", (ps: any[]) => ps.slice().reverse()), }; diff --git a/src/reporters/helpers.ts b/src/reporters/helpers.ts index e843be0..091f0e9 100644 --- a/src/reporters/helpers.ts +++ b/src/reporters/helpers.ts @@ -1,5 +1,6 @@ import * as fs from "fs"; import * as path from "path"; +import * as R from "ramda"; import { RunnerResult, PerturbConfig } from "../types"; @@ -7,6 +8,7 @@ export interface Stats { total: number; killed: number; rate: number; + timestamp: number; } export function stats(results: RunnerResult[]): Stats { @@ -16,6 +18,7 @@ export function stats(results: RunnerResult[]): Stats { total, killed, rate: Number((killed / total).toFixed(4)) * 100, + timestamp: Date.now(), }; } @@ -33,10 +36,13 @@ export function identifier(r: RunnerResult, includeErrorMessage = false) { // configurable? const RECORD_FILE = path.join(__dirname, "../../run-record.json"); -function writeResult(last: any, s: Stats, root: string) { - last[root] = s; +type RunRecord = { [project: string]: Stats[] } + +function writeResult(record: RunRecord, s: Stats, root: string) { + if (record[root] == null) record[root] = [] + record[root].push(s) try { - fs.writeFileSync(RECORD_FILE, JSON.stringify(last)); + fs.writeFileSync(RECORD_FILE, JSON.stringify(record)); } catch (e) { // log error? } @@ -44,14 +50,15 @@ function writeResult(last: any, s: Stats, root: string) { export function delta(rs: RunnerResult[], cfg: PerturbConfig) { const s = stats(rs); - let record: any = {}; + let record: RunRecord = {}; try { record = require(RECORD_FILE); - if (record[cfg.projectRoot]) { - console.log("change in total:", s.total - record[cfg.projectRoot].total); + if (record[cfg.projectRoot] && record[cfg.projectRoot].length) { + const prev: Stats = R.last(record[cfg.projectRoot])! + console.log("change in total:", s.total - prev.total); console.log( "change in killed:", - s.killed - record[cfg.projectRoot].killed, + s.killed - prev.killed, ); } } catch (e) { diff --git a/src/reporters/index.ts b/src/reporters/index.ts index 5cb03a7..44210de 100644 --- a/src/reporters/index.ts +++ b/src/reporters/index.ts @@ -1,11 +1,13 @@ import diffReporter from "./diff"; import nameReporter from "./name"; +import quietReporter from "./quiet"; import { ReporterPlugin } from "../types"; const plugins = new Map([ [diffReporter.name, diffReporter], [nameReporter.name, nameReporter], + [quietReporter.name, quietReporter], ]); export default function get(input: string | ReporterPlugin): ReporterPlugin { diff --git a/src/reporters/quiet.ts b/src/reporters/quiet.ts new file mode 100644 index 0000000..a4201c0 --- /dev/null +++ b/src/reporters/quiet.ts @@ -0,0 +1,20 @@ +import { stats, delta } from "./helpers"; + +import { + ReporterPlugin, + RunnerResult, + PerturbConfig, + PerturbMetadata, +} from "../types"; + +const plugin: ReporterPlugin = { + name: "quiet", + type: "reporter", + onResult(r: RunnerResult) { }, + onFinish(rs: RunnerResult[], cfg: PerturbConfig, m?: PerturbMetadata) { + const { total, killed, rate } = stats(rs); + console.log(`Total: ${total}. Killed: ${killed}. Rate: ${rate}%`); + delta(rs, cfg); + }, +}; +export default plugin; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 0584520..952d7a1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,28 +1,43 @@ import * as ESTree from "estree"; +// todo enium export type PluginType = "reporter" | "runner" | "matcher" | "mutator"; +// export enum PluginTypeEnum { +// Reporter = "reporter", +// Runner = "runner", +// Matcher = "matcher", +// Mutator = "mutator", +// } + export interface BasePlugin { readonly name: string; readonly type: PluginType; } export interface ReporterPlugin extends BasePlugin { + type: "reporter"; readonly onResult: ResultReporter; readonly onFinish: AggregateReporter; } +export interface ReporterPluginConstructor { + new(ms: Mutant[]): ReporterPlugin; +} + export interface MutatorPlugin extends BasePlugin { + readonly type: "mutator"; readonly nodeTypes: string[]; readonly mutator: NodeMutator; readonly filter?: NodeFilter; } export interface RunnerPluginConstructor { - new (m: Mutant): RunnerPlugin; + new(m: Mutant): RunnerPlugin; } export interface RunnerPlugin extends BasePlugin { + readonly type: "runner"; setup(): Promise; run(): Promise; cleanup(): Promise; @@ -33,6 +48,7 @@ export interface SkipperPlugin extends BasePlugin { } export interface MatcherPlugin extends BasePlugin { + readonly type: "matcher"; readonly matchType: "generative" | "comparative"; makeMatcher: (c: PerturbConfig) => GenerativeMatcher | ComparativeMatcher; } diff --git a/src/util/timer.ts b/src/util/timer.ts new file mode 100644 index 0000000..c4a9540 --- /dev/null +++ b/src/util/timer.ts @@ -0,0 +1,27 @@ +type AnyFunction = (...args: any[]) => any; + +export default class Timer { + mapping: { [name: string]: number } + + constructor() { + this.mapping = {} + } + + wrap(name: string, f: F) { + if (this.mapping[name] != null) { + throw new Error(`already registered timing wrapper with name ${name}`) + } + this.mapping[name] = 0; + return (...args: Parameters): ReturnType => { + const start = Date.now(); + const result = f(...args); + const diff = Date.now() - start; + this.mapping[name] += diff; + return result; + } + } + + dump() { + return { ...this.mapping } + } +} \ No newline at end of file diff --git a/test/filters/filter.js b/test/filters/filter.js index 7a62cfc..ffec6d7 100644 --- a/test/filters/filter.js +++ b/test/filters/filter.js @@ -9,27 +9,49 @@ const callFn = "fn(1, 2, 3);" describe("filter", () => { + describe("#isStringRequire", () => { + const notRejected = [ + "require('abc', 123)", + "reqired('abc')", + "require(123)", + "require({})", + ] + + for (const code of notRejected) { + it(`rejects \`${code}\``, () => { + const node = extractExpression(parseToLocation(code)); + expect(filters.isStringRequire(node)).toBe(false); + }) + } + + it("accepts properly", () => { + const valid = "require('abc')"; + const node = extractExpression(parseToLocation(valid)); + expect(filters.isStringRequire(node)).toBe(true); + }) + }) + describe("#nodeSourceMatchesText", () => { it("works", () => { const loc = parseToLocation(esModuleInterop); - expect(filters.isESModuleInterop(loc)).toBe(true); + expect(filters.isESModuleInterop(loc.node)).toBe(true); }) }); describe("#isCallOfname", () => { it("works", () => { const loc = parseToLocation(callFn); - offsetLocation(loc, [ "expression" ]) - expect(filters.isCallOfName("fn")(loc)).toBe(true); - expect(filters.isCallOfName("fun")(loc)).toBe(false); + offsetLocation(loc, ["expression"]) + expect(filters.isCallOfName("fn")(loc.node)).toBe(true); + expect(filters.isCallOfName("fun")(loc.node)).toBe(false); }) }); }) -function parseToLocation (text) { +function parseToLocation(text) { const program = esprima.parseModule(text); if (program.body.length === 0) throw new Error(`Did not create program with zero body statements: ${text}`) - if (program.body.length === 0) throw new Error(`Created program withmore than one body statement: ${text}`) + if (program.body.length > 1) throw new Error(`Created program withmore than one body statement: ${text}`) return { mutator: null, // TODO: if we start using filters that check the mutator path: [], @@ -38,7 +60,13 @@ function parseToLocation (text) { } // helper to descend into the node to be on the right top level node for a test -function offsetLocation (loc, path) { +function offsetLocation(loc, path) { loc.node = R.path(path, loc.node); return loc; } + +function extractExpression({ node }) { + if (Object.keys(node).length !== 2) throw new Error("expected two keys"); + if (node.type !== "ExpressionStatement") throw new Error("expected type to be ExpressionStatement") + return node.expression; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index c83edd5..14cf505 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,15 +5,24 @@ "target": "es2017", "moduleResolution": "node", "allowJs": false, - "rootDirs": ["src"], + "rootDirs": [ + "src" + ], "outDir": "lib", "strict": true, "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, "noUnusedLocals": true, "strictNullChecks": true, - "lib": ["es2017"] + "lib": [ + "es2019" + ] }, - "include": ["src/**/*"], - "exclude": ["node_modules", "test"] -} + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "test" + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 8a73420..1ef77ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -125,6 +125,7 @@ "@types/estraverse@^0.0.6": version "0.0.6" resolved "https://registry.yarnpkg.com/@types/estraverse/-/estraverse-0.0.6.tgz#669f7cdf72ab797e6125f8d00fed33d4cf30c221" + integrity sha1-Zp9833KreX5hJfjQD+0z1M8wwiE= dependencies: "@types/estree" "*" @@ -158,10 +159,15 @@ version "5.0.0" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.0.0.tgz#a3014921991066193f6c8e47290d4d598dfd19e6" -"@types/node@*", "@types/node@^9.6.1": +"@types/node@*": version "9.6.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.1.tgz#e2d374ef15b315b48e7efc308fa1a7cd51faa06c" +"@types/node@^12.7.12": + version "12.7.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.12.tgz#7c6c571cc2f3f3ac4a59a5f2bd48f5bdbc8653cc" + integrity sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ== + "@types/ramda@^0.25.21": version "0.25.21" resolved "https://registry.yarnpkg.com/@types/ramda/-/ramda-0.25.21.tgz#3399ac49ea99253c7c308916960b65058a1d63c4" @@ -3292,10 +3298,10 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -typescript@^3.4.0-rc: - version "3.4.0-rc" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.0-rc.tgz#bc4026c3c517a30fa514b896f2684c6ba92caadb" - integrity sha512-wtm2GwuV0Yy2zvmpFaMDtWHnRbzfERi9FLpL+K+JqGkHoclgEQ6TmD1AMxMIjs80NWRIq0Xp1veQyuZbGsUYsg== +typescript@^3.7.0-beta: + version "3.7.0-beta" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.0-beta.tgz#4ad556e0eee14b90ecc39261001690e16e5eeba9" + integrity sha512-4jyCX+IQamrPJxgkABPq9xf+hUN+GWHVxoj+oey1TadCPa4snQl1RKwUba+1dyzYCamwlCxKvZQ3TjyWLhMGBA== uglify-js@^2.6: version "2.7.3"