|
|
|
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');
|
|
|
|
|
|
|
|
// 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
|
|
|
|
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 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) {
|
|
|
|
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).
|
|
|
|
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}`);
|
|
|
|
|
|
|
|
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
|
|
|
|
// 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)}`);
|
|
|
|
|
|
|
|
// 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 => {
|
|
|
|
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);
|
|
|
|
const instrumentedFilePath = `${coverageDir}/contracts/${path.basename(file)}`
|
|
|
|
|
|
|
|
fs.writeFileSync(instrumentedFilePath, instrumentedContractInfo.contract);
|
|
|
|
coverage.addContract(instrumentedContractInfo, canonicalContractPath);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} catch (err) {
|
|
|
|
cleanUp(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 = `./../${modulesDir}/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 {
|
|
|
|
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);
|
|
|
|
shell.exec(istanbul);
|
|
|
|
|
|
|
|
} 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);
|
|
|
|
}
|
|
|
|
}
|