From 556d2d2e5821508f394c17983ba4eac781b372fe Mon Sep 17 00:00:00 2001 From: cgewecke Date: Mon, 16 Sep 2019 03:48:36 -0700 Subject: [PATCH] Use ganache server (instead of provider) (#402) + Support --network cli flag --- .circleci/config.yml | 3 +- dist/plugin-assets/truffle.ui.js | 24 +++++- dist/truffle.plugin.js | 141 +++++++++++++++++++++++++------ lib/app.js | 76 +++++++++++------ lib/collector.js | 56 ++++++++---- lib/ui.js | 8 +- package.json | 1 + scripts/run-zeppelin.sh | 14 +-- test/units/truffle/errors.js | 32 ++++--- test/units/truffle/flags.js | 96 +++++++++++++++++++-- test/units/truffle/standard.js | 39 +++++++++ yarn.lock | 24 ++++++ 12 files changed, 414 insertions(+), 100 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4715295..6148459 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -78,8 +78,7 @@ workflows: build: jobs: - unit-test - # TODO: re-enable when server logic is added - #- e2e-zeppelin + - e2e-zeppelin - e2e-metacoin # TODO: re-enable. # At the moment we're using forks so this is pointless diff --git a/dist/plugin-assets/truffle.ui.js b/dist/plugin-assets/truffle.ui.js index deaa5a9..b247778 100644 --- a/dist/plugin-assets/truffle.ui.js +++ b/dist/plugin-assets/truffle.ui.js @@ -21,9 +21,17 @@ class PluginUI extends UI { 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`, + 'sol-tests': `${w} ${c.red("This plugin cannot run Truffle's native solidity tests: ")}`+ + `${args[0]} test(s) will be skipped.\n`, + 'id-clash': `${w} ${c.red("The 'network_id' values in your truffle network ")}` + + `${c.red("and .solcover.js are different. Using truffle's: ")} ${c.bold(args[0])}.\n`, + + 'port-clash': `${w} ${c.red("The 'port' values in your truffle network ")}` + + `${c.red("and .solcover.js are different. Using truffle's: ")} ${c.bold(args[0])}.\n`, + + 'no-port': `${w} ${c.red("No 'port' was declared in your truffle network. ")}` + + `${c.red("Using solidity-coverage's: ")} ${c.bold(args[0])}.\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`, @@ -43,6 +51,15 @@ class PluginUI extends UI { '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]}`, + + 'network': `\n${c.bold('Network Info')}` + + `\n${c.bold('============')}\n` + + `${ct} ${c.bold('id')}: ${args[1]}\n` + + `${ct} ${c.bold('port')}: ${args[2]}\n` + + `${ct} ${c.bold('network')}: ${args[0]}\n`, + + + } this._write(kinds[kind]); @@ -66,6 +83,9 @@ class PluginUI extends UI { `${c.red('This can happen if it has a syntax error or ')}` + `${c.red('the path you specified for it is wrong.')}`, + 'no-network': `${c.red('Network: ')} ${args[0]} ` + + `${c.red(' is not defined in your truffle-config networks. ')}`, + } diff --git a/dist/truffle.plugin.js b/dist/truffle.plugin.js index 8e9d379..8d0f84b 100644 --- a/dist/truffle.plugin.js +++ b/dist/truffle.plugin.js @@ -11,6 +11,7 @@ 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]` @@ -25,12 +26,12 @@ async function plugin(truffleConfig){ let solcoverjs; let testsErrored = false; - // This needs it's own try block because this logic + // 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'); // Bail if --help + if(truffleConfig.help) return ui.report('help'); // Exit if --help truffle = loadTruffleLibrary(ui, truffleConfig); app = new App(loadSolcoverJS(ui, truffleConfig)); @@ -41,19 +42,30 @@ async function plugin(truffleConfig){ // Catch interrupt signals death(app.cleanUp); + setNetwork(ui, app, truffleConfig); + // Provider / Server launch - const provider = await app.provider(truffle.ganache); - const web3 = new Web3(provider); + const address = await app.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); + // Version Info ui.report('truffle-version', [truffle.version]); ui.report('ganache-version', [ganacheVersion]); ui.report('coverage-version',[pkg.version]); - if (truffleConfig.version) return app.cleanUp(); // Bail if --version + if (truffleConfig.version) return app.cleanUp(); // Exit if --version + + ui.report('network', [ + truffleConfig.network, + truffleConfig.networks[truffleConfig.network].network_id, + truffleConfig.networks[truffleConfig.network].port + ]); // Instrument app.sanityCheckContext(); @@ -76,25 +88,6 @@ async function plugin(truffleConfig){ // Compile Instrumented Contracts await truffle.contracts.compile(truffleConfig); - // Network Re-configuration - const networkName = 'soliditycoverage'; - truffleConfig.network = networkName; - - // Truffle complains that these keys *are not* set when running plugin fn directly. - // But throws saying they *cannot* be manually set when running as truffle command. - try { - truffleConfig.network_id = "*"; - truffleConfig.provider = provider; - } catch (err){} - - truffleConfig.networks[networkName] = { - network_id: "*", - provider: provider, - gas: app.gasLimit, - gasPrice: app.gasPrice, - from: accounts[0] - } - // Run tests try { failures = await truffle.test.run(truffleConfig) @@ -143,7 +136,101 @@ function getTestFilePaths(ui, truffle){ 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 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 + 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 @@ -188,8 +275,7 @@ function loadTruffleLibrary(ui, truffleConfig){ return require("./plugin-assets/truffle.library") } catch(err) { - const msg = ui.generate('lib-fail', [err]); - throw new Error(msg); + throw new Error(ui.generate('lib-fail', [err])); }; } @@ -218,11 +304,12 @@ function loadSolcoverJS(ui, truffleConfig){ coverageConfig = {}; } + // Truffle writes to coverage config coverageConfig.log = truffleConfig.logger.log; coverageConfig.cwd = truffleConfig.working_directory; coverageConfig.originalContractsDir = truffleConfig.contracts_directory; - //Merge solcoverjs mocha opts into truffle's + // Solidity-Coverage writes to Truffle config truffleConfig.mocha = truffleConfig.mocha || {}; if (coverageConfig.mocha && typeof coverageConfig.mocha === 'object'){ diff --git a/lib/app.js b/lib/app.js index e6def28..99635de 100644 --- a/lib/app.js +++ b/lib/app.js @@ -26,7 +26,7 @@ class App { this.testsErrored = false; this.instrumentToFile = (config.instrumentToFile === false) ? false : true; - this.cwd = config.cwd; + this.cwd = config.cwd || process.cwd(); this.contractsDirName = '.coverage_contracts'; this.artifactsDirName = '.coverage_artifacts'; this.contractsDir = path.join(this.cwd, this.contractsDirName); @@ -34,7 +34,12 @@ class App { this.originalContractsDir = config.originalContractsDir + this.server = null; + this.provider = null; + this.defaultPort = 8555; this.client = config.client; + this.port = config.port || this.defaultPort; + this.host = config.host || "127.0.0.1"; this.providerOptions = config.providerOptions || {}; this.skippedFolders = []; @@ -56,6 +61,21 @@ class App { // -------------------------------------- Methods ----------------------------------------------- /** * Setup temp folder, write instrumented contracts to it and register them as coverage targets + * + * 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: + * source: + * } + * + * Output should be array of these...: + * { + * canonicalPath: + * source: + * } */ instrument(targetFiles=[]) { let targets; @@ -114,14 +134,15 @@ class App { } /** - * Launch an in-process ethereum provider and hook up the DataCollector to its VM. + * 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.. */ - async provider(client){ + async ganache(client){ let retry = false; + let address = `http://${this.host}:${this.port}`; if(!this.client) this.client = client; // Prefer client from options @@ -130,23 +151,27 @@ class App { this.providerOptions.gasLimit = this.gasLimitString; this.providerOptions.allowUnlimitedContractSize = true; - // Try to launch provider and attach to vm step of - // either plugin's ganache or a provider passed via options + // Launch server and attach to vm step of supplied client try { - this.provider = await this.attachToVM(); - } catch(err){ - retry = true; - this.ui.report('vm-fail', []) + if (this.config.forceBackupServer) throw new Error() + await this.attachToVM() } // Fallback to ganache-core-sc (eq: ganache-core 2.7.0) - if (retry){ - this.providerOptions.logger = { log: this.collector.step.bind(this.collector) }; + catch(err) { + this.ui.report('vm-fail', []); this.client = require('ganache-core-sc'); - this.provider = this.client.provider(this.providerOptions); + + try { await this.attachToVM() } + + catch(err) { + err.message = `${this.ui.generate('server-fail', [address])} ${err.message}`; + throw err; + } } - return this.provider; + this.ui.report('server', [address]); + return address; } /** @@ -232,9 +257,9 @@ class App { shell.rm('-Rf', this.contractsDir); shell.rm('-Rf', this.artifactsDir); - if (this.provider && this.provider.close){ + if (this.server && this.server.close){ this.ui.report('cleanup'); - await pify(self.provider.close)(); + await pify(self.server.close)(); } } // ------------------------------------------ Utils ---------------------------------------------- @@ -244,27 +269,26 @@ class App { // ======== async attachToVM(){ const self = this; - const provider = this.client.provider(this.providerOptions); - this.assertHasBlockchain(provider); + this.server = this.client.server(this.providerOptions); - await this.vmIsResolved(provider); + this.assertHasBlockchain(this.server.provider); + await this.vmIsResolved(this.server.provider); - const blockchain = provider.engine.manager.state.blockchain; + const blockchain = this.server.provider.engine.manager.state.blockchain; const createVM = blockchain.createVMFromStateTrie; - // Attach to VM which ganache has already instantiated - // and which it uses to execute eth_send + // Attach to VM which ganache has already created for transactions blockchain.vm.on('step', self.collector.step.bind(self.collector)); - // Attach/hijack createVM method which ganache uses to run eth_calls + // Hijack createVM method which ganache runs for each `eth_call` blockchain.createVMFromStateTrie = function(state, activatePrecompiles) { const vm = createVM.apply(blockchain, arguments); vm.on('step', self.collector.step.bind(self.collector)); return vm; } - return provider; + await pify(this.server.listen)(this.port); } assertHasBlockchain(provider){ @@ -310,7 +334,11 @@ class App { */ makeKeysRelative(map, wd) { const newCoverage = {}; - Object.keys(map).forEach(pathKey => newCoverage[path.relative(wd, pathKey)] = map[pathKey]); + + Object + .keys(map) + .forEach(pathKey => newCoverage[path.relative(wd, pathKey)] = map[pathKey]); + return newCoverage; } diff --git a/lib/collector.js b/lib/collector.js index ea0ccd7..86dfc87 100644 --- a/lib/collector.js +++ b/lib/collector.js @@ -1,37 +1,61 @@ const web3Utils = require('web3-utils') +/** + * Writes data from the VM step to the in-memory + * coverage map constructed by the Instrumenter. + */ class DataCollector { constructor(instrumentationData={}){ this.instrumentationData = instrumentationData; + + this.validOpcodes = { + "PUSH1": true, + } } + /** + * VM step event handler. Detects instrumentation hashes when they are pushed to the + * top of the stack. This runs millions of times - trying to keep it fast. + * @param {Object} info vm step info + */ step(info){ - const self = this; - if (typeof info !== 'object' || !info.opcode ) return; - - if (info.opcode.name.includes("PUSH1") && info.stack.length > 0){ - const idx = info.stack.length - 1; - let hash = web3Utils.toHex(info.stack[idx]).toString(); - hash = self._normalizeHash(hash); + try { + if (this.validOpcodes[info.opcode.name] && info.stack.length > 0){ + const idx = info.stack.length - 1; + let hash = web3Utils.toHex(info.stack[idx]).toString(); + hash = this._normalizeHash(hash); - if(self.instrumentationData[hash]){ - self.instrumentationData[hash].hits++; + if(this.instrumentationData[hash]){ + this.instrumentationData[hash].hits++; + } } - } - } - - _setInstrumentationData(data){ - this.instrumentationData = data; + } catch (err) { /*Ignore*/ }; } + /** + * Left-pads zero prefixed bytes 32 hashes to length 66. The '59' in the + * comparison below is arbitrary. It provides a margin for recurring zeros + * but prevents left-padding shorter irrelevant hashes (like fn sigs) + * + * @param {String} hash data hash from evm stack. + * @return {String} 0x prefixed hash of length 66. + */ _normalizeHash(hash){ - if (hash.length < 66 && hash.length > 52){ + if (hash.length < 66 && hash.length > 59){ hash = hash.slice(2); while(hash.length < 64) hash = '0' + hash; hash = '0x' + hash } return hash; } + + /** + * Unit test helper + * @param {Object} data Instrumenter.instrumentationData + */ + _setInstrumentationData(data){ + this.instrumentationData = data; + } } -module.exports = DataCollector; +module.exports = DataCollector; \ No newline at end of file diff --git a/lib/ui.js b/lib/ui.js index 0d1c0df..c21fc70 100644 --- a/lib/ui.js +++ b/lib/ui.js @@ -73,7 +73,9 @@ class AppUI extends UI { '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')}`, + 'cleanup': `${ct} ${c.grey('solidity-coverage cleaning up, shutting down ganache server')}`, + + 'server': `${ct} ${c.bold('server: ')} ${c.grey(args[0])}`, } this._write(kinds[kind]); @@ -95,6 +97,10 @@ class AppUI extends UI { 'istanbul-fail': `${c.red('Istanbul coverage reports could not be generated. ')}`, 'sources-fail': `${c.red('Cannot locate expected contract sources folder: ')} ${args[0]}`, + + 'server-fail': `${c.red('Could not launch ganache server. Is ')}` + + `${args[0]} ${c.red('already in use? ')}` + + `${c.red('Run "lsof -i" in your terminal to check.\n')}`, } return this._format(kinds[kind]) diff --git a/package.json b/package.json index 8c70e44..b7201e2 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "author": "", "license": "ISC", "dependencies": { + "@truffle/provider": "^0.1.17", "chalk": "^2.4.2", "death": "^1.1.0", "ganache-core-sc": "2.7.0-sc.0", diff --git a/scripts/run-zeppelin.sh b/scripts/run-zeppelin.sh index 1cf7c19..2bd8583 100755 --- a/scripts/run-zeppelin.sh +++ b/scripts/run-zeppelin.sh @@ -20,6 +20,8 @@ fi echo "PR_PATH >>>>> $PR_PATH" +npm install -g yarn; + # Install sc-forks Zeppelin fork (temporarily). It's setup to # consume the plugin and skips a small set of GSN tests that rely on # the client being stand-alone. (See OZ issue #1918 for discussion) @@ -30,14 +32,14 @@ echo ">>>>> checkout provider-benchmarks branch" git checkout provider-benchmarks # Swap installed coverage for PR branch version -echo ">>>>> npm install" -npm install +echo ">>>>> yarn install" +yarn install -echo ">>>>> npm uninstall --save-dev solidity-coverage" -npm uninstall --save-dev solidity-coverage +echo ">>>>> yarn remove --dev solidity-coverage" +yarn remove solidity-coverage --dev -echo ">>>>> npm install --save-dev PR_PATH" -npm install --save-dev "$PR_PATH" +echo ">>>>> yarn add -dev $PR_PATH" +yarn add "$PR_PATH" --dev # Track perf time npx truffle run coverage diff --git a/test/units/truffle/errors.js b/test/units/truffle/errors.js index 497e9eb..8825091 100644 --- a/test/units/truffle/errors.js +++ b/test/units/truffle/errors.js @@ -64,6 +64,8 @@ describe('Truffle Plugin: error cases', function() { verify.coverageNotGenerated(truffleConfig); }) + + it('lib module load failure', async function(){ verify.cleanInitialState(); truffleConfig.usePluginTruffle = true; @@ -82,26 +84,28 @@ describe('Truffle Plugin: error cases', function() { } }); - // Simple.sol with a failing assertion in a truffle test - it('truffle tests failing', async function() { + it('--network is not declared in truffle-config.js', async function(){ verify.cleanInitialState(); - mock.install('Simple', 'truffle-test-fail.js', solcoverConfig); + truffleConfig.network = 'does-not-exist'; + + mock.install('Simple', 'simple.js', solcoverConfig); try { await plugin(truffleConfig); assert.fail() - } catch(err){ - assert(err.message.includes('failed under coverage')); - } - - verify.coverageGenerated(truffleConfig); + } catch (err) { - const output = mock.getOutput(truffleConfig); - const path = Object.keys(output)[0]; + assert( + err.message.includes('is not defined'), + `Should notify network 'is not defined': ${err.message}` + ); - assert(output[path].fnMap['1'].name === 'test', 'cov missing "test"'); - assert(output[path].fnMap['2'].name === 'getX', 'cov missing "getX"'); + assert( + err.message.includes('does-not-exist'), + `Should name missing network: 'does-not-exist': ${err.message}` + ); + } }); // Truffle test contains syntax error @@ -127,7 +131,7 @@ describe('Truffle Plugin: error cases', function() { await plugin(truffleConfig); assert.fail() } catch(err){ - assert(err.toString().includes('Compilation failed')); + assert(err.message.includes('Compilation failed')); } verify.coverageNotGenerated(truffleConfig); @@ -143,7 +147,7 @@ describe('Truffle Plugin: error cases', function() { assert.fail() } catch(err){ assert( - err.toString().includes('/Unparseable.sol.'), + err.message.includes('/Unparseable.sol.'), `Should throw instrumentation errors with file name: ${err.toString()}` ); diff --git a/test/units/truffle/flags.js b/test/units/truffle/flags.js index f6d968e..82a0674 100644 --- a/test/units/truffle/flags.js +++ b/test/units/truffle/flags.js @@ -25,7 +25,7 @@ describe('Truffle Plugin: command line options', function() { afterEach(() => mock.clean()); - it('truffle run coverage --file test/', async function() { + it('--file test/', async function() { verify.cleanInitialState(); truffleConfig.file = path.join( @@ -54,7 +54,7 @@ describe('Truffle Plugin: command line options', function() { verify.lineCoverage(expected); }); - it('truffle run coverage --file test/', async function() { + it('--file test/', async function() { verify.cleanInitialState(); truffleConfig.file = path.join( @@ -83,7 +83,7 @@ describe('Truffle Plugin: command line options', function() { verify.lineCoverage(expected); }); - it('truffle run coverage --file test/gl{o,b}*.js', async function() { + it('--file test/gl{o,b}*.js', async function() { verify.cleanInitialState(); truffleConfig.file = path.join( @@ -112,7 +112,7 @@ describe('Truffle Plugin: command line options', function() { verify.lineCoverage(expected); }); - it('truffle run coverage --config ../.solcover.js', async function() { + it('--config ../.solcover.js', async function() { verify.cleanInitialState(); solcoverConfig = { @@ -142,7 +142,7 @@ describe('Truffle Plugin: command line options', function() { shell.rm('.solcover.js'); }); - it('truffle run coverage --help', async function(){ + it('--help', async function(){ verify.cleanInitialState(); truffleConfig.help = "true"; @@ -157,7 +157,7 @@ describe('Truffle Plugin: command line options', function() { ); }) - it('truffle run coverage --version', async function(){ + it('--version', async function(){ verify.cleanInitialState(); truffleConfig.version = "true"; @@ -183,7 +183,7 @@ describe('Truffle Plugin: command line options', function() { }) - it('truffle run coverage --useGlobalTruffle', async function(){ + it('--useGlobalTruffle', async function(){ verify.cleanInitialState(); truffleConfig.useGlobalTruffle = true; @@ -198,7 +198,7 @@ describe('Truffle Plugin: command line options', function() { ); }); - it('truffle run coverage --usePluginTruffle', async function(){ + it('--usePluginTruffle', async function(){ verify.cleanInitialState(); truffleConfig.usePluginTruffle = true; @@ -212,5 +212,85 @@ describe('Truffle Plugin: command line options', function() { `Should notify it's using plugin truffle lib copy: ${mock.loggerOutput.val}` ); }); + + it('--usePluginTruffle', async function(){ + verify.cleanInitialState(); + + 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: ${mock.loggerOutput.val}` + ); + }); + + it('--network (network_id mismatch in configs)', async function(){ + verify.cleanInitialState(); + + truffleConfig.logger = mock.testLogger; + + solcoverConfig = { providerOptions: { network_id: 5 }} + + truffleConfig.network = 'development'; + truffleConfig.networks['development'].network_id = 7; + + mock.install('Simple', 'simple.js', solcoverConfig); + await plugin(truffleConfig); + + assert( + mock.loggerOutput.val.includes("'network_id' values"), + `Should notify about network_id values: ${mock.loggerOutput.val}` + ); + }); + + it('--network (truffle config missing port)', async function(){ + verify.cleanInitialState(); + + truffleConfig.logger = mock.testLogger; + + truffleConfig.network = 'development'; + truffleConfig.networks['development'].port = undefined; + + mock.install('Simple', 'simple.js', solcoverConfig); + await plugin(truffleConfig); + + assert( + mock.loggerOutput.val.includes("No 'port' was declared"), + `Should notify about missing port: ${mock.loggerOutput.val}` + ); + + assert( + mock.loggerOutput.val.includes("8555"), + `Should have used default coverage port 8555: ${mock.loggerOutput.val}` + ); + + }); + + it('--network (declared port mismatches)', async function(){ + verify.cleanInitialState(); + + truffleConfig.logger = mock.testLogger; + + truffleConfig.network = 'development'; // 8545 + solcoverConfig = { port: 8222 } + + mock.install('Simple', 'simple.js', solcoverConfig); + await plugin(truffleConfig); + + assert( + mock.loggerOutput.val.includes("The 'port' values"), + `Should notify about mismatched port values: ${mock.loggerOutput.val}` + ); + + assert( + mock.loggerOutput.val.includes("8545"), + `Should have used default coverage port 8545: ${mock.loggerOutput.val}` + ); + + }); }); diff --git a/test/units/truffle/standard.js b/test/units/truffle/standard.js index 167274d..2933a09 100644 --- a/test/units/truffle/standard.js +++ b/test/units/truffle/standard.js @@ -218,4 +218,43 @@ describe('Truffle Plugin: standard use cases', function() { mock.install('Expensive', 'block-gas-limit.js', solcoverConfig); await plugin(truffleConfig); }); + + // Simple.sol with a failing assertion in a truffle test + it('truffle tests failing', async function() { + verify.cleanInitialState(); + + mock.install('Simple', 'truffle-test-fail.js', solcoverConfig); + + try { + await plugin(truffleConfig); + assert.fail() + } catch(err){ + assert(err.message.includes('failed under coverage')); + } + + verify.coverageGenerated(truffleConfig); + + const output = mock.getOutput(truffleConfig); + 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"'); + }); + + it('uses the fallback server', async function(){ + verify.cleanInitialState(); + + truffleConfig.logger = mock.testLogger; + + solcoverConfig = { forceBackupServer: true } + + mock.install('Simple', 'simple.js', solcoverConfig); + await plugin(truffleConfig); + + assert( + mock.loggerOutput.val.includes("Using ganache-core-sc"), + `Should notify about backup server module: ${mock.loggerOutput.val}` + ); + + }); }) diff --git a/yarn.lock b/yarn.lock index 122dbbd..aa8893d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -166,6 +166,30 @@ dependencies: defer-to-connect "^1.0.1" +"@truffle/error@^0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@truffle/error/-/error-0.0.7.tgz#e9db39885575647ef08bf624b0c13fe46d41a209" + integrity sha512-UIfVKsXSXocKnn5+RNklUXNoGd/JVj7V8KmC48TQzmjU33HQI86PX0JDS7SpHMHasI3w9X//1q7Lu7nZtj3Zzg== + +"@truffle/interface-adapter@^0.2.6": + version "0.2.6" + resolved "https://registry.yarnpkg.com/@truffle/interface-adapter/-/interface-adapter-0.2.6.tgz#41bdf5a1a120e8b76c441fa1a746bdc3c6d3fad2" + integrity sha512-8ByMwqC9hWaHQDlwCEMoIhEewvkZXeeywu46/7yM7BHE1XoSDjiQzWjagg6nLJwmFRwB1ZHYC3RXiDyFJLWd0g== + dependencies: + bn.js "^4.11.8" + ethers "^4.0.32" + lodash "^4.17.13" + web3 "1.2.1" + +"@truffle/provider@^0.1.17": + version "0.1.17" + resolved "https://registry.yarnpkg.com/@truffle/provider/-/provider-0.1.17.tgz#9b72a76a7ce18a054eabb3ebd21a55a33dc7d7b6" + integrity sha512-V8k2jErxotpMt/R6qS7GL0stM1TWsuJYnm6iRcYDGwp0D33pZDLrnmwsuIbtXIfbAuaJ26kQHu2LGCUxuFh0LA== + dependencies: + "@truffle/error" "^0.0.7" + "@truffle/interface-adapter" "^0.2.6" + web3 "1.2.1" + "@types/bn.js@^4.11.2": version "4.11.5" resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-4.11.5.tgz#40e36197433f78f807524ec623afcf0169ac81dc"