Merge pull request #31 from JoinColony/PR30-redux

Start to add proper tests i.e. making sure coverage is as expected.
Closes #26 
Closes #27 

Has a known issue regarding highlighting (see #32).
uport
area 8 years ago committed by GitHub
commit e810429ddb
  1. 5
      circle.yml
  2. 92
      coverageMap.js
  3. 34
      instrumentSolidity.js
  4. 6
      package.json
  5. 56
      preprocessor.js
  6. 48
      runCoveredTests.js
  7. 167
      test/if.js
  8. 82
      test/loops.js
  9. 2
      test/sources/if/if-no-brackets-multiline.sol
  10. 2
      test/sources/if/if-no-brackets.sol
  11. 9
      test/sources/if/if-with-brackets-multiline.sol
  12. 2
      test/sources/if/if-with-brackets.sol
  13. 8
      test/sources/loops/for-no-brackets.sol
  14. 9
      test/sources/loops/for-with-brackets.sol
  15. 9
      test/sources/loops/while-no-brackets.sol
  16. 10
      test/sources/loops/while-with-brackets.sol
  17. 7
      test/statements.js
  18. 140
      test/util/vm.js

@ -1,3 +1,8 @@
machine:
node:
version: 6.9.1
dependencies: dependencies:
pre: pre:
- npm install -g truffle - npm install -g truffle
- rm -rf node_modules/

@ -0,0 +1,92 @@
/**
* This file contains methods that produce a coverage map to pass to instanbul
* from data generated by `instrumentSolidity.js`
*/
const SolidityCoder = require('web3/lib/solidity/coder.js');
const path = require('path');
const lineTopic = 'b8995a65f405d9756b41a334f38d8ff0c93c4934e170d3c1429c3e7ca101014d';
const functionTopic = 'd4ce765fd23c5cc3660249353d61ecd18ca60549dd62cb9ca350a4244de7b87f';
const branchTopic = 'd4cf56ed5ba572684f02f889f12ac42d9583c8e3097802060e949bfbb3c1bff5';
const statementTopic = 'b51abbff580b3a34bbc725f2dc6f736e9d4b45a41293fd0084ad865a31fde0c8';
/**
* Converts solcover event data into an object that can be
* be passed to instanbul to produce coverage reports.
* @type {CoverageMap}
*/
module.exports = class CoverageMap {
constructor() {
this.coverage = {};
}
/**
* Initializes a coverage map object for contract instrumented per `info` and located
* at `canonicalContractPath`
* @param {Object} info `info = getIntrumentedVersion(contract, fileName, true)`
* @param {String} canonicalContractPath target file location
* @return {Object} coverage map with all values set to zero
*/
addContract(info, canonicalContractPath) {
this.coverage[canonicalContractPath] = {
l: {},
path: canonicalContractPath,
s: {},
b: {},
f: {},
fnMap: {},
statementMap: {},
branchMap: {},
};
info.runnableLines.forEach((item, idx) => {
this.coverage[canonicalContractPath].l[info.runnableLines[idx]] = 0;
});
this.coverage[canonicalContractPath].fnMap = info.fnMap;
for (let x = 1; x <= Object.keys(info.fnMap).length; x++) {
this.coverage[canonicalContractPath].f[x] = 0;
}
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.coverage[canonicalContractPath].statementMap = info.statementMap;
for (let x = 1; x <= Object.keys(info.statementMap).length; x++) {
this.coverage[canonicalContractPath].s[x] = 0;
}
}
/**
* Populates an empty coverage map with values derived from an array of events
* fired by instrumented contracts as they are tested
* @param {Array} events
* @param {String} relative path to host contracts eg: './../contracts'
* @return {Object} coverage map.
*/
generate(events, pathPrefix) {
for (let idx = 0; idx < events.length; idx++) {
const event = JSON.parse(events[idx]);
if (event.topics.indexOf(lineTopic) >= 0) {
const data = SolidityCoder.decodeParams(['string', 'uint256'], event.data.replace('0x', ''));
const canonicalContractPath = path.resolve(pathPrefix + path.basename(data[0]));
this.coverage[canonicalContractPath].l[data[1].toNumber()] += 1;
} else if (event.topics.indexOf(functionTopic) >= 0) {
const data = SolidityCoder.decodeParams(['string', 'uint256'], event.data.replace('0x', ''));
const canonicalContractPath = path.resolve(pathPrefix + path.basename(data[0]));
this.coverage[canonicalContractPath].f[data[1].toNumber()] += 1;
} else if (event.topics.indexOf(branchTopic) >= 0) {
const data = SolidityCoder.decodeParams(['string', 'uint256', 'uint256'], event.data.replace('0x', ''));
const canonicalContractPath = path.resolve(pathPrefix + path.basename(data[0]));
this.coverage[canonicalContractPath].b[data[1].toNumber()][data[2].toNumber()] += 1;
} else if (event.topics.indexOf(statementTopic) >= 0) {
const data = SolidityCoder.decodeParams(['string', 'uint256'], event.data.replace('0x', ''));
const canonicalContractPath = path.resolve(pathPrefix + path.basename(data[0]));
this.coverage[canonicalContractPath].s[data[1].toNumber()] += 1;
}
}
return Object.assign({}, this.coverage);
}
};

@ -1,9 +1,12 @@
var SolidityParser = require("solidity-parser"); var SolidityParser = require("solidity-parser");
var preprocessor = require('./preprocessor');
//var solparse = require("solparse"); //var solparse = require("solparse");
var path = require("path"); var path = require("path");
module.exports = function(contract, fileName, instrumentingActive){ module.exports = function(contract, fileName, instrumentingActive){
contract = preprocessor.run(contract);
var result = SolidityParser.parse(contract); var result = SolidityParser.parse(contract);
//var result = solparse.parse(contract); //var result = solparse.parse(contract);
var instrumented = ""; var instrumented = "";
@ -100,10 +103,8 @@ module.exports = function(contract, fileName, instrumentingActive){
endcol = contract.slice(expressionContent.lastIndexOf('\n'), expression.end).length -1; endcol = contract.slice(expressionContent.lastIndexOf('\n'), expression.end).length -1;
}else{ }else{
endcol = startcol + expressionContent.length -1; endcol = startcol + expressionContent.length -1;
} }
statementMap[statementId] = {start:{line: startline, column:startcol},end:{line:endline, column:endcol}} statementMap[statementId] = {start:{line: startline, column:startcol},end:{line:endline, column:endcol}}
createOrAppendInjectionPoint(expression.start, {type:"statement", statementId: statementId}); createOrAppendInjectionPoint(expression.start, {type:"statement", statementId: statementId});
} }
@ -121,6 +122,8 @@ module.exports = function(contract, fileName, instrumentingActive){
// Is everything before us and after us on this line whitespace? // Is everything before us and after us on this line whitespace?
if (contract.slice(lastNewLine, startchar).trim().length===0 && contract.slice(endchar,nextNewLine).replace(';','').trim().length===0){ if (contract.slice(lastNewLine, startchar).trim().length===0 && contract.slice(endchar,nextNewLine).replace(';','').trim().length===0){
createOrAppendInjectionPoint(lastNewLine+1,{type:"callEvent"}); createOrAppendInjectionPoint(lastNewLine+1,{type:"callEvent"});
} else if (contract.slice(lastNewLine, startchar).replace('{','').trim().length===0 && contract.slice(endchar,nextNewLine).replace(/[;}]/g,'').trim().length===0){
createOrAppendInjectionPoint(expression.start,{type:"callEvent"});
} }
} }
@ -149,25 +152,18 @@ module.exports = function(contract, fileName, instrumentingActive){
var startcol = expression.start - contract.slice(0,expression.start).lastIndexOf('\n') -1; var startcol = expression.start - contract.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. //NB locations for if branches in istanbul are zero length and associated with the start of the if.
branchMap[branchId] = {line:linecount, type:'if', locations:[{start:{line:startline, column:startcol},end:{line:startline,column:startcol}},{start:{line:startline, column:startcol},end:{line:startline,column:startcol}}]} branchMap[branchId] = {line:linecount, type:'if', locations:[{start:{line:startline, column:startcol},end:{line:startline,column:startcol}},{start:{line:startline, column:startcol},end:{line:startline,column:startcol}}]}
if (contract.slice(expression.consequent.start,expression.consequent.end).trim().indexOf('{')===0){ if (expression.consequent.type === "BlockStatement"){
createOrAppendInjectionPoint(expression.consequent.start+1,{type: "callBranchEvent", branchId: branchId, locationIdx: 0} ) createOrAppendInjectionPoint(expression.consequent.start+1,{type: "callBranchEvent", branchId: branchId, locationIdx: 0} )
}else{
createOrAppendInjectionPoint(expression.consequent.start,{type: "callBranchEvent", branchId: branchId, locationIdx: 0, openBracket:true} )
createOrAppendInjectionPoint(expression.consequent.end, {type:"closeBracketStart"});
} }
if (expression.alternate && expression.alternate.type==='IfStatement'){ if (expression.alternate && expression.alternate.type==='IfStatement'){
createOrAppendInjectionPoint(expression.alternate.start, {type: "callBranchEvent", branchId: branchId, locationIdx:1, openBracket: true}) createOrAppendInjectionPoint(expression.alternate.start, {type: "callBranchEvent", branchId: branchId, locationIdx:1, openBracket: true})
createOrAppendInjectionPoint(expression.alternate.end, {type:"closeBracketEnd"}); createOrAppendInjectionPoint(expression.alternate.end, {type:"closeBracketEnd"});
//It should get instrumented when we parse it //It should get instrumented when we parse it
} else if (expression.alternate && contract.slice(expression.alternate.start,expression.alternate.end).trim().indexOf('{')===0){ } else if (expression.alternate && expression.alternate.type === "BlockStatement"){
createOrAppendInjectionPoint(expression.alternate.start+1, {type: "callBranchEvent", branchId: branchId, locationIdx: 1}) createOrAppendInjectionPoint(expression.alternate.start+1, {type: "callBranchEvent", branchId: branchId, locationIdx: 1})
} else if (expression.alternate){
createOrAppendInjectionPoint(expression.alternate.start, {type: "callBranchEvent", branchId: branchId, locationIdx: 1})
} else { } else {
createOrAppendInjectionPoint(expression.consequent.end, {type: "callEmptyBranchEvent", branchId: branchId, locationIdx: 1}); createOrAppendInjectionPoint(expression.consequent.end, {type: "callEmptyBranchEvent", branchId: branchId, locationIdx: 1});
} }
} }
parse["AssignmentExpression"] = function (expression, instrument){ parse["AssignmentExpression"] = function (expression, instrument){
@ -368,6 +364,16 @@ module.exports = function(contract, fileName, instrumentingActive){
} }
} }
parse["WhileStatement"] = function(expression, instrument){
if (instrument){instrumentStatement(expression)}
parse[expression.body.type](expression.body, instrument);
}
parse["ForStatement"] = function(expression, instrument){
if (instrument){instrumentStatement(expression)}
parse[expression.body.type](expression.body, instrument);
}
parse["StructDeclaration"] = function(expression, instrument){ parse["StructDeclaration"] = function(expression, instrument){
} }
@ -407,12 +413,6 @@ module.exports = function(contract, fileName, instrumentingActive){
parse["DoWhileStatement"] = function(expression, instrument){ parse["DoWhileStatement"] = function(expression, instrument){
} }
parse["WhileStatement"] = function(expression, instrument){
}
parse["ForStatement"] = function(expression, instrument){
}
parse["ForInStatement"] = function(expression, instrument){ parse["ForInStatement"] = function(expression, instrument){
} }

@ -15,9 +15,15 @@
"ethereumjs-testrpc": "^3.0.3", "ethereumjs-testrpc": "^3.0.3",
"istanbul": "^0.4.5", "istanbul": "^0.4.5",
"shelljs": "^0.7.4", "shelljs": "^0.7.4",
"sol-explore": "^1.6.2",
"solidity-parser": "git+https://github.com/ConsenSys/solidity-parser.git#master" "solidity-parser": "git+https://github.com/ConsenSys/solidity-parser.git#master"
}, },
"devDependencies": { "devDependencies": {
"crypto-js": "^3.1.9-1",
"ethereumjs-account": "^2.0.4",
"ethereumjs-tx": "^1.2.2",
"ethereumjs-util": "^5.0.1",
"merkle-patricia-tree": "^2.1.2",
"mocha": "^3.1.0", "mocha": "^3.1.0",
"solc": "^0.4.6" "solc": "^0.4.6"
} }

