Working truffle plugin draft w/ integration tests

pull/381/head
cgewecke 5 years ago
parent 29e368b3bf
commit 5616f3c455
  1. 119
      dist/truffle.plugin.js
  2. 233
      lib/app.js
  3. 30
      lib/collector.js
  4. 40
      lib/coverage.js
  5. 2
      lib/instrumenter.js
  6. 9
      package.json
  7. 3
      temp/.solcover.js
  8. 14
      temp/assets/SimpleError.sol
  9. 1
      temp/assets/asset.js
  10. 25
      temp/contracts/Migrations.sol
  11. 0
      temp/contracts/SimpleError.sol
  12. 4
      temp/migrations/1_initial.js
  13. 4
      temp/migrations/2_deploy.js
  14. 10
      temp/test/simple.js
  15. 86
      temp/truffle-config.js
  16. 2
      test/integration/truffle/contracts/Migrations.sol
  17. 4
      test/integration/truffle/migrations/1_initial.js
  18. 5
      test/sources/js/block-gas-limit.js
  19. 7
      test/sources/js/oraclize.js
  20. 17
      test/sources/js/requires-externally.js
  21. 31
      test/sources/js/sign.js
  22. 15
      test/sources/js/simple.js
  23. 11
      test/sources/js/testrpc-options.js
  24. 0
      test/sources/solidity/contracts/app/Oraclize.sol
  25. 6
      test/sources/solidity/contracts/app/SimpleError.sol
  26. 286
      test/units/app.js
  27. 25
      test/units/assert.js
  28. 22
      test/units/function.js
  29. 2
      test/units/if.js
  30. 2
      test/units/loops.js
  31. 4
      test/units/statements.js
  32. 172
      test/util/integration.truffle.js
  33. 121
      test/util/mock.truffle.js
  34. 74
      test/util/util.js
  35. 4213
      yarn.lock

@ -27,71 +27,128 @@
const App = require('./../lib/app'); const App = require('./../lib/app');
const req = require('req-cwd'); const req = require('req-cwd');
const path = require('path');
module.exports = async (truffleConfig) => const dir = require('node-dir');
const Web3 = require('web3');
const util = require('util');
const ganache = require('ganache-core-sc');
module.exports = async function(truffleConfig){
let app;
let error; let error;
let testsErrored = false;
try { try {
// Load truffle lib & coverage config // Load truffle lib & coverage config
const truffle = loadTruffleLibrary(); const truffle = loadTruffleLibrary();
const coverageConfig = req.silent('./.solcover.js') || {};
const coverageConfigPath = path.join(truffleConfig.working_directory, '.solcover.js');
const coverageConfig = req.silent(coverageConfigPath) || {};
coverageConfig.cwd = truffleConfig.working_directory;
coverageConfig.contractsDir = truffleConfig.contracts_directory;
// Start // Start
const app = new App(coverageConfig); app = new App(coverageConfig);
// Write instrumented sources to temp folder // Write instrumented sources to temp folder
app.contractsDirectory = coveragePaths.contracts(truffleConfig, app);
app.generateEnvironment(truffleConfig.contracts_directory, app.contractsDirectory);
app.instrument(); app.instrument();
// Have truffle use temp folders // Ask truffle to use temp folders
truffleConfig.contracts_directory = app.contractsDirectory; truffleConfig.contracts_directory = paths.contracts(app);
truffleConfig.build_directory = coveragePaths.artifacts.root(truffleConfig, app); truffleConfig.build_directory = paths.build(app);
truffleConfig.contracts_build_directory = coveragePaths.artifacts.contracts(truffleConfig, app); truffleConfig.contracts_build_directory = paths.artifacts(truffleConfig, app);
// Compile w/out optimization // Additional config
truffleConfig.compilers.solc.settings.optimization.enabled = false; truffleConfig.all = true;
truffleConfig.test_files = tests(truffleConfig);
truffleConfig.compilers.solc.settings.optimizer.enabled = false;
// Compile
await truffle.contracts.compile(truffleConfig); await truffle.contracts.compile(truffleConfig);
// Launch provider & run tests // Launch in-process provider
truffleConfig.provider = await app.getCoverageProvider(truffle); const networkName = 'soliditycoverage';
const provider = await app.provider(ganache);
const accounts = await (new Web3(provider)).eth.getAccounts();
truffleConfig.provider = provider;
truffleConfig.network = networkName;
truffleConfig.network_id = "*";
truffleConfig.networks[networkName] = {
network_id: truffleConfig.network_id,
provider: truffleConfig.provider,
gas: app.gasLimit,
gasPrice: app.gasPrice,
from: accounts[0]
}
// Run tests
try { try {
await truffle.test.run(truffleConfig) failures = await truffle.test.run(truffleConfig)
} catch (e) { } catch (e) {
error = e; error = e.stack;
app.testsErrored = true;
} }
// Produce report // Run Istanbul
app.generateCoverage(); await app.report();
} catch(e){ } catch(e){
error = e; error = e;
} }
// Finish // Finish
return app.cleanUp(error); await app.cleanUp();
if (error !== undefined) throw new Error(error)
if (failures > 0) throw new Error(`${failures} test(s) failed under coverage.`)
} }
// -------------------------------------- Helpers -------------------------------------------------- // -------------------------------------- Helpers --------------------------------------------------
function tests(truffle){
const regex = /.*\.(js|ts|es|es6|jsx|sol)$/;
const files = dir.files(truffle.test_directory, { sync: true }) || [];
return files.filter(f => f.match(regex) != null);
}
function loadTruffleLibrary(){ function loadTruffleLibrary(){
try { return require("truffle") } catch(err) {}; try { return require("truffle") } catch(err) {};
try { return require("./truffle.library")} catch(err) {}; try { return require("./truffle.library")} catch(err) {};
throw new Error(utils.errors.NO_TRUFFLE_LIB) throw new Error('Missing truffle lib...')
} }
const coveragePaths = { /**
contracts: (t, c) => path.join(path.dirname(t.contracts_directory), c.contractsDirName)), * Functions to generate substitute paths for instrumented contracts and artifacts.
* @type {Object}
artifacts: { */
root: (t, c) => path.join(path.dirname(t.build_directory), c.artifactsDirName), const paths = {
contracts: (t, c) => { // "contracts_directory":
const root = path.join(path.dirname(t.build_directory), c.artifactsDirName); contracts: (app) => {
return path.join(root, path.basename(t.contracts_build_directory)); return path.join(
} app.coverageDir,
app.contractsDirName
)
},
// "build_directory":
build: (app) => {
return path.join(
app.coverageDir,
app.artifactsDirName
)
},
// "contracts_build_directory":
artifacts: (truffle, app) => {
return path.join(
app.coverageDir,
app.artifactsDirName,
path.basename(truffle.contracts_build_directory)
)
} }
} }

