First stab at instrumenting asserts

pull/79/head
Alex Rea 7 years ago
parent ca19b64d76
commit f1389eecda
  1. 33
      lib/coverageMap.js
  2. 16
      lib/injector.js
  3. 32
      lib/instrumenter.js
  4. 3
      lib/parse.js
  5. 63
      test/assert.js
  6. 7
      test/sources/assert/Assert.sol

@ -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);
}
};

@ -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);
};

@ -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;

@ -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 {

@ -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);
});
});

@ -0,0 +1,7 @@
pragma solidity ^0.4.13;
contract Test {
function a(bool test){
assert(test);
}
}
Loading…
Cancel
Save