From f1389eecdae654f61002b64ca354bd42358a6c54 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Mon, 24 Jul 2017 18:18:20 +0100 Subject: [PATCH] First stab at instrumenting asserts --- lib/coverageMap.js | 33 +++++++++++++++++- lib/injector.js | 16 +++++++++ lib/instrumenter.js | 32 +++++++++++++++++ lib/parse.js | 3 ++ test/assert.js | 63 ++++++++++++++++++++++++++++++++++ test/sources/assert/Assert.sol | 7 ++++ 6 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 test/assert.js create mode 100644 test/sources/assert/Assert.sol diff --git a/lib/coverageMap.js b/lib/coverageMap.js index 813c891..a3cec78 100644 --- a/lib/coverageMap.js +++ b/lib/coverageMap.js @@ -17,10 +17,13 @@ module.exports = class CoverageMap { constructor() { this.coverage = {}; + this.assertCoverage = {}; this.lineTopics = []; this.functionTopics = []; this.branchTopics = []; this.statementTopics = []; + this.assertPreTopics = []; + this.assertPostTopics = []; } /** @@ -42,6 +45,7 @@ module.exports = class CoverageMap { statementMap: {}, branchMap: {}, }; + this.assertCoverage[canonicalContractPath] = { }; info.runnableLines.forEach((item, idx) => { this.coverage[canonicalContractPath].l[info.runnableLines[idx]] = 0; @@ -53,6 +57,10 @@ module.exports = class CoverageMap { this.coverage[canonicalContractPath].branchMap = info.branchMap; for (let x = 1; x <= Object.keys(info.branchMap).length; x++) { this.coverage[canonicalContractPath].b[x] = [0, 0]; + this.assertCoverage[canonicalContractPath][x] = { + preEvents: 0, + postEvents: 0, + }; } this.coverage[canonicalContractPath].statementMap = info.statementMap; for (let x = 1; x <= Object.keys(info.statementMap).length; x++) { @@ -69,13 +77,17 @@ module.exports = class CoverageMap { const fnHash = keccakhex('__FunctionCoverage' + info.contractName + '(string,uint256)'); const branchHash = keccakhex('__BranchCoverage' + info.contractName + '(string,uint256,uint256)'); const statementHash = keccakhex('__StatementCoverage' + info.contractName + '(string,uint256)'); + const assertPreHash = keccakhex('__AssertPreCoverage' + info.contractName + '(string,uint256)'); + const assertPostHash = keccakhex('__AssertPostCoverage' + info.contractName + '(string,uint256)'); this.lineTopics.push(lineHash); this.functionTopics.push(fnHash); this.branchTopics.push(branchHash); this.statementTopics.push(statementHash); + this.assertPreTopics.push(assertPreHash); + this.assertPostTopics.push(assertPostHash); - const topics = `${lineHash}\n${fnHash}\n${branchHash}\n${statementHash}\n`; + const topics = `${lineHash}\n${fnHash}\n${branchHash}\n${statementHash}\n${assertPreHash}\n${assertPostHash}`; fs.appendFileSync('./scTopics', topics); } @@ -106,8 +118,27 @@ module.exports = class CoverageMap { const data = SolidityCoder.decodeParams(['string', 'uint256'], event.data.replace('0x', '')); const canonicalContractPath = data[0]; this.coverage[canonicalContractPath].s[data[1].toNumber()] += 1; + } else if (event.topics.filter(t => this.assertPreTopics.indexOf(t) >= 0).length > 0) { + const data = SolidityCoder.decodeParams(['string', 'uint256'], event.data.replace('0x', '')); + const canonicalContractPath = data[0]; + this.assertCoverage[canonicalContractPath][data[1].toNumber()].preEvents += 1; + } else if (event.topics.filter(t => this.assertPostTopics.indexOf(t) >= 0).length > 0) { + const data = SolidityCoder.decodeParams(['string', 'uint256'], event.data.replace('0x', '')); + const canonicalContractPath = data[0]; + this.assertCoverage[canonicalContractPath][data[1].toNumber()].postEvents += 1; } } + // Finally, interpret the assert pre/post events + Object.keys(this.assertCoverage).forEach(contractPath => { + const contract = this.coverage[contractPath]; + for (let i = 1; i <= Object.keys(contract.b).length; i++) { + const branch = this.assertCoverage[contractPath][i]; + if (branch.preEvents > 0) { + // Then it was an assert branch. + this.coverage[contractPath].b[i] = [branch.postEvents, branch.preEvents - branch.postEvents]; + } + } + }); return Object.assign({}, this.coverage); } }; diff --git a/lib/injector.js b/lib/injector.js index d3f687d..4105768 100644 --- a/lib/injector.js +++ b/lib/injector.js @@ -30,6 +30,19 @@ injector.callEmptyBranchEvent = function injectCallEmptyBranchEvent(contract, fi contract.instrumented.slice(injectionPoint); }; + +injector.callAssertPreEvent = function callAssertPreEvent(contract, fileName, injectionPoint, injection) { + contract.instrumented = contract.instrumented.slice(0, injectionPoint) + + '__AssertPreCoverage' + contract.contractName + '(\'' + fileName + '\',' + injection.branchId + ');\n' + + contract.instrumented.slice(injectionPoint); +}; + +injector.callAssertPostEvent = function callAssertPostEvent(contract, fileName, injectionPoint, injection) { + contract.instrumented = contract.instrumented.slice(0, injectionPoint) + + '__AssertPostCoverage' + contract.contractName + '(\'' + fileName + '\',' + injection.branchId + ');\n' + + contract.instrumented.slice(injectionPoint); +}; + injector.openParen = function injectOpenParen(contract, fileName, injectionPoint, injection) { contract.instrumented = contract.instrumented.slice(0, injectionPoint) + '(' + contract.instrumented.slice(injectionPoint); }; @@ -54,6 +67,9 @@ injector.eventDefinition = function injectEventDefinition(contract, fileName, in 'event __FunctionCoverage' + contract.contractName + '(string fileName, uint256 fnId);\n' + 'event __StatementCoverage' + contract.contractName + '(string fileName, uint256 statementId);\n' + 'event __BranchCoverage' + contract.contractName + '(string fileName, uint256 branchId, uint256 locationIdx);\n' + + 'event __AssertPreCoverage' + contract.contractName + '(string fileName, uint256 branchId);\n' + + 'event __AssertPostCoverage' + contract.contractName + '(string fileName, uint256 branchId);\n' + + contract.instrumented.slice(injectionPoint); }; diff --git a/lib/instrumenter.js b/lib/instrumenter.js index 024b4d8..389549e 100644 --- a/lib/instrumenter.js +++ b/lib/instrumenter.js @@ -186,6 +186,38 @@ instrumenter.instrumentFunctionDeclaration = function instrumentFunctionDeclarat } }; +instrumenter.instrumentAssertOrRequire = function instrumentAssertOrRequire(contract, expression){ + contract.branchId += 1; + const startline = (contract.instrumented.slice(0, expression.start).match(/\n/g) || []).length + 1; + const startcol = expression.start - contract.instrumented.slice(0, expression.start).lastIndexOf('\n') - 1; + // NB locations for if branches in istanbul are zero length and associated with the start of the if. + contract.branchMap[contract.branchId] = { + line: startline, + type: 'if', + locations: [{ + start: { + line: startline, column: startcol, + }, + end: { + line: startline, column: startcol, + }, + }, { + start: { + line: startline, column: startcol, + }, + end: { + line: startline, column: startcol, + }, + }], + }; + createOrAppendInjectionPoint(contract, expression.start, { + type: 'callAssertPreEvent', branchId: contract.branchId, + }); + createOrAppendInjectionPoint(contract, expression.end + 1, { + type: 'callAssertPostEvent', branchId: contract.branchId, + }); +} + instrumenter.instrumentIfStatement = function instrumentIfStatement(contract, expression) { contract.branchId += 1; const startline = (contract.instrumented.slice(0, expression.start).match(/\n/g) || []).length + 1; diff --git a/lib/parse.js b/lib/parse.js index ac16916..3ac5fd4 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -27,6 +27,9 @@ parse.CallExpression = function parseCallExpression(contract, expression) { // In any given chain of call expressions, only the head callee is an Identifier node if (expression.callee.type === 'Identifier') { instrumenter.instrumentStatement(contract, expression); + if (expression.callee.name === 'assert' || expression.callee.name === 'require') { + instrumenter.instrumentAssertOrRequire(contract, expression); + } parse[expression.callee.type] && parse[expression.callee.type](contract, expression.callee); } else { diff --git a/test/assert.js b/test/assert.js new file mode 100644 index 0000000..e0c21bd --- /dev/null +++ b/test/assert.js @@ -0,0 +1,63 @@ +/* eslint-env node, mocha */ + +const path = require('path'); +const getInstrumentedVersion = require('./../lib/instrumentSolidity.js'); +const util = require('./util/util.js'); +const CoverageMap = require('./../lib/coverageMap'); +const vm = require('./util/vm'); +const assert = require('assert'); + +describe('asserts and requires', () => { + const filePath = path.resolve('./test.sol'); + const pathPrefix = './'; + + before(() => process.env.NO_EVENTS_FILTER = true); + + it.only('should cover assert statements as if they are if statements when they pass', done => { + const contract = util.getCode('assert/Assert.sol'); + const info = getInstrumentedVersion(contract, filePath); + const coverage = new CoverageMap(); + coverage.addContract(info, filePath); + + vm.execute(info.contract, 'a', [true]).then(events => { + const mapping = coverage.generate(events, pathPrefix); + assert.deepEqual(mapping[filePath].l, { + 5: 1, + }); + assert.deepEqual(mapping[filePath].b, { + 1: [1, 0], + }); + assert.deepEqual(mapping[filePath].s, { + 1: 1, + }); + assert.deepEqual(mapping[filePath].f, { + 1: 1, + }); + done(); + }).catch(done); + }); + + it.only('should cover assert statements as if they are if statements when they fail', done => { + const contract = util.getCode('assert/Assert.sol'); + const info = getInstrumentedVersion(contract, filePath); + const coverage = new CoverageMap(); + coverage.addContract(info, filePath); + + vm.execute(info.contract, 'a', [false]).then(events => { + const mapping = coverage.generate(events, pathPrefix); + assert.deepEqual(mapping[filePath].l, { + 5: 1, + }); + assert.deepEqual(mapping[filePath].b, { + 1: [0, 1], + }); + assert.deepEqual(mapping[filePath].s, { + 1: 1, + }); + assert.deepEqual(mapping[filePath].f, { + 1: 1, + }); + done(); + }).catch(done); + }); +}); diff --git a/test/sources/assert/Assert.sol b/test/sources/assert/Assert.sol new file mode 100644 index 0000000..2f5b1d5 --- /dev/null +++ b/test/sources/assert/Assert.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.4.13; + +contract Test { + function a(bool test){ + assert(test); + } +}