@ -0,0 +1,56 @@
const SolExplore = require('sol-explore');
const SolidityParser = require('solidity-parser');
/**
* Splices enclosing brackets into `contract` around `expression`;
* @param {String} contract solidity code
* @param {Object} node AST node to bracket
* @return {String} contract
*/
function blockWrap(contract, expression) {
return contract.slice(0, expression.start) + '{' + contract.slice(expression.start, expression.end) + '}' + contract.slice(expression.end);
}
/**
* Locates unbracketed singleton statements attached to if, else, for and while statements
* and brackets them. Instrumenter needs to inject events at these locations and having
* them pre-bracketed simplifies the process. Each time a modification is made the contract
* is passed back to the parser and re-walked because all the starts and ends get shifted.
* @param {String} contract solidity code
* @return {String} contract
*/
module.exports.run = function r(contract) {
let keepRunning = true;
while (keepRunning) {
const ast = SolidityParser.parse(contract);
keepRunning = false;
SolExplore.traverse(ast, {
enter(node, parent) {
// If consequents
if (node.type === 'IfStatement' && node.consequent.type !== 'BlockStatement') {
contract = blockWrap(contract, node.consequent);
keepRunning = true;
this.stopTraversal();
// If alternates
} else if (
node.type === 'IfStatement' &&
node.alternate &&
node.alternate.type !== 'IfStatement' &&
node.alternate.type !== 'BlockStatement') {
contract = blockWrap(contract, node.alternate);
keepRunning = true;
this.stopTraversal();
// Loops
} else if (
(node.type === 'ForStatement' || node.type === 'WhileStatement') &&
node.body.type !== 'BlockStatement') {
contract = blockWrap(contract, node.body);
keepRunning = true;
this.stopTraversal();
}
},
});
}
return contract;
};

