diff --git a/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts b/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts index a4415b2abf..77cb8222cf 100644 --- a/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts +++ b/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts @@ -13,7 +13,7 @@ module.exports = { return sources }, - 'Should launch solidity unit test plugin': function (browser: NightwatchBrowser) { + 'Should launch solidity unit test plugin and create test files in FE': function (browser: NightwatchBrowser) { browser.waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]') .clickLaunchIcon('filePanel') .addFile('simple_storage.sol', sources[0]['simple_storage.sol']) @@ -23,6 +23,15 @@ module.exports = { .click('*[data-id="verticalIconsKindsolidityUnitTesting"]') .waitForElementPresent('*[data-id="sidePanelSwapitTitle"]') .assert.containsText('*[data-id="sidePanelSwapitTitle"]', 'SOLIDITY UNIT TESTING') + .clickLaunchIcon('filePanel') + .waitForElementVisible('li[data-id="treeViewLitreeViewItem.deps/remix-tests/remix_tests.sol"]') + .waitForElementVisible('li[data-id="treeViewLitreeViewItem.deps/remix-tests/remix_accounts.sol"]') + .openFile('.deps/remix-tests/remix_tests.sol') + // remix_test.sol should be opened in editor + .getEditorValue((content) => browser.assert.ok(content.indexOf('library Assert {') !== -1)) + .openFile('.deps/remix-tests/remix_accounts.sol') + // remix_accounts.sol should be opened in editor + .getEditorValue((content) => browser.assert.ok(content.indexOf('library TestsAccounts {') !== -1)) }, 'Should generate test file': function (browser: NightwatchBrowser) { @@ -149,7 +158,7 @@ module.exports = { .click('*[data-id="testTabGenerateTestFolder"]') }, - 'Changing current path when workspace changed': function (browser: NightwatchBrowser) { + 'Changing current path when workspace changed and checking test files creation': function (browser: NightwatchBrowser) { browser .waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]') .clickLaunchIcon('settings') @@ -167,6 +176,14 @@ module.exports = { .waitForElementVisible('*[data-id="fileSystem-modal-footer-ok-react"]') .execute(function () { (document.querySelector('[data-id="fileSystem-modal-footer-ok-react"]') as HTMLElement).click() }) .waitForElementPresent('*[data-id="workspacesSelect"] option[value="workspace_new"]') + .waitForElementVisible('li[data-id="treeViewLitreeViewItem.deps/remix-tests/remix_tests.sol"]') + .waitForElementVisible('li[data-id="treeViewLitreeViewItem.deps/remix-tests/remix_accounts.sol"]') + .openFile('.deps/remix-tests/remix_tests.sol') + // remix_test.sol should be opened in editor + .getEditorValue((content) => browser.assert.ok(content.indexOf('library Assert {') !== -1)) + .openFile('.deps/remix-tests/remix_accounts.sol') + // remix_accounts.sol should be opened in editor + .getEditorValue((content) => browser.assert.ok(content.indexOf('library TestsAccounts {') !== -1)) // end of creating .clickLaunchIcon('solidityUnitTesting') .pause(2000) @@ -277,6 +294,8 @@ module.exports = { .setValue('*[data-id="slider"]', new Array(1).fill(browser.Keys.RIGHT_ARROW)) .waitForElementContainsText('*[data-id="functionPanel"]', 'equal(a, b, message)', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalPassed()', 60000) + // remix_test.sol should be opened in editor + .getEditorValue((content) => browser.assert.ok(content.indexOf('library Assert {') !== -1)) .pause(5000) .clickLaunchIcon('solidityUnitTesting').pause(2000) .scrollAndClick('#Check_winning_proposal_again') diff --git a/apps/remix-ide/src/app/files/fileProvider.js b/apps/remix-ide/src/app/files/fileProvider.js index 01cefdf188..607ddbd215 100644 --- a/apps/remix-ide/src/app/files/fileProvider.js +++ b/apps/remix-ide/src/app/files/fileProvider.js @@ -17,8 +17,9 @@ class FileProvider { } addNormalizedName (path, url) { - this.providerExternalsStorage.set(this.type + '/' + path, url) - this.providerExternalsStorage.set(this.reverseKey + url, this.type + '/' + path) + if (this.type) path = this.type + '/' + path + this.providerExternalsStorage.set(path, url) + this.providerExternalsStorage.set(this.reverseKey + url, path) } removeNormalizedName (path) { diff --git a/apps/remix-ide/src/app/tabs/test-tab.js b/apps/remix-ide/src/app/tabs/test-tab.js index 1995abe1f6..c82a2bcc1d 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 { UnitTestRunner } = require('@remix-project/remix-tests') +var { UnitTestRunner, assertLibCode } = require('@remix-project/remix-tests') const _paq = window._paq = window._paq || [] @@ -16,7 +16,7 @@ const TestTabLogic = require('./testTab/testTab') const profile = { name: 'solidityUnitTesting', displayName: 'Solidity unit testing', - methods: ['testFromPath', 'testFromSource', 'setTestFolderPath'], + methods: ['testFromPath', 'testFromSource', 'setTestFolderPath', 'getTestlibs'], events: [], icon: 'assets/img/unitTesting.webp', description: 'Fast tool to generate unit tests for your contracts', @@ -42,7 +42,7 @@ module.exports = class TestTab extends ViewPlugin { this.areTestsRunning = false this.defaultPath = 'tests' this.offsetToLineColumnConverter = offsetToLineColumnConverter - this.allFilesInvolved = [] + this.allFilesInvolved = ['.deps/remix-tests/remix_tests.sol', '.deps/remix-tests/remix_accounts.sol'] this.isDebugging = false this.currentErrors = [] @@ -74,11 +74,25 @@ module.exports = class TestTab extends ViewPlugin { } } + getTestlibs () { + return { assertLibCode, accountsLibCode: this.testRunner.accountsLibCode } + } + + async createTestLibs () { + const provider = await this.fileManager.currentFileProvider() + if (provider) { + provider.addExternal('.deps/remix-tests/remix_tests.sol', assertLibCode, 'remix_tests.sol') + provider.addExternal('.deps/remix-tests/remix_accounts.sol', this.testRunner.accountsLibCode, 'remix_accounts.sol') + } + } + async onActivation () { const isSolidityActive = await this.call('manager', 'isActive', 'solidity') if (!isSolidityActive) { await this.call('manager', 'activatePlugin', 'solidity') } + await this.testRunner.init() + await this.createTestLibs() this.updateRunAction() } @@ -110,9 +124,13 @@ module.exports = class TestTab extends ViewPlugin { this.setCurrentPath(this.defaultPath) }) + this.on('filePanel', 'workspaceCreated', async () => { + this.createTestLibs() + }) + this.testRunner.event.on('compilationFinished', (success, data, source) => { if (success) { - this.allFilesInvolved = Object.keys(data.sources) + this.allFilesInvolved.push(...Object.keys(data.sources)) // 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) @@ -526,8 +544,8 @@ module.exports = class TestTab extends ViewPlugin { this.fileManager.readFile(testFilePath).then((content) => { const runningTests = {} runningTests[testFilePath] = { content } - const { currentVersion, evmVersion, optimize, runs } = this.compileTab.getCurrentCompilerConfig() - const currentCompilerUrl = urlFromVersion(currentVersion) + const { currentVersion, evmVersion, optimize, runs, isUrl } = this.compileTab.getCurrentCompilerConfig() + const currentCompilerUrl = isUrl ? currentVersion : urlFromVersion(currentVersion) const compilerConfig = { currentCompilerUrl, evmVersion, diff --git a/apps/remix-ide/webpack.config.js b/apps/remix-ide/webpack.config.js index 02143af15b..4af33b4f3a 100644 --- a/apps/remix-ide/webpack.config.js +++ b/apps/remix-ide/webpack.config.js @@ -21,7 +21,7 @@ module.exports = config => { mode: 'production', devtool: 'source-map', optimization: { - minimize: true, + minimize: false, minimizer: [new TerserPlugin()] } } diff --git a/apps/solidity-compiler/src/app/compiler-api.ts b/apps/solidity-compiler/src/app/compiler-api.ts index 8082002ff4..733ee0026f 100644 --- a/apps/solidity-compiler/src/app/compiler-api.ts +++ b/apps/solidity-compiler/src/app/compiler-api.ts @@ -143,12 +143,17 @@ export const CompilerApiMixin = (Base) => class extends Base { // This function is used for passing the compiler configuration to 'remix-tests' getCurrentCompilerConfig () { const compilerState = this.getCompilerState() - return { + let compilerDetails: any = { currentVersion: compilerState.currentVersion, evmVersion: compilerState.evmVersion, optimize: compilerState.optimize, runs: compilerState.runs } + if (this.data.loading) { + compilerDetails.currentVersion = this.data.loadingUrl + compilerDetails.isUrl = true + } + return compilerDetails } /** @@ -193,8 +198,9 @@ export const CompilerApiMixin = (Base) => class extends Base { if (this.onContentChanged) this.onContentChanged() }) - this.data.eventHandlers.onLoadingCompiler = () => { + this.data.eventHandlers.onLoadingCompiler = (url) => { this.data.loading = true + this.data.loadingUrl = url this.emit('statusChanged', { key: 'loading', title: 'loading compiler...', type: 'info' }) } this.compiler.event.register('loadingCompiler', this.data.eventHandlers.onLoadingCompiler) diff --git a/libs/remix-core-plugin/src/lib/compiler-content-imports.ts b/libs/remix-core-plugin/src/lib/compiler-content-imports.ts index 84530cd1ee..bd550f988c 100644 --- a/libs/remix-core-plugin/src/lib/compiler-content-imports.ts +++ b/libs/remix-core-plugin/src/lib/compiler-content-imports.ts @@ -1,7 +1,6 @@ 'use strict' import { Plugin } from '@remixproject/engine' import { RemixURLResolver } from '@remix-project/remix-url-resolver' -const remixTests = require('@remix-project/remix-tests') const profile = { name: 'contentImport', @@ -117,7 +116,6 @@ export class CompilerImports extends Plugin { * @returns {Promise} - string content */ async resolveAndSave (url, targetPath) { - if (url.indexOf('remix_tests.sol') !== -1) return remixTests.assertLibCode try { const provider = await this.call('fileManager', 'getProviderOf', url) if (provider) { diff --git a/libs/remix-lib/src/web3Provider/web3VmProvider.ts b/libs/remix-lib/src/web3Provider/web3VmProvider.ts index f29e225e5a..49fa4d80a9 100644 --- a/libs/remix-lib/src/web3Provider/web3VmProvider.ts +++ b/libs/remix-lib/src/web3Provider/web3VmProvider.ts @@ -304,7 +304,9 @@ export class Web3VmProvider { nextKey: null }) } - cb('unable to retrieve storage ' + txIndex + ' ' + address) + // Before https://github.com/ethereum/remix-project/pull/1703, it used to throw error as + // 'unable to retrieve storage ' + txIndex + ' ' + address + cb(null, { storage: {} }) } getBlockNumber (cb) { cb(null, 'vm provider') } diff --git a/libs/remix-tests/sol/tests_accounts.sol.ts b/libs/remix-tests/sol/tests_accounts.sol.ts index d97aae8f34..847e37f083 100644 --- a/libs/remix-tests/sol/tests_accounts.sol.ts +++ b/libs/remix-tests/sol/tests_accounts.sol.ts @@ -3,7 +3,7 @@ module.exports = `// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.4.22 <0.9.0; library TestsAccounts { - function getAccount(uint index) public returns (address) { + function getAccount(uint index) pure public returns (address) { >accounts< return accounts[index]; } diff --git a/libs/remix-tests/src/compiler.ts b/libs/remix-tests/src/compiler.ts index e15bb656e4..f4b9250ecf 100644 --- a/libs/remix-tests/src/compiler.ts +++ b/libs/remix-tests/src/compiler.ts @@ -12,13 +12,13 @@ function regexIndexOf (inputString: string, regex: RegExp, startpos = 0) { return (indexOf >= 0) ? (indexOf + (startpos)) : indexOf } -function writeTestAccountsContract (accounts: string[]) { +export function writeTestAccountsContract (accounts: string[]) { const testAccountContract = require('../sol/tests_accounts.sol') let body = `address[${accounts.length}] memory accounts;` if (!accounts.length) body += ';' else { accounts.map((address, index) => { - body += `\naccounts[${index}] = ${address};\n` + body += `\n\t\taccounts[${index}] = ${address};\n` }) } return testAccountContract.replace('>accounts<', body) @@ -172,14 +172,7 @@ export function compileFileOrFiles (filename: string, isDirectory: boolean, opts */ export function compileContractSources (sources: SrcIfc, compilerConfig: CompilerConfiguration, importFileCb: any, opts: any, cb): void { let compiler - const accounts: string[] = opts.accounts || [] const filepath = opts.testFilePath || '' - // Iterate over sources keys. Inject test libraries. Inject test library import statements. - if (!('remix_tests.sol' in sources) && !('tests.sol' in sources)) { - sources['tests.sol'] = { content: require('../sol/tests.sol.js') } - sources['remix_tests.sol'] = { content: require('../sol/tests.sol.js') } - sources['remix_accounts.sol'] = { content: writeTestAccountsContract(accounts) } - } const testFileImportRegEx = /^(import)\s['"](remix_tests.sol|tests.sol)['"];/gm const includeTestLibs = '\nimport \'remix_tests.sol\';\n' diff --git a/libs/remix-tests/src/deployer.ts b/libs/remix-tests/src/deployer.ts index d700b08de5..505281fe00 100644 --- a/libs/remix-tests/src/deployer.ts +++ b/libs/remix-tests/src/deployer.ts @@ -11,18 +11,12 @@ import { compilationInterface } from './types' * @param callback Callback */ -export function deployAll (compileResult: compilationInterface, web3: Web3, withDoubleGas: boolean, deployCb, callback) { +export function deployAll (compileResult: compilationInterface, web3: Web3, testsAccounts, withDoubleGas: boolean, deployCb, callback) { const compiledObject = {} const contracts = {} - let accounts: string[] = [] + const accounts: string[] = testsAccounts async.waterfall([ - function getAccountList (next) { - web3.eth.getAccounts((_err, _accounts) => { - accounts = _accounts - next() - }) - }, function getContractData (next) { for (const contractFile in compileResult) { for (const contractName in compileResult[contractFile]) { diff --git a/libs/remix-tests/src/index.ts b/libs/remix-tests/src/index.ts index 9552d5b5ee..37862a47c6 100644 --- a/libs/remix-tests/src/index.ts +++ b/libs/remix-tests/src/index.ts @@ -3,3 +3,4 @@ export { UnitTestRunner } from './runTestSources' export { runTest } from './testRunner' export * from './types' export const assertLibCode = require('../sol/tests.sol') +export { writeTestAccountsContract } from './compiler' diff --git a/libs/remix-tests/src/runTestFiles.ts b/libs/remix-tests/src/runTestFiles.ts index d9ed0c2b6e..dbf2b16e46 100644 --- a/libs/remix-tests/src/runTestFiles.ts +++ b/libs/remix-tests/src/runTestFiles.ts @@ -61,13 +61,13 @@ export function runTestFiles (filepath: string, isDirectory: boolean, web3: Web3 for (const filename in asts) { if (filename.endsWith('_test.sol')) { sourceASTs[filename] = asts[filename].ast } } - deployAll(compilationResult, web3, false, null, (err, contracts) => { + deployAll(compilationResult, web3, accounts, false, null, (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, null, (error, contracts) => { + deployAll(compilationResult, web3, accounts, true, null, (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) }) diff --git a/libs/remix-tests/src/runTestSources.ts b/libs/remix-tests/src/runTestSources.ts index ecc35aa3f2..9d8b8ba400 100644 --- a/libs/remix-tests/src/runTestSources.ts +++ b/libs/remix-tests/src/runTestSources.ts @@ -1,9 +1,7 @@ import async, { ErrorCallback } from 'async' - -import { compileContractSources } from './compiler' +import { compileContractSources, writeTestAccountsContract } from './compiler' import { deployAll } from './deployer' import { runTest } from './testRunner' - import Web3 from 'web3' import { EventEmitter } from 'events' import { Provider, extend } from '@remix-project/remix-simulator' @@ -15,13 +13,22 @@ require('colors') export class UnitTestRunner { event + accountsLibCode + testsAccounts: string[] | null + web3 constructor () { this.event = new EventEmitter() } - async createWeb3Provider () { - const web3 = new Web3() + async init (web3 = null, accounts = null) { + this.web3 = await this.createWeb3Provider(web3) + this.testsAccounts = accounts || await this.web3.eth.getAccounts() + this.accountsLibCode = writeTestAccountsContract(this.testsAccounts) + } + + async createWeb3Provider (optWeb3) { + const web3 = optWeb3 || new Web3() const provider: any = new Provider() await provider.init() web3.setProvider(provider) @@ -42,30 +49,23 @@ export class UnitTestRunner { async runTestSources (contractSources: SrcIfc, compilerConfig: CompilerConfiguration, testCallback, resultCallback, deployCb:any, finalCallback: any, importFileCb, opts: Options) { opts = opts || {} const sourceASTs: any = {} - const web3 = opts.web3 || await this.createWeb3Provider() - let accounts: string[] | null = opts.accounts || null + if (opts.web3 || opts.accounts) this.init(opts.web3, opts.accounts) + async.waterfall([ - function getAccountList (next) { - if (accounts) return next() - web3.eth.getAccounts((_err, _accounts) => { - accounts = _accounts - next() - }) - }, (next) => { - compileContractSources(contractSources, compilerConfig, importFileCb, { accounts, testFilePath: opts.testFilePath, event: this.event }, next) + compileContractSources(contractSources, compilerConfig, importFileCb, { accounts: this.testsAccounts, testFilePath: opts.testFilePath, event: this.event }, next) }, - function deployAllContracts (compilationResult: compilationInterface, asts: ASTInterface, next) { + (compilationResult: compilationInterface, asts: ASTInterface, next) => { for (const filename in asts) { if (filename.endsWith('_test.sol')) { sourceASTs[filename] = asts[filename].ast } } - deployAll(compilationResult, web3, false, deployCb, (err, contracts) => { + deployAll(compilationResult, this.web3, this.testsAccounts, false, deployCb, (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, deployCb, (error, contracts) => { + deployAll(compilationResult, this.web3, this.testsAccounts, true, deployCb, (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) }) @@ -88,7 +88,7 @@ export class UnitTestRunner { } next(null, contractsToTest, contractsToTestDetails, contracts) }, - function runTests (contractsToTest: string[], contractsToTestDetails: any[], contracts: any, next) { + (contractsToTest: string[], contractsToTestDetails: any[], contracts: any, next) => { let totalPassing = 0 let totalFailing = 0 let totalTime = 0 @@ -111,7 +111,7 @@ export class UnitTestRunner { 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) => { + runTest(contractName, contracts[contractName], contractsToTestDetails[index], fileAST, { accounts: this.testsAccounts, web3: this.web3 }, _testCallback, (err, result) => { if (err) { return cb(err) } diff --git a/libs/remix-tests/tests/testRunner.spec.ts b/libs/remix-tests/tests/testRunner.spec.ts index 928491faa5..2f6d363a31 100644 --- a/libs/remix-tests/tests/testRunner.spec.ts +++ b/libs/remix-tests/tests/testRunner.spec.ts @@ -67,7 +67,7 @@ async function compileAndDeploy(filename: string, callback: Function) { } try { compilationData = compilationResult - deployAll(compilationResult, web3, false, null, next) + deployAll(compilationResult, web3, accounts, false, null, next) } catch (e) { throw e }