@ -2,15 +2,14 @@ const shell = require('shelljs');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const istanbul = require('istanbul'); const istanbul = require('istanbul');
const util = require('util');
const Instrumenter = require('./instrumenter'); const Instrumenter = require('./instrumenter');
const Coverage = require('./coverage'); const Coverage = require('./coverage');
const DataCollector = require('./collector');
const isWin = /^win/.test(process.platform); const isWin = /^win/.test(process.platform);
const gasLimitHex = 0xfffffffffff; // High gas block limit / contract deployment limit
const gasPriceHex = 0x01; // Low gas price
/** /**
* Coverage Runner * Coverage Runner
*/ */
@ -18,73 +17,58 @@ class App {
constructor(config) { constructor(config) {
this.coverage = new Coverage(); this.coverage = new Coverage();
this.instrumenter = new Instrumenter(); this.instrumenter = new Instrumenter();
this.provider = config.provider; this.config = config || {};
// Options // Options
this.silence = ''; // Default log level (passed to shell) this.testsErrored = false;
this.log = config.logger || console.log; // Configurable logging
this.cwd = config.cwd;
this.tempFolderName = '.coverageEnv';
this.contractsDir = config.contractsDir
this.coverageDir = path.join(this.cwd, this.tempFolderName);
this.contractsDirName = 'contracts';
this.artifactsDirName = 'artifacts';
this.client = config.provider;
this.providerOptions = config.providerOptions || {};
// Other
this.testsErrored = false; // Toggle true when tests error
this.skippedFolders = []; this.skippedFolders = [];
this.skipFiles = config.skipFiles || [];
// Config this.log = config.logger || console.log;
this.config = config || {};
this.contractsDir = config.contractsDir || 'contracts';
this.coverageDir = './.coverageEnv'; // Contracts dir of instrumented .sols
this.workingDir = config.dir || '.'; // Relative path to contracts folder
this.skipFiles = config.skipFiles || []; // Files to exclude from instrumentation
this.setLoggingLevel(config.silent); this.setLoggingLevel(config.silent);
}
// -------------------------------------- Methods ------------------------------------------------ this.gasLimit = 0xfffffffffff;
this.gasLimitString = "0xfffffffffff";
this.gasPrice = 0x01;
}
// -------------------------------------- Methods -----------------------------------------------
/** /**
* Generates a copy of the target project configured for solidity-coverage and saves to * Setup temp folder, write instrumented contracts to it and register them as coverage targets
* the coverage environment folder.
*/ */
generateCoverageEnvironment() { instrument() {
this.log('Generating coverage environment'); let currentFile;
try { try {
this.sanityCheckContext(); this.sanityCheckContext();
this.identifySkippedFolders(); this.registerSkippedItems();
this.generateEnvelope();
shell.mkdir(this.coverageDir);
shell.cp('-Rf', this.contractsDir, this.coverageDir) const target = `${this.coverageDir}/**/*.sol`;
} catch (err) { shell.ls(target).forEach(file => {
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 to a temp folder which will be the new 'contractsDir for tests'
* + Add instrumentation info to the coverage map
*/
instrumentTarget() {
this.skipFiles = this.skipFiles.map(contract => `${this.coverageDir}/${contract}`);
this.skipFiles.push(`${this.coverageDir}/Migrations.sol`);
let currentFile;
try {
shell.ls(`${this.coverageDir}/**/*.sol`).forEach(file => {
currentFile = file; currentFile = file;
if (!this.skipFiles.includes(file) && !this.inSkippedFolder(file)) { if (!this.shouldSkip(file)) {
this.log('Instrumenting ', file); this.log('Instrumenting ', file);
// Remember the real path // Remember the real path
const contractPath = this.platformNeutralPath(file); const contractPath = this.platformNeutralPath(file);
const working = this.workingDir.substring(1); const canonicalPath = path.join(
const canonicalPath = contractPath.split('/coverageEnv').join(working); this.cwd,
contractPath.split(`/${this.tempFolderName}`)[1]
);
// Instrument contract, save, add to coverage map // Instrument contract, save, add to coverage map
const contract = this.loadContract(contractPath); const contract = this.loadContract(contractPath);
@ -100,26 +84,39 @@ class App {
const msg = `There was a problem instrumenting ${currentFile}: `; const msg = `There was a problem instrumenting ${currentFile}: `;
this.cleanUp(msg + err); this.cleanUp(msg + err);
} }
}
/**
* Launch an in-process ethereum provider and hook up the DataCollector to its VM.
* @param {Object} client ethereum client
* @return {Object} provider
*
* TODO: generalize provider options setting for non-ganache clients..
*/
provider(client){
if(!this.client) this.client = client;
this.collector = new DataCollector(this.instrumenter.instrumentationData);
this.collector = new DataCollector( this.providerOptions.gasLimit = this.gasLimitString;
this.provider, this.providerOptions.allowUnlimitedContractSize = true;
this.instrumenter.intrumentationData this.providerOptions.logger = { log: this.collector.step.bind(this.collector) };
)
this.provider = this.client.provider(this.providerOptions);
return this.provider;
} }
/** /**
* Generate coverage / write coverage report / run istanbul * Generate coverage / write coverage report / run istanbul
*/ */
async generateReport() { async report() {
const collector = new istanbul.Collector(); const collector = new istanbul.Collector();
const reporter = new istanbul.Reporter(); const reporter = new istanbul.Reporter();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const contractsPath = `${this.workingDir}/${this.config.contractsDir}` this.coverage.generate(this.instrumenter.instrumentationData, this.contractsDir);
this.coverage.generate(this.instrumenter.instrumentationData, contractsPath); const relativeMapping = this.makeKeysRelative(this.coverage.data, this.cwd);
const relativeMapping = this.makeKeysRelative(this.coverage.data, this.workingDir);
this.saveCoverage(relativeMapping); this.saveCoverage(relativeMapping);
collector.add(relativeMapping); collector.add(relativeMapping);
@ -129,18 +126,39 @@ class App {
reporter.write(collector, true, () => { reporter.write(collector, true, () => {
this.log('Istanbul coverage reports generated'); this.log('Istanbul coverage reports generated');
this.cleanUp();
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';
console.log(err.stack); console.log(err.stack);
this.cleanUp(msg + err); throw new Error(msg + err);
} }
}); });
} }
/**
* Removes coverage build artifacts, kills testrpc.
* Exits (1) and prints msg on error, exits (0) otherwise.
* @param {String} err error message
*
* TODO this needs to delegate process exit to the outer tool....
*/
async cleanUp(err) {
const self = this;
this.log('Cleaning up...');
shell.config.silent = true;
shell.rm('-Rf', this.coverageDir);
if (this.provider && this.provider.close){
this.log('Shutting down ganache-core')
return new Promise(res => self.provider.close(res))
}
}
// ------------------------------------------ Utils ---------------------------------------------- // ------------------------------------------ Utils ----------------------------------------------
// ========
// File I/O
// ========
loadContract(_path){ loadContract(_path){
return fs.readFileSync(_path).toString(); return fs.readFileSync(_path).toString();
} }
@ -149,29 +167,41 @@ class App {
fs.writeFileSync(_path, contract); fs.writeFileSync(_path, contract);
} }
saveCoverage(coverageObject){ saveCoverage(data){
fs.writeFileSync('./coverage.json', JSON.stringify(coverageObject)); fs.writeFileSync('./coverage.json', JSON.stringify(data));
} }
sanityCheckContext(){ // ======
if (!shell.test('-e', `${this.workingDir}/contracts`)){ // Launch
// ======
sanityCheckContext(contractsDir){
if (!shell.test('-e', this.contractsDir)){
this.cleanUp("Couldn't find a 'contracts' folder to instrument."); this.cleanUp("Couldn't find a 'contracts' folder to instrument.");
} }
if (shell.test('-e', `${this.workingDir}/${this.coverageDir}`)){ if (shell.test('-e', path.join(this.cwd, this.coverageDir))){
shell.rm('-Rf', this.coverageDir); shell.rm('-Rf', this.coverageDir);
} }
} }
generateEnvelope(){
shell.mkdir(this.coverageDir);
shell.mkdir(path.join(this.coverageDir, this.artifactsDirName))
shell.cp('-Rf', this.contractsDir, this.coverageDir)
}
// =====
// Paths
// =====
/** /**
* Relativizes path keys so that istanbul report can be read on Windows * Relativizes path keys so that istanbul report can be read on Windows
* @param {Object} map coverage map generated by coverageMap * @param {Object} map coverage map generated by coverageMap
* @param {[type]} root working directory * @param {String} wd working directory
* @return {[type]} map with relativized keys * @return {Object} map with relativized keys
*/ */
makeKeysRelative(map, root) { makeKeysRelative(map, wd) {
const newCoverage = {}; const newCoverage = {};
Object.keys(map).forEach(pathKey => newCoverage[path.relative(root, pathKey)] = map[pathKey]); Object.keys(map).forEach(pathKey => newCoverage[path.relative(wd, pathKey)] = map[pathKey]);
return newCoverage; return newCoverage;
} }
@ -186,6 +216,9 @@ class App {
: path.resolve(file); : path.resolve(file);
} }
// ========
// Skipping
// ========
/** /**
* Determines if a file is in a folder marked skippable. * Determines if a file is in a folder marked skippable.
* @param {String} file file path * @param {String} file file path
@ -193,8 +226,9 @@ class App {
*/ */
inSkippedFolder(file){ inSkippedFolder(file){
let shouldSkip; let shouldSkip;
const root = `${this.coverageDir}/${this.contractsDirName}`;
this.skippedFolders.forEach(folderToSkip => { this.skippedFolders.forEach(folderToSkip => {
folderToSkip = `${this.coverageDir}/contracts/${folderToSkip}`; folderToSkip = `${root}/${folderToSkip}`;
if (file.indexOf(folderToSkip) === 0) if (file.indexOf(folderToSkip) === 0)
shouldSkip = true; shouldSkip = true;
}); });
@ -202,57 +236,38 @@ class App {
} }
/** /**
* Helper for parsing the skipFiles option, which also accepts folders. * Parses the skipFiles option (which also accepts folders)
*/ */
identifySkippedFolders(){ registerSkippedItems(){
let files = shell.ls('-A', this.workingDir); const root = `${this.coverageDir}/${this.contractsDirName}`;
this.skippedFolders = this.skipFiles.filter(item => path.extname(item) !== '.sol')
this.skipFiles.forEach(item => { this.skipFiles = this.skipFiles.map(contract => `${root}/${contract}`);
if (path.extname(item) !== '.sol') this.skipFiles.push(`${root}/Migrations.sol`);
this.skippedFolders.push(item);
});
} }
/** /**
* Allows config to turn logging off (for CI) * Returns true when file should not be instrumented, false otherwise
* @param {Boolean} isSilent * @param {String} file path segment
* @return {Boolean}
*/ */
setLoggingLevel(isSilent) { shouldSkip(file){
if (isSilent) { return this.skipFiles.includes(file) || this.inSkippedFolder(file)
this.silence = '> /dev/null 2>&1';
this.log = () => {};
}
} }
// =======
// Logging
// =======
/** /**
* Removes coverage build artifacts, kills testrpc. * Turn logging off (for CI)
* Exits (1) and prints msg on error, exits (0) otherwise. * @param {Boolean} isSilent
* @param {String} err error message *
* TODO: logic to toggle on/off (instead of just off)
* *
* TODO this needs to delegate process exit to the outer tool....
*/ */
cleanUp(err) { setLoggingLevel(isSilent) {
const self = this; if (isSilent) this.log = () => {};
function exit(err){
if (err) {
self.log(`${err}\nExiting without generating coverage...`);
process.exit(1);
} else if (self.testsErrored) {
self.log('Some truffle tests failed while running coverage');
process.exit(1);
} else {
self.log('Done.');
process.exit(0);
}
} }
self.log('Cleaning up...');
shell.config.silent = true;
shell.rm('-Rf', self.coverageDir);
exit(err);
}
} }
module.exports = App; module.exports = App;

@ -1,39 +1,37 @@
const web3Utils = require('web3-utils') const web3Utils = require('web3-utils')
class DataCollector { class DataCollector {
constructor(config={}, instrumentationData={}){ constructor(instrumentationData={}){
this.instrumentationData = instrumentationData; this.instrumentationData = instrumentationData;
this.vm = config.vmResolver
? config.vmResolver(config.provider)
: this._ganacheVMResolver(config.provider);
this._connect();
} }
// Subscribes to vm.on('step'). step(info){
_connect(){
const self = this; const self = this;
this.vm.on("step", function(info){ if (typeof info !== 'object' || !info.opcode ) return;
if (info.opcode.name.includes("PUSH") && info.stack.length > 0){ if (info.opcode.name.includes("PUSH") && info.stack.length > 0){
const idx = info.stack.length - 1; const idx = info.stack.length - 1;
const hash = web3Utils.toHex(info.stack[idx]).toString(); let hash = web3Utils.toHex(info.stack[idx]).toString();
hash = self._normalizeHash(hash);
if(self.instrumentationData[hash]){ if(self.instrumentationData[hash]){
self.instrumentationData[hash].hits++; self.instrumentationData[hash].hits++;
} }
} }
})
}
_ganacheVMResolver(provider){
return provider.engine.manager.state.blockchain.vm;
} }
_setInstrumentationData(data){ _setInstrumentationData(data){
this.instrumentationData = data; this.instrumentationData = data;
} }
_normalizeHash(hash){
if (hash.length < 66 && hash.length > 52){
hash = hash.slice(2);
while(hash.length < 64) hash = '0' + hash;
hash = '0x' + hash
}
return hash;
}
} }
module.exports = DataCollector; module.exports = DataCollector;

@ -10,27 +10,19 @@ class Coverage {
constructor() { constructor() {
this.data = {}; this.data = {};
this.assertData = {}; this.assertData = {};
this.lineTopics = [];
this.functionTopics = [];
this.branchTopics = [];
this.statementTopics = [];
this.assertPreTopics = [];
this.assertPostTopics = [];
} }
/** /**
* Initializes a coverage map object for contract * Initializes an entry in the coverage map for an instrumented contract. Tracks by
* + instrumented per `info` * its canonical contract path, e.g. *not* by its location in the temp folder.
* + located at `canonicalContractPath` * @param {Object} info 'info = instrumenter.instrument(contract, fileName, true)'
* @param {Object} info `info = getIntrumentedVersion(contract, fileName, true)` * @param {String} contractPath canonical path to contract file
* @param {String} canonicalContractPath path to contract file
* @return {Object} coverage map with all values set to zero
*/ */
addContract(info, canonicalContractPath) { addContract(info, contractPath) {
this.data[canonicalContractPath] = { this.data[contractPath] = {
l: {}, l: {},
path: canonicalContractPath, path: contractPath,
s: {}, s: {},
b: {}, b: {},
f: {}, f: {},
@ -38,29 +30,29 @@ class Coverage {
statementMap: {}, statementMap: {},
branchMap: {}, branchMap: {},
}; };
this.assertData[canonicalContractPath] = { }; this.assertData[contractPath] = { };
info.runnableLines.forEach((item, idx) => { info.runnableLines.forEach((item, idx) => {
this.data[canonicalContractPath].l[info.runnableLines[idx]] = 0; this.data[contractPath].l[info.runnableLines[idx]] = 0;
}); });
this.data[canonicalContractPath].fnMap = info.fnMap; this.data[contractPath].fnMap = info.fnMap;
for (let x = 1; x <= Object.keys(info.fnMap).length; x++) { for (let x = 1; x <= Object.keys(info.fnMap).length; x++) {
this.data[canonicalContractPath].f[x] = 0; this.data[contractPath].f[x] = 0;
} }
this.data[canonicalContractPath].branchMap = info.branchMap; this.data[contractPath].branchMap = info.branchMap;
for (let x = 1; x <= Object.keys(info.branchMap).length; x++) { for (let x = 1; x <= Object.keys(info.branchMap).length; x++) {
this.data[canonicalContractPath].b[x] = [0, 0]; this.data[contractPath].b[x] = [0, 0];
this.assertData[canonicalContractPath][x] = { this.assertData[contractPath][x] = {
preEvents: 0, preEvents: 0,
postEvents: 0, postEvents: 0,
}; };
} }
this.data[canonicalContractPath].statementMap = info.statementMap; this.data[contractPath].statementMap = info.statementMap;
for (let x = 1; x <= Object.keys(info.statementMap).length; x++) { for (let x = 1; x <= Object.keys(info.statementMap).length; x++) {
this.data[canonicalContractPath].s[x] = 0; this.data[contractPath].s[x] = 0;
} }
} }

@ -62,7 +62,7 @@ class Instrumenter {
// First, we run over the original contract to get the source mapping. // First, we run over the original contract to get the source mapping.
let ast = SolidityParser.parse(contract.source, {range: true}); let ast = SolidityParser.parse(contract.source, {range: true});
parse[ast.type](contract, ast); parse[ast.type](contract, ast);
const retValue = JSON.parse(JSON.stringify(contract)); // ????? const retValue = JSON.parse(JSON.stringify(contract)); // Possibly apotropaic.
// Now, we reset almost everything and use the preprocessor to increase our effectiveness. // Now, we reset almost everything and use the preprocessor to increase our effectiveness.
this._initializeCoverageFields(contract); this._initializeCoverageFields(contract);

@ -8,7 +8,7 @@
"test": "test" "test": "test"
}, },
"scripts": { "scripts": {
"test": "mocha test/units --timeout 70000 --no-warnings", "test": "mocha test/units --timeout 70000 --no-warnings --exit",
"test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- test/units --timeout 70000 --exit" "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- test/units --timeout 70000 --exit"
}, },
"homepage": "https://github.com/sc-forks/solidity-coverage", "homepage": "https://github.com/sc-forks/solidity-coverage",
@ -20,21 +20,24 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"death": "^1.1.0", "death": "^1.1.0",
"ganache-cli": "^6.5.0", "ganache-core-sc": "2.7.0-sc.0",
"istanbul": "^0.4.5", "istanbul": "^0.4.5",
"node-dir": "^0.1.17",
"req-cwd": "^1.0.1", "req-cwd": "^1.0.1",
"sha1": "^1.1.1", "sha1": "^1.1.1",
"shelljs": "^0.8.3", "shelljs": "^0.8.3",
"solidity-parser-antlr": "^0.4.7", "solidity-parser-antlr": "^0.4.7",
"web3": "1.2.1",
"web3-utils": "^1.0.0" "web3-utils": "^1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@nomiclabs/buidler": "^1.0.0-beta.8", "@nomiclabs/buidler": "^1.0.0-beta.8",
"@nomiclabs/buidler-truffle5": "^1.0.0-beta.8", "@nomiclabs/buidler-truffle5": "^1.0.0-beta.8",
"@nomiclabs/buidler-web3": "^1.0.0-beta.8", "@nomiclabs/buidler-web3": "^1.0.0-beta.8",
"decache": "^4.5.1",
"mocha": "5.2.0", "mocha": "5.2.0",
"solc": "^0.5.10", "solc": "^0.5.10",
"truffle": "^5.0.31", "truffle": "^5.0.31",
"web3": "1.0.0-beta.37" "truffle-config": "^1.1.18"
} }
} }

@ -0,0 +1,3 @@
module.exports = {
"silent": true
}

@ -0,0 +1,14 @@
// This contract should throw a parse error in instrumentSolidity.js
pragma solidity ^0.5.0;
contract SimpleError {
uint x = 0;
function test(uint val) public {
x = x + val // <-- no semi-colon
}
function getX() public returns (uint){
return x;
}
}

@ -0,0 +1 @@
module.exports = { value: true };

@ -0,0 +1,25 @@
pragma solidity >=0.4.22 <0.6.0;
contract Migrations {
address public owner;
uint public last_completed_migration;
modifier restricted() {
if (msg.sender == owner) { _; }
}
constructor() public {
owner = msg.sender;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
function upgrade(address new_address) public restricted {
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
}
}

@ -0,0 +1,4 @@
const Migrations = artifacts.require('./Migrations.sol');
module.exports = async function(deployer) {
await deployer.deploy(Migrations);
};

@ -0,0 +1,4 @@
const A = artifacts.require("SimpleError");
module.exports = function(deployer) { deployer.deploy(A) };

@ -0,0 +1,10 @@
const Simple = artifacts.require('Simple');
contract('Simple', () => {
it('should set x to 5', async function(){
const simple = await Simple.deployed()
await simple.test(5);
const val = await simple.getX.call();
assert.equal(val.toNumber(), 5);
});
});

@ -0,0 +1,86 @@
module.exports = {
"_deepCopy": [
"compilers"
],
"_values": {
"truffle_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/node_modules",
"working_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage",
"networks": {},
"verboseRpc": false,
"gas": null,
"gasPrice": null,
"from": null,
"confirmations": 0,
"timeoutBlocks": 0,
"production": false,
"skipDryRun": false,
"build": null,
"resolver": null,
"artifactor": null,
"ethpm": {
"ipfs_host": "ipfs.infura.io",
"ipfs_protocol": "https",
"registry": "0x8011df4830b4f696cd81393997e5371b93338878",
"install_provider_uri": "https://ropsten.infura.io/truffle"
},
"compilers": {
"solc": {
"settings": {
"optimizer": {
"enabled": false,
"runs": 200
}
}
},
"vyper": {}
},
"logger": {}
},
"truffle_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/node_modules",
"working_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/temp",
"networks": {
"development": {
"host": "localhost",
"port": 8545,
"network_id": "*"
}
},
"verboseRpc": false,
"build": null,
"resolver": null,
"artifactor": null,
"ethpm": {
"ipfs_host": "ipfs.infura.io",
"ipfs_protocol": "https",
"registry": "0x8011df4830b4f696cd81393997e5371b93338878",
"install_provider_uri": "https://ropsten.infura.io/truffle"
},
"logger": {},
"compilers": {
"solc": {
"version": "0.5.3",
"settings": {
"optimizer": {}
}
}
},
"build_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/temp/build",
"contracts_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/temp/contracts",
"contracts_build_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/temp/build/contracts",
"migrations_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/temp/migrations",
"migrations_file_extension_regexp": {},
"test_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/temp/test",
"test_file_extension_regexp": {},
"example_project_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/node_modules/example",
"network_id": null,
"from": null,
"gas": 6721975,
"gasPrice": 20000000000,
"provider": null,
"confirmations": 0,
"production": false,
"timeoutBlocks": 0,
"mocha": {
"reporter": "dot"
}
}

@ -1,4 +1,4 @@
pragma solidity ^0.5.0; pragma solidity >=0.4.22 <0.6.0;
contract Migrations { contract Migrations {

@ -0,0 +1,4 @@
const Migrations = artifacts.require('./Migrations.sol');
module.exports = async function(deployer) {
await deployer.deploy(Migrations);
};

@ -1,9 +1,10 @@
/* eslint-env node, mocha */
/* global artifacts, contract */
const Expensive = artifacts.require('./Expensive.sol'); const Expensive = artifacts.require('./Expensive.sol');
contract('Expensive', () => { contract('Expensive', () => {
it('should deploy', async () => { it('should deploy', async () => {
const instance = await Expensive.new() const instance = await Expensive.new()
const hash = instance.transactionHash;
const receipt = await web3.eth.getTransactionReceipt(hash);
assert(receipt.gasUsed > 20000000)
}); });
}); });

@ -1,10 +1,9 @@
/* eslint-env node, mocha */
/* global artifacts, contract, assert */
const usingOraclize = artifacts.require('usingOraclize'); const usingOraclize = artifacts.require('usingOraclize');
contract('Nothing', () => { contract('Oraclize', function(accounts){
it('nothing', async () => { it('oraclize', async function(){
const ora = await usingOraclize.new(); const ora = await usingOraclize.new();
await ora.test(); await ora.test();
}); });
}); });

@ -1,17 +0,0 @@
/* eslint-env node, mocha */
/* global artifacts, contract, assert */
const asset = require('../assets/asset.js');
const Simple = artifacts.require('./Simple.sol');
contract('Simple', () => {
it('should be able to require an external asset', () => {
let simple;
return Simple.deployed().then(instance => {
simple = instance;
assert.equal(asset.value, true);
return simple.test(5); // Make sure we generate an event;
});
});
});

@ -1,31 +0,0 @@
/* eslint-env node, mocha */
/* global artifacts, contract, assert */
const ethUtil = require('ethereumjs-util');
const Simple = artifacts.require('./Simple.sol');
contract('Simple', accounts => {
it('should set x to 5', () => {
let simple;
let messageSha3;
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';
messageSha3 = web3.utils.sha3(message);
const signature = web3.eth.sign(messageSha3, accounts[0]);
return signature;
})
.then((signature) => {
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].toLowerCase());
});
});
});

@ -1,13 +1,10 @@
const Simple = artifacts.require('./Simple.sol'); const Simple = artifacts.require('Simple');
contract('Simple', () => { contract('Simple', () => {
it('should set x to 5', () => { it('should set x to 5', async function(){
let simple; const simple = await Simple.deployed()
return Simple.deployed().then(instance => { await simple.test(5);
simple = instance; const val = await simple.getX.call();
return simple.test(5); assert.equal(val.toNumber(), 5);
})
.then(() => simple.getX.call())
.then(val => assert.equal(val.toNumber(), 5));
}); });
}); });

@ -1,12 +1,11 @@
/* eslint-env node, mocha */
/* global artifacts, contract, assert */
const Simple = artifacts.require('./Simple.sol'); 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', () => { it('should load with ~ expected balance', async function(){
assert(accounts[0] === '0xA4860CEDd5143Bd63F347CaB453Bf91425f8404f'); let balance = await web3.eth.getBalance(accounts[0]);
balance = web3.utils.fromWei(balance);
assert(parseInt(balance) >= 776)
}); });
// Generate some coverage so the script doesn't exit(1) because there are no events // Generate some coverage so the script doesn't exit(1) because there are no events

