feat(test/reporter): log tx functions during tests

Extract function log function to utils to use in the reporter
  to show txs in tests
This commit is contained in:
Jonathan Rainville 2019-02-27 13:59:11 -05:00
parent 89753c11e3
commit 87d92b6091
6 changed files with 304 additions and 136 deletions

View File

@ -4,4 +4,5 @@ export interface Contract {
abiDefinition: ABIDefinition[]; abiDefinition: ABIDefinition[];
deployedAddress: string; deployedAddress: string;
className: string; className: string;
silent?: boolean;
} }

View File

@ -1,5 +1,6 @@
const async = require('async'); const async = require('async');
const utils = require('../../utils/utils.js'); const utils = require('../../utils/utils.js');
const {getAddressToContract, getTransactionParams} = require('../../utils/transactionUtils');
class ConsoleListener { class ConsoleListener {
constructor(embark, options) { constructor(embark, options) {
@ -27,7 +28,7 @@ class ConsoleListener {
this.contractsDeployed = true; this.contractsDeployed = true;
this._getContractsList((contractsList) => { this._getContractsList((contractsList) => {
this._updateContractList(contractsList); this.addressToContract = getAddressToContract(contractsList, this.addressToContract);
}); });
}); });
@ -57,37 +58,6 @@ class ConsoleListener {
}); });
} }
_updateContractList(contractsList) {
if (!contractsList) return;
contractsList.forEach(contract => {
if (!contract.deployedAddress) return;
let address = contract.deployedAddress.toLowerCase();
if (!this.addressToContract[address]) {
let funcSignatures = {};
contract.abiDefinition
.filter(func => func.type === "function")
.map(func => {
const name = func.name +
'(' +
(func.inputs ? func.inputs.map(input => input.type).join(',') : '') +
')';
funcSignatures[utils.sha3(name).substring(0, 10)] = {
name,
abi: func,
functionName: func.name
};
});
this.addressToContract[address] = {
name: contract.className,
functions: funcSignatures,
silent: contract.silent
};
}
});
}
_listenForLogRequests() { _listenForLogRequests() {
this.events.on('deploy:contract:receipt', receipt => { this.events.on('deploy:contract:receipt', receipt => {
this.events.emit('contracts:log', { this.events.emit('contracts:log', {
@ -121,26 +91,14 @@ class ConsoleListener {
if (!contract) { if (!contract) {
this.logger.info(`Contract log for unknown contract: ${JSON.stringify(request)}`); this.logger.info(`Contract log for unknown contract: ${JSON.stringify(request)}`);
return this._getContractsList((contractsList) => { return this._getContractsList((contractsList) => {
this._updateContractList(contractsList); this.addressToContract = getAddressToContract(contractsList, this.addressToContract);
}); });
} }
const {name, silent} = contract; const {name, silent} = contract;
if (silent && !this.outputDone) { if (silent && !this.outputDone) {
return; return;
} }
const {functionName, paramString} = getTransactionParams(contract, data);
const func = contract.functions[data.substring(0, 10)];
const functionName = func.functionName;
const decodedParameters = utils.decodeParams(func.abi.inputs, data.substring(10));
let paramString = "";
if (func.abi.inputs) {
func.abi.inputs.forEach((input) => {
let quote = input.type.indexOf("int") === -1 ? '"' : '';
paramString += quote + decodedParameters[input.name] + quote + ", ";
});
paramString = paramString.substring(0, paramString.length - 2);
}
gasUsed = utils.hexToNumber(gasUsed); gasUsed = utils.hexToNumber(gasUsed);
blockNumber = utils.hexToNumber(blockNumber); blockNumber = utils.hexToNumber(blockNumber);

View File

@ -1,6 +1,7 @@
const Base = require('mocha/lib/reporters/base'); const Base = require('mocha/lib/reporters/base');
const ms = require('mocha/lib/ms'); const ms = require('mocha/lib/ms');
const color = Base.color; const color = Base.color;
const {getAddressToContract, getTransactionParams} = require('../../utils/transactionUtils');
class EmbarkApiSpec extends Base { class EmbarkApiSpec extends Base {
constructor(runner, options) { constructor(runner, options) {
@ -43,8 +44,19 @@ class EmbarkSpec extends Base {
self.stats.totalGasCost = 0; self.stats.totalGasCost = 0;
self.stats.test = {}; self.stats.test = {};
self.stats.test.gasUsed = 0; self.stats.test.gasUsed = 0;
self.contracts = [];
self.addressToContract = {};
self.txLogs = [];
function onContractReceipt(receipt) { function onContractReceipt(receipt) {
self.embarkEvents.request('contracts:contract', receipt.className, (contract) => {
if (contract) {
self.contracts.push(contract);
self.addressToContract = getAddressToContract(self.contracts, self.addressToContract);
}
});
if (self.gasDetails) {
const fmt = color('bright pass', ' ') + const fmt = color('bright pass', ' ') +
color('suite', ' %s') + color('suite', ' %s') +
color('light', ' deployed for ') + color('light', ' deployed for ') +
@ -53,18 +65,42 @@ class EmbarkSpec extends Base {
console.log(fmt, receipt.className, receipt.gasUsed); console.log(fmt, receipt.className, receipt.gasUsed);
} }
}
function onBlockHeader(blockHeader) { async function onBlockHeader(blockHeader) {
if(!self.listenForGas) { if(!self.listenForGas) {
return; return;
} }
self.stats.totalGasCost += blockHeader.gasUsed; self.stats.totalGasCost += blockHeader.gasUsed;
self.stats.test.gasUsed += blockHeader.gasUsed; self.stats.test.gasUsed += blockHeader.gasUsed;
self.embarkEvents.request("blockchain:block:byNumber", blockHeader.number, (err, block) => {
if (err) {
return this.logger.error('Error getting block header', err.message || err);
}
// Don't know why, but sometimes we receive nothing
if (!block || !block.transactions) {
return;
}
block.transactions.forEach(transaction => {
self.contracts.find(contract => {
if (!contract.silent && contract.deployedAddress && transaction.to && contract.deployedAddress.toLowerCase() === transaction.to.toLowerCase()) {
const c = self.addressToContract[contract.deployedAddress.toLowerCase()];
if (!c) {
return;
}
const {functionName, paramString} = getTransactionParams(c, transaction.input);
self.txLogs.push(`\t\t- ${contract.className}.${functionName}(${paramString}) [${transaction.gas} gas]`);
return true;
}
return false;
});
});
});
} }
if (self.gasDetails) {
self.embarkEvents.on("deploy:contract:receipt", onContractReceipt); self.embarkEvents.on("deploy:contract:receipt", onContractReceipt);
}
self.embarkEvents.on("block:header", onBlockHeader); self.embarkEvents.on("block:header", onBlockHeader);
self.embarkEvents.setCommandHandler("reporter:toggleGasListener", () => { self.embarkEvents.setCommandHandler("reporter:toggleGasListener", () => {
self.listenForGas = !self.listenForGas; self.listenForGas = !self.listenForGas;
@ -101,6 +137,7 @@ class EmbarkSpec extends Base {
runner.on('test', function () { runner.on('test', function () {
self.stats.test.gasUsed = 0; self.stats.test.gasUsed = 0;
self.contracts = [];
}); });
runner.on('pass', function (test) { runner.on('pass', function (test) {
@ -112,11 +149,15 @@ class EmbarkSpec extends Base {
' - ' + ' - ' +
color(self.getGasColor(self.stats.test.gasUsed), '[%d gas]'); color(self.getGasColor(self.stats.test.gasUsed), '[%d gas]');
console.log(fmt, test.title, test.duration, self.stats.test.gasUsed); console.log(fmt, test.title, test.duration, self.stats.test.gasUsed);
self.txLogs.forEach(log => console.log(log));
self.txLogs = [];
}); });
runner.on('fail', function (test) { runner.on('fail', function (test) {
console.log(indent() + color('fail', ' %d) %s') + ' - ' + color(self.getGasColor(self.stats.test.gasUsed), '[%d gas]'), console.log(indent() + color('fail', ' %d) %s') + ' - ' + color(self.getGasColor(self.stats.test.gasUsed), '[%d gas]'),
++n, test.title, self.stats.test.gasUsed); ++n, test.title, self.stats.test.gasUsed);
self.txLogs.forEach(log => console.log(log));
self.txLogs = [];
}); });
runner.once('end', function () { runner.once('end', function () {

View File

@ -0,0 +1,73 @@
import {Contract} from "embark";
import {ABIDefinition} from "web3/eth/abi";
const utils = require("./utils");
export interface AddressToContract {
name: string;
functions: { [functionName: string]: FunctionSignature; };
silent?: boolean;
}
export interface AddressToContractArray {
[address: string]: AddressToContract;
}
export interface FunctionSignature {
abi: ABIDefinition;
functionName?: string;
name: string;
}
export function getAddressToContract(contractsList: Contract[], addressToContract: AddressToContractArray): AddressToContractArray {
if (!contractsList) {
return addressToContract;
}
contractsList.forEach((contract: Contract) => {
if (!contract.deployedAddress) {
return;
}
const address = contract.deployedAddress.toLowerCase();
if (addressToContract[address]) {
return;
}
const funcSignatures: { [name: string]: FunctionSignature } = {};
contract.abiDefinition
.filter((func: ABIDefinition) => func.type === "function")
.map((func: ABIDefinition) => {
const name = `${func.name}(${func.inputs ? func.inputs.map((input) => input.type).join(",") : ""})`;
funcSignatures[utils.sha3(name).substring(0, 10)] = {
abi: func,
functionName: func.name,
name,
};
});
addressToContract[address] = {
functions: funcSignatures,
name: contract.className,
silent: contract.silent,
};
});
return addressToContract;
}
export function getTransactionParams(contract: AddressToContract, transactionInput: string): object {
const func = contract.functions[transactionInput.substring(0, 10)];
const functionName = func.functionName;
const decodedParameters = utils.decodeParams(func.abi.inputs, transactionInput.substring(10));
let paramString = "";
if (func.abi.inputs) {
func.abi.inputs.forEach((input) => {
const quote = input.type.indexOf("int") === -1 ? '"' : "";
paramString += quote + decodedParameters[input.name] + quote + ", ";
});
paramString = paramString.substring(0, paramString.length - 2);
}
return {
functionName,
paramString,
};
}

View File

@ -3,6 +3,7 @@ const {expect} = require('chai');
const sinon = require('sinon'); const sinon = require('sinon');
const Events = require('../../lib/core/events'); const Events = require('../../lib/core/events');
const Logger = require('../../lib/core/logger'); const Logger = require('../../lib/core/logger');
const transactionUtils = require('../../lib/utils/transactionUtils');
const ConsoleListener = require('../../lib/modules/console_listener'); const ConsoleListener = require('../../lib/modules/console_listener');
const IPC = require('../../lib/core/ipc.js'); const IPC = require('../../lib/core/ipc.js');
require('colors'); require('colors');
@ -99,8 +100,8 @@ function resetTest() {
events, events,
logger, logger,
fs: { fs: {
existsSync: () => { return false }, existsSync: () => { return false; },
dappPath: () => { return "ok" } dappPath: () => { return "ok"; }
}, },
config: { config: {
contractsConfig: {} contractsConfig: {}
@ -128,85 +129,9 @@ describe('Console Listener', function () {
done(); done();
}); });
describe('#updateContractList', function () {
it('should not update contracts list', function (done) {
contractsList.deployedAddress = undefined;
consoleListener._updateContractList(contractsList);
expect(consoleListener.addressToContract.length).to.be.equal(0);
done();
});
it('should update contracts list', function (done) {
consoleListener._updateContractList(contractsList);
expect(consoleListener.addressToContract["0x12345"]).to.deep.equal({
name: "SimpleStorage",
functions: {
"0x2a1afcd9": {
"abi": {
"constant": true,
"inputs": [],
"name": "storedData",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
"functionName": "storedData",
"name": "storedData()"
},
"0x60fe47b1": {
"abi": {
"constant": false,
"inputs": [
{
"name": "x",
"type": "uint256"
}
],
"name": "set",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
"functionName": "set",
"name": "set(uint256)"
},
"0x6d4ce63c": {
"abi": {
"constant": true,
"inputs": [],
"name": "get",
"outputs": [
{
"name": "retVal",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
"functionName": "get",
"name": "get()"
}
},
silent: true
});
done();
});
});
describe('#listenForLogRequests', function () { describe('#listenForLogRequests', function () {
it('should emit the correct contracts logs', function (done) { it('should emit the correct contracts logs', function (done) {
consoleListener._updateContractList(contractsList); transactionUtils.getAddressToContract(contractsList, consoleListener.addressToContract);
consoleListener._onIpcLogRequest(ipcRequest); consoleListener._onIpcLogRequest(ipcRequest);
const expectedContractLog = { const expectedContractLog = {
@ -249,7 +174,7 @@ describe('Console Listener', function () {
it('should emit a log for a non-contract log', function (done) { it('should emit a log for a non-contract log', function (done) {
ipcRequest.type = 'something-other-than-contract-log'; ipcRequest.type = 'something-other-than-contract-log';
consoleListener._updateContractList(contractsList); transactionUtils.getAddressToContract(contractsList, consoleListener.addressToContract);
consoleListener._onIpcLogRequest(ipcRequest); consoleListener._onIpcLogRequest(ipcRequest);
expect(loggerInfos[0]).to.be.equal(JSON.stringify(ipcRequest)); expect(loggerInfos[0]).to.be.equal(JSON.stringify(ipcRequest));

View File

@ -0,0 +1,170 @@
/*globals describe, it, beforeEach*/
const {expect} = require('chai');
const transactionUtils = require('../lib/utils/transactionUtils');
require('colors');
let contractsList;
function resetTest() {
contractsList = [
{
abiDefinition: [
{
"constant": true,
"inputs": [],
"name": "storedData",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "x",
"type": "uint256"
}
],
"name": "set",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "get",
"outputs": [
{
"name": "retVal",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"name": "initialValue",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
}
],
deployedAddress: "0x12345",
className: "SimpleStorage",
silent: true
}
];
}
describe('Transaction Utils', () => {
beforeEach(() => {
resetTest();
});
describe('#getAddressToContract', () => {
it('should not update contracts list when no contracts', () => {
contractsList = [];
const result = transactionUtils.getAddressToContract(contractsList, {});
expect(result).to.deep.equal({});
});
it('should not update contracts list when not deployed', () => {
contractsList[0].deployedAddress = undefined;
const result = transactionUtils.getAddressToContract(contractsList, {});
expect(result).to.deep.equal({});
});
it('should update contracts list', () => {
const result = transactionUtils.getAddressToContract(contractsList, {});
expect(result).to.deep.equal({
"0x12345": {
name: "SimpleStorage",
functions: {
"0x2a1afcd9": {
"abi": {
"constant": true,
"inputs": [],
"name": "storedData",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
"functionName": "storedData",
"name": "storedData()"
},
"0x60fe47b1": {
"abi": {
"constant": false,
"inputs": [
{
"name": "x",
"type": "uint256"
}
],
"name": "set",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
"functionName": "set",
"name": "set(uint256)"
},
"0x6d4ce63c": {
"abi": {
"constant": true,
"inputs": [],
"name": "get",
"outputs": [
{
"name": "retVal",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
"functionName": "get",
"name": "get()"
}
},
silent: true
}
});
});
});
describe('#getTransactionParams', () => {
it('should return the param string and function name', () => {
const result = transactionUtils.getAddressToContract(contractsList, {});
const {functionName, paramString} = transactionUtils.getTransactionParams(result['0x12345'], '0x60fe47b100000000000000000000000099db99c77ad807f89829f5bda99527438f64a798');
expect(functionName).to.equal('set');
expect(paramString).to.equal('878372847193751743539905734564138820017777321880');
});
});
});