/** * 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 = config.temp || '.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 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 ". * 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 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 }