diff --git a/bin/exec.js b/bin/exec.js old mode 100644 new mode 100755 diff --git a/lib/app.js b/lib/app.js index 1dffa87..c5138d4 100644 --- a/lib/app.js +++ b/lib/app.js @@ -45,6 +45,7 @@ class App { this.copyPackages = config.copyPackages || []; // Only copy specific node_modules packages into coverageEnv this.testrpcOptions = config.testrpcOptions || null; // Options for testrpc-sc this.testCommand = config.testCommand || null; // Optional test command + this.testCommand = config.compileCommand || null; // Optional compile command this.setLoggingLevel(config.silent); } @@ -207,6 +208,29 @@ class App { } } + /** + * Run truffle compile (or config.compileCommand) over instrumented contracts in the + * coverage environment folder. Shell cd command needs to be invoked + * as its own statement for command line options to work, apparently. + */ + runCompileCommand() { + try { + const defaultCommand = `truffle compile ${this.network} ${this.silence}`; + const command = this.compileCommand || defaultCommand; + this.log(`Running: ${command}\n(this can take a few seconds)...`); + shell.cd(this.coverageDir); + shell.exec(command); + this.testsErrored = shell.error(); + shell.cd('./..'); + } catch (err) { + const msg = + ` + There was an error compiling the contracts. + `; + this.cleanUp(msg + err); + } + } + /** * Generate coverage / write coverage report / run istanbul */ @@ -305,6 +329,24 @@ class App { fs.writeFileSync(contractPath, contractProcessed); } }); + // First, compile the instrumented contracts + this.runCompileCommand(); + // Now, run through the generated ABIs and reset all pure/view/constant functions + // so that truffle etc uses 'call' on them. + for (let i = 0; i < Object.keys(this.coverage.coverage).length; i += 1) { + const canonicalPath = Object.keys(this.coverage.coverage)[i]; + const contractName = path.basename(canonicalPath, '.sol'); + const abiPath = this.platformNeutralPath(this.coverageDir + '/build/contracts/' + contractName + '.json'); + const abi = fs.readFileSync(abiPath); + const abiJson = JSON.parse(abi); + for (let j = 0; j < abiJson.abi.length; j += 1) { + const func = abiJson.abi[j]; + if (this.coverage.coverage[canonicalPath].pureFunctionNames.indexOf(func.name) > -1) { + func.constant = true; + } + } + fs.writeFileSync(abiPath, JSON.stringify(abiJson)); + } } /** diff --git a/lib/coverageMap.js b/lib/coverageMap.js index 89d994c..f355903 100644 --- a/lib/coverageMap.js +++ b/lib/coverageMap.js @@ -66,6 +66,7 @@ module.exports = class CoverageMap { for (let x = 1; x <= Object.keys(info.statementMap).length; x++) { this.coverage[canonicalContractPath].s[x] = 0; } + this.coverage[canonicalContractPath].pureFunctionNames = info.pureFunctionNames; const keccakhex = (x => { const hash = new keccak(256); // eslint-disable-line new-cap diff --git a/lib/instrumentSolidity.js b/lib/instrumentSolidity.js index acc227b..fd053d2 100644 --- a/lib/instrumentSolidity.js +++ b/lib/instrumentSolidity.js @@ -18,6 +18,7 @@ module.exports = function instrumentSolidity(contractSource, fileName) { contract.statementMap = {}; contract.statementId = 0; contract.injectionPoints = {}; + contract.pureFunctionNames = []; // First, we run over the original contract to get the source mapping. let ast = SolidityParser.parse(contract.source); diff --git a/lib/parse.js b/lib/parse.js index f00d2ec..d85bc46 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -81,6 +81,13 @@ parse.ForStatement = function parseForStatement(contract, expression) { parse.FunctionDeclaration = function parseFunctionDeclaration(contract, expression) { parse.Modifiers(contract, expression.modifiers); + if (expression.modifiers) { + for (let i = 0; i < expression.modifiers.length; i++) { + if (['pure', 'constant', 'view'].indexOf(expression.modifiers[i].name) > -1) { + contract.pureFunctionNames.push(expression.name); + } + } + } if (expression.body) { instrumenter.instrumentFunctionDeclaration(contract, expression); diff --git a/test/cli/totallyPure.js b/test/cli/totallyPure.js index 4e31b72..30293d9 100644 --- a/test/cli/totallyPure.js +++ b/test/cli/totallyPure.js @@ -11,35 +11,41 @@ contract('TotallyPure', accounts => { it('calls an imported, inherited pure function', async () => { const instance = await TotallyPure.deployed(); - const value = await instance.isPure.call(4, 5); + const value = await instance.isPure(4, 5); assert.equal(value.toNumber(), 20); }); it('calls an imported, inherited view function', async () => { const instance = await TotallyPure.deployed(); - const value = await instance.isView.call(); + const value = await instance.isView(); assert.equal(value.toNumber(), 5); }); it('calls an imported, inherited constant function', async () => { const instance = await TotallyPure.deployed(); - const value = await instance.isConstant.call(); + const value = await instance.isConstant(); assert.equal(value.toNumber(), 99); }); it('overrides an imported, inherited abstract pure function', async () => { const instance = await TotallyPure.deployed(); - const value = await instance.bePure.call(4, 5); + const value = await instance.bePure(4, 5); assert.equal(value.toNumber(), 9); }); it('overrides an imported, inherited abstract view function', async () => { const instance = await TotallyPure.deployed(); - const value = await instance.beView.call(); + const value = await instance.beView(); assert.equal(value.toNumber(), 99); }); it('overrides an imported, inherited abstract constant function', async () => { + const instance = await TotallyPure.deployed(); + const value = await instance.beConstant(); + assert.equal(value.toNumber(), 99); + }); + + it('overrides an imported, inherited abstract constant function, and uses .call()', async () => { const instance = await TotallyPure.deployed(); const value = await instance.beConstant.call(); assert.equal(value.toNumber(), 99);