diff --git a/apps/remix-ide/src/app/tabs/test-tab.js b/apps/remix-ide/src/app/tabs/test-tab.js index affb22adcc..5d1e62a914 100644 --- a/apps/remix-ide/src/app/tabs/test-tab.js +++ b/apps/remix-ide/src/app/tabs/test-tab.js @@ -7,7 +7,7 @@ var async = require('async') var tooltip = require('../ui/tooltip') var Renderer = require('../ui/renderer') var css = require('./styles/test-tab-styles') -var remixTests = require('@remix-project/remix-tests') +var { UnitTestRunner } = require('@remix-project/remix-tests') const TestTabLogic = require('./testTab/testTab') @@ -33,6 +33,7 @@ module.exports = class TestTab extends ViewPlugin { this.data = {} this.appManager = appManager this.renderer = new Renderer(this) + this.testRunner = new UnitTestRunner() this.hasBeenStopped = false this.runningTestsNumber = 0 this.readyTestsNumber = 0 @@ -95,6 +96,14 @@ module.exports = class TestTab extends ViewPlugin { this.setCurrentPath(this.defaultPath) }) + this.testRunner.event.register('compilationFinished', (success, data, source) => { + if (success) { + // forwarding the event to the appManager infra + // This is listened by compilerArtefacts to show data while debugging + this.emit('compilationFinished', source.target, source, 'soljson', data) + } + }) + this.fileManager.events.on('noFileSelected', () => { }) @@ -462,7 +471,7 @@ module.exports = class TestTab extends ViewPlugin { usingWorker: canUseWorker(currentVersion), runs } - remixTests.runTestSources(runningTest, compilerConfig, () => {}, () => {}, (error, result) => { + this.testRunner.runTestSources(runningTest, compilerConfig, () => {}, () => {}, (error, result) => { if (error) return reject(error) resolve(result) }, (url, cb) => { @@ -489,7 +498,7 @@ module.exports = class TestTab extends ViewPlugin { usingWorker: canUseWorker(currentVersion), runs } - remixTests.runTestSources( + this.testRunner.runTestSources( runningTests, compilerConfig, (result) => this.testCallback(result, runningTests), diff --git a/libs/remix-core-plugin/src/lib/compiler-artefacts.ts b/libs/remix-core-plugin/src/lib/compiler-artefacts.ts index 47eb1d519b..da22ffb39e 100644 --- a/libs/remix-core-plugin/src/lib/compiler-artefacts.ts +++ b/libs/remix-core-plugin/src/lib/compiler-artefacts.ts @@ -52,6 +52,11 @@ export class CompilerArtefacts extends Plugin { this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source) saveCompilationPerFileResult(file, source, languageVersion, data) }) + + this.on('solidityUnitTesting', 'compilationFinished', (file, source, languageVersion, data) => { + this.compilersArtefacts.__last = new CompilerAbstract(languageVersion, data, source) + saveCompilationPerFileResult(file, source, languageVersion, data) + }) } getAllContractDatas () { diff --git a/libs/remix-tests/src/compiler.ts b/libs/remix-tests/src/compiler.ts index 3cf91bd9fb..3e3c8b85d4 100644 --- a/libs/remix-tests/src/compiler.ts +++ b/libs/remix-tests/src/compiler.ts @@ -205,6 +205,7 @@ export function compileContractSources (sources: SrcIfc, compilerConfig: Compile function doCompilation (next) { // @ts-ignore compiler.event.register('compilationFinished', this, (success, data, source) => { + if (opts && opts.event) opts.event.trigger('compilationFinished', [success, data, source]) next(null, data) }) compiler.compile(sources, filepath) diff --git a/libs/remix-tests/src/index.ts b/libs/remix-tests/src/index.ts index 854db0dcad..9552d5b5ee 100644 --- a/libs/remix-tests/src/index.ts +++ b/libs/remix-tests/src/index.ts @@ -1,5 +1,5 @@ export { runTestFiles } from './runTestFiles' -export { runTestSources } from './runTestSources' +export { UnitTestRunner } from './runTestSources' export { runTest } from './testRunner' export * from './types' export const assertLibCode = require('../sol/tests.sol') diff --git a/libs/remix-tests/src/lib/eventManager.ts b/libs/remix-tests/src/lib/eventManager.ts new file mode 100644 index 0000000000..8282e09b6c --- /dev/null +++ b/libs/remix-tests/src/lib/eventManager.ts @@ -0,0 +1,68 @@ +'use strict' + +export default class EventManager { + registered: any = {} // eslint-disable-line + anonymous: any = {} // eslint-disable-line + + /* + * Unregister a listener. + * Note that if obj is a function. the unregistration will be applied to the dummy obj {}. + * + * @param {String} eventName - the event name + * @param {Object or Func} obj - object that will listen on this event + * @param {Func} func - function of the listeners that will be executed + */ + unregister (eventName: any, obj: any, func: any): void { // eslint-disable-line + if (!this.registered[eventName]) { + return + } + if (obj instanceof Function) { + func = obj + obj = this.anonymous + } + for (const reg in this.registered[eventName]) { + if (this.registered[eventName][reg].obj === obj && this.registered[eventName][reg].func === func) { + this.registered[eventName].splice(reg, 1) + } + } + } + + /* + * Register a new listener. + * Note that if obj is a function, the function registration will be associated with the dummy object {} + * + * @param {String} eventName - the event name + * @param {Object or Func} obj - object that will listen on this event + * @param {Func} func - function of the listeners that will be executed + */ + register (eventName: any, obj: any, func: any): void { // eslint-disable-line + if (!this.registered[eventName]) { + this.registered[eventName] = [] + } + if (obj instanceof Function) { + func = obj + obj = this.anonymous + } + this.registered[eventName].push({ + obj: obj, + func: func + }) + } + + /* + * trigger event. + * Every listener have their associated function executed + * + * @param {String} eventName - the event name + * @param {Array}j - argument that will be passed to the executed function. + */ + trigger (eventName: any, args: any): void { // eslint-disable-line + if (!this.registered[eventName]) { + return + } + for (const listener in this.registered[eventName]) { + const l = this.registered[eventName][listener] + if (l.func) l.func.apply(l.obj === this.anonymous ? {} : l.obj, args) + } + } +} diff --git a/libs/remix-tests/src/runTestSources.ts b/libs/remix-tests/src/runTestSources.ts index 1393148272..78f4b11e69 100644 --- a/libs/remix-tests/src/runTestSources.ts +++ b/libs/remix-tests/src/runTestSources.ts @@ -5,6 +5,7 @@ import { deployAll } from './deployer' import { runTest } from './testRunner' import Web3 from 'web3' +import EventManager from './lib/eventManager' import { Provider, extend } from '@remix-project/remix-simulator' import { FinalResult, SrcIfc, compilationInterface, ASTInterface, Options, @@ -12,126 +13,134 @@ import { } from './types' require('colors') -const createWeb3Provider = async function () { - const web3 = new Web3() - const provider: any = new Provider() - await provider.init() - web3.setProvider(provider) - extend(web3) - return web3 -} +export class UnitTestRunner { + event -/** - * @dev Run tests from source of a test contract file (used for IDE) - * @param contractSources Sources of contract - * @param compilerConfig current compiler configuration - * @param testCallback Test callback - * @param resultCallback Result Callback - * @param finalCallback Final Callback - * @param importFileCb Import file callback - * @param opts Options - */ -export async function runTestSources (contractSources: SrcIfc, compilerConfig: CompilerConfiguration, testCallback, resultCallback, finalCallback: any, importFileCb, opts: Options) { - opts = opts || {} - const sourceASTs: any = {} - const web3 = opts.web3 || await createWeb3Provider() - let accounts: string[] | null = opts.accounts || null - async.waterfall([ - function getAccountList (next) { - if (accounts) return next() - web3.eth.getAccounts((_err, _accounts) => { - accounts = _accounts - next() - }) - }, - function compile (next) { - compileContractSources(contractSources, compilerConfig, importFileCb, { accounts }, next) - }, - function deployAllContracts (compilationResult: compilationInterface, asts: ASTInterface, next) { - for (const filename in asts) { - if (filename.endsWith('_test.sol')) { sourceASTs[filename] = asts[filename].ast } - } - deployAll(compilationResult, web3, false, (err, contracts) => { - if (err) { - // If contract deployment fails because of 'Out of Gas' error, try again with double gas - // This is temporary, should be removed when remix-tests will have a dedicated UI to - // accept deployment params from UI - if (err.message.includes('The contract code couldn\'t be stored, please check your gas limit')) { - deployAll(compilationResult, web3, true, (error, contracts) => { - if (error) next([{ message: 'contract deployment failed after trying twice: ' + error.message, severity: 'error' }]) // IDE expects errors in array - else next(null, compilationResult, contracts) - }) - } else { next([{ message: 'contract deployment failed: ' + err.message, severity: 'error' }]) } // IDE expects errors in array - } else { next(null, compilationResult, contracts) } - }) - }, - function determineTestContractsToRun (compilationResult: compilationInterface, contracts: any, next) { - const contractsToTest: string[] = [] - const contractsToTestDetails: any[] = [] + constructor() { + this.event = new EventManager() + } + + async createWeb3Provider () { + const web3 = new Web3() + const provider: any = new Provider() + await provider.init() + web3.setProvider(provider) + extend(web3) + return web3 + } - for (const filename in compilationResult) { - if (!filename.endsWith('_test.sol')) { - continue + /** + * @dev Run tests from source of a test contract file (used for IDE) + * @param contractSources Sources of contract + * @param compilerConfig current compiler configuration + * @param testCallback Test callback + * @param resultCallback Result Callback + * @param finalCallback Final Callback + * @param importFileCb Import file callback + * @param opts Options + */ + async runTestSources (contractSources: SrcIfc, compilerConfig: CompilerConfiguration, testCallback, resultCallback, finalCallback: any, importFileCb, opts: Options) { + opts = opts || {} + const sourceASTs: any = {} + const web3 = opts.web3 || await this.createWeb3Provider() + let accounts: string[] | null = opts.accounts || null + async.waterfall([ + function getAccountList (next) { + if (accounts) return next() + web3.eth.getAccounts((_err, _accounts) => { + accounts = _accounts + next() + }) + }, + (next) => { + compileContractSources(contractSources, compilerConfig, importFileCb, { accounts, event: this.event}, next) + }, + function deployAllContracts (compilationResult: compilationInterface, asts: ASTInterface, next) { + for (const filename in asts) { + if (filename.endsWith('_test.sol')) { sourceASTs[filename] = asts[filename].ast } } - Object.keys(compilationResult[filename]).forEach(contractName => { - contractsToTestDetails.push(compilationResult[filename][contractName]) - contractsToTest.push(contractName) + deployAll(compilationResult, web3, false, (err, contracts) => { + if (err) { + // If contract deployment fails because of 'Out of Gas' error, try again with double gas + // This is temporary, should be removed when remix-tests will have a dedicated UI to + // accept deployment params from UI + if (err.message.includes('The contract code couldn\'t be stored, please check your gas limit')) { + deployAll(compilationResult, web3, true, (error, contracts) => { + if (error) next([{ message: 'contract deployment failed after trying twice: ' + error.message, severity: 'error' }]) // IDE expects errors in array + else next(null, compilationResult, contracts) + }) + } else { next([{ message: 'contract deployment failed: ' + err.message, severity: 'error' }]) } // IDE expects errors in array + } else { next(null, compilationResult, contracts) } }) - } - next(null, contractsToTest, contractsToTestDetails, contracts) - }, - function runTests (contractsToTest: string[], contractsToTestDetails: any[], contracts: any, next) { - let totalPassing = 0 - let totalFailing = 0 - let totalTime = 0 - const errors: any[] = [] - // eslint-disable-next-line handle-callback-err - const _testCallback = function (err: Error | null | undefined, result: TestResultInterface) { - if (result.type === 'testFailure') { - errors.push(result) + }, + function determineTestContractsToRun (compilationResult: compilationInterface, contracts: any, next) { + const contractsToTest: string[] = [] + const contractsToTestDetails: any[] = [] + + for (const filename in compilationResult) { + if (!filename.endsWith('_test.sol')) { + continue + } + Object.keys(compilationResult[filename]).forEach(contractName => { + contractsToTestDetails.push(compilationResult[filename][contractName]) + contractsToTest.push(contractName) + }) + } + next(null, contractsToTest, contractsToTestDetails, contracts) + }, + function runTests (contractsToTest: string[], contractsToTestDetails: any[], contracts: any, next) { + let totalPassing = 0 + let totalFailing = 0 + let totalTime = 0 + const errors: any[] = [] + // eslint-disable-next-line handle-callback-err + const _testCallback = function (err: Error | null | undefined, result: TestResultInterface) { + if (result.type === 'testFailure') { + errors.push(result) + } + testCallback(result) } - testCallback(result) - } - const _resultsCallback = function (_err, result, cb) { - resultCallback(_err, result, () => {}) // eslint-disable-line @typescript-eslint/no-empty-function - totalPassing += result.passingNum - totalFailing += result.failureNum - totalTime += result.timePassed - cb() - } + const _resultsCallback = function (_err, result, cb) { + resultCallback(_err, result, () => {}) // eslint-disable-line @typescript-eslint/no-empty-function + totalPassing += result.passingNum + totalFailing += result.failureNum + totalTime += result.timePassed + cb() + } - async.eachOfLimit(contractsToTest, 1, (contractName: string, index: string | number, cb: ErrorCallback) => { - const fileAST: AstNode = sourceASTs[contracts[contractName]['filename']] - runTest(contractName, contracts[contractName], contractsToTestDetails[index], fileAST, { accounts, web3 }, _testCallback, (err, result) => { + async.eachOfLimit(contractsToTest, 1, (contractName: string, index: string | number, cb: ErrorCallback) => { + const fileAST: AstNode = sourceASTs[contracts[contractName]['filename']] + runTest(contractName, contracts[contractName], contractsToTestDetails[index], fileAST, { accounts, web3 }, _testCallback, (err, result) => { + if (err) { + return cb(err) + } + _resultsCallback(null, result, cb) + }) + }, function (err) { if (err) { - return cb(err) + return next(err) } - _resultsCallback(null, result, cb) - }) - }, function (err) { - if (err) { - return next(err) - } - const finalResults: FinalResult = { - totalPassing: 0, - totalFailing: 0, - totalTime: 0, - errors: [] - } + const finalResults: FinalResult = { + totalPassing: 0, + totalFailing: 0, + totalTime: 0, + errors: [] + } - finalResults.totalPassing = totalPassing || 0 - finalResults.totalFailing = totalFailing || 0 - finalResults.totalTime = totalTime || 0 - finalResults.errors = [] + finalResults.totalPassing = totalPassing || 0 + finalResults.totalFailing = totalFailing || 0 + finalResults.totalTime = totalTime || 0 + finalResults.errors = [] - errors.forEach((error, _index) => { - finalResults.errors.push({ context: error.context, value: error.value, message: error.errMsg }) - }) + errors.forEach((error, _index) => { + finalResults.errors.push({ context: error.context, value: error.value, message: error.errMsg }) + }) - next(null, finalResults) - }) - } - ], finalCallback) + next(null, finalResults) + }) + } + ], finalCallback) + } }