Refactor instrument method (#406)

* Move all filesystem & filtering logic to plugins

* Move plugin helpers to own file
truffle-plugin
cgewecke 5 years ago
parent 6bf6b1aa5f
commit efc321e388
  1. 460
      dist/plugin-assets/plugin.utils.js
  2. 14
      dist/plugin-assets/truffle.ui.js
  3. 316
      dist/truffle.plugin.js
  4. 221
      lib/api.js
  5. 6
      lib/ui.js
  6. 2
      package.json
  7. 2
      test/units/truffle/errors.js
  8. 6
      test/units/truffle/standard.js
  9. 33
      yarn.lock

@ -0,0 +1,460 @@
/**
* A collection of utilities for common tasks plugins will need in the course
* of composing a workflow using the solidity-coverage API
*
* TODO: Sweep back through here and make all `config.truffle_variable` plugin
* platform neutral...
*/
const PluginUI = require('./truffle.ui');
const path = require('path');
const fs = require('fs-extra');
const dir = require('node-dir');
const globby = require('globby');
const shell = require('shelljs');
const globalModules = require('global-modules');
const TruffleProvider = require('@truffle/provider');
// ===
// UI
// ===
/**
* Displays a list of skipped contracts
* @param {TruffleConfig} config
* @return {Object[]} skipped array of objects generated by `assembleTargets` method
*/
function reportSkipped(config, skipped=[]){
let started = false;
const ui = new PluginUI(config.logger.log);
for (let item of skipped){
if (!started) {
ui.report('instr-skip', []);
started = true;
}
ui.report('instr-skipped', [item.relativePath]);
}
}
// ========
// File I/O
// ========
/**
* Loads source
* @param {String} _path absolute path
* @return {String} source file
*/
function loadSource(_path){
return fs.readFileSync(_path).toString();
}
/**
* Save a set of instrumented files to a temporary directory.
* @param {Object[]} targets array of targets generated by `assembleTargets`
* @param {[type]} originalDir absolute path to parent directory of original source
* @param {[type]} tempDir absolute path to temp parent directory
*/
function save(targets, originalDir, tempDir){
let _path;
for (target of targets) {
_path = target.canonicalPath.replace(originalDir, tempDir);
fs.outputFileSync(_path, target.source);
}
}
/**
* Relativizes an absolute file path, given an absolute parent path
* @param {String} pathToFile
* @param {String} pathToParent
* @return {String} relative path
*/
function toRelativePath(pathToFile, pathToParent){
return pathToFile.replace(`${pathToParent}${path.sep}`, '');
}
/**
* Returns a pair of canonically named temporary directory paths for contracts
* and artifacts. Instrumented assets can be written & compiled to these.
* Then the unit tests can be run, consuming them as sources.
* @param {TruffleConfig} config
* @return {Object} temp paths
*/
function getTempLocations(config){
const cwd = config.working_directory;
const contractsDirName = '.coverage_contracts';
const artifactsDirName = '.coverage_artifacts';
return {
tempContractsDir: path.join(cwd, contractsDirName),
tempArtifactsDir: path.join(cwd, artifactsDirName)
}
}
/**
* Checks for existence of contract sources, and sweeps away debris
* left over from an uncontrolled crash.
*/
function checkContext(config, tempContractsDir, tempArtifactsDir){
const ui = new PluginUI(config.logger.log);
if (!shell.test('-e', config.contracts_directory)){
const msg = ui.generate('sources-fail', [config.contracts_directory])
throw new Error(msg);
}
if (shell.test('-e', tempContractsDir)){
shell.rm('-Rf', tempContractsDir);
}
if (shell.test('-e', tempArtifactsDir)){
shell.rm('-Rf', tempArtifactsDir);
}
}
// =============================
// Instrumentation Set Assembly
// =============================
function assembleFiles(config, skipFiles=[]){
let targets;
let skipFolders;
let skipped = [];
const {
tempContractsDir,
tempArtifactsDir
} = getTempLocations(config);
checkContext(config, tempContractsDir, tempArtifactsDir);
shell.mkdir(tempContractsDir);
shell.mkdir(tempArtifactsDir);
targets = shell.ls(`${config.contracts_directory}/**/*.sol`);
skipFiles = assembleSkipped(config, targets, skipFiles);
return assembleTargets(config, targets, skipFiles)
}
function assembleTargets(config, targets=[], skipFiles=[]){
const skipped = [];
const filtered = [];
const cd = config.contracts_directory;
for (let target of targets){
if (skipFiles.includes(target)){
skipped.push({
canonicalPath: target,
relativePath: toRelativePath(target, cd),
source: loadSource(target)
})
} else {
filtered.push({
canonicalPath: target,
relativePath: toRelativePath(target, cd),
source: loadSource(target)
})
}
}
return {
skipped: skipped,
targets: filtered
}
}
/**
* Parses the skipFiles option (which also accepts folders)
*/
function assembleSkipped(config, targets, skipFiles=[]){
const cd = config.contracts_directory;
// Make paths absolute
skipFiles = skipFiles.map(contract => `${cd}/${contract}`);
skipFiles.push(`${cd}/Migrations.sol`);
// Enumerate files in skipped folders
const skipFolders = skipFiles.filter(item => path.extname(item) !== '.sol')
for (let folder of skipFolders){
for (let target of targets ) {
if (target.indexOf(folder) === 0)
skipFiles.push(target);
}
};
return skipFiles;
}
// ========
// Truffle
// ========
/**
* Returns a list of test files to pass to mocha.
* @param {Object} config truffleConfig
* @return {String[]} list of files to pass to mocha
*/
function getTestFilePaths(config){
let target;
let ui = new PluginUI(config.logger.log);
// Handle --file <path|glob> cli option (subset of tests)
(typeof config.file === 'string')
? target = globby.sync([config.file])
: target = dir.files(config.test_directory, { sync: true }) || [];
// Filter native solidity tests and warn that they're skipped
const solregex = /.*\.(sol)$/;
const hasSols = target.filter(f => f.match(solregex) != null);
if (hasSols.length > 0) ui.report('sol-tests', [hasSols.length]);
// Return list of test files
const testregex = /.*\.(js|ts|es|es6|jsx)$/;
return target.filter(f => f.match(testregex) != null);
}
/**
* Configures the network. Runs before the server is launched.
* User can request a network from truffle-config with "--network <name>".
* There are overlapiing options in solcoverjs (like port and providerOptions.network_id).
* Where there are mismatches user is warned & the truffle network settings are preferred.
*
* Also generates a default config & sets the default gas high / gas price low.
*
* @param {TruffleConfig} config
* @param {SolidityCoverage} api
*/
function setNetwork(config, api){
const ui = new PluginUI(config.logger.log);
// --network <network-name>
if (config.network){
const network = config.networks[config.network];
// Check network:
if (!network){
throw new Error(ui.generate('no-network', [config.network]));
}
// Check network id
if (!isNaN(parseInt(network.network_id))){
// Warn: non-matching provider options id and network id
if (api.providerOptions.network_id &&
api.providerOptions.network_id !== parseInt(network.network_id)){
ui.report('id-clash', [ parseInt(network.network_id) ]);
}
// Prefer network defined id.
api.providerOptions.network_id = parseInt(network.network_id);
} else {
network.network_id = "*";
}
// Check port: use solcoverjs || default if undefined
if (!network.port) {
ui.report('no-port', [api.port]);
network.port = api.port;
}
// Warn: port conflicts
if (api.port !== api.defaultPort && api.port !== network.port){
ui.report('port-clash', [ network.port ])
}
// Prefer network port if defined;
api.port = network.port;
network.gas = api.gasLimit;
network.gasPrice = api.gasPrice;
setOuterConfigKeys(config, api, network.network_id);
return;
}
// Default Network Configuration
config.network = 'soliditycoverage';
setOuterConfigKeys(config, api, "*");
config.networks[config.network] = {
network_id: "*",
port: api.port,
host: api.host,
gas: api.gasLimit,
gasPrice: api.gasPrice
}
}
/**
* Sets the default `from` account field in the truffle network that will be used.
* This needs to be done after accounts are fetched from the launched client.
* @param {TruffleConfig} config
* @param {Array} accounts
*/
function setNetworkFrom(config, accounts){
if (!config.networks[config.network].from){
config.networks[config.network].from = accounts[0];
}
}
// Truffle complains that these outer keys *are not* set when running plugin fn directly.
// But throws saying they *cannot* be manually set when running as truffle command.
function setOuterConfigKeys(config, api, id){
try {
config.network_id = id;
config.port = api.port;
config.host = api.host;
config.provider = TruffleProvider.create(config);
} catch (err){}
}
/**
* Tries to load truffle module library and reports source. User can force use of
* a non-local version using cli flags (see option). It's necessary to maintain
* a fail-safe lib because feature was only introduced in 5.0.30. 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} truffleConfig config
* @return {Module}
*/
function loadTruffleLibrary(config){
const ui = new PluginUI(config.logger.log);
// Local
try {
if (config.useGlobalTruffle || config.usePluginTruffle) throw null;
const lib = require("truffle");
ui.report('lib-local');
return lib;
} catch(err) {};
// Global
try {
if (config.usePluginTruffle) throw null;
const globalTruffle = path.join(globalModules, 'truffle');
const lib = require(globalTruffle);
ui.report('lib-global');
return lib;
} catch(err) {};
// Plugin Copy @ v 5.0.31
try {
if (config.forceLibFailure) throw null; // For err unit testing
ui.report('lib-warn');
return require("./truffle.library")
} catch(err) {
throw new Error(ui.generate('lib-fail', [err]));
};
}
function loadSolcoverJS(config){
let solcoverjs;
let coverageConfig;
let ui = new PluginUI(config.logger.log);
// Handle --solcoverjs flag
(config.solcoverjs)
? solcoverjs = path.join(config.working_directory, config.solcoverjs)
: solcoverjs = path.join(config.working_directory, '.solcover.js');
// 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 = {};
}
// Truffle writes to coverage config
coverageConfig.log = config.logger.log;
coverageConfig.cwd = config.working_directory;
coverageConfig.originalContractsDir = config.contracts_directory;
// Solidity-Coverage writes to Truffle config
config.mocha = config.mocha || {};
if (coverageConfig.mocha && typeof coverageConfig.mocha === 'object'){
config.mocha = Object.assign(
config.mocha,
coverageConfig.mocha
);
}
return coverageConfig;
}
// ==========================
// Finishing / Cleanup
// ==========================
/**
* Silently removes temporary folders and calls api.finish to shut server down
* @param {TruffleConfig} config
* @param {SolidityCoverage} api
* @return {Promise}
*/
async function finish(config, api){
const {
tempContractsDir,
tempArtifactsDir
} = getTempLocations(config);
shell.config.silent = true;
shell.rm('-Rf', tempContractsDir);
shell.rm('-Rf', tempArtifactsDir);
if (api) await api.finish();
}
module.exports = {
assembleFiles: assembleFiles,
assembleSkipped: assembleSkipped,
assembleTargets: assembleTargets,
checkContext: checkContext,
finish: finish,
getTempLocations: getTempLocations,
getTestFilePaths: getTestFilePaths,
loadSource: loadSource,
loadSolcoverJS: loadSolcoverJS,
loadTruffleLibrary: loadTruffleLibrary,
reportSkipped: reportSkipped,
save: save,
setNetwork: setNetwork,
setNetworkFrom: setNetworkFrom,
setOuterConfigKeys: setOuterConfigKeys,
checkContext: checkContext,
toRelativePath: toRelativePath
}

@ -21,6 +21,11 @@ class PluginUI extends UI {
const kinds = {
'instr-skip': `\n${c.bold('Coverage skipped for:')}` +
`\n${c.bold('=====================')}\n`,
'instr-skipped': `${ds} ${c.grey(args[0])}`,
'sol-tests': `${w} ${c.red("This plugin cannot run Truffle's native solidity tests: ")}`+
`${args[0]} test(s) will be skipped.\n`,
@ -48,9 +53,9 @@ class PluginUI extends UI {
` --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]}`,
'versions': `${ct} ${c.bold('truffle')}: v${args[0]}\n` +
`${ct} ${c.bold('ganache-core')}: ${args[0]}\n` +
`${ct} ${c.bold('solidity-coverage')}: v${args[0]}`,
'network': `\n${c.bold('Network Info')}` +
`\n${c.bold('============')}\n` +
@ -75,6 +80,9 @@ class PluginUI extends UI {
const c = this.chalk;
const kinds = {
'sources-fail': `${c.red('Cannot locate expected contract sources folder: ')} ${args[0]}`,
'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`,

@ -1,326 +1,114 @@
const App = require('./../lib/app');
const API = require('./../lib/api');
const utils = require('./plugin-assets/plugin.utils');
const PluginUI = require('./plugin-assets/truffle.ui');
const pkg = require('./../package.json');
const req = require('req-cwd');
const death = require('death');
const path = require('path');
const dir = require('node-dir');
const Web3 = require('web3');
const util = require('util');
const globby = require('globby');
const shell = require('shelljs');
const globalModules = require('global-modules');
const TruffleProvider = require('@truffle/provider');
/**
* Truffle Plugin: `truffle run coverage [options]`
* @param {Object} truffleConfig @truffle/config config
* @param {Object} config @truffle/config config
* @return {Promise}
*/
async function plugin(truffleConfig){
async function plugin(config){
let ui;
let app;
let api;
let error;
let truffle;
let solcoverjs;
let testsErrored = false;
// Separate try block because this logic
// runs before app.cleanUp is defined.
try {
ui = new PluginUI(truffleConfig.logger.log);
if(truffleConfig.help) return ui.report('help'); // Exit if --help
death(utils.finish.bind(null, config, api)); // Catch interrupt signals
truffle = loadTruffleLibrary(ui, truffleConfig);
app = new App(loadSolcoverJS(ui, truffleConfig));
ui = new PluginUI(config.logger.log);
} catch (err) { throw err }
if(config.help) return ui.report('help'); // Exit if --help
try {
// Catch interrupt signals
death(app.cleanUp);
truffle = utils.loadTruffleLibrary(config);
api = new API(utils.loadSolcoverJS(config));
setNetwork(ui, app, truffleConfig);
utils.setNetwork(config, api);
// Provider / Server launch
const address = await app.ganache(truffle.ganache);
// Server launch
const address = await api.ganache(truffle.ganache);
const web3 = new Web3(address);
const accounts = await web3.eth.getAccounts();
const nodeInfo = await web3.eth.getNodeInfo();
const ganacheVersion = nodeInfo.split('/')[1];
setNetworkFrom(truffleConfig, accounts);
utils.setNetworkFrom(config, accounts);
// Version Info
ui.report('truffle-version', [truffle.version]);
ui.report('ganache-version', [ganacheVersion]);
ui.report('coverage-version',[pkg.version]);
ui.report('versions', [
truffle.version,
ganacheVersion,
pkg.version
]);
if (truffleConfig.version) return app.cleanUp(); // Exit if --version
// Exit if --version
if (config.version) return await utils.finish(config, api);
ui.report('network', [
truffleConfig.network,
truffleConfig.networks[truffleConfig.network].network_id,
truffleConfig.networks[truffleConfig.network].port
config.network,
config.networks[config.network].network_id,
config.networks[config.network].port
]);
// Instrument
app.sanityCheckContext();
app.generateStandardEnvironment();
app.instrument();
let {
targets,
skipped
} = utils.assembleFiles(config, api.skipFiles);
targets = api.instrument(targets);
utils.reportSkipped(config, skipped);
// Filesystem & Compiler Re-configuration
truffleConfig.contracts_directory = app.contractsDir;
truffleConfig.build_directory = app.artifactsDir;
const {
tempArtifactsDir,
tempContractsDir
} = utils.getTempLocations(config);
utils.save(targets, config.contracts_directory, tempContractsDir);
utils.save(skipped, config.contracts_directory, tempContractsDir);
truffleConfig.contracts_build_directory = path.join(
app.artifactsDir,
path.basename(truffleConfig.contracts_build_directory)
config.contracts_directory = tempContractsDir;
config.build_directory = tempArtifactsDir;
config.contracts_build_directory = path.join(
tempArtifactsDir,
path.basename(config.contracts_build_directory)
);
truffleConfig.all = true;
truffleConfig.test_files = getTestFilePaths(ui, truffleConfig);
truffleConfig.compilers.solc.settings.optimizer.enabled = false;
config.all = true;
config.test_files = utils.getTestFilePaths(config);
config.compilers.solc.settings.optimizer.enabled = false;
// Compile Instrumented Contracts
await truffle.contracts.compile(truffleConfig);
await truffle.contracts.compile(config);
// Run tests
try {
failures = await truffle.test.run(truffleConfig)
failures = await truffle.test.run(config)
} catch (e) {
error = e.stack;
}
// Run Istanbul
await app.report();
await api.report();
} catch(e){
error = e;
}
// Finish
await app.cleanUp();
await utils.finish(config, api);
if (error !== undefined) throw error;
if (failures > 0) throw new Error(`${failures} test(s) failed under coverage.`)
}
// -------------------------------------- Helpers --------------------------------------------------
/**
* 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;
// Handle --file <path|glob> cli option (subset of tests)
(typeof truffle.file === 'string')
? target = globby.sync([truffle.file])
: target = dir.files(truffle.test_directory, { sync: true }) || [];
// Filter native solidity tests and warn that they're skipped
const solregex = /.*\.(sol)$/;
const hasSols = target.filter(f => f.match(solregex) != null);
if (hasSols.length > 0) ui.report('sol-tests', [hasSols.length]);
// Return list of test files
const testregex = /.*\.(js|ts|es|es6|jsx)$/;
return target.filter(f => f.match(testregex) != null);
}
/**
* Configures the network. Runs before the server is launched.
* User can request a network from truffle-config with "--network <name>".
* There are overlapping options in solcoverjs (like port and providerOptions.network_id).
* Where there are mismatches user is warned & the truffle network settings are preferred.
*
* Also generates a default config & sets the default gas high / gas price low.
*
* @param {SolidityCoverage} app
* @param {TruffleConfig} config
*/
function setNetwork(ui, app, config){
// --network <network-name>
if (config.network){
const network = config.networks[config.network];
// Check network:
if (!network){
throw new Error(ui.generate('no-network', [config.network]));
}
// Check network id
if (!isNaN(parseInt(network.network_id))){
// Warn: non-matching provider options id and network id
if (app.providerOptions.network_id &&
app.providerOptions.network_id !== parseInt(network.network_id)){
ui.report('id-clash', [ parseInt(network.network_id) ]);
}
// Prefer network defined id.
app.providerOptions.network_id = parseInt(network.network_id);
} else {
network.network_id = "*";
}
// Check port: use solcoverjs || default if undefined
if (!network.port) {
ui.report('no-port', [app.port]);
network.port = app.port;
}
// Warn: port conflicts
if (app.port !== app.defaultPort && app.port !== network.port){
ui.report('port-clash', [ network.port ])
}
// Prefer network port if defined;
app.port = network.port;
network.gas = app.gasLimit;
network.gasPrice = app.gasPrice;
setOuterConfigKeys(config, app, network.network_id);
return;
}
// Default Network Configuration
config.network = 'soliditycoverage';
setOuterConfigKeys(config, app, "*");
config.networks[config.network] = {
network_id: "*",
port: app.port,
host: app.host,
gas: app.gasLimit,
gasPrice: app.gasPrice
}
}
/**
* Sets the default `from` account field in the truffle network that will be used.
* This needs to be done after accounts are fetched from the launched client.
* @param {TruffleConfig} config
* @param {Array} accounts
*/
function setNetworkFrom(config, accounts){
if (!config.networks[config.network].from){
config.networks[config.network].from = accounts[0];
}
}
// Truffle complains that these outer keys *are not* set when running plugin fn directly.
// But throws saying they *cannot* be manually set when running as truffle command.
function setOuterConfigKeys(config, app, id){
try {
config.network_id = id;
config.port = app.port;
config.host = app.host;
config.provider = TruffleProvider.create(config);
} catch (err){}
}
/**
* 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 {
if (truffleConfig.useGlobalTruffle || truffleConfig.usePluginTruffle) throw null;
const lib = require("truffle");
ui.report('lib-local');
return lib;
} catch(err) {};
// Global
try {
if (truffleConfig.usePluginTruffle) throw null;
const globalTruffle = path.join(globalModules, 'truffle');
const lib = require(globalTruffle);
ui.report('lib-global');
return lib;
} catch(err) {};
// Plugin Copy @ v 5.0.31
try {
if (truffleConfig.forceLibFailure) throw null; // For err unit testing
ui.report('lib-warn');
return require("./plugin-assets/truffle.library")
} catch(err) {
throw new Error(ui.generate('lib-fail', [err]));
};
}
function loadSolcoverJS(ui, truffleConfig){
let coverageConfig;
let solcoverjs;
// Handle --solcoverjs flag
(truffleConfig.solcoverjs)
? solcoverjs = path.join(truffleConfig.working_directory, truffleConfig.solcoverjs)
: solcoverjs = path.join(truffleConfig.working_directory, '.solcover.js');
// 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 = {};
}
// Truffle writes to coverage config
coverageConfig.log = truffleConfig.logger.log;
coverageConfig.cwd = truffleConfig.working_directory;
coverageConfig.originalContractsDir = truffleConfig.contracts_directory;
// Solidity-Coverage writes to Truffle config
truffleConfig.mocha = truffleConfig.mocha || {};
if (coverageConfig.mocha && typeof coverageConfig.mocha === 'object'){
truffleConfig.mocha = Object.assign(
truffleConfig.mocha,
coverageConfig.mocha
);
}
return coverageConfig;
}
module.exports = plugin;

@ -12,12 +12,10 @@ const Coverage = require('./coverage');
const DataCollector = require('./collector');
const AppUI = require('./ui').AppUI;
const isWin = /^win/.test(process.platform);
/**
* Coverage Runner
*/
class App {
class API {
constructor(config) {
this.coverage = new Coverage();
this.instrumenter = new Instrumenter();
@ -29,14 +27,8 @@ class App {
// Options
this.testsErrored = false;
this.instrumentToFile = (config.instrumentToFile === false) ? false : true;
this.cwd = config.cwd || process.cwd();
this.contractsDirName = '.coverage_contracts';
this.artifactsDirName = '.coverage_artifacts';
this.contractsDir = path.join(this.cwd, this.contractsDirName);
this.artifactsDir = path.join(this.cwd, this.artifactsDirName);
this.originalContractsDir = config.originalContractsDir
this.server = null;
@ -47,7 +39,6 @@ class App {
this.host = config.host || "127.0.0.1";
this.providerOptions = config.providerOptions || {};
this.skippedFolders = [];
this.skipFiles = config.skipFiles || [];
this.log = config.log || console.log;
@ -63,87 +54,68 @@ class App {
}
// -------------------------------------- Methods -----------------------------------------------
/**
* Setup temp folder, write instrumented contracts to it and register them as coverage targets
* Instruments a set of sources to prepare them for running under coverage
* @param {Object[]} targets (see below)
* @return {Object[]} (see below)
* @example:
*
* TODO: This function should be completely rewritten so that file loading, skip-filters and
* saving are done by the plugin API.
*
* Input should be array of these...
* {
* canonicalPath: <path>
* targets:
* [{
* canonicalPath: <absolute-path>
* relativePath: <relative-path>
* source: <source-file>
* }
*
* Output should be array of these...:
* {
* },...]
*
* outputs:
* [{
* canonicalPath: <path>
* source: <instrumented-source-file>
* }
* }...]
*/
instrument(targetFiles=[]) {
let targets;
instrument(targets=[]) {
let currentFile; // Keep track of filename in case we crash...
let started = false;
let skipped = [];
let outputs = [];
try {
this.registerSkippedItems();
(targetFiles.length)
? targets = targetFiles
: targets = shell.ls(`${this.contractsDir}/**/*.sol`);
for (let target of targets) {
currentFile = target.relativePath || target.canonicalPath;
targets.forEach(file => {
currentFile = file;
if (!this.shouldSkip(file)) {
!started && this.ui.report('instr-start');
if(!started){
started = true;
this.ui.report('instr-start');
}
// Remember the real path
const contractPath = this.platformNeutralPath(file);
const relativePath = this.toRelativePath(contractPath, this.contractsDirName);
const canonicalPath = path.join(
this.originalContractsDir,
relativePath
);
this.ui.report('instr-item', [target.relativePath]);
this.ui.report('instr-item', [relativePath])
const instrumented = this.instrumenter.instrument(
target.source,
target.canonicalPath
);
// Instrument contract, save, add to coverage map
const contract = this.loadContract(contractPath);
const instrumented = this.instrumenter.instrument(contract, canonicalPath);
this.saveContract(contractPath, instrumented.contract);
this.coverage.addContract(instrumented, canonicalPath);
this.coverage.addContract(instrumented, target.canonicalPath);
outputs.push({
canonicalPath: target.canonicalPath,
relativePath: target.relativePath,
source: instrumented.contract
})
}
} else {
skipped.push(file);
}
});
} catch (err) {
const name = this.toRelativePath(currentFile, this.contractsDirName);
err.message = this.ui.generate('instr-fail', [name]) + err.message;
err.message = this.ui.generate('instr-fail', [currentFile]) + err.message;
throw err;
}
if (skipped.length > 0){
this.ui.report('instr-skip');
skipped.forEach(item => {
item = item.split(`/${this.contractsDirName}`)[1]
this.ui.report('instr-skipped', [item])
});
}
return outputs;
}
/**
* Launch an in-process ethereum client 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..
* Launches an in-process ethereum client server, hooking the DataCollector to its VM.
* @param {Object} client ganache client
* @return {String} address of server to connect to
*/
async ganache(client){
let retry = false;
@ -214,57 +186,14 @@ class App {
})
}
// ============
// Public Utils
// ============
/**
* Should only be run before any temporary folders are created.
* It checks for existence of contract sources, server port conflicts
* 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.
*
* .coverage_contracts/
* .coverage_artifacts/
*/
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;
shell.config.silent = true;
shell.rm('-Rf', this.contractsDir);
shell.rm('-Rf', this.artifactsDir);
async finish() {
if (this.server && this.server.close){
this.ui.report('cleanup');
await pify(self.server.close)();
this.ui.report('finish');
await pify(this.server.close)();
}
}
// ------------------------------------------ Utils ----------------------------------------------
@ -316,13 +245,6 @@ class App {
// ========
// File I/O
// ========
loadContract(_path){
return fs.readFileSync(_path).toString();
}
saveContract(_path, contract){
fs.writeFileSync(_path, contract);
}
saveCoverage(data){
const covPath = path.join(this.cwd, "coverage.json");
@ -332,6 +254,7 @@ class App {
// =====
// Paths
// =====
//
/**
* Relativizes path keys so that istanbul report can be read on Windows
* @param {Object} map coverage map generated by coverageMap
@ -352,67 +275,13 @@ class App {
return absolutePath.split(`/${parentDir}`)[1]
}
/**
* Normalizes windows paths
* @param {String} file path
* @return {String} normalized path
*/
platformNeutralPath(file) {
return (isWin)
? path.resolve(file).split('\\').join('/')
: path.resolve(file);
}
// ========
// Skipping
// ========
/**
* 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
* @return {Boolean}
*/
inSkippedFolder(file){
let shouldSkip;
const root = `${this.contractsDir}`;
this.skippedFolders.forEach(folderToSkip => {
folderToSkip = `${root}/${folderToSkip}`;
if (file.indexOf(folderToSkip) === 0)
shouldSkip = true;
});
return shouldSkip;
}
/**
* Parses the skipFiles option (which also accepts folders) in a standard environment where
* instrumented files are in their own temporary folder.
*/
registerSkippedItems(){
const root = `${this.contractsDir}`;
this.skippedFolders = this.skipFiles.filter(item => path.extname(item) !== '.sol')
this.skipFiles = this.skipFiles.map(contract => `${root}/${contract}`);
this.skipFiles.push(`${root}/Migrations.sol`);
}
/**
* 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
* @return {Boolean}
*/
shouldSkip(file){
return this.skipFiles.includes(file) || this.inSkippedFolder(file)
}
// =======
// Logging
// =======
/**
* Turn logging off (for CI)
* @param {Boolean} isSilent
*
* TODO: logic to toggle on/off (instead of just off)
*
*/
setLoggingLevel(isSilent) {
if (isSilent) this.log = () => {};
@ -420,4 +289,4 @@ class App {
}
module.exports = App;
module.exports = API;

@ -64,16 +64,12 @@ class AppUI extends UI {
'instr-start': `\n${c.bold('Instrumenting for coverage...')}` +
`\n${c.bold('=============================')}\n`,
'instr-skip': `\n${c.bold('Coverage skipped for:')}` +
`\n${c.bold('=====================')}\n`,
'instr-item': `${ct} ${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 server')}`,
'finish': `${ct} ${c.grey('solidity-coverage cleaning up, shutting down ganache server')}`,
'server': `${ct} ${c.bold('server: ')} ${c.grey(args[0])}`,

@ -27,6 +27,7 @@
"@truffle/provider": "^0.1.17",
"chalk": "^2.4.2",
"death": "^1.1.0",
"fs-extra": "^8.1.0",
"ganache-core-sc": "2.7.0-sc.0",
"ghost-testrpc": "^0.0.2",
"global-modules": "^2.0.0",
@ -36,7 +37,6 @@
"node-dir": "^0.1.17",
"node-emoji": "^1.10.0",
"pify": "^4.0.1",
"req-cwd": "^1.0.1",
"shelljs": "^0.8.3",
"solidity-parser-antlr": "^0.4.7",
"web3": "1.2.1",

@ -184,7 +184,7 @@ describe('Truffle Plugin: error cases', function() {
assert.fail()
} catch(err){
assert(
err.message.includes('/Unparseable.sol.'),
err.message.includes('Unparseable.sol.'),
`Should throw instrumentation errors with file name: ${err.toString()}`
);

@ -25,7 +25,7 @@ describe('Truffle Plugin: standard use cases', function() {
afterEach(() => mock.clean());
it('simple contract: should generate coverage, cleanup & exit(0)', async function(){
it('simple contract', async function(){
verify.cleanInitialState();
mock.install('Simple', 'simple.js', solcoverConfig);
@ -116,7 +116,7 @@ describe('Truffle Plugin: standard use cases', function() {
verify.lineCoverage(expected);
});
it('project skips a folder', async function() {
it('skips a folder', async function() {
verify.cleanInitialState();
mock.installFullProject('skipping');
await plugin(truffleConfig);
@ -127,7 +127,7 @@ describe('Truffle Plugin: standard use cases', function() {
}];
const missing = [{
file: mock.pathToContract(truffleConfig, 'ContractB.sol'),
file: mock.pathToContract(truffleConfig, 'skipped-folder/ContractB.sol'),
}];
verify.lineCoverage(expected);

@ -3079,6 +3079,15 @@ fs-extra@^7.0.1:
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-minipass@^1.2.5:
version "1.2.6"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07"
@ -3400,6 +3409,11 @@ graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b"
integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==
graceful-fs@^4.2.0:
version "4.2.2"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02"
integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==
"graceful-readlink@>= 1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
@ -5854,20 +5868,6 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
req-cwd@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/req-cwd/-/req-cwd-1.0.1.tgz#0d73aeae9266e697a78f7976019677e76acf0fff"
integrity sha1-DXOurpJm5penj3l2AZZ352rPD/8=
dependencies:
req-from "^1.0.1"
req-from@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/req-from/-/req-from-1.0.1.tgz#bf81da5147947d32d13b947dc12a58ad4587350e"
integrity sha1-v4HaUUeUfTLRO5R9wSpYrUWHNQ4=
dependencies:
resolve-from "^2.0.0"
request@^2.79.0, request@^2.85.0:
version "2.88.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
@ -5923,11 +5923,6 @@ requireg@^0.2.2:
rc "~1.2.7"
resolve "~1.7.1"
resolve-from@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57"
integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"

Loading…
Cancel
Save