@ -3,10 +3,11 @@ var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
var shell = require('shelljs'); var shell = require('shelljs');
var SolidityCoder = require("web3/lib/solidity/coder.js"); var SolidityCoder = require("web3/lib/solidity/coder.js");
var coverage = {};
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
var getInstrumentedVersion = require('./instrumentSolidity.js'); var getInstrumentedVersion = require('./instrumentSolidity.js');
var CoverageMap = require('./coverageMap.js');
var coverage = new CoverageMap();
var childprocess = require('child_process'); var childprocess = require('child_process');
@ -34,22 +35,7 @@ shell.ls('./../originalContracts/*.sol').forEach(function(file) {
var instrumentedContractInfo = getInstrumentedVersion(contract, fileName, true); var instrumentedContractInfo = getInstrumentedVersion(contract, fileName, true);
fs.writeFileSync('./../contracts/' + path.basename(file), instrumentedContractInfo.contract); fs.writeFileSync('./../contracts/' + path.basename(file), instrumentedContractInfo.contract);
var canonicalContractPath = path.resolve('./../originalContracts/' + path.basename(file)); var canonicalContractPath = path.resolve('./../originalContracts/' + path.basename(file));
coverage[canonicalContractPath] = { "l": {}, "path": canonicalContractPath, "s": {}, "b": {}, "f": {}, "fnMap": {}, "statementMap": {}, "branchMap": {} }; coverage.addContract(instrumentedContractInfo, canonicalContractPath);
for (idx in instrumentedContractInfo.runnableLines) {
coverage[canonicalContractPath]["l"][instrumentedContractInfo.runnableLines[idx]] = 0;
}
coverage[canonicalContractPath].fnMap = instrumentedContractInfo.fnMap;
for (x=1; x<=Object.keys(instrumentedContractInfo.fnMap).length; x++ ){
coverage[canonicalContractPath]["f"][x] = 0;
}
coverage[canonicalContractPath].branchMap = instrumentedContractInfo.branchMap;
for (x=1; x<=Object.keys(instrumentedContractInfo.branchMap).length; x++ ){
coverage[canonicalContractPath]["b"][x] = [0,0];
}
coverage[canonicalContractPath].statementMap= instrumentedContractInfo.statementMap;
for (x=1; x<=Object.keys(instrumentedContractInfo.statementMap).length; x++ ){
coverage[canonicalContractPath]["s"][x] = 0;
}
} }
}); });
shell.cp("./../originalContracts/Migrations.sol", "./../contracts/Migrations.sol"); shell.cp("./../originalContracts/Migrations.sol", "./../contracts/Migrations.sol");
@ -58,30 +44,12 @@ shell.rm('./allFiredEvents'); //Delete previous results
shell.exec('truffle test --network coverage'); shell.exec('truffle test --network coverage');
events = fs.readFileSync('./allFiredEvents').toString().split('\n') events = fs.readFileSync('./allFiredEvents').toString().split('\n')
for (idx=0; idx < events.length-1; idx++){ events.pop();
//The limit here isn't a bug - there is an empty line at the end of this file, so we don't //The pop here isn't a bug - there is an empty line at the end of this file, so we
//want to go to the very end of the array. //don't want to include it as an event.
var event = JSON.parse(events[idx]); coverage.generate(events, './../originalContracts/');
if (event.topics.indexOf("b8995a65f405d9756b41a334f38d8ff0c93c4934e170d3c1429c3e7ca101014d") >= 0) {
var data = SolidityCoder.decodeParams(["string", "uint256"], event.data.replace("0x", ""));
var canonicalContractPath = path.resolve('./../originalContracts/' + path.basename(data[0]));
coverage[canonicalContractPath]["l"][data[1].toNumber()] += 1;
}else if(event.topics.indexOf("d4ce765fd23c5cc3660249353d61ecd18ca60549dd62cb9ca350a4244de7b87f")>=0){
var data = SolidityCoder.decodeParams(["string", "uint256"], event.data.replace("0x", ""));
var canonicalContractPath = path.resolve('./../originalContracts/' + path.basename(data[0]));
coverage[canonicalContractPath]["f"][data[1].toNumber()] += 1;
}else if(event.topics.indexOf("d4cf56ed5ba572684f02f889f12ac42d9583c8e3097802060e949bfbb3c1bff5")>=0){
var data = SolidityCoder.decodeParams(["string", "uint256", "uint256"], event.data.replace("0x", ""));
var canonicalContractPath = path.resolve('./../originalContracts/' + path.basename(data[0]));
coverage[canonicalContractPath]["b"][data[1].toNumber()][data[2].toNumber()] += 1;
}else if(event.topics.indexOf("b51abbff580b3a34bbc725f2dc6f736e9d4b45a41293fd0084ad865a31fde0c8")>=0){
var data = SolidityCoder.decodeParams(["string","uint256"], event.data.replace("0x", ""));
var canonicalContractPath = path.resolve('./../originalContracts/' + path.basename(data[0]));
coverage[canonicalContractPath]["s"][data[1].toNumber()]+= 1;
}
}
fs.writeFileSync('./coverage.json', JSON.stringify(coverage)); fs.writeFileSync('./coverage.json', JSON.stringify(coverage.coverage));
shell.exec("./node_modules/istanbul/lib/cli.js report html") shell.exec("./node_modules/istanbul/lib/cli.js report html")
testrpcProcess.kill(); testrpcProcess.kill();

