From b1fa110d8943866d4f151ad130369f8ed249c34f Mon Sep 17 00:00:00 2001 From: cgewecke Date: Tue, 28 Mar 2017 16:13:48 -0700 Subject: [PATCH 01/51] Add truffle fork dep, exec.js --- .eslintrc | 1 + exec.js | 140 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 exec.js diff --git a/.eslintrc b/.eslintrc index 66e2c2c..c3878f7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -40,6 +40,7 @@ "semi": [2, "always"], "no-unused-vars": 0, "import/extensions": [0], + "import/no-dynamic-require": [0], "arrow-parens":[2, "as-needed"], "no-plusplus":[2, { "allowForLoopAfterthoughts": true }], "no-bitwise": [2], diff --git a/exec.js b/exec.js new file mode 100644 index 0000000..2b26e64 --- /dev/null +++ b/exec.js @@ -0,0 +1,140 @@ + + +const shell = require('shelljs'); +const fs = require('fs'); +const path = require('path'); +const argv = require('yargs').argv; +const childprocess = require('child_process'); +const SolidityCoder = require('web3/lib/solidity/coder.js'); +const getInstrumentedVersion = require('./instrumentSolidity.js'); +const CoverageMap = require('./coverageMap.js'); + +const coverage = new CoverageMap(); +const coverageDir = './coverageEnv'; +let log = () => {}; +let workingDir = './..'; +const gasLimit = '0xfffffffffff'; +let port = 8555; +let testrpcProcess = null; +let events = null; +let silence = ''; + +// --------------------------------------- Script -------------------------------------------------- +if (argv.dir) workingDir = argv.dir; // Working dir relative to solcover +if (argv.port) port = argv.port; // Testrpc port + +if (argv.silent) { // Log level + silence = '> /dev/null 2>&1'; +} else { + log = console.log; +} + +// Patch our local testrpc if necessary & Run the modified testrpc with large block limit, +// on (hopefully) unused port. Changes here should be also be added to the before() block +// of test/run.js +if (!argv.norpc) { + if (!shell.test('-e', './node_modules/ethereumjs-vm/lib/opFns.js.orig')) { + log('Patch local testrpc...'); + shell.exec('patch -b ./node_modules/ethereumjs-vm/lib/opFns.js ./hookIntoEvents.patch'); + } + log(`Launching testrpc on port ${port}`); + try { + const command = `./node_modules/ethereumjs-testrpc/bin/testrpc --gasLimit ${gasLimit} --port ${port}`; + testrpcProcess = childprocess.exec(command); + } catch (err) { + const msg = `There was a problem launching testrpc: ${err}`; + cleanUp(msg); + } +} +// Generate a copy of the target truffle project configured for solcover. +// NB: the following assumes that the target's truffle.js doesn't specify a custom build with an +// atypical directory structure or depend on the options solcover will change: port, gasLimit, +// gasPrice. +log('Generating coverage environment'); + +const truffleConfig = require(`${workingDir}/truffle.js`); +truffleConfig.networks.development.port = port; +truffleConfig.networks.development.gas = 0xfffffffffff; +truffleConfig.networks.development.gasPrice = 0x01; + +shell.mkdir(`${coverageDir}`); +shell.cp('-R', `${workingDir}/contracts`, `${coverageDir}`); +shell.cp('-R', `${workingDir}/migrations`, `${coverageDir}`); +shell.cp('-R', `${workingDir}/test`, `${coverageDir}`); + +fs.writeFileSync(`${coverageDir}/truffle.js`, `module.exports = ${JSON.stringify(truffleConfig)}`); + +// For each contract in originalContracts, get the instrumented version +try { + shell.ls(`${coverageDir}/contracts/**/*.sol`).forEach(file => { + if (file !== `${coverageDir}/contracts/Migrations.sol`) { + log('Instrumenting ', file); + const canonicalContractPath = path.resolve(`${workingDir}/contracts/${path.basename(file)}`); + const contract = fs.readFileSync(canonicalContractPath).toString(); + const instrumentedContractInfo = getInstrumentedVersion(contract, canonicalContractPath); + fs.writeFileSync(`${coverageDir}/contracts/${path.basename(file)}`, instrumentedContractInfo.contract); + coverage.addContract(instrumentedContractInfo, canonicalContractPath); + } + }); +} catch (err) { + cleanUp(err); +} + +// Run truffle test on instrumented contracts +try { + log('Launching Truffle (this can take a few seconds)...'); + const truffle = './../node_modules/truffle/cli.js'; + const command = `cd coverageEnv && ${truffle} test ${silence}`; + shell.exec(command); +} catch (err) { + cleanUp(err); +} + +// Get events fired during instrumented contracts execution. +try { + events = fs.readFileSync('./allFiredEvents').toString().split('\n'); + events.pop(); +} catch (err) { + const msg = + ` + There was an error generating coverage. Possible reasons include: + 1. Another application is using port ${port} + 2. Truffle crashed because your tests errored + + `; + cleanUp(msg + err); +} + +// Generate coverage, write coverage report / run istanbul +try { + coverage.generate(events, `${coverageDir}/contracts/`); + fs.writeFileSync('./coverage.json', JSON.stringify(coverage.coverage)); + shell.exec(`./node_modules/istanbul/lib/cli.js report lcov ${silence}`); +} catch (err) { + const msg = 'There was a problem generating producing the coverage map / running Istanbul.\n'; + cleanUp(msg + err); +} + +// Finish +cleanUp(); + +// --------------------------------------- Utilities ----------------------------------------------- +/** + * Removes coverage build artifacts, kills testrpc. + * Exits (1) and prints msg on error, exits (0) otherwise. + * @param {String} err error message + */ +function cleanUp(err) { + log('Cleaning up...'); + shell.config.silent = true; + shell.rm('-Rf', `${coverageDir}`); + shell.rm('./allFiredEvents'); + if (testrpcProcess) { testrpcProcess.kill(); } + + if (err) { + log(`${err}\nExiting without generating coverage...`); + process.exit(1); + } else { + process.exit(0); + } +} diff --git a/package.json b/package.json index 25c3156..24fd19f 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "mkdirp": "^0.5.1", "shelljs": "^0.7.4", "sol-explore": "^1.6.2", - "solidity-parser": "0.3.0" + "solidity-parser": "0.3.0", + "truffle": "git+https://github.com/cgewecke/truffle.git" }, "devDependencies": { "crypto-js": "^3.1.9-1", From f1de0f42df0284be3f44bbddf457ac3266de8d0a Mon Sep 17 00:00:00 2001 From: cgewecke Date: Tue, 28 Mar 2017 19:19:49 -0700 Subject: [PATCH 02/51] exec.js test suite updated for truffle 3 --- exec.js | 17 ++-- runCoveredTests.js | 61 ----------- test/run.js | 168 +++++++++++++++++++++++++++++++ test/run/block-gas-limit.js | 6 ++ test/run/empty.js | 6 ++ test/run/only-call.js | 10 ++ test/run/simple.js | 14 +++ test/run/sol-parse-fail.js | 16 +++ test/run/truffle-crash.js | 10 ++ test/run/truffle-test-fail.js | 15 +++ test/sources/run/Empty.sol | 4 + test/sources/run/Expensive.sol | 15 +++ test/sources/run/Migrations.sol | 20 ++++ test/sources/run/OnlyCall.sol | 11 ++ test/sources/run/Simple.sol | 13 +++ test/sources/run/SimpleError.sol | 14 +++ test/util/mockTruffle.js | 93 +++++++++++++++++ 17 files changed, 424 insertions(+), 69 deletions(-) delete mode 100644 runCoveredTests.js create mode 100644 test/run.js create mode 100644 test/run/block-gas-limit.js create mode 100644 test/run/empty.js create mode 100644 test/run/only-call.js create mode 100644 test/run/simple.js create mode 100644 test/run/sol-parse-fail.js create mode 100644 test/run/truffle-crash.js create mode 100644 test/run/truffle-test-fail.js create mode 100644 test/sources/run/Empty.sol create mode 100644 test/sources/run/Expensive.sol create mode 100644 test/sources/run/Migrations.sol create mode 100644 test/sources/run/OnlyCall.sol create mode 100644 test/sources/run/Simple.sol create mode 100644 test/sources/run/SimpleError.sol create mode 100644 test/util/mockTruffle.js diff --git a/exec.js b/exec.js index 2b26e64..3880032 100644 --- a/exec.js +++ b/exec.js @@ -1,5 +1,3 @@ - - const shell = require('shelljs'); const fs = require('fs'); const path = require('path'); @@ -9,21 +7,24 @@ const SolidityCoder = require('web3/lib/solidity/coder.js'); const getInstrumentedVersion = require('./instrumentSolidity.js'); const CoverageMap = require('./coverageMap.js'); +const gasLimit = '0xfffffffffff'; const coverage = new CoverageMap(); const coverageDir = './coverageEnv'; +//const _MODULES_ = 'node_modules/solcover/node_modules'; +const _MODULES_ = 'node_modules'; let log = () => {}; let workingDir = './..'; -const gasLimit = '0xfffffffffff'; let port = 8555; let testrpcProcess = null; let events = null; let silence = ''; // --------------------------------------- Script -------------------------------------------------- + if (argv.dir) workingDir = argv.dir; // Working dir relative to solcover if (argv.port) port = argv.port; // Testrpc port -if (argv.silent) { // Log level +if (argv.silent) { // Log level silence = '> /dev/null 2>&1'; } else { log = console.log; @@ -33,13 +34,13 @@ if (argv.silent) { // Log level // on (hopefully) unused port. Changes here should be also be added to the before() block // of test/run.js if (!argv.norpc) { - if (!shell.test('-e', './node_modules/ethereumjs-vm/lib/opFns.js.orig')) { + if (!shell.test('-e', `./${_MODULES_}/ethereumjs-vm/lib/opFns.js.orig`)) { log('Patch local testrpc...'); - shell.exec('patch -b ./node_modules/ethereumjs-vm/lib/opFns.js ./hookIntoEvents.patch'); + shell.exec(`patch -b ./${_MODULES_}/ethereumjs-vm/lib/opFns.js ./hookIntoEvents.patch`); } log(`Launching testrpc on port ${port}`); try { - const command = `./node_modules/ethereumjs-testrpc/bin/testrpc --gasLimit ${gasLimit} --port ${port}`; + const command = `./${_MODULES_}/ethereumjs-testrpc/bin/testrpc --gasLimit ${gasLimit} --port ${port}`; testrpcProcess = childprocess.exec(command); } catch (err) { const msg = `There was a problem launching testrpc: ${err}`; @@ -109,7 +110,7 @@ try { try { coverage.generate(events, `${coverageDir}/contracts/`); fs.writeFileSync('./coverage.json', JSON.stringify(coverage.coverage)); - shell.exec(`./node_modules/istanbul/lib/cli.js report lcov ${silence}`); + shell.exec(`./${_MODULES_}/istanbul/lib/cli.js report lcov ${silence}`); } catch (err) { const msg = 'There was a problem generating producing the coverage map / running Istanbul.\n'; cleanUp(msg + err); diff --git a/runCoveredTests.js b/runCoveredTests.js deleted file mode 100644 index 71c9302..0000000 --- a/runCoveredTests.js +++ /dev/null @@ -1,61 +0,0 @@ -const Web3 = require('web3'); -const shell = require('shelljs'); -const SolidityCoder = require('web3/lib/solidity/coder.js'); -const getInstrumentedVersion = require('./instrumentSolidity.js'); -const fs = require('fs'); -const path = require('path'); -const CoverageMap = require('./coverageMap.js'); -const mkdirp = require('mkdirp'); -const childprocess = require('child_process'); - -const coverage = new CoverageMap(); -const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')); - -// Patch our local testrpc if necessary -if (!shell.test('-e', './node_modules/ethereumjs-vm/lib/opFns.js.orig')) { - console.log('patch local testrpc...'); - shell.exec('patch -b ./node_modules/ethereumjs-vm/lib/opFns.js ./hookIntoEvents.patch'); -} -// Run the modified testrpc with large block limit -const testrpcProcess = childprocess.exec('./node_modules/ethereumjs-testrpc/bin/testrpc --gasLimit 0xfffffffffffff --gasPrice 0x1'); - -if (shell.test('-d', '../originalContracts')) { - console.log('There is already an "originalContracts" directory in your truffle directory.\n' + - 'This is probably due to a previous solcover failure.\n' + - 'Please make sure the ./contracts/ directory contains your contracts (perhaps by copying them from originalContracts), ' + - 'and then delete the originalContracts directory.'); - process.exit(1); -} - -shell.mv('./../contracts/', './../originalContracts/'); -shell.mkdir('./../contracts/'); -// For each contract in originalContracts, get the instrumented version -shell.ls('./../originalContracts/**/*.sol').forEach(file => { - if (file !== 'originalContracts/Migrations.sol') { - const canonicalContractPath = path.resolve(file); - - console.log('instrumenting ', canonicalContractPath); - const contract = fs.readFileSync(canonicalContractPath).toString(); - const instrumentedContractInfo = getInstrumentedVersion(contract, canonicalContractPath); - mkdirp.sync(path.dirname(canonicalContractPath.replace('originalContracts', 'contracts'))); - fs.writeFileSync(canonicalContractPath.replace('originalContracts', 'contracts'), instrumentedContractInfo.contract); - coverage.addContract(instrumentedContractInfo, canonicalContractPath); - } -}); -shell.cp('./../originalContracts/Migrations.sol', './../contracts/Migrations.sol'); - -shell.rm('./allFiredEvents'); // Delete previous results -shell.exec('truffle test --network test'); - -const events = fs.readFileSync('./allFiredEvents').toString().split('\n'); -events.pop(); -// The pop here isn't a bug - there is an empty line at the end of this file, so we -// don't want to include it as an event. -coverage.generate(events, './../originalContracts/'); - -fs.writeFileSync('./coverage.json', JSON.stringify(coverage.coverage)); - -shell.exec('./node_modules/istanbul/lib/cli.js report lcov'); -testrpcProcess.kill(); -shell.rm('-rf', './../contracts'); -shell.mv('./../originalContracts', './../contracts'); diff --git a/test/run.js b/test/run.js new file mode 100644 index 0000000..57181bb --- /dev/null +++ b/test/run.js @@ -0,0 +1,168 @@ + + +const assert = require('assert'); +const shell = require('shelljs'); +const fs = require('fs'); +const childprocess = require('child_process'); +const mock = require('./util/mockTruffle.js'); + +let launchTestRpc = false; + +// shell.test alias for legibility +function pathExists(path) { return shell.test('-e', path); } + +// tests run out of memory in CI without this +function collectGarbage() { + if (global.gc) { global.gc(); } +} + +describe('run', () => { + let port = 8555; + let testrpcProcess = null; + let script = `node ./exec.js --dir "./mock" --port ${port} --silent`; // + + before(() => { + mock.protectCoverage(); + }); + + beforeEach(() => { + // CI (Ubuntu) doesn't seem to be freeing server resources until the parent process of these + // tests exits so there are errors launching testrpc repeatedly on the same port. Tests #2 through + // #last will use this instance of testrpc (port 8557). Test #1 uses the instance launched by + // the run script (which also installs the patch). This allows us to end run CI container issues + // AND verify that the script actually works. + if (launchTestRpc) { + port = 8557; + script = `node ./exec.js --dir "./mock" --port ${port} --norpc --silent`; // + const command = `./node_modules/ethereumjs-testrpc/bin/testrpc --gasLimit 0xfffffffffff --port ${port}`; + testrpcProcess = childprocess.exec(command); + launchTestRpc = false; + } + }); + + after(() => { + mock.restoreCoverage(); + testrpcProcess.kill(); + }); + + afterEach(() => { + mock.remove(); + }); + + // This pre-test flushes the suite. There's some kind of sequencing issue here in development + // - the first test always fails unless there is a fresh testrpc install. + // Should not be a problem on CI but results in false positives @home. + it('flush tests', () => { + mock.install('Simple.sol', 'simple.js'); + shell.exec(script); + collectGarbage(); + }); + + // This test should be positioned first in the suite because of the way testrpc is + // launched for these tests. + it('simple contract: should generate coverage, cleanup & exit(0)', () => { + // Directory should be clean + assert(pathExists('./coverage') === false, 'should start without: coverage'); + assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); + + // Run script (exits 0); + mock.install('Simple.sol', 'simple.js'); + shell.exec(script); + assert(shell.error() === null, 'script should not error'); + + // Directory should have coverage report + assert(pathExists('./coverage') === true, 'script should gen coverage folder'); + assert(pathExists('./coverage.json') === true, 'script should gen coverage.json'); + + // Coverage should be real. + // This test is tightly bound to the function names in Simple.sol + const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8')); + const path = Object.keys(produced)[0]; + assert(produced[path].fnMap['1'].name === 'test', 'coverage.json should map "test"'); + assert(produced[path].fnMap['2'].name === 'getX', 'coverage.json should map "getX"'); + collectGarbage(); + launchTestRpc = true; // Toggle flag to launch a single testrpc instance for rest of tests. + }); + + it('contract only uses .call: should generate coverage, cleanup & exit(0)', () => { + // Run against contract that only uses method.call. + assert(pathExists('./coverage') === false, 'should start without: coverage'); + assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); + mock.install('OnlyCall.sol', 'only-call.js'); + + shell.exec(script); + assert(shell.error() === null, 'script should not error'); + + assert(pathExists('./coverage') === true, 'script should gen coverage folder'); + assert(pathExists('./coverage.json') === true, 'script should gen coverage.json'); + + const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8')); + const path = Object.keys(produced)[0]; + assert(produced[path].fnMap['1'].name === 'getFive', 'coverage.json should map "getFive"'); + collectGarbage(); + }); + + it('truffle tests failing: should generate coverage, cleanup & exit(0)', () => { + assert(pathExists('./coverage') === false, 'should start without: coverage'); + assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); + + // Run with Simple.sol and a failing assertion in a truffle test + mock.install('Simple.sol', 'truffle-test-fail.js'); + shell.exec(script); + assert(shell.error() === null, 'script should not error'); + assert(pathExists('./coverage') === true, 'script should gen coverage folder'); + assert(pathExists('./coverage.json') === true, 'script should gen coverage.json'); + + const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8')); + const path = Object.keys(produced)[0]; + assert(produced[path].fnMap['1'].name === 'test', 'coverage.json should map "test"'); + assert(produced[path].fnMap['2'].name === 'getX', 'coverage.json should map "getX"'); + collectGarbage(); + }); + + it('deployment cost > block gasLimit: should generate coverage, cleanup & exit(0)', () => { + // Just making sure Expensive.sol compiles and deploys here. + mock.install('Expensive.sol', 'block-gas-limit.js'); + shell.exec(script); + assert(shell.error() === null, 'script should not error'); + collectGarbage(); + }); + + it('truffle crashes: should generate NO coverage, cleanup and exit(1)', () => { + assert(pathExists('./coverage') === false, 'should start without: coverage'); + assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); + + // Run with Simple.sol and a syntax error in the truffle test + mock.install('Simple.sol', 'truffle-crash.js'); + shell.exec(script); + assert(shell.error() !== null, 'script should error'); + assert(pathExists('./coverage') !== true, 'script should NOT gen coverage folder'); + assert(pathExists('./coverage.json') !== true, 'script should NOT gen coverage.json'); + collectGarbage(); + }); + + it('instrumentation errors: should generate NO coverage, cleanup and exit(1)', () => { + assert(pathExists('./coverage') === false, 'should start without: coverage'); + assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); + + // Run with SimpleError.sol (has syntax error) and working truffle test + mock.install('SimpleError.sol', 'simple.js'); + shell.exec(script); + assert(shell.error() !== null, 'script should error'); + assert(pathExists('./coverage') !== true, 'script should NOT gen coverage folder'); + assert(pathExists('./coverage.json') !== true, 'script should NOT gen coverage.json'); + collectGarbage(); + }); + + it('no events log produced: should generate NO coverage, cleanup and exit(1)', () => { + // Run contract and test that pass but fire no events + assert(pathExists('./coverage') === false, 'should start without: coverage'); + assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); + mock.install('Empty.sol', 'empty.js'); + shell.exec(script); + assert(shell.error() !== null, 'script should error'); + assert(pathExists('./coverage') !== true, 'script should NOT gen coverage folder'); + assert(pathExists('./coverage.json') !== true, 'script should NOT gen coverage.json'); + collectGarbage(); + }); +}); diff --git a/test/run/block-gas-limit.js b/test/run/block-gas-limit.js new file mode 100644 index 0000000..641ca76 --- /dev/null +++ b/test/run/block-gas-limit.js @@ -0,0 +1,6 @@ + +const Expensive = artifacts.require('./Expensive.sol'); + +contract('Expensive', accounts => { + it('should deploy', () => Expensive.deployed()); +}); diff --git a/test/run/empty.js b/test/run/empty.js new file mode 100644 index 0000000..2401a8c --- /dev/null +++ b/test/run/empty.js @@ -0,0 +1,6 @@ + +const Empty = artifacts.require('./Empty.sol'); + +contract('Empty', accounts => { + it('should deploy', () => Empty.deployed()); +}); diff --git a/test/run/only-call.js b/test/run/only-call.js new file mode 100644 index 0000000..3b2c697 --- /dev/null +++ b/test/run/only-call.js @@ -0,0 +1,10 @@ + +const OnlyCall = artifacts.require('./OnlyCall.sol'); + +contract('OnlyCall', accounts => { + it('should return 5', () => + OnlyCall.deployed() + .then(instance => instance.getFive.call()) + .then(val => assert.equal(val, 5)) + ); +}); diff --git a/test/run/simple.js b/test/run/simple.js new file mode 100644 index 0000000..9c974ab --- /dev/null +++ b/test/run/simple.js @@ -0,0 +1,14 @@ + +const Simple = artifacts.require('./Simple.sol'); + +contract('Simple', accounts => { + it('should set x to 5', () => { + let simple; + return Simple.deployed().then(instance => { + simple = instance; + return simple.test(5); + }) + .then(val => simple.getX.call()) + .then(val => assert.equal(val.toNumber(), 5)); + }); +}); diff --git a/test/run/sol-parse-fail.js b/test/run/sol-parse-fail.js new file mode 100644 index 0000000..2b09bd1 --- /dev/null +++ b/test/run/sol-parse-fail.js @@ -0,0 +1,16 @@ + + +const Simple = artifacts.require('./Simple.sol'); + +// This test is constructed correctly but the SimpleError.sol has a syntax error +contract('SimpleError', accounts => { + it('should set x to 5', () => { + let simple; + return Simple.deployed().then(instance => { + simple = instance; + return simple.test(5); + }) + .then(val => simple.getX.call()) + .then(val => assert.equal(val, 5)); + }); +}); diff --git a/test/run/truffle-crash.js b/test/run/truffle-crash.js new file mode 100644 index 0000000..6ee35ea --- /dev/null +++ b/test/run/truffle-crash.js @@ -0,0 +1,10 @@ +'use strict' + +var Simple = artifacts.require('./Simple.sol'); + +// This test should break truffle because it has a syntax error. +contract('Simple', function(accounts){ + it('should crash', function(){ + return Simple.deployed().then.why. + }) +}) \ No newline at end of file diff --git a/test/run/truffle-test-fail.js b/test/run/truffle-test-fail.js new file mode 100644 index 0000000..211a409 --- /dev/null +++ b/test/run/truffle-test-fail.js @@ -0,0 +1,15 @@ + + +const Simple = artifacts.require('./Simple.sol'); + +contract('Simple', accounts => { + it('should set x to 5', () => { + let simple; + return Simple.deployed().then(instance => { + simple = instance; + return simple.test(5); + }) + .then(val => simple.getX.call()) + .then(val => assert.equal(val.toNumber(), 4)); // <-- Wrong result: test fails + }); +}); diff --git a/test/sources/run/Empty.sol b/test/sources/run/Empty.sol new file mode 100644 index 0000000..4c43cfc --- /dev/null +++ b/test/sources/run/Empty.sol @@ -0,0 +1,4 @@ +pragma solidity ^0.4.3; + +contract Empty { +} \ No newline at end of file diff --git a/test/sources/run/Expensive.sol b/test/sources/run/Expensive.sol new file mode 100644 index 0000000..410e694 --- /dev/null +++ b/test/sources/run/Expensive.sol @@ -0,0 +1,15 @@ +// Cost to deploy Expensive: 0x4e042f +// Block gas limit is: 0x47e7c4 +// Should throw out of gas on unmodified truffle +// Should pass solcover truffle +pragma solidity ^0.4.2; + +contract Expensive { + mapping (uint => address) map; + + function Expensive(){ + for(uint i = 0; i < 250; i++ ){ + map[i] = address(this); + } + } +} \ No newline at end of file diff --git a/test/sources/run/Migrations.sol b/test/sources/run/Migrations.sol new file mode 100644 index 0000000..b001aa7 --- /dev/null +++ b/test/sources/run/Migrations.sol @@ -0,0 +1,20 @@ +pragma solidity ^0.4.4; +contract Migrations { + address public owner; + uint public last_completed_migration; + + modifier restricted() {if (msg.sender == owner) _;} + + function Migrations() { + owner = msg.sender; + } + + function setCompleted(uint completed) restricted { + last_completed_migration = completed; + } + + function upgrade(address new_address) restricted { + Migrations upgraded = Migrations(new_address); + upgraded.setCompleted(last_completed_migration); + } +} \ No newline at end of file diff --git a/test/sources/run/OnlyCall.sol b/test/sources/run/OnlyCall.sol new file mode 100644 index 0000000..9fe5838 --- /dev/null +++ b/test/sources/run/OnlyCall.sol @@ -0,0 +1,11 @@ +/** + * This contract contains a single function that is accessed using method.call + * With an unpatched testrpc it should not generate any events. + */ +pragma solidity ^0.4.3; + +contract OnlyCall { + function getFive() returns (uint){ + return 5; + } +} \ No newline at end of file diff --git a/test/sources/run/Simple.sol b/test/sources/run/Simple.sol new file mode 100644 index 0000000..f552b23 --- /dev/null +++ b/test/sources/run/Simple.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.4.3; + +contract Simple { + uint x = 0; + + function test(uint val) { + x = x + val; + } + + function getX() returns (uint){ + return x; + } +} \ No newline at end of file diff --git a/test/sources/run/SimpleError.sol b/test/sources/run/SimpleError.sol new file mode 100644 index 0000000..f1f9282 --- /dev/null +++ b/test/sources/run/SimpleError.sol @@ -0,0 +1,14 @@ +// This contract should throw a parse error in instrumentSolidity.js +pragma solidity ^0.4.3; + +contract SimpleError { + uint x = 0; + + function test(uint val) { + x = x + val // <-- no semi-colon + } + + function getX() returns (uint){ + return x; + } +} \ No newline at end of file diff --git a/test/util/mockTruffle.js b/test/util/mockTruffle.js new file mode 100644 index 0000000..c386bd1 --- /dev/null +++ b/test/util/mockTruffle.js @@ -0,0 +1,93 @@ + +/* + This file contains utilities for generating a mock truffle project to test solcover's + run script against. + */ +const assert = require('assert'); +const fs = require('fs'); +const shell = require('shelljs'); + +/** + * Moves existing coverage reports into a safe place while testing run script which + * would overwrite them. Silences shell complaints about non-existent files. + */ +module.exports.protectCoverage = function () { + shell.config.silent = true; + shell.rm('-Rf', './safe'); + shell.mkdir('./safe'); + shell.mv('./coverage', './safe/coverage'); + shell.mv('./coverage.json', './safe/coverage.json'); + shell.config.silent = false; +}; + +/** + * Restores pre-existing coverage reports after testing run script. + * Silences shell complaints about non-existent files. + */ +module.exports.restoreCoverage = function () { + shell.config.silent = true; + shell.mv('./safe/coverage', './coverage'); + shell.mv('./safe/coverage.json', './coverage.json'); + shell.rm('-Rf', './safe'); + shell.config.silent = false; +}; + +/** + * Installs mock truffle project at ./mock with a single contract + * and test specified by the params. + * @param {String} contract located in /test/sources/run/ + * @param {[type]} test located in /test/run/ + */ +module.exports.install = function (contract, test) { + shell.mkdir('./mock'); + shell.mkdir('./mock/contracts'); + shell.mkdir('./mock/migrations'); + shell.mkdir('./mock/test'); + + // Mock contracts + shell.cp(`./test/sources/run/${contract}`, `./mock/contracts/${contract}`); + shell.cp('./test/sources/run/Migrations.sol', './mock/contracts/Migrations.sol'); + + // Mock migrations + const initialMigration = ` + let Migrations = artifacts.require('Migrations.sol'); + module.exports = function(deployer) { + deployer.deploy(Migrations); + };`; + + const contractLocation = './' + contract; + const deployContracts = ` + var contract = artifacts.require('${contractLocation}'); + module.exports = function(deployer) { + deployer.deploy(contract); + };`; + + fs.writeFileSync('./mock/migrations/1_initial_migration.js', initialMigration); + fs.writeFileSync('./mock/migrations/2_deploy_contracts.js', deployContracts); + + // Mock test + shell.cp(`./test/run/${test}`, `./mock/test/${test}`); + + // Mock truffle.js + const trufflejs = `module.exports = { + networks: { + development: { + host: "localhost", + port: 8545, + network_id: "*" + }}};` + ; + + fs.writeFileSync('./mock/truffle.js', trufflejs); +}; + +/** + * Removes mock truffle project and coverage reports generated by runCovered tests + */ +module.exports.remove = function () { + shell.config.silent = true; + shell.rm('-Rf', 'mock'); + shell.rm('-Rf', 'coverage'); + shell.rm('coverage.json'); + shell.config.silent = false; +}; \ No newline at end of file From 31125bf4f5eaff82129ba9163da5c31f3dc23dbc Mon Sep 17 00:00:00 2001 From: cgewecke Date: Tue, 28 Mar 2017 19:41:12 -0700 Subject: [PATCH 03/51] Toggle silence off to debug CI failures --- test/run.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/run.js b/test/run.js index 57181bb..1087553 100644 --- a/test/run.js +++ b/test/run.js @@ -19,7 +19,7 @@ function collectGarbage() { describe('run', () => { let port = 8555; let testrpcProcess = null; - let script = `node ./exec.js --dir "./mock" --port ${port} --silent`; // + let script = `node ./exec.js --dir "./mock" --port ${port} `; // --silent before(() => { mock.protectCoverage(); @@ -33,7 +33,7 @@ describe('run', () => { // AND verify that the script actually works. if (launchTestRpc) { port = 8557; - script = `node ./exec.js --dir "./mock" --port ${port} --norpc --silent`; // + script = `node ./exec.js --dir "./mock" --port ${port} --norpc `; // --silent const command = `./node_modules/ethereumjs-testrpc/bin/testrpc --gasLimit 0xfffffffffff --port ${port}`; testrpcProcess = childprocess.exec(command); launchTestRpc = false; From bb50f660c530a088f85ae3cf66b0bdb8b115927a Mon Sep 17 00:00:00 2001 From: cgewecke Date: Tue, 28 Mar 2017 20:09:23 -0700 Subject: [PATCH 04/51] Comment out suite flush test, remove truffle -g from circle.yml --- circle.yml | 1 - test/run.js | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/circle.yml b/circle.yml index 7ea2dbc..280b523 100644 --- a/circle.yml +++ b/circle.yml @@ -3,7 +3,6 @@ machine: version: 6.9.1 dependencies: pre: - - npm install -g truffle - rm -rf node_modules/ test: override: diff --git a/test/run.js b/test/run.js index 1087553..09d7a4b 100644 --- a/test/run.js +++ b/test/run.js @@ -33,7 +33,7 @@ describe('run', () => { // AND verify that the script actually works. if (launchTestRpc) { port = 8557; - script = `node ./exec.js --dir "./mock" --port ${port} --norpc `; // --silent + script = `node ./exec.js --dir "./mock" --port ${port} --norpc`; // --silent const command = `./node_modules/ethereumjs-testrpc/bin/testrpc --gasLimit 0xfffffffffff --port ${port}`; testrpcProcess = childprocess.exec(command); launchTestRpc = false; @@ -52,11 +52,11 @@ describe('run', () => { // This pre-test flushes the suite. There's some kind of sequencing issue here in development // - the first test always fails unless there is a fresh testrpc install. // Should not be a problem on CI but results in false positives @home. - it('flush tests', () => { + /*it('flush tests', () => { mock.install('Simple.sol', 'simple.js'); shell.exec(script); collectGarbage(); - }); + });*/ // This test should be positioned first in the suite because of the way testrpc is // launched for these tests. From fa903ecba2b668747404aa7503e02579978dac2e Mon Sep 17 00:00:00 2001 From: cgewecke Date: Wed, 29 Mar 2017 09:18:48 -0700 Subject: [PATCH 05/51] Comments/cleanup in exec.js. Add --test flag, add CI logic to flush test --- exec.js | 110 +++++++++++++++++++++++++++++++++------------------- test/run.js | 27 +++++++------ 2 files changed, 85 insertions(+), 52 deletions(-) diff --git a/exec.js b/exec.js index 3880032..2cda52c 100644 --- a/exec.js +++ b/exec.js @@ -7,56 +7,71 @@ const SolidityCoder = require('web3/lib/solidity/coder.js'); const getInstrumentedVersion = require('./instrumentSolidity.js'); const CoverageMap = require('./coverageMap.js'); -const gasLimit = '0xfffffffffff'; +// Very high gas block limits / contract deployment limits +const gasLimitString = '0xfffffffffff'; +const gasLimitHex = 0xfffffffffff; +const gasPriceHex = 0x01; + const coverage = new CoverageMap(); -const coverageDir = './coverageEnv'; -//const _MODULES_ = 'node_modules/solcover/node_modules'; -const _MODULES_ = 'node_modules'; -let log = () => {}; -let workingDir = './..'; -let port = 8555; -let testrpcProcess = null; -let events = null; -let silence = ''; +const coverageDir = './coverageEnv'; // Path to env that instrumented .sols are tested in +//let modulesDir = 'node_modules/solcover/node_modules'; +let modulesDir = 'node_modules'; + +let workingDir = './..'; // Default location of contracts folder +let port = 8555; // Default port - NOT 8545 & configurable via --port +let silence = ''; // Default log level: configurable by --silence +let log = console.log; // Default log level: configurable by --silence + +let testrpcProcess; // ref to testrpc process we need to kill on exit +let events; // ref to string loaded from 'allFiredEvents' // --------------------------------------- Script -------------------------------------------------- -if (argv.dir) workingDir = argv.dir; // Working dir relative to solcover -if (argv.port) port = argv.port; // Testrpc port +if (argv.dir) workingDir = argv.dir; +if (argv.port) port = argv.port; +if (argv.test) modulesDir = 'node_modules'; -if (argv.silent) { // Log level - silence = '> /dev/null 2>&1'; -} else { - log = console.log; -} +if (argv.silent) { + silence = '> /dev/null 2>&1'; // Silence for solcover's unit tests / CI + log = () => {} +} -// Patch our local testrpc if necessary & Run the modified testrpc with large block limit, -// on (hopefully) unused port. Changes here should be also be added to the before() block -// of test/run.js +// Patch our local testrpc if necessary & run the modified testrpc with large block limit, +// on (hopefully) unused port. (Changes here should be also be added to the before() block +// of test/run.js). if (!argv.norpc) { - if (!shell.test('-e', `./${_MODULES_}/ethereumjs-vm/lib/opFns.js.orig`)) { - log('Patch local testrpc...'); - shell.exec(`patch -b ./${_MODULES_}/ethereumjs-vm/lib/opFns.js ./hookIntoEvents.patch`); + const patchRequired = `./${modulesDir}/ethereumjs-vm/lib/opFns.js` + const patchInstalled = `./${modulesDir}/ethereumjs-vm/lib/opFns.js.orig`; + + if (!shell.test('-e', patchInstalled)) { + log('Patching local testrpc...'); + shell.exec(`patch -b ${patchRequired} ./hookIntoEvents.patch`); } - log(`Launching testrpc on port ${port}`); + try { - const command = `./${_MODULES_}/ethereumjs-testrpc/bin/testrpc --gasLimit ${gasLimit} --port ${port}`; - testrpcProcess = childprocess.exec(command); + log(`Launching testrpc on port ${port}`); + + const command = `./${modulesDir}/ethereumjs-testrpc/bin/testrpc `; + const options = `--gasLimit ${gasLimitString} --port ${port}`; + testrpcProcess = childprocess.exec(command + options); + } catch (err) { const msg = `There was a problem launching testrpc: ${err}`; cleanUp(msg); } } -// Generate a copy of the target truffle project configured for solcover. -// NB: the following assumes that the target's truffle.js doesn't specify a custom build with an -// atypical directory structure or depend on the options solcover will change: port, gasLimit, +// Generate a copy of the target truffle project configured for solcover and save to the coverage +// environment folder. +// +// NB: this code assumes that truffle test can run successfully on the development network defaults +// and doesn't otherwise depend on the options solcover will change: port, gasLimit, // gasPrice. log('Generating coverage environment'); const truffleConfig = require(`${workingDir}/truffle.js`); truffleConfig.networks.development.port = port; -truffleConfig.networks.development.gas = 0xfffffffffff; -truffleConfig.networks.development.gasPrice = 0x01; +truffleConfig.networks.development.gas = gasLimitHex; +truffleConfig.networks.development.gasPrice = gasPriceHex; shell.mkdir(`${coverageDir}`); shell.cp('-R', `${workingDir}/contracts`, `${coverageDir}`); @@ -65,15 +80,24 @@ shell.cp('-R', `${workingDir}/test`, `${coverageDir}`); fs.writeFileSync(`${coverageDir}/truffle.js`, `module.exports = ${JSON.stringify(truffleConfig)}`); -// For each contract in originalContracts, get the instrumented version +// For each contract except migrations.sol: +// 1. Generate reference to its real path (this identifies it in the reports) +// 2. Load contract as string +// 3. Instrument contract +// 4. Save instrumented contract in the coverage environment folder where covered tests will run +// 5. Add instrumentation info to the coverage map try { shell.ls(`${coverageDir}/contracts/**/*.sol`).forEach(file => { - if (file !== `${coverageDir}/contracts/Migrations.sol`) { - log('Instrumenting ', file); + let migrations = `${coverageDir}/contracts/Migrations.sol`; + + if (file !== migrations) { + log('Instrumenting ', file); const canonicalContractPath = path.resolve(`${workingDir}/contracts/${path.basename(file)}`); const contract = fs.readFileSync(canonicalContractPath).toString(); const instrumentedContractInfo = getInstrumentedVersion(contract, canonicalContractPath); - fs.writeFileSync(`${coverageDir}/contracts/${path.basename(file)}`, instrumentedContractInfo.contract); + const instrumentedFilePath = `${coverageDir}/contracts/${path.basename(file)}` + + fs.writeFileSync(instrumentedFilePath, instrumentedContractInfo.contract); coverage.addContract(instrumentedContractInfo, canonicalContractPath); } }); @@ -81,10 +105,11 @@ try { cleanUp(err); } -// Run truffle test on instrumented contracts +// Run solcover's fork of truffle over instrumented contracts in the +// coverage environment folder try { log('Launching Truffle (this can take a few seconds)...'); - const truffle = './../node_modules/truffle/cli.js'; + const truffle = `./../${modulesDir}/truffle/cli.js`; const command = `cd coverageEnv && ${truffle} test ${silence}`; shell.exec(command); } catch (err) { @@ -106,11 +131,16 @@ try { cleanUp(msg + err); } -// Generate coverage, write coverage report / run istanbul +// Generate coverage / write coverage report / run istanbul try { + let json; + let istanbul = `./${modulesDir}/istanbul/lib/cli.js report lcov ${silence}` + coverage.generate(events, `${coverageDir}/contracts/`); - fs.writeFileSync('./coverage.json', JSON.stringify(coverage.coverage)); - shell.exec(`./${_MODULES_}/istanbul/lib/cli.js report lcov ${silence}`); + json = JSON.stringify(coverage.coverage); + fs.writeFileSync('./coverage.json', json); + shell.exec(istanbul); + } catch (err) { const msg = 'There was a problem generating producing the coverage map / running Istanbul.\n'; cleanUp(msg + err); diff --git a/test/run.js b/test/run.js index 09d7a4b..19abaa7 100644 --- a/test/run.js +++ b/test/run.js @@ -6,8 +6,6 @@ const fs = require('fs'); const childprocess = require('child_process'); const mock = require('./util/mockTruffle.js'); -let launchTestRpc = false; - // shell.test alias for legibility function pathExists(path) { return shell.test('-e', path); } @@ -16,10 +14,11 @@ function collectGarbage() { if (global.gc) { global.gc(); } } -describe('run', () => { +describe.only('run', () => { let port = 8555; let testrpcProcess = null; - let script = `node ./exec.js --dir "./mock" --port ${port} `; // --silent + let script = `node ./exec.js --dir "./mock" --port ${port} --test`; // --silent + let launchTestRpc = false; before(() => { mock.protectCoverage(); @@ -33,7 +32,7 @@ describe('run', () => { // AND verify that the script actually works. if (launchTestRpc) { port = 8557; - script = `node ./exec.js --dir "./mock" --port ${port} --norpc`; // --silent + script = `node ./exec.js --dir "./mock" --port ${port} --norpc --test`; // --silent const command = `./node_modules/ethereumjs-testrpc/bin/testrpc --gasLimit 0xfffffffffff --port ${port}`; testrpcProcess = childprocess.exec(command); launchTestRpc = false; @@ -49,14 +48,18 @@ describe('run', () => { mock.remove(); }); - // This pre-test flushes the suite. There's some kind of sequencing issue here in development + // This pre-test flushes the suite. There's some kind of sequencing issue here in development, + // possibly tied to the use of ethereumjs-vm in the coverage tests? + // - tests pass w/out this if we only run these test - e.g. it only fails when running the suite. // - the first test always fails unless there is a fresh testrpc install. - // Should not be a problem on CI but results in false positives @home. - /*it('flush tests', () => { - mock.install('Simple.sol', 'simple.js'); - shell.exec(script); - collectGarbage(); - });*/ + // - Running this on Circle CI causes it to crash though. + it('flush test suite', () => { + if (!process.env.CI){ // <---- CI is set by default on circle + mock.install('Simple.sol', 'simple.js'); + shell.exec(script); // <---- This fails mysteriously, but we don't test here. + collectGarbage(); + } + }); // This test should be positioned first in the suite because of the way testrpc is // launched for these tests. From 95f058029a0afed227aecf19bd49645e3667f82f Mon Sep 17 00:00:00 2001 From: cgewecke Date: Wed, 29 Mar 2017 09:26:26 -0700 Subject: [PATCH 06/51] Remove .only on run tests --- test/run.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/test/run.js b/test/run.js index 19abaa7..0e55d87 100644 --- a/test/run.js +++ b/test/run.js @@ -1,5 +1,3 @@ - - const assert = require('assert'); const shell = require('shelljs'); const fs = require('fs'); @@ -14,7 +12,7 @@ function collectGarbage() { if (global.gc) { global.gc(); } } -describe.only('run', () => { +describe('run', () => { let port = 8555; let testrpcProcess = null; let script = `node ./exec.js --dir "./mock" --port ${port} --test`; // --silent @@ -29,7 +27,7 @@ describe.only('run', () => { // tests exits so there are errors launching testrpc repeatedly on the same port. Tests #2 through // #last will use this instance of testrpc (port 8557). Test #1 uses the instance launched by // the run script (which also installs the patch). This allows us to end run CI container issues - // AND verify that the script actually works. + // AND verify that the script in exec actually works. if (launchTestRpc) { port = 8557; script = `node ./exec.js --dir "./mock" --port ${port} --norpc --test`; // --silent @@ -52,7 +50,7 @@ describe.only('run', () => { // possibly tied to the use of ethereumjs-vm in the coverage tests? // - tests pass w/out this if we only run these test - e.g. it only fails when running the suite. // - the first test always fails unless there is a fresh testrpc install. - // - Running this on Circle CI causes it to crash though. + // - Running this on Circle CI causes suite to crash it('flush test suite', () => { if (!process.env.CI){ // <---- CI is set by default on circle mock.install('Simple.sol', 'simple.js'); @@ -61,8 +59,8 @@ describe.only('run', () => { } }); - // This test should be positioned first in the suite because of the way testrpc is - // launched for these tests. + // This test should be positioned first (or second if flushing) in the suite because of + // the way we're launching testrpc it('simple contract: should generate coverage, cleanup & exit(0)', () => { // Directory should be clean assert(pathExists('./coverage') === false, 'should start without: coverage'); From d9d9789f5b7b346e290eb8158580e85ec7897074 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 30 Mar 2017 10:48:31 -0700 Subject: [PATCH 07/51] Attempt to normalize paths for "npm install --save-dev" use --- exec.js | 23 +++++++++++++---------- test/run.js | 4 ++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/exec.js b/exec.js index 2cda52c..e9ec950 100644 --- a/exec.js +++ b/exec.js @@ -13,11 +13,14 @@ const gasLimitHex = 0xfffffffffff; const gasPriceHex = 0x01; const coverage = new CoverageMap(); -const coverageDir = './coverageEnv'; // Path to env that instrumented .sols are tested in -//let modulesDir = 'node_modules/solcover/node_modules'; -let modulesDir = 'node_modules'; -let workingDir = './..'; // Default location of contracts folder +// Paths +const coverageDir = './coverageEnv'; // Env that instrumented .sols are tested in +const solcoverDir = 'node_modules/solcover' // Solcover assets +let modulesDir = 'node_modules/solcover/node_modules'; // Solcover's npm assets: configurable via test + + +let workingDir = '.'; // Default location of contracts folder let port = 8555; // Default port - NOT 8545 & configurable via --port let silence = ''; // Default log level: configurable by --silence let log = console.log; // Default log level: configurable by --silence @@ -29,10 +32,10 @@ let events; // ref to string loaded from 'allFiredEven if (argv.dir) workingDir = argv.dir; if (argv.port) port = argv.port; -if (argv.test) modulesDir = 'node_modules'; +if (argv.testing) modulesDir = 'node_modules'; if (argv.silent) { - silence = '> /dev/null 2>&1'; // Silence for solcover's unit tests / CI + silence = '> /dev/null 2>&1'; // Silence for solcover's unit tests / CI log = () => {} } @@ -45,7 +48,7 @@ if (!argv.norpc) { if (!shell.test('-e', patchInstalled)) { log('Patching local testrpc...'); - shell.exec(`patch -b ${patchRequired} ./hookIntoEvents.patch`); + shell.exec(`patch -b ${patchRequired} ./${solcoverDir}/hookIntoEvents.patch`); } try { @@ -118,7 +121,7 @@ try { // Get events fired during instrumented contracts execution. try { - events = fs.readFileSync('./allFiredEvents').toString().split('\n'); + events = fs.readFileSync(`./allFiredEvents`).toString().split('\n'); events.pop(); } catch (err) { const msg = @@ -138,7 +141,7 @@ try { coverage.generate(events, `${coverageDir}/contracts/`); json = JSON.stringify(coverage.coverage); - fs.writeFileSync('./coverage.json', json); + fs.writeFileSync(`./coverage.json`, json); shell.exec(istanbul); } catch (err) { @@ -159,7 +162,7 @@ function cleanUp(err) { log('Cleaning up...'); shell.config.silent = true; shell.rm('-Rf', `${coverageDir}`); - shell.rm('./allFiredEvents'); + shell.rm(`./allFiredEvents`); if (testrpcProcess) { testrpcProcess.kill(); } if (err) { diff --git a/test/run.js b/test/run.js index 0e55d87..f2596ab 100644 --- a/test/run.js +++ b/test/run.js @@ -15,7 +15,7 @@ function collectGarbage() { describe('run', () => { let port = 8555; let testrpcProcess = null; - let script = `node ./exec.js --dir "./mock" --port ${port} --test`; // --silent + let script = `node ./exec.js --dir "./mock" --port ${port} --testing --silent`; // --silent let launchTestRpc = false; before(() => { @@ -30,7 +30,7 @@ describe('run', () => { // AND verify that the script in exec actually works. if (launchTestRpc) { port = 8557; - script = `node ./exec.js --dir "./mock" --port ${port} --norpc --test`; // --silent + script = `node ./exec.js --dir "./mock" --port ${port} --norpc --testing --silent`; // --silent const command = `./node_modules/ethereumjs-testrpc/bin/testrpc --gasLimit 0xfffffffffff --port ${port}`; testrpcProcess = childprocess.exec(command); launchTestRpc = false; From b24dbc0a75ba76ea194b8ba3ecf39d9095400f59 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 30 Mar 2017 11:21:53 -0700 Subject: [PATCH 08/51] Remove patching - replace with testrpc/ethereumjs-vm forks --- exec.js | 13 ++----------- package.json | 2 +- test/run.js | 4 ++-- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/exec.js b/exec.js index e9ec950..010317c 100644 --- a/exec.js +++ b/exec.js @@ -39,18 +39,9 @@ if (argv.silent) { log = () => {} } -// Patch our local testrpc if necessary & run the modified testrpc with large block limit, -// on (hopefully) unused port. (Changes here should be also be added to the before() block -// of test/run.js). +// Run the modified testrpc with large block limit, on (hopefully) unused port. +// (Changes here should be also be added to the before() block of test/run.js). if (!argv.norpc) { - const patchRequired = `./${modulesDir}/ethereumjs-vm/lib/opFns.js` - const patchInstalled = `./${modulesDir}/ethereumjs-vm/lib/opFns.js.orig`; - - if (!shell.test('-e', patchInstalled)) { - log('Patching local testrpc...'); - shell.exec(`patch -b ${patchRequired} ./${solcoverDir}/hookIntoEvents.patch`); - } - try { log(`Launching testrpc on port ${port}`); diff --git a/package.json b/package.json index 24fd19f..bcabc74 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "author": "", "license": "ISC", "dependencies": { - "ethereumjs-testrpc": "https://github.com/ethereumjs/testrpc.git#5ba3c45b2ca306ab589f4a4c649d9afc39042680", + "ethereumjs-testrpc": "https://github.com/cgewecke/testrpc.git#solcover", "istanbul": "^0.4.5", "mkdirp": "^0.5.1", "shelljs": "^0.7.4", diff --git a/test/run.js b/test/run.js index f2596ab..5acc748 100644 --- a/test/run.js +++ b/test/run.js @@ -26,8 +26,8 @@ describe('run', () => { // CI (Ubuntu) doesn't seem to be freeing server resources until the parent process of these // tests exits so there are errors launching testrpc repeatedly on the same port. Tests #2 through // #last will use this instance of testrpc (port 8557). Test #1 uses the instance launched by - // the run script (which also installs the patch). This allows us to end run CI container issues - // AND verify that the script in exec actually works. + // the run script. This allows us to end run CI container issues AND verify that the script in + // exec actually works. if (launchTestRpc) { port = 8557; script = `node ./exec.js --dir "./mock" --port ${port} --norpc --testing --silent`; // --silent From 6de8846abf3479446cf1b743bfb1eea53a1ed6c2 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 30 Mar 2017 11:49:41 -0700 Subject: [PATCH 09/51] Remove silence to debug CI failure, lengthen timeout, npm bin --- exec.js | 2 ++ package.json | 8 +++++--- test/run.js | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/exec.js b/exec.js index 010317c..18fba8e 100644 --- a/exec.js +++ b/exec.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + const shell = require('shelljs'); const fs = require('fs'); const path = require('path'); diff --git a/package.json b/package.json index bcabc74..b80fe8b 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,15 @@ "name": "solcover", "version": "0.0.1", "description": "", - "main": "runCoveredTests.js", + "bin": { + "solcover": "./exec.js" + }, "directories": { "test": "test" }, "scripts": { - "test": "mocha --timeout 30000", - "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --timeout 30000" + "test": "mocha --timeout 60000", + "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --timeout 60000" }, "author": "", "license": "ISC", diff --git a/test/run.js b/test/run.js index 5acc748..cf0cf9c 100644 --- a/test/run.js +++ b/test/run.js @@ -15,7 +15,7 @@ function collectGarbage() { describe('run', () => { let port = 8555; let testrpcProcess = null; - let script = `node ./exec.js --dir "./mock" --port ${port} --testing --silent`; // --silent + let script = `node ./exec.js --dir "./mock" --port ${port} --testing`; // --silent let launchTestRpc = false; before(() => { @@ -30,7 +30,7 @@ describe('run', () => { // exec actually works. if (launchTestRpc) { port = 8557; - script = `node ./exec.js --dir "./mock" --port ${port} --norpc --testing --silent`; // --silent + script = `node ./exec.js --dir "./mock" --port ${port} --norpc --testing`; // --silent const command = `./node_modules/ethereumjs-testrpc/bin/testrpc --gasLimit 0xfffffffffff --port ${port}`; testrpcProcess = childprocess.exec(command); launchTestRpc = false; From 6fb6f0b800bd2d00d2c8c3cf6c4361fe0f351cee Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 30 Mar 2017 12:16:10 -0700 Subject: [PATCH 10/51] Add debugging line for norpc, explicitly include yargs --- exec.js | 4 +++- package.json | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/exec.js b/exec.js index 18fba8e..73b5a63 100644 --- a/exec.js +++ b/exec.js @@ -38,9 +38,11 @@ if (argv.testing) modulesDir = 'node_modules'; if (argv.silent) { silence = '> /dev/null 2>&1'; // Silence for solcover's unit tests / CI - log = () => {} + log = () => {}; } +console.log('argv.norpc --> ' + argv.norpc); + // Run the modified testrpc with large block limit, on (hopefully) unused port. // (Changes here should be also be added to the before() block of test/run.js). if (!argv.norpc) { diff --git a/package.json b/package.json index b80fe8b..bf36d8c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "shelljs": "^0.7.4", "sol-explore": "^1.6.2", "solidity-parser": "0.3.0", - "truffle": "git+https://github.com/cgewecke/truffle.git" + "truffle": "git+https://github.com/cgewecke/truffle.git", + "yargs": "^7.0.2" }, "devDependencies": { "crypto-js": "^3.1.9-1", From adbdf8e0802e0c855e53a0bcbf97be7c4ba9ce1c Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 30 Mar 2017 12:33:37 -0700 Subject: [PATCH 11/51] Try removing shebang to get CI to pass --- exec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/exec.js b/exec.js index 73b5a63..c96fe1b 100644 --- a/exec.js +++ b/exec.js @@ -1,5 +1,3 @@ -#!/usr/bin/env node - const shell = require('shelljs'); const fs = require('fs'); const path = require('path'); From 60c93326a033098cb2c1d22f8330bbd61973d8e7 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 30 Mar 2017 14:33:17 -0700 Subject: [PATCH 12/51] Try to fix CI: remove yargs, add commander for argv processing --- exec.js | 75 ++++++++++++++++++++++++-------------------- hookIntoEvents.patch | 31 ------------------ package.json | 4 +-- test/run.js | 4 +-- 4 files changed, 45 insertions(+), 69 deletions(-) delete mode 100644 hookIntoEvents.patch diff --git a/exec.js b/exec.js index c96fe1b..c5c78ae 100644 --- a/exec.js +++ b/exec.js @@ -1,7 +1,10 @@ +#!/usr/bin/env node + const shell = require('shelljs'); const fs = require('fs'); const path = require('path'); -const argv = require('yargs').argv; +const program = require('commander'); +// const argv = require('yargs').argv; const childprocess = require('child_process'); const SolidityCoder = require('web3/lib/solidity/coder.js'); const getInstrumentedVersion = require('./instrumentSolidity.js'); @@ -16,50 +19,54 @@ const coverage = new CoverageMap(); // Paths const coverageDir = './coverageEnv'; // Env that instrumented .sols are tested in -const solcoverDir = 'node_modules/solcover' // Solcover assets +const solcoverDir = 'node_modules/solcover'; // Solcover assets let modulesDir = 'node_modules/solcover/node_modules'; // Solcover's npm assets: configurable via test - let workingDir = '.'; // Default location of contracts folder let port = 8555; // Default port - NOT 8545 & configurable via --port -let silence = ''; // Default log level: configurable by --silence -let log = console.log; // Default log level: configurable by --silence +let silence = ''; // Default log level: configurable by --silence +let log = console.log; // Default log level: configurable by --silence let testrpcProcess; // ref to testrpc process we need to kill on exit let events; // ref to string loaded from 'allFiredEvents' // --------------------------------------- Script -------------------------------------------------- - -if (argv.dir) workingDir = argv.dir; -if (argv.port) port = argv.port; -if (argv.testing) modulesDir = 'node_modules'; - -if (argv.silent) { +program + .version('0.0.1') + .option('-d, --dir ', 'Default location of truffle contract folder') + .option('-p, --port ', 'Port to run solcovers custom testrpc instance on') + .option('-t, --testing', 'Run in solcover unit test mode') + .option('-nr, --norpc', 'Do not launch testrpc from exec.js script') + .option('-s, --silent', 'Suppress logging') + .parse(process.argv); + +if (program.dir) workingDir = program.dir; +if (program.port) port = program.port; +if (program.testing) modulesDir = 'node_modules'; + +if (program.silent) { silence = '> /dev/null 2>&1'; // Silence for solcover's unit tests / CI log = () => {}; -} - -console.log('argv.norpc --> ' + argv.norpc); +} -// Run the modified testrpc with large block limit, on (hopefully) unused port. +// Run the modified testrpc with large block limit, on (hopefully) unused port. // (Changes here should be also be added to the before() block of test/run.js). -if (!argv.norpc) { +if (!program.norpc) { try { log(`Launching testrpc on port ${port}`); - + const command = `./${modulesDir}/ethereumjs-testrpc/bin/testrpc `; const options = `--gasLimit ${gasLimitString} --port ${port}`; testrpcProcess = childprocess.exec(command + options); - } catch (err) { const msg = `There was a problem launching testrpc: ${err}`; cleanUp(msg); } } -// Generate a copy of the target truffle project configured for solcover and save to the coverage +// Generate a copy of the target truffle project configured for solcover and save to the coverage // environment folder. -// -// NB: this code assumes that truffle test can run successfully on the development network defaults +// +// NB: this code assumes that truffle test can run successfully on the development network defaults // and doesn't otherwise depend on the options solcover will change: port, gasLimit, // gasPrice. log('Generating coverage environment'); @@ -84,15 +91,15 @@ fs.writeFileSync(`${coverageDir}/truffle.js`, `module.exports = ${JSON.stringify // 5. Add instrumentation info to the coverage map try { shell.ls(`${coverageDir}/contracts/**/*.sol`).forEach(file => { - let migrations = `${coverageDir}/contracts/Migrations.sol`; - + const migrations = `${coverageDir}/contracts/Migrations.sol`; + if (file !== migrations) { - log('Instrumenting ', file); + log('Instrumenting ', file); const canonicalContractPath = path.resolve(`${workingDir}/contracts/${path.basename(file)}`); const contract = fs.readFileSync(canonicalContractPath).toString(); const instrumentedContractInfo = getInstrumentedVersion(contract, canonicalContractPath); - const instrumentedFilePath = `${coverageDir}/contracts/${path.basename(file)}` - + const instrumentedFilePath = `${coverageDir}/contracts/${path.basename(file)}`; + fs.writeFileSync(instrumentedFilePath, instrumentedContractInfo.contract); coverage.addContract(instrumentedContractInfo, canonicalContractPath); } @@ -101,7 +108,7 @@ try { cleanUp(err); } -// Run solcover's fork of truffle over instrumented contracts in the +// Run solcover's fork of truffle over instrumented contracts in the // coverage environment folder try { log('Launching Truffle (this can take a few seconds)...'); @@ -114,7 +121,7 @@ try { // Get events fired during instrumented contracts execution. try { - events = fs.readFileSync(`./allFiredEvents`).toString().split('\n'); + events = fs.readFileSync('./allFiredEvents').toString().split('\n'); events.pop(); } catch (err) { const msg = @@ -129,12 +136,12 @@ try { // Generate coverage / write coverage report / run istanbul try { - let json; - let istanbul = `./${modulesDir}/istanbul/lib/cli.js report lcov ${silence}` - coverage.generate(events, `${coverageDir}/contracts/`); - json = JSON.stringify(coverage.coverage); - fs.writeFileSync(`./coverage.json`, json); + + const json = JSON.stringify(coverage.coverage); + fs.writeFileSync('./coverage.json', json); + + const istanbul = `./${modulesDir}/istanbul/lib/cli.js report lcov ${silence}`; shell.exec(istanbul); } catch (err) { @@ -155,7 +162,7 @@ function cleanUp(err) { log('Cleaning up...'); shell.config.silent = true; shell.rm('-Rf', `${coverageDir}`); - shell.rm(`./allFiredEvents`); + shell.rm('./allFiredEvents'); if (testrpcProcess) { testrpcProcess.kill(); } if (err) { diff --git a/hookIntoEvents.patch b/hookIntoEvents.patch deleted file mode 100644 index aac8840..0000000 --- a/hookIntoEvents.patch +++ /dev/null @@ -1,31 +0,0 @@ ---- opFns.js.orig 2016-12-19 15:10:12.000000000 -0800 -+++ opFns.js 2016-12-19 15:15:51.000000000 -0800 -@@ -6,6 +6,7 @@ - const logTable = require('./logTable.js') - const ERROR = constants.ERROR - const MAX_INT = 9007199254740991 -+const fs = require('fs'); - - // the opcode functions - module.exports = { -@@ -489,7 +490,7 @@ - memLength = utils.bufferToInt(memLength) - const numOfTopics = runState.opCode - 0xa0 - const mem = memLoad(runState, memOffset, memLength) -- subGas(runState, new BN(numOfTopics * fees.logTopicGas.v + memLength * fees.logDataGas.v)) -+ //subGas(runState, new BN(numOfTopics * fees.logTopicGas.v + memLength * fees.logDataGas.v)) - - // add address - var log = [runState.address] -@@ -497,6 +498,11 @@ - - // add data - log.push(mem) -+ var toWrite = {}; -+ toWrite.address= log[0].toString('hex') -+ toWrite.topics = log[1].map(function(x){return x.toString('hex')}) -+ toWrite.data = log[2].toString('hex') -+ fs.appendFileSync('./allFiredEvents', JSON.stringify(toWrite) + '\n') - runState.logs.push(log) - }, - diff --git a/package.json b/package.json index bf36d8c..1715dbf 100644 --- a/package.json +++ b/package.json @@ -15,14 +15,14 @@ "author": "", "license": "ISC", "dependencies": { + "commander": "^2.9.0", "ethereumjs-testrpc": "https://github.com/cgewecke/testrpc.git#solcover", "istanbul": "^0.4.5", "mkdirp": "^0.5.1", "shelljs": "^0.7.4", "sol-explore": "^1.6.2", "solidity-parser": "0.3.0", - "truffle": "git+https://github.com/cgewecke/truffle.git", - "yargs": "^7.0.2" + "truffle": "git+https://github.com/cgewecke/truffle.git" }, "devDependencies": { "crypto-js": "^3.1.9-1", diff --git a/test/run.js b/test/run.js index cf0cf9c..5acc748 100644 --- a/test/run.js +++ b/test/run.js @@ -15,7 +15,7 @@ function collectGarbage() { describe('run', () => { let port = 8555; let testrpcProcess = null; - let script = `node ./exec.js --dir "./mock" --port ${port} --testing`; // --silent + let script = `node ./exec.js --dir "./mock" --port ${port} --testing --silent`; // --silent let launchTestRpc = false; before(() => { @@ -30,7 +30,7 @@ describe('run', () => { // exec actually works. if (launchTestRpc) { port = 8557; - script = `node ./exec.js --dir "./mock" --port ${port} --norpc --testing`; // --silent + script = `node ./exec.js --dir "./mock" --port ${port} --norpc --testing --silent`; // --silent const command = `./node_modules/ethereumjs-testrpc/bin/testrpc --gasLimit 0xfffffffffff --port ${port}`; testrpcProcess = childprocess.exec(command); launchTestRpc = false; From 9f770c8700af75b9f58372f87e0062d40fc5a486 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 30 Mar 2017 17:34:07 -0700 Subject: [PATCH 13/51] Move to config file based options, allow use of coverage network in truffle.js --- exec.js | 100 ++++++++++++++++++++------------------- package.json | 1 + test/run.js | 37 ++++++++++----- test/util/mockTruffle.js | 6 ++- 4 files changed, 83 insertions(+), 61 deletions(-) diff --git a/exec.js b/exec.js index c5c78ae..a20d7be 100644 --- a/exec.js +++ b/exec.js @@ -2,14 +2,15 @@ const shell = require('shelljs'); const fs = require('fs'); +const reqCwd = require('req-cwd'); const path = require('path'); const program = require('commander'); -// const argv = require('yargs').argv; const childprocess = require('child_process'); const SolidityCoder = require('web3/lib/solidity/coder.js'); const getInstrumentedVersion = require('./instrumentSolidity.js'); const CoverageMap = require('./coverageMap.js'); + // Very high gas block limits / contract deployment limits const gasLimitString = '0xfffffffffff'; const gasLimitHex = 0xfffffffffff; @@ -19,42 +20,36 @@ const coverage = new CoverageMap(); // Paths const coverageDir = './coverageEnv'; // Env that instrumented .sols are tested in -const solcoverDir = 'node_modules/solcover'; // Solcover assets +const solcoverDir = 'node_modules/solcover'; // Solcover assets let modulesDir = 'node_modules/solcover/node_modules'; // Solcover's npm assets: configurable via test -let workingDir = '.'; // Default location of contracts folder -let port = 8555; // Default port - NOT 8545 & configurable via --port -let silence = ''; // Default log level: configurable by --silence -let log = console.log; // Default log level: configurable by --silence +// Options +let workingDir = '.'; // Default location of contracts folder +let port = 8555; // Default port - NOT 8545 & configurable via --port +let coverageOption = '--network coverage'; // Default truffle network execution flag +let silence = ''; // Default log level: configurable by --silence +let log = console.log; // Default log level: configurable by --silence -let testrpcProcess; // ref to testrpc process we need to kill on exit -let events; // ref to string loaded from 'allFiredEvents' +let testrpcProcess; // ref to testrpc process we need to kill on exit +let events; // ref to string loaded from 'allFiredEvents' // --------------------------------------- Script -------------------------------------------------- -program - .version('0.0.1') - .option('-d, --dir ', 'Default location of truffle contract folder') - .option('-p, --port ', 'Port to run solcovers custom testrpc instance on') - .option('-t, --testing', 'Run in solcover unit test mode') - .option('-nr, --norpc', 'Do not launch testrpc from exec.js script') - .option('-s, --silent', 'Suppress logging') - .parse(process.argv); - -if (program.dir) workingDir = program.dir; -if (program.port) port = program.port; -if (program.testing) modulesDir = 'node_modules'; - -if (program.silent) { +const config = reqCwd.silent(`${workingDir}/.solcover.js`) || {}; + +if (config.dir) workingDir = config.dir; +if (config.port) port = config.port; +if (config.testing) modulesDir = 'node_modules'; + +if (config.silent) { silence = '> /dev/null 2>&1'; // Silence for solcover's unit tests / CI log = () => {}; } // Run the modified testrpc with large block limit, on (hopefully) unused port. // (Changes here should be also be added to the before() block of test/run.js). -if (!program.norpc) { +if (!config.norpc) { try { log(`Launching testrpc on port ${port}`); - const command = `./${modulesDir}/ethereumjs-testrpc/bin/testrpc `; const options = `--gasLimit ${gasLimitString} --port ${port}`; testrpcProcess = childprocess.exec(command + options); @@ -63,25 +58,35 @@ if (!program.norpc) { cleanUp(msg); } } + // Generate a copy of the target truffle project configured for solcover and save to the coverage // environment folder. -// -// NB: this code assumes that truffle test can run successfully on the development network defaults -// and doesn't otherwise depend on the options solcover will change: port, gasLimit, -// gasPrice. log('Generating coverage environment'); - -const truffleConfig = require(`${workingDir}/truffle.js`); -truffleConfig.networks.development.port = port; -truffleConfig.networks.development.gas = gasLimitHex; -truffleConfig.networks.development.gasPrice = gasPriceHex; - -shell.mkdir(`${coverageDir}`); -shell.cp('-R', `${workingDir}/contracts`, `${coverageDir}`); -shell.cp('-R', `${workingDir}/migrations`, `${coverageDir}`); -shell.cp('-R', `${workingDir}/test`, `${coverageDir}`); - -fs.writeFileSync(`${coverageDir}/truffle.js`, `module.exports = ${JSON.stringify(truffleConfig)}`); +try { + shell.mkdir(`${coverageDir}`); + shell.cp('-R', `${workingDir}/contracts`, `${coverageDir}`); + shell.cp('-R', `${workingDir}/migrations`, `${coverageDir}`); + shell.cp('-R', `${workingDir}/test`, `${coverageDir}`); + + const truffleConfig = reqCwd(`${workingDir}/truffle.js`); + + // Coverage network opts specified: copy truffle.js whole to coverage environment + if (truffleConfig.networks.coverage){ + shell.cp(`${workingDir}/truffle.js`, `${coverageDir}/truffle.js`); + + // Coverage network opts NOT specified: default to the development network w/ modified + // port, gasLimit, gasPrice. Export the config object only. + } else { + truffleConfig.networks.development.port = port; + truffleConfig.networks.development.gas = gasLimitHex; + truffleConfig.networks.development.gasPrice = gasPriceHex; + coverageOption = ''; + fs.writeFileSync(`${coverageDir}/truffle.js`, `module.exports = ${JSON.stringify(truffleConfig)}`); + } +} catch (err){ + const msg = ('There was a problem generating the coverage environment: '); + cleanUp(msg + err); +} // For each contract except migrations.sol: // 1. Generate reference to its real path (this identifies it in the reports) @@ -95,13 +100,12 @@ try { if (file !== migrations) { log('Instrumenting ', file); - const canonicalContractPath = path.resolve(`${workingDir}/contracts/${path.basename(file)}`); - const contract = fs.readFileSync(canonicalContractPath).toString(); - const instrumentedContractInfo = getInstrumentedVersion(contract, canonicalContractPath); - const instrumentedFilePath = `${coverageDir}/contracts/${path.basename(file)}`; - - fs.writeFileSync(instrumentedFilePath, instrumentedContractInfo.contract); - coverage.addContract(instrumentedContractInfo, canonicalContractPath); + const contractPath = path.resolve(file); + const contract = fs.readFileSync(contractPath).toString(); + const instrumentedContractInfo = getInstrumentedVersion(contract, contractPath); + + fs.writeFileSync(contractPath, instrumentedContractInfo.contract); + coverage.addContract(instrumentedContractInfo, contractPath); } }); } catch (err) { @@ -113,7 +117,7 @@ try { try { log('Launching Truffle (this can take a few seconds)...'); const truffle = `./../${modulesDir}/truffle/cli.js`; - const command = `cd coverageEnv && ${truffle} test ${silence}`; + const command = `cd coverageEnv && ${truffle} test ${coverageOption} ${silence}`; shell.exec(command); } catch (err) { cleanUp(err); diff --git a/package.json b/package.json index 1715dbf..dd23826 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "ethereumjs-testrpc": "https://github.com/cgewecke/testrpc.git#solcover", "istanbul": "^0.4.5", "mkdirp": "^0.5.1", + "req-cwd": "^1.0.1", "shelljs": "^0.7.4", "sol-explore": "^1.6.2", "solidity-parser": "0.3.0", diff --git a/test/run.js b/test/run.js index 5acc748..2b49367 100644 --- a/test/run.js +++ b/test/run.js @@ -13,10 +13,17 @@ function collectGarbage() { } describe('run', () => { - let port = 8555; let testrpcProcess = null; - let script = `node ./exec.js --dir "./mock" --port ${port} --testing --silent`; // --silent + let script = 'node ./exec.js' let launchTestRpc = false; + let port = 8555; + + let config = { + dir: "./mock", + port: port, + testing: true, + silent: true + }; before(() => { mock.protectCoverage(); @@ -29,11 +36,17 @@ describe('run', () => { // the run script. This allows us to end run CI container issues AND verify that the script in // exec actually works. if (launchTestRpc) { + launchTestRpc = false; port = 8557; - script = `node ./exec.js --dir "./mock" --port ${port} --norpc --testing --silent`; // --silent + config = { + dir: "./mock", + port: port, + testing: true, + silent: true, + norpc: true, + }; const command = `./node_modules/ethereumjs-testrpc/bin/testrpc --gasLimit 0xfffffffffff --port ${port}`; testrpcProcess = childprocess.exec(command); - launchTestRpc = false; } }); @@ -53,7 +66,7 @@ describe('run', () => { // - Running this on Circle CI causes suite to crash it('flush test suite', () => { if (!process.env.CI){ // <---- CI is set by default on circle - mock.install('Simple.sol', 'simple.js'); + mock.install('Simple.sol', 'simple.js', config); shell.exec(script); // <---- This fails mysteriously, but we don't test here. collectGarbage(); } @@ -67,7 +80,7 @@ describe('run', () => { assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); // Run script (exits 0); - mock.install('Simple.sol', 'simple.js'); + mock.install('Simple.sol', 'simple.js', config); shell.exec(script); assert(shell.error() === null, 'script should not error'); @@ -89,7 +102,7 @@ describe('run', () => { // Run against contract that only uses method.call. assert(pathExists('./coverage') === false, 'should start without: coverage'); assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); - mock.install('OnlyCall.sol', 'only-call.js'); + mock.install('OnlyCall.sol', 'only-call.js', config); shell.exec(script); assert(shell.error() === null, 'script should not error'); @@ -108,7 +121,7 @@ describe('run', () => { assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); // Run with Simple.sol and a failing assertion in a truffle test - mock.install('Simple.sol', 'truffle-test-fail.js'); + mock.install('Simple.sol', 'truffle-test-fail.js', config); shell.exec(script); assert(shell.error() === null, 'script should not error'); assert(pathExists('./coverage') === true, 'script should gen coverage folder'); @@ -123,7 +136,7 @@ describe('run', () => { it('deployment cost > block gasLimit: should generate coverage, cleanup & exit(0)', () => { // Just making sure Expensive.sol compiles and deploys here. - mock.install('Expensive.sol', 'block-gas-limit.js'); + mock.install('Expensive.sol', 'block-gas-limit.js', config); shell.exec(script); assert(shell.error() === null, 'script should not error'); collectGarbage(); @@ -134,7 +147,7 @@ describe('run', () => { assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); // Run with Simple.sol and a syntax error in the truffle test - mock.install('Simple.sol', 'truffle-crash.js'); + mock.install('Simple.sol', 'truffle-crash.js', config); shell.exec(script); assert(shell.error() !== null, 'script should error'); assert(pathExists('./coverage') !== true, 'script should NOT gen coverage folder'); @@ -147,7 +160,7 @@ describe('run', () => { assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); // Run with SimpleError.sol (has syntax error) and working truffle test - mock.install('SimpleError.sol', 'simple.js'); + mock.install('SimpleError.sol', 'simple.js', config); shell.exec(script); assert(shell.error() !== null, 'script should error'); assert(pathExists('./coverage') !== true, 'script should NOT gen coverage folder'); @@ -159,7 +172,7 @@ describe('run', () => { // Run contract and test that pass but fire no events assert(pathExists('./coverage') === false, 'should start without: coverage'); assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); - mock.install('Empty.sol', 'empty.js'); + mock.install('Empty.sol', 'empty.js', config); shell.exec(script); assert(shell.error() !== null, 'script should error'); assert(pathExists('./coverage') !== true, 'script should NOT gen coverage folder'); diff --git a/test/util/mockTruffle.js b/test/util/mockTruffle.js index c386bd1..84ac4b0 100644 --- a/test/util/mockTruffle.js +++ b/test/util/mockTruffle.js @@ -38,7 +38,7 @@ module.exports.restoreCoverage = function () { * @param {String} contract located in /test/sources/run/ * @param {[type]} test located in /test/run/ */ -module.exports.install = function (contract, test) { +module.exports.install = function (contract, test, config) { shell.mkdir('./mock'); shell.mkdir('./mock/contracts'); shell.mkdir('./mock/migrations'); @@ -78,7 +78,10 @@ module.exports.install = function (contract, test) { }}};` ; + const configjs = `module.exports = ${JSON.stringify(config)}`; + fs.writeFileSync('./mock/truffle.js', trufflejs); + fs.writeFileSync('./.solcover.js', configjs); }; /** @@ -86,6 +89,7 @@ module.exports.install = function (contract, test) { */ module.exports.remove = function () { shell.config.silent = true; + shell.rm('./.solcover.js'); shell.rm('-Rf', 'mock'); shell.rm('-Rf', 'coverage'); shell.rm('coverage.json'); From c98c53bd92ce24d61e4dabadd34b8e944e10617b Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 30 Mar 2017 17:42:13 -0700 Subject: [PATCH 14/51] Debug CI - untoggle silent --- test/run.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/run.js b/test/run.js index 2b49367..736c7ce 100644 --- a/test/run.js +++ b/test/run.js @@ -22,7 +22,7 @@ describe('run', () => { dir: "./mock", port: port, testing: true, - silent: true + silent: false }; before(() => { @@ -42,7 +42,7 @@ describe('run', () => { dir: "./mock", port: port, testing: true, - silent: true, + silent: false, norpc: true, }; const command = `./node_modules/ethereumjs-testrpc/bin/testrpc --gasLimit 0xfffffffffff --port ${port}`; From 1bf4ac32f895ae686c7d06ae39c166dc33ddd19f Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 30 Mar 2017 18:00:02 -0700 Subject: [PATCH 15/51] Only run single testrpc instance in tests --- test/run.js | 32 +++++++------------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/test/run.js b/test/run.js index 736c7ce..23a3422 100644 --- a/test/run.js +++ b/test/run.js @@ -18,36 +18,22 @@ describe('run', () => { let launchTestRpc = false; let port = 8555; - let config = { + config = { dir: "./mock", port: port, testing: true, - silent: false + silent: false, + norpc: true, }; before(() => { mock.protectCoverage(); + const command = `./node_modules/ethereumjs-testrpc/bin/testrpc --gasLimit 0xfffffffffff --port ${port}`; + testrpcProcess = childprocess.exec(command); }); - beforeEach(() => { - // CI (Ubuntu) doesn't seem to be freeing server resources until the parent process of these - // tests exits so there are errors launching testrpc repeatedly on the same port. Tests #2 through - // #last will use this instance of testrpc (port 8557). Test #1 uses the instance launched by - // the run script. This allows us to end run CI container issues AND verify that the script in - // exec actually works. - if (launchTestRpc) { - launchTestRpc = false; - port = 8557; - config = { - dir: "./mock", - port: port, - testing: true, - silent: false, - norpc: true, - }; - const command = `./node_modules/ethereumjs-testrpc/bin/testrpc --gasLimit 0xfffffffffff --port ${port}`; - testrpcProcess = childprocess.exec(command); - } + afterEach(() => { + mock.remove(); }); after(() => { @@ -55,10 +41,6 @@ describe('run', () => { testrpcProcess.kill(); }); - afterEach(() => { - mock.remove(); - }); - // This pre-test flushes the suite. There's some kind of sequencing issue here in development, // possibly tied to the use of ethereumjs-vm in the coverage tests? // - tests pass w/out this if we only run these test - e.g. it only fails when running the suite. From e075842225ff9c2e14451b132c70e85818736023 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 30 Mar 2017 18:48:09 -0700 Subject: [PATCH 16/51] Use istanbul API instead of shell.exec b/c incompatible w/ npm install --save-dev solcover --- exec.js | 14 ++++++++++---- test/run.js | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/exec.js b/exec.js index a20d7be..e3f63c7 100644 --- a/exec.js +++ b/exec.js @@ -10,7 +10,10 @@ const SolidityCoder = require('web3/lib/solidity/coder.js'); const getInstrumentedVersion = require('./instrumentSolidity.js'); const CoverageMap = require('./coverageMap.js'); - +const istanbul = require('istanbul'); +const istanbulCollector = new istanbul.Collector(); +const istanbulReporter = new istanbul.Reporter(); + // Very high gas block limits / contract deployment limits const gasLimitString = '0xfffffffffff'; const gasLimitHex = 0xfffffffffff; @@ -89,7 +92,7 @@ try { } // For each contract except migrations.sol: -// 1. Generate reference to its real path (this identifies it in the reports) +// 1. Generate file path reference for coverage report // 2. Load contract as string // 3. Instrument contract // 4. Save instrumented contract in the coverage environment folder where covered tests will run @@ -145,8 +148,11 @@ try { const json = JSON.stringify(coverage.coverage); fs.writeFileSync('./coverage.json', json); - const istanbul = `./${modulesDir}/istanbul/lib/cli.js report lcov ${silence}`; - shell.exec(istanbul); + istanbulCollector.add(coverage.coverage); + istanbulReporter.addAll([ 'lcov', 'html' ]); + istanbulReporter.write(istanbulCollector, false, () => { + log('Istanbul coverage reports generated'); + }); } catch (err) { const msg = 'There was a problem generating producing the coverage map / running Istanbul.\n'; diff --git a/test/run.js b/test/run.js index 23a3422..14bc0fd 100644 --- a/test/run.js +++ b/test/run.js @@ -22,7 +22,7 @@ describe('run', () => { dir: "./mock", port: port, testing: true, - silent: false, + silent: true, norpc: true, }; From f6b617ea22076b814c0c35cb1904ad3b617f7cd4 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Fri, 31 Mar 2017 12:43:12 -0700 Subject: [PATCH 17/51] Debug CI: run flush test --- test/run.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/run.js b/test/run.js index 14bc0fd..a92b2d0 100644 --- a/test/run.js +++ b/test/run.js @@ -22,7 +22,7 @@ describe('run', () => { dir: "./mock", port: port, testing: true, - silent: true, + silent: false, norpc: true, }; @@ -47,11 +47,11 @@ describe('run', () => { // - the first test always fails unless there is a fresh testrpc install. // - Running this on Circle CI causes suite to crash it('flush test suite', () => { - if (!process.env.CI){ // <---- CI is set by default on circle + //if (!process.env.CI){ // <---- CI is set by default on circle mock.install('Simple.sol', 'simple.js', config); shell.exec(script); // <---- This fails mysteriously, but we don't test here. collectGarbage(); - } + //} }); // This test should be positioned first (or second if flushing) in the suite because of From 5228fe7054553da7766d90c30420e9aeb3450816 Mon Sep 17 00:00:00 2001 From: Adria Massanet Date: Wed, 12 Apr 2017 12:45:52 +0200 Subject: [PATCH 18/51] Exit process at end (travis got stuck if not) --- runCoveredTests.js | 1 + 1 file changed, 1 insertion(+) diff --git a/runCoveredTests.js b/runCoveredTests.js index 71c9302..19d1ecb 100644 --- a/runCoveredTests.js +++ b/runCoveredTests.js @@ -59,3 +59,4 @@ shell.exec('./node_modules/istanbul/lib/cli.js report lcov'); testrpcProcess.kill(); shell.rm('-rf', './../contracts'); shell.mv('./../originalContracts', './../contracts'); +process.exit(0); From da24a90e0d44808661fb820733493b0450e5c5de Mon Sep 17 00:00:00 2001 From: Adria Massanet Date: Wed, 12 Apr 2017 17:05:03 +0200 Subject: [PATCH 19/51] Allows contract inheritance --- coverageMap.js | 37 ++++++++++++++++++++++++++++--------- injector.js | 18 +++++++++--------- instrumentSolidity.js | 6 ++++++ 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/coverageMap.js b/coverageMap.js index d17a0f3..f1c6b8b 100644 --- a/coverageMap.js +++ b/coverageMap.js @@ -5,11 +5,7 @@ */ const SolidityCoder = require('web3/lib/solidity/coder.js'); const path = require('path'); - -const lineTopic = 'b8995a65f405d9756b41a334f38d8ff0c93c4934e170d3c1429c3e7ca101014d'; -const functionTopic = 'd4ce765fd23c5cc3660249353d61ecd18ca60549dd62cb9ca350a4244de7b87f'; -const branchTopic = 'd4cf56ed5ba572684f02f889f12ac42d9583c8e3097802060e949bfbb3c1bff5'; -const statementTopic = 'b51abbff580b3a34bbc725f2dc6f736e9d4b45a41293fd0084ad865a31fde0c8'; +const keccak = require('keccakjs') /** * Converts solcover event data into an object that can be @@ -20,6 +16,10 @@ module.exports = class CoverageMap { constructor() { this.coverage = {}; + this.lineTopics = [] + this.functionTopics = [] + this.branchTopics = [] + this.statementTopics = [] } /** @@ -29,7 +29,9 @@ module.exports = class CoverageMap { * @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, @@ -56,6 +58,18 @@ module.exports = class CoverageMap { for (let x = 1; x <= Object.keys(info.statementMap).length; x++) { this.coverage[canonicalContractPath].s[x] = 0; } + + const keccakhex = (x => { + var hash = new keccak(256) + hash.update(x) + return hash.digest('hex') + }); + + this.lineTopics.push(keccakhex("__Coverage"+info.contractName+"(string,uint256)")); + this.functionTopics.push(keccakhex("__FunctionCoverage"+info.contractName+"(string,uint256)")); + this.branchTopics.push(keccakhex("__BranchCoverage"+info.contractName+"(string,uint256,uint256)")); + this.statementTopics.push(keccakhex("__StatementCoverage"+info.contractName+"(string,uint256)")); + } /** @@ -68,22 +82,27 @@ module.exports = class CoverageMap { generate(events, pathPrefix) { for (let idx = 0; idx < events.length; idx++) { const event = JSON.parse(events[idx]); - if (event.topics.indexOf(lineTopic) >= 0) { + + if (event.topics.filter( t => this.lineTopics.indexOf(t) >= 0 ).length > 0) { const data = SolidityCoder.decodeParams(['string', 'uint256'], event.data.replace('0x', '')); const canonicalContractPath = data[0]; this.coverage[canonicalContractPath].l[data[1].toNumber()] += 1; - } else if (event.topics.indexOf(functionTopic) >= 0) { + + } else if (event.topics.filter( t => this.functionTopics.indexOf(t) >= 0 ).length > 0) { const data = SolidityCoder.decodeParams(['string', 'uint256'], event.data.replace('0x', '')); const canonicalContractPath = data[0]; this.coverage[canonicalContractPath].f[data[1].toNumber()] += 1; - } else if (event.topics.indexOf(branchTopic) >= 0) { + + } else if (event.topics.filter( t => this.branchTopics.indexOf(t) >= 0 ).length > 0) { const data = SolidityCoder.decodeParams(['string', 'uint256', 'uint256'], event.data.replace('0x', '')); const canonicalContractPath = data[0]; this.coverage[canonicalContractPath].b[data[1].toNumber()][data[2].toNumber()] += 1; - } else if (event.topics.indexOf(statementTopic) >= 0) { + + } else if (event.topics.filter( t => this.statementTopics.indexOf(t) >= 0 ).length > 0) { const data = SolidityCoder.decodeParams(['string', 'uint256'], event.data.replace('0x', '')); const canonicalContractPath = data[0]; this.coverage[canonicalContractPath].s[data[1].toNumber()] += 1; + } } return Object.assign({}, this.coverage); diff --git a/injector.js b/injector.js index c3dd7a5..169e114 100644 --- a/injector.js +++ b/injector.js @@ -6,27 +6,27 @@ injector.callEvent = function injectCallEvent(contract, fileName, injectionPoint const linecount = (contract.instrumented.slice(0, injectionPoint).match(/\n/g) || []).length + 1; contract.runnableLines.push(linecount); contract.instrumented = contract.instrumented.slice(0, injectionPoint) + - 'Coverage(\'' + fileName + '\',' + linecount + ');\n' + + '__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) + - 'FunctionCoverage(\'' + fileName + '\',' + injection.fnId + ');\n' + + '__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 ? '{' : '') + - 'BranchCoverage(\'' + fileName + '\',' + injection.branchId + ',' + injection.locationIdx + ')' + + '__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 { BranchCoverage(\'' + fileName + '\',' + injection.branchId + ',' + injection.locationIdx + ');}\n' + + 'else { __BranchCoverage'+contract.contractName+'(\'' + fileName + '\',' + injection.branchId + ',' + injection.locationIdx + ');}\n' + contract.instrumented.slice(injectionPoint); }; @@ -44,16 +44,16 @@ injector.literal = function injectLiteral(contract, fileName, injectionPoint, in injector.statement = function injectStatement(contract, fileName, injectionPoint, injection) { contract.instrumented = contract.instrumented.slice(0, injectionPoint) + - ' StatementCoverage(\'' + fileName + '\',' + injection.statementId + ');\n' + + ' __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(string fileName, uint256 lineNumber);\n' + - 'event FunctionCoverage(string fileName, uint256 fnId);\n' + - 'event StatementCoverage(string fileName, uint256 statementId);\n' + - 'event BranchCoverage(string fileName, uint256 branchId, uint256 locationIdx);\n' + + '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' + contract.instrumented.slice(injectionPoint); }; diff --git a/instrumentSolidity.js b/instrumentSolidity.js index 7837a1b..2d75844 100644 --- a/instrumentSolidity.js +++ b/instrumentSolidity.js @@ -38,8 +38,12 @@ module.exports = function instrumentSolidity(contractSource, fileName) { contract.preprocessed = preprocessor.run(contract.source); contract.instrumented = contract.preprocessed; + ast = SolidityParser.parse(contract.preprocessed); + const contractStatement = ast['body'].filter( node => { return node.type == 'ContractStatement' }); + contract.contractName = contractStatement[0].name + parse[ast.type](contract, ast); // var result = solparse.parse(contract); @@ -59,5 +63,7 @@ module.exports = function instrumentSolidity(contractSource, fileName) { }); retValue.runnableLines = contract.runnableLines; retValue.contract = contract.instrumented; + retValue.contractName = contractStatement[0].name + return retValue; }; From 128dd31c6ff34a129c36d8e6ef0eb61580dc0df6 Mon Sep 17 00:00:00 2001 From: Adria Massanet Date: Wed, 12 Apr 2017 17:13:10 +0200 Subject: [PATCH 20/51] Added keccakjs dependency --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 25c3156..1917c5c 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dependencies": { "ethereumjs-testrpc": "https://github.com/ethereumjs/testrpc.git#5ba3c45b2ca306ab589f4a4c649d9afc39042680", "istanbul": "^0.4.5", + "keccakjs": "^0.2.1", "mkdirp": "^0.5.1", "shelljs": "^0.7.4", "sol-explore": "^1.6.2", From fbfd049f06dff4ba9955334bccb35d0205193a75 Mon Sep 17 00:00:00 2001 From: Adria Massanet Date: Wed, 12 Apr 2017 22:32:09 +0200 Subject: [PATCH 21/51] eslint-ed --- coverageMap.js | 38 ++++++++++++++++---------------------- injector.js | 18 +++++++++--------- instrumentSolidity.js | 6 +++--- 3 files changed, 28 insertions(+), 34 deletions(-) diff --git a/coverageMap.js b/coverageMap.js index f1c6b8b..d2880b2 100644 --- a/coverageMap.js +++ b/coverageMap.js @@ -5,7 +5,7 @@ */ const SolidityCoder = require('web3/lib/solidity/coder.js'); const path = require('path'); -const keccak = require('keccakjs') +const keccak = require('keccakjs'); /** * Converts solcover event data into an object that can be @@ -16,10 +16,10 @@ module.exports = class CoverageMap { constructor() { this.coverage = {}; - this.lineTopics = [] - this.functionTopics = [] - this.branchTopics = [] - this.statementTopics = [] + this.lineTopics = []; + this.functionTopics = []; + this.branchTopics = []; + this.statementTopics = []; } /** @@ -31,7 +31,6 @@ module.exports = class CoverageMap { */ addContract(info, canonicalContractPath) { - this.coverage[canonicalContractPath] = { l: {}, path: canonicalContractPath, @@ -60,16 +59,15 @@ module.exports = class CoverageMap { } const keccakhex = (x => { - var hash = new keccak(256) - hash.update(x) - return hash.digest('hex') + const hash = new keccak(256); // eslint-disable-line new-cap + hash.update(x); + return hash.digest('hex'); }); - this.lineTopics.push(keccakhex("__Coverage"+info.contractName+"(string,uint256)")); - this.functionTopics.push(keccakhex("__FunctionCoverage"+info.contractName+"(string,uint256)")); - this.branchTopics.push(keccakhex("__BranchCoverage"+info.contractName+"(string,uint256,uint256)")); - this.statementTopics.push(keccakhex("__StatementCoverage"+info.contractName+"(string,uint256)")); - + this.lineTopics.push(keccakhex('__Coverage' + info.contractName + '(string,uint256)')); + this.functionTopics.push(keccakhex('__FunctionCoverage' + info.contractName + '(string,uint256)')); + this.branchTopics.push(keccakhex('__BranchCoverage' + info.contractName + '(string,uint256,uint256)')); + this.statementTopics.push(keccakhex('__StatementCoverage' + info.contractName + '(string,uint256)')); } /** @@ -83,26 +81,22 @@ module.exports = class CoverageMap { 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) { + if (event.topics.filter(t => this.lineTopics.indexOf(t) >= 0).length > 0) { const data = SolidityCoder.decodeParams(['string', 'uint256'], event.data.replace('0x', '')); const canonicalContractPath = data[0]; this.coverage[canonicalContractPath].l[data[1].toNumber()] += 1; - - } else if (event.topics.filter( t => this.functionTopics.indexOf(t) >= 0 ).length > 0) { + } else if (event.topics.filter(t => this.functionTopics.indexOf(t) >= 0).length > 0) { const data = SolidityCoder.decodeParams(['string', 'uint256'], event.data.replace('0x', '')); const canonicalContractPath = data[0]; this.coverage[canonicalContractPath].f[data[1].toNumber()] += 1; - - } else if (event.topics.filter( t => this.branchTopics.indexOf(t) >= 0 ).length > 0) { + } else if (event.topics.filter(t => this.branchTopics.indexOf(t) >= 0).length > 0) { const data = SolidityCoder.decodeParams(['string', 'uint256', 'uint256'], event.data.replace('0x', '')); const canonicalContractPath = data[0]; this.coverage[canonicalContractPath].b[data[1].toNumber()][data[2].toNumber()] += 1; - - } else if (event.topics.filter( t => this.statementTopics.indexOf(t) >= 0 ).length > 0) { + } else if (event.topics.filter(t => this.statementTopics.indexOf(t) >= 0).length > 0) { const data = SolidityCoder.decodeParams(['string', 'uint256'], event.data.replace('0x', '')); const canonicalContractPath = data[0]; this.coverage[canonicalContractPath].s[data[1].toNumber()] += 1; - } } return Object.assign({}, this.coverage); diff --git a/injector.js b/injector.js index 169e114..d3f687d 100644 --- a/injector.js +++ b/injector.js @@ -6,27 +6,27 @@ injector.callEvent = function injectCallEvent(contract, fileName, injectionPoint const linecount = (contract.instrumented.slice(0, injectionPoint).match(/\n/g) || []).length + 1; contract.runnableLines.push(linecount); contract.instrumented = contract.instrumented.slice(0, injectionPoint) + - '__Coverage'+contract.contractName+'(\'' + fileName + '\',' + linecount + ');\n' + + '__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) + - '__FunctionCoverage'+contract.contractName+'(\'' + fileName + '\',' + injection.fnId + ');\n' + + '__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 ? '{' : '') + - '__BranchCoverage'+contract.contractName+'(\'' + fileName + '\',' + injection.branchId + ',' + injection.locationIdx + ')' + + '__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 { __BranchCoverage'+contract.contractName+'(\'' + fileName + '\',' + injection.branchId + ',' + injection.locationIdx + ');}\n' + + 'else { __BranchCoverage' + contract.contractName + '(\'' + fileName + '\',' + injection.branchId + ',' + injection.locationIdx + ');}\n' + contract.instrumented.slice(injectionPoint); }; @@ -44,16 +44,16 @@ injector.literal = function injectLiteral(contract, fileName, injectionPoint, in injector.statement = function injectStatement(contract, fileName, injectionPoint, injection) { contract.instrumented = contract.instrumented.slice(0, injectionPoint) + - ' __StatementCoverage'+contract.contractName+'(\'' + fileName + '\',' + injection.statementId + ');\n' + + ' __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 __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' + contract.instrumented.slice(injectionPoint); }; diff --git a/instrumentSolidity.js b/instrumentSolidity.js index 2d75844..f8cdd6f 100644 --- a/instrumentSolidity.js +++ b/instrumentSolidity.js @@ -41,8 +41,8 @@ module.exports = function instrumentSolidity(contractSource, fileName) { ast = SolidityParser.parse(contract.preprocessed); - const contractStatement = ast['body'].filter( node => { return node.type == 'ContractStatement' }); - contract.contractName = contractStatement[0].name + const contractStatement = ast.body.filter(node => node.type === 'ContractStatement'); + contract.contractName = contractStatement[0].name; parse[ast.type](contract, ast); @@ -63,7 +63,7 @@ module.exports = function instrumentSolidity(contractSource, fileName) { }); retValue.runnableLines = contract.runnableLines; retValue.contract = contract.instrumented; - retValue.contractName = contractStatement[0].name + retValue.contractName = contractStatement[0].name; return retValue; }; From 2004589037f57e3440cabc250213f9ea29cbbd8a Mon Sep 17 00:00:00 2001 From: Adria Massanet Date: Wed, 12 Apr 2017 22:37:34 +0200 Subject: [PATCH 22/51] Updated solc to 0.4.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1917c5c..396a4d4 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,6 @@ "istanbul": "^0.4.5", "merkle-patricia-tree": "~2.1.2", "mocha": "^3.1.0", - "solc": "0.4.6" + "solc": "0.4.10" } } From 7012cda82ee19535ff87d6e6faeb65261b79487a Mon Sep 17 00:00:00 2001 From: Adria Massanet Date: Wed, 12 Apr 2017 22:49:07 +0200 Subject: [PATCH 23/51] reverted version to solc 0.4.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 396a4d4..310b335 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,6 @@ "istanbul": "^0.4.5", "merkle-patricia-tree": "~2.1.2", "mocha": "^3.1.0", - "solc": "0.4.10" + "solc": "0.4.8" } } From 9d24ac70879bbf8a881f8df7b9810a22d222117c Mon Sep 17 00:00:00 2001 From: cgewecke Date: Sun, 16 Apr 2017 18:22:32 -0700 Subject: [PATCH 24/51] Remove some extraneous deps, use sc-forks testrpc --- .gitignore | 1 + exec.js | 18 ++++++++---------- package.json | 2 +- test/run.js | 13 +++++-------- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 88ae067..f5b0cd5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ allFiredEvents coverage.json coverage/ node_modules/ +.changelog diff --git a/exec.js b/exec.js index e3f63c7..7c7a90b 100644 --- a/exec.js +++ b/exec.js @@ -4,9 +4,7 @@ const shell = require('shelljs'); const fs = require('fs'); const reqCwd = require('req-cwd'); const path = require('path'); -const program = require('commander'); const childprocess = require('child_process'); -const SolidityCoder = require('web3/lib/solidity/coder.js'); const getInstrumentedVersion = require('./instrumentSolidity.js'); const CoverageMap = require('./coverageMap.js'); @@ -27,14 +25,14 @@ const solcoverDir = 'node_modules/solcover'; // Solcover assets let modulesDir = 'node_modules/solcover/node_modules'; // Solcover's npm assets: configurable via test // Options -let workingDir = '.'; // Default location of contracts folder -let port = 8555; // Default port - NOT 8545 & configurable via --port -let coverageOption = '--network coverage'; // Default truffle network execution flag -let silence = ''; // Default log level: configurable by --silence -let log = console.log; // Default log level: configurable by --silence - -let testrpcProcess; // ref to testrpc process we need to kill on exit -let events; // ref to string loaded from 'allFiredEvents' +let workingDir = '.'; // Default location of contracts folder +let port = 8555; // Default port - NOT 8545 & configurable via --port +let coverageOption = '--network coverage'; // Default truffle network execution flag +let silence = ''; // Default log level: configurable by --silence +let log = console.log; // Default log level: configurable by --silence + +let testrpcProcess; // ref to testrpc server we need to close on exit +let events; // ref to string loaded from 'allFiredEvents' // --------------------------------------- Script -------------------------------------------------- const config = reqCwd.silent(`${workingDir}/.solcover.js`) || {}; diff --git a/package.json b/package.json index dd23826..cf64f78 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "license": "ISC", "dependencies": { "commander": "^2.9.0", - "ethereumjs-testrpc": "https://github.com/cgewecke/testrpc.git#solcover", + "ethereumjs-testrpc": "https://github.com/sc-forks/testrpc.git", "istanbul": "^0.4.5", "mkdirp": "^0.5.1", "req-cwd": "^1.0.1", diff --git a/test/run.js b/test/run.js index a92b2d0..790853b 100644 --- a/test/run.js +++ b/test/run.js @@ -12,7 +12,7 @@ function collectGarbage() { if (global.gc) { global.gc(); } } -describe('run', () => { +describe.only('run', () => { let testrpcProcess = null; let script = 'node ./exec.js' let launchTestRpc = false; @@ -22,7 +22,7 @@ describe('run', () => { dir: "./mock", port: port, testing: true, - silent: false, + silent: true, norpc: true, }; @@ -45,13 +45,10 @@ describe('run', () => { // possibly tied to the use of ethereumjs-vm in the coverage tests? // - tests pass w/out this if we only run these test - e.g. it only fails when running the suite. // - the first test always fails unless there is a fresh testrpc install. - // - Running this on Circle CI causes suite to crash it('flush test suite', () => { - //if (!process.env.CI){ // <---- CI is set by default on circle - mock.install('Simple.sol', 'simple.js', config); - shell.exec(script); // <---- This fails mysteriously, but we don't test here. - collectGarbage(); - //} + mock.install('Simple.sol', 'simple.js', config); + shell.exec(script); // <---- This fails mysteriously, but we don't test here. + collectGarbage(); }); // This test should be positioned first (or second if flushing) in the suite because of From 440e6850ace9edfed18f5b6340e5ed69347136cc Mon Sep 17 00:00:00 2001 From: cgewecke Date: Tue, 18 Apr 2017 15:21:07 -0700 Subject: [PATCH 25/51] Use sc-forks, add inheritance test, fix report bug --- exec.js | 36 ++++++++++++-------- package.json | 2 +- test/run.js | 32 ++++++++++++++---- test/run/inheritance.js | 12 +++++++ test/run/only-call.js | 13 ++++--- test/sources/run/OnlyCall.sol | 5 +-- test/sources/run/Owned.sol | 6 ++++ test/sources/run/Proxy.sol | 13 +++++++ test/util/mockTruffle.js | 64 ++++++++++++++++++++++++++++++++++- 9 files changed, 153 insertions(+), 30 deletions(-) create mode 100644 test/run/inheritance.js create mode 100644 test/sources/run/Owned.sol create mode 100644 test/sources/run/Proxy.sol diff --git a/exec.js b/exec.js index 7c7a90b..cb5187c 100644 --- a/exec.js +++ b/exec.js @@ -20,9 +20,7 @@ const gasPriceHex = 0x01; const coverage = new CoverageMap(); // Paths -const coverageDir = './coverageEnv'; // Env that instrumented .sols are tested in -const solcoverDir = 'node_modules/solcover'; // Solcover assets -let modulesDir = 'node_modules/solcover/node_modules'; // Solcover's npm assets: configurable via test +const coverageDir = './coverageEnv'; // Env that instrumented .sols are tested in // Options let workingDir = '.'; // Default location of contracts folder @@ -32,14 +30,13 @@ let silence = ''; // Default log level: configurable b let log = console.log; // Default log level: configurable by --silence let testrpcProcess; // ref to testrpc server we need to close on exit -let events; // ref to string loaded from 'allFiredEvents' +let events; // ref to string loaded from 'allFiredEvents' // --------------------------------------- Script -------------------------------------------------- const config = reqCwd.silent(`${workingDir}/.solcover.js`) || {}; if (config.dir) workingDir = config.dir; if (config.port) port = config.port; -if (config.testing) modulesDir = 'node_modules'; if (config.silent) { silence = '> /dev/null 2>&1'; // Silence for solcover's unit tests / CI @@ -51,7 +48,7 @@ if (config.silent) { if (!config.norpc) { try { log(`Launching testrpc on port ${port}`); - const command = `./${modulesDir}/ethereumjs-testrpc/bin/testrpc `; + const command = `./node_modules/ethereumjs-testrpc-sc/bin/testrpc`; const options = `--gasLimit ${gasLimitString} --port ${port}`; testrpcProcess = childprocess.exec(command + options); } catch (err) { @@ -102,11 +99,11 @@ try { if (file !== migrations) { log('Instrumenting ', file); const contractPath = path.resolve(file); + const canonicalPath = contractPath.split(`/coverageEnv`).join(''); const contract = fs.readFileSync(contractPath).toString(); - const instrumentedContractInfo = getInstrumentedVersion(contract, contractPath); - + const instrumentedContractInfo = getInstrumentedVersion(contract, canonicalPath); fs.writeFileSync(contractPath, instrumentedContractInfo.contract); - coverage.addContract(instrumentedContractInfo, contractPath); + coverage.addContract(instrumentedContractInfo, canonicalPath); } }); } catch (err) { @@ -117,8 +114,9 @@ try { // coverage environment folder try { log('Launching Truffle (this can take a few seconds)...'); - const truffle = `./../${modulesDir}/truffle/cli.js`; + const truffle = `./../node_modules/truffle/cli.js`; const command = `cd coverageEnv && ${truffle} test ${coverageOption} ${silence}`; + //const command = `cd coverageEnv && truffle test ${coverageOption} ${silence}`; shell.exec(command); } catch (err) { cleanUp(err); @@ -141,20 +139,28 @@ try { // Generate coverage / write coverage report / run istanbul try { - coverage.generate(events, `${coverageDir}/contracts/`); + //coverage.generate(events, `${coverageDir}/contracts/`); + coverage.generate(events, './contracts'); const json = JSON.stringify(coverage.coverage); fs.writeFileSync('./coverage.json', json); istanbulCollector.add(coverage.coverage); - istanbulReporter.addAll([ 'lcov', 'html' ]); - istanbulReporter.write(istanbulCollector, false, () => { + istanbulReporter.add('html'); + istanbulReporter.add('lcov'); + istanbulReporter.add('text'); + istanbulReporter.write(istanbulCollector, true, () => { log('Istanbul coverage reports generated'); }); } catch (err) { - const msg = 'There was a problem generating producing the coverage map / running Istanbul.\n'; - cleanUp(msg + err); + if (config.testing){ + cleanUp() + } else { + const msg = 'There was a problem generating producing the coverage map / running Istanbul.\n'; + cleanUp(msg + err); + } + } // Finish diff --git a/package.json b/package.json index cf64f78..a3dddae 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "license": "ISC", "dependencies": { "commander": "^2.9.0", - "ethereumjs-testrpc": "https://github.com/sc-forks/testrpc.git", + "ethereumjs-testrpc": "https://github.com/sc-forks/testrpc-sc.git", "istanbul": "^0.4.5", "mkdirp": "^0.5.1", "req-cwd": "^1.0.1", diff --git a/test/run.js b/test/run.js index 790853b..766fe11 100644 --- a/test/run.js +++ b/test/run.js @@ -12,7 +12,7 @@ function collectGarbage() { if (global.gc) { global.gc(); } } -describe.only('run', () => { +describe('run', () => { let testrpcProcess = null; let script = 'node ./exec.js' let launchTestRpc = false; @@ -22,13 +22,13 @@ describe.only('run', () => { dir: "./mock", port: port, testing: true, - silent: true, + silent: true, // <-- Set to false to debug tests norpc: true, }; before(() => { mock.protectCoverage(); - const command = `./node_modules/ethereumjs-testrpc/bin/testrpc --gasLimit 0xfffffffffff --port ${port}`; + const command = `./node_modules/ethereumjs-testrpc-sc/bin/testrpc --gasLimit 0xfffffffffff --port ${port}`; testrpcProcess = childprocess.exec(command); }); @@ -38,7 +38,7 @@ describe.only('run', () => { after(() => { mock.restoreCoverage(); - testrpcProcess.kill(); + //testrpcProcess.kill(); }); // This pre-test flushes the suite. There's some kind of sequencing issue here in development, @@ -74,7 +74,6 @@ describe.only('run', () => { assert(produced[path].fnMap['1'].name === 'test', 'coverage.json should map "test"'); assert(produced[path].fnMap['2'].name === 'getX', 'coverage.json should map "getX"'); collectGarbage(); - launchTestRpc = true; // Toggle flag to launch a single testrpc instance for rest of tests. }); it('contract only uses .call: should generate coverage, cleanup & exit(0)', () => { @@ -91,7 +90,28 @@ describe.only('run', () => { const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8')); const path = Object.keys(produced)[0]; - assert(produced[path].fnMap['1'].name === 'getFive', 'coverage.json should map "getFive"'); + assert(produced[path].fnMap['1'].name === 'addTwo', 'coverage.json should map "addTwo"'); + collectGarbage(); + }); + + it('contract uses inheritance: should generate coverage, cleanup & exit(0)', () => { + // Run against a contract that 'is' another contract + assert(pathExists('./coverage') === false, 'should start without: coverage'); + assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); + mock.installInheritanceTest(config); + + shell.exec(script); + assert(shell.error() === null, 'script should not error'); + + assert(pathExists('./coverage') === true, 'script should gen coverage folder'); + assert(pathExists('./coverage.json') === true, 'script should gen coverage.json'); + + const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8')); + const ownedPath = Object.keys(produced)[0]; + const proxyPath = Object.keys(produced)[1]; + + assert(produced[ownedPath].fnMap['1'].name === 'Owned', 'coverage.json should map "Owned"'); + assert(produced[proxyPath].fnMap['1'].name === 'isOwner', 'coverage.json should map "isOwner"'); collectGarbage(); }); diff --git a/test/run/inheritance.js b/test/run/inheritance.js new file mode 100644 index 0000000..91b0a25 --- /dev/null +++ b/test/run/inheritance.js @@ -0,0 +1,12 @@ +const Owned = artifacts.require('./Owned.sol'); +const Proxy = artifacts.require('./Proxy.sol'); + +contract('Proxy', accounts => { + it('Should compile and run when one contract inherits from another', () => { + let proxy; + return Owned.deployed() + .then(instance => Proxy.deployed()) + .then(instance => instance.isOwner.call({from: accounts[0]})) + .then(val => assert.equal(val, true)); + }); +}); \ No newline at end of file diff --git a/test/run/only-call.js b/test/run/only-call.js index 3b2c697..c10a5fb 100644 --- a/test/run/only-call.js +++ b/test/run/only-call.js @@ -2,9 +2,12 @@ const OnlyCall = artifacts.require('./OnlyCall.sol'); contract('OnlyCall', accounts => { - it('should return 5', () => - OnlyCall.deployed() - .then(instance => instance.getFive.call()) - .then(val => assert.equal(val, 5)) - ); + it('should return val + 2', function(done){ + OnlyCall.deployed().then(function(instance){ + instance.addTwo.call(5, {from: accounts[0]}).then(function(val){ + assert.equal(val, 7); + done(); + }) + }) + }); }); diff --git a/test/sources/run/OnlyCall.sol b/test/sources/run/OnlyCall.sol index 9fe5838..87dda61 100644 --- a/test/sources/run/OnlyCall.sol +++ b/test/sources/run/OnlyCall.sol @@ -5,7 +5,8 @@ pragma solidity ^0.4.3; contract OnlyCall { - function getFive() returns (uint){ - return 5; + function addTwo(uint val) returns (uint){ + val = val + 2; + return val; } } \ No newline at end of file diff --git a/test/sources/run/Owned.sol b/test/sources/run/Owned.sol new file mode 100644 index 0000000..c6b91c7 --- /dev/null +++ b/test/sources/run/Owned.sol @@ -0,0 +1,6 @@ +pragma solidity ^0.4.4; + +contract Owned { + function Owned() { owner = msg.sender; } + address owner; +} \ No newline at end of file diff --git a/test/sources/run/Proxy.sol b/test/sources/run/Proxy.sol new file mode 100644 index 0000000..25bcba5 --- /dev/null +++ b/test/sources/run/Proxy.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.4.4; + +import "./Owned.sol"; + +contract Proxy is Owned { + function isOwner() returns (bool) { + if (msg.sender == owner) { + return true; + } else { + return false; + } + } +} \ No newline at end of file diff --git a/test/util/mockTruffle.js b/test/util/mockTruffle.js index 84ac4b0..5ab7998 100644 --- a/test/util/mockTruffle.js +++ b/test/util/mockTruffle.js @@ -45,7 +45,14 @@ module.exports.install = function (contract, test, config) { shell.mkdir('./mock/test'); // Mock contracts - shell.cp(`./test/sources/run/${contract}`, `./mock/contracts/${contract}`); + if (Array.isArray(contract)){ + contract.forEach(item => { + shell.cp(`./test/sources/run/${item}`, `./mock/contracts/${item}`); + }) + } else { + shell.cp(`./test/sources/run/${contract}`, `./mock/contracts/${contract}`); + } + shell.cp('./test/sources/run/Migrations.sol', './mock/contracts/Migrations.sol'); // Mock migrations @@ -84,6 +91,61 @@ module.exports.install = function (contract, test, config) { fs.writeFileSync('./.solcover.js', configjs); }; +/** + * Installs mock truffle project at ./mock with a single contract + * and test specified by the params. + * @param {String} contract located in /test/sources/run/ + * @param {[type]} test located in /test/run/ + */ +module.exports.installInheritanceTest = function (config) { + shell.mkdir('./mock'); + shell.mkdir('./mock/contracts'); + shell.mkdir('./mock/migrations'); + shell.mkdir('./mock/test'); + + // Mock contracts + shell.cp(`./test/sources/run/Proxy.sol`, `./mock/contracts/Proxy.sol`); + shell.cp(`./test/sources/run/Owned.sol`, `./mock/contracts/Owned.sol`); + shell.cp('./test/sources/run/Migrations.sol', './mock/contracts/Migrations.sol'); + + // Mock migrations + const initialMigration = ` + let Migrations = artifacts.require('Migrations.sol'); + module.exports = function(deployer) { + deployer.deploy(Migrations); + };`; + + const deployContracts = ` + var Owned = artifacts.require('./Owned.sol'); + var Proxy = artifacts.require('./Proxy.sol'); + module.exports = function(deployer) { + deployer.deploy(Owned); + deployer.link(Owned, Proxy); + deployer.deploy(Proxy); + };`; + + fs.writeFileSync('./mock/migrations/1_initial_migration.js', initialMigration); + fs.writeFileSync('./mock/migrations/2_deploy_contracts.js', deployContracts); + + // Mock test + shell.cp(`./test/run/inheritance.js`, `./mock/test/inheritance.js`); + + // Mock truffle.js + const trufflejs = `module.exports = { + networks: { + development: { + host: "localhost", + port: 8545, + network_id: "*" + }}};` + ; + + const configjs = `module.exports = ${JSON.stringify(config)}`; + + fs.writeFileSync('./mock/truffle.js', trufflejs); + fs.writeFileSync('./.solcover.js', configjs); +}; + /** * Removes mock truffle project and coverage reports generated by runCovered tests */ From 108d7a0327211e90a4f97c415ec1857550017970 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Tue, 18 Apr 2017 17:16:26 -0700 Subject: [PATCH 26/51] Try resolving merge conflict --- runCoveredTests.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 runCoveredTests.js diff --git a/runCoveredTests.js b/runCoveredTests.js new file mode 100644 index 0000000..e69de29 From d8eafb8178443bfd9ca0f251d93ed9cb5c1dfcb5 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Tue, 18 Apr 2017 18:57:17 -0700 Subject: [PATCH 27/51] Merge adriamb inheritance fix, use non-forked (global) truffle --- .gitignore | 1 + circle.yml | 1 + exec.js | 4 +--- package.json | 3 +-- test/run.js | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index f5b0cd5..b038cf3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ coverage.json coverage/ node_modules/ .changelog +.DS_Store diff --git a/circle.yml b/circle.yml index 280b523..7ea2dbc 100644 --- a/circle.yml +++ b/circle.yml @@ -3,6 +3,7 @@ machine: version: 6.9.1 dependencies: pre: + - npm install -g truffle - rm -rf node_modules/ test: override: diff --git a/exec.js b/exec.js index cb5187c..1dee969 100644 --- a/exec.js +++ b/exec.js @@ -114,9 +114,7 @@ try { // coverage environment folder try { log('Launching Truffle (this can take a few seconds)...'); - const truffle = `./../node_modules/truffle/cli.js`; - const command = `cd coverageEnv && ${truffle} test ${coverageOption} ${silence}`; - //const command = `cd coverageEnv && truffle test ${coverageOption} ${silence}`; + const command = `cd coverageEnv && truffle test ${coverageOption} ${silence}`; shell.exec(command); } catch (err) { cleanUp(err); diff --git a/package.json b/package.json index 8db4000..ba59f8d 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,7 @@ "req-cwd": "^1.0.1", "shelljs": "^0.7.4", "sol-explore": "^1.6.2", - "solidity-parser": "0.3.0", - "truffle": "git+https://github.com/cgewecke/truffle.git" + "solidity-parser": "0.3.0" }, "devDependencies": { "crypto-js": "^3.1.9-1", diff --git a/test/run.js b/test/run.js index 766fe11..d69a821 100644 --- a/test/run.js +++ b/test/run.js @@ -38,7 +38,7 @@ describe('run', () => { after(() => { mock.restoreCoverage(); - //testrpcProcess.kill(); + testrpcProcess.kill(); }); // This pre-test flushes the suite. There's some kind of sequencing issue here in development, From c4f03dc0b2825a87eca35e06843ce9c8f0ef2b43 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Wed, 19 Apr 2017 09:30:44 -0700 Subject: [PATCH 28/51] Lint --- .eslintrc | 4 ++ .eslintrc.js | 6 +++ exec.js | 71 +++++++++++++++++------------------ package.json | 2 +- test/run.js | 23 ++++++------ test/run/block-gas-limit.js | 5 ++- test/run/empty.js | 4 +- test/run/inheritance.js | 18 ++++----- test/run/only-call.js | 18 +++++---- test/run/simple.js | 8 ++-- test/run/sol-parse-fail.js | 9 +++-- test/run/truffle-crash.js | 5 ++- test/run/truffle-test-fail.js | 9 +++-- test/util/mockTruffle.js | 27 +++++++------ 14 files changed, 113 insertions(+), 96 deletions(-) create mode 100644 .eslintrc.js diff --git a/.eslintrc b/.eslintrc index c3878f7..4e1e5b7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,6 +1,10 @@ { "extends": ["airbnb-base"], "plugins": ["mocha"], + "globals": { + "artifacts": true, + "contract": true + }, "env":{ "meteor": true, "es6": true, diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..08a3f28 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + "extends": "airbnb-base", + "plugins": [ + "import" + ] +}; \ No newline at end of file diff --git a/exec.js b/exec.js index 1dee969..da5d6cc 100644 --- a/exec.js +++ b/exec.js @@ -7,11 +7,11 @@ const path = require('path'); const childprocess = require('child_process'); const getInstrumentedVersion = require('./instrumentSolidity.js'); const CoverageMap = require('./coverageMap.js'); - const istanbul = require('istanbul'); + const istanbulCollector = new istanbul.Collector(); const istanbulReporter = new istanbul.Reporter(); - + // Very high gas block limits / contract deployment limits const gasLimitString = '0xfffffffffff'; const gasLimitHex = 0xfffffffffff; @@ -30,8 +30,28 @@ let silence = ''; // Default log level: configurable b let log = console.log; // Default log level: configurable by --silence let testrpcProcess; // ref to testrpc server we need to close on exit -let events; // ref to string loaded from 'allFiredEvents' +let events; // ref to string loaded from 'allFiredEvents' + +// --------------------------------------- Utilities ----------------------------------------------- +/** + * Removes coverage build artifacts, kills testrpc. + * Exits (1) and prints msg on error, exits (0) otherwise. + * @param {String} err error message + */ +function cleanUp(err) { + log('Cleaning up...'); + shell.config.silent = true; + shell.rm('-Rf', `${coverageDir}`); + shell.rm('./allFiredEvents'); + if (testrpcProcess) { testrpcProcess.kill(); } + if (err) { + log(`${err}\nExiting without generating coverage...`); + process.exit(1); + } else { + process.exit(0); + } +} // --------------------------------------- Script -------------------------------------------------- const config = reqCwd.silent(`${workingDir}/.solcover.js`) || {}; @@ -48,7 +68,7 @@ if (config.silent) { if (!config.norpc) { try { log(`Launching testrpc on port ${port}`); - const command = `./node_modules/ethereumjs-testrpc-sc/bin/testrpc`; + const command = './node_modules/ethereumjs-testrpc-sc/bin/testrpc'; const options = `--gasLimit ${gasLimitString} --port ${port}`; testrpcProcess = childprocess.exec(command + options); } catch (err) { @@ -69,19 +89,19 @@ try { const truffleConfig = reqCwd(`${workingDir}/truffle.js`); // Coverage network opts specified: copy truffle.js whole to coverage environment - if (truffleConfig.networks.coverage){ + if (truffleConfig.networks.coverage) { shell.cp(`${workingDir}/truffle.js`, `${coverageDir}/truffle.js`); // Coverage network opts NOT specified: default to the development network w/ modified // port, gasLimit, gasPrice. Export the config object only. - } else { + } else { truffleConfig.networks.development.port = port; truffleConfig.networks.development.gas = gasLimitHex; truffleConfig.networks.development.gasPrice = gasPriceHex; coverageOption = ''; fs.writeFileSync(`${coverageDir}/truffle.js`, `module.exports = ${JSON.stringify(truffleConfig)}`); } -} catch (err){ +} catch (err) { const msg = ('There was a problem generating the coverage environment: '); cleanUp(msg + err); } @@ -93,13 +113,13 @@ try { // 4. Save instrumented contract in the coverage environment folder where covered tests will run // 5. Add instrumentation info to the coverage map try { - shell.ls(`${coverageDir}/contracts/**/*.sol`).forEach(file => { + shell.ls(`${coverageDir}/contracts/**/*.sol`).forEach((file) => { const migrations = `${coverageDir}/contracts/Migrations.sol`; if (file !== migrations) { log('Instrumenting ', file); const contractPath = path.resolve(file); - const canonicalPath = contractPath.split(`/coverageEnv`).join(''); + const canonicalPath = contractPath.split('/coverageEnv').join(''); const contract = fs.readFileSync(contractPath).toString(); const instrumentedContractInfo = getInstrumentedVersion(contract, canonicalPath); fs.writeFileSync(contractPath, instrumentedContractInfo.contract); @@ -137,9 +157,9 @@ try { // Generate coverage / write coverage report / run istanbul try { - //coverage.generate(events, `${coverageDir}/contracts/`); + // coverage.generate(events, `${coverageDir}/contracts/`); coverage.generate(events, './contracts'); - + const json = JSON.stringify(coverage.coverage); fs.writeFileSync('./coverage.json', json); @@ -148,39 +168,16 @@ try { istanbulReporter.add('lcov'); istanbulReporter.add('text'); istanbulReporter.write(istanbulCollector, true, () => { - log('Istanbul coverage reports generated'); + log('Istanbul coverage reports generated'); }); - } catch (err) { - if (config.testing){ - cleanUp() + if (config.testing) { + cleanUp(); } else { const msg = 'There was a problem generating producing the coverage map / running Istanbul.\n'; cleanUp(msg + err); } - } // Finish cleanUp(); - -// --------------------------------------- Utilities ----------------------------------------------- -/** - * Removes coverage build artifacts, kills testrpc. - * Exits (1) and prints msg on error, exits (0) otherwise. - * @param {String} err error message - */ -function cleanUp(err) { - log('Cleaning up...'); - shell.config.silent = true; - shell.rm('-Rf', `${coverageDir}`); - shell.rm('./allFiredEvents'); - if (testrpcProcess) { testrpcProcess.kill(); } - - if (err) { - log(`${err}\nExiting without generating coverage...`); - process.exit(1); - } else { - process.exit(0); - } -} diff --git a/package.json b/package.json index ba59f8d..7b1b208 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ }, "devDependencies": { "crypto-js": "^3.1.9-1", - "eslint": "^3.13.1", + "eslint": "^3.19.0", "eslint-config-airbnb-base": "^11.0.1", "eslint-plugin-import": "^2.2.0", "eslint-plugin-mocha": "^4.8.0", diff --git a/test/run.js b/test/run.js index d69a821..fe60bc9 100644 --- a/test/run.js +++ b/test/run.js @@ -1,3 +1,5 @@ +/* eslint-env node, mocha */ + const assert = require('assert'); const shell = require('shelljs'); const fs = require('fs'); @@ -14,13 +16,12 @@ function collectGarbage() { describe('run', () => { let testrpcProcess = null; - let script = 'node ./exec.js' - let launchTestRpc = false; - let port = 8555; + const script = 'node ./exec.js'; + const port = 8555; - config = { - dir: "./mock", - port: port, + const config = { + dir: './mock', + port, testing: true, silent: true, // <-- Set to false to debug tests norpc: true, @@ -41,17 +42,17 @@ describe('run', () => { testrpcProcess.kill(); }); - // This pre-test flushes the suite. There's some kind of sequencing issue here in development, + // This pre-test flushes the suite. There's some kind of sequencing issue here in development, // possibly tied to the use of ethereumjs-vm in the coverage tests? // - tests pass w/out this if we only run these test - e.g. it only fails when running the suite. - // - the first test always fails unless there is a fresh testrpc install. + // - the first test always fails unless there is a fresh testrpc install. it('flush test suite', () => { mock.install('Simple.sol', 'simple.js', config); shell.exec(script); // <---- This fails mysteriously, but we don't test here. collectGarbage(); }); - // This test should be positioned first (or second if flushing) in the suite because of + // This test should be positioned first (or second if flushing) in the suite because of // the way we're launching testrpc it('simple contract: should generate coverage, cleanup & exit(0)', () => { // Directory should be clean @@ -95,7 +96,7 @@ describe('run', () => { }); it('contract uses inheritance: should generate coverage, cleanup & exit(0)', () => { - // Run against a contract that 'is' another contract + // Run against a contract that 'is' another contract assert(pathExists('./coverage') === false, 'should start without: coverage'); assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); mock.installInheritanceTest(config); @@ -109,7 +110,7 @@ describe('run', () => { const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8')); const ownedPath = Object.keys(produced)[0]; const proxyPath = Object.keys(produced)[1]; - + assert(produced[ownedPath].fnMap['1'].name === 'Owned', 'coverage.json should map "Owned"'); assert(produced[proxyPath].fnMap['1'].name === 'isOwner', 'coverage.json should map "isOwner"'); collectGarbage(); diff --git a/test/run/block-gas-limit.js b/test/run/block-gas-limit.js index 641ca76..795933a 100644 --- a/test/run/block-gas-limit.js +++ b/test/run/block-gas-limit.js @@ -1,6 +1,7 @@ - +/* eslint-env node, mocha */ +/* global artifacts, contract */ const Expensive = artifacts.require('./Expensive.sol'); -contract('Expensive', accounts => { +contract('Expensive', () => { it('should deploy', () => Expensive.deployed()); }); diff --git a/test/run/empty.js b/test/run/empty.js index 2401a8c..def0cd0 100644 --- a/test/run/empty.js +++ b/test/run/empty.js @@ -1,6 +1,8 @@ +/* eslint-env node, mocha */ +/* global artifacts, contract */ const Empty = artifacts.require('./Empty.sol'); -contract('Empty', accounts => { +contract('Empty', () => { it('should deploy', () => Empty.deployed()); }); diff --git a/test/run/inheritance.js b/test/run/inheritance.js index 91b0a25..ebcde77 100644 --- a/test/run/inheritance.js +++ b/test/run/inheritance.js @@ -1,12 +1,12 @@ +/* eslint-env node, mocha */ +/* global artifacts, contract, assert */ + const Owned = artifacts.require('./Owned.sol'); const Proxy = artifacts.require('./Proxy.sol'); -contract('Proxy', accounts => { - it('Should compile and run when one contract inherits from another', () => { - let proxy; - return Owned.deployed() - .then(instance => Proxy.deployed()) - .then(instance => instance.isOwner.call({from: accounts[0]})) - .then(val => assert.equal(val, true)); - }); -}); \ No newline at end of file +contract('Proxy', (accounts) => { + it('Should compile and run when one contract inherits from another', () => Owned.deployed() + .then(() => Proxy.deployed()) + .then(instance => instance.isOwner.call({ from: accounts[0] })) + .then(val => assert.equal(val, true))); +}); diff --git a/test/run/only-call.js b/test/run/only-call.js index c10a5fb..ebeecbb 100644 --- a/test/run/only-call.js +++ b/test/run/only-call.js @@ -1,13 +1,15 @@ +/* eslint-env node, mocha */ +/* global artifacts, contract, assert */ const OnlyCall = artifacts.require('./OnlyCall.sol'); -contract('OnlyCall', accounts => { - it('should return val + 2', function(done){ - OnlyCall.deployed().then(function(instance){ - instance.addTwo.call(5, {from: accounts[0]}).then(function(val){ - assert.equal(val, 7); - done(); - }) - }) +contract('OnlyCall', (accounts) => { + it('should return val + 2', (done) => { + OnlyCall.deployed().then((instance) => { + instance.addTwo.call(5, { from: accounts[0] }).then((val) => { + assert.equal(val, 7); + done(); + }); + }); }); }); diff --git a/test/run/simple.js b/test/run/simple.js index 9c974ab..4d8c220 100644 --- a/test/run/simple.js +++ b/test/run/simple.js @@ -1,14 +1,16 @@ +/* eslint-env node, mocha */ +/* global artifacts, contract, assert */ const Simple = artifacts.require('./Simple.sol'); -contract('Simple', accounts => { +contract('Simple', () => { it('should set x to 5', () => { let simple; - return Simple.deployed().then(instance => { + return Simple.deployed().then((instance) => { simple = instance; return simple.test(5); }) - .then(val => simple.getX.call()) + .then(() => simple.getX.call()) .then(val => assert.equal(val.toNumber(), 5)); }); }); diff --git a/test/run/sol-parse-fail.js b/test/run/sol-parse-fail.js index 2b09bd1..d944311 100644 --- a/test/run/sol-parse-fail.js +++ b/test/run/sol-parse-fail.js @@ -1,16 +1,17 @@ - +/* eslint-env node, mocha */ +/* global artifacts, contract, assert */ const Simple = artifacts.require('./Simple.sol'); // This test is constructed correctly but the SimpleError.sol has a syntax error -contract('SimpleError', accounts => { +contract('SimpleError', () => { it('should set x to 5', () => { let simple; - return Simple.deployed().then(instance => { + return Simple.deployed().then((instance) => { simple = instance; return simple.test(5); }) - .then(val => simple.getX.call()) + .then(() => simple.getX.call()) .then(val => assert.equal(val, 5)); }); }); diff --git a/test/run/truffle-crash.js b/test/run/truffle-crash.js index 6ee35ea..33633aa 100644 --- a/test/run/truffle-crash.js +++ b/test/run/truffle-crash.js @@ -1,9 +1,10 @@ -'use strict' +/* eslint-env node, mocha */ +/* global artifacts, contract */ var Simple = artifacts.require('./Simple.sol'); // This test should break truffle because it has a syntax error. -contract('Simple', function(accounts){ +contract('Simple', () => { it('should crash', function(){ return Simple.deployed().then.why. }) diff --git a/test/run/truffle-test-fail.js b/test/run/truffle-test-fail.js index 211a409..86b4425 100644 --- a/test/run/truffle-test-fail.js +++ b/test/run/truffle-test-fail.js @@ -1,15 +1,16 @@ - +/* eslint-env node, mocha */ +/* global artifacts, contract, assert */ const Simple = artifacts.require('./Simple.sol'); -contract('Simple', accounts => { +contract('Simple', () => { it('should set x to 5', () => { let simple; - return Simple.deployed().then(instance => { + return Simple.deployed().then((instance) => { simple = instance; return simple.test(5); }) - .then(val => simple.getX.call()) + .then(() => simple.getX.call()) .then(val => assert.equal(val.toNumber(), 4)); // <-- Wrong result: test fails }); }); diff --git a/test/util/mockTruffle.js b/test/util/mockTruffle.js index 5ab7998..f9b952b 100644 --- a/test/util/mockTruffle.js +++ b/test/util/mockTruffle.js @@ -3,7 +3,6 @@ This file contains utilities for generating a mock truffle project to test solcover's run script against. */ -const assert = require('assert'); const fs = require('fs'); const shell = require('shelljs'); @@ -11,7 +10,7 @@ const shell = require('shelljs'); * Moves existing coverage reports into a safe place while testing run script which * would overwrite them. Silences shell complaints about non-existent files. */ -module.exports.protectCoverage = function () { +module.exports.protectCoverage = function protectCoverage() { shell.config.silent = true; shell.rm('-Rf', './safe'); shell.mkdir('./safe'); @@ -24,7 +23,7 @@ module.exports.protectCoverage = function () { * Restores pre-existing coverage reports after testing run script. * Silences shell complaints about non-existent files. */ -module.exports.restoreCoverage = function () { +module.exports.restoreCoverage = function restoreCoverage() { shell.config.silent = true; shell.mv('./safe/coverage', './coverage'); shell.mv('./safe/coverage.json', './coverage.json'); @@ -38,17 +37,17 @@ module.exports.restoreCoverage = function () { * @param {String} contract located in /test/sources/run/ * @param {[type]} test located in /test/run/ */ -module.exports.install = function (contract, test, config) { +module.exports.install = function install(contract, test, config) { shell.mkdir('./mock'); shell.mkdir('./mock/contracts'); shell.mkdir('./mock/migrations'); shell.mkdir('./mock/test'); // Mock contracts - if (Array.isArray(contract)){ - contract.forEach(item => { + if (Array.isArray(contract)) { + contract.forEach((item) => { shell.cp(`./test/sources/run/${item}`, `./mock/contracts/${item}`); - }) + }); } else { shell.cp(`./test/sources/run/${contract}`, `./mock/contracts/${contract}`); } @@ -62,7 +61,7 @@ module.exports.install = function (contract, test, config) { deployer.deploy(Migrations); };`; - const contractLocation = './' + contract; + const contractLocation = `./${contract}`; const deployContracts = ` var contract = artifacts.require('${contractLocation}'); module.exports = function(deployer) { @@ -97,15 +96,15 @@ module.exports.install = function (contract, test, config) { * @param {String} contract located in /test/sources/run/ * @param {[type]} test located in /test/run/ */ -module.exports.installInheritanceTest = function (config) { +module.exports.installInheritanceTest = function installInheritanceTest(config) { shell.mkdir('./mock'); shell.mkdir('./mock/contracts'); shell.mkdir('./mock/migrations'); shell.mkdir('./mock/test'); // Mock contracts - shell.cp(`./test/sources/run/Proxy.sol`, `./mock/contracts/Proxy.sol`); - shell.cp(`./test/sources/run/Owned.sol`, `./mock/contracts/Owned.sol`); + shell.cp('./test/sources/run/Proxy.sol', './mock/contracts/Proxy.sol'); + shell.cp('./test/sources/run/Owned.sol', './mock/contracts/Owned.sol'); shell.cp('./test/sources/run/Migrations.sol', './mock/contracts/Migrations.sol'); // Mock migrations @@ -128,7 +127,7 @@ module.exports.installInheritanceTest = function (config) { fs.writeFileSync('./mock/migrations/2_deploy_contracts.js', deployContracts); // Mock test - shell.cp(`./test/run/inheritance.js`, `./mock/test/inheritance.js`); + shell.cp('./test/run/inheritance.js', './mock/test/inheritance.js'); // Mock truffle.js const trufflejs = `module.exports = { @@ -149,11 +148,11 @@ module.exports.installInheritanceTest = function (config) { /** * Removes mock truffle project and coverage reports generated by runCovered tests */ -module.exports.remove = function () { +module.exports.remove = function remove() { shell.config.silent = true; shell.rm('./.solcover.js'); shell.rm('-Rf', 'mock'); shell.rm('-Rf', 'coverage'); shell.rm('coverage.json'); shell.config.silent = false; -}; \ No newline at end of file +}; From af947809339e3ce7dbe1f43e13e9dbfb1220301e Mon Sep 17 00:00:00 2001 From: cgewecke Date: Wed, 19 Apr 2017 09:50:08 -0700 Subject: [PATCH 29/51] Lint II --- .eslintrc | 5 ----- .eslintrc.js | 6 ------ test/conditional.js | 30 +++++++++++++++--------------- test/expressions.js | 4 ---- test/if.js | 33 ++++++++++++++++----------------- test/loops.js | 17 ++++++++--------- test/statements.js | 12 ++++++------ test/util/util.js | 4 ++-- test/util/vm.js | 9 ++++----- 9 files changed, 51 insertions(+), 69 deletions(-) delete mode 100644 .eslintrc.js diff --git a/.eslintrc b/.eslintrc index 4e1e5b7..66e2c2c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,10 +1,6 @@ { "extends": ["airbnb-base"], "plugins": ["mocha"], - "globals": { - "artifacts": true, - "contract": true - }, "env":{ "meteor": true, "es6": true, @@ -44,7 +40,6 @@ "semi": [2, "always"], "no-unused-vars": 0, "import/extensions": [0], - "import/no-dynamic-require": [0], "arrow-parens":[2, "as-needed"], "no-plusplus":[2, { "allowForLoopAfterthoughts": true }], "no-bitwise": [2], diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 08a3f28..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - "extends": "airbnb-base", - "plugins": [ - "import" - ] -}; \ No newline at end of file diff --git a/test/conditional.js b/test/conditional.js index 7d0aad3..e97dfff 100644 --- a/test/conditional.js +++ b/test/conditional.js @@ -1,6 +1,5 @@ /* eslint-env node, mocha */ -const solc = require('solc'); const path = require('path'); const getInstrumentedVersion = require('./../instrumentSolidity.js'); const util = require('./util/util.js'); @@ -12,13 +11,13 @@ describe('conditional statements', () => { const filePath = path.resolve('./test.sol'); const pathPrefix = './'; - it('should cover a conditional that reaches the consequent (same-line)', done => { + it('should cover a conditional that reaches the consequent (same-line)', (done) => { const contract = util.getCode('conditional/sameline-consequent.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); - vm.execute(info.contract, 'a', []).then(events => { + vm.execute(info.contract, 'a', []).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, 7: 1, @@ -36,13 +35,13 @@ describe('conditional statements', () => { }).catch(done); }); - it('should cover a conditional that reaches the alternate (same-line)', done => { + it('should cover a conditional that reaches the alternate (same-line)', (done) => { const contract = util.getCode('conditional/sameline-alternate.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); - vm.execute(info.contract, 'a', []).then(events => { + vm.execute(info.contract, 'a', []).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, 7: 1, @@ -60,13 +59,13 @@ describe('conditional statements', () => { }).catch(done); }); - it('should cover a conditional that reaches the consequent (multi-line)', done => { + it('should cover a conditional that reaches the consequent (multi-line)', (done) => { const contract = util.getCode('conditional/multiline-consequent.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); - vm.execute(info.contract, 'a', []).then(events => { + vm.execute(info.contract, 'a', []).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, 7: 1, @@ -84,13 +83,13 @@ describe('conditional statements', () => { }).catch(done); }); - it('should cover a conditional that reaches the alternate (multi-line)', done => { + it('should cover a conditional that reaches the alternate (multi-line)', (done) => { const contract = util.getCode('conditional/multiline-alternate.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); - vm.execute(info.contract, 'a', []).then(events => { + vm.execute(info.contract, 'a', []).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, 7: 1, @@ -108,14 +107,14 @@ describe('conditional statements', () => { }).catch(done); }); - it('should cover a DeclarativeExpression assignment by conditional that reaches the alternate', done => { + it('should cover a DeclarativeExpression assignment by conditional that reaches the alternate', (done) => { const contract = util.getCode('conditional/declarative-exp-assignment-alternate.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); // Runs bool z = (x) ? false : true; - vm.execute(info.contract, 'a', []).then(events => { + vm.execute(info.contract, 'a', []).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, 7: 1, @@ -133,14 +132,14 @@ describe('conditional statements', () => { }).catch(done); }); - it('should cover an Identifier assignment by conditional that reaches the alternate', done => { + it('should cover an Identifier assignment by conditional that reaches the alternate', (done) => { const contract = util.getCode('conditional/identifier-assignment-alternate.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); // Runs z = (x) ? false : true; - vm.execute(info.contract, 'a', []).then(events => { + vm.execute(info.contract, 'a', []).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, 7: 1, 8: 1, @@ -159,9 +158,10 @@ describe('conditional statements', () => { }); // Solcover has trouble with this case. The conditional coverage strategy relies on being able to - // reference the left-hand variable before its value is assigned. Solidity doesn't allow this for 'var'. + // reference the left-hand variable before its value is assigned. Solidity doesn't allow this + // for 'var'. - /* it('should cover a variable delcaration assignment by conditional that reaches the alternate', (done) => { + /* it('should cover a var decl assignment by conditional that reaches the alternate', (done) => { const contract = util.getCode('conditional/variable-decl-assignment-alternate.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); diff --git a/test/expressions.js b/test/expressions.js index 126e034..d0033d4 100644 --- a/test/expressions.js +++ b/test/expressions.js @@ -3,10 +3,7 @@ const solc = require('solc'); const getInstrumentedVersion = require('./../instrumentSolidity.js'); const util = require('./util/util.js'); -const CoverageMap = require('./../coverageMap'); const path = require('path'); -const vm = require('./util/vm'); -const assert = require('assert'); /** * NB: passing '1' to solc as an option activates the optimiser @@ -15,7 +12,6 @@ const assert = require('assert'); */ describe('generic expressions', () => { const filePath = path.resolve('./test.sol'); - const pathPrefix = './'; it('should compile after instrumenting a single binary expression', () => { const contract = util.getCode('expressions/single-binary-expression.sol'); diff --git a/test/if.js b/test/if.js index ab49b7a..d9e81ab 100644 --- a/test/if.js +++ b/test/if.js @@ -1,6 +1,5 @@ /* eslint-env node, mocha */ -const solc = require('solc'); const path = require('path'); const getInstrumentedVersion = require('./../instrumentSolidity.js'); const util = require('./util/util.js'); @@ -12,14 +11,14 @@ describe('if, else, and else if statements', () => { const filePath = path.resolve('./test.sol'); const pathPrefix = './'; - it('should cover an if statement with a bracketed consequent', done => { + 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 => { + vm.execute(info.contract, 'a', [1]).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, @@ -38,14 +37,14 @@ describe('if, else, and else if statements', () => { }); // Runs: a(1) => if (x == 1) x = 2; - it('should cover an unbracketed if consequent (single line)', done => { + 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 => { + vm.execute(info.contract, 'a', [1]).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, @@ -63,14 +62,14 @@ describe('if, else, and else if statements', () => { }).catch(done); }); - it('should cover an if statement with multiline bracketed consequent', 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 => { + vm.execute(info.contract, 'a', [1]).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, @@ -89,13 +88,13 @@ describe('if, else, and else if statements', () => { }); // Runs: a(1) => if (x == 1)\n x = 3; - it('should cover an unbracketed if consequent (multi-line)', done => { + 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 => { + vm.execute(info.contract, 'a', [1]).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, @@ -113,14 +112,14 @@ describe('if, else, and else if statements', () => { }).catch(done); }); - it('should cover a simple if statement with a failing condition', 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 => { + vm.execute(info.contract, 'a', [2]).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, @@ -139,13 +138,13 @@ describe('if, else, and else if statements', () => { }); // 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 => { + 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 => { + 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, @@ -163,13 +162,13 @@ describe('if, else, and else if statements', () => { }).catch(done); }); - it('should cover an if statement with an unbracketed alternate', 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 => { + 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, @@ -187,12 +186,12 @@ describe('if, else, and else if statements', () => { }).catch(done); }); - it('should cover nested if statements with missing else statements', 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 => { + 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, diff --git a/test/loops.js b/test/loops.js index f929251..c6b2d7f 100644 --- a/test/loops.js +++ b/test/loops.js @@ -1,6 +1,5 @@ /* eslint-env node, mocha */ -const solc = require('solc'); const path = require('path'); const getInstrumentedVersion = require('./../instrumentSolidity.js'); const util = require('./util/util.js'); @@ -12,14 +11,14 @@ describe('for and while statements', () => { const filePath = path.resolve('./test.sol'); const pathPrefix = './'; - it('should cover a for statement with a bracketed body (multiline)', done => { + 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, filePath); 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 => { + vm.execute(info.contract, 'a', []).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 10, @@ -35,14 +34,14 @@ describe('for and while statements', () => { }).catch(done); }); - it('should cover a for statement with an unbracketed body', 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, filePath); 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 => { + vm.execute(info.contract, 'a', []).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 10, @@ -58,14 +57,14 @@ describe('for and while statements', () => { }).catch(done); }); - it('should cover a while statement with an bracketed body (multiline)', 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, filePath); 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 => { + vm.execute(info.contract, 'a', []).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, 7: 1, @@ -81,14 +80,14 @@ describe('for and while statements', () => { }).catch(done); }); - it('should cover a while statement with an unbracketed body (multiline)', 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, filePath); 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 => { + vm.execute(info.contract, 'a', []).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, 7: 1, diff --git a/test/statements.js b/test/statements.js index f8138f0..af3ae59 100644 --- a/test/statements.js +++ b/test/statements.js @@ -45,13 +45,13 @@ describe('generic statements', () => { const output = solc.compile(info.contract, 1); util.report(output.errors); }); - it('should cover a statement following a close brace', done => { + it('should cover a statement following a close brace', (done) => { const contract = util.getCode('statements/post-close-brace.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); - vm.execute(info.contract, 'a', [1]).then(events => { + vm.execute(info.contract, 'a', [1]).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 0, 8: 1, @@ -69,13 +69,13 @@ describe('generic statements', () => { }).catch(done); }); - it('should cover a library statement and an invoked library method', done => { + it('should cover a library statement and an invoked library method', (done) => { const contract = util.getCode('statements/library.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); - vm.execute(info.contract, 'not', []).then(events => { + vm.execute(info.contract, 'not', []).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 9: 1, 10: 1, 19: 1, @@ -91,13 +91,13 @@ describe('generic statements', () => { }).catch(done); }); - it('should cover a tuple statement', done => { + it('should cover a tuple statement', (done) => { const contract = util.getCode('statements/tuple.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); - vm.execute(info.contract, 'a', []).then(events => { + vm.execute(info.contract, 'a', []).then((events) => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, diff --git a/test/util/util.js b/test/util/util.js index c476cce..d7b7c9c 100644 --- a/test/util/util.js +++ b/test/util/util.js @@ -7,11 +7,11 @@ const path = require('path'); * @return {String} contents of a .sol file */ module.exports.getCode = function getCode(_path) { - return fs.readFileSync(path.join(__dirname, './../sources/' + _path), 'utf8'); + return fs.readFileSync(path.join(__dirname, `./../sources/${_path}`), 'utf8'); }; module.exports.report = function report(errors) { if (errors) { - throw new Error('Instrumented solidity invalid: ' + errors); + throw new Error(`Instrumented solidity invalid: ${errors}`); } }; diff --git a/test/util/vm.js b/test/util/vm.js index 28a54b1..f8cae80 100644 --- a/test/util/vm.js +++ b/test/util/vm.js @@ -1,5 +1,4 @@ const solc = require('solc'); -const path = require('path'); const VM = require('ethereumjs-vm'); const Account = require('ethereumjs-account'); const Transaction = require('ethereumjs-tx'); @@ -17,12 +16,12 @@ const accountAddress = new Buffer('7caf6f9bc8b3ba5c7824f934c826bd6dc38c8467', 'h * Source: consensys/eth-lightwallet/lib/txutils.js (line 18) */ function encodeFunctionTxData(functionName, types, args) { - const fullName = functionName + '(' + types.join() + ')'; + 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; + return `0x${dataHex}`; } /** @@ -112,12 +111,12 @@ function callMethod(vm, abi, address, functionName, args) { const tx = new Transaction(options); tx.sign(new Buffer(secretKey, 'hex')); - return new Promise((resolve, reject) => { + return new Promise((resolve) => { vm.runTx({ tx, }, (err, results) => { const seenEvents = []; - results.vm.runState.logs.forEach(log => { + results.vm.runState.logs.forEach((log) => { const toWrite = {}; toWrite.address = log[0].toString('hex'); toWrite.topics = log[1].map(x => x.toString('hex')); From 1bea7577e8556fc95a926c8b77fd7b85874d581c Mon Sep 17 00:00:00 2001 From: cgewecke Date: Wed, 19 Apr 2017 09:56:27 -0700 Subject: [PATCH 30/51] Lint III: add .eslintignore for truffle-crash test (intentional syntax error) --- .eslintignore | 1 + exec.js | 2 +- test/conditional.js | 24 ++++++++++++------------ test/if.js | 32 ++++++++++++++++---------------- test/loops.js | 16 ++++++++-------- test/run/inheritance.js | 6 ++++-- test/run/only-call.js | 10 ++++++---- test/run/simple.js | 2 +- test/run/sol-parse-fail.js | 2 +- test/run/truffle-test-fail.js | 2 +- test/statements.js | 12 ++++++------ test/util/mockTruffle.js | 2 +- test/util/vm.js | 4 ++-- 13 files changed, 60 insertions(+), 55 deletions(-) create mode 100644 .eslintignore diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..bda468c --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +test/run/truffle-crash.js \ No newline at end of file diff --git a/exec.js b/exec.js index da5d6cc..e3d9169 100644 --- a/exec.js +++ b/exec.js @@ -113,7 +113,7 @@ try { // 4. Save instrumented contract in the coverage environment folder where covered tests will run // 5. Add instrumentation info to the coverage map try { - shell.ls(`${coverageDir}/contracts/**/*.sol`).forEach((file) => { + shell.ls(`${coverageDir}/contracts/**/*.sol`).forEach(file => { const migrations = `${coverageDir}/contracts/Migrations.sol`; if (file !== migrations) { diff --git a/test/conditional.js b/test/conditional.js index e97dfff..6385daa 100644 --- a/test/conditional.js +++ b/test/conditional.js @@ -11,13 +11,13 @@ describe('conditional statements', () => { const filePath = path.resolve('./test.sol'); const pathPrefix = './'; - it('should cover a conditional that reaches the consequent (same-line)', (done) => { + it('should cover a conditional that reaches the consequent (same-line)', done => { const contract = util.getCode('conditional/sameline-consequent.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); - vm.execute(info.contract, 'a', []).then((events) => { + vm.execute(info.contract, 'a', []).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, 7: 1, @@ -35,13 +35,13 @@ describe('conditional statements', () => { }).catch(done); }); - it('should cover a conditional that reaches the alternate (same-line)', (done) => { + it('should cover a conditional that reaches the alternate (same-line)', done => { const contract = util.getCode('conditional/sameline-alternate.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); - vm.execute(info.contract, 'a', []).then((events) => { + vm.execute(info.contract, 'a', []).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, 7: 1, @@ -59,13 +59,13 @@ describe('conditional statements', () => { }).catch(done); }); - it('should cover a conditional that reaches the consequent (multi-line)', (done) => { + it('should cover a conditional that reaches the consequent (multi-line)', done => { const contract = util.getCode('conditional/multiline-consequent.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); - vm.execute(info.contract, 'a', []).then((events) => { + vm.execute(info.contract, 'a', []).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, 7: 1, @@ -83,13 +83,13 @@ describe('conditional statements', () => { }).catch(done); }); - it('should cover a conditional that reaches the alternate (multi-line)', (done) => { + it('should cover a conditional that reaches the alternate (multi-line)', done => { const contract = util.getCode('conditional/multiline-alternate.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); - vm.execute(info.contract, 'a', []).then((events) => { + vm.execute(info.contract, 'a', []).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, 7: 1, @@ -107,14 +107,14 @@ describe('conditional statements', () => { }).catch(done); }); - it('should cover a DeclarativeExpression assignment by conditional that reaches the alternate', (done) => { + it('should cover a DeclarativeExpression assignment by conditional that reaches the alternate', done => { const contract = util.getCode('conditional/declarative-exp-assignment-alternate.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); // Runs bool z = (x) ? false : true; - vm.execute(info.contract, 'a', []).then((events) => { + vm.execute(info.contract, 'a', []).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, 7: 1, @@ -132,14 +132,14 @@ describe('conditional statements', () => { }).catch(done); }); - it('should cover an Identifier assignment by conditional that reaches the alternate', (done) => { + it('should cover an Identifier assignment by conditional that reaches the alternate', done => { const contract = util.getCode('conditional/identifier-assignment-alternate.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); // Runs z = (x) ? false : true; - vm.execute(info.contract, 'a', []).then((events) => { + vm.execute(info.contract, 'a', []).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, 7: 1, 8: 1, diff --git a/test/if.js b/test/if.js index d9e81ab..ed3816f 100644 --- a/test/if.js +++ b/test/if.js @@ -11,14 +11,14 @@ describe('if, else, and else if statements', () => { const filePath = path.resolve('./test.sol'); const pathPrefix = './'; - it('should cover an if statement with a bracketed consequent', (done) => { + 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) => { + vm.execute(info.contract, 'a', [1]).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, @@ -37,14 +37,14 @@ describe('if, else, and else if statements', () => { }); // Runs: a(1) => if (x == 1) x = 2; - it('should cover an unbracketed if consequent (single line)', (done) => { + 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) => { + vm.execute(info.contract, 'a', [1]).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, @@ -62,14 +62,14 @@ describe('if, else, and else if statements', () => { }).catch(done); }); - it('should cover an if statement with multiline bracketed consequent', (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) => { + vm.execute(info.contract, 'a', [1]).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, @@ -88,13 +88,13 @@ describe('if, else, and else if statements', () => { }); // Runs: a(1) => if (x == 1)\n x = 3; - it('should cover an unbracketed if consequent (multi-line)', (done) => { + 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) => { + vm.execute(info.contract, 'a', [1]).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, @@ -112,14 +112,14 @@ describe('if, else, and else if statements', () => { }).catch(done); }); - it('should cover a simple if statement with a failing condition', (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) => { + vm.execute(info.contract, 'a', [2]).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, @@ -138,13 +138,13 @@ describe('if, else, and else if statements', () => { }); // 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) => { + 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) => { + 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, @@ -162,13 +162,13 @@ describe('if, else, and else if statements', () => { }).catch(done); }); - it('should cover an if statement with an unbracketed alternate', (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) => { + 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, @@ -186,12 +186,12 @@ describe('if, else, and else if statements', () => { }).catch(done); }); - it('should cover nested if statements with missing else statements', (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) => { + 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, diff --git a/test/loops.js b/test/loops.js index c6b2d7f..186ca28 100644 --- a/test/loops.js +++ b/test/loops.js @@ -11,14 +11,14 @@ describe('for and while statements', () => { const filePath = path.resolve('./test.sol'); const pathPrefix = './'; - it('should cover a for statement with a bracketed body (multiline)', (done) => { + 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, filePath); 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) => { + vm.execute(info.contract, 'a', []).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 10, @@ -34,14 +34,14 @@ describe('for and while statements', () => { }).catch(done); }); - it('should cover a for statement with an unbracketed body', (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, filePath); 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) => { + vm.execute(info.contract, 'a', []).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 10, @@ -57,14 +57,14 @@ describe('for and while statements', () => { }).catch(done); }); - it('should cover a while statement with an bracketed body (multiline)', (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, filePath); 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) => { + vm.execute(info.contract, 'a', []).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, 7: 1, @@ -80,14 +80,14 @@ describe('for and while statements', () => { }).catch(done); }); - it('should cover a while statement with an unbracketed body (multiline)', (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, filePath); 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) => { + vm.execute(info.contract, 'a', []).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 1, 7: 1, diff --git a/test/run/inheritance.js b/test/run/inheritance.js index ebcde77..91fba75 100644 --- a/test/run/inheritance.js +++ b/test/run/inheritance.js @@ -4,9 +4,11 @@ const Owned = artifacts.require('./Owned.sol'); const Proxy = artifacts.require('./Proxy.sol'); -contract('Proxy', (accounts) => { +contract('Proxy', accounts => { it('Should compile and run when one contract inherits from another', () => Owned.deployed() .then(() => Proxy.deployed()) - .then(instance => instance.isOwner.call({ from: accounts[0] })) + .then(instance => instance.isOwner.call({ + from: accounts[0], + })) .then(val => assert.equal(val, true))); }); diff --git a/test/run/only-call.js b/test/run/only-call.js index ebeecbb..da72716 100644 --- a/test/run/only-call.js +++ b/test/run/only-call.js @@ -3,10 +3,12 @@ const OnlyCall = artifacts.require('./OnlyCall.sol'); -contract('OnlyCall', (accounts) => { - it('should return val + 2', (done) => { - OnlyCall.deployed().then((instance) => { - instance.addTwo.call(5, { from: accounts[0] }).then((val) => { +contract('OnlyCall', accounts => { + it('should return val + 2', done => { + OnlyCall.deployed().then(instance => { + instance.addTwo.call(5, { + from: accounts[0], + }).then(val => { assert.equal(val, 7); done(); }); diff --git a/test/run/simple.js b/test/run/simple.js index 4d8c220..1fcf550 100644 --- a/test/run/simple.js +++ b/test/run/simple.js @@ -6,7 +6,7 @@ const Simple = artifacts.require('./Simple.sol'); contract('Simple', () => { it('should set x to 5', () => { let simple; - return Simple.deployed().then((instance) => { + return Simple.deployed().then(instance => { simple = instance; return simple.test(5); }) diff --git a/test/run/sol-parse-fail.js b/test/run/sol-parse-fail.js index d944311..ff17dba 100644 --- a/test/run/sol-parse-fail.js +++ b/test/run/sol-parse-fail.js @@ -7,7 +7,7 @@ const Simple = artifacts.require('./Simple.sol'); contract('SimpleError', () => { it('should set x to 5', () => { let simple; - return Simple.deployed().then((instance) => { + return Simple.deployed().then(instance => { simple = instance; return simple.test(5); }) diff --git a/test/run/truffle-test-fail.js b/test/run/truffle-test-fail.js index 86b4425..e9457d0 100644 --- a/test/run/truffle-test-fail.js +++ b/test/run/truffle-test-fail.js @@ -6,7 +6,7 @@ const Simple = artifacts.require('./Simple.sol'); contract('Simple', () => { it('should set x to 5', () => { let simple; - return Simple.deployed().then((instance) => { + return Simple.deployed().then(instance => { simple = instance; return simple.test(5); }) diff --git a/test/statements.js b/test/statements.js index af3ae59..f8138f0 100644 --- a/test/statements.js +++ b/test/statements.js @@ -45,13 +45,13 @@ describe('generic statements', () => { const output = solc.compile(info.contract, 1); util.report(output.errors); }); - it('should cover a statement following a close brace', (done) => { + it('should cover a statement following a close brace', done => { const contract = util.getCode('statements/post-close-brace.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); - vm.execute(info.contract, 'a', [1]).then((events) => { + vm.execute(info.contract, 'a', [1]).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, 6: 0, 8: 1, @@ -69,13 +69,13 @@ describe('generic statements', () => { }).catch(done); }); - it('should cover a library statement and an invoked library method', (done) => { + it('should cover a library statement and an invoked library method', done => { const contract = util.getCode('statements/library.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); - vm.execute(info.contract, 'not', []).then((events) => { + vm.execute(info.contract, 'not', []).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 9: 1, 10: 1, 19: 1, @@ -91,13 +91,13 @@ describe('generic statements', () => { }).catch(done); }); - it('should cover a tuple statement', (done) => { + it('should cover a tuple statement', done => { const contract = util.getCode('statements/tuple.sol'); const info = getInstrumentedVersion(contract, filePath); const coverage = new CoverageMap(); coverage.addContract(info, filePath); - vm.execute(info.contract, 'a', []).then((events) => { + vm.execute(info.contract, 'a', []).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { 5: 1, diff --git a/test/util/mockTruffle.js b/test/util/mockTruffle.js index f9b952b..75193a2 100644 --- a/test/util/mockTruffle.js +++ b/test/util/mockTruffle.js @@ -45,7 +45,7 @@ module.exports.install = function install(contract, test, config) { // Mock contracts if (Array.isArray(contract)) { - contract.forEach((item) => { + contract.forEach(item => { shell.cp(`./test/sources/run/${item}`, `./mock/contracts/${item}`); }); } else { diff --git a/test/util/vm.js b/test/util/vm.js index f8cae80..61d9057 100644 --- a/test/util/vm.js +++ b/test/util/vm.js @@ -111,12 +111,12 @@ function callMethod(vm, abi, address, functionName, args) { const tx = new Transaction(options); tx.sign(new Buffer(secretKey, 'hex')); - return new Promise((resolve) => { + return new Promise(resolve => { vm.runTx({ tx, }, (err, results) => { const seenEvents = []; - results.vm.runState.logs.forEach((log) => { + results.vm.runState.logs.forEach(log => { const toWrite = {}; toWrite.address = log[0].toString('hex'); toWrite.topics = log[1].map(x => x.toString('hex')); From c98d08571897a2839c0ebdaa12ba5d92c131eb33 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Wed, 19 Apr 2017 14:35:33 -0700 Subject: [PATCH 31/51] Fix testrpc fork name in pkg, remove unused testing utils --- package.json | 2 +- test/run.js | 2 -- test/util/mockTruffle.js | 25 ------------------------- 3 files changed, 1 insertion(+), 28 deletions(-) diff --git a/package.json b/package.json index 7b1b208..229a4cf 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "license": "ISC", "dependencies": { "commander": "^2.9.0", - "ethereumjs-testrpc": "https://github.com/sc-forks/testrpc-sc.git", + "ethereumjs-testrpc-sc": "https://github.com/sc-forks/testrpc-sc.git", "istanbul": "^0.4.5", "keccakjs": "^0.2.1", "mkdirp": "^0.5.1", diff --git a/test/run.js b/test/run.js index fe60bc9..ef1a93d 100644 --- a/test/run.js +++ b/test/run.js @@ -28,7 +28,6 @@ describe('run', () => { }; before(() => { - mock.protectCoverage(); const command = `./node_modules/ethereumjs-testrpc-sc/bin/testrpc --gasLimit 0xfffffffffff --port ${port}`; testrpcProcess = childprocess.exec(command); }); @@ -38,7 +37,6 @@ describe('run', () => { }); after(() => { - mock.restoreCoverage(); testrpcProcess.kill(); }); diff --git a/test/util/mockTruffle.js b/test/util/mockTruffle.js index 75193a2..fce6fdd 100644 --- a/test/util/mockTruffle.js +++ b/test/util/mockTruffle.js @@ -6,31 +6,6 @@ const fs = require('fs'); const shell = require('shelljs'); -/** - * Moves existing coverage reports into a safe place while testing run script which - * would overwrite them. Silences shell complaints about non-existent files. - */ -module.exports.protectCoverage = function protectCoverage() { - shell.config.silent = true; - shell.rm('-Rf', './safe'); - shell.mkdir('./safe'); - shell.mv('./coverage', './safe/coverage'); - shell.mv('./coverage.json', './safe/coverage.json'); - shell.config.silent = false; -}; - -/** - * Restores pre-existing coverage reports after testing run script. - * Silences shell complaints about non-existent files. - */ -module.exports.restoreCoverage = function restoreCoverage() { - shell.config.silent = true; - shell.mv('./safe/coverage', './coverage'); - shell.mv('./safe/coverage.json', './coverage.json'); - shell.rm('-Rf', './safe'); - shell.config.silent = false; -}; - /** * Installs mock truffle project at ./mock with a single contract * and test specified by the params. From a46f73355d38c6c5f98ee9d8f4d43d501698e007 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Wed, 19 Apr 2017 15:02:22 -0700 Subject: [PATCH 32/51] Accomodate both locations NPM might install testrpc-sc --- exec.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/exec.js b/exec.js index e3d9169..17d8c69 100644 --- a/exec.js +++ b/exec.js @@ -67,10 +67,19 @@ if (config.silent) { // (Changes here should be also be added to the before() block of test/run.js). if (!config.norpc) { try { - log(`Launching testrpc on port ${port}`); - const command = './node_modules/ethereumjs-testrpc-sc/bin/testrpc'; + // NPM installs the testrpc-sc fork at different locations in node_modules based on + // whether testrpc is a pre-existing dependency of the project solcover is being added to + // using (--save-dev). + let command; + if (shell.test('-e', './node_modules/ethereumjs-testrpc-sc')) { + command = './node_modules/ethereumjs-testrpc-sc/bin/testrpc '; + } else { + command = './node_modules/solcover/node_modules/ethereumjs-testrpc-sc/bin/testrpc '; + } + const options = `--gasLimit ${gasLimitString} --port ${port}`; testrpcProcess = childprocess.exec(command + options); + log(`Launching testrpc on port ${port}`); } catch (err) { const msg = `There was a problem launching testrpc: ${err}`; cleanUp(msg); From 52f0a4b88951012f0bc2f379dc740826eadfed54 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Wed, 19 Apr 2017 16:19:55 -0700 Subject: [PATCH 33/51] Add assembly AST node names, edit README for npm install --- README.md | 70 ++++++++++++++----------------------------------------- parse.js | 7 +++++- 2 files changed, 24 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 0153cda..a00506c 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,41 @@ -#SolCover +# SolCover ![CircleCI Status](https://circleci.com/gh/JoinColony/solcover.svg?style=shield&circle-token=53d5360d290ef593c7bdce505b86ae8b9414e684) [![codecov](https://codecov.io/gh/JoinColony/solcover/branch/master/graph/badge.svg)](https://codecov.io/gh/JoinColony/solcover) - -###Code coverage for Solidity testing +### Code coverage for Solidity testing ![coverage example](https://cdn-images-1.medium.com/max/800/1*uum8t-31bUaa6dTRVVhj6w.png) For more details about what this is, how it work and potential limitations, see [the accompanying article](https://blog.colony.io/code-coverage-for-solidity-eecfa88668c2). -###Installation and preparation - -From your truffle directory, clone this repo: -``` -git clone http://github.com/JoinColony/solcover.git -cd solcover -npm install -``` - -Until [Truffle allows the `--network` flag for the `test` command](https://github.com/ConsenSys/truffle/issues/239), in `truffle.js` you have to set a large gas amount for deployment. While this is set, uninstrumented tests likely won't run correctly, so this should only be set when running the coverage tests. An appropriately modified `truffle.js` might look like +This branch is an attempt to prepare solcover for npm publication and simplify its use as a +command line utility. Gas cost issues etc are managed under the hood - you don't have to modify +`truffle.js` - and the tool cleans up after itself if (when) it crashes. +### Install ``` -module.exports = { - rpc: { - host: 'localhost', - gasPrice: 20e9, - gas: 0xfffffff, - } - }; +$ npm install --save-dev https://github.com/JoinColony/solcover.git#truffle3 ``` -In the future, hopefully just adding the 'coverage' network to `truffle.js` will be enough. This will look like +### Run ``` -module.exports = { - rpc: { - host: 'localhost', - gasPrice: 20e9, - }, - networks:{ - "coverage":{ - gas: 0xfffffff, - } - } -} +$ ./node_modules/solcover/exec.js ``` -and will not interfere with normal `truffle test` - or other commands - being run during development. - -Note that if you have hardcoded gas costs into your tests, some of them may fail when using SolCover. This is because the instrumentation process increases the gas costs for using the contracts, due to the extra events. If this is the case, then the coverage may be incomplete. To avoid this, using `estimateGas` to estimate your gas costs should be more resilient in most cases. - -###Execution - -Firstly, make sure that your contracts in your truffle directory are saved elsewhere too - this script moves them and modifies them to do the instrumentation and allow `truffle` to run the tests with the instrumented contracts. It returns them after the tests are complete, but if something goes wrong, then `originalContracts` in the truffle directory should contain the unmodified contracts. - -SolCover runs its own (modified) `testrpc` to get the coverage data, so make sure that you've not left a previous instance running on port 8545, otherwise the coverage reported will be.... sparse... - -From inside the SolCover directory, run - -```node ./runCoveredTests.js``` - -Upon completion of the tests, open the `./coverage/lcov-report/index.html` file to browse the HTML coverage report. -###A few, uh, provisos, a, a couple of quid pro quos... -It is very likely that there are valid Solidity statements that this tool won't instrument correctly, as it's only been developed against a small number of contracts. If (and when) you find such cases, please raise an issue. +Tests run signficantly slower while coverage is being generated. A 1 to 2 minute delay +between the end of Truffle compilation and the beginning of test execution is not impossible if your +test suite is large. +Note that if you have hardcoded gas costs into your tests some of them may fail when using SolCover. +This is because the instrumentation process increases the gas costs for using the contracts, due to +the extra events. If this is the case, then the coverage may be incomplete. To avoid this, using +`estimateGas` to estimate your gas costs should be more resilient in most cases. -###TODO +### TODO -- [ ] **TESTS** - [ ] Turn into a true command line tool, rather than just a hacked-together script +- [ ] Allow the use of a dedicated coverage network in `truffle.js` - [ ] Release on NPM -- [ ] Do not modify the `../contract/` directory at all during operation (might need changes to truffle) - [ ] Support for arbitrary testing commands - [ ] [You tell me](http://github.com/JoinColony/solcover/issues) diff --git a/parse.js b/parse.js index 7e92582..d42b2f1 100644 --- a/parse.js +++ b/parse.js @@ -5,7 +5,10 @@ const instrumenter = require('./instrumenter'); // functions where appropriate, which determine where to inject events. // Assign a dummy function to everything we might see in the AST -['AssignmentExpression', +['AssemblyAssignment', + 'AssemblyItem', + 'AssemblyLocalBinding', + 'AssignmentExpression', 'BinaryExpression', 'BlockStatement', 'BreakStatement', @@ -24,12 +27,14 @@ const instrumenter = require('./instrumenter'); 'ExpressionStatement', 'ForInStatement', 'ForStatement', + 'FunctionalAssemblyExpression', 'FunctionDeclaration', 'FunctionName', 'Identifier', 'IfStatement', 'ImportStatement', 'InformalParameter', + 'InlineAssemblyBlock', 'IsStatement', 'LibraryStatement', 'Literal', From 7d652aa7e8acc1e7ac279d6ea1680a8b458d92e8 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Wed, 19 Apr 2017 16:37:11 -0700 Subject: [PATCH 34/51] Generate a config for truffle.js networks -> development, instead of modifying pre-existing --- exec.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/exec.js b/exec.js index 17d8c69..d2b1b9d 100644 --- a/exec.js +++ b/exec.js @@ -104,9 +104,13 @@ try { // Coverage network opts NOT specified: default to the development network w/ modified // port, gasLimit, gasPrice. Export the config object only. } else { - truffleConfig.networks.development.port = port; - truffleConfig.networks.development.gas = gasLimitHex; - truffleConfig.networks.development.gasPrice = gasPriceHex; + truffleConfig.networks.development = { + host: "localhost", + network_id: "*", + port: port, + gas: gasLimitHex, + gasPrice: gasPriceHex + } coverageOption = ''; fs.writeFileSync(`${coverageDir}/truffle.js`, `module.exports = ${JSON.stringify(truffleConfig)}`); } From 9af5c30070b1421b8046de118ba47b230869a054 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Wed, 19 Apr 2017 16:55:22 -0700 Subject: [PATCH 35/51] Revert previous config fix, update README to require a development network config --- README.md | 27 +++++++++++++++++++++++++-- exec.js | 10 +++------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a00506c..f66c2ad 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,31 @@ For more details about what this is, how it work and potential limitations, see [the accompanying article](https://blog.colony.io/code-coverage-for-solidity-eecfa88668c2). This branch is an attempt to prepare solcover for npm publication and simplify its use as a -command line utility. Gas cost issues etc are managed under the hood - you don't have to modify -`truffle.js` - and the tool cleans up after itself if (when) it crashes. +command line utility. Gas cost issues etc are managed under the hood if your tests run using the +the development network and the tool cleans up after itself if (when) it crashes. + +If your `truffle.js` doesn't include a development network, you should add one with bare-bones +options. + +``` +// Example truffle.js +module.exports = { + networks: { + development: { + host: "localhost", + network_id: "*" // Match any network id + }, + 'dev.auction': { + host: "localhost", + port: 8545, + network_id: "*" // Match any network id + }, + test: { + provider: TestRPC.provider(), // in-memory TestRPC provider + network_id: "*" // Match any network id + } + } +``` ### Install ``` diff --git a/exec.js b/exec.js index d2b1b9d..17d8c69 100644 --- a/exec.js +++ b/exec.js @@ -104,13 +104,9 @@ try { // Coverage network opts NOT specified: default to the development network w/ modified // port, gasLimit, gasPrice. Export the config object only. } else { - truffleConfig.networks.development = { - host: "localhost", - network_id: "*", - port: port, - gas: gasLimitHex, - gasPrice: gasPriceHex - } + truffleConfig.networks.development.port = port; + truffleConfig.networks.development.gas = gasLimitHex; + truffleConfig.networks.development.gasPrice = gasPriceHex; coverageOption = ''; fs.writeFileSync(`${coverageDir}/truffle.js`, `module.exports = ${JSON.stringify(truffleConfig)}`); } From 3e53edc87f17ff278e30dcf644c98297b3e6fa87 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Wed, 19 Apr 2017 17:19:40 -0700 Subject: [PATCH 36/51] Add missing assembly AST node type, ignore client truffle.js, revert previous README changes --- README.md | 27 ++------------------------- exec.js | 18 ++++++++++++++---- parse.js | 1 + 3 files changed, 17 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index f66c2ad..e8ca544 100644 --- a/README.md +++ b/README.md @@ -10,31 +10,8 @@ For more details about what this is, how it work and potential limitations, see [the accompanying article](https://blog.colony.io/code-coverage-for-solidity-eecfa88668c2). This branch is an attempt to prepare solcover for npm publication and simplify its use as a -command line utility. Gas cost issues etc are managed under the hood if your tests run using the -the development network and the tool cleans up after itself if (when) it crashes. - -If your `truffle.js` doesn't include a development network, you should add one with bare-bones -options. - -``` -// Example truffle.js -module.exports = { - networks: { - development: { - host: "localhost", - network_id: "*" // Match any network id - }, - 'dev.auction': { - host: "localhost", - port: 8545, - network_id: "*" // Match any network id - }, - test: { - provider: TestRPC.provider(), // in-memory TestRPC provider - network_id: "*" // Match any network id - } - } -``` +command line utility. Gas cost issues etc are managed under the hood if your tests are able to run +using the default development network and the tool cleans up after itself if (when) it crashes. ### Install ``` diff --git a/exec.js b/exec.js index 17d8c69..e4e9da7 100644 --- a/exec.js +++ b/exec.js @@ -104,11 +104,21 @@ try { // Coverage network opts NOT specified: default to the development network w/ modified // port, gasLimit, gasPrice. Export the config object only. } else { - truffleConfig.networks.development.port = port; - truffleConfig.networks.development.gas = gasLimitHex; - truffleConfig.networks.development.gasPrice = gasPriceHex; + const trufflejs = ` + module.exports = { + networks: { + development: { + host: "localhost", + network_id: "*", + port: ${port}, + gas: ${gasLimitHex}, + gasPrice: ${gasPriceHex} + } + } + };`; + coverageOption = ''; - fs.writeFileSync(`${coverageDir}/truffle.js`, `module.exports = ${JSON.stringify(truffleConfig)}`); + fs.writeFileSync(`${coverageDir}/truffle.js`, trufflejs); } } catch (err) { const msg = ('There was a problem generating the coverage environment: '); diff --git a/parse.js b/parse.js index d42b2f1..d376dcf 100644 --- a/parse.js +++ b/parse.js @@ -35,6 +35,7 @@ const instrumenter = require('./instrumenter'); 'ImportStatement', 'InformalParameter', 'InlineAssemblyBlock', + 'InlineAssemblyStatement', 'IsStatement', 'LibraryStatement', 'Literal', From 0b57ebc8df306710889b39056b86665e9ac5267e Mon Sep 17 00:00:00 2001 From: cgewecke Date: Fri, 21 Apr 2017 09:03:54 -0700 Subject: [PATCH 37/51] Accomodate LibraryStatements in instrumentSolidity.js --- instrumentSolidity.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/instrumentSolidity.js b/instrumentSolidity.js index f8cdd6f..05de292 100644 --- a/instrumentSolidity.js +++ b/instrumentSolidity.js @@ -41,7 +41,8 @@ module.exports = function instrumentSolidity(contractSource, fileName) { ast = SolidityParser.parse(contract.preprocessed); - const contractStatement = ast.body.filter(node => node.type === 'ContractStatement'); + const contractStatement = ast.body.filter(node => (node.type === 'ContractStatement' || + node.type === 'LibraryStatement')); contract.contractName = contractStatement[0].name; parse[ast.type](contract, ast); From 695b2fef8bd10912d24c72c58bbf08de01f37b0f Mon Sep 17 00:00:00 2001 From: cgewecke Date: Fri, 21 Apr 2017 09:30:24 -0700 Subject: [PATCH 38/51] Get truffle exec to recognize networks option flag --- exec.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/exec.js b/exec.js index e4e9da7..b3a57af 100644 --- a/exec.js +++ b/exec.js @@ -150,11 +150,14 @@ try { } // Run solcover's fork of truffle over instrumented contracts in the -// coverage environment folder +// coverage environment folder. Shell cd command needs to be invoked +// as its own statement for command line options to work, apparently. try { log('Launching Truffle (this can take a few seconds)...'); - const command = `cd coverageEnv && truffle test ${coverageOption} ${silence}`; + const command = `truffle test ${coverageOption} ${silence}`; + shell.cd('./coverageEnv'); shell.exec(command); + shell.cd('./..'); } catch (err) { cleanUp(err); } From 818689dc57efd68b81e396b979ba08c1179f1fa4 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Fri, 21 Apr 2017 11:11:15 -0700 Subject: [PATCH 39/51] Update readme for networks option --- README.md | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e8ca544..ed6522a 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,37 @@ For more details about what this is, how it work and potential limitations, see [the accompanying article](https://blog.colony.io/code-coverage-for-solidity-eecfa88668c2). This branch is an attempt to prepare solcover for npm publication and simplify its use as a -command line utility. Gas cost issues etc are managed under the hood if your tests are able to run -using the default development network and the tool cleans up after itself if (when) it crashes. +command line utility. Gas cost issues etc are managed under the hood and the tool cleans up after +itself if (when) it crashes. +### Configuration + +By default, solcover generates a stub `truffle.js` that accomodates its special gas needs and +connects to a modified version of testrpc on port 8555. If your tests can run on the development network +using a standard `truffle.js`, you shouldn't have to do any configuration. If your tests +depend on logic added to `truffle.js` - for example: [zeppelin-solidity](https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/truffle.js) +uses the file to expose a babel polyfill that its suite needs to run correctly - you can override the default behavior +by specifying a coverage network in `truffle.js`. + +Example coverage network config +```javascript +module.exports = { + networks: { + development: { + host: "localhost", + port: 8545, + network_id: "*" // Match any network id + }, + coverage: { + host: "localhost", + network_id: "*", + port: 8555, // <-- Use this port only + gas: 0xfffffffffff, // <-- Use this high gas value + gasPrice: 0x01 // <-- Use this low gas price + } + } +}; +``` ### Install ``` $ npm install --save-dev https://github.com/JoinColony/solcover.git#truffle3 From eacbe634b82f73a96b44b2b7f19aff77e7a9c4e8 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Fri, 21 Apr 2017 12:42:47 -0700 Subject: [PATCH 40/51] Run testrpc with 35 accounts (uport-proxy) / allow configuration --- exec.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/exec.js b/exec.js index b3a57af..796b454 100644 --- a/exec.js +++ b/exec.js @@ -25,6 +25,7 @@ const coverageDir = './coverageEnv'; // Env that instrumented .sols are t // Options let workingDir = '.'; // Default location of contracts folder let port = 8555; // Default port - NOT 8545 & configurable via --port +let accounts = 35; // Default number of accounts to run testrpc w let coverageOption = '--network coverage'; // Default truffle network execution flag let silence = ''; // Default log level: configurable by --silence let log = console.log; // Default log level: configurable by --silence @@ -57,6 +58,7 @@ const config = reqCwd.silent(`${workingDir}/.solcover.js`) || {}; if (config.dir) workingDir = config.dir; if (config.port) port = config.port; +if (config.accounts) accounts = config.accounts; if (config.silent) { silence = '> /dev/null 2>&1'; // Silence for solcover's unit tests / CI @@ -77,7 +79,7 @@ if (!config.norpc) { command = './node_modules/solcover/node_modules/ethereumjs-testrpc-sc/bin/testrpc '; } - const options = `--gasLimit ${gasLimitString} --port ${port}`; + const options = `--gasLimit ${gasLimitString} --accounts ${accounts} --port ${port}`; testrpcProcess = childprocess.exec(command + options); log(`Launching testrpc on port ${port}`); } catch (err) { From 440bd8b602a71b5b900cd4e9e009e0a23006dbbf Mon Sep 17 00:00:00 2001 From: cgewecke Date: Sun, 23 Apr 2017 15:31:10 -0700 Subject: [PATCH 41/51] Fix chained constructor/method call bug. Add compilation unit test for case --- parse.js | 13 ++++++++----- test/function.js | 7 +++++++ test/sources/function/chainable.sol | 14 ++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 test/sources/function/chainable.sol diff --git a/parse.js b/parse.js index d376dcf..ac2231d 100644 --- a/parse.js +++ b/parse.js @@ -112,11 +112,14 @@ parse.MemberExpression = function parseMemberExpression(contract, expression) { }; parse.CallExpression = function parseCallExpression(contract, expression) { - instrumenter.instrumentStatement(contract, expression); - parse[expression.callee.type](contract, expression.callee); - // for (x in expression.arguments){ - // parse[expression.arguments[x].type](contract, expression.arguments[x]) - // } + // Peek ahead to see if this is non-newed contract constructor chained to a method call + // AST represents this as nested nested call expressions and we only want to instrument it once. + if (expression.callee.object && expression.callee.object.type === 'CallExpression') { + parse[expression.callee.type](contract, expression.callee); + } else { + instrumenter.instrumentStatement(contract, expression); + parse[expression.callee.type](contract, expression.callee); + } }; parse.UnaryExpression = function parseUnaryExpression(contract, expression) { diff --git a/test/function.js b/test/function.js index 8d7342e..bcc3ccd 100644 --- a/test/function.js +++ b/test/function.js @@ -37,4 +37,11 @@ describe('function declarations', () => { const output = solc.compile(info.contract, 1); util.report(output.errors); }); + + it('should compile after instrumenting a constructor call that chains to a method call', () => { + const contract = util.getCode('function/chainable.sol'); + const info = getInstrumentedVersion(contract, 'test.sol'); + const output = solc.compile(info.contract, 1); + util.report(output.errors); + }); }); diff --git a/test/sources/function/chainable.sol b/test/sources/function/chainable.sol new file mode 100644 index 0000000..412ad37 --- /dev/null +++ b/test/sources/function/chainable.sol @@ -0,0 +1,14 @@ +pragma solidity ^0.4.3; + +// This is for a test that verifies solcover can instrument a +// chained constructor/method call. +contract Chainable { + function chainWith(uint y, uint z){} +} + +contract Test { + function Test(){ + Chainable(0x00).chainWith(3, 4); + new Chainable().chainWith(3, 4); + } +} From 769c7f864744f459e3636d38919893995f6820e2 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Sun, 23 Apr 2017 15:41:57 -0700 Subject: [PATCH 42/51] Add lint script to package.json --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 229a4cf..2e4848e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ }, "scripts": { "test": "mocha --timeout 60000", - "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --timeout 60000" + "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --timeout 60000", + "lint": "./node_modules/.bin/eslint --fix ./" }, "author": "", "license": "ISC", From 21d3c04ead15dd17d81aff2662ecc642d42a5ae6 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Sun, 23 Apr 2017 18:12:59 -0700 Subject: [PATCH 43/51] Fix empty contract body parsing bug. Add coverage unit test for case --- parse.js | 8 +++++--- test/run.js | 2 -- test/sources/statements/empty-contract-body.sol | 4 ++++ test/statements.js | 16 ++++++++++++++++ test/util/vm.js | 3 ++- 5 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 test/sources/statements/empty-contract-body.sol diff --git a/parse.js b/parse.js index ac2231d..9c16d15 100644 --- a/parse.js +++ b/parse.js @@ -197,9 +197,11 @@ parse.ContractOrLibraryStatement = function parseContractOrLibraryStatement(cont }]; } - expression.body.forEach(construct => { - parse[construct.type](contract, construct); - }); + if (expression.body) { + expression.body.forEach(construct => { + parse[construct.type](contract, construct); + }); + } }; parse.ContractStatement = function ParseContractStatement(contract, expression) { diff --git a/test/run.js b/test/run.js index ef1a93d..8959b66 100644 --- a/test/run.js +++ b/test/run.js @@ -50,8 +50,6 @@ describe('run', () => { collectGarbage(); }); - // This test should be positioned first (or second if flushing) in the suite because of - // the way we're launching testrpc it('simple contract: should generate coverage, cleanup & exit(0)', () => { // Directory should be clean assert(pathExists('./coverage') === false, 'should start without: coverage'); diff --git a/test/sources/statements/empty-contract-body.sol b/test/sources/statements/empty-contract-body.sol new file mode 100644 index 0000000..2c26fa8 --- /dev/null +++ b/test/sources/statements/empty-contract-body.sol @@ -0,0 +1,4 @@ +pragma solidity ^0.4.3; + +contract Test { +} \ No newline at end of file diff --git a/test/statements.js b/test/statements.js index f8138f0..eb70b4c 100644 --- a/test/statements.js +++ b/test/statements.js @@ -110,4 +110,20 @@ describe('generic statements', () => { done(); }).catch(done); }); + + it('should cover an empty bodied contract statement', done => { + const contract = util.getCode('statements/empty-contract-body.sol'); + const info = getInstrumentedVersion(contract, filePath); + const coverage = new CoverageMap(); + coverage.addContract(info, filePath); + + vm.execute(info.contract, null, []).then(events => { + const mapping = coverage.generate(events, pathPrefix); + assert.deepEqual(mapping[filePath].l, {}); + assert.deepEqual(mapping[filePath].b, {}); + assert.deepEqual(mapping[filePath].s, {}); + assert.deepEqual(mapping[filePath].f, {}); + done(); + }).catch(done); + }); }); diff --git a/test/util/vm.js b/test/util/vm.js index 61d9057..e90179d 100644 --- a/test/util/vm.js +++ b/test/util/vm.js @@ -36,7 +36,8 @@ function getTypesFromAbi(abi, functionName) { return json.type; } const funcJson = abi.filter(matchesFunctionName)[0]; - return (funcJson.inputs).map(getTypes); + + return funcJson ? (funcJson.inputs).map(getTypes) : []; } /** From e65a03580215f842686a838e678273bab00475c3 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Sun, 23 Apr 2017 20:15:24 -0700 Subject: [PATCH 44/51] Update README with known issues, links to zeppelin example report --- README.md | 59 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index ed6522a..75959ac 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,32 @@ For more details about what this is, how it work and potential limitations, see [the accompanying article](https://blog.colony.io/code-coverage-for-solidity-eecfa88668c2). This branch is an attempt to prepare solcover for npm publication and simplify its use as a -command line utility. Gas cost issues etc are managed under the hood and the tool cleans up after +command line utility. Gas cost issues are managed under the hood and the tool cleans up after itself if (when) it crashes. +### Install +``` +$ npm install --save-dev https://github.com/JoinColony/solcover.git#truffle3 +``` + +### Run +``` +$ ./node_modules/solcover/exec.js +``` + +Tests run signficantly slower while coverage is being generated. A 1 to 2 minute delay +between the end of Truffle compilation and the beginning of test execution is not impossible if your +test suite is large. Large solidity files can also take a while to instrument. + ### Configuration By default, solcover generates a stub `truffle.js` that accomodates its special gas needs and connects to a modified version of testrpc on port 8555. If your tests can run on the development network -using a standard `truffle.js`, you shouldn't have to do any configuration. If your tests -depend on logic added to `truffle.js` - for example: [zeppelin-solidity](https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/truffle.js) -uses the file to expose a babel polyfill that its suite needs to run correctly - you can override the default behavior -by specifying a coverage network in `truffle.js`. +using a standard `truffle.js` and a testrpc instance with no special options, you shouldn't have to +do any configuration. If your tests depend on logic added to `truffle.js` - for example: +[zeppelin-solidity](https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/truffle.js) +uses the file to expose a babel polyfill that its suite needs to run correctly - you can override the +default behavior by specifying a coverage network in `truffle.js`. Example coverage network config ```javascript @@ -34,36 +49,40 @@ module.exports = { coverage: { host: "localhost", network_id: "*", - port: 8555, // <-- Use this port only + port: 8555, // <-- Use port 8555 gas: 0xfffffffffff, // <-- Use this high gas value gasPrice: 0x01 // <-- Use this low gas price } } }; -``` -### Install -``` -$ npm install --save-dev https://github.com/JoinColony/solcover.git#truffle3 ``` -### Run -``` -$ ./node_modules/solcover/exec.js -``` +### Known Issues -Tests run signficantly slower while coverage is being generated. A 1 to 2 minute delay -between the end of Truffle compilation and the beginning of test execution is not impossible if your -test suite is large. - -Note that if you have hardcoded gas costs into your tests some of them may fail when using SolCover. +**Hardcoded gas costs**: If you have hardcoded gas costs into your tests some of them may fail when using SolCover. This is because the instrumentation process increases the gas costs for using the contracts, due to the extra events. If this is the case, then the coverage may be incomplete. To avoid this, using `estimateGas` to estimate your gas costs should be more resilient in most cases. +**Events testing**: Solcover injects events into your solidity files to log which lines your tests reach, +so any tests that depend on how many events are fired or where the event sits in the logs array +will probably error while coverage is being generated. + +**Using `require` in `migrations.js` files**: Truffle overloads Node's `require` function but +implements a simplified search algorithm for node_modules packages +([see issue #383 at Truffle](https://github.com/trufflesuite/truffle/issues/383)). +Because Solcover copies an instrumented version of your project into a temporary folder, `require` +statements handled by Truffle internally don't resolve correctly. + +### Examples + ++ **zeppelin-solidity at commit 453a19825013a586751b87c67bebd551a252fb50** + + [HTML reports]( https://sc-forks.github.io/zeppelin-solidity/) + + [Zeppelin with Solcover installed](https://github.com/sc-forks/zeppelin-solidity) (declares own coverage network in truffle.js) + ### TODO - [ ] Turn into a true command line tool, rather than just a hacked-together script -- [ ] Allow the use of a dedicated coverage network in `truffle.js` - [ ] Release on NPM - [ ] Support for arbitrary testing commands - [ ] [You tell me](http://github.com/JoinColony/solcover/issues) From 381285c92069a2b9ac562e0b9434d9a6dbd03d9a Mon Sep 17 00:00:00 2001 From: cgewecke Date: Mon, 24 Apr 2017 09:31:59 -0700 Subject: [PATCH 45/51] Allow testrpc options string in config, rename run to cli (test sequencing fix) --- exec.js | 38 ++++++++++++++++++++----------------- test/{run.js => cli.js} | 23 +++++++++++++++++++++- test/run/testrpc-options.js | 25 ++++++++++++++++++++++++ test/util/mockTruffle.js | 2 +- 4 files changed, 69 insertions(+), 19 deletions(-) rename test/{run.js => cli.js} (89%) create mode 100644 test/run/testrpc-options.js diff --git a/exec.js b/exec.js index 796b454..22083f8 100644 --- a/exec.js +++ b/exec.js @@ -23,9 +23,6 @@ const coverage = new CoverageMap(); const coverageDir = './coverageEnv'; // Env that instrumented .sols are tested in // Options -let workingDir = '.'; // Default location of contracts folder -let port = 8555; // Default port - NOT 8545 & configurable via --port -let accounts = 35; // Default number of accounts to run testrpc w let coverageOption = '--network coverage'; // Default truffle network execution flag let silence = ''; // Default log level: configurable by --silence let log = console.log; // Default log level: configurable by --silence @@ -54,18 +51,23 @@ function cleanUp(err) { } } // --------------------------------------- Script -------------------------------------------------- -const config = reqCwd.silent(`${workingDir}/.solcover.js`) || {}; +const config = reqCwd.silent('./.solcover.js') || {}; -if (config.dir) workingDir = config.dir; -if (config.port) port = config.port; -if (config.accounts) accounts = config.accounts; +const workingDir = config.dir || '.'; // Relative path to contracts folder +const port = config.port || 8555; // Port testrpc listens on +const accounts = config.accounts || 35; // Number of accounts to testrpc launches with +// Set testrpc options +const defaultRpcOptions = `--gasLimit ${gasLimitString} --accounts ${accounts} --port ${port}`; +const testrpcOptions = config.testrpcOptions || defaultRpcOptions; + +// Silence shell and script logging (for solcover's unit tests / CI) if (config.silent) { - silence = '> /dev/null 2>&1'; // Silence for solcover's unit tests / CI + silence = '> /dev/null 2>&1'; log = () => {}; } -// Run the modified testrpc with large block limit, on (hopefully) unused port. +// Run modified testrpc with large block limit, on (hopefully) unused port. // (Changes here should be also be added to the before() block of test/run.js). if (!config.norpc) { try { @@ -78,10 +80,8 @@ if (!config.norpc) { } else { command = './node_modules/solcover/node_modules/ethereumjs-testrpc-sc/bin/testrpc '; } - - const options = `--gasLimit ${gasLimitString} --accounts ${accounts} --port ${port}`; - testrpcProcess = childprocess.exec(command + options); - log(`Launching testrpc on port ${port}`); + testrpcProcess = childprocess.exec(command + testrpcOptions); + log(`Testrpc launched on port ${port}`); } catch (err) { const msg = `There was a problem launching testrpc: ${err}`; cleanUp(msg); @@ -133,12 +133,15 @@ try { // 3. Instrument contract // 4. Save instrumented contract in the coverage environment folder where covered tests will run // 5. Add instrumentation info to the coverage map +let currentFile; try { shell.ls(`${coverageDir}/contracts/**/*.sol`).forEach(file => { const migrations = `${coverageDir}/contracts/Migrations.sol`; if (file !== migrations) { log('Instrumenting ', file); + currentFile = file; + const contractPath = path.resolve(file); const canonicalPath = contractPath.split('/coverageEnv').join(''); const contract = fs.readFileSync(contractPath).toString(); @@ -148,15 +151,17 @@ try { } }); } catch (err) { - cleanUp(err); + const msg = (`There was a problem instrumenting ${currentFile}: `); + cleanUp(msg + err); } // Run solcover's fork of truffle 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. try { - log('Launching Truffle (this can take a few seconds)...'); - const command = `truffle test ${coverageOption} ${silence}`; + log('Launching test command (this can take a few seconds)...'); + const defaultCommand = `truffle test ${coverageOption} ${silence}`; + const command = config.testCommand || defaultCommand; shell.cd('./coverageEnv'); shell.exec(command); shell.cd('./..'); @@ -181,7 +186,6 @@ try { // Generate coverage / write coverage report / run istanbul try { - // coverage.generate(events, `${coverageDir}/contracts/`); coverage.generate(events, './contracts'); const json = JSON.stringify(coverage.coverage); diff --git a/test/run.js b/test/cli.js similarity index 89% rename from test/run.js rename to test/cli.js index 8959b66..b1e646e 100644 --- a/test/run.js +++ b/test/cli.js @@ -14,7 +14,7 @@ function collectGarbage() { if (global.gc) { global.gc(); } } -describe('run', () => { +describe('cli', () => { let testrpcProcess = null; const script = 'node ./exec.js'; const port = 8555; @@ -112,6 +112,27 @@ describe('run', () => { collectGarbage(); }); + it('config with testrpc options string: should generate coverage, cleanup & exit(0)', () => { + assert(pathExists('./coverage') === false, 'should start without: coverage'); + assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); + + const privateKey = '0x3af46c9ac38ee1f01b05f9915080133f644bf57443f504d339082cb5285ccae4'; + const balance = "0xfffffffffffffff"; + const testConfig = Object.assign({}, config); + + testConfig.testrpcOptions = `--account="${privateKey},${balance}" --port 8777`; + testConfig.norpc = false; + testConfig.port = 8777; + + // Installed test will process.exit(1) and crash truffle if the test isn't + // loaded with the account specified above + mock.install('Simple.sol', 'testrpc-options.js', testConfig); + shell.exec(script); + assert(shell.error() === null, 'script should not error'); + + collectGarbage(); + }); + it('truffle tests failing: should generate coverage, cleanup & exit(0)', () => { assert(pathExists('./coverage') === false, 'should start without: coverage'); assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); diff --git a/test/run/testrpc-options.js b/test/run/testrpc-options.js new file mode 100644 index 0000000..54048f4 --- /dev/null +++ b/test/run/testrpc-options.js @@ -0,0 +1,25 @@ +/* eslint-env node, mocha */ +/* global artifacts, contract, assert */ + +const Simple = artifacts.require('./Simple.sol'); + +contract('Simple', (accounts) => { + + // Crash truffle if the account loaded in the options string isn't found here. + it('should load with expected account', () => { + if (accounts[0] !== '0xa4860cedd5143bd63f347cab453bf91425f8404f'){ + process.exit(1); + }; + }) + + // Generate some coverage so the script doesn't exit(1) because there are no events + it('should set x to 5', () => { + let simple; + return Simple.deployed().then(instance => { + simple = instance; + return simple.test(5); + }) + .then(() => simple.getX.call()) + .then(val => assert.equal(val.toNumber(), 5)); + }); +}); \ No newline at end of file diff --git a/test/util/mockTruffle.js b/test/util/mockTruffle.js index fce6fdd..8ec88ea 100644 --- a/test/util/mockTruffle.js +++ b/test/util/mockTruffle.js @@ -12,7 +12,7 @@ const shell = require('shelljs'); * @param {String} contract located in /test/sources/run/ * @param {[type]} test located in /test/run/ */ -module.exports.install = function install(contract, test, config) { +module.exports.install = function install(contract, test, config ) { shell.mkdir('./mock'); shell.mkdir('./mock/contracts'); shell.mkdir('./mock/migrations'); From 7617134adf16821a23e91f6ba71d07017a8c964e Mon Sep 17 00:00:00 2001 From: cgewecke Date: Mon, 24 Apr 2017 12:09:44 -0700 Subject: [PATCH 46/51] Add unit test for arbitrary testing command config option, remove test flush --- test/cli.js | 62 ++++++++++++++++++++----------------- test/run/command-options.js | 22 +++++++++++++ test/run/testrpc-options.js | 15 +++++---- test/util/mockTruffle.js | 2 +- 4 files changed, 64 insertions(+), 37 deletions(-) create mode 100644 test/run/command-options.js diff --git a/test/cli.js b/test/cli.js index b1e646e..3fbc3e9 100644 --- a/test/cli.js +++ b/test/cli.js @@ -40,13 +40,40 @@ describe('cli', () => { testrpcProcess.kill(); }); - // This pre-test flushes the suite. There's some kind of sequencing issue here in development, - // possibly tied to the use of ethereumjs-vm in the coverage tests? - // - tests pass w/out this if we only run these test - e.g. it only fails when running the suite. - // - the first test always fails unless there is a fresh testrpc install. - it('flush test suite', () => { - mock.install('Simple.sol', 'simple.js', config); - shell.exec(script); // <---- This fails mysteriously, but we don't test here. + // The 'config' tests ask exec.js to run testrpc on special ports, the subsequent tests use + // the testrpc launched in the before() block. For some reason config tests fail randomly + // unless they are at the top of the suite. Hard to debug since they pass if logging is turned + // on - there might be a timing issue around resource cleanup or something. + it('config with testrpc options string: should generate coverage, cleanup & exit(0)', () => { + const privateKey = '0x3af46c9ac38ee1f01b05f9915080133f644bf57443f504d339082cb5285ccae4'; + const balance = '0xfffffffffffffff'; + const testConfig = Object.assign({}, config); + + testConfig.testrpcOptions = `--account="${privateKey},${balance}" --port 8777`; + testConfig.norpc = false; + testConfig.port = 8777; + + // Installed test will process.exit(1) and crash truffle if the test isn't + // loaded with the account specified above + mock.install('Simple.sol', 'testrpc-options.js', testConfig); + shell.exec(script); + assert(shell.error() === null, 'script should not error'); + collectGarbage(); + }); + + it('config with test command options string: should run test', () => { + assert(pathExists('./allFiredEvents') === false, 'should start without: events log'); + const testConfig = Object.assign({}, config); + + testConfig.testCommand = 'mocha --timeout 5000 > /dev/null 2>&1'; + testConfig.norpc = false; + testConfig.port = 8888; + + // Installed test will write a fake allFiredEvents to ./ after 4000ms + // allowing test to pass + mock.install('Simple.sol', 'command-options.js', testConfig); + shell.exec(script); + assert(shell.error() === null, 'script should not error'); collectGarbage(); }); @@ -112,27 +139,6 @@ describe('cli', () => { collectGarbage(); }); - it('config with testrpc options string: should generate coverage, cleanup & exit(0)', () => { - assert(pathExists('./coverage') === false, 'should start without: coverage'); - assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); - - const privateKey = '0x3af46c9ac38ee1f01b05f9915080133f644bf57443f504d339082cb5285ccae4'; - const balance = "0xfffffffffffffff"; - const testConfig = Object.assign({}, config); - - testConfig.testrpcOptions = `--account="${privateKey},${balance}" --port 8777`; - testConfig.norpc = false; - testConfig.port = 8777; - - // Installed test will process.exit(1) and crash truffle if the test isn't - // loaded with the account specified above - mock.install('Simple.sol', 'testrpc-options.js', testConfig); - shell.exec(script); - assert(shell.error() === null, 'script should not error'); - - collectGarbage(); - }); - it('truffle tests failing: should generate coverage, cleanup & exit(0)', () => { assert(pathExists('./coverage') === false, 'should start without: coverage'); assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); diff --git a/test/run/command-options.js b/test/run/command-options.js new file mode 100644 index 0000000..d1a5763 --- /dev/null +++ b/test/run/command-options.js @@ -0,0 +1,22 @@ +/* eslint-env node, mocha */ + +const assert = require('assert'); +const fs = require('fs'); + +// Fake event for Simple.sol +const fakeEvent = { + 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', fakeEvent); + done(); + }, 4000); + }); +}); \ No newline at end of file diff --git a/test/run/testrpc-options.js b/test/run/testrpc-options.js index 54048f4..704096a 100644 --- a/test/run/testrpc-options.js +++ b/test/run/testrpc-options.js @@ -3,15 +3,14 @@ const Simple = artifacts.require('./Simple.sol'); -contract('Simple', (accounts) => { - +contract('Simple', accounts => { // Crash truffle if the account loaded in the options string isn't found here. - it('should load with expected account', () => { - if (accounts[0] !== '0xa4860cedd5143bd63f347cab453bf91425f8404f'){ - process.exit(1); - }; - }) - + it('should load with expected account', () => { + if (accounts[0] !== '0xa4860cedd5143bd63f347cab453bf91425f8404f') { + process.exit(1); + } + }); + // Generate some coverage so the script doesn't exit(1) because there are no events it('should set x to 5', () => { let simple; diff --git a/test/util/mockTruffle.js b/test/util/mockTruffle.js index 8ec88ea..fce6fdd 100644 --- a/test/util/mockTruffle.js +++ b/test/util/mockTruffle.js @@ -12,7 +12,7 @@ const shell = require('shelljs'); * @param {String} contract located in /test/sources/run/ * @param {[type]} test located in /test/run/ */ -module.exports.install = function install(contract, test, config ) { +module.exports.install = function install(contract, test, config) { shell.mkdir('./mock'); shell.mkdir('./mock/contracts'); shell.mkdir('./mock/migrations'); From 4b627f6b9e9262c9dd981f9514be4e49630162c1 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Mon, 24 Apr 2017 14:50:48 -0700 Subject: [PATCH 47/51] Fix broken chained call handling, add unit tests to verify cases get coverage --- parse.js | 8 +- test/function.js | 92 +++++++++++++++++++++++ test/sources/function/chainable-new.sol | 12 +++ test/sources/function/chainable-value.sol | 12 +++ test/sources/function/chainable.sol | 11 +-- test/sources/function/function-call.sol | 9 +++ 6 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 test/sources/function/chainable-new.sol create mode 100644 test/sources/function/chainable-value.sol create mode 100644 test/sources/function/function-call.sol diff --git a/parse.js b/parse.js index 9c16d15..02262e0 100644 --- a/parse.js +++ b/parse.js @@ -112,12 +112,12 @@ parse.MemberExpression = function parseMemberExpression(contract, expression) { }; parse.CallExpression = function parseCallExpression(contract, expression) { - // Peek ahead to see if this is non-newed contract constructor chained to a method call - // AST represents this as nested nested call expressions and we only want to instrument it once. - if (expression.callee.object && expression.callee.object.type === 'CallExpression') { + // It looks like in any given chain of call expressions, only the head callee is an Identifier + // node ... and we only want to instrument the statement once. + if (expression.callee.type === 'Identifier') { + instrumenter.instrumentStatement(contract, expression); parse[expression.callee.type](contract, expression.callee); } else { - instrumenter.instrumentStatement(contract, expression); parse[expression.callee.type](contract, expression.callee); } }; diff --git a/test/function.js b/test/function.js index bcc3ccd..a2553bc 100644 --- a/test/function.js +++ b/test/function.js @@ -3,6 +3,10 @@ const solc = require('solc'); const getInstrumentedVersion = require('./../instrumentSolidity.js'); const util = require('./util/util.js'); +const path = require('path'); +const CoverageMap = require('./../coverageMap'); +const vm = require('./util/vm'); +const assert = require('assert'); /** * NB: passing '1' to solc as an option activates the optimiser @@ -10,6 +14,9 @@ const util = require('./util/util.js'); * and passing the error to mocha. */ describe('function declarations', () => { + const filePath = path.resolve('./test.sol'); + const pathPrefix = './'; + it('should compile after instrumenting an ordinary function declaration', () => { const contract = util.getCode('function/function.sol'); const info = getInstrumentedVersion(contract, 'test.sol'); @@ -38,10 +45,95 @@ describe('function declarations', () => { util.report(output.errors); }); + it('should compile after instrumenting a new->constructor-->method chain', () => { + const contract = util.getCode('function/chainable-new.sol'); + const info = getInstrumentedVersion(contract, 'test.sol'); + const output = solc.compile(info.contract, 1); + util.report(output.errors); + }); + it('should compile after instrumenting a constructor call that chains to a method call', () => { const contract = util.getCode('function/chainable.sol'); const info = getInstrumentedVersion(contract, 'test.sol'); const output = solc.compile(info.contract, 1); util.report(output.errors); }); + + it('should compile after instrumenting a constructor-->method-->value chain', () => { + const contract = util.getCode('function/chainable-value.sol'); + const info = getInstrumentedVersion(contract, 'test.sol'); + const output = solc.compile(info.contract, 1); + util.report(output.errors); + }); + + it('should cover a simple invoked function call', done => { + const contract = util.getCode('function/function-call.sol'); + const info = getInstrumentedVersion(contract, filePath); + const coverage = new CoverageMap(); + coverage.addContract(info, filePath); + + vm.execute(info.contract, 'a', []).then(events => { + const mapping = coverage.generate(events, pathPrefix); + assert.deepEqual(mapping[filePath].l, { + 7: 1, + }); + assert.deepEqual(mapping[filePath].b, {}); + assert.deepEqual(mapping[filePath].s, { + 1: 1, + }); + assert.deepEqual(mapping[filePath].f, { + 1: 1, + 2: 1, + }); + done(); + }).catch(done); + }); + + it('should cover a constructor call that chains to a method call', done => { + const contract = util.getCode('function/chainable.sol'); + const info = getInstrumentedVersion(contract, filePath); + const coverage = new CoverageMap(); + coverage.addContract(info, filePath); + // The vm runs out of gas here - but we can verify line / statement / fn + // coverage is getting mapped. + vm.execute(info.contract, 'a', []).then(events => { + const mapping = coverage.generate(events, pathPrefix); + assert.deepEqual(mapping[filePath].l, { + 9: 0, + }); + assert.deepEqual(mapping[filePath].b, {}); + assert.deepEqual(mapping[filePath].s, { + 1: 0, + }); + assert.deepEqual(mapping[filePath].f, { + 1: 0, + 2: 0, + }); + done(); + }).catch(done); + }); + + it('should cover a constructor call that chains to a method call', done => { + const contract = util.getCode('function/chainable-value.sol'); + const info = getInstrumentedVersion(contract, filePath); + const coverage = new CoverageMap(); + coverage.addContract(info, filePath); + // The vm runs out of gas here - but we can verify line / statement / fn + // coverage is getting mapped. + vm.execute(info.contract, 'a', []).then(events => { + const mapping = coverage.generate(events, pathPrefix); + assert.deepEqual(mapping[filePath].l, { + 10: 0, + }); + assert.deepEqual(mapping[filePath].b, {}); + assert.deepEqual(mapping[filePath].s, { + 1: 0, + }); + assert.deepEqual(mapping[filePath].f, { + 1: 0, + 2: 0, + }); + done(); + }).catch(done); + }); }); diff --git a/test/sources/function/chainable-new.sol b/test/sources/function/chainable-new.sol new file mode 100644 index 0000000..ae5b519 --- /dev/null +++ b/test/sources/function/chainable-new.sol @@ -0,0 +1,12 @@ +pragma solidity ^0.4.3; + +// This is for a test that verifies solcover can instrument a +// chained constructor/method call invoked by the new operator. +contract Chainable { + function chainWith(uint y, uint z) {} +} +contract Test { + function a(){ + new Chainable().chainWith(3, 4); + } +} \ No newline at end of file diff --git a/test/sources/function/chainable-value.sol b/test/sources/function/chainable-value.sol new file mode 100644 index 0000000..84a9312 --- /dev/null +++ b/test/sources/function/chainable-value.sol @@ -0,0 +1,12 @@ +pragma solidity ^0.4.3; + +// This is for a test that verifies solcover can instrument a +// another kind of long CallExpression chain +contract Test { + function paySomeone(address x, address y) payable { + } + + function a() payable { + Test(0x00).paySomeone.value(msg.value)(0x00, 0x00); + } +} \ No newline at end of file diff --git a/test/sources/function/chainable.sol b/test/sources/function/chainable.sol index 412ad37..bf4081b 100644 --- a/test/sources/function/chainable.sol +++ b/test/sources/function/chainable.sol @@ -2,13 +2,10 @@ pragma solidity ^0.4.3; // This is for a test that verifies solcover can instrument a // chained constructor/method call. -contract Chainable { - function chainWith(uint y, uint z){} -} - contract Test { - function Test(){ - Chainable(0x00).chainWith(3, 4); - new Chainable().chainWith(3, 4); + function chainWith(uint y, uint z) {} + + function a(){ + Test(0x00).chainWith(3, 4); } } diff --git a/test/sources/function/function-call.sol b/test/sources/function/function-call.sol new file mode 100644 index 0000000..07f463b --- /dev/null +++ b/test/sources/function/function-call.sol @@ -0,0 +1,9 @@ +pragma solidity ^0.4.3; + +// This test verifies that an invoked function gets logged as a statement +contract Test { + function loggedAsStatement(uint x) {} + function a(){ + loggedAsStatement(5); + } +} \ No newline at end of file From 8223aec0651e5061d397a33c5dd2a8d62777de7e Mon Sep 17 00:00:00 2001 From: cgewecke Date: Mon, 24 Apr 2017 14:57:13 -0700 Subject: [PATCH 48/51] Rename "run" folders/files "cli" for consistency --- .eslintignore | 2 +- test/{run => cli}/block-gas-limit.js | 0 test/{run => cli}/command-options.js | 0 test/{run => cli}/empty.js | 0 test/{run => cli}/inheritance.js | 0 test/{run => cli}/only-call.js | 0 test/{run => cli}/simple.js | 0 test/{run => cli}/sol-parse-fail.js | 0 test/{run => cli}/testrpc-options.js | 0 test/{run => cli}/truffle-crash.js | 0 test/{run => cli}/truffle-test-fail.js | 0 test/sources/{run => cli}/Empty.sol | 0 test/sources/{run => cli}/Expensive.sol | 0 test/sources/{run => cli}/Migrations.sol | 0 test/sources/{run => cli}/OnlyCall.sol | 0 test/sources/{run => cli}/Owned.sol | 0 test/sources/{run => cli}/Proxy.sol | 0 test/sources/{run => cli}/Simple.sol | 0 test/sources/{run => cli}/SimpleError.sol | 0 test/util/mockTruffle.js | 26 +++++++++++------------ 20 files changed, 14 insertions(+), 14 deletions(-) rename test/{run => cli}/block-gas-limit.js (100%) rename test/{run => cli}/command-options.js (100%) rename test/{run => cli}/empty.js (100%) rename test/{run => cli}/inheritance.js (100%) rename test/{run => cli}/only-call.js (100%) rename test/{run => cli}/simple.js (100%) rename test/{run => cli}/sol-parse-fail.js (100%) rename test/{run => cli}/testrpc-options.js (100%) rename test/{run => cli}/truffle-crash.js (100%) rename test/{run => cli}/truffle-test-fail.js (100%) rename test/sources/{run => cli}/Empty.sol (100%) rename test/sources/{run => cli}/Expensive.sol (100%) rename test/sources/{run => cli}/Migrations.sol (100%) rename test/sources/{run => cli}/OnlyCall.sol (100%) rename test/sources/{run => cli}/Owned.sol (100%) rename test/sources/{run => cli}/Proxy.sol (100%) rename test/sources/{run => cli}/Simple.sol (100%) rename test/sources/{run => cli}/SimpleError.sol (100%) diff --git a/.eslintignore b/.eslintignore index bda468c..d2b7a2a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1 @@ -test/run/truffle-crash.js \ No newline at end of file +test/cli/truffle-crash.js \ No newline at end of file diff --git a/test/run/block-gas-limit.js b/test/cli/block-gas-limit.js similarity index 100% rename from test/run/block-gas-limit.js rename to test/cli/block-gas-limit.js diff --git a/test/run/command-options.js b/test/cli/command-options.js similarity index 100% rename from test/run/command-options.js rename to test/cli/command-options.js diff --git a/test/run/empty.js b/test/cli/empty.js similarity index 100% rename from test/run/empty.js rename to test/cli/empty.js diff --git a/test/run/inheritance.js b/test/cli/inheritance.js similarity index 100% rename from test/run/inheritance.js rename to test/cli/inheritance.js diff --git a/test/run/only-call.js b/test/cli/only-call.js similarity index 100% rename from test/run/only-call.js rename to test/cli/only-call.js diff --git a/test/run/simple.js b/test/cli/simple.js similarity index 100% rename from test/run/simple.js rename to test/cli/simple.js diff --git a/test/run/sol-parse-fail.js b/test/cli/sol-parse-fail.js similarity index 100% rename from test/run/sol-parse-fail.js rename to test/cli/sol-parse-fail.js diff --git a/test/run/testrpc-options.js b/test/cli/testrpc-options.js similarity index 100% rename from test/run/testrpc-options.js rename to test/cli/testrpc-options.js diff --git a/test/run/truffle-crash.js b/test/cli/truffle-crash.js similarity index 100% rename from test/run/truffle-crash.js rename to test/cli/truffle-crash.js diff --git a/test/run/truffle-test-fail.js b/test/cli/truffle-test-fail.js similarity index 100% rename from test/run/truffle-test-fail.js rename to test/cli/truffle-test-fail.js diff --git a/test/sources/run/Empty.sol b/test/sources/cli/Empty.sol similarity index 100% rename from test/sources/run/Empty.sol rename to test/sources/cli/Empty.sol diff --git a/test/sources/run/Expensive.sol b/test/sources/cli/Expensive.sol similarity index 100% rename from test/sources/run/Expensive.sol rename to test/sources/cli/Expensive.sol diff --git a/test/sources/run/Migrations.sol b/test/sources/cli/Migrations.sol similarity index 100% rename from test/sources/run/Migrations.sol rename to test/sources/cli/Migrations.sol diff --git a/test/sources/run/OnlyCall.sol b/test/sources/cli/OnlyCall.sol similarity index 100% rename from test/sources/run/OnlyCall.sol rename to test/sources/cli/OnlyCall.sol diff --git a/test/sources/run/Owned.sol b/test/sources/cli/Owned.sol similarity index 100% rename from test/sources/run/Owned.sol rename to test/sources/cli/Owned.sol diff --git a/test/sources/run/Proxy.sol b/test/sources/cli/Proxy.sol similarity index 100% rename from test/sources/run/Proxy.sol rename to test/sources/cli/Proxy.sol diff --git a/test/sources/run/Simple.sol b/test/sources/cli/Simple.sol similarity index 100% rename from test/sources/run/Simple.sol rename to test/sources/cli/Simple.sol diff --git a/test/sources/run/SimpleError.sol b/test/sources/cli/SimpleError.sol similarity index 100% rename from test/sources/run/SimpleError.sol rename to test/sources/cli/SimpleError.sol diff --git a/test/util/mockTruffle.js b/test/util/mockTruffle.js index fce6fdd..ef95941 100644 --- a/test/util/mockTruffle.js +++ b/test/util/mockTruffle.js @@ -9,8 +9,8 @@ const shell = require('shelljs'); /** * Installs mock truffle project at ./mock with a single contract * and test specified by the params. - * @param {String} contract located in /test/sources/run/ - * @param {[type]} test located in /test/run/ + * @param {String} contract located in /test/sources/cli/ + * @param {[type]} test located in /test/cli/ */ module.exports.install = function install(contract, test, config) { shell.mkdir('./mock'); @@ -21,13 +21,13 @@ module.exports.install = function install(contract, test, config) { // Mock contracts if (Array.isArray(contract)) { contract.forEach(item => { - shell.cp(`./test/sources/run/${item}`, `./mock/contracts/${item}`); + shell.cp(`./test/sources/cli/${item}`, `./mock/contracts/${item}`); }); } else { - shell.cp(`./test/sources/run/${contract}`, `./mock/contracts/${contract}`); + shell.cp(`./test/sources/cli/${contract}`, `./mock/contracts/${contract}`); } - shell.cp('./test/sources/run/Migrations.sol', './mock/contracts/Migrations.sol'); + shell.cp('./test/sources/cli/Migrations.sol', './mock/contracts/Migrations.sol'); // Mock migrations const initialMigration = ` @@ -47,7 +47,7 @@ module.exports.install = function install(contract, test, config) { fs.writeFileSync('./mock/migrations/2_deploy_contracts.js', deployContracts); // Mock test - shell.cp(`./test/run/${test}`, `./mock/test/${test}`); + shell.cp(`./test/cli/${test}`, `./mock/test/${test}`); // Mock truffle.js const trufflejs = `module.exports = { @@ -68,8 +68,8 @@ module.exports.install = function install(contract, test, config) { /** * Installs mock truffle project at ./mock with a single contract * and test specified by the params. - * @param {String} contract located in /test/sources/run/ - * @param {[type]} test located in /test/run/ + * @param {String} contract located in /test/sources/cli/ + * @param {[type]} test located in /test/cli/ */ module.exports.installInheritanceTest = function installInheritanceTest(config) { shell.mkdir('./mock'); @@ -78,9 +78,9 @@ module.exports.installInheritanceTest = function installInheritanceTest(config) shell.mkdir('./mock/test'); // Mock contracts - shell.cp('./test/sources/run/Proxy.sol', './mock/contracts/Proxy.sol'); - shell.cp('./test/sources/run/Owned.sol', './mock/contracts/Owned.sol'); - shell.cp('./test/sources/run/Migrations.sol', './mock/contracts/Migrations.sol'); + shell.cp('./test/sources/cli/Proxy.sol', './mock/contracts/Proxy.sol'); + shell.cp('./test/sources/cli/Owned.sol', './mock/contracts/Owned.sol'); + shell.cp('./test/sources/cli/Migrations.sol', './mock/contracts/Migrations.sol'); // Mock migrations const initialMigration = ` @@ -102,7 +102,7 @@ module.exports.installInheritanceTest = function installInheritanceTest(config) fs.writeFileSync('./mock/migrations/2_deploy_contracts.js', deployContracts); // Mock test - shell.cp('./test/run/inheritance.js', './mock/test/inheritance.js'); + shell.cp('./test/cli/inheritance.js', './mock/test/inheritance.js'); // Mock truffle.js const trufflejs = `module.exports = { @@ -121,7 +121,7 @@ module.exports.installInheritanceTest = function installInheritanceTest(config) }; /** - * Removes mock truffle project and coverage reports generated by runCovered tests + * Removes mock truffle project and coverage reports generated by exec.js */ module.exports.remove = function remove() { shell.config.silent = true; From 2f933a640fa6bc84ab622998ad0cbc2ca70c1495 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Mon, 24 Apr 2017 15:29:35 -0700 Subject: [PATCH 49/51] Disable two "config" tests for CI - multiple testrpc launches max out container memory limit --- test/cli.js | 63 +++++++++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/test/cli.js b/test/cli.js index 3fbc3e9..4b22b94 100644 --- a/test/cli.js +++ b/test/cli.js @@ -40,41 +40,48 @@ describe('cli', () => { testrpcProcess.kill(); }); - // The 'config' tests ask exec.js to run testrpc on special ports, the subsequent tests use + // #1: The 'config' tests ask exec.js to run testrpc on special ports, the subsequent tests use // the testrpc launched in the before() block. For some reason config tests fail randomly // unless they are at the top of the suite. Hard to debug since they pass if logging is turned // on - there might be a timing issue around resource cleanup or something. + // + // #2: Creating repeated instances of testrpc hits the container memory limit on + // CI so these tests are disabled for that context it('config with testrpc options string: should generate coverage, cleanup & exit(0)', () => { - const privateKey = '0x3af46c9ac38ee1f01b05f9915080133f644bf57443f504d339082cb5285ccae4'; - const balance = '0xfffffffffffffff'; - const testConfig = Object.assign({}, config); - - testConfig.testrpcOptions = `--account="${privateKey},${balance}" --port 8777`; - testConfig.norpc = false; - testConfig.port = 8777; - - // Installed test will process.exit(1) and crash truffle if the test isn't - // loaded with the account specified above - mock.install('Simple.sol', 'testrpc-options.js', testConfig); - shell.exec(script); - assert(shell.error() === null, 'script should not error'); - collectGarbage(); + if (!process.env.CI) { + const privateKey = '0x3af46c9ac38ee1f01b05f9915080133f644bf57443f504d339082cb5285ccae4'; + const balance = '0xfffffffffffffff'; + const testConfig = Object.assign({}, config); + + testConfig.testrpcOptions = `--account="${privateKey},${balance}" --port 8777`; + testConfig.norpc = false; + testConfig.port = 8777; + + // Installed test will process.exit(1) and crash truffle if the test isn't + // loaded with the account specified above + mock.install('Simple.sol', 'testrpc-options.js', testConfig); + shell.exec(script); + assert(shell.error() === null, 'script should not error'); + collectGarbage(); + } }); it('config with test command options string: should run test', () => { - assert(pathExists('./allFiredEvents') === false, 'should start without: events log'); - const testConfig = Object.assign({}, config); - - testConfig.testCommand = 'mocha --timeout 5000 > /dev/null 2>&1'; - testConfig.norpc = false; - testConfig.port = 8888; - - // Installed test will write a fake allFiredEvents to ./ after 4000ms - // allowing test to pass - mock.install('Simple.sol', 'command-options.js', testConfig); - shell.exec(script); - assert(shell.error() === null, 'script should not error'); - collectGarbage(); + if (!process.env.CI) { + assert(pathExists('./allFiredEvents') === false, 'should start without: events log'); + const testConfig = Object.assign({}, config); + + testConfig.testCommand = 'mocha --timeout 5000 > /dev/null 2>&1'; + testConfig.norpc = false; + testConfig.port = 8888; + + // Installed test will write a fake allFiredEvents to ./ after 4000ms + // allowing test to pass + mock.install('Simple.sol', 'command-options.js', testConfig); + shell.exec(script); + assert(shell.error() === null, 'script should not error'); + collectGarbage(); + } }); it('simple contract: should generate coverage, cleanup & exit(0)', () => { From c47f1604c701b4365c7d52e9e530a6a6bc2b0e33 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Mon, 24 Apr 2017 18:38:16 -0700 Subject: [PATCH 50/51] README updates for .solcover.js config options, more example reports --- README.md | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 75959ac..ca4b686 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ do any configuration. If your tests depend on logic added to `truffle.js` - for uses the file to expose a babel polyfill that its suite needs to run correctly - you can override the default behavior by specifying a coverage network in `truffle.js`. -Example coverage network config +**Example coverage network config** ```javascript module.exports = { networks: { @@ -57,6 +57,26 @@ module.exports = { }; ``` +You can also create a `.solcover.js` config file in the root directory of your project and specify +some additional options: ++ **port**: The port you want Solcover to run testrpc on / have truffle connect to. ++ **testrpcOptions**: A string of options to be appended to a command line invocation +of testrpc. Ex: `--account="0x89a..f',10000" --port 8777`". If you use this option specify the port +in your testrpcOptions string AND as a `port` option. ++ **testCommand**: By default Solcover runs `truffle test` or `truffle test --network coverage`. +`testCommand` let you run tests some other way: ex: `mocha --timeout 5000`. If you're doing this, you +will probably need to make sure the web3 provider for your tests explicitly connects to the port solcover's +testrpc is set to run on, e.g: `var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8555"))` + +**Example .solcover.js config file** +``` +module.exports = { + port: 6545, + testrpcOptions: '-p 6545 -u 0x54fd80d6ae7584d8e9a19fe1df43f04e5282cc43', + testCommand: 'mocha --timeout 5000' +}; +``` + ### Known Issues **Hardcoded gas costs**: If you have hardcoded gas costs into your tests some of them may fail when using SolCover. @@ -74,11 +94,22 @@ implements a simplified search algorithm for node_modules packages Because Solcover copies an instrumented version of your project into a temporary folder, `require` statements handled by Truffle internally don't resolve correctly. +**Coveralls / CodeCov**: These CI services take the Istanbul reports generated by Solcover and display +line coverage. Istanbul's own html report provides significantly more detailed +reporting and can show whether your tests actually cover all the conditional branches +in your code. It can be found inside the `coverage` folder at `index.html` after you run the tool. + ### Examples -+ **zeppelin-solidity at commit 453a19825013a586751b87c67bebd551a252fb50** ++ **Metacoin**: The default truffle project + + [HTML reports](https://sc-forks.github.io/metacoin/) + + [Metacoin with Solcover installed](https://github.com/sc-forks/metacoin) (simple, without configuration) ++ **zeppelin-solidity** at commit 453a19825013a586751b87c67bebd551a252fb50 + [HTML reports]( https://sc-forks.github.io/zeppelin-solidity/) + [Zeppelin with Solcover installed](https://github.com/sc-forks/zeppelin-solidity) (declares own coverage network in truffle.js) ++ **numeraire** at commit 5ac3fa432c6b4192468c95a66e52ca086c804c95 + + [HTML reports](https://sc-forks.github.io/contract/) + + [Numeraire with Solcover installed](https://github.com/sc-forks/contract) (uses .solcover.js) ### TODO From 48e77b290f571cb787eec1bce173c0583a247cc3 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Tue, 25 Apr 2017 12:39:11 -0700 Subject: [PATCH 51/51] Misc edits for clarity --- README.md | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ca4b686..4241a41 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,9 @@ ### Code coverage for Solidity testing ![coverage example](https://cdn-images-1.medium.com/max/800/1*uum8t-31bUaa6dTRVVhj6w.png) -For more details about what this is, how it work and potential limitations, see +For more details about what this is, how it works and potential limitations, see [the accompanying article](https://blog.colony.io/code-coverage-for-solidity-eecfa88668c2). -This branch is an attempt to prepare solcover for npm publication and simplify its use as a -command line utility. Gas cost issues are managed under the hood and the tool cleans up after -itself if (when) it crashes. - ### Install ``` $ npm install --save-dev https://github.com/JoinColony/solcover.git#truffle3 @@ -24,18 +20,19 @@ $ ./node_modules/solcover/exec.js ``` Tests run signficantly slower while coverage is being generated. A 1 to 2 minute delay -between the end of Truffle compilation and the beginning of test execution is not impossible if your +between the end of Truffle compilation and the beginning of test execution is possible if your test suite is large. Large solidity files can also take a while to instrument. ### Configuration -By default, solcover generates a stub `truffle.js` that accomodates its special gas needs and -connects to a modified version of testrpc on port 8555. If your tests can run on the development network +By default, Solcover generates a stub `truffle.js` that accomodates its special gas needs and +connects to a modified version of testrpc on port 8555. If your tests will run on the development network using a standard `truffle.js` and a testrpc instance with no special options, you shouldn't have to do any configuration. If your tests depend on logic added to `truffle.js` - for example: [zeppelin-solidity](https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/truffle.js) -uses the file to expose a babel polyfill that its suite needs to run correctly - you can override the -default behavior by specifying a coverage network in `truffle.js`. +uses the file to expose a babel polyfill that its suite requires - you can override the +default behavior by declaring a coverage network in `truffle.js`. Solcover will use your 'truffle.js' +instead of a dynamically generated one. **Example coverage network config** ```javascript @@ -61,12 +58,14 @@ You can also create a `.solcover.js` config file in the root directory of your p some additional options: + **port**: The port you want Solcover to run testrpc on / have truffle connect to. + **testrpcOptions**: A string of options to be appended to a command line invocation -of testrpc. Ex: `--account="0x89a..f',10000" --port 8777`". If you use this option specify the port -in your testrpcOptions string AND as a `port` option. +of testrpc. + + Example: `--account="0x89a...b1f',10000" --port 8777`". + + Note: you should specify the port in your `testrpcOptions` string AND as a `port` option. + **testCommand**: By default Solcover runs `truffle test` or `truffle test --network coverage`. -`testCommand` let you run tests some other way: ex: `mocha --timeout 5000`. If you're doing this, you -will probably need to make sure the web3 provider for your tests explicitly connects to the port solcover's -testrpc is set to run on, e.g: `var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8555"))` +This option lets you run tests some other way: ex: `mocha --timeout 5000`. You +will probably also need to make sure the web3 provider for your tests explicitly connects to the port Solcover's +testrpc is set to run on, e.g: + + `var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8555"))` **Example .solcover.js config file** ``` @@ -84,20 +83,20 @@ This is because the instrumentation process increases the gas costs for using th the extra events. If this is the case, then the coverage may be incomplete. To avoid this, using `estimateGas` to estimate your gas costs should be more resilient in most cases. -**Events testing**: Solcover injects events into your solidity files to log which lines your tests reach, -so any tests that depend on how many events are fired or where the event sits in the logs array +**Events testing**: Because Solcover injects events into your contracts to log which lines your tests reach, +any tests that ask how many events are fired or where the event sits in the logs array will probably error while coverage is being generated. **Using `require` in `migrations.js` files**: Truffle overloads Node's `require` function but implements a simplified search algorithm for node_modules packages ([see issue #383 at Truffle](https://github.com/trufflesuite/truffle/issues/383)). Because Solcover copies an instrumented version of your project into a temporary folder, `require` -statements handled by Truffle internally don't resolve correctly. +statements handled by Truffle internally won't resolve correctly. **Coveralls / CodeCov**: These CI services take the Istanbul reports generated by Solcover and display -line coverage. Istanbul's own html report provides significantly more detailed -reporting and can show whether your tests actually cover all the conditional branches -in your code. It can be found inside the `coverage` folder at `index.html` after you run the tool. +line coverage. Istanbul's own html report publishes significantly more detail and can show whether +your tests actually reach all the conditional branches in your code. It can be found inside the +`coverage` folder at `index.html` after you run the tool. ### Examples