@ -0,0 +1,6 @@
pragma solidity ^0.5.0;
contract Test {
address a;
address a;
}

@ -1,12 +1,14 @@
/* eslint-env node, mocha */
const assert = require('assert'); const assert = require('assert');
const shell = require('shelljs');
const fs = require('fs'); const fs = require('fs');
const childprocess = require('child_process'); const shell = require('shelljs');
const mock = require('../util/mockTruffle.js'); const mock = require('../util/integration.truffle');
const plugin = require('../../dist/truffle.plugin');
// shell.test alias for legibility const util = require('util')
const opts = { compact: false, depth: 5, breakLength: 80 };
// =======
// Helpers
// =======
function pathExists(path) { return shell.test('-e', path); } function pathExists(path) { return shell.test('-e', path); }
function assertCleanInitialState(){ function assertCleanInitialState(){
@ -15,114 +17,73 @@ function assertCleanInitialState(){
} }
function assertCoverageGenerated(){ function assertCoverageGenerated(){
assert(pathExists('./coverage') === true, 'script should gen coverage folder'); assert(pathExists('./coverage') === true, 'should gen coverage folder');
assert(pathExists('./coverage.json') === true, 'script should gen coverage.json'); assert(pathExists('./coverage.json') === true, 'should gen coverage.json');
} }
function assertCoverageNotGenerated(){ function assertCoverageNotGenerated(){
assert(pathExists('./coverage') !== true, 'script should NOT gen coverage folder'); assert(pathExists('./coverage') !== true, 'should NOT gen coverage folder');
assert(pathExists('./coverage.json') !== true, 'script should NOT gen coverage.json'); assert(pathExists('./coverage.json') !== true, 'should NOT gen coverage.json');
} }
function assertExecutionSucceeds(){ function getOutput(){
return JSON.parse(fs.readFileSync('./coverage.json', 'utf8'));
} }
function assertExecutionFails(){ // ========
// Tests
// ========
describe('app', function() {
let truffleConfig;
let solcoverConfig;
} beforeEach(() => {
mock.clean();
truffleConfig = mock.getDefaultTruffleConfig();
solcoverConfig = {};
if (process.env.SILENT)
solcoverConfig.silent = true;
})
describe.skip('app', function() { //afterEach(() => mock.clean());
afterEach(() => mock.remove());
it('simple contract: should generate coverage, cleanup & exit(0)', () => { it('simple contract: should generate coverage, cleanup & exit(0)', async function(){
assertCleanInitialState(); assertCleanInitialState();
// Run script (exits 0); mock.install('Simple', 'simple.js', solcoverConfig);
mock.install('Simple.sol', 'simple.js', config); await plugin(truffleConfig);
shell.exec(script);
assert(shell.error() === null, 'script should not error');
assertCoverageGenerated(); assertCoverageGenerated();
// Coverage should be real. const output = getOutput();
// This test is tightly bound to the function names in Simple.sol const path = Object.keys(output)[0];
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"');
assert(output[path].fnMap['1'].name === 'test', 'coverage.json missing "test"');
assert(output[path].fnMap['2'].name === 'getX', 'coverage.json missing "getX"');
}); });
it('config with testrpc options string: should generate coverage, cleanup & exit(0)', () => { // Truffle test asserts balance is 777 ether
it('config with providerOptions', async function() {
const privateKey = '0x3af46c9ac38ee1f01b05f9915080133f644bf57443f504d339082cb5285ccae4'; solcoverConfig.providerOptions = { default_balance_ether: 777 }
const balance = '0xfffffffffffffff';
const testConfig = Object.assign({}, config);
testConfig.testrpcOptions = `--account="${privateKey},${balance}" --port 8777`;
testConfig.dir = './mock';
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');
mock.install('Simple', 'testrpc-options.js', solcoverConfig);
await plugin(truffleConfig);
}); });
it('config with test command options string: should run test', () => { it('large contract with many unbracketed statements (time check)', async function() {
assert(pathExists('./allFiredEvents') === false, 'should start without: events log');
const testConfig = Object.assign({}, config);
testConfig.testCommand = 'mocha --timeout 5000';
testConfig.dir = './mock';
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');
});
it('Oraclize @ solc v.0.4.24 (large, many unbracketed statements)', () => {
const trufflejs =
`module.exports = {
networks: {
coverage: {
host: "localhost",
network_id: "*",
port: 8555,
gas: 0xfffffffffff,
gasPrice: 0x01
},
},
compilers: {
solc: {
version: "0.4.24",
}
}
};`;
assertCleanInitialState(); assertCleanInitialState();
// Run script (exits 0); truffleConfig.compilers.solc.version = "0.4.24";
mock.install('Oraclize.sol', 'oraclize.js', config, trufflejs, null, true);
shell.exec(script);
assert(shell.error() === null, 'script should not error');
mock.install('Oraclize', 'oraclize.js', solcoverConfig, truffleConfig, true);
await plugin(truffleConfig);
}); });
it('tests use pure and view modifiers, including with libraries', () => { it.skip('with pure and view modifiers and libraries', () => {
assertCleanInitialState(); assertCleanInitialState();
// Run script (exits 0); mock.installDouble(config);
mock.installLibraryTest(config);
shell.exec(script); shell.exec(script);
assert(shell.error() === null, 'script should not error'); assert(shell.error() === null, 'script should not error');
@ -132,139 +93,122 @@ describe.skip('app', function() {
// This test is tightly bound to the function names in TotallyPure.sol // This test is tightly bound to the function names in TotallyPure.sol
const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8')); const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8'));
const path = Object.keys(produced)[0]; const path = Object.keys(produced)[0];
assert(produced[path].fnMap['1'].name === 'usesThem', 'coverage.json should map "usesThem"'); assert(produced[path].fnMap['1'].name === 'usesThem', 'should map "usesThem"');
assert(produced[path].fnMap['2'].name === 'isPure', 'coverage.json should map "getX"'); assert(produced[path].fnMap['2'].name === 'isPure', 'should map "getX"');
}); });
it('tests require assets outside of test folder: should generate coverage, cleanup & exit(0)', () => { it('contract only uses ".call"', async function(){
assertCleanInitialState(); assertCleanInitialState();
// Run script (exits 0); mock.install('OnlyCall', 'only-call.js', solcoverConfig);
mock.install('Simple.sol', 'requires-externally.js', config); await plugin(truffleConfig);
shell.exec(script);
assert(shell.error() === null, 'script should not error');
assertCoverageGenerated(); assertCoverageGenerated();
// Coverage should be real. const output = getOutput();
// This test is tightly bound to the function names in Simple.sol const path = Object.keys(output)[0];
const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8')); assert(output[path].fnMap['1'].name === 'addTwo', 'cov should map "addTwo"');
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"');
}); });
it('contract only uses .call: should generate coverage, cleanup & exit(0)', () => { it('contract sends / transfers to instrumented fallback', async function(){
assertCleanInitialState(); assertCleanInitialState();
mock.install('OnlyCall.sol', 'only-call.js', config); mock.install('Wallet', 'wallet.js', solcoverConfig);
await plugin(truffleConfig);
shell.exec(script);
assert(shell.error() === null, 'script should not error');
assertCoverageGenerated(); assertCoverageGenerated();
const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8')); const output = getOutput();
const path = Object.keys(produced)[0]; const path = Object.keys(output)[0];
assert(produced[path].fnMap['1'].name === 'addTwo', 'coverage.json should map "addTwo"'); assert(output[path].fnMap['1'].name === 'transferPayment', 'cov should map "transferPayment"');
}); });
it('contract sends / transfers to instrumented fallback: coverage, cleanup & exit(0)', () => { it('contracts are skipped', async function() {
assertCleanInitialState(); assertCleanInitialState();
mock.install('Wallet.sol', 'wallet.js', config); solcoverConfig.skipFiles = ['Owned.sol'];
shell.exec(script);
assert(shell.error() === null, 'script should not error');
assertCoverageGenerated(); mock.installDouble(['Proxy', 'Owned'], 'inheritance.js', solcoverConfig);
await plugin(truffleConfig);
const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8')); assertCoverageGenerated();
const path = Object.keys(produced)[0];
assert(produced[path].fnMap['1'].name === 'transferPayment', 'should map "transferPayment"');
const output = getOutput();
const firstKey = Object.keys(output)[0];
assert(Object.keys(output).length === 1, 'Wrong # of contracts covered');
assert(firstKey.substr(firstKey.length - 9) === 'Proxy.sol', 'Wrong contract covered');
}); });
it('contract uses inheritance: should generate coverage, cleanup & exit(0)', () => { it('contract uses inheritance', async function() {
assertCleanInitialState(); assertCleanInitialState();
mock.installInheritanceTest(config); mock.installDouble(['Proxy', 'Owned'], 'inheritance.js', solcoverConfig);
shell.exec(script); await plugin(truffleConfig);
assert(shell.error() === null, 'script should not error');
assertCoverageGenerated(); assertCoverageGenerated();
const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8')); const output = getOutput();
const ownedPath = Object.keys(produced)[0]; const ownedPath = Object.keys(output)[0];
const proxyPath = Object.keys(produced)[1]; const proxyPath = Object.keys(output)[1];
assert(produced[ownedPath].fnMap['1'].name === 'constructor', 'coverage.json should map "constructor"'); assert(output[ownedPath].fnMap['1'].name === 'constructor', '"constructor" not covered');
assert(produced[proxyPath].fnMap['1'].name === 'isOwner', 'coverage.json should map "isOwner"'); assert(output[proxyPath].fnMap['1'].name === 'isOwner', '"isOwner" not covered');
}); });
it('contracts are skipped: should generate coverage, cleanup & exit(0)', () => { // Simple.sol with a failing assertion in a truffle test
it('truffle tests failing', async function() {
assertCleanInitialState(); assertCleanInitialState();
const testConfig = Object.assign({}, config); mock.install('Simple', 'truffle-test-fail.js', solcoverConfig);
testConfig.skipFiles = ['Owned.sol']; try {
mock.installInheritanceTest(testConfig); await plugin(truffleConfig);
assert.fail()
shell.exec(script); } catch(err){
assert(shell.error() === null, 'script should not error'); assert(err.message.includes('failed under coverage'));
}
assertCoverageGenerated(); assertCoverageGenerated();
const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8')); const output = getOutput();
const firstKey = Object.keys(produced)[0]; const path = Object.keys(output)[0];
assert(Object.keys(produced).length === 1, 'coverage.json should only contain instrumentation for one contract');
assert(firstKey.substr(firstKey.length - 9) === 'Proxy.sol', 'coverage.json should only contain instrumentation for Proxy.sol');
assert(output[path].fnMap['1'].name === 'test', 'cov missing "test"');
assert(output[path].fnMap['2'].name === 'getX', 'cov missing "getX"');
}); });
it('truffle tests failing: should generate coverage, cleanup & exit(1)', () => { // Truffle test asserts deployment cost is greater than 20,000,000 gas
assertCleanInitialState(); it('deployment cost > block gasLimit', async function() {
mock.install('Expensive', 'block-gas-limit.js', solcoverConfig);
// Run with Simple.sol and a failing assertion in a truffle test await plugin(truffleConfig);
mock.install('Simple.sol', 'truffle-test-fail.js', config);
shell.exec(script);
assert(shell.error() !== null, 'script should exit 1');
assertCoverageGenerated();
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"');
}); });
it('deployment cost > block gasLimit: should generate coverage, cleanup & exit(0)', () => { // Truffle test contains syntax error
// Just making sure Expensive.sol compiles and deploys here. it('truffle crashes', async function() {
mock.install('Expensive.sol', 'block-gas-limit.js', config); assertCleanInitialState();
shell.exec(script);
assert(shell.error() === null, 'script should not error');
mock.install('Simple', 'truffle-crash.js', solcoverConfig);
try {
await plugin(truffleConfig);
assert.fail()
} catch(err){
assert(err.message.includes('SyntaxError'));
}
}); });
it('truffle crashes: should generate NO coverage, cleanup and exit(1)', () => { // Solidity syntax errors
it('compilation failure', async function(){
assertCleanInitialState(); assertCleanInitialState();
// Run with Simple.sol and a syntax error in the truffle test mock.install('SimpleError', 'simple.js', solcoverConfig);
mock.install('Simple.sol', 'truffle-crash.js', config);
shell.exec(script);
assert(shell.error() !== null, 'script should error');
assertCoverageNotGenerated();
});
it('instrumentation errors: should generate NO coverage, cleanup and exit(1)', () => { try {
assertCleanInitialState(); await plugin(truffleConfig);
assert.fail()
} catch(err){
assert(err.message.includes('Compilation failed'));
}
// Run with SimpleError.sol (has syntax error) and working truffle test
mock.install('SimpleError.sol', 'simple.js', config);
shell.exec(script);
assert(shell.error() !== null, 'script should error');
assertCoverageNotGenerated(); assertCoverageNotGenerated();
}); });

@ -1,7 +1,7 @@
const assert = require('assert'); const assert = require('assert');
const util = require('./../util/util.js'); const util = require('./../util/util.js');
const ganache = require('ganache-cli'); const ganache = require('ganache-core-sc');
const Coverage = require('./../../lib/coverage'); const Coverage = require('./../../lib/coverage');
describe('asserts and requires', () => { describe('asserts and requires', () => {
@ -9,7 +9,7 @@ describe('asserts and requires', () => {
let provider; let provider;
let collector; let collector;
before(async () => ({ provider, collector } = await util.initializeProvider(ganache))); before(() => ({ provider, collector } = util.initializeProvider(ganache)));
beforeEach(() => coverage = new Coverage()); beforeEach(() => coverage = new Coverage());
after((done) => provider.close(done)); after((done) => provider.close(done));
@ -33,6 +33,8 @@ describe('asserts and requires', () => {
}); });
}); });
// NB: Truffle replays failing txs as .calls to obtain the revert reason from the return
// data. Hence the 2X measurements.
it('should cover assert statements as `if` statements when they fail', async function() { it('should cover assert statements as `if` statements when they fail', async function() {
const contract = await util.bootstrapCoverage('assert/Assert', provider, collector); const contract = await util.bootstrapCoverage('assert/Assert', provider, collector);
coverage.addContract(contract.instrumented, util.filePath); coverage.addContract(contract.instrumented, util.filePath);
@ -40,18 +42,17 @@ describe('asserts and requires', () => {
try { await contract.instance.a(false) } catch(err) { /* Invalid opcode */ } try { await contract.instance.a(false) } catch(err) { /* Invalid opcode */ }
const mapping = coverage.generate(contract.data, util.pathPrefix); const mapping = coverage.generate(contract.data, util.pathPrefix);
assert.deepEqual(mapping[util.filePath].l, { assert.deepEqual(mapping[util.filePath].l, {
5: 1, 5: 2,
}); });
assert.deepEqual(mapping[util.filePath].b, { assert.deepEqual(mapping[util.filePath].b, {
1: [0, 1], 1: [0, 2],
}); });
assert.deepEqual(mapping[util.filePath].s, { assert.deepEqual(mapping[util.filePath].s, {
1: 1, 1: 2,
}); });
assert.deepEqual(mapping[util.filePath].f, { assert.deepEqual(mapping[util.filePath].f, {
1: 1, 1: 2,
}); });
}); });
@ -75,6 +76,8 @@ describe('asserts and requires', () => {
}); });
}); });
// NB: Truffle replays failing txs as .calls to obtain the revert reason from the return
// data. Hence the 2X measurements.
it('should cover multi-line require stmts as `if` statements when they fail', async function() { it('should cover multi-line require stmts as `if` statements when they fail', async function() {
const contract = await util.bootstrapCoverage('assert/RequireMultiline', provider, collector); const contract = await util.bootstrapCoverage('assert/RequireMultiline', provider, collector);
coverage.addContract(contract.instrumented, util.filePath); coverage.addContract(contract.instrumented, util.filePath);
@ -84,16 +87,16 @@ describe('asserts and requires', () => {
const mapping = coverage.generate(contract.data, util.pathPrefix); const mapping = coverage.generate(contract.data, util.pathPrefix);
assert.deepEqual(mapping[util.filePath].l, { assert.deepEqual(mapping[util.filePath].l, {
5: 1, 5: 2,
}); });
assert.deepEqual(mapping[util.filePath].b, { assert.deepEqual(mapping[util.filePath].b, {
1: [0, 1], 1: [0, 2],
}); });
assert.deepEqual(mapping[util.filePath].s, { assert.deepEqual(mapping[util.filePath].s, {
1: 1, 1: 2,
}); });
assert.deepEqual(mapping[util.filePath].f, { assert.deepEqual(mapping[util.filePath].f, {
1: 1, 1: 2,
}); });
}); });
}); });