@ -1,44 +1,149 @@
var solc = require('solc'); const solc = require('solc');
var getInstrumentedVersion = require('./../instrumentSolidity.js'); const path = require('path');
var util = require('./util/util.js') const getInstrumentedVersion = require('./../instrumentSolidity.js');
const util = require('./util/util.js');
const CoverageMap = require('./../coverageMap');
const vm = require('./util/vm');
const assert = require('assert');
/**
* NB: passing '1' to solc as an option activates the optimiser
*/
describe('if, else, and else if statements', function(){ describe('if, else, and else if statements', function(){
it('should compile after instrumenting else statements with brackets',function(){ const fileName = 'test.sol';
var contract = util.getCode('if/else-with-brackets.sol'); const filePath = path.resolve('./test.sol');
var info = getInstrumentedVersion(contract, "test.sol", true); const pathPrefix = './';
var output = solc.compile(info.contract, 1);
util.report(output.errors); it('should cover an if statement with a bracketed consequent', (done) => {
const contract = util.getCode('if/if-with-brackets.sol');
const info = getInstrumentedVersion(contract, fileName, true);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
// Runs: a(1) => if (x == 1) { x = 3; }
vm.execute(info.contract, 'a', [1]).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, 2: 1});
assert.deepEqual(mapping[filePath].f, {1: 1});
done();
});
});
// Runs: a(1) => if (x == 1) x = 2;
it('should cover an unbracketed if consequent (single line)',function(done){
const contract = util.getCode('if/if-no-brackets.sol');
const info = getInstrumentedVersion(contract, fileName, true);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
// Same results as previous test
vm.execute(info.contract, 'a', [1]).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, 2: 1});
assert.deepEqual(mapping[filePath].f, {1: 1});
done();
}).catch(err => {console.log(err); done() })
}) })
it('should compile after instrumenting else statements without brackets',function(){ it('should cover an if statement with multiline bracketed consequent', (done) => {
var contract = util.getCode('if/else-without-brackets.sol'); const contract = util.getCode('if/if-with-brackets-multiline.sol');
var info = getInstrumentedVersion(contract, "test.sol", true); const info = getInstrumentedVersion(contract, fileName, true);
var output = solc.compile(info.contract, 1); const coverage = new CoverageMap();
util.report(output.errors); coverage.addContract(info, filePath);
// Runs: a(1) => if (x == 1){\n x = 3; }
vm.execute(info.contract, 'a', [1]).then(events => {
const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, {5: 1, 6: 1});
assert.deepEqual(mapping[filePath].b, {1: [1, 0]});
assert.deepEqual(mapping[filePath].s, {1: 1, 2: 1});
assert.deepEqual(mapping[filePath].f, {1: 1});
done();
});
});
// Runs: a(1) => if (x == 1)\n x = 3;
it('should cover an unbracketed if consequent (multi-line)', function(done){
const contract = util.getCode('if/if-no-brackets-multiline.sol');
const info = getInstrumentedVersion(contract, fileName, true);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
// Same results as previous test
vm.execute(info.contract, 'a', [1]).then(events => {
const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, {5: 1, 6: 1});
assert.deepEqual(mapping[filePath].b, {1: [1, 0]});
assert.deepEqual(mapping[filePath].s, {1: 1, 2: 1});
assert.deepEqual(mapping[filePath].f, {1: 1});
done();
})
}) })
it('should compile after instrumenting if statements with no brackets',function(){ it('should cover a simple if statement with a failing condition', (done) => {
var contract = util.getCode('if/if-no-brackets.sol'); const contract = util.getCode('if/if-with-brackets.sol');
var info = getInstrumentedVersion(contract, "test.sol", true); const info = getInstrumentedVersion(contract, fileName, true);
var output = solc.compile(info.contract, 1); const coverage = new CoverageMap();
util.report(output.errors); coverage.addContract(info, filePath);
// Runs: a(2) => if (x == 1) { x = 3; }
vm.execute(info.contract, 'a', [2]).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, 2: 0});
assert.deepEqual(mapping[filePath].f, {1: 1});
done();
}).catch(err => {console.log(err); done() })
});
// Runs: a(2) => if (x == 1){\n throw;\n }else{\n x = 5; \n}
it('should cover an if statement with a bracketed alternate', (done) => {
const contract = util.getCode('if/else-with-brackets.sol');
const info = getInstrumentedVersion(contract, fileName, true);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
vm.execute(info.contract, 'a', [2]).then(events => {
const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, {5: 1, 6: 0, 8: 1});
assert.deepEqual(mapping[filePath].b, {1: [0, 1]});
assert.deepEqual(mapping[filePath].s, {1: 1, 2: 0, 3: 1});
assert.deepEqual(mapping[filePath].f, {1: 1});
done();
}) })
});
it('should cover an if statement with an unbracketed alternate',function(done){
const contract = util.getCode('if/else-without-brackets.sol');
const info = getInstrumentedVersion(contract, "test.sol", true);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
it('should compile after instrumenting if statements with brackets',function(){ vm.execute(info.contract, 'a', [2]).then(events => {
var contract = util.getCode('if/if-with-brackets.sol'); const mapping = coverage.generate(events, pathPrefix);
var info = getInstrumentedVersion(contract, "test.sol", true); assert.deepEqual(mapping[filePath].l, {5: 1, 6: 0, 8: 1});
var output = solc.compile(info.contract, 1); assert.deepEqual(mapping[filePath].b, {1: [0, 1]});
util.report(output.errors); assert.deepEqual(mapping[filePath].s, {1: 1, 2: 0, 3: 1});
assert.deepEqual(mapping[filePath].f, {1: 1});
done();
}) })
})
it('should cover nested if statements with missing else statements',function(done){
const contract = util.getCode('if/nested-if-missing-else.sol');
const info = getInstrumentedVersion(contract, fileName, true);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
it('should compile after instrumenting nested if statements with missing else statements',function(){ vm.execute(info.contract, 'a', [2, 3, 3]).then(events => {
var contract = util.getCode('if/nested-if-missing-else.sol'); const mapping = coverage.generate(events, pathPrefix);
var info = getInstrumentedVersion(contract, "test.sol", true); assert.deepEqual(mapping[filePath].l, {5: 1, 7: 1});
var output = solc.compile(info.contract, 1); assert.deepEqual(mapping[filePath].b, { '1': [ 0, 1 ], '2': [ 1, 0 ], '3': [ 1, 0 ] });
util.report(output.errors); assert.deepEqual(mapping[filePath].s, {1: 1, 2: 1});
assert.deepEqual(mapping[filePath].f, {1: 1});
done();
})
}) })
}) })

