From a3b52676fc02adcf87909ff79251516c1592818b Mon Sep 17 00:00:00 2001 From: Jonathan Rainville Date: Thu, 17 Oct 2019 14:39:25 -0400 Subject: [PATCH] Fix part of the test app and add new test util functions (#1977) * fix: fix tests hanging because the console is not started * fix(@embark/proxy): send back errors correctly to the client Code originally by @emizzle and fixed by me * feat(@embark/test-runner): add assert.reverts to test reverts * fix: make test app actually run its test and not hang * fix(@embark/proxy): fix listening to contract event in the proxy * feat(@embark/test-runner): add assertion for events being triggered * docs(@embark/site): add docs for the new assert functions * feat(@embark/test-runner): add increaseTime util function to globals * docs(@embark/site): add docs for increaseTime --- dapps/tests/app/app/contracts/expiration.sol | 16 ++ .../app/app/contracts/simple_storage.sol | 5 + dapps/tests/app/test/another_storage_spec.js | 4 +- dapps/tests/app/test/expiration_spec.js | 25 ++ dapps/tests/app/test/interface_spec.js | 21 +- .../app/test/simple_storage_deploy_spec.js | 2 +- dapps/tests/app/test/simple_storage_spec.js | 8 + .../ethereum-blockchain-client/src/index.js | 64 ++--- packages/plugins/mocha-tests/src/lib/index.js | 36 ++- .../specialconfigs/src/functionConfigs.js | 2 +- .../plugins/specialconfigs/src/listConfigs.js | 2 +- packages/plugins/web3/src/index.js | 11 +- packages/stack/proxy/src/proxy.js | 252 +++++++++++------- packages/stack/test-runner/package.json | 3 +- packages/stack/test-runner/src/lib/index.js | 87 ++++++ site/package.json | 4 +- site/source/docs/contracts_testing.md | 62 +++++ 17 files changed, 446 insertions(+), 158 deletions(-) create mode 100644 dapps/tests/app/app/contracts/expiration.sol create mode 100644 dapps/tests/app/test/expiration_spec.js diff --git a/dapps/tests/app/app/contracts/expiration.sol b/dapps/tests/app/app/contracts/expiration.sol new file mode 100644 index 000000000..ab77e18e0 --- /dev/null +++ b/dapps/tests/app/app/contracts/expiration.sol @@ -0,0 +1,16 @@ +pragma solidity ^0.4.25; + +contract Expiration { + uint public expirationTime; // In milliseconds + address owner; + + constructor(uint expiration) public { + expirationTime = expiration; + } + + function isExpired() public view returns (bool retVal) { +// retVal = block.timestamp; + retVal = expirationTime < block.timestamp * 1000; + return retVal; + } +} diff --git a/dapps/tests/app/app/contracts/simple_storage.sol b/dapps/tests/app/app/contracts/simple_storage.sol index 59acff94d..3afc8d780 100644 --- a/dapps/tests/app/app/contracts/simple_storage.sol +++ b/dapps/tests/app/app/contracts/simple_storage.sol @@ -23,6 +23,11 @@ contract SimpleStorage { emit EventOnSet2(true, "hi"); } + function set3(uint x) public { + require(x > 5, "Value needs to be higher than 5"); + storedData = x; + } + function get() public view returns (uint retVal) { return storedData; } diff --git a/dapps/tests/app/test/another_storage_spec.js b/dapps/tests/app/test/another_storage_spec.js index cf39cef1e..78fe16143 100644 --- a/dapps/tests/app/test/another_storage_spec.js +++ b/dapps/tests/app/test/another_storage_spec.js @@ -3,7 +3,7 @@ const assert = require('assert'); const AnotherStorage = require('Embark/contracts/AnotherStorage'); const SimpleStorage = require('Embark/contracts/SimpleStorage'); -let accounts; +let accounts, defaultAccount; config({ blockchain: { @@ -26,10 +26,10 @@ config({ } }, (err, theAccounts) => { accounts = theAccounts; + defaultAccount = accounts[0]; }); contract("AnotherStorage", function(accountsAgain) { - const defaultAccount = accounts[0]; this.timeout(0); it("should have got the default account in the describe", function () { diff --git a/dapps/tests/app/test/expiration_spec.js b/dapps/tests/app/test/expiration_spec.js new file mode 100644 index 000000000..9b1de8795 --- /dev/null +++ b/dapps/tests/app/test/expiration_spec.js @@ -0,0 +1,25 @@ +/*global contract, config, it, assert, increaseTime*/ +const Expiration = require('Embark/contracts/Expiration'); + +config({ + contracts: { + deploy: { + "Expiration": { + args: [Date.now() + 5000] + } + } + } +}); + +contract("Expiration", function() { + it("should not have expired yet", async function () { + const isExpired = await Expiration.methods.isExpired().call(); + assert.strictEqual(isExpired, false); + }); + + it("should have expired after skipping time", async function () { + await increaseTime(5001); + const isExpired = await Expiration.methods.isExpired().call(); + assert.strictEqual(isExpired, true); + }); +}); diff --git a/dapps/tests/app/test/interface_spec.js b/dapps/tests/app/test/interface_spec.js index d00a63764..cea91e759 100644 --- a/dapps/tests/app/test/interface_spec.js +++ b/dapps/tests/app/test/interface_spec.js @@ -2,21 +2,22 @@ const assert = require('assert'); const AnotherStorage = require('Embark/contracts/AnotherStorage'); -config({ - contracts: { - deploy: { - AnotherStorage: { - args: ['$ERC20'] - } - } - } -}); +// FIXME this doesn't work and no idea how it ever worked because ERC20 is not defined anywhere +// config({ +// contracts: { +// deploy: { +// AnotherStorage: { +// args: ['$ERC20'] +// } +// } +// } +// }); contract("AnotherStorageWithInterface", function() { this.timeout(0); - it("sets an empty address because ERC20 is an interface", async function() { + xit("sets an empty address because ERC20 is an interface", async function() { let result = await AnotherStorage.methods.simpleStorageAddress().call(); assert.strictEqual(result.toString(), '0x0000000000000000000000000000000000000000'); }); diff --git a/dapps/tests/app/test/simple_storage_deploy_spec.js b/dapps/tests/app/test/simple_storage_deploy_spec.js index 48f47eff9..58e60fb72 100644 --- a/dapps/tests/app/test/simple_storage_deploy_spec.js +++ b/dapps/tests/app/test/simple_storage_deploy_spec.js @@ -1,5 +1,5 @@ /*global contract, it, embark, assert, before, web3*/ -const SimpleStorage = embark.require('Embark/contracts/SimpleStorage'); +const SimpleStorage = require('Embark/contracts/SimpleStorage'); const {Utils} = require('Embark/EmbarkJS'); contract("SimpleStorage Deploy", function () { diff --git a/dapps/tests/app/test/simple_storage_spec.js b/dapps/tests/app/test/simple_storage_spec.js index 1d0b70007..f66a3776b 100644 --- a/dapps/tests/app/test/simple_storage_spec.js +++ b/dapps/tests/app/test/simple_storage_spec.js @@ -59,4 +59,12 @@ contract("SimpleStorage", function() { SimpleStorage.methods.set2(150).send(); }); + it('asserts event triggered', async function() { + const tx = await SimpleStorage.methods.set2(160).send(); + assert.eventEmitted(tx, 'EventOnSet2', {passed: true, message: "hi"}); + }); + + it("should revert with a value lower than 5", async function() { + await assert.reverts(SimpleStorage.methods.set3(2), {from: web3.eth.defaultAccount}, 'Returned error: VM Exception while processing transaction: revert Value needs to be higher than 5'); + }); }); diff --git a/packages/plugins/ethereum-blockchain-client/src/index.js b/packages/plugins/ethereum-blockchain-client/src/index.js index 20764cf1f..fd1b58fda 100644 --- a/packages/plugins/ethereum-blockchain-client/src/index.js +++ b/packages/plugins/ethereum-blockchain-client/src/index.js @@ -57,37 +57,41 @@ class EthereumBlockchainClient { } async deployer(contract, done) { - const web3 = await this.web3; - const [account] = await web3.eth.getAccounts(); - const contractObj = new web3.eth.Contract(contract.abiDefinition, contract.address); - const code = contract.code.substring(0, 2) === '0x' ? contract.code : "0x" + contract.code; - const contractObject = contractObj.deploy({arguments: (contract.args || []), data: code}); - - if (contract.gas === 'auto' || !contract.gas) { - const gasValue = await contractObject.estimateGas(); - const increase_per = 1 + (Math.random() / 10.0); - contract.gas = Math.floor(gasValue * increase_per); - } - - if (!contract.gasPrice) { - const gasPrice = await web3.eth.getGasPrice(); - contract.gasPrice = contract.gasPrice || gasPrice; - } - - embarkJsUtils.secureSend(web3, contractObject, { - from: account, gas: contract.gas - }, true, (err, receipt) => { - if (err) { - return done(err); + try { + const web3 = await this.web3; + const [account] = await web3.eth.getAccounts(); + const contractObj = new web3.eth.Contract(contract.abiDefinition, contract.address); + const code = contract.code.substring(0, 2) === '0x' ? contract.code : "0x" + contract.code; + const contractObject = contractObj.deploy({arguments: (contract.args || []), data: code}); + if (contract.gas === 'auto' || !contract.gas) { + const gasValue = await contractObject.estimateGas(); + const increase_per = 1 + (Math.random() / 10.0); + contract.gas = Math.floor(gasValue * increase_per); } - contract.deployedAddress = receipt.contractAddress; - contract.transactionHash = receipt.transactionHash; - contract.log(`${contract.className.bold.cyan} ${__('deployed at').green} ${receipt.contractAddress.bold.cyan} ${__("using").green} ${receipt.gasUsed} ${__("gas").green} (txHash: ${receipt.transactionHash.bold.cyan})`); - done(err, receipt); - }, (hash) => { - const estimatedCost = contract.gas * contract.gasPrice; - contract.log(`${__("Deploying")} ${contract.className.bold.cyan} ${__("with").green} ${contract.gas} ${__("gas at the price of").green} ${contract.gasPrice} ${__("Wei. Estimated cost:").green} ${estimatedCost} ${"Wei".green} (txHash: ${hash.bold.cyan})`); - }); + + if (!contract.gasPrice) { + const gasPrice = await web3.eth.getGasPrice(); + contract.gasPrice = contract.gasPrice || gasPrice; + } + + embarkJsUtils.secureSend(web3, contractObject, { + from: account, gas: contract.gas + }, true, (err, receipt) => { + if (err) { + return done(err); + } + contract.deployedAddress = receipt.contractAddress; + contract.transactionHash = receipt.transactionHash; + contract.log(`${contract.className.bold.cyan} ${__('deployed at').green} ${receipt.contractAddress.bold.cyan} ${__("using").green} ${receipt.gasUsed} ${__("gas").green} (txHash: ${receipt.transactionHash.bold.cyan})`); + done(err, receipt); + }, (hash) => { + const estimatedCost = contract.gas * contract.gasPrice; + contract.log(`${__("Deploying")} ${contract.className.bold.cyan} ${__("with").green} ${contract.gas} ${__("gas at the price of").green} ${contract.gasPrice} ${__("Wei. Estimated cost:").green} ${estimatedCost} ${"Wei".green} (txHash: ${hash.bold.cyan})`); + }); + } catch (e) { + this.logger.error(__('Error deploying contract %s', contract.className.underline)); + done(e); + } } async doLinking(params, callback) { diff --git a/packages/plugins/mocha-tests/src/lib/index.js b/packages/plugins/mocha-tests/src/lib/index.js index 93b48fe2e..4ab166d09 100644 --- a/packages/plugins/mocha-tests/src/lib/index.js +++ b/packages/plugins/mocha-tests/src/lib/index.js @@ -1,6 +1,5 @@ import {__} from 'embark-i18n'; -const assert = require('assert').strict; const async = require('async'); const EmbarkJS = require('embarkjs'); const Mocha = require('mocha'); @@ -72,7 +71,15 @@ class MochaTestRunner { events.request("contracts:build", cfg, compiledContracts, next); }, (contractsList, contractDeps, next) => { - events.request("deployment:contracts:deploy", contractsList, contractDeps, next); + // Remove contracts that are not in the configs + const realContracts = {}; + const deployKeys = Object.keys(cfg.contracts); + Object.keys(contractsList).forEach((className) => { + if (deployKeys.includes(className)) { + realContracts[className] = contractsList[className]; + } + }); + events.request("deployment:contracts:deploy", realContracts, contractDeps, next); }, (_result, next) => { events.request("contracts:list", next); @@ -159,17 +166,22 @@ class MochaTestRunner { Module.prototype.require = function(req) { const prefix = "Embark/contracts/"; - if (!req.startsWith(prefix)) { - return originalRequire.apply(this, arguments); + if (req.startsWith(prefix)) { + const contractClass = req.replace(prefix, ""); + const instance = compiledContracts[contractClass]; + + if (!instance) { + compiledContracts[contractClass] = {}; + return compiledContracts[contractClass]; + // throw new Error(`Cannot find module '${req}'`); + } + return instance; + } + if (req === "Embark/EmbarkJS") { + return EmbarkJS; } - const contractClass = req.replace(prefix, ""); - const instance = compiledContracts[contractClass]; - - if (!instance) { - throw new Error(`Cannot find module '${req}'`); - } - return instance; + return originalRequire.apply(this, arguments); }; const mocha = new Mocha(); @@ -181,11 +193,9 @@ class MochaTestRunner { mocha.suite.on('pre-require', () => { global.describe = describeWithAccounts; global.contract = describeWithAccounts; - global.assert = assert; global.config = config; }); - mocha.suite.timeout(TEST_TIMEOUT); mocha.addFile(file); diff --git a/packages/plugins/specialconfigs/src/functionConfigs.js b/packages/plugins/specialconfigs/src/functionConfigs.js index 4476f17d8..3292a5f08 100644 --- a/packages/plugins/specialconfigs/src/functionConfigs.js +++ b/packages/plugins/specialconfigs/src/functionConfigs.js @@ -90,7 +90,7 @@ class FunctionConfigs { const contractRegisteredInVM = await this.checkContractRegisteredInVM(contract); if (!contractRegisteredInVM) { // eslint-disable-next-line no-await-in-loop - await this.events.request2("embarkjs:contract:runInVM", contract); + await this.events.request2("embarkjs:contract:runInVm", contract); } // eslint-disable-next-line no-await-in-loop let contractInstance = await this.events.request2("runcode:eval", contract.className); diff --git a/packages/plugins/specialconfigs/src/listConfigs.js b/packages/plugins/specialconfigs/src/listConfigs.js index 3ec7374ae..70c1372fe 100644 --- a/packages/plugins/specialconfigs/src/listConfigs.js +++ b/packages/plugins/specialconfigs/src/listConfigs.js @@ -137,7 +137,7 @@ class ListConfigs { } let referedContractName = match.slice(1); - this.events.request('contracts:contract', referedContractName, (referedContract) => { + this.events.request('contracts:contract', referedContractName, (_err, referedContract) => { if (!referedContract) { this.logger.error(referedContractName + ' does not exist'); this.logger.error("error running cmd: " + cmd); diff --git a/packages/plugins/web3/src/index.js b/packages/plugins/web3/src/index.js index c09d16e48..4cbbbf324 100644 --- a/packages/plugins/web3/src/index.js +++ b/packages/plugins/web3/src/index.js @@ -16,7 +16,6 @@ class EmbarkWeb3 { this.events = embark.events; this.config = embark.config; - this.setupWeb3Api(); this.setupEmbarkJS(); embark.registerActionForEvent("deployment:contract:deployed", this.registerInVm.bind(this)); @@ -34,24 +33,20 @@ class EmbarkWeb3 { await this.events.request2("embarkjs:console:register", 'blockchain', 'web3', 'embarkjs-web3'); } - async setupWeb3Api() { - this.events.request("runcode:whitelist", 'web3', () => { }); - this.events.on("blockchain:started", this.registerWeb3Object.bind(this)); - } - async registerWeb3Object() { const provider = await this.events.request2("blockchain:client:provider", "ethereum"); const web3 = new Web3(provider); + this.events.request("runcode:whitelist", 'web3', () => {}); await this.events.request2("runcode:register", 'web3', web3); const accounts = await web3.eth.getAccounts(); if (accounts.length) { await this.events.request2('runcode:eval', `web3.eth.defaultAccount = '${accounts[0]}'`); } - await this.events.request2('console:register:helpCmd', { + this.events.request('console:register:helpCmd', { cmdName: "web3", cmdHelp: __("instantiated web3.js object configured to the current environment") - }); + }, () => {}); } async registerInVm(params, cb) { diff --git a/packages/stack/proxy/src/proxy.js b/packages/stack/proxy/src/proxy.js index 2c91cc665..7ef1b1597 100644 --- a/packages/stack/proxy/src/proxy.js +++ b/packages/stack/proxy/src/proxy.js @@ -1,5 +1,5 @@ /* global Buffer exports require */ -import {__} from 'embark-i18n'; +import { __ } from 'embark-i18n'; import express from 'express'; import expressWs from 'express-ws'; import cors from 'cors'; @@ -18,17 +18,17 @@ export class Proxy { this.logger = options.logger; this.vms = options.vms; this.app = null; - this.server = null; + this.requestManager; } async serve(endpoint, localHost, localPort, ws) { if (endpoint === constants.blockchain.vm) { endpoint = this.vms[this.vms.length - 1](); } - const requestManager = new Web3RequestManager.Manager(endpoint); + this.requestManager = new Web3RequestManager.Manager(endpoint); try { - await requestManager.send({method: 'eth_accounts'}); + await this.requestManager.send({ method: 'eth_accounts' }); } catch (e) { throw new Error(__('Unable to connect to the blockchain endpoint')); } @@ -43,47 +43,36 @@ export class Proxy { this.app.use(express.urlencoded({extended: true})); if (ws) { - this.app.ws('/', (ws, _wsReq) => { - ws.on('message', (msg) => { - let jsonMsg; + this.app.ws('/', async (ws, _wsReq) => { + // Watch from subscription data for events + this.requestManager.provider.on('data', function(result, deprecatedResult) { + ws.send(JSON.stringify(result || deprecatedResult)) + }); + + ws.on('message', async (msg) => { try { - jsonMsg = JSON.parse(msg); - } catch (e) { - this.logger.error(__('Error parsing request'), e.message); - return; + const jsonMsg = JSON.parse(msg); + await this.processRequest(jsonMsg, ws, true); + } + catch (err) { + const error = __('Error processing request: %s', err.message); + this.logger.error(error); + this.respondWs(ws, error); } - // Modify request - this.emitActionsForRequest(jsonMsg, (_err, resp) => { - // Send the possibly modified request to the Node - requestManager.send(resp.reqData, (err, result) => { - if (err) { - this.logger.debug(JSON.stringify(resp.reqData)); - return this.logger.error(__('Error executing the request on the Node'), err.message || err); - } - this.emitActionsForResponse(resp.reqData, {jsonrpc: "2.0", id: resp.reqData.id, result}, (_err, resp) => { - // Send back to the caller (web3) - ws.send(JSON.stringify(resp.respData)); - }); - }); - }); }); }); } else { // HTTP - this.app.use((req, res) => { + this.app.use(async (req, res) => { // Modify request - this.emitActionsForRequest(req.body, (_err, resp) => { - // Send the possibly modified request to the Node - requestManager.send(resp.reqData, (err, result) => { - if (err) { - return res.status(500).send(err.message || err); - } - this.emitActionsForResponse(resp.reqData, {jsonrpc: "2.0", id: resp.reqData.id, result}, (_err, resp) => { - // Send back to the caller (web3) - res.status(200).send(resp.respData); - }); - }); - }); + try { + await this.processRequest(req, res, false); + } + catch (err) { + const error = __('Error processing request: %s', err.message); + this.logger.error(error); + this.respondHttp(res, 500, error); + } }); } @@ -95,65 +84,150 @@ export class Proxy { }); } - emitActionsForRequest(body, cb) { - let calledBack = false; - setTimeout(() => { - if (calledBack) { - return; - } - this.logger.warn(__('Action for request "%s" timed out', body.method)); - this.logger.debug(body); - cb(null, {reqData: body}); - calledBack = true; - }, ACTION_TIMEOUT); + async processRequest(request, transport, isWs) { + // Modify request + let modifiedRequest; + const rpcRequest = request.method === "POST" ? request.body : request; + try { + modifiedRequest = await this.emitActionsForRequest(rpcRequest); + } + catch (reqError) { + const error = reqError.message || reqError; + this.logger.error(__(`Error executing request actions: ${error}`)); + // TODO: Change error code to be more specific. Codes in section 5.1 of the JSON-RPC spec: https://www.jsonrpc.org/specification + const rpcErrorObj = { "jsonrpc": "2.0", "error": { "code": -32603, "message": error }, "id": request.id }; + return this.respondError(transport, rpcErrorObj, isWs); + } - this.plugins.emitAndRunActionsForEvent('blockchain:proxy:request', - {reqData: body}, - (err, resp) => { - if (err) { - this.logger.error(__('Error parsing the request in the proxy')); - this.logger.error(err); - // Reset the data to the original request so that it can be used anyway - resp = {reqData: body}; - } - if (calledBack) { - // Action timed out - return; - } - cb(null, resp); - calledBack = true; - }); + // Send the possibly modified request to the Node + const respData = { jsonrpc: "2.0", id: modifiedRequest.reqData.id }; + if (modifiedRequest.sendToNode !== false) { + try { + const result = await this.forwardRequestToNode(modifiedRequest.reqData); + respData.result = result; + } + catch (fwdReqErr) { + // the node responded with an error. Set up the error so that it can be + // stripped out by modifying the response (via actions for blockchain:proxy:response) + respData.error = fwdReqErr.message || fwdReqErr; + } + } + + try { + const modifiedResp = await this.emitActionsForResponse(modifiedRequest.reqData, respData); + // Send back to the caller (web3) + if (modifiedResp && modifiedResp.respData && modifiedResp.respData.error) { + // error returned from the node and it wasn't stripped by our response actions + const error = modifiedResp.respData.error.message || modifiedResp.respData.error; + this.logger.error(__(`Error returned from the node: ${error}`)); + const rpcErrorObj = { "jsonrpc": "2.0", "error": { "code": -32603, "message": error }, "id": modifiedResp.respData.id }; + return this.respondError(transport, rpcErrorObj, isWs); + } + this.respondOK(transport, modifiedResp.respData, isWs); + } + catch (resError) { + // if was an error in response actions (resError), send the error in the response + const error = resError.message || resError; + this.logger.error(__(`Error executing response actions: ${error}`)); + const rpcErrorObj = { "jsonrpc": "2.0", "error": { "code": -32603, "message": error }, "id": modifiedRequest.reqData.id }; + return this.respondError(transport, rpcErrorObj, isWs); + } } - emitActionsForResponse(reqData, respData, cb) { - let calledBack = false; - setTimeout(() => { - if (calledBack) { - return; - } - this.logger.warn(__('Action for request "%s" timed out', reqData.method)); - this.logger.debug(reqData); - this.logger.debug(respData); - cb(null, {respData}); - calledBack = true; - }, ACTION_TIMEOUT); - - this.plugins.emitAndRunActionsForEvent('blockchain:proxy:response', - {respData, reqData}, - (err, resp) => { - if (err) { - this.logger.error(__('Error parsing the response in the proxy')); - this.logger.error(err); - // Reset the data to the original response so that it can be used anyway - resp = {respData}; + forwardRequestToNode(reqData) { + return new Promise((resolve, reject) => { + this.requestManager.send(reqData, (fwdReqErr, result) => { + if (fwdReqErr) { + return reject(fwdReqErr); } + resolve(result); + }); + }); + } + + respondWs(ws, response) { + if (typeof response === "object") { + response = JSON.stringify(response); + } + ws.send(response); + } + respondHttp(res, statusCode, response) { + res.status(statusCode).send(response); + } + + respondError(transport, error, isWs) { + return isWs ? this.respondWs(transport, error) : this.respondHttp(transport, 500, error) + } + + respondOK(transport, response, isWs) { + return isWs ? this.respondWs(transport, response) : this.respondHttp(transport, 200, response) + } + + emitActionsForRequest(body) { + return new Promise((resolve, reject) => { + let calledBack = false; + setTimeout(() => { if (calledBack) { - // Action timed out return; } - cb(null, resp); + this.logger.warn(__('Action for request "%s" timed out', body.method)); + this.logger.debug(body); calledBack = true; - }); + resolve({ reqData: body }); + }, ACTION_TIMEOUT); + + this.plugins.emitAndRunActionsForEvent('blockchain:proxy:request', + { reqData: body }, + (err, resp) => { + if (calledBack) { + // Action timed out + return; + } + if (err) { + this.logger.error(__('Error parsing the request in the proxy')); + this.logger.error(err); + // Reset the data to the original request so that it can be used anyway + resp = { reqData: body }; + calledBack = true; + return reject(err); + } + calledBack = true; + resolve(resp); + }); + }); + } + + emitActionsForResponse(reqData, respData) { + return new Promise((resolve, reject) => { + let calledBack = false; + setTimeout(() => { + if (calledBack) { + return; + } + this.logger.warn(__('Action for response "%s" timed out', reqData.method)); + this.logger.debug(reqData); + this.logger.debug(respData); + calledBack = true; + resolve({ respData }); + }, ACTION_TIMEOUT); + + this.plugins.emitAndRunActionsForEvent('blockchain:proxy:response', + { respData, reqData }, + (err, resp) => { + if (calledBack) { + // Action timed out + return; + } + if (err) { + this.logger.error(__('Error parsing the response in the proxy')); + this.logger.error(err); + calledBack = true; + reject(err); + } + calledBack = true; + resolve(resp); + }); + }); } stop() { diff --git a/packages/stack/test-runner/package.json b/packages/stack/test-runner/package.json index d0d885f87..84109ff76 100644 --- a/packages/stack/test-runner/package.json +++ b/packages/stack/test-runner/package.json @@ -56,7 +56,8 @@ "istanbul-lib-report": "2.0.8", "istanbul-reports": "2.2.4", "mocha": "6.2.0", - "open": "6.4.0" + "open": "6.4.0", + "web3": "1.2.1" }, "devDependencies": { "@types/async": "2.0.50", diff --git a/packages/stack/test-runner/src/lib/index.js b/packages/stack/test-runner/src/lib/index.js index 685c8ebd6..c3e0e546e 100644 --- a/packages/stack/test-runner/src/lib/index.js +++ b/packages/stack/test-runner/src/lib/index.js @@ -1,5 +1,6 @@ import { __ } from 'embark-i18n'; import {buildUrl, deconstructUrl, recursiveMerge} from "embark-utils"; +const assert = require('assert').strict; const async = require('async'); const chalk = require('chalk'); const path = require('path'); @@ -7,6 +8,7 @@ const { dappPath } = require('embark-utils'); import cloneDeep from "lodash.clonedeep"; import { COVERAGE_GAS_LIMIT, GAS_LIMIT } from './constants'; const constants = require('embark-core/constants'); +const Web3 = require('web3'); const coverage = require('istanbul-lib-coverage'); const reporter = require('istanbul-lib-report'); @@ -48,7 +50,12 @@ class TestRunner { const reporter = new Reporter(this.embark); const testPath = options.file || "test"; + this.setupGlobalVariables(); + async.waterfall([ + (next) => { + this.events.request("config:contractsConfig:set", Object.assign(this.configObj.contractsConfig, {explicit: true}), next); + }, (next) => { this.getFilesFromDir(testPath, next); }, @@ -102,6 +109,55 @@ class TestRunner { }); } + setupGlobalVariables() { + assert.reverts = async function(method, params = {}, message) { + if (typeof params === 'string') { + message = params; + params = {}; + } + try { + await method.send(params); + } catch (error) { + if (message) { + assert.strictEqual(error.message, message); + } else { + assert.ok(error); + } + return; + } + assert.fail('Method did not revert'); + }; + + assert.eventEmitted = function(transaction, event, values) { + if (!transaction.events) { + return assert.fail('No events triggered for the transaction'); + } + if (values === undefined || values === null || !transaction.events[event]) { + return assert.ok(transaction.events[event], `Event ${event} was not triggered`); + } + if (Array.isArray(values)) { + values.forEach((value, index) => { + assert.strictEqual(transaction.events[event].returnValues[index], value, `Value at index ${index} incorrect.\n\tExpected: ${value}\n\tActual: ${transaction.events[event].returnValues[index]}`); + }); + return; + } + if (typeof values === 'object') { + Object.keys(values).forEach(key => { + assert.strictEqual(transaction.events[event].returnValues[key], values[key], `Value at key "${key}" incorrect.\n\tExpected: ${values[key]}\n\tActual: ${transaction.events[event].returnValues[key]}`); + }); + } + }; + + global.assert = assert; + + global.embark = this.embark; + + global.increaseTime = async (amount) => { + await this.evmMethod("evm_increaseTime", [Number(amount)]); + await this.evmMethod("evm_mine"); + }; + } + generateCoverageReport() { const coveragePath = dappPath(".embark", "coverage.json"); const coverageMap = JSON.parse(this.fs.readFileSync(coveragePath)); @@ -212,6 +268,37 @@ class TestRunner { cb(null, provider); return provider; } + + get web3() { + return (async () => { + if (!this._web3) { + const provider = await this.events.request2("blockchain:client:provider", "ethereum"); + this._web3 = new Web3(provider); + } + return this._web3; + })(); + } + + evmMethod(method, params = []) { + return new Promise(async (resolve, reject) => { + const web3 = await this.web3; + const sendMethod = (web3.currentProvider.sendAsync) ? web3.currentProvider.sendAsync.bind(web3.currentProvider) : web3.currentProvider.send.bind(web3.currentProvider); + sendMethod( + { + jsonrpc: '2.0', + method, + params, + id: Date.now().toString().substring(9) + }, + (error, res) => { + if (error) { + return reject(error); + } + resolve(res.result); + } + ); + }); + } } module.exports = TestRunner; diff --git a/site/package.json b/site/package.json index 309533f23..b7a37a036 100644 --- a/site/package.json +++ b/site/package.json @@ -4,7 +4,7 @@ "private": true, "license": "MIT", "hexo": { - "version": "3.9.0" + "version": "3.8.0" }, "dependencies": { "cheerio": "^0.22.0", @@ -42,4 +42,4 @@ "gulp-useref": "^3.1.6", "rename": "^1.0.4" } -} +} \ No newline at end of file diff --git a/site/source/docs/contracts_testing.md b/site/source/docs/contracts_testing.md index 60174a4f6..5a84da7d8 100644 --- a/site/source/docs/contracts_testing.md +++ b/site/source/docs/contracts_testing.md @@ -241,6 +241,68 @@ contract('SimpleStorage Deploy', () => { }); ``` +## Util functions + +### assert.reverts + +Using `assert.reverts`, you can easily assert that your transaction reverts. + +```javascript +await assert.reverts(contractMethodAndArguments[, options][, message]) +``` + +- `contractMethodAndArguments`: [Function] Contract method to call `send` on, including the arguments +- `options`: [Object] Optional options to pass to the `send` function +- `message`: [String] Optional string to match the revert message + +Returns a promise that you can wait for with `await`. + +```javascript +it("should revert with a value lower than 5", async function() { + await assert.reverts(SimpleStorage.methods.setHigher5(2), {from: web3.eth.defaultAccount}, + 'Returned error: VM Exception while processing transaction: revert Value needs to be higher than 5'); + }); +``` + +### assert.eventEmitted + +Using `eventEmitted`, you can assert that a transaction has emitted an event. You can also check for the returned values. + +```javascript +assert.eventEmitted(transaction, event[, values]) +``` + +- `transaction`: [Object] Transaction object returns by a `send` call +- `event`: [String] Name of the event being emitted +- `values`: [Array or Object] Optional array or object of the returned values of the event. + - Using array: The order of the values put in the array need to match the order in which the values are returned by the event + - Using object: The object needs to have the right key/value pair(s) + +```javascript +it('asserts that the event was triggered', async function() { + const transaction = await SimpleStorage.methods.set(100).send(); + assert.eventEmitted(transaction, 'EventOnSet', {value: "100", success: true}); +}); +``` + +### increaseTime + +This function lets you increase the time of the EVM. It is useful in the case where you want to test expiration times for example. + +```javascript +await increaseTime(amount); +``` + +`amount`: [Number] Number of seconds to increase + +```javascript +it("should have expired after increasing time", async function () { + await increaseTime(5001); + const isExpired = await Expiration.methods.isExpired().call(); + assert.strictEqual(isExpired, true); +}); +``` + ## Code coverage Embark allows you to generate a coverage report for your Solidity Smart Contracts by passing the `--coverage` option on the `embark test` command.