@ -1,7 +1,7 @@
const assert = require('assert'); const assert = require('assert');
const util = require('./../util/util.js'); const util = require('./../util/util.js');
const ganache = require('ganache-cli'); const ganache = require('ganache-core-sc');
const Coverage = require('./../../lib/coverage'); const Coverage = require('./../../lib/coverage');
describe('function declarations', () => { describe('function declarations', () => {
@ -112,7 +112,9 @@ describe('function declarations', () => {
// We try and call a contract at an address where it doesn't exist and the VM // We try and call a contract at an address where it doesn't exist and the VM
// throws, but we can verify line / statement / fn coverage is getting mapped. // throws, but we can verify line / statement / fn coverage is getting mapped.
it('should cover a constructor call that chains to a method call', async function() { //
// NB: 2x values are result of Truffle replaying failing txs to get reason string...
it('should cover a constructor --> method call chain', async function() {
const contract = await util.bootstrapCoverage('function/chainable', provider, collector); const contract = await util.bootstrapCoverage('function/chainable', provider, collector);
coverage.addContract(contract.instrumented, util.filePath); coverage.addContract(contract.instrumented, util.filePath);
@ -121,21 +123,23 @@ describe('function declarations', () => {
const mapping = coverage.generate(contract.data, util.pathPrefix); const mapping = coverage.generate(contract.data, util.pathPrefix);
assert.deepEqual(mapping[util.filePath].l, { assert.deepEqual(mapping[util.filePath].l, {
9: 1, 9: 2,
}); });
assert.deepEqual(mapping[util.filePath].b, {}); assert.deepEqual(mapping[util.filePath].b, {});
assert.deepEqual(mapping[util.filePath].s, { assert.deepEqual(mapping[util.filePath].s, {
1: 1, 1: 2,
}); });
assert.deepEqual(mapping[util.filePath].f, { assert.deepEqual(mapping[util.filePath].f, {
1: 0, 1: 0,
2: 1, 2: 2,
}); });
}); });
// The vm runs out of gas here - but we can verify line / statement / fn // The vm runs out of gas here - but we can verify line / statement / fn
// coverage is getting mapped. // coverage is getting mapped.
it('should cover a constructor call that chains to a method call', async function() { //
// NB: 2x values are result of Truffle replaying failing txs to get reason string...
it('should cover a constructor --> method --> value call chain', async function() {
const contract = await util.bootstrapCoverage('function/chainable-value', provider, collector); const contract = await util.bootstrapCoverage('function/chainable-value', provider, collector);
coverage.addContract(contract.instrumented, util.filePath); coverage.addContract(contract.instrumented, util.filePath);
@ -144,15 +148,15 @@ describe('function declarations', () => {
const mapping = coverage.generate(contract.data, util.pathPrefix); const mapping = coverage.generate(contract.data, util.pathPrefix);
assert.deepEqual(mapping[util.filePath].l, { assert.deepEqual(mapping[util.filePath].l, {
10: 1, 10: 2,
}); });
assert.deepEqual(mapping[util.filePath].b, {}); assert.deepEqual(mapping[util.filePath].b, {});
assert.deepEqual(mapping[util.filePath].s, { assert.deepEqual(mapping[util.filePath].s, {
1: 1, 1: 2,
}); });
assert.deepEqual(mapping[util.filePath].f, { assert.deepEqual(mapping[util.filePath].f, {
1: 0, 1: 0,
2: 1, 2: 2,
}); });
}); });
}); });

@ -1,7 +1,7 @@
const assert = require('assert'); const assert = require('assert');
const util = require('./../util/util.js'); const util = require('./../util/util.js');
const ganache = require('ganache-cli'); const ganache = require('ganache-core-sc');
const Coverage = require('./../../lib/coverage'); const Coverage = require('./../../lib/coverage');
describe('if, else, and else if statements', () => { describe('if, else, and else if statements', () => {

@ -1,7 +1,7 @@
const assert = require('assert'); const assert = require('assert');
const util = require('./../util/util.js'); const util = require('./../util/util.js');
const ganache = require('ganache-cli'); const ganache = require('ganache-core-sc');
const Coverage = require('./../../lib/coverage'); const Coverage = require('./../../lib/coverage');
describe('for and while statements', () => { describe('for and while statements', () => {

@ -1,7 +1,7 @@
const assert = require('assert'); const assert = require('assert');
const util = require('./../util/util.js'); const util = require('./../util/util.js');
const ganache = require('ganache-cli'); const ganache = require('ganache-core-sc');
const Coverage = require('./../../lib/coverage'); const Coverage = require('./../../lib/coverage');
describe('generic statements', () => { describe('generic statements', () => {
@ -49,7 +49,7 @@ describe('generic statements', () => {
}); });
it('should NOT pass tests if the contract has a compilation error', () => { it('should NOT pass tests if the contract has a compilation error', () => {
const info = util.instrumentAndCompile('../errors/compilation-error'); const info = util.instrumentAndCompile('app/SimpleError');
try { try {
util.report(output.errors); util.report(output.errors);
assert.fail('failure'); // We shouldn't hit this. assert.fail('failure'); // We shouldn't hit this.

@ -0,0 +1,172 @@
/*
Utilities for generating a mock truffle project to test plugin.
*/
const path = require('path');
const fs = require('fs');
const shell = require('shelljs');
const TruffleConfig = require('truffle-config');
const decache = require('decache');
const temp = './temp';
const truffleConfigName = 'truffle-config.js';
const configPath = `${temp}/.solcover.js`;
const testPath = './test/sources/js/';
const sourcesPath = './test/sources/solidity/contracts/app/';
const migrationPath = `${temp}/migrations/2_deploy.js`;
const templatePath = './test/integration/truffle/*';
function getDefaultTruffleConfig(){
const logger = process.env.SILENT ? { log: () => {} } : console;
const reporter = process.env.SILENT ? 'dot' : 'spec';
const mockwd = path.join(process.cwd(), temp);
const vals = {
working_directory: mockwd,
build_directory: path.join(mockwd, 'build'),
contracts_directory: path.join(mockwd, 'contracts'),
contracts_build_directory: path.join(mockwd, 'build', 'contracts'),
migrations_directory: path.join(mockwd, 'migrations'),
test_directory: path.join(mockwd, 'test'),
logger: logger,
mocha: { reporter: reporter },
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "*"
}
},
compilers: {
solc: {
version: "0.5.3",
settings: { optimizer: {} }
}
}
}
return (new TruffleConfig()).with(vals);
}
function getSolcoverJS(config){
return `module.exports = ${JSON.stringify(config, null, ' ')}`
}
function getTruffleConfigJS(config){
if (config) return `module.exports = ${JSON.stringify(config, null, ' ')}`
return `module.exports = ${JSON.stringify(getDefaultTruffleConfig(), null, ' ')}`
}
function deploySingle(contractName){
return `
const A = artifacts.require("${contractName}");
module.exports = function(deployer) { deployer.deploy(A) };
`;
}
function deployDouble(contractNames){
return `
var A = artifacts.require("${contractNames[0]}");
var B = artifacts.require("${contractNames[1]}");
module.exports = function(deployer) {
deployer.deploy(A);
deployer.link(A, B);
deployer.deploy(B);
};
`;
}
/**
* Installs mock truffle project at ./temp with a single contract
* and test specified by the params.
* @param {String} contract <contractName.sol> located in /test/sources/cli/
* @param {[type]} test <testName.js> located in /test/cli/
*/
function install(
contract,
test,
config,
_truffleConfig,
noMigrations
) {
const configjs = getSolcoverJS(config);
const trufflejs = getTruffleConfigJS(_truffleConfig);
const migration = deploySingle(contract);
// Scaffold
shell.mkdir(temp);
shell.cp('-Rf', templatePath, temp);
// Contract
shell.cp(`${sourcesPath}${contract}.sol`, `${temp}/contracts/${contract}.sol`);
// Migration
if (!noMigrations) fs.writeFileSync(migrationPath, migration);
// Test
shell.cp(`${testPath}${test}`, `${temp}/test/${test}`);
// Configs
fs.writeFileSync(`${temp}/${truffleConfigName}`, trufflejs);
fs.writeFileSync(configPath, configjs);
decache(`${process.cwd()}/${temp}/.solcover.js`);
decache(`${process.cwd()}/${temp}/${truffleConfigName}`);
};
/**
* Installs mock truffle project with two contracts (for inheritance, libraries, etc)
*
*/
function installDouble(contracts, test, config) {
const configjs = getSolcoverJS(config);
const migration = deployDouble(contracts);
// Scaffold
shell.mkdir(temp);
shell.cp('-Rf', templatePath, temp);
// Contracts
contracts.forEach(item => {
shell.cp(`${sourcesPath}${item}.sol`, `${temp}/contracts/${item}.sol`)
});
// Migration
fs.writeFileSync(migrationPath, migration)
// Test
shell.cp(`${testPath}${test}`, `${temp}/test/${test}`);
// Configs
fs.writeFileSync(`${temp}/${truffleConfigName}`, getTruffleConfigJS());
fs.writeFileSync(configPath, configjs);
decache(`${process.cwd()}/${temp}/.solcover.js`);
decache(`${process.cwd()}/${temp}/${truffleConfigName}`);
};
/**
* Removes mock truffle project and coverage reports generated by test
*/
function clean() {
shell.config.silent = true;
shell.rm('-Rf', 'temp');
shell.rm('-Rf', 'coverage');
shell.rm('coverage.json');
shell.config.silent = false;
};
module.exports = {
getDefaultTruffleConfig: getDefaultTruffleConfig,
install: install,
installDouble: installDouble,
clean: clean
}

@ -1,121 +0,0 @@
/*
Utilities for generating a mock truffle project to test plugin.
*/
const fs = require('fs');
const shell = require('shelljs');
const configPath = './mock/.solcover.js';
const testPath = './test/sources/js/';
const sourcesPath = './test/sources/solidity/contracts/app/';
const migrationPath = './mock/migrations/2_deploy.js';
const defaultTruffleConfig = `
module.exports = {
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "*"
}
},
compilers: {
solc: {
version: "0.5.3",
}
}
};
`
/**
* Installs mock truffle project at ./mock with a single contract
* and test specified by the params.
* @param {String} contract <contractName.sol> located in /test/sources/cli/
* @param {[type]} test <testName.js> located in /test/cli/
*/
function install(
contract,
test,
config,
_truffleConfig,
_trufflejsName,
noMigrations
) {
const configjs = `module.exports = ${JSON.stringify(config)}`;
const migration = `
const A = artifacts.require('${contract}');
module.exports = function(deployer) { deployer.deploy(A) };
`;
// Mock truffle-config.js
const trufflejsName = _trufflejsName || 'truffle-config.js';
const trufflejs = _truffleConfig || defaultTruffleConfig;
// Generate mock
shell.mkdir('./mock');
shell.cp('-Rf', './test/integration/truffle', './mock');
shell.cp(`${sourcesPath}${contract}.sol`, `./mock/contracts/${contract}.sol`);
if (!noMigrations){
fs.writeFileSync(migrationPath, migration);
}
fs.writeFileSync(`./mock/${trufflejsName}`, trufflejs);
fs.writeFileSync(`${configPath}`, configjs);
shell.cp(`${testPath}${test}`, `./mock/test/${test}`);
};
/**
* Installs mock truffle project with two contracts (for inheritance, libraries, etc)
* @param {config} .solcover.js configuration
*/
function installMultiple(contracts, test, config) {
const configjs = `module.exports = ${JSON.stringify(config)}`;
const deployContracts = `
var A = artifacts.require(`${contracts[0]}`);
var B = artifacts.require(`${contracts[1]}`);
module.exports = function(deployer) {
deployer.deploy(A);
deployer.link(A, B);
deployer.deploy(B);
};
`;
shell.mkdir('./mock');
shell.cp('-Rf', './test/integration/truffle', './mock');
contracts.forEach(item => {
shell.cp(`${sourcesPath}${item}.sol`, `./mock/contracts/${item}.sol`)
});
shell.cp(`${testPath}${test}`, `./mock/test/${test}`);
fs.writeFileSync('./mock/truffle-config.js', defaultTruffleJs);
fs.writeFileSync('./.solcover.js', configjs);
fs.writeFileSync(migrationPath, migration)
};
/**
* Removes mock truffle project and coverage reports generated by exec.js
*/
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;
};
module.exports = {
install: install,
installMultiple: installMultiple,
remove: remove
}

@ -1,3 +1,7 @@
/**
* Setup and reporting helpers for the suites which test instrumentation
* and coverage correctness. (Integration test helpers are elsewhere)
*/
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const solc = require('solc'); const solc = require('solc');
@ -6,14 +10,15 @@ const TruffleContract = require('truffle-contract');
const Instrumenter = require('./../../lib/instrumenter'); const Instrumenter = require('./../../lib/instrumenter');
const DataCollector = require('./../../lib/collector') const DataCollector = require('./../../lib/collector')
// ====================
// Path constants
// ====================
const filePath = path.resolve('./test.sol'); const filePath = path.resolve('./test.sol');
const pathPrefix = './'; const pathPrefix = './';
function getCode(_path) { // ====================
const pathToSources = `./../sources/solidity/contracts/${_path}`; // Contract deployments
return fs.readFileSync(path.join(__dirname, pathToSources), 'utf8'); // ====================
};
function getABI(solcOutput, testFile="test.sol", testName="Test"){ function getABI(solcOutput, testFile="test.sol", testName="Test"){
return solcOutput.contracts[testFile][testName].abi; return solcOutput.contracts[testFile][testName].abi;
} }
@ -30,6 +35,7 @@ async function getDeployedContractInstance(info, provider){
}) })
contract.setProvider(provider); contract.setProvider(provider);
contract.autoGas = false;
const accounts = await contract.web3.eth.getAccounts(); const accounts = await contract.web3.eth.getAccounts();
contract.defaults({ contract.defaults({
@ -41,20 +47,30 @@ async function getDeployedContractInstance(info, provider){
return contract.new(); return contract.new();
} }
// ============
// Compilation
// ============
function getCode(_path) {
const pathToSources = `./../sources/solidity/contracts/${_path}`;
return fs.readFileSync(path.join(__dirname, pathToSources), 'utf8');
};
function compile(source){ function compile(source){
const compilerInput = codeToCompilerInput(source); const compilerInput = codeToCompilerInput(source);
return JSON.parse(solc.compile(compilerInput)); return JSON.parse(solc.compile(compilerInput));
} }
function report(output=[]) { function codeToCompilerInput(code) {
output.forEach(item => { return JSON.stringify({
if (item.severity === 'error') { language: 'Solidity',
const errors = JSON.stringify(output, null, ' '); sources: { 'test.sol': { content: code } },
throw new Error(`Instrumentation fault: ${errors}`); settings: { outputSelection: {'*': { '*': [ '*' ] }} }
}
}); });
} }
// ============================
// Instrumentation Correctness
// ============================
function instrumentAndCompile(sourceName) { function instrumentAndCompile(sourceName) {
const contract = getCode(`${sourceName}.sol`) const contract = getCode(`${sourceName}.sol`)
const instrumenter = new Instrumenter(); const instrumenter = new Instrumenter();
@ -68,14 +84,18 @@ function instrumentAndCompile(sourceName) {
} }
} }
function codeToCompilerInput(code) { function report(output=[]) {
return JSON.stringify({ output.forEach(item => {
language: 'Solidity', if (item.severity === 'error') {
sources: { 'test.sol': { content: code } }, const errors = JSON.stringify(output, null, ' ');
settings: { outputSelection: {'*': { '*': [ '*' ] }} } throw new Error(`Instrumentation fault: ${errors}`);
}
}); });
} }
// =====================
// Coverage Correctness
// =====================
async function bootstrapCoverage(file, provider, collector){ async function bootstrapCoverage(file, provider, collector){
const info = instrumentAndCompile(file); const info = instrumentAndCompile(file);
info.instance = await getDeployedContractInstance(info, provider); info.instance = await getDeployedContractInstance(info, provider);
@ -83,22 +103,18 @@ async function bootstrapCoverage(file, provider, collector){
return info; return info;
} }
async function initializeProvider(ganache){ // =========
const provider = ganache.provider(); // Provider
// =========
return new Promise(resolve => { function initializeProvider(ganache){
const interval = setInterval(() => { const collector = new DataCollector();
const options = { logger: { log: collector.step.bind(collector) }};
if (provider.engine.manager.state.blockchain.vm !== undefined){ const provider = ganache.provider(options);
clearInterval(interval);
resolve({ return {
provider: provider, provider: provider,
collector: new DataCollector({provider: provider}) collector: collector
});
} }
});
})
} }
module.exports = { module.exports = {

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save