Route all errors through reporter (#399)

* Decouple app & plugin UIs. Make UI extensible class consumed by plugin.

* Add flags to force loading truffle lib module from different sources (global, plugin)  

* Route all error output through UI Class 

* Add error checking for solcover.js loading 

* Add truffle lib bundle map
pull/400/head
cgewecke 5 years ago committed by GitHub
parent 901430ddf9
commit c25697d5c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .circleci/config.yml
  2. 2
      README.md
  3. 0
      dist/plugin-assets/truffle.library.js
  4. 1
      dist/plugin-assets/truffle.library.js.map
  5. 76
      dist/plugin-assets/truffle.ui.js
  6. 193
      dist/truffle.plugin.js
  7. 144
      lib/app.js
  8. 89
      lib/ui.js
  9. 1
      package.json
  10. 0
      test/integration/projects/.solcover.js
  11. 0
      test/integration/projects/bad-solcover.js
  12. 3
      test/integration/projects/bad-solcoverjs/.solcover.js
  13. 99
      test/integration/projects/bad-solcoverjs/truffle-config.js
  14. 99
      test/integration/projects/no-sources/truffle-config.js
  15. 14
      test/sources/solidity/contracts/app/Unparseable.sol
  16. 105
      test/units/app.js
  17. 1740
      yarn.lock

@ -26,6 +26,10 @@ jobs:
name: Install yarn name: Install yarn
command: | command: |
npm install -g yarn npm install -g yarn
- run:
name: Install truffle (globally)
command: |
npm install -g truffle
- run: - run:
name: Install dependencies name: Install dependencies
command: | command: |

@ -47,6 +47,8 @@ truffle run coverage [options]
|--------------|--------------------------------|-------------| |--------------|--------------------------------|-------------|
| `--file` | `--file="test/registry/*.js"` | Filename or glob describing a subset of JS tests to run. (Globs must be enclosed by quotes.)| | `--file` | `--file="test/registry/*.js"` | Filename or glob describing a subset of JS tests to run. (Globs must be enclosed by quotes.)|
| `--solcoverjs` | `--solcoverjs ./../.solcover.js` | Relative path from working directory to config. Useful for monorepo packages that share settings. (Path must be "./" prefixed) | | `--solcoverjs` | `--solcoverjs ./../.solcover.js` | Relative path from working directory to config. Useful for monorepo packages that share settings. (Path must be "./" prefixed) |
| `--useGlobalTruffle` | | Force use of truffle library module from globally installed truffle. (Default: false)|
| `--usePluginTruffle` | | Force use of truffle library module from plugin provided copy (Truffle v5.0.31.) Requires you have locally installed Truffle V5 <= 5.0.30 (Default: false)|
| `--version` | | Version info | | `--version` | | Version info |
| `--help` | | Usage notes | | `--help` | | Usage notes |
|<img width=250/>|<img width=500/> |<img width=100/>| |<img width=250/>|<img width=500/> |<img width=100/>|

File diff suppressed because one or more lines are too long

@ -0,0 +1,76 @@
const UI = require('./../../lib/ui').UI;
/**
* Truffle Plugin logging
*/
class PluginUI extends UI {
constructor(log){
super(log);
}
/**
* Writes a formatted message via log
* @param {String} kind message selector
* @param {String[]} args info to inject into template
*/
report(kind, args=[]){
const c = this.chalk;
const ct = c.bold.green('>');
const ds = c.bold.yellow('>');
const w = ":warning:";
const kinds = {
'sol-tests': `\n${w} ${c.red("This plugin cannot run Truffle's native solidity tests: ")}`+
`${args[0]} test(s) will be skipped.\n`,
'lib-local': `\n${ct} ${c.grey('Using Truffle library from local node_modules.')}\n`,
'lib-global': `\n${ct} ${c.grey('Using Truffle library from global node_modules.')}\n`,
'lib-warn': `${w} ${c.red('Unable to require Truffle library locally or globally.\n')}`+
`${w} ${c.red('Using fallback Truffle library module instead (v5.0.31)')}\n` +
`${w} ${c.red('Truffle V5 must be a local dependency for fallback to work.')}\n`,
'help': `Usage: truffle run coverage [options]\n\n` +
`Options:\n` +
` --file: path (or glob) to subset of JS test files. (Quote your globs)\n` +
` --solcoverjs: relative path to .solcover.js (ex: ./../.solcover.js)\n` +
` --version: version info\n`,
'truffle-version': `${ct} ${c.bold('truffle')}: v${args[0]}`,
'ganache-version': `${ct} ${c.bold('ganache-core')}: ${args[0]}`,
'coverage-version': `${ct} ${c.bold('solidity-coverage')}: v${args[0]}`,
}
this._write(kinds[kind]);
}
/**
* Returns a formatted message. Useful for error message.
* @param {String} kind message selector
* @param {String[]} args info to inject into template
* @return {String} message
*/
generate(kind, args=[]){
const c = this.chalk;
const kinds = {
'lib-fail': `${c.red('Unable to load plugin copy of Truffle library module. ')}` +
`${c.red('Try installing Truffle >= v5.0.31 locally or globally.\n')}` +
`Caught error message: ${args[0]}\n`,
'solcoverjs-fail': `${c.red('Could not load .solcover.js config file. ')}` +
`${c.red('This can happen if it has a syntax error or ')}` +
`${c.red('the path you specified for it is wrong.')}`,
}
return this._format(kinds[kind])
}
}
module.exports = PluginUI;

@ -1,31 +1,6 @@
/*
TruffleConfig Paths
===========================
build_directory /users/myPath/to/someProject/build
contracts_directory. /users/myPath/to/someProject/contracts
working_directory /users/myPath/to/someProject
contracts_build_directory /users/myPath/to/someProject/build/contracts
Compilation options override
----------------------------
build_directory /users/myPath/to/someProject/.coverageArtifacts
contracts_directory /users/myPath/to/someProject/.coverageContracts
Test options override
---------------------
contracts_directory, /users/myPath/to/someProject/.coverageContracts
contracts_build_directory, /users/myPath/to/someProject/.coverageArtifacts/contracts
provider ganache.provider (async b/c vm must be resolved)
logger add filter for unused variables...
Truffle Lib API
===============
load: const truffle = require("truffle") (or require("sc-truffle"))
compilation: await truffle.contracts.compile(config)
test: await truffle.test.run(config)
*/
const App = require('./../lib/app'); const App = require('./../lib/app');
const PluginUI = require('./plugin-assets/truffle.ui');
const pkg = require('./../package.json'); const pkg = require('./../package.json');
const req = require('req-cwd'); const req = require('req-cwd');
const death = require('death'); const death = require('death');
@ -34,79 +9,78 @@ const dir = require('node-dir');
const Web3 = require('web3'); const Web3 = require('web3');
const util = require('util'); const util = require('util');
const globby = require('globby'); const globby = require('globby');
const shell = require('shelljs');
const globalModules = require('global-modules'); const globalModules = require('global-modules');
/**
* Truffle Plugin: `truffle run coverage [options]`
* @param {Object} truffleConfig @truffle/config config
* @return {Promise}
*/
async function plugin(truffleConfig){ async function plugin(truffleConfig){
let ui;
let app; let app;
let error; let error;
let truffle; let truffle;
let testsErrored = false; let testsErrored = false;
let coverageConfig;
let solcoverjs;
// Load truffle lib, .solcover.js & launch app // This needs it's own try block because this logic
// runs before app.cleanUp is defined.
try { try {
(truffleConfig.solcoverjs) ui = new PluginUI(truffleConfig.logger.log);
? solcoverjs = path.join(truffleConfig.working_directory, truffleConfig.solcoverjs)
: solcoverjs = path.join(truffleConfig.working_directory, '.solcover.js');
coverageConfig = req.silent(solcoverjs) || {};
coverageConfig.cwd = truffleConfig.working_directory;
coverageConfig.originalContractsDir = truffleConfig.contracts_directory;
coverageConfig.log = coverageConfig.log || truffleConfig.logger.log;
app = new App(coverageConfig);
if (truffleConfig.help){ if(truffleConfig.help) return ui.report('help'); // Bail if --help
return app.ui.report('truffle-help')
}
truffle = loadTruffleLibrary(app); truffle = loadTruffleLibrary(ui, truffleConfig);
app = new App(loadSolcoverJS(ui, truffleConfig));
} catch (err) { } catch (err) { throw err }
throw err;
}
// Instrument and test..
try { try {
death(app.cleanUp); // This doesn't work... // Catch interrupt signals
death(app.cleanUp);
// Launch in-process provider // Provider / Server launch
const provider = await app.provider(truffle.ganache); const provider = await app.provider(truffle.ganache);
const web3 = new Web3(provider); const web3 = new Web3(provider);
const accounts = await web3.eth.getAccounts(); const accounts = await web3.eth.getAccounts();
const nodeInfo = await web3.eth.getNodeInfo(); const nodeInfo = await web3.eth.getNodeInfo();
const ganacheVersion = nodeInfo.split('/')[1]; const ganacheVersion = nodeInfo.split('/')[1];
app.ui.report('truffle-version', [truffle.version]); // Version Info
app.ui.report('ganache-version', [ganacheVersion]); ui.report('truffle-version', [truffle.version]);
app.ui.report('coverage-version',[pkg.version]); ui.report('ganache-version', [ganacheVersion]);
ui.report('coverage-version',[pkg.version]);
// Bail early if user ran: --version if (truffleConfig.version) return app.cleanUp(); // Bail if --version
if (truffleConfig.version) return;
// Write instrumented sources to temp folder // Instrument
app.sanityCheckContext();
app.generateStandardEnvironment();
app.instrument(); app.instrument();
// Ask truffle to use temp folders // Filesystem & Compiler Re-configuration
truffleConfig.contracts_directory = app.contractsDir; truffleConfig.contracts_directory = app.contractsDir;
truffleConfig.build_directory = app.artifactsDir; truffleConfig.build_directory = app.artifactsDir;
truffleConfig.contracts_build_directory = paths.artifacts(truffleConfig, app);
// Additional config truffleConfig.contracts_build_directory = path.join(
app.artifactsDir,
path.basename(truffleConfig.contracts_build_directory)
);
truffleConfig.all = true; truffleConfig.all = true;
truffleConfig.test_files = tests(app, truffleConfig); truffleConfig.test_files = getTestFilePaths(ui, truffleConfig);
truffleConfig.compilers.solc.settings.optimizer.enabled = false; truffleConfig.compilers.solc.settings.optimizer.enabled = false;
// Compile // Compile Instrumented Contracts
await truffle.contracts.compile(truffleConfig); await truffle.contracts.compile(truffleConfig);
// Launch in-process provider // Network Re-configuration
const networkName = 'soliditycoverage'; const networkName = 'soliditycoverage';
truffleConfig.network = networkName; truffleConfig.network = networkName;
// Truffle alternately complains that fields are and // Truffle complains that these keys *are not* set when running plugin fn directly.
// are not manually set // But throws saying they *cannot* be manually set when running as truffle command.
try { try {
truffleConfig.network_id = "*"; truffleConfig.network_id = "*";
truffleConfig.provider = provider; truffleConfig.provider = provider;
@ -133,6 +107,7 @@ async function plugin(truffleConfig){
error = e; error = e;
} }
// Finish // Finish
await app.cleanUp(); await app.cleanUp();
@ -142,68 +117,112 @@ async function plugin(truffleConfig){
// -------------------------------------- Helpers -------------------------------------------------- // -------------------------------------- Helpers --------------------------------------------------
function tests(app, truffle){ /**
* Returns a list of test files to pass to mocha.
* @param {Object} ui reporter utility
* @param {Object} truffle truffleConfig
* @return {String[]} list of files to pass to mocha
*/
function getTestFilePaths(ui, truffle){
let target; let target;
// Handle --file <path|glob> cli option (subset of tests)
(typeof truffle.file === 'string') (typeof truffle.file === 'string')
? target = globby.sync([truffle.file]) ? target = globby.sync([truffle.file])
: target = dir.files(truffle.test_directory, { sync: true }) || []; : target = dir.files(truffle.test_directory, { sync: true }) || [];
// Filter native solidity tests and warn that they're skipped
const solregex = /.*\.(sol)$/; const solregex = /.*\.(sol)$/;
const hasSols = target.filter(f => f.match(solregex) != null); const hasSols = target.filter(f => f.match(solregex) != null);
if (hasSols.length > 0) app.ui.report('sol-tests', [hasSols.length]); if (hasSols.length > 0) ui.report('sol-tests', [hasSols.length]);
// Return list of test files
const testregex = /.*\.(js|ts|es|es6|jsx)$/; const testregex = /.*\.(js|ts|es|es6|jsx)$/;
return target.filter(f => f.match(testregex) != null); return target.filter(f => f.match(testregex) != null);
} }
function loadTruffleLibrary(app){
// Case: from local node_modules /**
* Tries to load truffle module library and reports source. User can force use of
* a non-local version using cli flags (see option). Load order is:
*
* 1. local node_modules
* 2. global node_modules
* 3. fail-safe (truffle lib v 5.0.31 at ./plugin-assets/truffle.library)
*
* @param {Object} ui reporter utility
* @param {Object} truffleConfig config
* @return {Module}
*/
function loadTruffleLibrary(ui, truffleConfig){
// Local
try { try {
if (truffleConfig.useGlobalTruffle || truffleConfig.usePluginTruffle) throw null;
const lib = require("truffle"); const lib = require("truffle");
app.ui.report('truffle-local'); ui.report('lib-local');
return lib; return lib;
} catch(err) {}; } catch(err) {};
// Case: global // Global
try { try {
if (truffleConfig.usePluginTruffle) throw null;
const globalTruffle = path.join(globalModules, 'truffle'); const globalTruffle = path.join(globalModules, 'truffle');
const lib = require(globalTruffle); const lib = require(globalTruffle);
app.ui.report('truffle-global'); ui.report('lib-global');
return lib; return lib;
} catch(err) {}; } catch(err) {};
// Default: fallback // Plugin Copy @ v 5.0.31
try { try {
if (truffleConfig.forceLibFailure) throw null; // For err unit testing
app.ui.report('truffle-warn'); ui.report('lib-warn');
return require("./truffle.library")} return require("./plugin-assets/truffle.library")
catch(err) { } catch(err) {
const msg = app.ui.generate('truffle-fail', [err]); const msg = ui.generate('lib-fail', [err]);
throw new Error(msg); throw new Error(msg);
}; };
} }
/** function loadSolcoverJS(ui, truffleConfig){
* Functions to generate substitute paths for instrumented contracts and artifacts. let coverageConfig;
* @type {Object} let solcoverjs;
*/
const paths = {
// "contracts_build_directory": // Handle --solcoverjs flag
artifacts: (truffle, app) => { (truffleConfig.solcoverjs)
return path.join( ? solcoverjs = path.join(truffleConfig.working_directory, truffleConfig.solcoverjs)
app.artifactsDir, : solcoverjs = path.join(truffleConfig.working_directory, '.solcover.js');
path.basename(truffle.contracts_build_directory)
) // Catch solcoverjs syntax errors
if (shell.test('-e', solcoverjs)){
try {
coverageConfig = require(solcoverjs);
} catch(error){
error.message = ui.generate('solcoverjs-fail') + error.message;
throw new Error(error)
}
// Config is optional
} else {
coverageConfig = {};
} }
coverageConfig.log = truffleConfig.logger.log;
coverageConfig.cwd = truffleConfig.working_directory;
coverageConfig.originalContractsDir = truffleConfig.contracts_directory;
return coverageConfig;
} }
module.exports = plugin; module.exports = plugin;

@ -1,4 +1,5 @@
const shell = require('shelljs'); const shell = require('shelljs');
const pify = require('pify');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const istanbul = require('istanbul'); const istanbul = require('istanbul');
@ -8,7 +9,7 @@ const assert = require('assert');
const Instrumenter = require('./instrumenter'); const Instrumenter = require('./instrumenter');
const Coverage = require('./coverage'); const Coverage = require('./coverage');
const DataCollector = require('./collector'); const DataCollector = require('./collector');
const UI = require('./ui'); const AppUI = require('./ui').AppUI;
const isWin = /^win/.test(process.platform); const isWin = /^win/.test(process.platform);
@ -23,6 +24,7 @@ class App {
// Options // Options
this.testsErrored = false; this.testsErrored = false;
this.instrumentToFile = (config.instrumentToFile === false) ? false : true;
this.cwd = config.cwd; this.cwd = config.cwd;
this.contractsDirName = '.coverage_contracts'; this.contractsDirName = '.coverage_contracts';
@ -39,33 +41,36 @@ class App {
this.skipFiles = config.skipFiles || []; this.skipFiles = config.skipFiles || [];
this.log = config.log || console.log; this.log = config.log || console.log;
this.setLoggingLevel(config.silent);
this.gasLimit = 0xfffffffffff; this.gasLimit = 0xfffffffffff;
this.gasLimitString = "0xfffffffffff"; this.gasLimitString = "0xfffffffffff";
this.gasPrice = 0x01; this.gasPrice = 0x01;
this.istanbulReporter = config.istanbulReporter || ['html', 'lcov', 'text']; this.istanbulReporter = config.istanbulReporter || ['html', 'lcov', 'text'];
this.ui = new UI(this.log);
this.setLoggingLevel(config.silent);
this.ui = new AppUI(this.log);
} }
// -------------------------------------- Methods ----------------------------------------------- // -------------------------------------- Methods -----------------------------------------------
/** /**
* Setup temp folder, write instrumented contracts to it and register them as coverage targets * Setup temp folder, write instrumented contracts to it and register them as coverage targets
*/ */
instrument() { instrument(targetFiles=[]) {
let currentFile; let targets;
let currentFile; // Keep track of filename in case we crash...
let started = false; let started = false;
let skipped = []; let skipped = [];
try { try {
this.sanityCheckContext();
this.registerSkippedItems(); this.registerSkippedItems();
this.generateEnvelope();
const target = `${this.contractsDir}/**/*.sol`; (targetFiles.length)
? targets = targetFiles
: targets = shell.ls(`${this.contractsDir}/**/*.sol`);
shell.ls(target).forEach(file => { targets.forEach(file => {
currentFile = file; currentFile = file;
if (!this.shouldSkip(file)) { if (!this.shouldSkip(file)) {
@ -74,7 +79,7 @@ class App {
// Remember the real path // Remember the real path
const contractPath = this.platformNeutralPath(file); const contractPath = this.platformNeutralPath(file);
const relativePath = contractPath.split(`/${this.contractsDirName}`)[1] const relativePath = this.toRelativePath(contractPath, this.contractsDirName);
const canonicalPath = path.join( const canonicalPath = path.join(
this.originalContractsDir, this.originalContractsDir,
relativePath relativePath
@ -93,8 +98,9 @@ class App {
} }
}); });
} catch (err) { } catch (err) {
const msg = `There was a problem instrumenting ${currentFile}: `; const name = this.toRelativePath(currentFile, this.contractsDirName);
this.cleanUp(msg + err); err.message = this.ui.generate('instr-fail', [name]) + err.message;
throw err;
} }
if (skipped.length > 0){ if (skipped.length > 0){
@ -152,43 +158,83 @@ class App {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
this.coverage.generate(this.instrumenter.instrumentationData, this.originalContractsDir); this.coverage.generate(
const relativeMapping = this.makeKeysRelative(this.coverage.data, this.cwd); this.instrumenter.instrumentationData,
this.saveCoverage(relativeMapping); this.originalContractsDir
);
const mapping = this.makeKeysRelative(this.coverage.data, this.cwd);
this.saveCoverage(mapping);
collector.add(relativeMapping); collector.add(mapping);
this.istanbulReporter.forEach(report => reporter.add(report)); this.istanbulReporter.forEach(report => reporter.add(report));
reporter.write(collector, true, () => { // Pify doesn't like this one...
this.log('Istanbul coverage reports generated'); reporter.write(collector, true, (err) => {
if (err) throw err;
this.ui.report('istanbul');
resolve(); resolve();
}); });
} catch (err) {
const msg = 'There was a problem generating the coverage map / running Istanbul.\n'; } catch (error) {
console.log(err.stack); error.message = this.ui.generate('istanbul-fail') + error.message;
throw new Error(msg + err); throw error;
} }
}); })
} }
// ============
// Public Utils
// ============
/** /**
* Removes coverage build artifacts, kills testrpc. * Should only be run before any temporary folders are created.
* Exits (1) and prints msg on error, exits (0) otherwise. * It checks for existence of contract sources, server port conflicts
* @param {String} err error message * and sweeps away debris left over from an uncontrolled crash.
*/
sanityCheckContext(){
if (!shell.test('-e', this.originalContractsDir)){
const msg = this.ui.generate('sources-fail', [this.originalContractsDir])
throw new Error(msg);
}
if (shell.test('-e', this.contractsDir)){
shell.rm('-Rf', this.contractsDir);
}
if (shell.test('-e', this.artifactsDir)){
shell.rm('-Rf', this.artifactsDir);
}
}
/**
* Creates two temporary folders in the cwd and
* copies contract sources to a temp contracts folder prior to their
* instrumentation. This method is useful for plugin APIs that
* consume contracts and build artifacts from configurable locations.
* *
* TODO this needs to delegate process exit to the outer tool.... * .coverage_contracts/
* .coverage_artifacts/
*/ */
async cleanUp(err) { generateStandardEnvironment(){
shell.mkdir(this.contractsDir);
shell.mkdir(this.artifactsDir);
shell.cp('-Rf', `${this.originalContractsDir}/*`, this.contractsDir);
}
/**
* Removes coverage build artifacts, kills testrpc.
*/
async cleanUp() {
const self = this; const self = this;
this.log('Cleaning up...');
shell.config.silent = true; shell.config.silent = true;
shell.rm('-Rf', this.contractsDir); shell.rm('-Rf', this.contractsDir);
shell.rm('-Rf', this.artifactsDir); shell.rm('-Rf', this.artifactsDir);
if (this.provider && this.provider.close){ if (this.provider && this.provider.close){
this.log('Shutting down ganache-core') this.ui.report('cleanup');
return new Promise(res => self.provider.close(res)) await pify(self.provider.close)();
} }
} }
// ------------------------------------------ Utils ---------------------------------------------- // ------------------------------------------ Utils ----------------------------------------------
@ -253,29 +299,6 @@ class App {
fs.writeFileSync(covPath, JSON.stringify(data)); fs.writeFileSync(covPath, JSON.stringify(data));
} }
// ======
// Launch
// ======
sanityCheckContext(){
if (!shell.test('-e', this.originalContractsDir)){
this.cleanUp("Couldn't find a 'contracts' folder to instrument.");
}
if (shell.test('-e', this.contractsDir)){
shell.rm('-Rf', this.contractsDir);
}
if (shell.test('-e', this.artifactsDir)){
shell.rm('-Rf', this.artifactsDir);
}
}
generateEnvelope(){
shell.mkdir(this.contractsDir);
shell.mkdir(this.artifactsDir);
shell.cp('-Rf', `${this.originalContractsDir}/*`, this.contractsDir);
}
// ===== // =====
// Paths // Paths
// ===== // =====
@ -291,6 +314,10 @@ class App {
return newCoverage; return newCoverage;
} }
toRelativePath(absolutePath, parentDir){
return absolutePath.split(`/${parentDir}`)[1]
}
/** /**
* Normalizes windows paths * Normalizes windows paths
* @param {String} file path * @param {String} file path
@ -306,7 +333,8 @@ class App {
// Skipping // Skipping
// ======== // ========
/** /**
* Determines if a file is in a folder marked skippable. * Determines if a file is in a folder marked skippable in a standard environment where
* instrumented files are in their own temporary folder.
* @param {String} file file path * @param {String} file file path
* @return {Boolean} * @return {Boolean}
*/ */
@ -322,7 +350,8 @@ class App {
} }
/** /**
* Parses the skipFiles option (which also accepts folders) * Parses the skipFiles option (which also accepts folders) in a standard environment where
* instrumented files are in their own temporary folder.
*/ */
registerSkippedItems(){ registerSkippedItems(){
const root = `${this.contractsDir}`; const root = `${this.contractsDir}`;
@ -332,7 +361,8 @@ class App {
} }
/** /**
* Returns true when file should not be instrumented, false otherwise * Returns true when file should not be instrumented, false otherwise.
* This method should be overwritten if plugin does in-flight instrumentation
* @param {String} file path segment * @param {String} file path segment
* @return {Boolean} * @return {Boolean}
*/ */

@ -1,23 +1,56 @@
const c = require('chalk'); const chalk = require('chalk');
const emoji = require('node-emoji'); const emoji = require('node-emoji');
/** /**
* Coverage tool output handler. This is where any logging solidity-coverage does on its * Coverage tool output formatters. These classes support any the logging solidity-coverage API
* own behalf is managed. NB, most output is generated by the host dev stack (e.g. truffle, * (or plugins which consume it) do on their own behalf. NB, most output is generated by the host
* buidler or by the coverage generator (e.g. Istanbul). * dev stack (ex: the truffle compile command, or istanbul).
* )
*/ */
class UI { class UI {
constructor(log){ constructor(log){
this.log = log || console.log; this.log = log || console.log;
this.chalk = chalk;
} }
/** /**
* Writes a formatted message to console * Writes a formatted message
* @param {String} kind message selector
* @param {String[]} args info to inject into template
*/
report(kind, args=[]){}
/**
* Returns a formatted message. Useful for error messages.
* @param {String} kind message selector
* @param {String[]} args info to inject into template
* @return {String} message
*/
generate(kind, args=[]){}
_write(msg){
this.log(this._format(msg))
}
_format(msg){
return emoji.emojify(msg)
}
}
/**
* UI for solidity-coverage/lib/app.js
*/
class AppUI extends UI {
constructor(log){
super(log);
}
/**
* Writes a formatted message via log
* @param {String} kind message selector * @param {String} kind message selector
* @param {String[]} args info to inject into template * @param {String[]} args info to inject into template
*/ */
report(kind, args=[]){ report(kind, args=[]){
const c = this.chalk;
const ct = c.bold.green('>'); const ct = c.bold.green('>');
const ds = c.bold.yellow('>'); const ds = c.bold.yellow('>');
const w = ":warning:"; const w = ":warning:";
@ -27,25 +60,6 @@ class UI {
`${c.red('Check the provider option syntax in solidity-coverage docs.')}\n`+ `${c.red('Check the provider option syntax in solidity-coverage docs.')}\n`+
`${w} ${c.red('Using ganache-core-sc (eq. core v2.7.0) instead.')}\n`, `${w} ${c.red('Using ganache-core-sc (eq. core v2.7.0) instead.')}\n`,
'sol-tests': `\n${w} ${c.red("This plugin cannot run Truffle's native solidity tests: ")}`+
`${args[0]} test(s) will be skipped.\n`,
'truffle-local': `\n${ct} ${c.grey('Using Truffle library from local node_modules.')}\n`,
'truffle-global': `\n${ct} ${c.grey('Using Truffle library from global node_modules.')}\n`,
'truffle-warn': `${w} ${c.red('Unable to require Truffle library locally or globally. ')} `+
`${c.red('Expected to find installed Truffle >= v5.0.31 ...')}\n` +
`${w} ${c.red('Using fallback Truffle library instead (v5.0.31)')}\n`,
'truffle-help': `Usage: truffle run coverage [options]\n\n` +
`Options:\n` +
` --file: path (or glob) to subset of JS test files. (Quote your globs)\n` +
` --solcoverjs: relative path to .solcover.js (ex: ./../.solcover.js)\n` +
` --version: version info\n`,
'truffle-version': `${ct} ${c.bold('truffle')}: v${args[0]}`,
'ganache-version': `${ct} ${c.bold('ganache-core')}: ${args[0]}`,
'coverage-version': `${ct} ${c.bold('solidity-coverage')}: v${args[0]}`,
'instr-start': `\n${c.bold('Instrumenting for coverage...')}` + 'instr-start': `\n${c.bold('Instrumenting for coverage...')}` +
`\n${c.bold('=============================')}\n`, `\n${c.bold('=============================')}\n`,
@ -55,9 +69,14 @@ class UI {
'instr-item': `${ct} ${args[0]}`, 'instr-item': `${ct} ${args[0]}`,
'instr-skipped': `${ds} ${c.grey(args[0])}`, 'instr-skipped': `${ds} ${c.grey(args[0])}`,
'istanbul': `${ct} ${c.grey('Istanbul reports written to')} ./coverage/ ` +
`${c.grey('and')} ./coverage.json`,
'cleanup': `${ct} ${c.grey('solidity-coverage cleaning up, shutting down ganache-core')}`,
} }
this.log(emoji.emojify(kinds[kind])); this._write(kinds[kind]);
} }
/** /**
@ -67,14 +86,22 @@ class UI {
* @return {String} message * @return {String} message
*/ */
generate(kind, args=[]){ generate(kind, args=[]){
const c = this.chalk;
const kinds = { const kinds = {
'truffle-fail': `${c.red('Unable to load fail-safe Truffle library. Caught: ')} ${args[0]}\n` + 'instr-fail': `${c.red('Could not instrument:')} ${args[0]}. ` +
`:x: ${c.red('Try installing Truffle >= v5.0.31 locally or globally.\n')}`, `${c.red('(Please verify solc can compile this file without errors.) ')}`,
}
'istanbul-fail': `${c.red('Istanbul coverage reports could not be generated. ')}`,
'sources-fail': `${c.red('Cannot locate expected contract sources folder: ')} ${args[0]}`,
}
return emoji.emojify(kinds[kind]) return this._format(kinds[kind])
} }
} }
module.exports = UI; module.exports = {
AppUI: AppUI,
UI: UI
};

@ -29,6 +29,7 @@
"istanbul": "^0.4.5", "istanbul": "^0.4.5",
"node-dir": "^0.1.17", "node-dir": "^0.1.17",
"node-emoji": "^1.10.0", "node-emoji": "^1.10.0",
"pify": "^4.0.1",
"req-cwd": "^1.0.1", "req-cwd": "^1.0.1",
"shelljs": "^0.8.3", "shelljs": "^0.8.3",
"solidity-parser-antlr": "^0.4.7", "solidity-parser-antlr": "^0.4.7",

@ -0,0 +1,3 @@
module.exports = {
wrong, noooooo oh noooooooo.!!!!!
}

@ -0,0 +1,99 @@
/**
* Use this file to configure your truffle project. It's seeded with some
* common settings for different networks and features like migrations,
* compilation and testing. Uncomment the ones you need or modify
* them to suit your project as necessary.
*
* More information about configuration can be found at:
*
* truffleframework.com/docs/advanced/configuration
*
* To deploy via Infura you'll need a wallet provider (like truffle-hdwallet-provider)
* to sign your transactions before they're sent to a remote public node. Infura accounts
* are available for free at: infura.io/register.
*
* You'll also need a mnemonic - the twelve word phrase the wallet uses to generate
* public/private key pairs. If you're publishing your code to GitHub make sure you load this
* phrase from a file you've .gitignored so it doesn't accidentally become public.
*
*/
// const HDWalletProvider = require('truffle-hdwallet-provider');
// const infuraKey = "fj4jll3k.....";
//
// const fs = require('fs');
// const mnemonic = fs.readFileSync(".secret").toString().trim();
module.exports = {
/**
* Networks define how you connect to your ethereum client and let you set the
* defaults web3 uses to send transactions. If you don't specify one truffle
* will spin up a development blockchain for you on port 9545 when you
* run `develop` or `test`. You can ask a truffle command to use a specific
* network from the command line, e.g
*
* $ truffle test --network <network-name>
*/
networks: {
// Useful for testing. The `development` name is special - truffle uses it by default
// if it's defined here and no other network is specified at the command line.
// You should run a client (like ganache-cli, geth or parity) in a separate terminal
// tab if you use this network and you must also set the `host`, `port` and `network_id`
// options below to some value.
//
// development: {
// host: "127.0.0.1", // Localhost (default: none)
// port: 8545, // Standard Ethereum port (default: none)
// network_id: "*", // Any network (default: none)
// },
// Another network with more advanced options...
// advanced: {
// port: 8777, // Custom port
// network_id: 1342, // Custom network
// gas: 8500000, // Gas sent with each transaction (default: ~6700000)
// gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei)
// from: <address>, // Account to send txs from (default: accounts[0])
// websockets: true // Enable EventEmitter interface for web3 (default: false)
// },
// Useful for deploying to a public network.
// NB: It's important to wrap the provider as a function.
// ropsten: {
// provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`),
// network_id: 3, // Ropsten's id
// gas: 5500000, // Ropsten has a lower block limit than mainnet
// confirmations: 2, // # of confs to wait between deployments. (default: 0)
// timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
// skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
// },
// Useful for private networks
// private: {
// provider: () => new HDWalletProvider(mnemonic, `https://network.io`),
// network_id: 2111, // This network is yours, in the cloud.
// production: true // Treats this network as if it was a public net. (default: false)
// }
},
// Set default mocha options here, use special reporters etc.
mocha: {
// timeout: 100000
},
// Configure your compilers
compilers: {
solc: {
// version: "0.5.1", // Fetch exact version from solc-bin (default: truffle's version)
// docker: true, // Use "0.5.1" you've installed locally with docker (default: false)
// settings: { // See the solidity docs for advice about optimization and evmVersion
// optimizer: {
// enabled: false,
// runs: 200
// },
// evmVersion: "byzantium"
// }
}
}
}

@ -0,0 +1,99 @@
/**
* Use this file to configure your truffle project. It's seeded with some
* common settings for different networks and features like migrations,
* compilation and testing. Uncomment the ones you need or modify
* them to suit your project as necessary.
*
* More information about configuration can be found at:
*
* truffleframework.com/docs/advanced/configuration
*
* To deploy via Infura you'll need a wallet provider (like truffle-hdwallet-provider)
* to sign your transactions before they're sent to a remote public node. Infura accounts
* are available for free at: infura.io/register.
*
* You'll also need a mnemonic - the twelve word phrase the wallet uses to generate
* public/private key pairs. If you're publishing your code to GitHub make sure you load this
* phrase from a file you've .gitignored so it doesn't accidentally become public.
*
*/
// const HDWalletProvider = require('truffle-hdwallet-provider');
// const infuraKey = "fj4jll3k.....";
//
// const fs = require('fs');
// const mnemonic = fs.readFileSync(".secret").toString().trim();
module.exports = {
/**
* Networks define how you connect to your ethereum client and let you set the
* defaults web3 uses to send transactions. If you don't specify one truffle
* will spin up a development blockchain for you on port 9545 when you
* run `develop` or `test`. You can ask a truffle command to use a specific
* network from the command line, e.g
*
* $ truffle test --network <network-name>
*/
networks: {
// Useful for testing. The `development` name is special - truffle uses it by default
// if it's defined here and no other network is specified at the command line.
// You should run a client (like ganache-cli, geth or parity) in a separate terminal
// tab if you use this network and you must also set the `host`, `port` and `network_id`
// options below to some value.
//
// development: {
// host: "127.0.0.1", // Localhost (default: none)
// port: 8545, // Standard Ethereum port (default: none)
// network_id: "*", // Any network (default: none)
// },
// Another network with more advanced options...
// advanced: {
// port: 8777, // Custom port
// network_id: 1342, // Custom network
// gas: 8500000, // Gas sent with each transaction (default: ~6700000)
// gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei)
// from: <address>, // Account to send txs from (default: accounts[0])
// websockets: true // Enable EventEmitter interface for web3 (default: false)
// },
// Useful for deploying to a public network.
// NB: It's important to wrap the provider as a function.
// ropsten: {
// provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`),
// network_id: 3, // Ropsten's id
// gas: 5500000, // Ropsten has a lower block limit than mainnet
// confirmations: 2, // # of confs to wait between deployments. (default: 0)
// timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
// skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
// },
// Useful for private networks
// private: {
// provider: () => new HDWalletProvider(mnemonic, `https://network.io`),
// network_id: 2111, // This network is yours, in the cloud.
// production: true // Treats this network as if it was a public net. (default: false)
// }
},
// Set default mocha options here, use special reporters etc.
mocha: {
// timeout: 100000
},
// Configure your compilers
compilers: {
solc: {
// version: "0.5.1", // Fetch exact version from solc-bin (default: truffle's version)
// docker: true, // Use "0.5.1" you've installed locally with docker (default: false)
// settings: { // See the solidity docs for advice about optimization and evmVersion
// optimizer: {
// enabled: false,
// runs: 200
// },
// evmVersion: "byzantium"
// }
}
}
}

@ -0,0 +1,14 @@
pragma solidity ^0.5.3;
contract Unparseable {
uint x = 0;
function test(uint val) public {
x = x + val;
}
function getX() public view returns (uint){
return x;
// Missing a bracket!
}

@ -141,6 +141,28 @@ describe('app', function() {
assertCoverageMissing(missing); assertCoverageMissing(missing);
}); });
it('project contains no contract sources folder', async function() {
assertCleanInitialState();
mock.installFullProject('no-sources');
try {
await plugin(truffleConfig);
assert.fail()
} catch(err){
assert(
err.message.includes('Cannot locate expected contract sources folder'),
`Should error when contract sources cannot be found: (output --> ${err.message}`
);
assert(
err.message.includes('sc_temp/contracts'),
`Error message should contain path: (output --> ${err.message}`
);
}
assertCoverageNotGenerated(truffleConfig);
});
it('project with relative path solidity imports', async function() { it('project with relative path solidity imports', async function() {
assertCleanInitialState(); assertCleanInitialState();
mock.installFullProject('import-paths'); mock.installFullProject('import-paths');
@ -161,6 +183,23 @@ describe('app', function() {
); );
}); });
it('project .solcover.js has syntax error', async function(){
assertCleanInitialState();
mock.installFullProject('bad-solcoverjs');
try {
await plugin(truffleConfig);
assert.fail()
} catch(err){
assert(
err.message.includes('Could not load .solcover.js config file.'),
`Should notify when solcoverjs has syntax error: (output --> ${err.message}`
);
}
assertCoverageNotGenerated(truffleConfig);
})
it('truffle run coverage --config ../.solcover.js', async function() { it('truffle run coverage --config ../.solcover.js', async function() {
assertCleanInitialState(); assertCleanInitialState();
@ -227,6 +266,52 @@ describe('app', function() {
}) })
it('truffle run coverage --useGlobalTruffle', async function(){
assertCleanInitialState();
truffleConfig.useGlobalTruffle = true;
truffleConfig.logger = mock.testLogger;
mock.install('Simple', 'simple.js', solcoverConfig);
await plugin(truffleConfig);
assert(
mock.loggerOutput.val.includes('global node_modules'),
`Should notify it's using global truffle (output --> ${mock.loggerOutput.val}`
);
});
it('truffle run coverage --usePluginTruffle', async function(){
assertCleanInitialState();
truffleConfig.usePluginTruffle = true;
truffleConfig.logger = mock.testLogger;
mock.install('Simple', 'simple.js', solcoverConfig);
await plugin(truffleConfig);
assert(
mock.loggerOutput.val.includes('fallback Truffle library module'),
`Should notify it's using plugin truffle lib copy (output --> ${mock.loggerOutput.val}`
);
});
it('lib module load failure', async function(){
assertCleanInitialState();
truffleConfig.usePluginTruffle = true;
truffleConfig.forceLibFailure = true;
mock.install('Simple', 'simple.js', solcoverConfig);
try {
await plugin(truffleConfig);
assert.fail()
} catch (err) {
assert(
err.message.includes('Unable to load plugin copy of Truffle library module'),
`Should error on failed lib module load (output --> ${err.message}`
);
}
});
it('truffle run coverage --file test/<fileName>', async function() { it('truffle run coverage --file test/<fileName>', async function() {
assertCleanInitialState(); assertCleanInitialState();
@ -419,4 +504,24 @@ describe('app', function() {
assertCoverageNotGenerated(truffleConfig); assertCoverageNotGenerated(truffleConfig);
}); });
it('instrumentation failure', async function(){
assertCleanInitialState();
mock.install('Unparseable', 'simple.js', solcoverConfig);
try {
await plugin(truffleConfig);
assert.fail()
} catch(err){
assert(
err.toString().includes('/Unparseable.sol.'),
`Should throw instrumentation errors with file name (output --> ${err.toString()}`
);
assert(err.stack !== undefined, 'Should have error trace')
}
assertCoverageNotGenerated(truffleConfig);
})
}); });

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