diff --git a/lib/instrumenter.js b/lib/instrumenter.js index 684ba67..90f316f 100644 --- a/lib/instrumenter.js +++ b/lib/instrumenter.js @@ -15,9 +15,13 @@ class Instrumenter { constructor(config={}){ this.instrumentationData = {}; this.injector = new Injector(); - this.measureStatementCoverage = (config.measureStatementCoverage === false) ? false : true; - this.measureFunctionCoverage = (config.measureFunctionCoverage === false) ? false: true; - this.measureModifierCoverage = (config.measureModifierCoverage === false) ? false: true; + this.enabled = { + statements: (config.measureStatementCoverage === false) ? false : true, + functions: (config.measureFunctionCoverage === false) ? false: true, + modifiers: (config.measureModifierCoverage === false) ? false: true, + branches: (config.measureBranchCoverage === false) ? false: true, + lines: (config.measureLineCoverage === false) ? false: true + }; } _isRootNode(node){ @@ -58,9 +62,7 @@ class Instrumenter { const contract = {}; this.injector.resetModifierMapping(); - parse.configureStatementCoverage(this.measureStatementCoverage) - parse.configureFunctionCoverage(this.measureFunctionCoverage) - parse.configureModifierCoverage(this.measureModifierCoverage) + parse.configure(this.enabled); contract.source = contractSource; contract.instrumented = contractSource; diff --git a/lib/parse.js b/lib/parse.js index 5f14823..b7e7c02 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -11,16 +11,8 @@ const FILE_SCOPED_ID = "fileScopedId"; const parse = {}; // Utilities -parse.configureStatementCoverage = function(val){ - register.measureStatementCoverage = val; -} - -parse.configureFunctionCoverage = function(val){ - register.measureFunctionCoverage = val; -} - -parse.configureModifierCoverage = function(val){ - register.measureModifierCoverage = val; +parse.configure = function(_enabled){ + register.enabled = Object.assign(register.enabled, _enabled); } // Nodes diff --git a/lib/registrar.js b/lib/registrar.js index 1c2dcb5..328f25b 100644 --- a/lib/registrar.js +++ b/lib/registrar.js @@ -12,9 +12,13 @@ class Registrar { this.trackStatements = true; // These are set by user option and enable/disable the measurement completely - this.measureStatementCoverage = true; - this.measureFunctionCoverage = true; - this.measureModifierCoverage = true; + this.enabled = { + statements: true, + functions: true, + modifiers: true, + branches: true, + lines: true + } } /** @@ -37,7 +41,7 @@ class Registrar { * @param {Object} expression AST node */ statement(contract, expression) { - if (!this.trackStatements || !this.measureStatementCoverage) return; + if (!this.trackStatements || !this.enabled.statements) return; const startContract = contract.instrumented.slice(0, expression.range[0]); const startline = ( startContract.match(/\n/g) || [] ).length + 1; @@ -77,6 +81,8 @@ class Registrar { * @param {Object} expression AST node */ line(contract, expression) { + if (!this.enabled.lines) return; + const startchar = expression.range[0]; const endchar = expression.range[1] + 1; const lastNewLine = contract.instrumented.slice(0, startchar).lastIndexOf('\n'); @@ -108,7 +114,7 @@ class Registrar { * @param {Object} expression AST node */ functionDeclaration(contract, expression) { - if (!this.measureFunctionCoverage) return; + if (!this.enabled.functions) return; let start = 0; contract.fnId += 1; @@ -123,7 +129,7 @@ class Registrar { } // Add modifier branch coverage - if (!this.measureModifierCoverage) continue; + if (!this.enabled.modifiers) continue; this.addNewModifierBranch(contract, modifier); this._createInjectionPoint( @@ -342,6 +348,8 @@ class Registrar { }; conditional(contract, expression){ + if (!this.enabled.branches) return; + this.addNewConditionalBranch(contract, expression); // Double open parens @@ -388,6 +396,8 @@ class Registrar { * @param {Number} injectionIdx pre/post branch index (left=0, right=1) */ logicalOR(contract, expression) { + if (!this.enabled.branches) return; + this.addNewLogicalORBranch(contract, expression); // Left @@ -433,6 +443,8 @@ class Registrar { * @param {Object} expression AST node */ requireBranch(contract, expression) { + if (!this.enabled.branches) return; + this.addNewBranch(contract, expression); this._createInjectionPoint( contract, @@ -458,6 +470,8 @@ class Registrar { * @param {Object} expression AST node */ ifStatement(contract, expression) { + if (!this.enabled.branches) return; + this.addNewBranch(contract, expression); if (expression.trueBody.type === 'Block') { diff --git a/lib/validator.js b/lib/validator.js index d41fd20..e3f6a9a 100644 --- a/lib/validator.js +++ b/lib/validator.js @@ -21,8 +21,10 @@ const configSchema = { autoLaunchServer: {type: "boolean"}, istanbulFolder: {type: "string"}, measureStatementCoverage: {type: "boolean"}, - measureFunctionCoverage: {type: "boolean"}, - measureModifierCoverage: {type: "boolean"}, + measureFunctionCoverage: {type: "boolean"}, + measureModifierCoverage: {type: "boolean"}, + measureLineCoverage: {type: "boolean"}, + measureBranchCoverage: {type: "boolean"}, // Hooks: onServerReady: {type: "function", format: "isFunction"}, diff --git a/test/units/options.js b/test/units/options.js new file mode 100644 index 0000000..a2d37e2 --- /dev/null +++ b/test/units/options.js @@ -0,0 +1,121 @@ +const assert = require('assert'); +const util = require('./../util/util.js'); + +const client = require('ganache-cli'); +const Coverage = require('./../../lib/coverage'); +const Api = require('./../../lib/api') + +describe('measureCoverage options', () => { + let coverage; + let api; + + before(async () => { + api = new Api({silent: true}); + await api.ganache(client); + }) + beforeEach(() => { + api.config = {} + coverage = new Coverage() + }); + after(async() => await api.finish()); + + async function setupAndRun(solidityFile, val){ + const contract = await util.bootstrapCoverage(solidityFile, api); + coverage.addContract(contract.instrumented, util.filePath); + + /* some methods intentionally fail */ + try { + (val) + ? await contract.instance.a(val) + : await contract.instance.a(); + } catch(e){} + + return coverage.generate(contract.data, util.pathPrefix); + } + + // if (x == 1 || x == 2) { } else ... + it('should ignore OR branches when measureBranchCoverage = false', async function() { + api.config.measureBranchCoverage = false; + const mapping = await setupAndRun('or/if-or', 1); + + assert.deepEqual(mapping[util.filePath].l, { + 5: 1, 8: 0 + }); + assert.deepEqual(mapping[util.filePath].b, {}); + assert.deepEqual(mapping[util.filePath].s, { + 1: 1, 2: 0, + }); + assert.deepEqual(mapping[util.filePath].f, { + 1: 1, + }); + }); + + it('should ignore if/else branches when measureBranchCoverage = false', async function() { + api.config.measureBranchCoverage = false; + const mapping = await setupAndRun('if/if-with-brackets', 1); + + assert.deepEqual(mapping[util.filePath].l, { + 5: 1, + }); + assert.deepEqual(mapping[util.filePath].b, {}); + assert.deepEqual(mapping[util.filePath].s, { + 1: 1, 2: 1, + }); + assert.deepEqual(mapping[util.filePath].f, { + 1: 1, + }); + }); + + it('should ignore ternary conditionals when measureBranchCoverage = false', async function() { + api.config.measureBranchCoverage = false; + const mapping = await setupAndRun('conditional/sameline-consequent'); + + assert.deepEqual(mapping[util.filePath].l, { + 5: 1, 6: 1, 7: 1, + }); + assert.deepEqual(mapping[util.filePath].b, {}); + + assert.deepEqual(mapping[util.filePath].s, { + 1: 1, 2: 1, 3: 1, + }); + assert.deepEqual(mapping[util.filePath].f, { + 1: 1, + }); + }); + + it('should ignore modifier branches when measureModifierCoverage = false', async function() { + api.config.measureModifierCoverage = false; + const mapping = await setupAndRun('modifiers/same-contract-pass'); + + assert.deepEqual(mapping[util.filePath].l, { + 5: 1, 6: 1, 10: 1, + }); + assert.deepEqual(mapping[util.filePath].b, { // Source contains a `require` + 1: [1, 0] + }); + assert.deepEqual(mapping[util.filePath].s, { + 1: 1, 2: 1, + }); + assert.deepEqual(mapping[util.filePath].f, { + 1: 1, 2: 1 + }); + }); + + it('should ignore statements when measureStatementCoverage = false', async function() { + api.config.measureStatementCoverage = false; + const mapping = await setupAndRun('modifiers/same-contract-pass'); + assert.deepEqual(mapping[util.filePath].s, {}); + }); + + it('should ignore lines when measureLineCoverage = false', async function() { + api.config.measureLineCoverage = false; + const mapping = await setupAndRun('modifiers/same-contract-pass'); + assert.deepEqual(mapping[util.filePath].l, {}); + }); + + it('should ignore functions when measureFunctionCoverage = false', async function() { + api.config.measureFunctionCoverage = false; + const mapping = await setupAndRun('modifiers/same-contract-pass'); + assert.deepEqual(mapping[util.filePath].f, {}); + }); +}); diff --git a/test/units/validator.js b/test/units/validator.js index a3a6f99..bb81f68 100644 --- a/test/units/validator.js +++ b/test/units/validator.js @@ -48,7 +48,9 @@ describe('config validation', () => { "autoLaunchServer", "measureStatementCoverage", "measureFunctionCoverage", - "measureModifierCoverage" + "measureModifierCoverage", + "measureBranchCoverage", + "measureLineCoverage" ] options.forEach(name => { diff --git a/test/util/util.js b/test/util/util.js index 31e6a13..aa306a9 100644 --- a/test/util/util.js +++ b/test/util/util.js @@ -71,9 +71,9 @@ function codeToCompilerInput(code) { // ============================ // Instrumentation Correctness // ============================ -function instrumentAndCompile(sourceName) { +function instrumentAndCompile(sourceName, api={}) { const contract = getCode(`${sourceName}.sol`) - const instrumenter = new Instrumenter(); + const instrumenter = new Instrumenter(api.config); const instrumented = instrumenter.instrument(contract, filePath); return { @@ -97,7 +97,7 @@ function report(output=[]) { // Coverage Correctness // ===================== async function bootstrapCoverage(file, api){ - const info = instrumentAndCompile(file); + const info = instrumentAndCompile(file, api); info.instance = await getDeployedContractInstance(info, api.server.provider); api.collector._setInstrumentationData(info.data); return info;