Merge pull request #73 from sc-forks/signing-test

Add testrpc-sc signing test and lint
pull/76/head
c-g-e-w-e-k-e- 7 years ago committed by GitHub
commit ebda87dd3c
  1. 2
      .eslintrc
  2. 1
      bin/exec.js
  3. 65
      lib/app.js
  4. 10
      lib/instrumenter.js
  5. 4
      lib/truffleConfig.js
  6. 17
      test/app.js
  7. 31
      test/cli/sign.js
  8. 4
      test/conditional.js
  9. 8
      test/statements.js
  10. 34
      test/util/mockTestCommand.js

@ -14,7 +14,7 @@
/* General */ /* General */
"consistent-return": [0], "consistent-return": [0],
"no-return-assign": [0], "no-return-assign": [0],
"complexity": ["error", 6], "complexity": ["error", 20],
"eol-last": [0], "eol-last": [0],
"eqeqeq": ["error", "smart"], "eqeqeq": ["error", "smart"],
"max-len": ["error", 150, 2], "max-len": ["error", 150, 2],

@ -2,6 +2,7 @@
const App = require('./../lib/app.js'); const App = require('./../lib/app.js');
const reqCwd = require('req-cwd'); const reqCwd = require('req-cwd');
const death = require('death'); const death = require('death');
const log = console.log; const log = console.log;
const config = reqCwd.silent('./.solcover.js') || {}; const config = reqCwd.silent('./.solcover.js') || {};