@ -0,0 +1,82 @@
const solc = require('solc');
const path = require('path');
const getInstrumentedVersion = require('./../instrumentSolidity.js');
const util = require('./util/util.js');
const CoverageMap = require('./../coverageMap');
const vm = require('./util/vm');
const assert = require('assert');
describe('for and while statements', function(){
const fileName = 'test.sol';
const filePath = path.resolve('./test.sol');
const pathPrefix = './';
it('should cover a for statement with a bracketed body (multiline)', (done) => {
const contract = util.getCode('loops/for-with-brackets.sol');
const info = getInstrumentedVersion(contract, fileName, true);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
// Runs: a() => for(var x = 1; x < 10; x++){\n sha3(x);\n }
vm.execute(info.contract, 'a', []).then(events => {
const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, {5: 1, 6: 10});
assert.deepEqual(mapping[filePath].b, {});
assert.deepEqual(mapping[filePath].s, {1: 1, 2: 10});
assert.deepEqual(mapping[filePath].f, {1: 1});
done();
})
});
it('should cover a for statement with an unbracketed body', (done) => {
const contract = util.getCode('loops/for-no-brackets.sol');
const info = getInstrumentedVersion(contract, fileName, true);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
// Runs: a() => for(var x = 1; x < 10; x++)\n sha3(x);\n
vm.execute(info.contract, 'a', []).then(events => {
const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, {5: 1, 6: 10});
assert.deepEqual(mapping[filePath].b, {});
assert.deepEqual(mapping[filePath].s, {1: 1, 2: 10});
assert.deepEqual(mapping[filePath].f, {1: 1});
done();
})
});
it('should cover a while statement with an bracketed body (multiline)', (done) => {
const contract = util.getCode('loops/while-with-brackets.sol');
const info = getInstrumentedVersion(contract, fileName, true);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
// Runs: a() => var t = true;\n while(t){\n t = false;\n }
vm.execute(info.contract, 'a', []).then(events => {
const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, {5: 1, 6: 1, 7: 1});
assert.deepEqual(mapping[filePath].b, {});
assert.deepEqual(mapping[filePath].s, {1: 1, 2: 1, 3: 1});
assert.deepEqual(mapping[filePath].f, {1: 1});
done();
})
});
it('should cover a while statement with an unbracketed body (multiline)', (done) => {
const contract = util.getCode('loops/while-no-brackets.sol');
const info = getInstrumentedVersion(contract, fileName, true);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
// Runs: a() => var t = true;\n while(t)\n t = false;\n
vm.execute(info.contract, 'a', []).then(events => {
const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, {5: 1, 6: 1, 7: 1});
assert.deepEqual(mapping[filePath].b, {});
assert.deepEqual(mapping[filePath].s, {1: 1, 2: 1, 3: 1});
assert.deepEqual(mapping[filePath].f, {1: 1});
done();
})
});
})

