diff --git a/.eslintrc b/.eslintrc index c212615..ed70c92 100644 --- a/.eslintrc +++ b/.eslintrc @@ -14,7 +14,7 @@ /* General */ "consistent-return": [0], "no-return-assign": [0], - "complexity": ["error", 6], + "complexity": ["error", 20], "eol-last": [0], "eqeqeq": ["error", "smart"], "max-len": ["error", 150, 2], diff --git a/bin/exec.js b/bin/exec.js index 93c1147..5adeaa0 100644 --- a/bin/exec.js +++ b/bin/exec.js @@ -2,6 +2,7 @@ const App = require('./../lib/app.js'); const reqCwd = require('req-cwd'); const death = require('death'); + const log = console.log; const config = reqCwd.silent('./.solcover.js') || {}; diff --git a/lib/app.js b/lib/app.js index 9f66ad7..5aba3b7 100644 --- a/lib/app.js +++ b/lib/app.js @@ -15,20 +15,20 @@ const gasPriceHex = 0x01; // Low gas price * Coverage Runner */ class App { - constructor(config){ + constructor(config) { this.coverageDir = './coverageEnv'; // Env that instrumented .sols are tested in // Options this.network = ''; // Default truffle network execution flag - this.silence = ''; // Default log level: configurable by --silence + this.silence = ''; // Default log level passed to shell this.log = console.log; // Other - this.testrpcProcess; // ref to testrpc server we need to close on exit - this.events; // ref to string loaded from 'allFiredEvents' + this.testrpcProcess = null; // ref to testrpc server we need to close on exit + this.events = null; // ref to string array loaded from 'allFiredEvents' this.testsErrored = null; // flag set to non-null if truffle tests error this.coverage = new CoverageMap(); // initialize a coverage map - + // Config this.config = config || {}; this.workingDir = config.dir || '.'; // Relative path to contracts folder @@ -36,7 +36,7 @@ class App { this.skipFiles = config.skipFiles || []; // Which files should be skipped during instrumentation this.norpc = config.norpc || false; // Launch testrpc-sc internally? this.port = config.port || 8555; // Port testrpc should listen on - + this.copyNodeModules = config.copyNodeModules || false; // Copy node modules into coverageEnv? this.testrpcOptions = config.testrpcOptions || null; // Options for testrpc-sc this.testCommand = config.testCommand || null; // Optional test command @@ -44,23 +44,22 @@ class App { this.setLoggingLevel(config.silent); } - //-------------------------------------- Methods ------------------------------------------------ - + // -------------------------------------- Methods ------------------------------------------------ + /** - * Generates a copy of the target project configured for solidity-coverage and saves to + * Generates a copy of the target project configured for solidity-coverage and saves to * the coverage environment folder. Process exits(1) if try fails */ - generateCoverageEnvironment(){ - + generateCoverageEnvironment() { this.log('Generating coverage environment'); - + try { let files = shell.ls(this.workingDir); const nmIndex = files.indexOf('node_modules'); // Removes node_modules from array (unless requested). if (!this.copyNodeModules && nmIndex > -1) { - files.splice(nmIndex, 1); + files.splice(nmIndex, 1); } files = files.map(file => `${this.workingDir}/${file}`); @@ -73,10 +72,10 @@ class App { if (truffleConfig && truffleConfig.networks && truffleConfig.networks.coverage) { this.port = truffleConfig.networks.coverage.port || this.port; this.network = '--network coverage'; - - // No coverage network defaults to the dev network on port 8555, high gas / low price. + + // No coverage network defaults to the dev network on port 8555, high gas / low price. } else { - const trufflejs = defaultTruffleConfig(this.port, gasLimitHex, gasPriceHex) + const trufflejs = defaultTruffleConfig(this.port, gasLimitHex, gasPriceHex); fs.writeFileSync(`${this.coverageDir}/truffle.js`, trufflejs); } } catch (err) { @@ -93,8 +92,7 @@ class App { * + Save instrumented contract in the coverage environment folder where covered tests will run * + Add instrumentation info to the coverage map */ - instrumentTarget(){ - + instrumentTarget() { this.skipFiles = this.skipFiles.map(contract => `${this.coverageDir}/contracts/${contract}`); this.skipFiles.push(`${this.coverageDir}/contracts/Migrations.sol`); @@ -126,9 +124,8 @@ class App { * Changes here should be also be added to the before() block of test/run.js). * @return {Promise} Resolves when testrpc prints 'Listening' to std out / norpc is true. */ - launchTestrpc(){ + launchTestrpc() { return new Promise((resolve, reject) => { - if (!this.norpc) { const defaultRpcOptions = `--gasLimit ${gasLimitHex} --accounts ${this.accounts} --port ${this.port}`; const options = this.testrpcOptions || defaultRpcOptions; @@ -137,13 +134,13 @@ class App { // Launch this.testrpcProcess = childprocess.exec(command + options, null, (err, stdout, stderr) => { if (err) { - if(stdout) this.log(`testRpc stdout:\n${stdout}`); - if(stderr) this.log(`testRpc stderr:\n${stderr}`); + if (stdout) this.log(`testRpc stdout:\n${stdout}`); + if (stderr) this.log(`testRpc stderr:\n${stderr}`); this.cleanUp('testRpc errored after launching as a childprocess.'); } }); - // Resolve when testrpc logs that it's listening. + // Resolve when testrpc logs that it's listening. this.testrpcProcess.stdout.on('data', data => { if (data.includes('Listening')) { this.log(`Launched testrpc on port ${this.port}`); @@ -153,7 +150,7 @@ class App { } else { return resolve(); } - }) + }); } /** @@ -162,10 +159,8 @@ class App { * as its own statement for command line options to work, apparently. * Also reads the 'allFiredEvents' log. */ - runTestCommand(){ - + runTestCommand() { try { - const defaultCommand = `truffle test ${this.network} ${this.silence}`; const command = this.testCommand || defaultCommand; this.log(`Running: ${command}\n(this can take a few seconds)...`); @@ -196,8 +191,7 @@ class App { /** * Generate coverage / write coverage report / run istanbul */ - generateReport(){ - + generateReport() { const collector = new istanbul.Collector(); const reporter = new istanbul.Reporter(); @@ -218,20 +212,19 @@ class App { resolve(); }); } catch (err) { - const msg = 'There was a problem generating the coverage map / running Istanbul.\n'; - this.cleanUp(msg + err); - + const msg = 'There was a problem generating the coverage map / running Istanbul.\n'; + this.cleanUp(msg + err); } - }) + }); } // ------------------------------------------ Utils ---------------------------------------------- - + /** * Allows config to turn logging off (for CI) - * @param {Boolean} isSilent + * @param {Boolean} isSilent */ - setLoggingLevel(isSilent){ + setLoggingLevel(isSilent) { if (isSilent) { this.silence = '> /dev/null 2>&1'; this.log = () => {}; diff --git a/lib/instrumenter.js b/lib/instrumenter.js index ec382ef..024b4d8 100644 --- a/lib/instrumenter.js +++ b/lib/instrumenter.js @@ -11,12 +11,12 @@ function createOrAppendInjectionPoint(contract, key, value) { } } -instrumenter.prePosition = function prePosition(expression){ - if (expression.right.type === 'ConditionalExpression' && - expression.left.type === 'MemberExpression'){ +instrumenter.prePosition = function prePosition(expression) { + if (expression.right.type === 'ConditionalExpression' && + expression.left.type === 'MemberExpression') { expression.start -= 2; } -} +}; instrumenter.instrumentAssignmentExpression = function instrumentAssignmentExpression(contract, expression) { // The only time we instrument an assignment expression is if there's a conditional expression on @@ -29,7 +29,7 @@ instrumenter.instrumentAssignmentExpression = function instrumentAssignmentExpre type: 'literal', string: '; (,' + expression.left.name + ')', }); instrumenter.instrumentConditionalExpression(contract, expression.right); - } else if (expression.left.type === 'MemberExpression'){ + } else if (expression.left.type === 'MemberExpression') { createOrAppendInjectionPoint(contract, expression.left.start, { type: 'literal', string: '(,', }); diff --git a/lib/truffleConfig.js b/lib/truffleConfig.js index e1bb15c..51eb480 100644 --- a/lib/truffleConfig.js +++ b/lib/truffleConfig.js @@ -1,5 +1,5 @@ module.exports = function truffleConfig(port, gasLimit, gasPrice) { - return ` + return ` module.exports = { networks: { development: { @@ -10,5 +10,5 @@ module.exports = function truffleConfig(port, gasLimit, gasPrice) { gasPrice: ${gasPrice} } } - };` + };`; }; \ No newline at end of file diff --git a/test/app.js b/test/app.js index fb2e80e..e3a5706 100644 --- a/test/app.js +++ b/test/app.js @@ -27,9 +27,15 @@ describe('app', () => { norpc: true, }; - before(() => { + before(done => { const command = `./node_modules/.bin/testrpc-sc --gasLimit 0xfffffffffff --port ${port}`; testrpcProcess = childprocess.exec(command); + + testrpcProcess.stdout.on('data', data => { + if (data.includes('Listening')) { + done(); + } + }); }); afterEach(() => { @@ -198,6 +204,14 @@ describe('app', () => { collectGarbage(); }); + it('testrpc-sc signs and recovers messages correctly', () => { + // sign.js signs and recovers + mock.install('Simple.sol', 'sign.js', config); + shell.exec(script); + assert(shell.error() === null, 'script should not error'); + collectGarbage(); + }); + it('tests require assets outside of test folder: should generate coverage, cleanup & exit(0)', () => { // Directory should be clean assert(pathExists('./coverage') === false, 'should start without: coverage'); @@ -239,6 +253,7 @@ describe('app', () => { 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'); diff --git a/test/cli/sign.js b/test/cli/sign.js new file mode 100644 index 0000000..c4d7913 --- /dev/null +++ b/test/cli/sign.js @@ -0,0 +1,31 @@ +/* eslint-env node, mocha */ +/* global artifacts, contract, assert */ + +const Web3 = require('web3'); +const ethUtil = require('ethereumjs-util'); + +const provider = new Web3.providers.HttpProvider('http://localhost:8555'); // testrpc-sc +const web3 = new Web3(provider); +const Simple = artifacts.require('./Simple.sol'); + +contract('Simple', accounts => { + it('should set x to 5', () => { + let simple; + return Simple.deployed() + .then(instance => instance.test(5)) // We need this line to generate some coverage + .then(() => { + const message = 'Enclosed is my formal application for permanent residency in New Zealand'; + const messageSha3 = web3.sha3(message); + const signature = web3.eth.sign(accounts[0], messageSha3); + + const messageBuffer = new Buffer(messageSha3.replace('0x', ''), 'hex'); + const messagePersonalHash = ethUtil.hashPersonalMessage(messageBuffer); + + const sigParams = ethUtil.fromRpcSig(signature); + const publicKey = ethUtil.ecrecover(messagePersonalHash, sigParams.v, sigParams.r, sigParams.s); + const senderBuffer = ethUtil.pubToAddress(publicKey); + const sender = ethUtil.bufferToHex(senderBuffer); + assert.equal(sender, accounts[0]); + }); + }); +}); \ No newline at end of file diff --git a/test/conditional.js b/test/conditional.js index 1d2a73c..17e848e 100644 --- a/test/conditional.js +++ b/test/conditional.js @@ -168,13 +168,13 @@ describe('conditional statements', () => { vm.execute(info.contract, 'a', []).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { - '11': 1, '12': 1, + 11: 1, 12: 1, }); assert.deepEqual(mapping[filePath].b, { 1: [0, 1], }); assert.deepEqual(mapping[filePath].s, { - '1': 1, '2': 1, + 1: 1, 2: 1, }); assert.deepEqual(mapping[filePath].f, { 1: 1, diff --git a/test/statements.js b/test/statements.js index 1a61db8..6e222d2 100644 --- a/test/statements.js +++ b/test/statements.js @@ -109,14 +109,14 @@ describe('generic statements', () => { vm.execute(info.contract, 'a', []).then(events => { const mapping = coverage.generate(events, pathPrefix); assert.deepEqual(mapping[filePath].l, { - 6: 1, 10: 1, 11: 1 + 6: 1, 10: 1, 11: 1, }); assert.deepEqual(mapping[filePath].b, {}); - assert.deepEqual(mapping[filePath].s, { - 1: 1, 2: 1, 3: 1 + assert.deepEqual(mapping[filePath].s, { + 1: 1, 2: 1, 3: 1, }); assert.deepEqual(mapping[filePath].f, { - 1: 1, 2: 1 + 1: 1, 2: 1, }); done(); }).catch(done); diff --git a/test/util/mockTestCommand.js b/test/util/mockTestCommand.js index 543aacf..3222606 100644 --- a/test/util/mockTestCommand.js +++ b/test/util/mockTestCommand.js @@ -1,22 +1,22 @@ #!/usr/bin/env node -const fs = require('fs') -const request = require('request') +const fs = require('fs'); +const request = require('request'); request({ - uri: 'http://localhost:8888', - body: { - jsonrpc: "2.0", - method: "web3_clientVersion", - params: [], - id: 0 - }, - json: true + uri: 'http://localhost:8888', + body: { + jsonrpc: '2.0', + method: 'web3_clientVersion', + params: [], + id: 0, + }, + json: true, }, (error, response, body) => { - if(error) { - console.error(error) - process.exit(1) - } - fs.writeFileSync('../allFiredEvents', 'foobar') - process.exit(0) -}) + if (error) { + console.error(error); + process.exit(1); + } + fs.writeFileSync('../allFiredEvents', 'foobar'); + process.exit(0); +});