@ -15,20 +15,20 @@ const gasPriceHex = 0x01; // Low gas price
* Coverage Runner * Coverage Runner
*/ */
class App { class App {
constructor(config){ constructor(config) {
this.coverageDir = './coverageEnv'; // Env that instrumented .sols are tested in this.coverageDir = './coverageEnv'; // Env that instrumented .sols are tested in
// Options // Options
this.network = ''; // Default truffle network execution flag 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; this.log = console.log;
// Other // Other
this.testrpcProcess; // ref to testrpc server we need to close on exit this.testrpcProcess = null; // ref to testrpc server we need to close on exit
this.events; // ref to string loaded from 'allFiredEvents' this.events = null; // ref to string array loaded from 'allFiredEvents'
this.testsErrored = null; // flag set to non-null if truffle tests error this.testsErrored = null; // flag set to non-null if truffle tests error
this.coverage = new CoverageMap(); // initialize a coverage map this.coverage = new CoverageMap(); // initialize a coverage map
// Config // Config
this.config = config || {}; this.config = config || {};
this.workingDir = config.dir || '.'; // Relative path to contracts folder 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.skipFiles = config.skipFiles || []; // Which files should be skipped during instrumentation
this.norpc = config.norpc || false; // Launch testrpc-sc internally? this.norpc = config.norpc || false; // Launch testrpc-sc internally?
this.port = config.port || 8555; // Port testrpc should listen on this.port = config.port || 8555; // Port testrpc should listen on
this.copyNodeModules = config.copyNodeModules || false; // Copy node modules into coverageEnv? this.copyNodeModules = config.copyNodeModules || false; // Copy node modules into coverageEnv?
this.testrpcOptions = config.testrpcOptions || null; // Options for testrpc-sc this.testrpcOptions = config.testrpcOptions || null; // Options for testrpc-sc
this.testCommand = config.testCommand || null; // Optional test command this.testCommand = config.testCommand || null; // Optional test command
@ -44,23 +44,22 @@ class App {
this.setLoggingLevel(config.silent); 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 * the coverage environment folder. Process exits(1) if try fails
*/ */
generateCoverageEnvironment(){ generateCoverageEnvironment() {
this.log('Generating coverage environment'); this.log('Generating coverage environment');
try { try {
let files = shell.ls(this.workingDir); let files = shell.ls(this.workingDir);
const nmIndex = files.indexOf('node_modules'); const nmIndex = files.indexOf('node_modules');
// Removes node_modules from array (unless requested). // Removes node_modules from array (unless requested).
if (!this.copyNodeModules && nmIndex > -1) { if (!this.copyNodeModules && nmIndex > -1) {
files.splice(nmIndex, 1); files.splice(nmIndex, 1);
} }
files = files.map(file => `${this.workingDir}/${file}`); files = files.map(file => `${this.workingDir}/${file}`);
@ -73,10 +72,10 @@ class App {
if (truffleConfig && truffleConfig.networks && truffleConfig.networks.coverage) { if (truffleConfig && truffleConfig.networks && truffleConfig.networks.coverage) {
this.port = truffleConfig.networks.coverage.port || this.port; this.port = truffleConfig.networks.coverage.port || this.port;
this.network = '--network coverage'; 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 { } else {
const trufflejs = defaultTruffleConfig(this.port, gasLimitHex, gasPriceHex) const trufflejs = defaultTruffleConfig(this.port, gasLimitHex, gasPriceHex);
fs.writeFileSync(`${this.coverageDir}/truffle.js`, trufflejs); fs.writeFileSync(`${this.coverageDir}/truffle.js`, trufflejs);
} }
} catch (err) { } catch (err) {
@ -93,8 +92,7 @@ class App {
* + Save instrumented contract in the coverage environment folder where covered tests will run * + Save instrumented contract in the coverage environment folder where covered tests will run
* + Add instrumentation info to the coverage map * + Add instrumentation info to the coverage map
*/ */
instrumentTarget(){ instrumentTarget() {
this.skipFiles = this.skipFiles.map(contract => `${this.coverageDir}/contracts/${contract}`); this.skipFiles = this.skipFiles.map(contract => `${this.coverageDir}/contracts/${contract}`);
this.skipFiles.push(`${this.coverageDir}/contracts/Migrations.sol`); 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). * 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. * @return {Promise} Resolves when testrpc prints 'Listening' to std out / norpc is true.
*/ */
launchTestrpc(){ launchTestrpc() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!this.norpc) { if (!this.norpc) {
const defaultRpcOptions = `--gasLimit ${gasLimitHex} --accounts ${this.accounts} --port ${this.port}`; const defaultRpcOptions = `--gasLimit ${gasLimitHex} --accounts ${this.accounts} --port ${this.port}`;
const options = this.testrpcOptions || defaultRpcOptions; const options = this.testrpcOptions || defaultRpcOptions;
@ -137,13 +134,13 @@ class App {
// Launch // Launch
this.testrpcProcess = childprocess.exec(command + options, null, (err, stdout, stderr) => { this.testrpcProcess = childprocess.exec(command + options, null, (err, stdout, stderr) => {
if (err) { if (err) {
if(stdout) this.log(`testRpc stdout:\n${stdout}`); if (stdout) this.log(`testRpc stdout:\n${stdout}`);
if(stderr) this.log(`testRpc stderr:\n${stderr}`); if (stderr) this.log(`testRpc stderr:\n${stderr}`);
this.cleanUp('testRpc errored after launching as a childprocess.'); 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 => { this.testrpcProcess.stdout.on('data', data => {
if (data.includes('Listening')) { if (data.includes('Listening')) {
this.log(`Launched testrpc on port ${this.port}`); this.log(`Launched testrpc on port ${this.port}`);
@ -153,7 +150,7 @@ class App {
} else { } else {
return resolve(); return resolve();
} }
}) });
} }
/** /**
@ -162,10 +159,8 @@ class App {
* as its own statement for command line options to work, apparently. * as its own statement for command line options to work, apparently.
* Also reads the 'allFiredEvents' log. * Also reads the 'allFiredEvents' log.
*/ */
runTestCommand(){ runTestCommand() {
try { try {
const defaultCommand = `truffle test ${this.network} ${this.silence}`; const defaultCommand = `truffle test ${this.network} ${this.silence}`;
const command = this.testCommand || defaultCommand; const command = this.testCommand || defaultCommand;
this.log(`Running: ${command}\n(this can take a few seconds)...`); this.log(`Running: ${command}\n(this can take a few seconds)...`);
@ -196,8 +191,7 @@ class App {
/** /**
* Generate coverage / write coverage report / run istanbul * Generate coverage / write coverage report / run istanbul
*/ */
generateReport(){ generateReport() {
const collector = new istanbul.Collector(); const collector = new istanbul.Collector();
const reporter = new istanbul.Reporter(); const reporter = new istanbul.Reporter();
@ -218,20 +212,19 @@ class App {
resolve(); resolve();
}); });
} catch (err) { } catch (err) {
const msg = 'There was a problem generating the coverage map / running Istanbul.\n'; const msg = 'There was a problem generating the coverage map / running Istanbul.\n';
this.cleanUp(msg + err); this.cleanUp(msg + err);
} }
}) });
} }
// ------------------------------------------ Utils ---------------------------------------------- // ------------------------------------------ Utils ----------------------------------------------
/** /**
* Allows config to turn logging off (for CI) * Allows config to turn logging off (for CI)
* @param {Boolean} isSilent * @param {Boolean} isSilent
*/ */
setLoggingLevel(isSilent){ setLoggingLevel(isSilent) {
if (isSilent) { if (isSilent) {
this.silence = '> /dev/null 2>&1'; this.silence = '> /dev/null 2>&1';
this.log = () => {}; this.log = () => {};

@ -11,12 +11,12 @@ function createOrAppendInjectionPoint(contract, key, value) {
} }
} }
instrumenter.prePosition = function prePosition(expression){ instrumenter.prePosition = function prePosition(expression) {
if (expression.right.type === 'ConditionalExpression' && if (expression.right.type === 'ConditionalExpression' &&
expression.left.type === 'MemberExpression'){ expression.left.type === 'MemberExpression') {
expression.start -= 2; expression.start -= 2;
} }
} };
instrumenter.instrumentAssignmentExpression = function instrumentAssignmentExpression(contract, expression) { instrumenter.instrumentAssignmentExpression = function instrumentAssignmentExpression(contract, expression) {
// The only time we instrument an assignment expression is if there's a conditional expression on // 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 + ')', type: 'literal', string: '; (,' + expression.left.name + ')',
}); });
instrumenter.instrumentConditionalExpression(contract, expression.right); instrumenter.instrumentConditionalExpression(contract, expression.right);
} else if (expression.left.type === 'MemberExpression'){ } else if (expression.left.type === 'MemberExpression') {
createOrAppendInjectionPoint(contract, expression.left.start, { createOrAppendInjectionPoint(contract, expression.left.start, {
type: 'literal', string: '(,', type: 'literal', string: '(,',
}); });

@ -1,5 +1,5 @@
module.exports = function truffleConfig(port, gasLimit, gasPrice) { module.exports = function truffleConfig(port, gasLimit, gasPrice) {
return ` return `
module.exports = { module.exports = {
networks: { networks: {
development: { development: {
@ -10,5 +10,5 @@ module.exports = function truffleConfig(port, gasLimit, gasPrice) {
gasPrice: ${gasPrice} gasPrice: ${gasPrice}
} }
} }
};` };`;
}; };

@ -27,9 +27,15 @@ describe('app', () => {
norpc: true, norpc: true,
}; };
before(() => { before(done => {
const command = `./node_modules/.bin/testrpc-sc --gasLimit 0xfffffffffff --port ${port}`; const command = `./node_modules/.bin/testrpc-sc --gasLimit 0xfffffffffff --port ${port}`;
testrpcProcess = childprocess.exec(command); testrpcProcess = childprocess.exec(command);
testrpcProcess.stdout.on('data', data => {
if (data.includes('Listening')) {
done();
}
});
}); });
afterEach(() => { afterEach(() => {
@ -198,6 +204,14 @@ describe('app', () => {
collectGarbage(); 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)', () => { it('tests require assets outside of test folder: should generate coverage, cleanup & exit(0)', () => {
// Directory should be clean // Directory should be clean
assert(pathExists('./coverage') === false, 'should start without: coverage'); assert(pathExists('./coverage') === false, 'should start without: coverage');
@ -239,6 +253,7 @@ describe('app', () => {
collectGarbage(); collectGarbage();
}); });
it('contract uses inheritance: should generate coverage, cleanup & exit(0)', () => { 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') === false, 'should start without: coverage');

@ -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]);
});
});
});

@ -168,13 +168,13 @@ describe('conditional statements', () => {
vm.execute(info.contract, 'a', []).then(events => { vm.execute(info.contract, 'a', []).then(events => {
const mapping = coverage.generate(events, pathPrefix); const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, { assert.deepEqual(mapping[filePath].l, {
'11': 1, '12': 1, 11: 1, 12: 1,
}); });
assert.deepEqual(mapping[filePath].b, { assert.deepEqual(mapping[filePath].b, {
1: [0, 1], 1: [0, 1],
}); });
assert.deepEqual(mapping[filePath].s, { assert.deepEqual(mapping[filePath].s, {
'1': 1, '2': 1, 1: 1, 2: 1,
}); });
assert.deepEqual(mapping[filePath].f, { assert.deepEqual(mapping[filePath].f, {
1: 1, 1: 1,

@ -109,14 +109,14 @@ describe('generic statements', () => {
vm.execute(info.contract, 'a', []).then(events => { vm.execute(info.contract, 'a', []).then(events => {
const mapping = coverage.generate(events, pathPrefix); const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, { 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].b, {});
assert.deepEqual(mapping[filePath].s, { assert.deepEqual(mapping[filePath].s, {
1: 1, 2: 1, 3: 1 1: 1, 2: 1, 3: 1,
}); });
assert.deepEqual(mapping[filePath].f, { assert.deepEqual(mapping[filePath].f, {
1: 1, 2: 1 1: 1, 2: 1,
}); });
done(); done();
}).catch(done); }).catch(done);

@ -1,22 +1,22 @@
#!/usr/bin/env node #!/usr/bin/env node
const fs = require('fs') const fs = require('fs');
const request = require('request') const request = require('request');
request({ request({
uri: 'http://localhost:8888', uri: 'http://localhost:8888',
body: { body: {
jsonrpc: "2.0", jsonrpc: '2.0',
method: "web3_clientVersion", method: 'web3_clientVersion',
params: [], params: [],
id: 0 id: 0,
}, },
json: true json: true,
}, (error, response, body) => { }, (error, response, body) => {
if(error) { if (error) {
console.error(error) console.error(error);
process.exit(1) process.exit(1);
} }
fs.writeFileSync('../allFiredEvents', 'foobar') fs.writeFileSync('../allFiredEvents', 'foobar');
process.exit(0) process.exit(0);
}) });

Loading…
Cancel
Save