@ -3,6 +3,6 @@ pragma solidity ^0.4.3;
contract Test { contract Test {
function a(uint x) { function a(uint x) {
if (x == 1) if (x == 1)
throw; x = 2;
} }
} }

@ -2,6 +2,6 @@ pragma solidity ^0.4.3;
contract Test { contract Test {
function a(uint x) { function a(uint x) {
if (x == 1) throw; if (x == 1) x = 2;
} }
} }

@ -0,0 +1,9 @@
pragma solidity ^0.4.3;
contract Test {
function a(uint x) {
if (x == 1) {
x = 3;
}
}
}

@ -2,6 +2,6 @@ pragma solidity ^0.4.3;
contract Test { contract Test {
function a(uint x) { function a(uint x) {
if (x == 1) { throw; } if (x == 1) {x = 3;}
} }
} }

@ -0,0 +1,8 @@
pragma solidity ^0.4.3;
contract Test {
function a() {
for(var x = 0; x < 10; x++)
sha3(x);
}
}

@ -0,0 +1,9 @@
pragma solidity ^0.4.3;
contract Test {
function a() {
for(var x = 0; x < 10; x++){
sha3(x);
}
}
}

@ -0,0 +1,9 @@
pragma solidity ^0.4.3;
contract Test {
function a() {
var t = true;
while(t)
t = false;
}
}

