diff --git a/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts b/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts index 326b88b020..da986f4898 100644 --- a/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts +++ b/apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts @@ -90,11 +90,10 @@ module.exports = { .waitForElementContainsText('*[data-id="testTabTestsExecutionStopped"]', 'The test execution has been stopped', 60000) }, - 'Should fail on compilation': function (browser: NightwatchBrowser) { + 'Should fail on compilation, open file on error click, not disappear error': function (browser: NightwatchBrowser) { browser.waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]') .addFile('tests/compilationError_test.sol', sources[0]['compilationError_test.sol']) - .clickLaunchIcon('filePanel') - .openFile('tests/compilationError_test.sol') + .click('div[title="default_workspace/tests/compilationError_test.sol"] span[class="close"]') .clickLaunchIcon('solidityUnitTesting') .pause(2000) .click('*[data-id="testTabCheckAllTests"]') @@ -102,6 +101,12 @@ module.exports = { .scrollAndClick('*[data-id="testTabRunTestsTabRunAction"]') .waitForElementContainsText('*[data-id="testTabSolidityUnitTestsOutput"]', 'SyntaxError: No visibility specified', 120000) .waitForElementContainsText('*[data-id="testTabTestsExecutionStoppedError"]', 'The test execution has been stopped because of error(s) in your test file', 120000) + .click('*[data-id="tests/compilationError_test.sol"]') + .pause(1000) + .getEditorValue((content) => browser.assert.ok(content.indexOf('contract failOnCompilation {') !== -1)) + // Verify that compilation error is still present after a file is opened + // usually, tests result is cleared on opening a new file + .verify.elementPresent('*[data-id="tests/compilationError_test.sol"]') }, 'Should fail on deploy': function (browser: NightwatchBrowser) { @@ -207,6 +212,23 @@ module.exports = { .removeFile('tests/hhLogs_test.sol', 'workspace_new') }, + 'Solidity Unit tests with hardhat console log for EVM revert': function (browser: NightwatchBrowser) { + browser + .waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]') + .addFile('tests/ballotFailedLog_test.sol', sources[0]['tests/ballotFailedLog_test.sol']) + .clickLaunchIcon('solidityUnitTesting') + .waitForElementVisible('*[id="singleTesttests/4_Ballot_test.sol"]', 60000) + .click('*[id="singleTesttests/4_Ballot_test.sol"]') + .click('#runTestsTabRunAction') + .pause(2000) + .waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000) + .waitForElementContainsText('#solidityUnittestsOutput', 'tests/ballotFailedLog_test.sol', 60000) + .assert.containsText('#journal > div:nth-child(6) > span > div', 'Check winning proposal:') + .assert.containsText('#journal > div:nth-child(6) > span > div', 'Inside checkWinningProposal') + .openFile('tests/ballotFailedLog_test.sol') + .removeFile('tests/ballotFailedLog_test.sol', 'workspace_new') + }, + 'Debug failed test using debugger': function (browser: NightwatchBrowser) { browser .waitForElementPresent('*[data-id="verticalIconsKindfilePanel"]') @@ -424,7 +446,7 @@ const sources = [ }, 'compilationError_test.sol': { content: ` - pragma solidity ^0.7.0; + pragma solidity ^0.8.0; contract failOnCompilation { fallback() { @@ -483,6 +505,31 @@ const sources = [ } }` }, + 'tests/ballotFailedLog_test.sol': { + content: `// SPDX-License-Identifier: GPL-3.0 + + pragma solidity >=0.7.0 <0.9.0; + import "remix_tests.sol"; // this import is automatically injected by Remix. + import "../contracts/3_Ballot.sol"; + + import "hardhat/console.sol"; + + contract BallotTest { + + bytes32[] proposalNames; + + Ballot ballotToTest; + function beforeAll () public { + proposalNames.push(bytes32("candidate1")); + ballotToTest = new Ballot(proposalNames); + } + + function checkWinningProposal () public { + console.log("Inside checkWinningProposal"); + ballotToTest.vote(1); // This will revert the transaction + } + }` + }, 'tests/hhLogs_test.sol': { content: `// SPDX-License-Identifier: GPL-3.0 diff --git a/apps/remix-ide/src/app/tabs/test-tab.js b/apps/remix-ide/src/app/tabs/test-tab.js index afa3d1bc1c..277abab11d 100644 --- a/apps/remix-ide/src/app/tabs/test-tab.js +++ b/apps/remix-ide/src/app/tabs/test-tab.js @@ -44,6 +44,7 @@ module.exports = class TestTab extends ViewPlugin { this.offsetToLineColumnConverter = offsetToLineColumnConverter this.allFilesInvolved = [] this.isDebugging = false + this.currentErrors = [] appManager.event.on('activate', (name) => { if (name === 'solidity') this.updateRunAction() @@ -117,6 +118,14 @@ module.exports = class TestTab extends ViewPlugin { } async updateForNewCurrent (file) { + // Ensure that when someone clicks on compilation error and that opens a new file + // Test result, which is compilation error in this case, is not cleared + if (this.currentErrors) { + if (Array.isArray(this.currentErrors) && this.currentErrors.length > 0) { + const errFiles = this.currentErrors.map(err => { if (err.sourceLocation && err.sourceLocation.file) return err.sourceLocation.file }) + if (errFiles.includes(file)) return + } else if (this.currentErrors.sourceLocation && this.currentErrors.sourceLocation.file && this.currentErrors.sourceLocation.file === file) return + } // if current file is changed while debugging and one of the files imported in test file are opened // do not clear the test results in SUT plugin if (this.isDebugging && this.allFilesInvolved.includes(file)) return @@ -377,12 +386,13 @@ module.exports = class TestTab extends ViewPlugin { this.testsOutput.hidden = false if (!result && (_errors && (_errors.errors || (Array.isArray(_errors) && (_errors[0].message || _errors[0].formattedMessage))))) { this.testCallback({ type: 'contract', filename }) + this.currentErrors = _errors.errors this.setHeader(false) } if (_errors && _errors.errors) { - _errors.errors.forEach((err) => this.renderer.error(err.formattedMessage || err.message, this.testsOutput, { type: err.severity })) + _errors.errors.forEach((err) => this.renderer.error(err.formattedMessage || err.message, this.testsOutput, { type: err.severity, errorType: err.type })) } else if (_errors && Array.isArray(_errors) && (_errors[0].message || _errors[0].formattedMessage)) { - _errors.forEach((err) => this.renderer.error(err.formattedMessage || err.message, this.testsOutput, { type: err.severity })) + _errors.forEach((err) => this.renderer.error(err.formattedMessage || err.message, this.testsOutput, { type: err.severity, errorType: err.type })) } else if (_errors && !_errors.errors && !Array.isArray(_errors)) { // To track error like this: https://github.com/ethereum/remix/pull/1438 this.renderer.error(_errors.formattedMessage || _errors.message, this.testsOutput, { type: 'error' }) diff --git a/libs/remix-tests/src/testRunner.ts b/libs/remix-tests/src/testRunner.ts index dca4a5f748..c3b59480d0 100644 --- a/libs/remix-tests/src/testRunner.ts +++ b/libs/remix-tests/src/testRunner.ts @@ -232,6 +232,7 @@ export function runTest (testName: string, testObject: any, contractDetails: Com testCallback(undefined, resp) async.eachOfLimit(runList, 1, function (func, index, next) { let sender: string | null = null + let hhLogs if (func.signature) { sender = getOverridedSender(contractDetails.userdoc, func.signature, contractDetails.evm.methodIdentifiers) if (opts.accounts && sender) { @@ -293,7 +294,6 @@ export function runTest (testName: string, testObject: any, contractDetails: Com sendParams.gas = 10000000 * 8 method.send(sendParams).on('receipt', async (receipt) => { try { - let hhLogs if (web3.eth && web3.eth.getHHLogsForTx) hhLogs = await web3.eth.getHHLogsForTx(receipt.transactionHash) const time: number = (Date.now() - startTime) / 1000.0 const assertionEventHashes = assertionEvents.map(e => Web3.utils.sha3(e.name + '(' + e.params.join() + ')')) @@ -366,7 +366,7 @@ export function runTest (testName: string, testObject: any, contractDetails: Com console.error(err) return next(err) } - }).on('error', function (err: Error) { + }).on('error', async (err: Error) => { const time: number = (Date.now() - startTime) / 1000.0 const resp: TestResultInterface = { type: 'testFailure', @@ -377,6 +377,11 @@ export function runTest (testName: string, testObject: any, contractDetails: Com context: testName, web3 } + if (err.message.includes('Transaction has been reverted by the EVM')) { + const txHash = JSON.parse(err.message.replace('Transaction has been reverted by the EVM:', '')).transactionHash + if (web3.eth && web3.eth.getHHLogsForTx) hhLogs = await web3.eth.getHHLogsForTx(txHash) + if (hhLogs) resp.hhLogs = hhLogs + } testCallback(undefined, resp) failureNum += 1 timePassed += time