parent
e2c9c90766
commit
91c5202483
@ -1,8 +1,7 @@ |
|||||||
allFiredEvents |
|
||||||
scTopics |
|
||||||
scDebugLog |
|
||||||
coverage.json |
coverage.json |
||||||
coverage/ |
coverage/ |
||||||
node_modules/ |
node_modules/ |
||||||
.changelog |
.changelog |
||||||
.DS_Store |
.DS_Store |
||||||
|
test/artifacts |
||||||
|
test/cache |
||||||
|
@ -0,0 +1,3 @@ |
|||||||
|
test/ |
||||||
|
.circleci/ |
||||||
|
docs/ |
@ -0,0 +1,18 @@ |
|||||||
|
|
||||||
|
// Mute compiler warnings - this will need to be addressed properly in
|
||||||
|
// the Buidler plugin by overloading TASK_COMPILE_COMPILE.
|
||||||
|
const originalLog = console.log; |
||||||
|
console.warn = () => {}; |
||||||
|
console.log = val => val === '\n' ? null : originalLog(val); |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
solc: { |
||||||
|
version: "0.5.8" |
||||||
|
}, |
||||||
|
paths: { |
||||||
|
artifacts: "./test/artifacts", |
||||||
|
cache: "./test/cache", |
||||||
|
test: "./test/units", |
||||||
|
sources: "./test/sources/contracts", |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
const web3Utils = require('web3-utils') |
||||||
|
|
||||||
|
class DataCollector { |
||||||
|
constructor(config={}, instrumentationData={}){ |
||||||
|
this.instrumentationData = instrumentationData; |
||||||
|
|
||||||
|
this.vm = config.vmResolver |
||||||
|
? config.vmResolver(config.provider) |
||||||
|
: this._ganacheVMResolver(config.provider); |
||||||
|
|
||||||
|
this._connect(); |
||||||
|
} |
||||||
|
|
||||||
|
// Subscribes to vm.on('step').
|
||||||
|
_connect(){ |
||||||
|
const self = this; |
||||||
|
this.vm.on("step", function(info){ |
||||||
|
|
||||||
|
if (info.opcode.name.includes("PUSH") && info.stack.length > 0){ |
||||||
|
const idx = info.stack.length - 1; |
||||||
|
const hash = web3Utils.toHex(info.stack[idx]).toString(); |
||||||
|
|
||||||
|
if(self.instrumentationData[hash]){ |
||||||
|
self.instrumentationData[hash].hits++; |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
_ganacheVMResolver(provider){ |
||||||
|
return provider.engine.manager.state.blockchain.vm; |
||||||
|
} |
||||||
|
|
||||||
|
_setInstrumentationData(data){ |
||||||
|
this.instrumentationData = data; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = DataCollector; |
@ -0,0 +1,112 @@ |
|||||||
|
/** |
||||||
|
* Converts instrumentation data accumulated a the vm steps to an instanbul spec coverage object. |
||||||
|
* @type {Coverage} |
||||||
|
*/ |
||||||
|
class Coverage { |
||||||
|
|
||||||
|
constructor() { |
||||||
|
this.data = {}; |
||||||
|
this.assertData = {}; |
||||||
|
this.lineTopics = []; |
||||||
|
this.functionTopics = []; |
||||||
|
this.branchTopics = []; |
||||||
|
this.statementTopics = []; |
||||||
|
this.assertPreTopics = []; |
||||||
|
this.assertPostTopics = []; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Initializes a coverage map object for contract |
||||||
|
* + instrumented per `info` |
||||||
|
* + located at `canonicalContractPath` |
||||||
|
* @param {Object} info `info = getIntrumentedVersion(contract, fileName, true)` |
||||||
|
* @param {String} canonicalContractPath path to contract file |
||||||
|
* @return {Object} coverage map with all values set to zero |
||||||
|
*/ |
||||||
|
|
||||||
|
addContract(info, canonicalContractPath) { |
||||||
|
this.data[canonicalContractPath] = { |
||||||
|
l: {}, |
||||||
|
path: canonicalContractPath, |
||||||
|
s: {}, |
||||||
|
b: {}, |
||||||
|
f: {}, |
||||||
|
fnMap: {}, |
||||||
|
statementMap: {}, |
||||||
|
branchMap: {}, |
||||||
|
}; |
||||||
|
this.assertData[canonicalContractPath] = { }; |
||||||
|
|
||||||
|
info.runnableLines.forEach((item, idx) => { |
||||||
|
this.data[canonicalContractPath].l[info.runnableLines[idx]] = 0; |
||||||
|
}); |
||||||
|
|
||||||
|
this.data[canonicalContractPath].fnMap = info.fnMap; |
||||||
|
for (let x = 1; x <= Object.keys(info.fnMap).length; x++) { |
||||||
|
this.data[canonicalContractPath].f[x] = 0; |
||||||
|
} |
||||||
|
|
||||||
|
this.data[canonicalContractPath].branchMap = info.branchMap; |
||||||
|
for (let x = 1; x <= Object.keys(info.branchMap).length; x++) { |
||||||
|
this.data[canonicalContractPath].b[x] = [0, 0]; |
||||||
|
this.assertData[canonicalContractPath][x] = { |
||||||
|
preEvents: 0, |
||||||
|
postEvents: 0, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
this.data[canonicalContractPath].statementMap = info.statementMap; |
||||||
|
for (let x = 1; x <= Object.keys(info.statementMap).length; x++) { |
||||||
|
this.data[canonicalContractPath].s[x] = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Populates an empty coverage map with values derived from a hash map of |
||||||
|
* data collected as the instrumented contracts are tested |
||||||
|
* @param {Object} map of collected instrumentation data |
||||||
|
* @return {Object} coverage map. |
||||||
|
*/ |
||||||
|
generate(collectedData) { |
||||||
|
const hashes = Object.keys(collectedData); |
||||||
|
|
||||||
|
for (let hash of hashes){ |
||||||
|
const data = collectedData[hash]; |
||||||
|
const contractPath = collectedData[hash].contractPath; |
||||||
|
const id = collectedData[hash].id; |
||||||
|
const hits = collectedData[hash].hits; |
||||||
|
|
||||||
|
switch(collectedData[hash].type){ |
||||||
|
case 'line': this.data[contractPath].l[id] = hits; break; |
||||||
|
case 'function': this.data[contractPath].f[id] = hits; break; |
||||||
|
case 'statement': this.data[contractPath].s[id] = hits; break; |
||||||
|
case 'branch': this.data[contractPath].b[id][data.locationIdx] = hits; break; |
||||||
|
case 'assertPre': this.assertData[contractPath][id].preEvents = hits; break; |
||||||
|
case 'assertPost': this.assertData[contractPath][id].postEvents = hits; break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Finally, interpret the assert pre/post events
|
||||||
|
const contractPaths = Object.keys(this.assertData); |
||||||
|
|
||||||
|
for (let contractPath of contractPaths){ |
||||||
|
const contract = this.data[contractPath]; |
||||||
|
|
||||||
|
Object.keys(contract.b).forEach((item, i) => { |
||||||
|
const branch = this.assertData[contractPath][i]; |
||||||
|
|
||||||
|
// Was it an assert branch?
|
||||||
|
if (branch && branch.preEvents > 0){ |
||||||
|
this.data[contractPath].b[i] = [ |
||||||
|
branch.postEvents, |
||||||
|
branch.preEvents - branch.postEvents |
||||||
|
] |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
return Object.assign({}, this.data); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
module.exports = Coverage; |
@ -1,145 +0,0 @@ |
|||||||
|
|
||||||
/** |
|
||||||
* This file contains methods that produce a coverage map to pass to instanbul |
|
||||||
* from data generated by `instrumentSolidity.js` |
|
||||||
*/ |
|
||||||
const { AbiCoder } = require('web3-eth-abi'); |
|
||||||
const SolidityCoder = AbiCoder(); |
|
||||||
const path = require('path'); |
|
||||||
const keccak = require('keccakjs'); |
|
||||||
const fs = require('fs'); |
|
||||||
|
|
||||||
/** |
|
||||||
* 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 = {}; |
|
||||||
this.assertCoverage = {}; |
|
||||||
this.lineTopics = []; |
|
||||||
this.functionTopics = []; |
|
||||||
this.branchTopics = []; |
|
||||||
this.statementTopics = []; |
|
||||||
this.assertPreTopics = []; |
|
||||||
this.assertPostTopics = []; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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: {}, |
|
||||||
}; |
|
||||||
this.assertCoverage[canonicalContractPath] = { }; |
|
||||||
|
|
||||||
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.assertCoverage[canonicalContractPath][x] = { |
|
||||||
preEvents: 0, |
|
||||||
postEvents: 0, |
|
||||||
}; |
|
||||||
} |
|
||||||
this.coverage[canonicalContractPath].statementMap = info.statementMap; |
|
||||||
for (let x = 1; x <= Object.keys(info.statementMap).length; x++) { |
|
||||||
this.coverage[canonicalContractPath].s[x] = 0; |
|
||||||
} |
|
||||||
|
|
||||||
const keccakhex = (x => { |
|
||||||
const hash = new keccak(256); // eslint-disable-line new-cap
|
|
||||||
hash.update(x); |
|
||||||
return hash.digest('hex'); |
|
||||||
}); |
|
||||||
|
|
||||||
const lineHash = keccakhex('__Coverage' + info.contractName + '(string,uint256)'); |
|
||||||
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${assertPreHash}\n${assertPostHash}\n`; |
|
||||||
fs.appendFileSync('./scTopics', topics); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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.filter(t => this.lineTopics.indexOf(t) >= 0).length > 0) { |
|
||||||
const data = SolidityCoder.decodeParameters(['string', 'uint256'], `0x${event.data}`); |
|
||||||
const canonicalContractPath = data[0]; |
|
||||||
this.coverage[canonicalContractPath].l[parseInt(data[1], 10)] += 1; |
|
||||||
} else if (event.topics.filter(t => this.functionTopics.indexOf(t) >= 0).length > 0) { |
|
||||||
const data = SolidityCoder.decodeParameters(['string', 'uint256'], `0x${event.data}`); |
|
||||||
const canonicalContractPath = data[0]; |
|
||||||
this.coverage[canonicalContractPath].f[parseInt(data[1], 10)] += 1; |
|
||||||
} else if (event.topics.filter(t => this.branchTopics.indexOf(t) >= 0).length > 0) { |
|
||||||
const data = SolidityCoder.decodeParameters(['string', 'uint256', 'uint256'], `0x${event.data}`); |
|
||||||
const canonicalContractPath = data[0]; |
|
||||||
this.coverage[canonicalContractPath].b[parseInt(data[1], 10)][parseInt(data[2], 10)] += 1; |
|
||||||
} else if (event.topics.filter(t => this.statementTopics.indexOf(t) >= 0).length > 0) { |
|
||||||
const data = SolidityCoder.decodeParameters(['string', 'uint256'], `0x${event.data}`); |
|
||||||
const canonicalContractPath = data[0]; |
|
||||||
this.coverage[canonicalContractPath].s[parseInt(data[1], 10)] += 1; |
|
||||||
} else if (event.topics.filter(t => this.assertPreTopics.indexOf(t) >= 0).length > 0) { |
|
||||||
const data = SolidityCoder.decodeParameters(['string', 'uint256'], `0x${event.data}`); |
|
||||||
const canonicalContractPath = data[0]; |
|
||||||
this.assertCoverage[canonicalContractPath][parseInt(data[1], 10)].preEvents += 1; |
|
||||||
} else if (event.topics.filter(t => this.assertPostTopics.indexOf(t) >= 0).length > 0) { |
|
||||||
const data = SolidityCoder.decodeParameters(['string', 'uint256'], `0x${event.data}`); |
|
||||||
const canonicalContractPath = data[0]; |
|
||||||
this.assertCoverage[canonicalContractPath][parseInt(data[1], 10)].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); |
|
||||||
} |
|
||||||
}; |
|
@ -1,77 +1,184 @@ |
|||||||
const injector = {}; |
const sha1 = require("sha1"); |
||||||
|
const web3Utils = require("web3-utils"); |
||||||
// These functions are used to actually inject the instrumentation events.
|
|
||||||
|
class Injector { |
||||||
injector.callEvent = function injectCallEvent(contract, fileName, injectionPoint) { |
constructor(){ |
||||||
const linecount = (contract.instrumented.slice(0, injectionPoint).match(/\n/g) || []).length + 1; |
this.hashCounter = 0; |
||||||
|
this.definitionCounter = 0; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Generates solidity statement to inject for line, stmt, branch, fn 'events' |
||||||
|
* @param {String} memoryVariable |
||||||
|
* @param {String} hash hash key to an instrumentationData entry (see _getHash) |
||||||
|
* @param {String} type instrumentation type, e.g. line, statement |
||||||
|
* @return {String} ex: _sc_82e0891[0] = bytes32(0xdc08...08ed1); // function
|
||||||
|
*/ |
||||||
|
_getInjectable(memoryVariable, hash, type){ |
||||||
|
return `${memoryVariable}[0] = bytes32(${hash}); /* ${type} */ \n`; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
_getHash(fileName) { |
||||||
|
this.hashCounter++; |
||||||
|
return web3Utils.keccak256(`${fileName}:${this.hashCounter}`); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Generates a solidity statement injection. Declared once per fn. |
||||||
|
* Definition is the same for every fn in file. |
||||||
|
* @param {String} fileName |
||||||
|
* @return {String} ex: bytes32[1] memory _sc_82e0891 |
||||||
|
*/ |
||||||
|
_getMemoryVariableDefinition(fileName){ |
||||||
|
this.definitionCounter++; |
||||||
|
return `\nbytes32[1] memory _sc_${sha1(fileName).slice(0,7)};\n`; |
||||||
|
} |
||||||
|
|
||||||
|
_getMemoryVariableAssignment(fileName){ |
||||||
|
return `\n_sc_${sha1(fileName).slice(0,7)}`; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
injectLine(contract, fileName, injectionPoint, injection, instrumentation){ |
||||||
|
const type = 'line'; |
||||||
|
const start = contract.instrumented.slice(0, injectionPoint); |
||||||
|
const end = contract.instrumented.slice(injectionPoint); |
||||||
|
|
||||||
|
const newLines = start.match(/\n/g); |
||||||
|
const linecount = ( newLines || []).length + 1; |
||||||
contract.runnableLines.push(linecount); |
contract.runnableLines.push(linecount); |
||||||
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + |
|
||||||
'emit __Coverage' + contract.contractName + '(\'' + fileName + '\',' + linecount + ');\n' + |
|
||||||
contract.instrumented.slice(injectionPoint); |
|
||||||
}; |
|
||||||
|
|
||||||
injector.callFunctionEvent = function injectCallFunctionEvent(contract, fileName, injectionPoint, injection) { |
|
||||||
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + |
|
||||||
'emit __FunctionCoverage' + contract.contractName + '(\'' + fileName + '\',' + injection.fnId + ');\n' + |
|
||||||
contract.instrumented.slice(injectionPoint); |
|
||||||
}; |
|
||||||
|
|
||||||
injector.callBranchEvent = function injectCallFunctionEvent(contract, fileName, injectionPoint, injection) { |
|
||||||
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + |
|
||||||
(injection.openBracket ? '{' : '') + |
|
||||||
'emit __BranchCoverage' + contract.contractName + '(\'' + fileName + '\',' + injection.branchId + ',' + injection.locationIdx + ')' + |
|
||||||
(injection.comma ? ',' : ';') + |
|
||||||
contract.instrumented.slice(injectionPoint); |
|
||||||
}; |
|
||||||
|
|
||||||
injector.callEmptyBranchEvent = function injectCallEmptyBranchEvent(contract, fileName, injectionPoint, injection) { |
|
||||||
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + |
|
||||||
'else { emit __BranchCoverage' + contract.contractName + '(\'' + fileName + '\',' + injection.branchId + ',' + injection.locationIdx + ');}\n' + |
|
||||||
contract.instrumented.slice(injectionPoint); |
|
||||||
}; |
|
||||||
|
|
||||||
|
|
||||||
injector.callAssertPreEvent = function callAssertPreEvent(contract, fileName, injectionPoint, injection) { |
|
||||||
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + |
|
||||||
'emit __AssertPreCoverage' + contract.contractName + '(\'' + fileName + '\',' + injection.branchId + ');\n' + |
|
||||||
contract.instrumented.slice(injectionPoint); |
|
||||||
}; |
|
||||||
|
|
||||||
injector.callAssertPostEvent = function callAssertPostEvent(contract, fileName, injectionPoint, injection) { |
const hash = this._getHash(fileName); |
||||||
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + |
const memoryVariable = this._getMemoryVariableAssignment(fileName); |
||||||
'emit __AssertPostCoverage' + contract.contractName + '(\'' + fileName + '\',' + injection.branchId + ');\n' + |
const injectable = this._getInjectable(memoryVariable, hash , type) |
||||||
contract.instrumented.slice(injectionPoint); |
|
||||||
|
instrumentation[hash] = { |
||||||
|
id: linecount, |
||||||
|
type: type, |
||||||
|
contractPath: fileName, |
||||||
|
hits: 0 |
||||||
|
} |
||||||
|
|
||||||
|
contract.instrumented = `${start}${injectable}${end}`; |
||||||
|
} |
||||||
|
|
||||||
|
injectStatement(contract, fileName, injectionPoint, injection, instrumentation) { |
||||||
|
const type = 'statement'; |
||||||
|
const start = contract.instrumented.slice(0, injectionPoint); |
||||||
|
const end = contract.instrumented.slice(injectionPoint); |
||||||
|
|
||||||
|
const hash = this._getHash(fileName); |
||||||
|
const memoryVariable = this._getMemoryVariableAssignment(fileName); |
||||||
|
const injectable = this._getInjectable(memoryVariable, hash, type) |
||||||
|
|
||||||
|
instrumentation[hash] = { |
||||||
|
id: injection.statementId, |
||||||
|
type: type, |
||||||
|
contractPath: fileName, |
||||||
|
hits: 0 |
||||||
|
} |
||||||
|
|
||||||
|
contract.instrumented = `${start}${injectable}${end}`; |
||||||
|
}; |
||||||
|
|
||||||
|
injectFunction(contract, fileName, injectionPoint, injection, instrumentation){ |
||||||
|
const type = 'function'; |
||||||
|
const start = contract.instrumented.slice(0, injectionPoint); |
||||||
|
const end = contract.instrumented.slice(injectionPoint); |
||||||
|
|
||||||
|
const hash = this._getHash(fileName); |
||||||
|
const memoryVariableDefinition = this._getMemoryVariableDefinition(fileName); |
||||||
|
const memoryVariable = this._getMemoryVariableAssignment(fileName); |
||||||
|
const injectable = this._getInjectable(memoryVariable, hash, type); |
||||||
|
|
||||||
|
instrumentation[hash] = { |
||||||
|
id: injection.fnId, |
||||||
|
type: type, |
||||||
|
contractPath: fileName, |
||||||
|
hits: 0 |
||||||
|
} |
||||||
|
|
||||||
|
contract.instrumented = `${start}${memoryVariableDefinition}${injectable}${end}`; |
||||||
|
} |
||||||
|
|
||||||
|
injectBranch(contract, fileName, injectionPoint, injection, instrumentation){ |
||||||
|
const type = 'branch'; |
||||||
|
const start = contract.instrumented.slice(0, injectionPoint); |
||||||
|
const end = contract.instrumented.slice(injectionPoint); |
||||||
|
|
||||||
|
const hash = this._getHash(fileName); |
||||||
|
const memoryVariable = this._getMemoryVariableAssignment(fileName); |
||||||
|
const injectable = this._getInjectable(memoryVariable, hash, type); |
||||||
|
|
||||||
|
instrumentation[hash] = { |
||||||
|
id: injection.branchId, |
||||||
|
locationIdx: injection.locationIdx, |
||||||
|
type: type, |
||||||
|
contractPath: fileName, |
||||||
|
hits: 0 |
||||||
|
} |
||||||
|
|
||||||
|
contract.instrumented = `${start}${injectable}${end}`; |
||||||
|
} |
||||||
|
|
||||||
|
injectEmptyBranch(contract, fileName, injectionPoint, injection, instrumentation) { |
||||||
|
const type = 'branch'; |
||||||
|
const start = contract.instrumented.slice(0, injectionPoint); |
||||||
|
const end = contract.instrumented.slice(injectionPoint); |
||||||
|
|
||||||
|
const hash = this._getHash(fileName); |
||||||
|
const memoryVariable = this._getMemoryVariableAssignment(fileName); |
||||||
|
const injectable = this._getInjectable(memoryVariable, hash, type); |
||||||
|
|
||||||
|
instrumentation[hash] = { |
||||||
|
id: injection.branchId, |
||||||
|
locationIdx: injection.locationIdx, |
||||||
|
type: type, |
||||||
|
contractPath: fileName, |
||||||
|
hits: 0 |
||||||
|
} |
||||||
|
|
||||||
|
contract.instrumented = `${start}else { ${injectable}}${end}`; |
||||||
|
} |
||||||
|
|
||||||
|
injectAssertPre(contract, fileName, injectionPoint, injection, instrumentation) { |
||||||
|
const type = 'assertPre'; |
||||||
|
const start = contract.instrumented.slice(0, injectionPoint); |
||||||
|
const end = contract.instrumented.slice(injectionPoint); |
||||||
|
|
||||||
|
const hash = this._getHash(fileName); |
||||||
|
const memoryVariable = this._getMemoryVariableAssignment(fileName); |
||||||
|
const injectable = this._getInjectable(memoryVariable, hash, type); |
||||||
|
|
||||||
|
instrumentation[hash] = { |
||||||
|
id: injection.branchId, |
||||||
|
type: type, |
||||||
|
contractPath: fileName, |
||||||
|
hits: 0 |
||||||
|
} |
||||||
|
|
||||||
|
contract.instrumented = `${start}${injectable}${end}`; |
||||||
|
} |
||||||
|
|
||||||
|
injectAssertPost(contract, fileName, injectionPoint, injection, instrumentation) { |
||||||
|
const type = 'assertPost'; |
||||||
|
const start = contract.instrumented.slice(0, injectionPoint); |
||||||
|
const end = contract.instrumented.slice(injectionPoint); |
||||||
|
|
||||||
|
const hash = this._getHash(fileName); |
||||||
|
const memoryVariable = this._getMemoryVariableAssignment(fileName); |
||||||
|
const injectable = this._getInjectable(memoryVariable, hash, type); |
||||||
|
|
||||||
|
instrumentation[hash] = { |
||||||
|
id: injection.branchId, |
||||||
|
type: type, |
||||||
|
contractPath: fileName, |
||||||
|
hits: 0 |
||||||
|
} |
||||||
|
|
||||||
|
contract.instrumented = `${start}${injectable}${end}`; |
||||||
|
} |
||||||
}; |
}; |
||||||
|
|
||||||
injector.openParen = function injectOpenParen(contract, fileName, injectionPoint, injection) { |
module.exports = Injector; |
||||||
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + '(' + contract.instrumented.slice(injectionPoint); |
|
||||||
}; |
|
||||||
|
|
||||||
injector.closeParen = function injectCloseParen(contract, fileName, injectionPoint, injection) { |
|
||||||
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + ')' + contract.instrumented.slice(injectionPoint); |
|
||||||
}; |
|
||||||
|
|
||||||
injector.literal = function injectLiteral(contract, fileName, injectionPoint, injection) { |
|
||||||
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + injection.string + contract.instrumented.slice(injectionPoint); |
|
||||||
}; |
|
||||||
|
|
||||||
injector.statement = function injectStatement(contract, fileName, injectionPoint, injection) { |
|
||||||
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + |
|
||||||
'emit __StatementCoverage' + contract.contractName + '(\'' + fileName + '\',' + injection.statementId + ');\n' + |
|
||||||
contract.instrumented.slice(injectionPoint); |
|
||||||
}; |
|
||||||
|
|
||||||
injector.eventDefinition = function injectEventDefinition(contract, fileName, injectionPoint, injection) { |
|
||||||
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + |
|
||||||
'event __Coverage' + contract.contractName + '(string fileName, uint256 lineNumber);\n' + |
|
||||||
'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); |
|
||||||
}; |
|
||||||
|
|
||||||
|
|
||||||
module.exports = injector; |
|
||||||
|
@ -1,65 +0,0 @@ |
|||||||
const SolidityParser = require('solidity-parser-antlr'); |
|
||||||
const preprocessor = require('./preprocessor'); |
|
||||||
const injector = require('./injector'); |
|
||||||
const parse = require('./parse'); |
|
||||||
|
|
||||||
const path = require('path'); |
|
||||||
|
|
||||||
module.exports = function instrumentSolidity(contractSource, fileName) { |
|
||||||
const contract = {}; |
|
||||||
contract.source = contractSource; |
|
||||||
contract.instrumented = contractSource; |
|
||||||
|
|
||||||
contract.runnableLines = []; |
|
||||||
contract.fnMap = {}; |
|
||||||
contract.fnId = 0; |
|
||||||
contract.branchMap = {}; |
|
||||||
contract.branchId = 0; |
|
||||||
contract.statementMap = {}; |
|
||||||
contract.statementId = 0; |
|
||||||
contract.injectionPoints = {}; |
|
||||||
|
|
||||||
// First, we run over the original contract to get the source mapping.
|
|
||||||
let ast = SolidityParser.parse(contract.source, {range: true}); |
|
||||||
parse[ast.type](contract, ast); |
|
||||||
const retValue = JSON.parse(JSON.stringify(contract)); |
|
||||||
|
|
||||||
// Now, we reset almost everything and use the preprocessor first to increase our effectiveness.
|
|
||||||
contract.runnableLines = []; |
|
||||||
contract.fnMap = {}; |
|
||||||
contract.fnId = 0; |
|
||||||
contract.branchMap = {}; |
|
||||||
contract.branchId = 0; |
|
||||||
contract.statementMap = {}; |
|
||||||
contract.statementId = 0; |
|
||||||
contract.injectionPoints = {}; |
|
||||||
|
|
||||||
contract.preprocessed = preprocessor.run(contract.source); |
|
||||||
contract.instrumented = contract.preprocessed; |
|
||||||
ast = SolidityParser.parse(contract.preprocessed, {range: true}); |
|
||||||
const contractStatement = ast.children.filter(node => (node.type === 'ContractDefinition' || |
|
||||||
node.type === 'LibraryDefinition' || |
|
||||||
node.type === 'InterfaceDefinition')); |
|
||||||
contract.contractName = contractStatement[0].name; |
|
||||||
|
|
||||||
parse[ast.type](contract, ast); |
|
||||||
|
|
||||||
// We have to iterate through these injection points in descending order to not mess up
|
|
||||||
// the injection process.
|
|
||||||
const sortedPoints = Object.keys(contract.injectionPoints).sort((a, b) => b - a); |
|
||||||
sortedPoints.forEach(injectionPoint => { |
|
||||||
// Line instrumentation has to happen first
|
|
||||||
contract.injectionPoints[injectionPoint].sort((a, b) => { |
|
||||||
const eventTypes = ['openParen', 'callBranchEvent', 'callEmptyBranchEvent', 'callEvent']; |
|
||||||
return eventTypes.indexOf(b.type) - eventTypes.indexOf(a.type); |
|
||||||
}); |
|
||||||
contract.injectionPoints[injectionPoint].forEach(injection => { |
|
||||||
injector[injection.type](contract, fileName, injectionPoint, injection); |
|
||||||
}); |
|
||||||
}); |
|
||||||
retValue.runnableLines = contract.runnableLines; |
|
||||||
retValue.contract = contract.instrumented; |
|
||||||
retValue.contractName = contractStatement[0].name; |
|
||||||
|
|
||||||
return retValue; |
|
||||||
}; |
|
@ -1,254 +1,106 @@ |
|||||||
const instrumenter = {}; |
const SolidityParser = require('solidity-parser-antlr'); |
||||||
|
const path = require('path'); |
||||||
// These functions work out where in an expression we can inject our
|
|
||||||
// instrumenation events.
|
const Injector = require('./injector'); |
||||||
|
const preprocess = require('./preprocessor'); |
||||||
function createOrAppendInjectionPoint(contract, key, value) { |
const parse = require('./parse'); |
||||||
if (contract.injectionPoints[key]) { |
|
||||||
contract.injectionPoints[key].push(value); |
/** |
||||||
} else { |
* Top level controller for the instrumentation sequence. Also hosts the instrumentation data map |
||||||
contract.injectionPoints[key] = [value]; |
* which the vm step listener writes its output to. This only needs to be instantiated once |
||||||
|
* per coverage run. |
||||||
|
*/ |
||||||
|
class Instrumenter { |
||||||
|
|
||||||
|
constructor(){ |
||||||
|
this.instrumentationData = {}; |
||||||
|
this.injector = new Injector(); |
||||||
} |
} |
||||||
} |
|
||||||
|
|
||||||
instrumenter.prePosition = function prePosition(expression) { |
_isRootNode(node){ |
||||||
if (expression.right.type === 'ConditionalExpression' && |
return (node.type === 'ContractDefinition' || |
||||||
expression.left.type === 'MemberExpression') { |
node.type === 'LibraryDefinition' || |
||||||
expression.range[0] -= 2; |
node.type === 'InterfaceDefinition'); |
||||||
} |
} |
||||||
}; |
|
||||||
|
|
||||||
instrumenter.instrumentAssignmentExpression = function instrumentAssignmentExpression(contract, expression) { |
|
||||||
|
|
||||||
// This is suspended for 0.5.0 which tries to accomodate the new `emit` keyword.
|
|
||||||
// Solc is not allowing us to use the construction `emit SomeEvent()` within the parens :/
|
|
||||||
return; |
|
||||||
// --------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// The only time we instrument an assignment expression is if there's a conditional expression on
|
|
||||||
// the right
|
|
||||||
/*if (expression.right.type === 'ConditionalExpression') { |
|
||||||
if (expression.left.type === 'DeclarativeExpression' || expression.left.type === 'Identifier') { |
|
||||||
// Then we need to go from bytes32 varname = (conditional expression)
|
|
||||||
// to bytes32 varname; (,varname) = (conditional expression)
|
|
||||||
createOrAppendInjectionPoint(contract, expression.left.range[1], { |
|
||||||
type: 'literal', string: '; (,' + expression.left.name + ')', |
|
||||||
}); |
|
||||||
instrumenter.instrumentConditionalExpression(contract, expression.right); |
|
||||||
} else if (expression.left.type === 'MemberExpression') { |
|
||||||
createOrAppendInjectionPoint(contract, expression.left.range[0], { |
|
||||||
type: 'literal', string: '(,', |
|
||||||
}); |
|
||||||
createOrAppendInjectionPoint(contract, expression.left.range[1], { |
|
||||||
type: 'literal', string: ')', |
|
||||||
}); |
|
||||||
instrumenter.instrumentConditionalExpression(contract, expression.right); |
|
||||||
} else { |
|
||||||
const err = 'Error instrumenting assignment expression @ solidity-coverage/lib/instrumenter.js'; |
|
||||||
console.log(err, contract, expression.left); |
|
||||||
process.exit(); |
|
||||||
} |
|
||||||
}*/ |
|
||||||
}; |
|
||||||
|
|
||||||
instrumenter.instrumentConditionalExpression = function instrumentConditionalExpression(contract, expression) { |
|
||||||
// ----------------------------------------------------------------------------------------------
|
|
||||||
// This is suspended for 0.5.0 which tries to accomodate the new `emit` keyword.
|
|
||||||
// Solc is not allowing us to use the construction `emit SomeEvent()` within the parens :/
|
|
||||||
// Very sad, this is the coolest thing in here.
|
|
||||||
return; |
|
||||||
// ----------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/*contract.branchId += 1; |
|
||||||
|
|
||||||
const startline = (contract.instrumented.slice(0, expression.range[0]).match(/\n/g) || []).length + 1; |
|
||||||
const startcol = expression.range[0] - contract.instrumented.slice(0, expression.range[0]).lastIndexOf('\n') - 1; |
|
||||||
const consequentStartCol = startcol + (contract, expression.trueBody.range[0] - expression.range[0]); |
|
||||||
const consequentEndCol = consequentStartCol + (contract, expression.trueBody.range[1] - expression.trueBody.range[0]); |
|
||||||
const alternateStartCol = startcol + (contract, expression.falseBody.range[0] - expression.range[0]); |
|
||||||
const alternateEndCol = alternateStartCol + (contract, expression.falseBody.range[1] - expression.falseBody.range[0]); |
|
||||||
// NB locations for conditional branches in istanbul are length 1 and associated with the : and ?.
|
|
||||||
contract.branchMap[contract.branchId] = { |
|
||||||
line: startline, |
|
||||||
type: 'cond-expr', |
|
||||||
locations: [{ |
|
||||||
start: { |
|
||||||
line: startline, column: consequentStartCol, |
|
||||||
}, |
|
||||||
end: { |
|
||||||
line: startline, column: consequentEndCol, |
|
||||||
}, |
|
||||||
}, { |
|
||||||
start: { |
|
||||||
line: startline, column: alternateStartCol, |
|
||||||
}, |
|
||||||
end: { |
|
||||||
line: startline, column: alternateEndCol, |
|
||||||
}, |
|
||||||
}], |
|
||||||
}; |
|
||||||
// Right, this could be being used just by itself or as an assignment. In the case of the latter, because
|
|
||||||
// the comma operator doesn't exist, we're going to have to get funky.
|
|
||||||
// if we're on a line by ourselves, this is easier
|
|
||||||
//
|
|
||||||
// Now if we've got to wrap the expression it's being set equal to, do that...
|
|
||||||
|
|
||||||
|
|
||||||
// Wrap the consequent
|
|
||||||
createOrAppendInjectionPoint(contract, expression.trueBody.range[0], { |
|
||||||
type: 'openParen', |
|
||||||
}); |
|
||||||
createOrAppendInjectionPoint(contract, expression.trueBody.range[0], { |
|
||||||
type: 'callBranchEvent', comma: true, branchId: contract.branchId, locationIdx: 0, |
|
||||||
}); |
|
||||||
createOrAppendInjectionPoint(contract, expression.trueBody.range[1], { |
|
||||||
type: 'closeParen', |
|
||||||
}); |
|
||||||
|
|
||||||
// Wrap the alternate
|
_initializeCoverageFields(contract){ |
||||||
createOrAppendInjectionPoint(contract, expression.falseBody.range[0], { |
contract.runnableLines = []; |
||||||
type: 'openParen', |
contract.fnMap = {}; |
||||||
}); |
contract.fnId = 0; |
||||||
createOrAppendInjectionPoint(contract, expression.falseBody.range[0], { |
contract.branchMap = {}; |
||||||
type: 'callBranchEvent', comma: true, branchId: contract.branchId, locationIdx: 1, |
contract.branchId = 0; |
||||||
}); |
contract.statementMap = {}; |
||||||
createOrAppendInjectionPoint(contract, expression.falseBody.range[1], { |
contract.statementId = 0; |
||||||
type: 'closeParen', |
contract.injectionPoints = {}; |
||||||
});*/ |
|
||||||
}; |
|
||||||
|
|
||||||
instrumenter.instrumentStatement = function instrumentStatement(contract, expression) { |
|
||||||
contract.statementId += 1; |
|
||||||
// We need to work out the lines and columns the expression starts and ends
|
|
||||||
const startline = (contract.instrumented.slice(0, expression.range[0]).match(/\n/g) || []).length + 1; |
|
||||||
const startcol = expression.range[0] - contract.instrumented.slice(0, expression.range[0]).lastIndexOf('\n') - 1; |
|
||||||
const expressionContent = contract.instrumented.slice(expression.range[0], expression.range[1] + 1); |
|
||||||
|
|
||||||
const endline = startline + (contract, expressionContent.match('/\n/g') || []).length; |
|
||||||
let endcol; |
|
||||||
if (expressionContent.lastIndexOf('\n') >= 0) { |
|
||||||
endcol = contract.instrumented.slice(expressionContent.lastIndexOf('\n'), expression.range[1]).length; |
|
||||||
} else { |
|
||||||
endcol = startcol + (contract, expressionContent.length - 1); |
|
||||||
} |
|
||||||
contract.statementMap[contract.statementId] = { |
|
||||||
start: { |
|
||||||
line: startline, column: startcol, |
|
||||||
}, |
|
||||||
end: { |
|
||||||
line: endline, column: endcol, |
|
||||||
}, |
|
||||||
}; |
|
||||||
createOrAppendInjectionPoint(contract, expression.range[0], { |
|
||||||
type: 'statement', statementId: contract.statementId, |
|
||||||
}); |
|
||||||
}; |
|
||||||
|
|
||||||
instrumenter.instrumentLine = function instrumentLine(contract, expression) { |
|
||||||
// what's the position of the most recent newline?
|
|
||||||
const startchar = expression.range[0]; |
|
||||||
const endchar = expression.range[1] + 1; |
|
||||||
const lastNewLine = contract.instrumented.slice(0, startchar).lastIndexOf('\n'); |
|
||||||
const nextNewLine = startchar + contract.instrumented.slice(startchar).indexOf('\n'); |
|
||||||
const contractSnipped = contract.instrumented.slice(lastNewLine, nextNewLine); |
|
||||||
const restOfLine = contract.instrumented.slice(endchar, nextNewLine); |
|
||||||
|
|
||||||
if (contract.instrumented.slice(lastNewLine, startchar).trim().length === 0 && |
|
||||||
(restOfLine.replace(';', '').trim().length === 0 || restOfLine.replace(';', '').trim().substring(0, 2) === '//')) { |
|
||||||
createOrAppendInjectionPoint(contract, lastNewLine + 1, { |
|
||||||
type: 'callEvent', |
|
||||||
}); |
|
||||||
} else if (contract.instrumented.slice(lastNewLine, startchar).replace('{', '').trim().length === 0 && |
|
||||||
contract.instrumented.slice(endchar, nextNewLine).replace(/[;}]/g, '').trim().length === 0) { |
|
||||||
createOrAppendInjectionPoint(contract, expression.range[0], { |
|
||||||
type: 'callEvent', |
|
||||||
}); |
|
||||||
} |
} |
||||||
// Is everything before us and after us on this line whitespace?
|
|
||||||
}; |
|
||||||
|
|
||||||
instrumenter.instrumentFunctionDeclaration = function instrumentFunctionDeclaration(contract, expression) { |
|
||||||
contract.fnId += 1; |
|
||||||
const startline = (contract.instrumented.slice(0, expression.range[0]).match(/\n/g) || []).length + 1; |
|
||||||
// We need to work out the lines and columns the function declaration starts and ends
|
|
||||||
const startcol = expression.range[0] - contract.instrumented.slice(0, expression.range[0]).lastIndexOf('\n') - 1; |
|
||||||
const endlineDelta = contract.instrumented.slice(expression.range[0]).indexOf('{'); |
|
||||||
const functionDefinition = contract.instrumented.slice(expression.range[0], expression.range[0] + endlineDelta); |
|
||||||
const endline = startline + (functionDefinition.match(/\n/g) || []).length; |
|
||||||
const endcol = functionDefinition.length - functionDefinition.lastIndexOf('\n'); |
|
||||||
contract.fnMap[contract.fnId] = { |
|
||||||
name: expression.isConstructor ? 'constructor' : expression.name, |
|
||||||
line: startline, |
|
||||||
loc: { |
|
||||||
start: { |
|
||||||
line: startline, column: startcol, |
|
||||||
}, |
|
||||||
end: { |
|
||||||
line: endline, column: endcol, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}; |
|
||||||
createOrAppendInjectionPoint(contract, expression.range[0] + endlineDelta + 1, { |
|
||||||
type: 'callFunctionEvent', fnId: contract.fnId, |
|
||||||
}); |
|
||||||
}; |
|
||||||
|
|
||||||
instrumenter.addNewBranch = function addNewBranch(contract, expression) { |
|
||||||
contract.branchId += 1; |
|
||||||
const startline = (contract.instrumented.slice(0, expression.range[0]).match(/\n/g) || []).length + 1; |
|
||||||
const startcol = expression.range[0] - contract.instrumented.slice(0, expression.range[0]).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, |
|
||||||
}, |
|
||||||
}], |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
instrumenter.instrumentAssertOrRequire = function instrumentAssertOrRequire(contract, expression) { |
|
||||||
instrumenter.addNewBranch(contract, expression); |
|
||||||
createOrAppendInjectionPoint(contract, expression.range[0], { |
|
||||||
type: 'callAssertPreEvent', branchId: contract.branchId, |
|
||||||
}); |
|
||||||
createOrAppendInjectionPoint(contract, expression.range[1] + 2, { |
|
||||||
type: 'callAssertPostEvent', branchId: contract.branchId, |
|
||||||
}); |
|
||||||
}; |
|
||||||
|
|
||||||
instrumenter.instrumentIfStatement = function instrumentIfStatement(contract, expression) { |
|
||||||
instrumenter.addNewBranch(contract, expression); |
/** |
||||||
if (expression.trueBody.type === 'Block') { |
* Per `contractSource`: |
||||||
createOrAppendInjectionPoint(contract, expression.trueBody.range[0] + 1, { |
* - wraps any unbracketed singleton consequents of if, for, while stmts (preprocessor.js) |
||||||
type: 'callBranchEvent', branchId: contract.branchId, locationIdx: 0, |
* - walks the file's AST, creating an instrumentation map (parse.js, registrar.js) |
||||||
|
* - injects `instrumentation` solidity statements into the target solidity source (injector.js) |
||||||
|
* |
||||||
|
* @param {String} contractSource solidity source code |
||||||
|
* @param {String} fileName absolute path to source file |
||||||
|
* @return {Object} instrumented `contract` object |
||||||
|
* { |
||||||
|
* contract: instrumented solidity source code, |
||||||
|
* contractName: contract name, |
||||||
|
* runnableLines: integer |
||||||
|
* } |
||||||
|
* |
||||||
|
*/ |
||||||
|
instrument(contractSource, fileName) { |
||||||
|
const contract = {}; |
||||||
|
|
||||||
|
contract.source = contractSource; |
||||||
|
contract.instrumented = contractSource; |
||||||
|
|
||||||
|
this._initializeCoverageFields(contract); |
||||||
|
|
||||||
|
// First, we run over the original contract to get the source mapping.
|
||||||
|
let ast = SolidityParser.parse(contract.source, {range: true}); |
||||||
|
parse[ast.type](contract, ast); |
||||||
|
const retValue = JSON.parse(JSON.stringify(contract)); // ?????
|
||||||
|
|
||||||
|
// Now, we reset almost everything and use the preprocessor to increase our effectiveness.
|
||||||
|
this._initializeCoverageFields(contract); |
||||||
|
contract.instrumented = preprocess(contract.source); |
||||||
|
|
||||||
|
// Walk the AST, recording injection points
|
||||||
|
ast = SolidityParser.parse(contract.instrumented, {range: true}); |
||||||
|
contract.contractName = ast.children.filter(node => this._isRootNode(node))[0].name; |
||||||
|
parse[ast.type](contract, ast); |
||||||
|
|
||||||
|
// We have to iterate through these points in descending order
|
||||||
|
const sortedPoints = Object.keys(contract.injectionPoints).sort((a, b) => b - a); |
||||||
|
|
||||||
|
sortedPoints.forEach(injectionPoint => { |
||||||
|
|
||||||
|
// Line instrumentation has to happen first
|
||||||
|
contract.injectionPoints[injectionPoint].sort((a, b) => { |
||||||
|
const injections = ['injectBranch', 'injectEmptyBranch', 'injectLine']; |
||||||
|
return injections.indexOf(b.type) - injections.indexOf(a.type); |
||||||
}); |
}); |
||||||
} |
|
||||||
if (expression.falseBody && expression.falseBody.type === 'IfStatement') { |
contract.injectionPoints[injectionPoint].forEach(injection => { |
||||||
// Do nothing - we must be pre-preprocessor, so don't bother instrumenting -
|
this.injector[injection.type]( |
||||||
// when we're actually instrumenting, this will never happen (we've wrapped it in
|
contract, |
||||||
// a block statement)
|
fileName, |
||||||
} else if (expression.falseBody && expression.falseBody.type === 'Block') { |
injectionPoint, |
||||||
createOrAppendInjectionPoint(contract, expression.falseBody.range[0] + 1, { |
injection, |
||||||
type: 'callBranchEvent', branchId: contract.branchId, locationIdx: 1, |
this.instrumentationData |
||||||
|
); |
||||||
}); |
}); |
||||||
} else { |
|
||||||
createOrAppendInjectionPoint(contract, expression.trueBody.range[1] + 1, { |
|
||||||
type: 'callEmptyBranchEvent', branchId: contract.branchId, locationIdx: 1, |
|
||||||
}); |
}); |
||||||
|
|
||||||
|
retValue.runnableLines = contract.runnableLines; |
||||||
|
retValue.contract = contract.instrumented; |
||||||
|
retValue.contractName = contract.contractName; |
||||||
|
|
||||||
|
return retValue; |
||||||
} |
} |
||||||
}; |
} |
||||||
|
|
||||||
module.exports = instrumenter; |
module.exports = Instrumenter; |
||||||
|
@ -0,0 +1,251 @@ |
|||||||
|
/** |
||||||
|
* Registers injections points (e.g source location, type) and their associated data with |
||||||
|
* a contract / instrumentation target. Run during the `parse` step. This data is |
||||||
|
* consumed by the Injector as it modifies the source code in instrumentation's final step. |
||||||
|
*/ |
||||||
|
class Registrar { |
||||||
|
|
||||||
|
constructor(){} |
||||||
|
|
||||||
|
/** |
||||||
|
* Adds injection point to injection points map |
||||||
|
* @param {Object} contract instrumentation target |
||||||
|
* @param {String} key injection point `type` |
||||||
|
* @param {Number} value injection point `id` |
||||||
|
*/ |
||||||
|
_createInjectionPoint(contract, key, value) { |
||||||
|
(contract.injectionPoints[key]) |
||||||
|
? contract.injectionPoints[key].push(value) |
||||||
|
: contract.injectionPoints[key] = [value]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* TODO - idk what this is anymore |
||||||
|
* @param {Object} expression AST node |
||||||
|
*/ |
||||||
|
prePosition(expression) { |
||||||
|
if ( |
||||||
|
expression.right.type === 'ConditionalExpression' && |
||||||
|
expression.left.type === 'MemberExpression' |
||||||
|
) { |
||||||
|
expression.range[0] -= 2; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Registers injections for statement measurements |
||||||
|
* @param {Object} contract instrumentation target |
||||||
|
* @param {Object} expression AST node |
||||||
|
*/ |
||||||
|
statement(contract, expression) { |
||||||
|
const startContract = contract.instrumented.slice(0, expression.range[0]); |
||||||
|
const startline = ( startContract.match(/\n/g) || [] ).length + 1; |
||||||
|
const startcol = expression.range[0] - startContract.lastIndexOf('\n') - 1; |
||||||
|
|
||||||
|
const expressionContent = contract.instrumented.slice( |
||||||
|
expression.range[0], |
||||||
|
expression.range[1] + 1 |
||||||
|
); |
||||||
|
|
||||||
|
const endline = startline + (contract, expressionContent.match('/\n/g') || []).length; |
||||||
|
|
||||||
|
let endcol; |
||||||
|
if (expressionContent.lastIndexOf('\n') >= 0) { |
||||||
|
|
||||||
|
endcol = contract.instrumented.slice( |
||||||
|
expressionContent.lastIndexOf('\n'), |
||||||
|
expression.range[1] |
||||||
|
).length; |
||||||
|
|
||||||
|
} else endcol = startcol + (contract, expressionContent.length - 1); |
||||||
|
|
||||||
|
contract.statementId += 1; |
||||||
|
contract.statementMap[contract.statementId] = { |
||||||
|
start: { line: startline, column: startcol }, |
||||||
|
end: { line: endline, column: endcol }, |
||||||
|
}; |
||||||
|
|
||||||
|
this._createInjectionPoint(contract, expression.range[0], { |
||||||
|
type: 'injectStatement', statementId: contract.statementId, |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Registers injections for line measurements |
||||||
|
* @param {Object} contract instrumentation target |
||||||
|
* @param {Object} expression AST node |
||||||
|
*/ |
||||||
|
line(contract, expression) { |
||||||
|
const startchar = expression.range[0]; |
||||||
|
const endchar = expression.range[1] + 1; |
||||||
|
const lastNewLine = contract.instrumented.slice(0, startchar).lastIndexOf('\n'); |
||||||
|
const nextNewLine = startchar + contract.instrumented.slice(startchar).indexOf('\n'); |
||||||
|
const contractSnipped = contract.instrumented.slice(lastNewLine, nextNewLine); |
||||||
|
const restOfLine = contract.instrumented.slice(endchar, nextNewLine); |
||||||
|
|
||||||
|
if ( |
||||||
|
contract.instrumented.slice(lastNewLine, startchar).trim().length === 0 && |
||||||
|
( |
||||||
|
restOfLine.replace(';', '').trim().length === 0 || |
||||||
|
restOfLine.replace(';', '').trim().substring(0, 2) === '//' |
||||||
|
) |
||||||
|
) |
||||||
|
{ |
||||||
|
this._createInjectionPoint(contract, lastNewLine + 1, { type: 'injectLine' }); |
||||||
|
|
||||||
|
} else if ( |
||||||
|
contract.instrumented.slice(lastNewLine, startchar).replace('{', '').trim().length === 0 && |
||||||
|
contract.instrumented.slice(endchar, nextNewLine).replace(/[;}]/g, '').trim().length === 0) |
||||||
|
{ |
||||||
|
this._createInjectionPoint(contract, expression.range[0], { type: 'injectLine' }); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Registers injections for function measurements |
||||||
|
* @param {Object} contract instrumentation target |
||||||
|
* @param {Object} expression AST node |
||||||
|
*/ |
||||||
|
functionDeclaration(contract, expression) { |
||||||
|
const startContract = contract.instrumented.slice(0, expression.range[0]); |
||||||
|
const startline = ( startContract.match(/\n/g) || [] ).length + 1; |
||||||
|
const startcol = expression.range[0] - startContract.lastIndexOf('\n') - 1; |
||||||
|
|
||||||
|
const endlineDelta = contract.instrumented.slice(expression.range[0]).indexOf('{'); |
||||||
|
const functionDefinition = contract.instrumented.slice( |
||||||
|
expression.range[0], |
||||||
|
expression.range[0] + endlineDelta |
||||||
|
); |
||||||
|
const endline = startline + (functionDefinition.match(/\n/g) || []).length; |
||||||
|
const endcol = functionDefinition.length - functionDefinition.lastIndexOf('\n'); |
||||||
|
|
||||||
|
contract.fnId += 1; |
||||||
|
contract.fnMap[contract.fnId] = { |
||||||
|
name: expression.isConstructor ? 'constructor' : expression.name, |
||||||
|
line: startline, |
||||||
|
loc: { |
||||||
|
start: { line: startline, column: startcol }, |
||||||
|
end: { line: endline, column: endcol }, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
this._createInjectionPoint( |
||||||
|
contract, |
||||||
|
expression.range[0] + endlineDelta + 1, |
||||||
|
{ |
||||||
|
type: 'injectFunction', |
||||||
|
fnId: contract.fnId, |
||||||
|
} |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Registers injections for branch measurements. This generic is consumed by |
||||||
|
* the `assert/require` and `if` registration methods. |
||||||
|
* @param {Object} contract instrumentation target |
||||||
|
* @param {Object} expression AST node |
||||||
|
*/ |
||||||
|
addNewBranch(contract, expression) { |
||||||
|
const startContract = contract.instrumented.slice(0, expression.range[0]); |
||||||
|
const startline = ( startContract.match(/\n/g) || [] ).length + 1; |
||||||
|
const startcol = expression.range[0] - startContract.lastIndexOf('\n') - 1; |
||||||
|
|
||||||
|
contract.branchId += 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, |
||||||
|
}, |
||||||
|
}], |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Registers injections for assert/require statement measurements (branches) |
||||||
|
* @param {Object} contract instrumentation target |
||||||
|
* @param {Object} expression AST node |
||||||
|
*/ |
||||||
|
assertOrRequire(contract, expression) { |
||||||
|
this.addNewBranch(contract, expression); |
||||||
|
this._createInjectionPoint( |
||||||
|
contract, |
||||||
|
expression.range[0], |
||||||
|
{ |
||||||
|
type: 'injectAssertPre', |
||||||
|
branchId: contract.branchId, |
||||||
|
} |
||||||
|
); |
||||||
|
this._createInjectionPoint( |
||||||
|
contract, |
||||||
|
expression.range[1] + 2, |
||||||
|
{ |
||||||
|
type: 'injectAssertPost', |
||||||
|
branchId: contract.branchId, |
||||||
|
} |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Registers injections for if statement measurements (branches) |
||||||
|
* @param {Object} contract instrumentation target |
||||||
|
* @param {Object} expression AST node |
||||||
|
*/ |
||||||
|
ifStatement(contract, expression) { |
||||||
|
this.addNewBranch(contract, expression); |
||||||
|
|
||||||
|
if (expression.trueBody.type === 'Block') { |
||||||
|
this._createInjectionPoint( |
||||||
|
contract, |
||||||
|
expression.trueBody.range[0] + 1, |
||||||
|
{ |
||||||
|
type: 'injectBranch', |
||||||
|
branchId: contract.branchId, |
||||||
|
locationIdx: 0, |
||||||
|
} |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
if (expression.falseBody && expression.falseBody.type === 'IfStatement') { |
||||||
|
|
||||||
|
// Do nothing - we must be pre-preprocessing
|
||||||
|
|
||||||
|
} else if (expression.falseBody && expression.falseBody.type === 'Block') { |
||||||
|
this._createInjectionPoint( |
||||||
|
contract, |
||||||
|
expression.falseBody.range[0] + 1, |
||||||
|
{ |
||||||
|
type: 'injectBranch', |
||||||
|
branchId: contract.branchId, |
||||||
|
locationIdx: 1, |
||||||
|
} |
||||||
|
); |
||||||
|
} else { |
||||||
|
this._createInjectionPoint( |
||||||
|
contract, |
||||||
|
expression.trueBody.range[1] + 1, |
||||||
|
{ |
||||||
|
type: 'injectEmptyBranch', |
||||||
|
branchId: contract.branchId, |
||||||
|
locationIdx: 1, |
||||||
|
} |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = Registrar; |
@ -0,0 +1,121 @@ |
|||||||
|
/** |
||||||
|
* This is logic to instrument ternary conditional assignment statements. Preserving |
||||||
|
* here for the time being, because instrumentation of these became impossible in |
||||||
|
* solc >= 0.5.0 |
||||||
|
*/ |
||||||
|
|
||||||
|
function instrumentAssignmentExpression(contract, expression) { |
||||||
|
|
||||||
|
// This is suspended for 0.5.0 which tries to accomodate the new `emit` keyword.
|
||||||
|
// Solc is not allowing us to use the construction `emit SomeEvent()` within the parens :/
|
||||||
|
return; |
||||||
|
// --------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// The only time we instrument an assignment expression is if there's a conditional expression on
|
||||||
|
// the right
|
||||||
|
/*if (expression.right.type === 'ConditionalExpression') { |
||||||
|
if (expression.left.type === 'DeclarativeExpression' || expression.left.type === 'Identifier') { |
||||||
|
// Then we need to go from bytes32 varname = (conditional expression)
|
||||||
|
// to bytes32 varname; (,varname) = (conditional expression)
|
||||||
|
createOrAppendInjectionPoint(contract, expression.left.range[1], { |
||||||
|
type: 'literal', string: '; (,' + expression.left.name + ')', |
||||||
|
}); |
||||||
|
instrumenter.instrumentConditionalExpression(contract, expression.right); |
||||||
|
} else if (expression.left.type === 'MemberExpression') { |
||||||
|
createOrAppendInjectionPoint(contract, expression.left.range[0], { |
||||||
|
type: 'literal', string: '(,', |
||||||
|
}); |
||||||
|
createOrAppendInjectionPoint(contract, expression.left.range[1], { |
||||||
|
type: 'literal', string: ')', |
||||||
|
}); |
||||||
|
instrumenter.instrumentConditionalExpression(contract, expression.right); |
||||||
|
} else { |
||||||
|
const err = 'Error instrumenting assignment expression @ solidity-coverage/lib/instrumenter.js'; |
||||||
|
console.log(err, contract, expression.left); |
||||||
|
process.exit(); |
||||||
|
} |
||||||
|
}*/ |
||||||
|
}; |
||||||
|
|
||||||
|
function instrumentConditionalExpression(contract, expression) { |
||||||
|
// ----------------------------------------------------------------------------------------------
|
||||||
|
// This is suspended for 0.5.0 which tries to accomodate the new `emit` keyword.
|
||||||
|
// Solc is not allowing us to use the construction `emit SomeEvent()` within the parens :/
|
||||||
|
// Very sad, this is the coolest thing in here.
|
||||||
|
return; |
||||||
|
// ----------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/*contract.branchId += 1; |
||||||
|
|
||||||
|
const startline = (contract.instrumented.slice(0, expression.range[0]).match(/\n/g) || []).length + 1; |
||||||
|
const startcol = expression.range[0] - contract.instrumented.slice(0, expression.range[0]).lastIndexOf('\n') - 1; |
||||||
|
const consequentStartCol = startcol + (contract, expression.trueBody.range[0] - expression.range[0]); |
||||||
|
const consequentEndCol = consequentStartCol + (contract, expression.trueBody.range[1] - expression.trueBody.range[0]); |
||||||
|
const alternateStartCol = startcol + (contract, expression.falseBody.range[0] - expression.range[0]); |
||||||
|
const alternateEndCol = alternateStartCol + (contract, expression.falseBody.range[1] - expression.falseBody.range[0]); |
||||||
|
// NB locations for conditional branches in istanbul are length 1 and associated with the : and ?.
|
||||||
|
contract.branchMap[contract.branchId] = { |
||||||
|
line: startline, |
||||||
|
type: 'cond-expr', |
||||||
|
locations: [{ |
||||||
|
start: { |
||||||
|
line: startline, column: consequentStartCol, |
||||||
|
}, |
||||||
|
end: { |
||||||
|
line: startline, column: consequentEndCol, |
||||||
|
}, |
||||||
|
}, { |
||||||
|
start: { |
||||||
|
line: startline, column: alternateStartCol, |
||||||
|
}, |
||||||
|
end: { |
||||||
|
line: startline, column: alternateEndCol, |
||||||
|
}, |
||||||
|
}], |
||||||
|
}; |
||||||
|
// Right, this could be being used just by itself or as an assignment. In the case of the latter, because
|
||||||
|
// the comma operator doesn't exist, we're going to have to get funky.
|
||||||
|
// if we're on a line by ourselves, this is easier
|
||||||
|
//
|
||||||
|
// Now if we've got to wrap the expression it's being set equal to, do that...
|
||||||
|
|
||||||
|
|
||||||
|
// Wrap the consequent
|
||||||
|
createOrAppendInjectionPoint(contract, expression.trueBody.range[0], { |
||||||
|
type: 'openParen', |
||||||
|
}); |
||||||
|
createOrAppendInjectionPoint(contract, expression.trueBody.range[0], { |
||||||
|
type: 'callBranchEvent', comma: true, branchId: contract.branchId, locationIdx: 0, |
||||||
|
}); |
||||||
|
createOrAppendInjectionPoint(contract, expression.trueBody.range[1], { |
||||||
|
type: 'closeParen', |
||||||
|
}); |
||||||
|
|
||||||
|
// Wrap the alternate
|
||||||
|
createOrAppendInjectionPoint(contract, expression.falseBody.range[0], { |
||||||
|
type: 'openParen', |
||||||
|
}); |
||||||
|
createOrAppendInjectionPoint(contract, expression.falseBody.range[0], { |
||||||
|
type: 'callBranchEvent', comma: true, branchId: contract.branchId, locationIdx: 1, |
||||||
|
}); |
||||||
|
createOrAppendInjectionPoint(contract, expression.falseBody.range[1], { |
||||||
|
type: 'closeParen', |
||||||
|
});*/ |
||||||
|
}; |
||||||
|
|
||||||
|
// Paren / Literal injectors
|
||||||
|
/* |
||||||
|
|
||||||
|
injector.openParen = function injectOpenParen(contract, fileName, injectionPoint, injection) { |
||||||
|
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + '(' + contract.instrumented.slice(injectionPoint); |
||||||
|
}; |
||||||
|
|
||||||
|
injector.closeParen = function injectCloseParen(contract, fileName, injectionPoint, injection) { |
||||||
|
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + ')' + contract.instrumented.slice(injectionPoint); |
||||||
|
}; |
||||||
|
|
||||||
|
injector.literal = function injectLiteral(contract, fileName, injectionPoint, injection) { |
||||||
|
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + injection.string + contract.instrumented.slice(injectionPoint); |
||||||
|
}; |
||||||
|
|
||||||
|
*/ |
@ -1,14 +0,0 @@ |
|||||||
module.exports = function truffleConfig(port, gasLimit, gasPrice) { |
|
||||||
return ` |
|
||||||
module.exports = { |
|
||||||
networks: { |
|
||||||
development: { |
|
||||||
host: "localhost", |
|
||||||
network_id: "*", |
|
||||||
port: ${port}, |
|
||||||
gas: ${gasLimit}, |
|
||||||
gasPrice: ${gasPrice} |
|
||||||
} |
|
||||||
} |
|
||||||
};`;
|
|
||||||
}; |
|
@ -1,24 +0,0 @@ |
|||||||
/* eslint-env node, mocha */ |
|
||||||
|
|
||||||
const assert = require('assert'); |
|
||||||
const fs = require('fs'); |
|
||||||
|
|
||||||
// Fake event for Simple.sol
|
|
||||||
const fakeEvent = {"address":"6d6cf716c2a7672047e15a255d4c9624db60f215","topics":["34b35f4b1a8c3eb2caa69f05fb5aadc827cedd2d8eb3bb3623b6c4bba3baec17"],"data":"00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000003a2f55736572732f757365722f53697465732f73632d666f726b732f6d657461636f696e2f636f6e7472616374732f4d657461436f696e2e736f6c000000000000"} |
|
||||||
|
|
||||||
/* { |
|
||||||
address: '7c548f8a5ba3a37774440587743bb50f58c7e91c', |
|
||||||
topics: ['1accf53d733f86cbefdf38d52682bc905cf6715eb3d860be0b5b052e58b0741d'], |
|
||||||
data: '0', |
|
||||||
};*/ |
|
||||||
// Tests whether or not the testCommand option is invoked by exec.js
|
|
||||||
// Mocha's default timeout is 2000 - here we fake the creation of
|
|
||||||
// allFiredEvents at 4000.
|
|
||||||
describe('Test uses mocha', () => { |
|
||||||
it('should run "mocha --timeout 5000" successfully', done => { |
|
||||||
setTimeout(() => { |
|
||||||
fs.writeFileSync('./../allFiredEvents', JSON.stringify(fakeEvent) + '\n'); |
|
||||||
done(); |
|
||||||
}, 4000); |
|
||||||
}); |
|
||||||
}); |
|
@ -1,21 +0,0 @@ |
|||||||
/* eslint-env node, mocha */ |
|
||||||
/* global artifacts, contract, assert */ |
|
||||||
|
|
||||||
const Events = artifacts.require('./Events.sol'); |
|
||||||
|
|
||||||
contract('Events', accounts => { |
|
||||||
it('logs events correctly', done => { |
|
||||||
const loggedEvents = []; |
|
||||||
Events.deployed().then(instance => { |
|
||||||
const allEvents = instance.allEvents(); |
|
||||||
|
|
||||||
allEvents.on("data", event => { loggedEvents.push(event); }); |
|
||||||
|
|
||||||
instance.test(5).then(() => { |
|
||||||
const bad = loggedEvents.filter(e => e.event !== 'LogEventOne' && e.event !== 'LogEventTwo'); |
|
||||||
assert(bad.length === 0, 'Did not filter events correctly'); |
|
||||||
done(); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
@ -1,274 +0,0 @@ |
|||||||
/* eslint-env node, mocha */ |
|
||||||
|
|
||||||
const solc = require('solc'); |
|
||||||
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('if, else, and else if statements', () => { |
|
||||||
const filePath = path.resolve('./test.sol'); |
|
||||||
const pathPrefix = './'; |
|
||||||
|
|
||||||
it('should compile after instrumenting multiple if-elses', () => { |
|
||||||
const contract = util.getCode('if/else-if-unbracketed-multi.sol'); |
|
||||||
const info = getInstrumentedVersion(contract, filePath); |
|
||||||
const output = JSON.parse(solc.compile(util.codeToCompilerInput(info.contract))); |
|
||||||
util.report(output.errors); |
|
||||||
}); |
|
||||||
|
|
||||||
it('should compile after instrumenting unbracketed if-elses', () => { |
|
||||||
const contract = util.getCode('if/if-else-no-brackets.sol'); |
|
||||||
const info = getInstrumentedVersion(contract, filePath); |
|
||||||
const output = JSON.parse(solc.compile(util.codeToCompilerInput(info.contract))); |
|
||||||
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, filePath); |
|
||||||
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(); |
|
||||||
}).catch(done); |
|
||||||
}); |
|
||||||
|
|
||||||
// Runs: a(1) => if (x == 1) x = 2;
|
|
||||||
it('should cover an unbracketed if consequent (single line)', done => { |
|
||||||
const contract = util.getCode('if/if-no-brackets.sol'); |
|
||||||
const info = getInstrumentedVersion(contract, filePath); |
|
||||||
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(done); |
|
||||||
}); |
|
||||||
|
|
||||||
it('should cover an if statement with multiline bracketed consequent', done => { |
|
||||||
const contract = util.getCode('if/if-with-brackets-multiline.sol'); |
|
||||||
const info = getInstrumentedVersion(contract, filePath); |
|
||||||
const coverage = new CoverageMap(); |
|
||||||
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(); |
|
||||||
}).catch(done); |
|
||||||
}); |
|
||||||
|
|
||||||
// Runs: a(1) => if (x == 1)\n x = 3;
|
|
||||||
it('should cover an unbracketed if consequent (multi-line)', done => { |
|
||||||
const contract = util.getCode('if/if-no-brackets-multiline.sol'); |
|
||||||
const info = getInstrumentedVersion(contract, filePath); |
|
||||||
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(); |
|
||||||
}).catch(done); |
|
||||||
}); |
|
||||||
|
|
||||||
it('should cover a simple if statement with a failing condition', done => { |
|
||||||
const contract = util.getCode('if/if-with-brackets.sol'); |
|
||||||
const info = getInstrumentedVersion(contract, filePath); |
|
||||||
const coverage = new CoverageMap(); |
|
||||||
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(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, filePath); |
|
||||||
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(); |
|
||||||
}).catch(done); |
|
||||||
}); |
|
||||||
|
|
||||||
it('should cover an if statement with an unbracketed alternate', done => { |
|
||||||
const contract = util.getCode('if/else-without-brackets.sol'); |
|
||||||
const info = getInstrumentedVersion(contract, filePath); |
|
||||||
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(); |
|
||||||
}).catch(done); |
|
||||||
}); |
|
||||||
|
|
||||||
it('should cover an else if statement with an unbracketed alternate', done => { |
|
||||||
const contract = util.getCode('if/else-if-without-brackets.sol'); |
|
||||||
const info = getInstrumentedVersion(contract, filePath); |
|
||||||
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: 0, |
|
||||||
}); |
|
||||||
assert.deepEqual(mapping[filePath].b, { |
|
||||||
1: [0, 1], 2: [0, 1] |
|
||||||
}); |
|
||||||
assert.deepEqual(mapping[filePath].s, { |
|
||||||
1: 1, 2: 0, 3: 1, 4: 0 |
|
||||||
}); |
|
||||||
assert.deepEqual(mapping[filePath].f, { |
|
||||||
1: 1, |
|
||||||
}); |
|
||||||
done(); |
|
||||||
}).catch(done); |
|
||||||
}); |
|
||||||
|
|
||||||
it('should cover nested if statements with missing else statements', done => { |
|
||||||
const contract = util.getCode('if/nested-if-missing-else.sol'); |
|
||||||
const info = getInstrumentedVersion(contract, filePath); |
|
||||||
const coverage = new CoverageMap(); |
|
||||||
coverage.addContract(info, filePath); |
|
||||||
vm.execute(info.contract, 'a', [2, 3, 3]).then(events => { |
|
||||||
const mapping = coverage.generate(events, pathPrefix); |
|
||||||
assert.deepEqual(mapping[filePath].l, { |
|
||||||
5: 1, 7: 1, |
|
||||||
}); |
|
||||||
assert.deepEqual(mapping[filePath].b, { |
|
||||||
1: [0, 1], 2: [1, 0], 3: [1, 0], |
|
||||||
}); |
|
||||||
assert.deepEqual(mapping[filePath].s, { |
|
||||||
1: 1, 2: 1, 3: 1, |
|
||||||
}); |
|
||||||
assert.deepEqual(mapping[filePath].f, { |
|
||||||
1: 1, |
|
||||||
}); |
|
||||||
done(); |
|
||||||
}).catch(done); |
|
||||||
}); |
|
||||||
|
|
||||||
it('should cover if-elseif-else statements that are at the same depth as each other', done => { |
|
||||||
const contract = util.getCode('if/if-elseif-else.sol'); |
|
||||||
const info = getInstrumentedVersion(contract, filePath); |
|
||||||
const coverage = new CoverageMap(); |
|
||||||
coverage.addContract(info, filePath); |
|
||||||
vm.execute(info.contract, 'a', [2, 3, 3]).then(events => { |
|
||||||
const mapping = coverage.generate(events, pathPrefix); |
|
||||||
assert.deepEqual(mapping[filePath].l, { |
|
||||||
5: 1, 6: 0, 8: 1, 10: 0, 13: 1, 14: 0, 16: 1, 18: 0, |
|
||||||
}); |
|
||||||
assert.deepEqual(mapping[filePath].b, { |
|
||||||
1: [0, 1], 2: [1, 0], 3: [0, 1], 4: [1, 0] |
|
||||||
}); |
|
||||||
assert.deepEqual(mapping[filePath].s, { |
|
||||||
1: 1, 2: 0, 3: 1, 4: 1, 5: 0, 6: 1, 7: 0, 8: 1, 9: 1, 10: 0, |
|
||||||
}); |
|
||||||
assert.deepEqual(mapping[filePath].f, { |
|
||||||
1: 1, |
|
||||||
}); |
|
||||||
done(); |
|
||||||
}).catch(done); |
|
||||||
}); |
|
||||||
}); |
|
@ -1,6 +1,3 @@ |
|||||||
/* eslint-env node, mocha */ |
|
||||||
/* global artifacts, contract, assert */ |
|
||||||
|
|
||||||
const Owned = artifacts.require('./Owned.sol'); |
const Owned = artifacts.require('./Owned.sol'); |
||||||
const Proxy = artifacts.require('./Proxy.sol'); |
const Proxy = artifacts.require('./Proxy.sol'); |
||||||
|
|
@ -1,6 +1,3 @@ |
|||||||
/* eslint-env node, mocha */ |
|
||||||
/* global artifacts, contract, assert */ |
|
||||||
|
|
||||||
const Simple = artifacts.require('./Simple.sol'); |
const Simple = artifacts.require('./Simple.sol'); |
||||||
|
|
||||||
contract('Simple', () => { |
contract('Simple', () => { |
@ -0,0 +1,9 @@ |
|||||||
|
pragma solidity ^0.5.0; |
||||||
|
|
||||||
|
contract Test { |
||||||
|
function a(bool _a, bool _b, bool _c) public { |
||||||
|
require(_a && |
||||||
|
_b && |
||||||
|
_c); |
||||||
|
} |
||||||
|
} |
@ -1,8 +1,8 @@ |
|||||||
pragma solidity ^0.5.0; |
pragma solidity ^0.5.0; |
||||||
|
|
||||||
import "./../assets/Face.sol"; |
import "./../../externalSources/Face.sol"; |
||||||
import "./../assets/PureView.sol"; |
import "./../../externalSources/PureView.sol"; |
||||||
import "./../assets/CLibrary.sol"; |
import "./../../externalSources/CLibrary.sol"; |
||||||
|
|
||||||
contract TotallyPure is PureView, Face { |
contract TotallyPure is PureView, Face { |
||||||
uint onehundred = 99; |
uint onehundred = 99; |
@ -1,3 +1,5 @@ |
|||||||
|
pragma solidity ^0.5.0; |
||||||
|
|
||||||
contract Test { |
contract Test { |
||||||
struct Fn { |
struct Fn { |
||||||
function(bytes32) internal view returns(bool) startConditions; |
function(bytes32) internal view returns(bool) startConditions; |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue