diff --git a/dist/buidler.plugin.js b/dist/buidler.plugin.js index e69de29b..dba89c48 100644 --- a/dist/buidler.plugin.js +++ b/dist/buidler.plugin.js @@ -0,0 +1,139 @@ +const API = require('./../lib/api'); +const utils = require('./plugin-assets/plugin.utils'); +const buidlerUtils = require('./plugin-assets/buidler.utils'); +const PluginUI = require('./plugin-assets/buidler.ui'); + +const pkg = require('./../package.json'); +const death = require('death'); +const path = require('path'); +const Web3 = require('web3'); +const ganache = require('ganache-cli'); + +const { task, types } = require("@nomiclabs/buidler/config"); +const { ensurePluginLoadedWithUsePlugin } = require("@nomiclabs/buidler/plugins"); + +const { + TASK_TEST, + TASK_COMPILE, +} = require("@nomiclabs/buidler/builtin-tasks/task-names"); + +function plugin() { + + // UI for the task flags... + const ui = new PluginUI(); + + task("coverage", "Generates a code coverage report for tests") + + .addOptionalParam("file", ui.flags.file, null, types.string) + .addOptionalParam("solcoverjs", ui.flags.solcoverjs, null, types.string) + .addOptionalParam('temp', ui.flags.temp, null, types.string) + + .setAction(async function(taskArguments, env){ + let error; + let ui; + let api; + let config; + + try { + death(buidlerUtils.finish.bind(null, config, api)); // Catch interrupt signals + + config = buidlerUtils.normalizeConfig(env.config); + ui = new PluginUI(config.logger.log); + api = new API(utils.loadSolcoverJS(config)); + + // ============== + // Server launch + // ============== + + const network = buidlerUtils.setupNetwork(env, api); + + const address = await api.ganache(ganache); + const web3 = new Web3(address); + const accounts = await web3.eth.getAccounts(); + const nodeInfo = await web3.eth.getNodeInfo(); + const ganacheVersion = nodeInfo.split('/')[1]; + + // Set default account + network.from = accounts[0]; + + // Version Info + ui.report('versions', [ + ganacheVersion, + pkg.version + ]); + + // Network Info + ui.report('network', [ + api.defaultNetworkName, + api.port + ]); + + // Run post-launch server hook; + await api.onServerReady(config); + + // ================ + // Instrumentation + // ================ + + const skipFiles = api.skipFiles || []; + + let { + targets, + skipped + } = utils.assembleFiles(config, skipFiles); + + targets = api.instrument(targets); + utils.reportSkipped(config, skipped); + + // ============== + // Compilation + // ============== + + const { + tempArtifactsDir, + tempContractsDir + } = utils.getTempLocations(config); + + utils.save(targets, config.paths.sources, tempContractsDir); + utils.save(skipped, config.paths.sources, tempContractsDir); + + config.paths.sources = tempContractsDir; + config.paths.artifacts = tempArtifactsDir; + config.paths.cache = buidlerUtils.tempCacheDir(config); + config.solc.optimizer.enabled = false; + + await env.run(TASK_COMPILE); + + await api.onCompileComplete(config); + + // ====== + // Tests + // ====== + const testFiles = buidlerUtils.getTestFilePaths(config); + + try { + await env.run(TASK_TEST, {testFiles: testFiles}) + } catch (e) { + error = e; + } + await api.onTestsComplete(config); + + // ======== + // Istanbul + // ======== + await api.report(); + await api.onIstanbulComplete(config); + + } catch(e) { + error = e; + } + + await utils.finish(config, api); + + if (error !== undefined ) throw error; + if (process.exitCode > 0) throw new Error(ui.generate('tests-fail', [process.exitCode])); + }) +} + +module.exports = plugin; + diff --git a/dist/plugin-assets/buidler.ui.js b/dist/plugin-assets/buidler.ui.js new file mode 100644 index 00000000..78c39a38 --- /dev/null +++ b/dist/plugin-assets/buidler.ui.js @@ -0,0 +1,83 @@ +const UI = require('./../../lib/ui').UI; + +/** + * Truffle Plugin logging + */ +class PluginUI extends UI { + constructor(log){ + super(log); + + this.flags = { + file: `Path (or glob) defining a subset of tests to run`, + + solcoverjs: `Relative path from working directory to config. ` + + `Useful for monorepo packages that share settings.`, + + temp: `Path to a disposable folder to store compilation artifacts in. ` + + `Useful when your test setup scripts include hard-coded paths to ` + + `a build directory.`, + + } + } + + /** + * 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 = { + + 'instr-skip': `\n${c.bold('Coverage skipped for:')}` + + `\n${c.bold('=====================')}\n`, + + 'instr-skipped': `${ds} ${c.grey(args[0])}`, + + 'versions': `${ct} ${c.bold('ganache-core')}: ${args[0]}\n` + + `${ct} ${c.bold('solidity-coverage')}: v${args[1]}`, + + 'network': `\n${c.bold('Network Info')}` + + `\n${c.bold('============')}\n` + + `${ct} ${c.bold('port')}: ${args[1]}\n` + + `${ct} ${c.bold('network')}: ${args[0]}\n`, + } + + 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 x = ":x:"; + + const kinds = { + + 'sources-fail': `${c.red('Cannot locate expected contract sources folder: ')} ${args[0]}`, + + '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.')}`, + + 'tests-fail': `${x} ${c.bold(args[0])} ${c.red('test(s) failed under coverage.')}`, + + 'no-network': `${c.red('Network: ')} ${args[0]} ` + + `${c.red(' is not defined in your truffle-config networks. ')}`, + + } + + + return this._format(kinds[kind]) + } +} + +module.exports = PluginUI; \ No newline at end of file diff --git a/dist/plugin-assets/buidler.utils.js b/dist/plugin-assets/buidler.utils.js new file mode 100644 index 00000000..23fff0c8 --- /dev/null +++ b/dist/plugin-assets/buidler.utils.js @@ -0,0 +1,108 @@ +const shell = require('shelljs'); +const globby = require('globby'); +const pluginUtils = require("./plugin.utils"); +const path = require('path'); +const util = require('util'); +const { createProvider } = require("@nomiclabs/buidler/internal/core/providers/construction"); + + +// ============================= +// Buidler Specific Plugin Utils +// ============================= + +/** + * Returns a list of test files to pass to TASK_TEST. + * @param {BuidlerConfig} config + * @return {String[]} list of files to pass to mocha + */ +function getTestFilePaths(config){ + let target; + + // Handle --file cli option (subset of tests) + (typeof config.file === 'string') + ? target = globby.sync([config.file]) + : target = []; + + // Return list of test files + const testregex = /.*\.(js|ts|es|es6|jsx)$/; + return target.filter(f => f.match(testregex) != null); +} + +/** + * Normalizes buidler paths / logging for use by the plugin utilities and + * attaches them to the config + * @param {BuidlerConfig} config + * @return {BuidlerConfig} updated config + */ +function normalizeConfig(config){ + config.workingDir = config.paths.root; + config.contractsDir = config.paths.sources; + config.testDir = config.paths.tests; + config.artifactsDir = config.paths.artifacts; + config.logger = config.logger ? config.logger : {log: null}; + + return config; +} + +function setupNetwork(env, api){ + const networkConfig = { + url: `http://${api.host}:${api.port}`, + gas: api.gasLimit, + gasPrice: api.gasPrice + } + + const provider = createProvider(api.defaultNetworkName, networkConfig); + + env.config.networks[api.defaultNetworkName] = networkConfig; + env.config.defaultNetwork = api.defaultNetworkName; + + env.network = { + name: api.defaultNetworkName, + config: networkConfig, + provider: provider, + } + + env.ethereum = provider; + + // Return a reference so we can set the from account + return env.network; +} + +/** + * Generates a path to a temporary compilation cache directory + * @param {BuidlerConfig} config + * @return {String} .../.coverage_cache + */ +function tempCacheDir(config){ + return path.join(config.paths.root, '.coverage_cache'); +} + +/** + * Silently removes temporary folders and calls api.finish to shut server down + * @param {BuidlerConfig} config + * @param {SolidityCoverage} api + * @return {Promise} + */ +async function finish(config, api){ + const { + tempContractsDir, + tempArtifactsDir + } = pluginUtils.getTempLocations(config); + + shell.config.silent = true; + shell.rm('-Rf', tempContractsDir); + shell.rm('-Rf', tempArtifactsDir); + shell.rm('-Rf', path.join(config.paths.root, '.coverage_cache')); + shell.config.silent = false; + + if (api) await api.finish(); +} + +module.exports = { + normalizeConfig: normalizeConfig, + finish: finish, + tempCacheDir: tempCacheDir, + getTestFilePaths: getTestFilePaths, + setupNetwork: setupNetwork +} + diff --git a/dist/plugin-assets/plugin.utils.js b/dist/plugin-assets/plugin.utils.js index cabbf0c7..d4162eee 100644 --- a/dist/plugin-assets/plugin.utils.js +++ b/dist/plugin-assets/plugin.utils.js @@ -11,6 +11,7 @@ const PluginUI = require('./truffle.ui'); const path = require('path'); const fs = require('fs-extra'); const shell = require('shelljs'); +const util = require('util') // === // UI @@ -193,7 +194,6 @@ function loadSolcoverJS(config){ let coverageConfig; let ui = new PluginUI(config.logger.log); - // Handle --solcoverjs flag (config.solcoverjs) ? solcoverjs = path.join(config.workingDir, config.solcoverjs) @@ -251,6 +251,7 @@ async function finish(config, api){ shell.config.silent = true; shell.rm('-Rf', tempContractsDir); shell.rm('-Rf', tempArtifactsDir); + shell.config.silent = false; if (api) await api.finish(); } diff --git a/lib/api.js b/lib/api.js index 14aa5803..4ed9d4c3 100644 --- a/lib/api.js +++ b/lib/api.js @@ -30,7 +30,6 @@ class API { this.testsErrored = false; this.cwd = config.cwd || process.cwd(); - this.originalContractsDir = config.originalContractsDir this.defaultHook = () => {}; this.onServerReady = config.onServerReady || this.defaultHook; @@ -41,12 +40,12 @@ class API { this.server = null; this.provider = null; this.defaultPort = 8555; + this.defaultNetworkName = 'soliditycoverage'; this.client = config.client; this.port = config.port || this.defaultPort; this.host = config.host || "127.0.0.1"; this.providerOptions = config.providerOptions || {}; - this.skipFiles = config.skipFiles || []; this.log = config.log || console.log; @@ -167,10 +166,7 @@ class API { return new Promise((resolve, reject) => { try { - this.coverage.generate( - this.instrumenter.instrumentationData, - this.originalContractsDir - ); + this.coverage.generate(this.instrumenter.instrumentationData); const mapping = this.makeKeysRelative(this.coverage.data, this.cwd); this.saveCoverage(mapping); @@ -230,7 +226,7 @@ class API { } // NB: EADDRINUSE errors are uncatch-able? - pify(this.server.listen)(this.port); + await pify(this.server.listen)(this.port); } assertHasBlockchain(provider){ diff --git a/lib/validator.js b/lib/validator.js index cdc5b624..1b224034 100644 --- a/lib/validator.js +++ b/lib/validator.js @@ -15,8 +15,6 @@ const configSchema = { cwd: {type: "string"}, host: {type: "string"}, - - originalContractsDir: {type: "string"}, port: {type: "number"}, providerOptions: {type: "object"}, silent: {type: "boolean"}, diff --git a/test/integration/projects/bad-solcoverjs/buidler.config.js b/test/integration/projects/bad-solcoverjs/buidler.config.js index 0e563633..084a9f23 100644 --- a/test/integration/projects/bad-solcoverjs/buidler.config.js +++ b/test/integration/projects/bad-solcoverjs/buidler.config.js @@ -1 +1,8 @@ -modules.exports={}; +const { loadPluginFile } = require("@nomiclabs/buidler/plugins-testing"); +loadPluginFile(__dirname + "/../dist/buidler.plugin"); +usePlugin("@nomiclabs/buidler-truffle5"); + +module.exports={ + defaultNetwork: "buidlerevm", + logger: process.env.SILENT ? { log: () => {} } : console, +}; \ No newline at end of file diff --git a/test/integration/projects/import-paths/buidler.config.js b/test/integration/projects/import-paths/buidler.config.js index 0e563633..f170d4cc 100644 --- a/test/integration/projects/import-paths/buidler.config.js +++ b/test/integration/projects/import-paths/buidler.config.js @@ -1 +1,8 @@ -modules.exports={}; +const { loadPluginFile } = require("@nomiclabs/buidler/plugins-testing"); +loadPluginFile(__dirname + "/../dist/buidler.plugin"); +usePlugin("@nomiclabs/buidler-truffle5"); + +module.exports={ + defaultNetwork: "buidlerevm", + logger: process.env.SILENT ? { log: () => {} } : console, +}; diff --git a/test/integration/projects/import-paths/node_modules/package/package.json b/test/integration/projects/import-paths/node_modules/package/package.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/test/integration/projects/import-paths/node_modules/package/package.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/integration/projects/libraries/buidler.config.js b/test/integration/projects/libraries/buidler.config.js index 0e563633..46ea0f59 100644 --- a/test/integration/projects/libraries/buidler.config.js +++ b/test/integration/projects/libraries/buidler.config.js @@ -1 +1,7 @@ -modules.exports={}; +const { loadPluginFile } = require("@nomiclabs/buidler/plugins-testing"); +loadPluginFile(__dirname + "/../dist/buidler.plugin"); +usePlugin("@nomiclabs/buidler-truffle5"); + +module.exports={ + logger: process.env.SILENT ? { log: () => {} } : console, +}; \ No newline at end of file diff --git a/test/integration/projects/multiple-migrations/buidler.config.js b/test/integration/projects/multiple-migrations/buidler.config.js index 0e563633..2ed08bf0 100644 --- a/test/integration/projects/multiple-migrations/buidler.config.js +++ b/test/integration/projects/multiple-migrations/buidler.config.js @@ -1 +1 @@ -modules.exports={}; +module.exports={}; diff --git a/test/integration/projects/multiple-suites/.solcover.js b/test/integration/projects/multiple-suites/.solcover.js new file mode 100644 index 00000000..71b990cc --- /dev/null +++ b/test/integration/projects/multiple-suites/.solcover.js @@ -0,0 +1,4 @@ +module.exports = { + "silent": false, + "istanbulReporter": [ "json-summary", "text"] +} diff --git a/test/integration/projects/multiple-suites/buidler.config.js b/test/integration/projects/multiple-suites/buidler.config.js new file mode 100644 index 00000000..084a9f23 --- /dev/null +++ b/test/integration/projects/multiple-suites/buidler.config.js @@ -0,0 +1,8 @@ +const { loadPluginFile } = require("@nomiclabs/buidler/plugins-testing"); +loadPluginFile(__dirname + "/../dist/buidler.plugin"); +usePlugin("@nomiclabs/buidler-truffle5"); + +module.exports={ + defaultNetwork: "buidlerevm", + logger: process.env.SILENT ? { log: () => {} } : console, +}; \ No newline at end of file diff --git a/test/integration/projects/multiple-suites/contracts/ContractA.sol b/test/integration/projects/multiple-suites/contracts/ContractA.sol new file mode 100644 index 00000000..9d8d1344 --- /dev/null +++ b/test/integration/projects/multiple-suites/contracts/ContractA.sol @@ -0,0 +1,17 @@ +pragma solidity ^0.5.0; + + +contract ContractA { + uint x; + constructor() public { + } + + function sendFn() public { + x = 5; + } + + function callFn() public pure returns (uint){ + uint y = 5; + return y; + } +} diff --git a/test/integration/projects/multiple-suites/contracts/ContractB.sol b/test/integration/projects/multiple-suites/contracts/ContractB.sol new file mode 100644 index 00000000..daa42f7d --- /dev/null +++ b/test/integration/projects/multiple-suites/contracts/ContractB.sol @@ -0,0 +1,17 @@ +pragma solidity ^0.5.0; + + +contract ContractB { + uint x; + constructor() public { + } + + function sendFn() public { + x = 5; + } + + function callFn() public pure returns (uint){ + uint y = 5; + return y; + } +} diff --git a/test/integration/projects/multiple-suites/contracts/ContractC.sol b/test/integration/projects/multiple-suites/contracts/ContractC.sol new file mode 100644 index 00000000..454c86cd --- /dev/null +++ b/test/integration/projects/multiple-suites/contracts/ContractC.sol @@ -0,0 +1,17 @@ +pragma solidity ^0.5.0; + + +contract ContractC { + uint x; + constructor() public { + } + + function sendFn() public { + x = 5; + } + + function callFn() public pure returns (uint){ + uint y = 5; + return y; + } +} diff --git a/test/integration/projects/multiple-suites/contracts/Migrations.sol b/test/integration/projects/multiple-suites/contracts/Migrations.sol new file mode 100644 index 00000000..c378ffb0 --- /dev/null +++ b/test/integration/projects/multiple-suites/contracts/Migrations.sol @@ -0,0 +1,23 @@ +pragma solidity >=0.4.21 <0.6.0; + +contract Migrations { + address public owner; + uint public last_completed_migration; + + constructor() public { + owner = msg.sender; + } + + modifier restricted() { + if (msg.sender == owner) _; + } + + 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); + } +} diff --git a/test/integration/projects/multiple-suites/test/contracta.js b/test/integration/projects/multiple-suites/test/contracta.js new file mode 100644 index 00000000..cc778027 --- /dev/null +++ b/test/integration/projects/multiple-suites/test/contracta.js @@ -0,0 +1,15 @@ +const ContractA = artifacts.require("ContractA"); + +contract("contracta", function(accounts) { + let instance; + + before(async () => instance = await ContractA.new()) + + it('sends [ @skipForCoverage ]', async function(){ + await instance.sendFn(); + }); + + it('calls [ @skipForCoverage ]', async function(){ + await instance.callFn(); + }) +}); diff --git a/test/integration/projects/multiple-suites/test/contractb.js b/test/integration/projects/multiple-suites/test/contractb.js new file mode 100644 index 00000000..71ebfb7a --- /dev/null +++ b/test/integration/projects/multiple-suites/test/contractb.js @@ -0,0 +1,15 @@ +const ContractB = artifacts.require("ContractB"); + +contract("contractB [ @skipForCoverage ]", function(accounts) { + let instance; + + before(async () => instance = await ContractB.new()) + + it('sends', async function(){ + await instance.sendFn(); + }); + + it('calls', async function(){ + await instance.callFn(); + }) +}); diff --git a/test/integration/projects/multiple-suites/test/contractc.js b/test/integration/projects/multiple-suites/test/contractc.js new file mode 100644 index 00000000..9b3d950d --- /dev/null +++ b/test/integration/projects/multiple-suites/test/contractc.js @@ -0,0 +1,20 @@ +const ContractC = artifacts.require("ContractC"); + +contract("contractc", function(accounts) { + let instance; + + before(async () => instance = await ContractC.new()) + + it('sends', async function(){ + await instance.sendFn(); + }); + + it('calls', async function(){ + await instance.callFn(); + }) + + it('sends', async function(){ + await instance.sendFn(); + }); + +}); diff --git a/test/integration/projects/multiple-suites/truffle-config.js b/test/integration/projects/multiple-suites/truffle-config.js new file mode 100644 index 00000000..b398b071 --- /dev/null +++ b/test/integration/projects/multiple-suites/truffle-config.js @@ -0,0 +1,7 @@ +module.exports = { + networks: {}, + mocha: {}, + compilers: { + solc: {} + } +} diff --git a/test/integration/projects/no-sources/buidler.config.js b/test/integration/projects/no-sources/buidler.config.js index 0e563633..46ea0f59 100644 --- a/test/integration/projects/no-sources/buidler.config.js +++ b/test/integration/projects/no-sources/buidler.config.js @@ -1 +1,7 @@ -modules.exports={}; +const { loadPluginFile } = require("@nomiclabs/buidler/plugins-testing"); +loadPluginFile(__dirname + "/../dist/buidler.plugin"); +usePlugin("@nomiclabs/buidler-truffle5"); + +module.exports={ + logger: process.env.SILENT ? { log: () => {} } : console, +}; \ No newline at end of file diff --git a/test/integration/projects/skipping/buidler.config.js b/test/integration/projects/skipping/buidler.config.js index 0e563633..084a9f23 100644 --- a/test/integration/projects/skipping/buidler.config.js +++ b/test/integration/projects/skipping/buidler.config.js @@ -1 +1,8 @@ -modules.exports={}; +const { loadPluginFile } = require("@nomiclabs/buidler/plugins-testing"); +loadPluginFile(__dirname + "/../dist/buidler.plugin"); +usePlugin("@nomiclabs/buidler-truffle5"); + +module.exports={ + defaultNetwork: "buidlerevm", + logger: process.env.SILENT ? { log: () => {} } : console, +}; \ No newline at end of file diff --git a/test/integration/projects/test-files/.solcover.js b/test/integration/projects/test-files/.solcover.js index f73c861e..42903c81 100644 --- a/test/integration/projects/test-files/.solcover.js +++ b/test/integration/projects/test-files/.solcover.js @@ -2,6 +2,7 @@ const fn = (msg, config) => config.logger.log(msg); module.exports = { + skipFiles: ['Migrations.sol'], silent: process.env.SILENT ? true : false, istanbulReporter: ['json-summary', 'text'], onServerReady: fn.bind(null, 'running onServerReady'), diff --git a/test/integration/projects/test-files/buidler.config.js b/test/integration/projects/test-files/buidler.config.js index 0e563633..817a3eb3 100644 --- a/test/integration/projects/test-files/buidler.config.js +++ b/test/integration/projects/test-files/buidler.config.js @@ -1 +1,7 @@ -modules.exports={}; +const { loadPluginFile } = require("@nomiclabs/buidler/plugins-testing"); +loadPluginFile(__dirname + "/../dist/buidler.plugin"); +usePlugin("@nomiclabs/buidler-truffle5"); + +module.exports={ + defaultNetwork: "buidlerevm", +}; diff --git a/test/sources/js/truffle-test-fail.js b/test/sources/js/truffle-test-fail.js index cb08d550..5e8b79c0 100644 --- a/test/sources/js/truffle-test-fail.js +++ b/test/sources/js/truffle-test-fail.js @@ -10,4 +10,11 @@ contract('Simple', () => { const val = await simple.getX(); assert.equal(val.toNumber(), 4) // <-- Wrong result: test fails }); + + it('should set x to 2', async function() { + let simple = await Simple.new(); + await simple.test(5); + const val = await simple.getX(); + assert.equal(val.toNumber(), 2) // <-- Wrong result: test fails + }); }); diff --git a/test/units/buidler/standard.js b/test/units/buidler/standard.js new file mode 100644 index 00000000..527cca19 --- /dev/null +++ b/test/units/buidler/standard.js @@ -0,0 +1,258 @@ +const assert = require('assert'); +const fs = require('fs'); +const path = require('path') +const shell = require('shelljs'); + +const verify = require('../../util/verifiers') +const mock = require('../../util/integration'); +const plugin = require('../../../dist/buidler.plugin'); + +// ======================= +// Standard Use-case Tests +// ======================= + +describe('Buidler Plugin: standard use cases', function() { + let buidlerConfig; + let solcoverConfig; + + beforeEach(() => { + mock.clean(); + + mock.loggerOutput.val = ''; + solcoverConfig = { skipFiles: ['Migrations.sol']}; + buidlerConfig = mock.getDefaultBuidlerConfig(); + verify.cleanInitialState(); + }) + + afterEach(() => { + mock.buidlerTearDownEnv(); + mock.clean(); + }); + + it('simple contract', async function(){ + mock.install('Simple', 'simple.js', solcoverConfig); + mock.buidlerSetupEnv(this); + + await this.env.run("coverage"); + + verify.coverageGenerated(buidlerConfig); + + const output = mock.getOutput(buidlerConfig); + const path = Object.keys(output)[0]; + + assert( + output[path].fnMap['1'].name === 'test', + 'coverage.json missing "test"' + ); + + assert( + output[path].fnMap['2'].name === 'getX', + 'coverage.json missing "getX"' + ); + }); + + it('with relative path solidity imports', async function() { + mock.installFullProject('import-paths'); + mock.buidlerSetupEnv(this); + + await this.env.run("coverage"); + }); + + it('uses inheritance', async function() { + mock.installDouble( + ['Proxy', 'Owned'], + 'inheritance.js', + solcoverConfig + ); + + mock.buidlerSetupEnv(this); + + await this.env.run("coverage"); + + verify.coverageGenerated(buidlerConfig); + + const output = mock.getOutput(buidlerConfig); + const ownedPath = Object.keys(output)[0]; + const proxyPath = Object.keys(output)[1]; + + assert( + output[ownedPath].fnMap['1'].name === 'constructor', + '"constructor" not covered' + ); + + assert( + output[proxyPath].fnMap['1'].name === 'isOwner', + '"isOwner" not covered' + ); + }); + + it('only uses ".call"', async function(){ + mock.install('OnlyCall', 'only-call.js', solcoverConfig); + mock.buidlerSetupEnv(this); + + await this.env.run("coverage"); + + verify.coverageGenerated(buidlerConfig); + + const output = mock.getOutput(buidlerConfig); + const path = Object.keys(output)[0]; + assert( + output[path].fnMap['1'].name === 'addTwo', + 'cov should map "addTwo"' + ); + }); + + it('sends / transfers to instrumented fallback', async function(){ + mock.install('Wallet', 'wallet.js', solcoverConfig); + mock.buidlerSetupEnv(this); + + await this.env.run("coverage"); + + verify.coverageGenerated(buidlerConfig); + + const output = mock.getOutput(buidlerConfig); + const path = Object.keys(output)[0]; + assert( + output[path].fnMap['1'].name === 'transferPayment', + 'cov should map "transferPayment"' + ); + }); + + // Truffle test asserts deployment cost is greater than 20,000,000 gas + it('deployment cost > block gasLimit', async function() { + mock.install('Expensive', 'block-gas-limit.js', solcoverConfig); + mock.buidlerSetupEnv(this); + + await this.env.run("coverage"); + }); + + // Simple.sol with a failing assertion in a truffle test + it('unit tests failing', async function() { + mock.install('Simple', 'truffle-test-fail.js', solcoverConfig); + mock.buidlerSetupEnv(this); + + try { + await this.env.run("coverage"); + assert.fail() + } catch(err){ + assert(err.message.includes('failed under coverage')); + } + + verify.coverageGenerated(buidlerConfig); + + const output = mock.getOutput(buidlerConfig); + const path = Object.keys(output)[0]; + + assert(output[path].fnMap['1'].name === 'test', 'cov missing "test"'); + assert(output[path].fnMap['2'].name === 'getX', 'cov missing "getX"'); + }); + + // This project has [ @skipForCoverage ] tags in the test descriptions + // at selected 'contract' and 'it' blocks. + it('config: mocha options', async function() { + solcoverConfig.mocha = { + grep: '@skipForCoverage', + invert: true, + }; + + solcoverConfig.silent = process.env.SILENT ? true : false, + solcoverConfig.istanbulReporter = ['json-summary', 'text'] + + mock.installFullProject('multiple-suites', solcoverConfig); + mock.buidlerSetupEnv(this); + + await this.env.run("coverage"); + + const expected = [ + { + file: mock.pathToContract(buidlerConfig, 'ContractA.sol'), + pct: 0 + }, + { + file: mock.pathToContract(buidlerConfig, 'ContractB.sol'), + pct: 0, + }, + { + file: mock.pathToContract(buidlerConfig, 'ContractC.sol'), + pct: 100, + }, + ]; + + verify.lineCoverage(expected); + }); + + // Truffle test asserts balance is 777 ether + it('config: providerOptions', async function() { + solcoverConfig.providerOptions = { default_balance_ether: 777 } + + mock.install('Simple', 'testrpc-options.js', solcoverConfig); + mock.buidlerSetupEnv(this); + + await this.env.run("coverage"); + }); + + it('config: skipped file', async function() { + solcoverConfig.skipFiles = ['Migrations.sol', 'Owned.sol']; + + mock.installDouble( + ['Proxy', 'Owned'], + 'inheritance.js', + solcoverConfig + ); + + mock.buidlerSetupEnv(this); + + await this.env.run("coverage"); + + verify.coverageGenerated(buidlerConfig); + + const output = mock.getOutput(buidlerConfig); + 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('config: skipped folder', async function() { + mock.installFullProject('skipping'); + mock.buidlerSetupEnv(this); + + await this.env.run("coverage"); + + const expected = [{ + file: mock.pathToContract(buidlerConfig, 'ContractA.sol'), + pct: 100 + }]; + + const missing = [{ + file: mock.pathToContract(buidlerConfig, 'skipped-folder/ContractB.sol'), + }]; + + verify.lineCoverage(expected); + verify.coverageMissing(missing); + }); + + it('config: "onServerReady", "onTestsComplete", ...', async function() { + mock.installFullProject('test-files'); + + mock.buidlerSetupEnv(this); + + await this.env.run("coverage"); + + assert( + mock.loggerOutput.val.includes('running onServerReady') && + mock.loggerOutput.val.includes('running onTestsComplete') && + mock.loggerOutput.val.includes('running onCompileComplete') && + mock.loggerOutput.val.includes('running onIstanbulComplete'), + + `Should run "on" hooks : ${mock.loggerOutput.val}` + ); + }); +}) \ No newline at end of file diff --git a/test/units/validator.js b/test/units/validator.js index 14335fc1..cc9aee41 100644 --- a/test/units/validator.js +++ b/test/units/validator.js @@ -22,7 +22,6 @@ describe('config validation', () => { const options = [ "cwd", "host", - "originalContractsDir", ] options.forEach(name => { diff --git a/test/util/integration.js b/test/util/integration.js index 8a50dcc6..a5313d8d 100644 --- a/test/util/integration.js +++ b/test/util/integration.js @@ -1,14 +1,16 @@ /* - Utilities for generating a mock truffle project to test plugin. + Utilities for generating & managing mock projects to test plugins. */ const path = require('path'); const fs = require('fs'); const shell = require('shelljs'); -const TruffleConfig = require('truffle-config'); const decache = require('decache'); +const TruffleConfig = require('truffle-config'); +const { resetBuidlerContext } = require("@nomiclabs/buidler/plugins-testing") + const temp = './sc_temp'; const truffleConfigName = 'truffle-config.js'; const buidlerConfigName = 'buidler.config.js'; @@ -19,6 +21,7 @@ const migrationPath = `${temp}/migrations/2_deploy.js`; const templatePath = './test/integration/generic/*'; const projectPath = './test/integration/projects/' +let previousCWD; // ========================== // Misc Utils @@ -44,11 +47,29 @@ function pathToContract(config, file) { return path.join('contracts', file); } -function getOutput(truffleConfig){ - const jsonPath = path.join(truffleConfig.working_directory, "coverage.json"); +function getOutput(config){ + const workingDir = config.working_directory || config.paths.root; + const jsonPath = path.join(workingDir, "coverage.json"); return JSON.parse(fs.readFileSync(jsonPath, 'utf8')); } +// Buidler env set up +function buidlerSetupEnv(mocha) { + const mockwd = path.join(process.cwd(), temp); + previousCWD = process.cwd(); + process.chdir(mockwd); + mocha.env = require("@nomiclabs/buidler"); + mocha.env.config.logger = testLogger + mocha.logger = testLogger +}; + +// Buidler env tear down +function buidlerTearDownEnv() { + resetBuidlerContext(); + process.chdir(previousCWD); +}; + + // ========================== // Truffle Configuration // ========================== @@ -101,34 +122,39 @@ function getDefaultBuidlerConfig() { const mockwd = path.join(process.cwd(), temp); const vals = { - root: mockwd, - artifacts: path.join(mockwd, 'artifacts'), - cache: path.join(mockwd, 'cache'), - sources: path.join(mockwd, 'contracts'), - tests: path.join(mockwd, 'test'), + paths : { + root: mockwd, + artifacts: path.join(mockwd, 'artifacts'), + cache: path.join(mockwd, 'cache'), + sources: path.join(mockwd, 'contracts'), + tests: path.join(mockwd, 'test'), + }, logger: logger, mocha: { reporter: reporter }, + defaultNetwork: "buidlerevm", networks: { development: { url: "http://127.0.0.1:8545", } }, - solc: { - version: "0.5.3", - optimizer: {} - } } return vals; } function getBuidlerConfigJS(config){ + const prefix =` + const { loadPluginFile } = require("@nomiclabs/buidler/plugins-testing"); + loadPluginFile(__dirname + "/../dist/buidler.plugin"); + usePlugin("@nomiclabs/buidler-truffle5"); + ` + if (config) { - return `module.exports = ${JSON.stringify(config, null, ' ')}` + return `${prefix}module.exports = ${JSON.stringify(config, null, ' ')}`; } else { - return `module.exports = ${JSON.stringify(getDefaultBuidlerConfig(), null, ' ')}` + return `${prefix}module.exports = ${JSON.stringify(getDefaultBuidlerConfig(), null, ' ')}`; } } @@ -278,6 +304,8 @@ module.exports = { installFullProject: installFullProject, clean: clean, pathToContract: pathToContract, - getOutput: getOutput + getOutput: getOutput, + buidlerSetupEnv: buidlerSetupEnv, + buidlerTearDownEnv: buidlerTearDownEnv } diff --git a/test/util/verifiers.js b/test/util/verifiers.js index 3d3afa59..38bbb5cb 100644 --- a/test/util/verifiers.js +++ b/test/util/verifiers.js @@ -28,14 +28,18 @@ function cleanInitialState(){ assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); } -function coverageGenerated(truffleConfig){ - const jsonPath = path.join(truffleConfig.working_directory, "coverage.json"); +function coverageGenerated(config){ + const workingDir = config.working_directory || config.paths.root; + const jsonPath = path.join(workingDir, "coverage.json"); + assert(pathExists('./coverage') === true, 'should gen coverage folder'); assert(pathExists(jsonPath) === true, 'should gen coverage.json'); } -function coverageNotGenerated(truffleConfig){ - const jsonPath = path.join(truffleConfig.working_directory, "coverage.json"); +function coverageNotGenerated(config){ + const workingDir = config.working_directory || config.paths.root; + const jsonPath = path.join(workingDir, "coverage.json"); + assert(pathExists('./coverage') !== true, 'should NOT gen coverage folder'); assert(pathExists(jsonPath) !== true, 'should NOT gen coverage.json'); }