Merge pull request #71 from sc-forks/refactor-exec

exec.js refactor
pull/72/head
c-g-e-w-e-k-e- 7 years ago committed by GitHub
commit 62eaa0be8d
  1. 1
      .gitignore
  2. 235
      bin/exec.js
  3. 3
      circle.yml
  4. 266
      lib/app.js
  5. 6
      lib/coverageMap.js
  6. 14
      lib/truffleConfig.js
  7. 3
      package.json
  8. 2
      test/app.js

1
.gitignore vendored

@ -1,5 +1,6 @@
allFiredEvents
scTopics
scDebugLog
coverage.json
coverage/
node_modules/

@ -1,229 +1,20 @@
#!/usr/bin/env node
const shell = require('shelljs');
const fs = require('fs');
const App = require('./../lib/app.js');
const reqCwd = require('req-cwd');
const path = require('path');
const childprocess = require('child_process');
const istanbul = require('istanbul');
const getInstrumentedVersion = require('./../lib/instrumentSolidity.js');
const CoverageMap = require('./../lib/coverageMap.js');
const istanbulCollector = new istanbul.Collector();
const istanbulReporter = new istanbul.Reporter();
// Very high gas block limits / contract deployment limits
const gasLimitString = '0xfffffffffff';
const gasLimitHex = 0xfffffffffff;
const gasPriceHex = 0x01;
const coverage = new CoverageMap();
// Paths
const coverageDir = './coverageEnv'; // Env that instrumented .sols are tested in
// Options
let coverageOption = '--network coverage'; // Default truffle network execution flag
let silence = ''; // Default log level: configurable by --silence
let log = console.log;
let testrpcProcess; // ref to testrpc server we need to close on exit
let events; // ref to string loaded from 'allFiredEvents'
let testsErrored = null; // flag set to non-null if truffle tests error
const death = require('death');
const log = console.log;
// --------------------------------------- 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');
shell.rm('./scTopics');
if (testrpcProcess) { testrpcProcess.kill(); }
if (err) {
log(`${err}\nExiting without generating coverage...`);
process.exit(1);
} else if (testsErrored) {
log('Some truffle tests failed while running coverage');
process.exit(1);
} else {
process.exit(0);
}
}
// --------------------------------------- Script --------------------------------------------------
const config = reqCwd.silent('./.solcover.js') || {};
const app = new App(config);
const workingDir = config.dir || '.'; // Relative path to contracts folder
let port = config.port || 8555; // Port testrpc listens on
const accounts = config.accounts || 35; // Number of accounts to testrpc launches with
const copyNodeModules = config.copyNodeModules || false; // Whether we copy node_modules when making coverage environment
let skipFiles = config.skipFiles || []; // Which files should be skipped during instrumentation
// Silence shell and script logging (for solcover's unit tests / CI)
if (config.silent) {
silence = '> /dev/null 2>&1';
log = () => {};
}
// Generate a copy of the target project configured for solcover and save to the coverage
// environment folder.
log('Generating coverage environment');
try {
let files = shell.ls(workingDir);
const nmIndex = files.indexOf('node_modules');
if (!config.copyNodeModules && nmIndex > -1) {
files.splice(nmIndex, 1); // Removes node_modules from array.
}
files = files.map(file => `${workingDir}/` + file);
shell.mkdir(coverageDir);
shell.cp('-R', files, coverageDir);
const truffleConfig = reqCwd.silent(`${workingDir}/truffle.js`);
// Coverage network opts specified: use port if declared
if (truffleConfig && truffleConfig.networks && truffleConfig.networks.coverage) {
port = truffleConfig.networks.coverage.port || port;
// Coverage network opts NOT specified: default to the development network w/ modified
// port, gasLimit, gasPrice. Export the config object only.
} else {
const trufflejs = `
module.exports = {
networks: {
development: {
host: "localhost",
network_id: "*",
port: ${port},
gas: ${gasLimitHex},
gasPrice: ${gasPriceHex}
}
}
};`;
coverageOption = '';
fs.writeFileSync(`${coverageDir}/truffle.js`, trufflejs);
}
} catch (err) {
const msg = ('There was a problem generating the coverage environment: ');
cleanUp(msg + err);
}
// For each contract except migrations.sol (or those in skipFiles):
// 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
// 5. Add instrumentation info to the coverage map
skipFiles = skipFiles.map(contract => `${coverageDir}/contracts/` + contract);
skipFiles.push(`${coverageDir}/contracts/Migrations.sol`);
let currentFile;
try {
shell.ls(`${coverageDir}/contracts/**/*.sol`).forEach(file => {
if (!skipFiles.includes(file)) {
log('Instrumenting ', file);
currentFile = file;
const contractPath = path.resolve(file);
const canonicalPath = contractPath.split('/coverageEnv').join('');
const contract = fs.readFileSync(contractPath).toString();
const instrumentedContractInfo = getInstrumentedVersion(contract, canonicalPath);
fs.writeFileSync(contractPath, instrumentedContractInfo.contract);
coverage.addContract(instrumentedContractInfo, canonicalPath);
} else {
log('Skipping instrumentation of ', file);
}
});
} catch (err) {
const msg = (`There was a problem instrumenting ${currentFile}: `);
cleanUp(msg + err);
}
new Promise((resolve, reject) => {
// 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) {
const defaultRpcOptions = `--gasLimit ${gasLimitString} --accounts ${accounts} --port ${port}`;
const testrpcOptions = config.testrpcOptions || defaultRpcOptions;
const command = './node_modules/ethereumjs-testrpc-sc/bin/testrpc ';
testrpcProcess = childprocess.exec(command + testrpcOptions, null, (err, stdout, stderr) => {
if (err) {
if(stdout) log(`testRpc stdout:\n${stdout}`);
if(stderr) log(`testRpc stderr:\n${stderr}`);
cleanUp('testRpc errored after launching as a childprocess.');
}
});
testrpcProcess.stdout.on('data', data => {
if (data.includes('Listening')) {
log(`Launched testrpc on port ${port}`);
return resolve();
}
});
} else {
return resolve();
}
}).then(() => {
// Run truffle (or config.testCommand) 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 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);
testsErrored = shell.error();
shell.cd('./..');
} 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, './contracts');
const json = JSON.stringify(coverage.coverage);
fs.writeFileSync('./coverage.json', json);
app.generateCoverageEnvironment();
app.instrumentTarget();
app.launchTestrpc()
.then(() => {
app.runTestCommand();
app.generateReport();
})
.catch(err => log(err));
istanbulCollector.add(coverage.coverage);
istanbulReporter.add('html');
istanbulReporter.add('lcov');
istanbulReporter.add('text');
istanbulReporter.write(istanbulCollector, true, () => {
log('Istanbul coverage reports generated');
});
} catch (err) {
if (config.testing) {
cleanUp();
} else {
const msg = 'There was a problem generating producing the coverage map / running Istanbul.\n';
cleanUp(msg + err);
}
}
death((signal, err) => app.cleanUp(err));
// Finish
cleanUp();
});