@ -0,0 +1,10 @@
pragma solidity ^0.4.3;
contract Test {
function a() {
var t = true;
while(t){
t = false;
}
}
}

@ -35,11 +35,4 @@ describe('generic statements', function(){
var output = solc.compile(info.contract, 1); var output = solc.compile(info.contract, 1);
util.report(output.errors); util.report(output.errors);
}) })
it('should compile after instrumenting a statement that is an unbracketed "if" consequent (multi-line)', function(){
var contract = util.getCode('statements/if-consequent-no-brackets-multiline.sol');
var info = getInstrumentedVersion(contract, "test.sol", true);
var output = solc.compile(info.contract, 1);
util.report(output.errors);
})
}) })

@ -0,0 +1,140 @@
const solc = require('solc');
const path = require('path');
const VM = require('ethereumjs-vm');
const Account = require('ethereumjs-account');
const Transaction = require('ethereumjs-tx');
const utils = require('ethereumjs-util');
const CryptoJS = require('crypto-js');
const Trie = require('merkle-patricia-tree');
const coder = require('web3/lib/solidity/coder.js');
// Don't use this address for anything, obviously!
const secretKey = 'e81cb653c260ee12c72ec8750e6bfd8a4dc2c3d7e3ede68dd2f150d0a67263d8';
const accountAddress = new Buffer('7caf6f9bc8b3ba5c7824f934c826bd6dc38c8467', 'hex');
/**
* Encodes function data
* Source: consensys/eth-lightwallet/lib/txutils.js (line 18)
*/
function encodeFunctionTxData(functionName, types, args) {
const fullName = functionName + '(' + types.join() + ')';
const signature = CryptoJS.SHA3(fullName, {outputLength: 256}).toString(CryptoJS.enc.Hex).slice(0, 8);
const dataHex = signature + coder.encodeParams(types, args);
return '0x' + dataHex;
}
/**
* Extracts types from abi
* Source: consensys/eth-lightwallet/lib/txutils.js (line 27)
*/
function getTypesFromAbi(abi, functionName) {
function matchesFunctionName(json) {
return (json.name === functionName && json.type === 'function');
}
function getTypes(json) {
return json.type;
}
const funcJson = abi.filter(matchesFunctionName)[0];
return (funcJson.inputs).map(getTypes);
}
/**
* Retrieves abi for contract
* Source: raineorshine/eth-new-contract/src/index.js (line 8)
* @param {String} source solidity contract
* @param {Object} compilation compiled `source`
* @return {Object} abi
*/
function getAbi(source, compilation){
const contractNameMatch = source.match(/(?:contract|library)\s([^\s]*)\s*{/)
if(!contractNameMatch) {
throw new Error('Could not parse contract name from source.')
}
const contractName = contractNameMatch[1]
return JSON.parse(compilation.contracts[contractName].interface)
}
/**
* Creates, funds and publishes account to Trie
*/
function createAccount(trie) {
const account = new Account();
account.balance = 'f00000000000000000';
trie.put(accountAddress, account.serialize());
}
/**
* Deploys contract represented by `code`
* @param {String} code contract bytecode
*/
function deploy(vm, code) {
const tx = new Transaction({gasPrice: '1', gasLimit: 'ffffff', data: code,});
tx.sign(new Buffer(secretKey, 'hex'));
return new Promise((resolve, reject) => {
vm.runTx({tx: tx}, (err, results) => {
(err)
? reject(err)
: resolve(results.createdAddress);
});
});
}
/**
* Invokes `functionName` with `args` on contract at `address`. Tx construction logic
* is poached from consensys/eth-lightwallet/lib/txutils:functionTx
* @param {Array} abi contract abi
* @param {String} address deployed contract to invoke method on
* @param {String} functionName method to invoke
* @param {Array} args functionName's arguments
* @return {Promise} resolves array of logged events
*/
function callMethod(vm, abi, address, functionName, args) {
const types = getTypesFromAbi(abi, functionName);
const txData = encodeFunctionTxData(functionName, types, args);
const options = {
gasPrice: '0x1',
gasLimit: '0xffffff',
to: utils.bufferToHex(address),
data: txData,
nonce: '0x1',
};
let tx = new Transaction(options);
tx.sign(new Buffer(secretKey, 'hex'));
return new Promise((resolve, reject) => {
vm.runTx({tx: tx}, (err, results) => {
let seenEvents = [];
results.vm.runState.logs.map(log => {
const toWrite = {};
toWrite.address = log[0].toString('hex');
toWrite.topics = log[1].map(x => x.toString('hex'));
toWrite.data = log[2].toString('hex');
seenEvents.push(JSON.stringify(toWrite));
});
resolve(seenEvents);
});
});
}
/**
* Runs method `functionName` with parameters `args` on contract. Resolves a
* CR delimited list of logged events.
* @param {String} contract solidity to compile
* @param {String} functionName method to invoke on contract
* @param {Array} args parameter values to pass to method
* @return {Promise} resolves array of logged events.
*/
module.exports.execute = function ex(contract, functionName, args) {
const output = solc.compile(contract, 1);
const code = new Buffer(output.contracts.Test.bytecode, 'hex');
const abi = getAbi(contract, output);
const stateTrie = new Trie();
const vm = new VM({ state: stateTrie });
createAccount(stateTrie);
return deploy(vm, code).then(address => callMethod(vm, abi, address, functionName, args));
};
Loading…
Cancel
Save