@ -1,6 +1,9 @@
machine:
node:
version: 6.11.0
dependencies:
override:
- npm install
test:
override:
- npm run test-cov

@ -0,0 +1,266 @@
const shell = require('shelljs');
const fs = require('fs');
const path = require('path');
const childprocess = require('child_process');
const reqCwd = require('req-cwd');
const istanbul = require('istanbul');
const getInstrumentedVersion = require('./instrumentSolidity.js');
const CoverageMap = require('./coverageMap.js');
const defaultTruffleConfig = require('./truffleConfig.js');
const gasLimitHex = 0xfffffffffff; // High gas block limit / contract deployment limit
const gasPriceHex = 0x01; // Low gas price
/**
* Coverage Runner
*/
class App {
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.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.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
this.accounts = config.accounts || 35; // Number of accounts to testrpc launches with
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
this.setLoggingLevel(config.silent);
}
//-------------------------------------- Methods ------------------------------------------------
/**
* 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(){
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 = files.map(file => `${this.workingDir}/${file}`);
shell.mkdir(this.coverageDir);
shell.cp('-R', files, this.coverageDir);
const truffleConfig = reqCwd.silent(`${this.workingDir}/truffle.js`);
// Coverage network opts specified: use port if declared
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.
} else {
const trufflejs = defaultTruffleConfig(this.port, gasLimitHex, gasPriceHex)
fs.writeFileSync(`${this.coverageDir}/truffle.js`, trufflejs);
}
} catch (err) {
const msg = ('There was a problem generating the coverage environment: ');
this.cleanUp(msg + err);
}
}
/**
* For each contract except migrations.sol (or those in skipFiles):
* + Generate file path reference for coverage report
* + Load contract as string
* + Instrument contract
* + Save instrumented contract in the coverage environment folder where covered tests will run
* + Add instrumentation info to the coverage map
*/
instrumentTarget(){
this.skipFiles = this.skipFiles.map(contract => `${this.coverageDir}/contracts/${contract}`);
this.skipFiles.push(`${this.coverageDir}/contracts/Migrations.sol`);
let currentFile;
try {
shell.ls(`${this.coverageDir}/contracts/**/*.sol`).forEach(file => {
if (!this.skipFiles.includes(file)) {
this.log('Instrumenting ', file);
currentFile = file;
const contractPath = path.resolve(file);
const canonicalPath = contractPath.split(this.coverageDir).join('');
const contract = fs.readFileSync(contractPath).toString();
const instrumentedContractInfo = getInstrumentedVersion(contract, canonicalPath);
fs.writeFileSync(contractPath, instrumentedContractInfo.contract);
this.coverage.addContract(instrumentedContractInfo, canonicalPath);
} else {
this.log('Skipping instrumentation of ', file);
}
});
} catch (err) {
const msg = `There was a problem instrumenting ${currentFile}: `;
this.cleanUp(msg + err);
}
}
/**
* 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).
* @return {Promise} Resolves when testrpc prints 'Listening' to std out / norpc is true.
*/
launchTestrpc(){
return new Promise((resolve, reject) => {
if (!this.norpc) {
const defaultRpcOptions = `--gasLimit ${gasLimitHex} --accounts ${this.accounts} --port ${this.port}`;
const options = this.testrpcOptions || defaultRpcOptions;
const command = './node_modules/ethereumjs-testrpc-sc/bin/testrpc ';
// 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}`);
this.cleanUp('testRpc errored after launching as a childprocess.');
}
});
// 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}`);
return resolve();
}
});
} else {
return resolve();
}
})
}
/**
* Run truffle (or config.testCommand) 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.
* Also reads the 'allFiredEvents' log.
*/
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)...`);
shell.cd(this.coverageDir);
shell.exec(command);
this.testsErrored = shell.error();
shell.cd('./..');
} catch (err) {
this.cleanUp(err);
}
// Get events fired during instrumented contracts execution.
try {
this.events = fs.readFileSync('./allFiredEvents').toString().split('\n');
this.events.pop();
} catch (err) {
const msg =
`
There was an error generating coverage. Possible reasons include:
1. Another application is using port ${this.port}
2. Truffle crashed because your tests errored
`;
this.cleanUp(msg + err);
}
}
/**
* Generate coverage / write coverage report / run istanbul
*/
generateReport(){
const collector = new istanbul.Collector();
const reporter = new istanbul.Reporter();
return new Promise((resolve, reject) => {
try {
this.coverage.generate(this.events, './contracts');
const json = JSON.stringify(this.coverage.coverage);
fs.writeFileSync('./coverage.json', json);
collector.add(this.coverage.coverage);
reporter.add('html');
reporter.add('lcov');
reporter.add('text');
reporter.write(collector, true, () => {
this.log('Istanbul coverage reports generated');
this.cleanUp();
resolve();
});
} catch (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
*/
setLoggingLevel(isSilent){
if (isSilent) {
this.silence = '> /dev/null 2>&1';
this.log = () => {};
}
}
/**
* Removes coverage build artifacts, kills testrpc.
* Exits (1) and prints msg on error, exits (0) otherwise.
* @param {String} err error message
*/
cleanUp(err) {
this.log('Cleaning up...');
shell.config.silent = true;
shell.rm('-Rf', this.coverageDir);
shell.rm('./allFiredEvents');
shell.rm('./scTopics');
if (this.testrpcProcess) { this.testrpcProcess.kill(); }
if (err) {
this.log(`${err}\nExiting without generating coverage...`);
process.exit(1);
} else if (this.testsErrored) {
this.log('Some truffle tests failed while running coverage');
process.exit(1);
} else {
process.exit(0);
}
}
}
module.exports = App;

@ -75,10 +75,8 @@ module.exports = class CoverageMap {
this.branchTopics.push(branchHash);
this.statementTopics.push(statementHash);
fs.appendFileSync('./scTopics', `${lineHash}\n`);
fs.appendFileSync('./scTopics', `${fnHash}\n`);
fs.appendFileSync('./scTopics', `${branchHash}\n`);
fs.appendFileSync('./scTopics', `${statementHash}\n`);
const topics = `${lineHash}\n${fnHash}\n${branchHash}\n${statementHash}\n`;
fs.appendFileSync('./scTopics', topics);
}
/**

@ -0,0 +1,14 @@
module.exports = function truffleConfig(port, gasLimit, gasPrice) {
return `
module.exports = {
networks: {
development: {
host: "localhost",
network_id: "*",
port: ${port},
gas: ${gasLimit},
gasPrice: ${gasPrice}
}
}
};`
};

@ -22,6 +22,7 @@
"license": "ISC",
"dependencies": {
"commander": "^2.9.0",
"death": "^1.1.0",
"ethereumjs-testrpc-sc": "https://github.com/sc-forks/testrpc-sc.git",
"istanbul": "^0.4.5",
"keccakjs": "^0.2.1",
@ -44,6 +45,6 @@
"merkle-patricia-tree": "~2.1.2",
"mocha": "^3.1.0",
"solc": "0.4.8",
"truffle": "3.2.5"
"truffle": "3.4.4"
}
}

@ -14,7 +14,7 @@ function collectGarbage() {
if (global.gc) { global.gc(); }
}
describe('cli', () => {
describe('app', () => {
let testrpcProcess = null;
const script = 'node ./bin/exec.js';
const port = 8555;
Loading…
Cancel
Save