diff --git a/apps/debugger/src/app/debugger-api.ts b/apps/debugger/src/app/debugger-api.ts index 13238c6add..007c42de36 100644 --- a/apps/debugger/src/app/debugger-api.ts +++ b/apps/debugger/src/app/debugger-api.ts @@ -1,7 +1,7 @@ import Web3 from 'web3' import remixDebug, { TransactionDebugger as Debugger } from '@remix-project/remix-debug' import { CompilerAbstract } from '@remix-project/remix-solidity' - +import { lineText } from '@remix-ui/editor' export const DebuggerApiMixin = (Base) => class extends Base { @@ -39,10 +39,25 @@ export const DebuggerApiMixin = (Base) => class extends Base { async discardHighlight () { await this.call('editor', 'discardHighlight') + await this.call('editor', 'discardLineTexts' as any) } - async highlight (lineColumnPos, path) { + async highlight (lineColumnPos, path, rawLocation, stepDetail, lineGasCost) { await this.call('editor', 'highlight', lineColumnPos, path, '', { focus: true }) + const label = `${stepDetail.op} costs ${stepDetail.gasCost} gas - this line costs ${lineGasCost} gas - ${stepDetail.gas} gas left` + const linetext: lineText = { + content: label, + position: lineColumnPos, + hide: false, + className: 'text-muted small', + afterContentClassName: 'text-muted small fas fa-gas-pump pl-4', + from: 'debugger', + hoverMessage: [{ + value: label, + }, + ], + } + await this.call('editor', 'addLineText' as any, linetext, path) } async getFile (path) { diff --git a/apps/remix-ide-e2e/src/commands/waitForElementContainsText.ts b/apps/remix-ide-e2e/src/commands/waitForElementContainsText.ts index 5376da014d..6ce01beaa0 100644 --- a/apps/remix-ide-e2e/src/commands/waitForElementContainsText.ts +++ b/apps/remix-ide-e2e/src/commands/waitForElementContainsText.ts @@ -4,8 +4,10 @@ import EventEmitter from 'events' class WaitForElementContainsText extends EventEmitter { command (this: NightwatchBrowser, id: string, value: string, timeout = 10000): NightwatchBrowser { let waitId // eslint-disable-line + let currentValue const runid = setInterval(() => { this.api.getText(id, (result) => { + currentValue = result.value if (typeof result.value === 'string' && result.value.indexOf(value) !== -1) { clearInterval(runid) clearTimeout(waitId) @@ -17,7 +19,7 @@ class WaitForElementContainsText extends EventEmitter { waitId = setTimeout(() => { clearInterval(runid) - this.api.assert.fail(`TimeoutError: An error occurred while running .waitForElementContainsText() command on ${id} after ${timeout} milliseconds`) + this.api.assert.fail(`TimeoutError: An error occurred while running .waitForElementContainsText() command on ${id} after ${timeout} milliseconds. expected: ${value} - got: ${currentValue}`) }, timeout) return this } diff --git a/apps/remix-ide-e2e/src/tests/debugger.test.ts b/apps/remix-ide-e2e/src/tests/debugger.test.ts index 9fdd7fb6c2..60408cf1d3 100644 --- a/apps/remix-ide-e2e/src/tests/debugger.test.ts +++ b/apps/remix-ide-e2e/src/tests/debugger.test.ts @@ -97,11 +97,6 @@ module.exports = { locateStrategy: 'xpath', selector: '//*[@data-id="treeViewLivm trace step" and contains(.,"545")]', }) - .goToVMTraceStep(10) - .waitForElementVisible({ - locateStrategy: 'xpath', - selector: '//*[@data-id="treeViewLivm trace step" and contains(.,"10")]', - }) .getEditorValue((content) => { browser.assert.ok(content.indexOf(`constructor (string memory name_, string memory symbol_) { _name = name_; @@ -109,6 +104,11 @@ module.exports = { }`) !== -1, 'current displayed content is not from the ERC20 source code') }) + .goToVMTraceStep(10) + .waitForElementVisible({ + locateStrategy: 'xpath', + selector: '//*[@data-id="treeViewLivm trace step" and contains(.,"10")]', + }) }, 'Should display correct source highlighting while debugging a contract which has ABIEncoderV2 #group2': function (browser: NightwatchBrowser) { diff --git a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts index 6a7840c6cd..01d48ef889 100644 --- a/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts +++ b/apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts @@ -290,7 +290,7 @@ module.exports = { .waitForElementContainsText('*[data-id="sidePanelSwapitTitle"]', 'DEBUGGER', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalFailed()', 60000) .waitForElementVisible('*[data-id="dropdownPanelSolidityLocals"]').pause(1000) - .waitForElementContainsText('*[data-id="solidityLocals"]', 'no locals', 60000) + .waitForElementContainsText('*[data-id="solidityLocals"]', 'No data available', 60000) .goToVMTraceStep(316) .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalFailed()', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'vote(proposal)', 60000) diff --git a/libs/remix-analyzer/package.json b/libs/remix-analyzer/package.json index 8cd88adb79..8d705b625d 100644 --- a/libs/remix-analyzer/package.json +++ b/libs/remix-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@remix-project/remix-analyzer", - "version": "0.5.28", + "version": "0.5.29", "description": "Tool to perform static analysis on Solidity smart contracts", "main": "src/index.js", "types": "src/index.d.ts", @@ -22,8 +22,8 @@ "@ethereumjs/block": "^3.5.1", "@ethereumjs/tx": "^3.3.2", "@ethereumjs/vm": "^5.5.3", - "@remix-project/remix-astwalker": "^0.0.49", - "@remix-project/remix-lib": "^0.5.19", + "@remix-project/remix-astwalker": "^0.0.50", + "@remix-project/remix-lib": "^0.5.20", "async": "^2.6.2", "ethereumjs-util": "^7.0.10", "ethers": "^5.4.2", @@ -52,5 +52,5 @@ "typescript": "^3.7.5" }, "typings": "src/index.d.ts", - "gitHead": "2cec195f5a3678b17745155536a1714d9b1a5694" + "gitHead": "569f7d53f6f218d99aa8281929b541ab7e00058f" } \ No newline at end of file diff --git a/libs/remix-astwalker/package.json b/libs/remix-astwalker/package.json index 8607412bda..04c2b64049 100644 --- a/libs/remix-astwalker/package.json +++ b/libs/remix-astwalker/package.json @@ -1,6 +1,6 @@ { "name": "@remix-project/remix-astwalker", - "version": "0.0.49", + "version": "0.0.50", "description": "Tool to walk through Solidity AST", "main": "src/index.js", "scripts": { @@ -37,7 +37,7 @@ "@ethereumjs/block": "^3.5.1", "@ethereumjs/tx": "^3.3.2", "@ethereumjs/vm": "^5.5.3", - "@remix-project/remix-lib": "^0.5.19", + "@remix-project/remix-lib": "^0.5.20", "@types/tape": "^4.2.33", "async": "^2.6.2", "ethereumjs-util": "^7.0.10", @@ -54,5 +54,5 @@ "tap-spec": "^5.0.0" }, "typings": "src/index.d.ts", - "gitHead": "2cec195f5a3678b17745155536a1714d9b1a5694" + "gitHead": "569f7d53f6f218d99aa8281929b541ab7e00058f" } \ No newline at end of file diff --git a/libs/remix-core-plugin/src/lib/offset-line-to-column-converter.ts b/libs/remix-core-plugin/src/lib/offset-line-to-column-converter.ts index b6e02bbcdd..0c416b2953 100644 --- a/libs/remix-core-plugin/src/lib/offset-line-to-column-converter.ts +++ b/libs/remix-core-plugin/src/lib/offset-line-to-column-converter.ts @@ -13,9 +13,11 @@ const profile = { export class OffsetToLineColumnConverter extends Plugin { lineBreakPositionsByContent: Record> sourceMappingDecoder: any + offsetConvertion: any constructor () { super(profile) this.lineBreakPositionsByContent = {} + this.offsetConvertion = {} this.sourceMappingDecoder = sourceMappingDecoder } @@ -45,7 +47,15 @@ export class OffsetToLineColumnConverter extends Plugin { } } } - return this.sourceMappingDecoder.convertOffsetToLineColumn(rawLocation, this.lineBreakPositionsByContent[file]) + + const token = `${rawLocation.start}:${rawLocation.length}:${file}` + if (this.offsetConvertion[token]) { + return this.offsetConvertion[token] + } else { + const convertion = this.sourceMappingDecoder.convertOffsetToLineColumn(rawLocation, this.lineBreakPositionsByContent[file]) + this.offsetConvertion[token] = convertion + return convertion + } } /** @@ -64,6 +74,7 @@ export class OffsetToLineColumnConverter extends Plugin { */ clear () { this.lineBreakPositionsByContent = {} + this.offsetConvertion = {} } /** diff --git a/libs/remix-debug/package.json b/libs/remix-debug/package.json index f36802e182..c2c3c18831 100644 --- a/libs/remix-debug/package.json +++ b/libs/remix-debug/package.json @@ -1,6 +1,6 @@ { "name": "@remix-project/remix-debug", - "version": "0.5.19", + "version": "0.5.20", "description": "Tool to debug Ethereum transactions", "contributors": [ { @@ -22,9 +22,9 @@ "@ethereumjs/common": "^2.5.0", "@ethereumjs/tx": "^3.3.2", "@ethereumjs/vm": "^5.5.3", - "@remix-project/remix-astwalker": "^0.0.49", - "@remix-project/remix-lib": "^0.5.19", - "@remix-project/remix-simulator": "^0.2.19", + "@remix-project/remix-astwalker": "^0.0.50", + "@remix-project/remix-lib": "^0.5.20", + "@remix-project/remix-simulator": "^0.2.20", "ansi-gray": "^0.1.1", "async": "^2.6.2", "color-support": "^1.1.3", @@ -68,5 +68,5 @@ }, "homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-debug#readme", "typings": "src/index.d.ts", - "gitHead": "2cec195f5a3678b17745155536a1714d9b1a5694" + "gitHead": "569f7d53f6f218d99aa8281929b541ab7e00058f" } \ No newline at end of file diff --git a/libs/remix-debug/src/Ethdebugger.ts b/libs/remix-debug/src/Ethdebugger.ts index 644a98e57a..f04a740e34 100644 --- a/libs/remix-debug/src/Ethdebugger.ts +++ b/libs/remix-debug/src/Ethdebugger.ts @@ -32,9 +32,11 @@ export class Ethdebugger { storageResolver callTree breakpointManager + offsetToLineColumnConverter constructor (opts) { this.compilationResult = opts.compilationResult || function (contractAddress) { return null } + this.offsetToLineColumnConverter = opts.offsetToLineColumnConverter this.web3 = opts.web3 this.opts = opts @@ -49,7 +51,8 @@ export class Ethdebugger { this.traceManager, this.solidityProxy, this.codeManager, - { ...opts, includeLocalVariables }) + { ...opts, includeLocalVariables }, + this.offsetToLineColumnConverter) } setManagers () { @@ -63,7 +66,8 @@ export class Ethdebugger { this.traceManager, this.solidityProxy, this.codeManager, - { ...this.opts, includeLocalVariables }) + { ...this.opts, includeLocalVariables }, + this.offsetToLineColumnConverter) } resolveStep (index) { @@ -71,7 +75,7 @@ export class Ethdebugger { } setCompilationResult (compilationResult) { - this.solidityProxy.reset((compilationResult && compilationResult.data) || {}) + this.solidityProxy.reset((compilationResult && compilationResult.data) || {}, (compilationResult && compilationResult.source && compilationResult.source.sources) || {}) } async sourceLocationFromVMTraceIndex (address, stepIndex) { diff --git a/libs/remix-debug/src/debugger/debugger.ts b/libs/remix-debug/src/debugger/debugger.ts index 41c846b937..a96171e196 100644 --- a/libs/remix-debug/src/debugger/debugger.ts +++ b/libs/remix-debug/src/debugger/debugger.ts @@ -14,6 +14,8 @@ export class Debugger { breakPointManager step_manager // eslint-disable-line camelcase vmDebuggerLogic + currentFile = -1 + currentLine = -1 constructor (options) { this.event = new EventManager() @@ -26,7 +28,8 @@ export class Debugger { this.debugger = new Ethdebugger({ web3: options.web3, debugWithGeneratedSources: options.debugWithGeneratedSources, - compilationResult: this.compilationResult + compilationResult: this.compilationResult, + offsetToLineColumnConverter: this.offsetToLineColumnConverter }) const { traceManager, callTree, solidityProxy } = this.debugger @@ -73,35 +76,56 @@ export class Debugger { const compilationResultForAddress = await this.compilationResult(address) if (!compilationResultForAddress) { this.event.trigger('newSourceLocation', [null]) + this.currentFile = -1 + this.currentLine = -1 + this.vmDebuggerLogic.event.trigger('lineGasCostChanged', [null]) return } - this.debugger.callTree.sourceLocationTracker.getValidSourceLocationFromVMTraceIndex(address, index, compilationResultForAddress.data.contracts).then(async (rawLocation) => { + this.debugger.callTree.getValidSourceLocationFromVMTraceIndexFromCache(address, index, compilationResultForAddress.data.contracts).then(async (rawLocationAndOpcode) => { if (compilationResultForAddress && compilationResultForAddress.data) { + const rawLocation = rawLocationAndOpcode.sourceLocation + const stepDetail = rawLocationAndOpcode.stepDetail const generatedSources = this.debugger.callTree.sourceLocationTracker.getGeneratedSourcesFromAddress(address) - const astSources = Object.assign({}, compilationResultForAddress.data.sources) - const sources = Object.assign({}, compilationResultForAddress.source.sources) - if (generatedSources) { - for (const genSource of generatedSources) { - astSources[genSource.name] = { id: genSource.id, ast: genSource.ast } - sources[genSource.name] = { content: genSource.contents } - } + + const lineColumnPos = rawLocationAndOpcode.lineColumnPos + + let lineGasCostObj = null + try { + lineGasCostObj = await this.debugger.callTree.getGasCostPerLine(rawLocation.file, lineColumnPos.start.line) + } catch (e) { + console.log(e) } - const lineColumnPos = await this.offsetToLineColumnConverter.offsetToLineColumn(rawLocation, rawLocation.file, sources, astSources) - this.event.trigger('newSourceLocation', [lineColumnPos, rawLocation, generatedSources, address]) + this.event.trigger('newSourceLocation', [lineColumnPos, rawLocation, generatedSources, address, stepDetail, (lineGasCostObj && lineGasCostObj.gasCost) || -1]) this.vmDebuggerLogic.event.trigger('sourceLocationChanged', [rawLocation]) + if (this.currentFile !== rawLocation.file || this.currentLine !== lineColumnPos.start.line) { + const instructionIndexes = lineGasCostObj.indexes.map((index) => { // translate from vmtrace index to instruction index + return this.debugger.codeManager.getInstructionIndex(address, index) + }) + this.vmDebuggerLogic.event.trigger('lineGasCostChanged', [instructionIndexes, lineColumnPos.start.line ]) + this.currentFile = rawLocation.file + this.currentLine = lineColumnPos.start.line + } } else { this.event.trigger('newSourceLocation', [null]) - this.vmDebuggerLogic.event.trigger('sourceLocationChanged', [null]) + this.currentFile = -1 + this.currentLine = -1 + this.vmDebuggerLogic.event.trigger('lineGasCostChanged', [null]) } }).catch((_error) => { this.event.trigger('newSourceLocation', [null]) this.vmDebuggerLogic.event.trigger('sourceLocationChanged', [null]) + this.currentFile = -1 + this.currentLine = -1 + this.vmDebuggerLogic.event.trigger('lineGasCostChanged', [null]) }) // }) } catch (error) { this.event.trigger('newSourceLocation', [null]) this.vmDebuggerLogic.event.trigger('sourceLocationChanged', [null]) + this.currentFile = -1 + this.currentLine = -1 + this.vmDebuggerLogic.event.trigger('lineGasCostChanged', [null]) return console.log(error) } } diff --git a/libs/remix-debug/src/solidity-decoder/decodeInfo.ts b/libs/remix-debug/src/solidity-decoder/decodeInfo.ts index f141a7e1f9..a66434a2b7 100644 --- a/libs/remix-debug/src/solidity-decoder/decodeInfo.ts +++ b/libs/remix-debug/src/solidity-decoder/decodeInfo.ts @@ -241,9 +241,7 @@ function getStructMembers (type, stateDefinitions, contractName, location) { if (type.indexOf('.') === -1) { type = contractName + '.' + type } - if (!contractName) { - contractName = type.split('.')[0] - } + contractName = type.split('.')[0] const state = stateDefinitions[contractName] if (state) { for (const dec of state.stateDefinitions) { diff --git a/libs/remix-debug/src/solidity-decoder/internalCallTree.ts b/libs/remix-debug/src/solidity-decoder/internalCallTree.ts index b5440d2d7d..ad0e1638f4 100644 --- a/libs/remix-debug/src/solidity-decoder/internalCallTree.ts +++ b/libs/remix-debug/src/solidity-decoder/internalCallTree.ts @@ -7,6 +7,16 @@ import { parseType } from './decodeInfo' import { isContractCreation, isCallInstruction, isCreateInstruction, isJumpDestInstruction } from '../trace/traceHelper' import { extractLocationFromAstVariable } from './types/util' +export type StepDetail = { + depth: number, + gas: number | string, + gasCost: number, + memory: number[], + op: string, + pc: number, + stack: number[], +} + /** * Tree representing internal jump into function. * Triggers `callTreeReady` event when tree is ready @@ -27,6 +37,15 @@ export class InternalCallTree { functionDefinitionByFile astWalker reducedTrace + locationAndOpcodePerVMTraceIndex: { + [Key: number]: any + } + gasCostPerLine + offsetToLineColumnConverter + pendingConstructorExecutionAt: number + pendingConstructorId: number + pendingConstructor + constructorsStartExecution /** * constructor @@ -37,14 +56,16 @@ export class InternalCallTree { * @param {Object} codeManager - code manager * @param {Object} opts - { includeLocalVariables, debugWithGeneratedSources } */ - constructor (debuggerEvent, traceManager, solidityProxy, codeManager, opts) { + constructor (debuggerEvent, traceManager, solidityProxy, codeManager, opts, offsetToLineColumnConverter?) { this.includeLocalVariables = opts.includeLocalVariables this.debugWithGeneratedSources = opts.debugWithGeneratedSources this.event = new EventManager() this.solidityProxy = solidityProxy this.traceManager = traceManager + this.offsetToLineColumnConverter = offsetToLineColumnConverter this.sourceLocationTracker = new SourceLocationTracker(codeManager, { debugWithGeneratedSources: opts.debugWithGeneratedSources }) debuggerEvent.register('newTraceLoaded', (trace) => { + const time = Date.now() this.reset() if (!this.solidityProxy.loaded()) { this.event.trigger('callTreeBuildFailed', ['compilation result not loaded. Cannot build internal call tree']) @@ -52,11 +73,17 @@ export class InternalCallTree { // each recursive call to buildTree represent a new context (either call, delegatecall, internal function) const calledAddress = traceManager.getCurrentCalledAddressAt(0) const isCreation = isContractCreation(calledAddress) - buildTree(this, 0, '', true, isCreation).then((result) => { + + const scopeId = '1' + this.scopeStarts[0] = scopeId + this.scopes[scopeId] = { firstStep: 0, locals: {}, isCreation, gasCost: 0 } + + buildTree(this, 0, scopeId, isCreation).then((result) => { if (result.error) { this.event.trigger('callTreeBuildFailed', [result.error]) } else { createReducedTrace(this, traceManager.trace.length - 1) + console.log('call tree build lasts ', (Date.now() - time) / 1000) this.event.trigger('callTreeReady', [this.scopes, this.scopeStarts]) } }, (reason) => { @@ -85,10 +112,16 @@ export class InternalCallTree { this.functionCallStack = [] this.functionDefinitionsByScope = {} this.scopeStarts = {} + this.gasCostPerLine = {} this.variableDeclarationByFile = {} this.functionDefinitionByFile = {} this.astWalker = new AstWalker() this.reducedTrace = [] + this.locationAndOpcodePerVMTraceIndex = {} + this.pendingConstructorExecutionAt = -1 + this.pendingConstructorId = -1 + this.constructorsStartExecution = {} + this.pendingConstructor = null } /** @@ -123,6 +156,7 @@ export class InternalCallTree { const scope = this.findScope(vmtraceIndex) if (!scope) return [] let scopeId = this.scopeStarts[scope.firstStep] + const scopeDetail = this.scopes[scopeId] const functions = [] if (!scopeId) return functions let i = 0 @@ -132,7 +166,7 @@ export class InternalCallTree { if (i > 1000) throw new Error('retrieFunctionStack: recursion too deep') const functionDefinition = this.functionDefinitionsByScope[scopeId] if (functionDefinition !== undefined) { - functions.push(functionDefinition) + functions.push({ ...functionDefinition, ...scopeDetail }) } const parent = this.parentScope(scopeId) if (!parent) break @@ -141,32 +175,42 @@ export class InternalCallTree { return functions } - async extractSourceLocation (step) { + async extractSourceLocation (step: number, address?: string) { try { - const address = this.traceManager.getCurrentCalledAddressAt(step) - const location = await this.sourceLocationTracker.getSourceLocationFromVMTraceIndex(address, step, this.solidityProxy.contracts) - return location + if (!address) address = this.traceManager.getCurrentCalledAddressAt(step) + return await this.sourceLocationTracker.getSourceLocationFromVMTraceIndex(address, step, this.solidityProxy.contracts) } catch (error) { throw new Error('InternalCallTree - Cannot retrieve sourcelocation for step ' + step + ' ' + error) } } - async extractValidSourceLocation (step) { + async extractValidSourceLocation (step: number, address?: string) { try { - const address = this.traceManager.getCurrentCalledAddressAt(step) - const location = await this.sourceLocationTracker.getValidSourceLocationFromVMTraceIndex(address, step, this.solidityProxy.contracts) - return location + if (!address) address = this.traceManager.getCurrentCalledAddressAt(step) + return await this.sourceLocationTracker.getValidSourceLocationFromVMTraceIndex(address, step, this.solidityProxy.contracts) } catch (error) { throw new Error('InternalCallTree - Cannot retrieve valid sourcelocation for step ' + step + ' ' + error) } } + + async getValidSourceLocationFromVMTraceIndexFromCache (address: string, step: number, contracts: any) { + return await this.sourceLocationTracker.getValidSourceLocationFromVMTraceIndexFromCache(address, step, contracts, this.locationAndOpcodePerVMTraceIndex) + } + + async getGasCostPerLine(file: number, line: number) { + if (this.gasCostPerLine[file] && this.gasCostPerLine[file][line]) { + return this.gasCostPerLine[file][line] + } + throw new Error('Could not find gas cost per line') + } } -async function buildTree (tree, step, scopeId, isExternalCall, isCreation) { +async function buildTree (tree, step, scopeId, isCreation, functionDefinition?, contractObj?, sourceLocation?, validSourceLocation?) { let subScope = 1 - tree.scopeStarts[step] = scopeId - tree.scopes[scopeId] = { firstStep: step, locals: {}, isCreation } - + if (functionDefinition) { + await registerFunctionParameters(tree, functionDefinition, step, scopeId, contractObj, validSourceLocation) + } + function callDepthChange (step, trace) { if (step + 1 < trace.length) { return trace[step].depth !== trace[step + 1].depth @@ -183,30 +227,104 @@ async function buildTree (tree, step, scopeId, isExternalCall, isCreation) { included.file === source.file) } - let currentSourceLocation = { start: -1, length: -1, file: -1 } + let currentSourceLocation = sourceLocation || { start: -1, length: -1, file: -1, jump: '-' } let previousSourceLocation = currentSourceLocation + let previousValidSourceLocation = validSourceLocation || currentSourceLocation while (step < tree.traceManager.trace.length) { let sourceLocation - let newLocation = false + let validSourceLocation + let address try { - sourceLocation = await tree.extractSourceLocation(step) + address = tree.traceManager.getCurrentCalledAddressAt(step) + sourceLocation = await tree.extractSourceLocation(step, address) + if (!includedSource(sourceLocation, currentSourceLocation)) { tree.reducedTrace.push(step) currentSourceLocation = sourceLocation - newLocation = true } + + const amountOfSources = tree.sourceLocationTracker.getTotalAmountOfSources(address, tree.solidityProxy.contracts) + if (tree.sourceLocationTracker.isInvalidSourceLocation(currentSourceLocation, amountOfSources)) { // file is -1 or greater than amount of sources + validSourceLocation = previousValidSourceLocation + } else + validSourceLocation = currentSourceLocation + } catch (e) { return { outStep: step, error: 'InternalCallTree - Error resolving source location. ' + step + ' ' + e } } if (!sourceLocation) { return { outStep: step, error: 'InternalCallTree - No source Location. ' + step } } - const isCallInstrn = isCallInstruction(tree.traceManager.trace[step]) - const isCreateInstrn = isCreateInstruction(tree.traceManager.trace[step]) + const stepDetail: StepDetail = tree.traceManager.trace[step] + const nextStepDetail: StepDetail = tree.traceManager.trace[step + 1] + if (stepDetail && nextStepDetail) { + stepDetail.gasCost = parseInt(stepDetail.gas as string) - parseInt(nextStepDetail.gas as string) + } + + // gas per line + let lineColumnPos + if (tree.offsetToLineColumnConverter) { + try { + const generatedSources = tree.sourceLocationTracker.getGeneratedSourcesFromAddress(address) + const astSources = Object.assign({}, tree.solidityProxy.sources) + const sources = Object.assign({}, tree.solidityProxy.sourcesCode) + if (generatedSources) { + for (const genSource of generatedSources) { + astSources[genSource.name] = { id: genSource.id, ast: genSource.ast } + sources[genSource.name] = { content: genSource.contents } + } + } + + lineColumnPos = await tree.offsetToLineColumnConverter.offsetToLineColumn(validSourceLocation, validSourceLocation.file, sources, astSources) + if (!tree.gasCostPerLine[validSourceLocation.file]) tree.gasCostPerLine[validSourceLocation.file] = {} + if (!tree.gasCostPerLine[validSourceLocation.file][lineColumnPos.start.line]) { + tree.gasCostPerLine[validSourceLocation.file][lineColumnPos.start.line] = { + gasCost: 0, + indexes: [] + } + } + tree.gasCostPerLine[validSourceLocation.file][lineColumnPos.start.line].gasCost += stepDetail.gasCost + tree.gasCostPerLine[validSourceLocation.file][lineColumnPos.start.line].indexes.push(step) + } catch (e) { + console.log(e) + } + } + + tree.locationAndOpcodePerVMTraceIndex[step] = { sourceLocation, stepDetail, lineColumnPos } + tree.scopes[scopeId].gasCost += stepDetail.gasCost + + const contractObj = await tree.solidityProxy.contractObjectAtAddress(address) + const generatedSources = getGeneratedSources(tree, scopeId, contractObj) + const functionDefinition = resolveFunctionDefinition(tree, sourceLocation, generatedSources) + + const isInternalTxInstrn = isCallInstruction(stepDetail) + const isCreateInstrn = isCreateInstruction(stepDetail) // we are checking if we are jumping in a new CALL or in an internal function - if (isCallInstrn || sourceLocation.jump === 'i') { + + const constructorExecutionStarts = tree.pendingConstructorExecutionAt > -1 && tree.pendingConstructorExecutionAt < validSourceLocation.start + if (functionDefinition && functionDefinition.kind === 'constructor' && tree.pendingConstructorExecutionAt === -1 && !tree.constructorsStartExecution[functionDefinition.id]) { + tree.pendingConstructorExecutionAt = validSourceLocation.start + tree.pendingConstructorId = functionDefinition.id + tree.pendingConstructor = functionDefinition + // from now on we'll be waiting for a change in the source location which will mark the beginning of the constructor execution. + // constructorsStartExecution allows to keep track on which constructor has already been executed. + } + const internalfunctionCall = functionDefinition && previousSourceLocation.jump === 'i' + if (constructorExecutionStarts || isInternalTxInstrn || internalfunctionCall) { try { - const externalCallResult = await buildTree(tree, step + 1, scopeId === '' ? subScope.toString() : scopeId + '.' + subScope, isCallInstrn, isCreateInstrn) + const newScopeId = scopeId === '' ? subScope.toString() : scopeId + '.' + subScope + tree.scopeStarts[step] = newScopeId + tree.scopes[newScopeId] = { firstStep: step, locals: {}, isCreation, gasCost: 0 } + // for the ctor we we are at the start of its trace, we have to replay this step in order to catch all the locals: + const nextStep = constructorExecutionStarts ? step : step + 1 + if (constructorExecutionStarts) { + tree.constructorsStartExecution[tree.pendingConstructorId] = tree.pendingConstructorExecutionAt + tree.pendingConstructorExecutionAt = -1 + tree.pendingConstructorId = -1 + await registerFunctionParameters(tree, tree.pendingConstructor, step, newScopeId, contractObj, previousValidSourceLocation) + tree.pendingConstructor = null + } + const externalCallResult = await buildTree(tree, nextStep, newScopeId, isCreateInstrn, functionDefinition, contractObj, sourceLocation, validSourceLocation) if (externalCallResult.error) { return { outStep: step, error: 'InternalCallTree - ' + externalCallResult.error } } else { @@ -216,7 +334,7 @@ async function buildTree (tree, step, scopeId, isExternalCall, isCreation) { } catch (e) { return { outStep: step, error: 'InternalCallTree - ' + e.message } } - } else if ((isExternalCall && callDepthChange(step, tree.traceManager.trace)) || (!isExternalCall && sourceLocation.jump === 'o')) { + } else if (callDepthChange(step, tree.traceManager.trace) || (sourceLocation.jump === 'o' && functionDefinition)) { // if not, we might be returning from a CALL or internal function. This is what is checked here. tree.scopes[scopeId].lastStep = step return { outStep: step + 1 } @@ -224,9 +342,10 @@ async function buildTree (tree, step, scopeId, isExternalCall, isCreation) { // if not, we are in the current scope. // We check in `includeVariableDeclaration` if there is a new local variable in scope for this specific `step` if (tree.includeLocalVariables) { - await includeVariableDeclaration(tree, step, sourceLocation, scopeId, newLocation, previousSourceLocation) + await includeVariableDeclaration(tree, step, sourceLocation, scopeId, contractObj, generatedSources) } previousSourceLocation = sourceLocation + previousValidSourceLocation = validSourceLocation step++ } } @@ -245,10 +364,33 @@ function getGeneratedSources (tree, scopeId, contractObj) { return null } -async function includeVariableDeclaration (tree, step, sourceLocation, scopeId, newLocation, previousSourceLocation) { - const contractObj = await tree.solidityProxy.contractObjectAt(step) +async function registerFunctionParameters (tree, functionDefinition, step, scopeId, contractObj, sourceLocation) { + tree.functionCallStack.push(step) + const functionDefinitionAndInputs = { functionDefinition, inputs: [] } + // means: the previous location was a function definition && JUMPDEST + // => we are at the beginning of the function and input/output are setup + try { + const stack = tree.traceManager.getStackAt(step) + const states = tree.solidityProxy.extractStatesDefinitions() + if (functionDefinition.parameters) { + const inputs = functionDefinition.parameters + const outputs = functionDefinition.returnParameters + // input params + if (inputs && inputs.parameters) { + functionDefinitionAndInputs.inputs = addParams(inputs, tree, scopeId, states, contractObj, sourceLocation, stack.length, inputs.parameters.length, -1) + } + // output params + if (outputs) addParams(outputs, tree, scopeId, states, contractObj, sourceLocation, stack.length, 0, 1) + } + } catch (error) { + console.log(error) + } + + tree.functionDefinitionsByScope[scopeId] = functionDefinitionAndInputs +} + +async function includeVariableDeclaration (tree, step, sourceLocation, scopeId, contractObj, generatedSources) { let states = null - const generatedSources = getGeneratedSources(tree, scopeId, contractObj) const variableDeclarations = resolveVariableDeclaration(tree, sourceLocation, generatedSources) // using the vm trace step, the current source location and the ast, // we check if the current vm trace step target a new ast node of type VariableDeclaration @@ -278,49 +420,6 @@ async function includeVariableDeclaration (tree, step, sourceLocation, scopeId, } } } - - // we check here if we are at the beginning inside a new function. - // if that is the case, we have to add to locals tree the inputs and output params - const functionDefinition = resolveFunctionDefinition(tree, previousSourceLocation, generatedSources) - if (!functionDefinition) return - - const previousIsJumpDest2 = isJumpDestInstruction(tree.traceManager.trace[step - 2]) - const previousIsJumpDest1 = isJumpDestInstruction(tree.traceManager.trace[step - 1]) - const isConstructor = functionDefinition.kind === 'constructor' - if (newLocation && (previousIsJumpDest1 || previousIsJumpDest2 || isConstructor)) { - tree.functionCallStack.push(step) - const functionDefinitionAndInputs = { functionDefinition, inputs: [] } - // means: the previous location was a function definition && JUMPDEST - // => we are at the beginning of the function and input/output are setup - - try { - const stack = tree.traceManager.getStackAt(step) - states = tree.solidityProxy.extractStatesDefinitions() - if (functionDefinition.parameters) { - const inputs = functionDefinition.parameters - const outputs = functionDefinition.returnParameters - // for (const element of functionDefinition.parameters) { - // if (element.nodeType === 'ParameterList') { - // if (!inputs) inputs = element - // else { - // outputs = element - // break - // } - // } - // } - // input params - if (inputs && inputs.parameters) { - functionDefinitionAndInputs.inputs = addParams(inputs, tree, scopeId, states, contractObj, previousSourceLocation, stack.length, inputs.parameters.length, -1) - } - // output params - if (outputs) addParams(outputs, tree, scopeId, states, contractObj, previousSourceLocation, stack.length, 0, 1) - } - } catch (error) { - console.log(error) - } - - tree.functionDefinitionsByScope[scopeId] = functionDefinitionAndInputs - } } // this extract all the variable declaration for a given ast and file @@ -388,7 +487,8 @@ function addParams (parameterList, tree, scopeId, states, contractObj, sourceLoc type: parseType(param.typeDescriptions.typeString, states, contractName, location), stackDepth: stackDepth, sourceLocation: sourceLocation, - abi: contractObj.contract.abi + abi: contractObj.contract.abi, + isParameter: true } params.push(attributesName) } diff --git a/libs/remix-debug/src/solidity-decoder/localDecoder.ts b/libs/remix-debug/src/solidity-decoder/localDecoder.ts index 4d697e9cc4..d7619ce12b 100644 --- a/libs/remix-debug/src/solidity-decoder/localDecoder.ts +++ b/libs/remix-debug/src/solidity-decoder/localDecoder.ts @@ -11,7 +11,7 @@ export async function solidityLocals (vmtraceIndex, internalTreeCall, stack, mem let anonymousIncr = 1 for (const local in scope.locals) { const variable = scope.locals[local] - if (variable.stackDepth < stack.length && variable.sourceLocation.start <= currentSourceLocation.start) { + if (variable.stackDepth < stack.length && (variable.sourceLocation.start <= currentSourceLocation.start || variable.isParameter)) { let name = variable.name if (name.indexOf('$') !== -1) { name = '<' + anonymousIncr + '>' @@ -21,7 +21,7 @@ export async function solidityLocals (vmtraceIndex, internalTreeCall, stack, mem locals[name] = await variable.type.decodeFromStack(variable.stackDepth, stack, memory, storageResolver, calldata, cursor, variable) } catch (e) { console.log(e) - locals[name] = { error: '' } + locals[name] = { error: '', type: variable && variable.type && variable.type.typeName || 'unknown' } } } } diff --git a/libs/remix-debug/src/solidity-decoder/solidityProxy.ts b/libs/remix-debug/src/solidity-decoder/solidityProxy.ts index b9c27dca82..0ec4029020 100644 --- a/libs/remix-debug/src/solidity-decoder/solidityProxy.ts +++ b/libs/remix-debug/src/solidity-decoder/solidityProxy.ts @@ -10,6 +10,8 @@ export class SolidityProxy { getCode sources contracts + compilationResult + sourcesCode constructor ({ getCurrentCalledAddressAt, getCode }) { this.cache = new Cache() @@ -23,9 +25,10 @@ export class SolidityProxy { * * @param {Object} compilationResult - result os a compilatiion (diectly returned by the compiler) */ - reset (compilationResult) { - this.sources = compilationResult.sources + reset (compilationResult, sources?) { + this.sources = compilationResult.sources // ast this.contracts = compilationResult.contracts + if (sources) this.sourcesCode = sources this.cache.reset() } @@ -44,8 +47,18 @@ export class SolidityProxy { * @param {Int} vmTraceIndex - index in the vm trave where to resolve the executed contract name * @param {Function} cb - callback returns (error, contractName) */ - async contractObjectAt (vmTraceIndex) { + async contractObjectAt (vmTraceIndex: number) { const address = this.getCurrentCalledAddressAt(vmTraceIndex) + return this.contractObjectAtAddress(address) + } + + /** + * retrieve the compiled contract name at the @arg vmTraceIndex (cached) + * + * @param {Int} vmTraceIndex - index in the vm trave where to resolve the executed contract name + * @param {Function} cb - callback returns (error, contractName) + */ + async contractObjectAtAddress (address: string) { if (this.cache.contractObjectByAddress[address]) { return this.cache.contractObjectByAddress[address] } diff --git a/libs/remix-debug/src/solidity-decoder/types/StringType.ts b/libs/remix-debug/src/solidity-decoder/types/StringType.ts index 849b8f65a3..84c6483e8e 100644 --- a/libs/remix-debug/src/solidity-decoder/types/StringType.ts +++ b/libs/remix-debug/src/solidity-decoder/types/StringType.ts @@ -25,7 +25,7 @@ export class StringType extends DynamicByteArray { return await super.decodeFromStack(stackDepth, stack, memory, storageResolver, calldata, cursor, variableDetails) } catch (e) { console.log(e) - return { error: '' } + return { error: '', type: this.typeName } } } diff --git a/libs/remix-debug/src/source/offsetToLineColumnConverter.ts b/libs/remix-debug/src/source/offsetToLineColumnConverter.ts index 6605b660b9..a1f296c76f 100644 --- a/libs/remix-debug/src/source/offsetToLineColumnConverter.ts +++ b/libs/remix-debug/src/source/offsetToLineColumnConverter.ts @@ -4,9 +4,11 @@ import { getLinebreakPositions, convertOffsetToLineColumn } from './sourceMappin export class OffsetToColumnConverter { lineBreakPositionsByContent sourceMappingDecoder + offsetConvertion constructor (compilerEvent) { this.lineBreakPositionsByContent = {} + this.offsetConvertion = {} if (compilerEvent) { compilerEvent.register('compilationFinished', (success, data, source, input, version) => { this.clear() @@ -26,10 +28,18 @@ export class OffsetToColumnConverter { } } } - return convertOffsetToLineColumn(rawLocation, this.lineBreakPositionsByContent[file]) + const token = `${rawLocation.start}:${rawLocation.length}:${file}` + if (this.offsetConvertion[token]) { + return this.offsetConvertion[token] + } else { + const convertion = convertOffsetToLineColumn(rawLocation, this.lineBreakPositionsByContent[file]) + this.offsetConvertion[token] = convertion + return convertion + } } clear () { this.lineBreakPositionsByContent = {} + this.offsetConvertion = {} } } diff --git a/libs/remix-debug/src/source/sourceLocationTracker.ts b/libs/remix-debug/src/source/sourceLocationTracker.ts index f9967b5e98..c7502db960 100644 --- a/libs/remix-debug/src/source/sourceLocationTracker.ts +++ b/libs/remix-debug/src/source/sourceLocationTracker.ts @@ -87,13 +87,34 @@ export class SourceLocationTracker { (map.file > amountOfSources - 1) this indicates the current file index exceed the total number of files. this happens when generated sources should not be considered. */ - while (vmtraceStepIndex >= 0 && (map.file === -1 || map.file > amountOfSources - 1)) { + while (vmtraceStepIndex >= 0 && this.isInvalidSourceLocation(map, amountOfSources)) { map = await this.getSourceLocationFromVMTraceIndex(address, vmtraceStepIndex, contracts) vmtraceStepIndex = vmtraceStepIndex - 1 } return map } + isInvalidSourceLocation (sourceLocation, amountOfSources) { + return sourceLocation.file === -1 || sourceLocation.file > amountOfSources - 1 + } + + async getValidSourceLocationFromVMTraceIndexFromCache (address: string, vmtraceStepIndex: number, contracts: any, cache: Map) { + const amountOfSources = this.getTotalAmountOfSources(address, contracts) + let map: any = { file: -1 } + /* + (map.file === -1) this indicates that it isn't associated with a known source code + (map.file > amountOfSources - 1) this indicates the current file index exceed the total number of files. + this happens when generated sources should not be considered. + */ + const originStep = cache[vmtraceStepIndex] + while (vmtraceStepIndex >= 0 && (map.file === -1 || map.file > amountOfSources - 1)) { + map = cache[vmtraceStepIndex].sourceLocation + vmtraceStepIndex = vmtraceStepIndex - 1 + originStep.sourceLocation = map + } + return originStep + } + clearCache () { this.sourceMapByAddress = {} } diff --git a/libs/remix-debug/test/decoder/localDecoder.ts b/libs/remix-debug/test/decoder/localDecoder.ts index fc79752e08..fdeb743683 100644 --- a/libs/remix-debug/test/decoder/localDecoder.ts +++ b/libs/remix-debug/test/decoder/localDecoder.ts @@ -23,7 +23,7 @@ tape('solidity', function (t) { async function test (st, privateKey) { var output = compiler.compile(compilerInput(intLocal.contract)) output = JSON.parse(output) - await intLocalTest(st, privateKey, output.contracts['test.sol']['intLocal'].evm.bytecode.object, output) + await intLocalTest(st, privateKey, output.contracts['test.sol']['intLocal'].evm.bytecode.object, output, intLocal.contract) output = compiler.compile(compilerInput(miscLocal.contract)) output = JSON.parse(output) await miscLocalTest(st, privateKey, output.contracts['test.sol']['miscLocal'].evm.bytecode.object, output) diff --git a/libs/remix-debug/test/decoder/localsTests/int.ts b/libs/remix-debug/test/decoder/localsTests/int.ts index 9bf1c4307f..c36a217df3 100644 --- a/libs/remix-debug/test/decoder/localsTests/int.ts +++ b/libs/remix-debug/test/decoder/localsTests/int.ts @@ -7,9 +7,10 @@ import { contractCreationToken } from '../../../src/trace/traceHelper' import { SolidityProxy } from '../../../src/solidity-decoder/solidityProxy' import { InternalCallTree } from '../../../src/solidity-decoder/internalCallTree' import { EventManager } from '../../../src/eventManager' +import * as sourceMappingDecoder from '../../../src/source/sourceMappingDecoder' import * as helper from './helper' -module.exports = function (st, privateKey, contractBytecode, compilationResult) { +module.exports = function (st, privateKey, contractBytecode, compilationResult, contractCode) { return new Promise(async (resolve) => { let web3 = await (vmCall as any).getWeb3(); (vmCall as any).sendTx(web3, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, hash) { @@ -27,22 +28,37 @@ module.exports = function (st, privateKey, contractBytecode, compilationResult) var solidityProxy = new SolidityProxy({ getCurrentCalledAddressAt: traceManager.getCurrentCalledAddressAt.bind(traceManager), getCode: codeManager.getCode.bind(codeManager) }) solidityProxy.reset(compilationResult) var debuggerEvent = new EventManager() - var callTree = new InternalCallTree(debuggerEvent, traceManager, solidityProxy, codeManager, { includeLocalVariables: true }) + const offsetToLineColumnConverter = { + offsetToLineColumn: (rawLocation) => { + return new Promise((resolve) => { + const lineBreaks = sourceMappingDecoder.getLinebreakPositions(contractCode) + resolve(sourceMappingDecoder.convertOffsetToLineColumn(rawLocation, lineBreaks)) + }) + } + } + var callTree = new InternalCallTree(debuggerEvent, traceManager, solidityProxy, codeManager, { includeLocalVariables: true }, offsetToLineColumnConverter) callTree.event.register('callTreeBuildFailed', (error) => { st.fail(error) }) callTree.event.register('callTreeNotReady', (reason) => { st.fail(reason) }) - callTree.event.register('callTreeReady', (scopes, scopeStarts) => { + callTree.event.register('callTreeReady', async (scopes, scopeStarts) => { try { - let functions1 = callTree.retrieveFunctionsStack(102) - let functions2 = callTree.retrieveFunctionsStack(115) + + // test gas cost per line + st.equals((await callTree.getGasCostPerLine(0, 16)).gasCost, 11) + st.equals((await callTree.getGasCostPerLine(0, 32)).gasCost, 84) + + let functions1 = callTree.retrieveFunctionsStack(103) + let functions2 = callTree.retrieveFunctionsStack(116) let functions3 = callTree.retrieveFunctionsStack(13) - - st.equals(functions1.length, 1) - st.equals(functions2.length, 2) - st.equals(functions3.length, 0) + + st.equals(functions1.length, 2) + st.equals(functions2.length, 3) + st.equals(functions3.length, 1) + + st.equal(functions1[0].gasCost, 54) st.equals(Object.keys(functions1[0])[0], 'functionDefinition') st.equals(Object.keys(functions1[0])[1], 'inputs') @@ -57,34 +73,34 @@ module.exports = function (st, privateKey, contractBytecode, compilationResult) st.equals(functions1[0].functionDefinition.name, 'level11') st.equals(functions2[0].functionDefinition.name, 'level12') st.equals(functions2[1].functionDefinition.name, 'level11') - - st.equals(scopeStarts[0], '') - st.equals(scopeStarts[13], '1') - st.equals(scopeStarts[102], '2') - st.equals(scopeStarts[115], '2.1') - st.equals(scopeStarts[136], '3') - st.equals(scopeStarts[153], '4') - st.equals(scopeStarts[166], '4.1') - st.equals(scopes[''].locals['ui8'].type.typeName, 'uint8') - st.equals(scopes[''].locals['ui16'].type.typeName, 'uint16') - st.equals(scopes[''].locals['ui32'].type.typeName, 'uint32') - st.equals(scopes[''].locals['ui64'].type.typeName, 'uint64') - st.equals(scopes[''].locals['ui128'].type.typeName, 'uint128') - st.equals(scopes[''].locals['ui256'].type.typeName, 'uint256') - st.equals(scopes[''].locals['ui'].type.typeName, 'uint256') - st.equals(scopes[''].locals['i8'].type.typeName, 'int8') - st.equals(scopes[''].locals['i16'].type.typeName, 'int16') - st.equals(scopes[''].locals['i32'].type.typeName, 'int32') - st.equals(scopes[''].locals['i64'].type.typeName, 'int64') - st.equals(scopes[''].locals['i128'].type.typeName, 'int128') - st.equals(scopes[''].locals['i256'].type.typeName, 'int256') - st.equals(scopes[''].locals['i'].type.typeName, 'int256') - st.equals(scopes[''].locals['ishrink'].type.typeName, 'int32') - st.equals(scopes['2'].locals['ui8'].type.typeName, 'uint8') - st.equals(scopes['2.1'].locals['ui81'].type.typeName, 'uint8') - st.equals(scopes['3'].locals['ui81'].type.typeName, 'uint8') - st.equals(scopes['4'].locals['ui8'].type.typeName, 'uint8') - st.equals(scopes['4.1'].locals['ui81'].type.typeName, 'uint8') + + st.equals(scopeStarts[0], '1') + st.equals(scopeStarts[10], '1.1') + st.equals(scopeStarts[102], '1.1.1') + st.equals(scopeStarts[115], '1.1.1.1') + st.equals(scopeStarts[136], '1.1.2') + st.equals(scopeStarts[153], '1.1.3') + st.equals(scopeStarts[166], '1.1.3.1') + st.equals(scopes['1.1'].locals['ui8'].type.typeName, 'uint8') + st.equals(scopes['1.1'].locals['ui16'].type.typeName, 'uint16') + st.equals(scopes['1.1'].locals['ui32'].type.typeName, 'uint32') + st.equals(scopes['1.1'].locals['ui64'].type.typeName, 'uint64') + st.equals(scopes['1.1'].locals['ui128'].type.typeName, 'uint128') + st.equals(scopes['1.1'].locals['ui256'].type.typeName, 'uint256') + st.equals(scopes['1.1'].locals['ui'].type.typeName, 'uint256') + st.equals(scopes['1.1'].locals['i8'].type.typeName, 'int8') + st.equals(scopes['1.1'].locals['i16'].type.typeName, 'int16') + st.equals(scopes['1.1'].locals['i32'].type.typeName, 'int32') + st.equals(scopes['1.1'].locals['i64'].type.typeName, 'int64') + st.equals(scopes['1.1'].locals['i128'].type.typeName, 'int128') + st.equals(scopes['1.1'].locals['i256'].type.typeName, 'int256') + st.equals(scopes['1.1'].locals['i'].type.typeName, 'int256') + st.equals(scopes['1.1'].locals['ishrink'].type.typeName, 'int32') + st.equals(scopes['1.1.1'].locals['ui8'].type.typeName, 'uint8') + st.equals(scopes['1.1.1.1'].locals['ui81'].type.typeName, 'uint8') + st.equals(scopes['1.1.2'].locals['ui81'].type.typeName, 'uint8') + st.equals(scopes['1.1.3'].locals['ui8'].type.typeName, 'uint8') + st.equals(scopes['1.1.3.1'].locals['ui81'].type.typeName, 'uint8') } catch (e) { st.fail(e.message) } diff --git a/libs/remix-lib/package.json b/libs/remix-lib/package.json index de16bf37b2..03fffb03d7 100644 --- a/libs/remix-lib/package.json +++ b/libs/remix-lib/package.json @@ -1,6 +1,6 @@ { "name": "@remix-project/remix-lib", - "version": "0.5.19", + "version": "0.5.20", "description": "Library to various Remix tools", "contributors": [ { @@ -54,5 +54,5 @@ }, "homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-lib#readme", "typings": "src/index.d.ts", - "gitHead": "2cec195f5a3678b17745155536a1714d9b1a5694" + "gitHead": "569f7d53f6f218d99aa8281929b541ab7e00058f" } \ No newline at end of file diff --git a/libs/remix-simulator/package.json b/libs/remix-simulator/package.json index 185a5ccafb..74ade97b55 100644 --- a/libs/remix-simulator/package.json +++ b/libs/remix-simulator/package.json @@ -1,6 +1,6 @@ { "name": "@remix-project/remix-simulator", - "version": "0.2.19", + "version": "0.2.20", "description": "Ethereum IDE and tools for the web", "contributors": [ { @@ -18,7 +18,7 @@ "@ethereumjs/common": "^2.5.0", "@ethereumjs/tx": "^3.3.2", "@ethereumjs/vm": "^5.5.3", - "@remix-project/remix-lib": "^0.5.19", + "@remix-project/remix-lib": "^0.5.20", "ansi-gray": "^0.1.1", "async": "^3.1.0", "body-parser": "^1.18.2", @@ -67,5 +67,5 @@ }, "homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-simulator#readme", "typings": "src/index.d.ts", - "gitHead": "2cec195f5a3678b17745155536a1714d9b1a5694" + "gitHead": "569f7d53f6f218d99aa8281929b541ab7e00058f" } \ No newline at end of file diff --git a/libs/remix-solidity/package.json b/libs/remix-solidity/package.json index 0cba64b655..f58e6f94e0 100644 --- a/libs/remix-solidity/package.json +++ b/libs/remix-solidity/package.json @@ -1,6 +1,6 @@ { "name": "@remix-project/remix-solidity", - "version": "0.5.5", + "version": "0.5.6", "description": "Tool to load and run Solidity compiler", "main": "src/index.js", "types": "src/index.d.ts", @@ -18,7 +18,7 @@ "@ethereumjs/block": "^3.5.1", "@ethereumjs/tx": "^3.3.2", "@ethereumjs/vm": "^5.5.3", - "@remix-project/remix-lib": "^0.5.19", + "@remix-project/remix-lib": "^0.5.20", "async": "^2.6.2", "eslint-scope": "^5.0.0", "ethereumjs-util": "^7.0.10", @@ -61,5 +61,5 @@ }, "homepage": "https://github.com/ethereum/remix-project/tree/master/libs/remix-solidity#readme", "typings": "src/index.d.ts", - "gitHead": "2cec195f5a3678b17745155536a1714d9b1a5694" + "gitHead": "569f7d53f6f218d99aa8281929b541ab7e00058f" } \ No newline at end of file diff --git a/libs/remix-solidity/src/compiler/compiler-utils.ts b/libs/remix-solidity/src/compiler/compiler-utils.ts index 310cc50f1c..df959df896 100644 --- a/libs/remix-solidity/src/compiler/compiler-utils.ts +++ b/libs/remix-solidity/src/compiler/compiler-utils.ts @@ -52,7 +52,7 @@ export function canUseWorker (selectedVersion) { } function browserSupportWorker () { - return document.location.protocol !== 'file:' && Worker !== undefined + return document ? document.location.protocol !== 'file:' && Worker !== undefined : false } // returns a promise for minixhr diff --git a/libs/remix-tests/package.json b/libs/remix-tests/package.json index 74a0cf5f42..0f00c4209f 100644 --- a/libs/remix-tests/package.json +++ b/libs/remix-tests/package.json @@ -1,6 +1,6 @@ { "name": "@remix-project/remix-tests", - "version": "0.2.19", + "version": "0.2.20", "description": "Tool to test Solidity smart contracts", "main": "src/index.js", "types": "./src/index.d.ts", @@ -40,10 +40,10 @@ "@ethereumjs/common": "^2.5.0", "@ethereumjs/tx": "^3.3.2", "@ethereumjs/vm": "^5.5.3", - "@remix-project/remix-lib": "^0.5.19", - "@remix-project/remix-simulator": "^0.2.19", - "@remix-project/remix-solidity": "^0.5.5", - "@remix-project/remix-url-resolver": "^0.0.40", + "@remix-project/remix-lib": "^0.5.20", + "@remix-project/remix-simulator": "^0.2.20", + "@remix-project/remix-solidity": "^0.5.6", + "@remix-project/remix-url-resolver": "^0.0.41", "ansi-gray": "^0.1.1", "async": "^2.6.0", "axios": "1.1.2", @@ -81,5 +81,5 @@ "typescript": "^3.3.1" }, "typings": "src/index.d.ts", - "gitHead": "2cec195f5a3678b17745155536a1714d9b1a5694" + "gitHead": "569f7d53f6f218d99aa8281929b541ab7e00058f" } \ No newline at end of file diff --git a/libs/remix-tests/tests/testRunner.spec.__ b/libs/remix-tests/tests/testRunner.spec.__ new file mode 100644 index 0000000000..c475914522 --- /dev/null +++ b/libs/remix-tests/tests/testRunner.spec.__ @@ -0,0 +1,484 @@ +import * as async from 'async' +import Web3 from 'web3'; +import * as assert from 'assert' +import { Provider, extend } from '@remix-project/remix-simulator' + +import { compileFileOrFiles } from '../src/compiler' +import { deployAll } from '../src/deployer' +import { runTest, compilationInterface } from '../src/index' +import { ResultsInterface, TestCbInterface, ResultCbInterface } from '../src/index' + +// deepEqualExcluding allows us to exclude specific keys whose values vary. +// In this specific test, we'll use this helper to exclude `time` keys. +// Assertions for the existance of these will be made at the correct places. +function deepEqualExcluding(a: any, b: any, excludedKeys: string[]) { + function removeKeysFromObject(obj: any, excludedKeys: string[]) { + if (obj !== Object(obj)) { + return obj + } + + if(Object.prototype.toString.call(obj) !== '[object Array]') { + obj = Object.assign({}, obj) + for (const key of excludedKeys) { + delete obj[key] + } + + return obj + } + + let newObj = [] + for (const idx in obj) { + newObj[idx] = removeKeysFromObject(obj[idx], excludedKeys); + } + + return newObj + } + + let aStripped: any = removeKeysFromObject(a, excludedKeys); + let bStripped: any = removeKeysFromObject(b, excludedKeys); + assert.deepEqual(aStripped, bStripped) +} + +let accounts: string[] +let provider: any = new Provider() + +async function compileAndDeploy(filename: string, callback: Function) { + let web3: Web3 = new Web3() + let sourceASTs: any = {} + await provider.init() + web3.setProvider(provider) + extend(web3) + let compilationData: object + async.waterfall([ + function getAccountList(next: Function): void { + web3.eth.getAccounts((_err: Error | null | undefined, _accounts: string[]) => { + accounts = _accounts + web3.eth.defaultAccount = accounts[0] + next(_err) + }) + }, + function compile(next: Function): void { + compileFileOrFiles(filename, false, { accounts }, null, next) + }, + function deployAllContracts(compilationResult: compilationInterface, asts, next: Function): void { + for(const filename in asts) { + if(filename.endsWith('_test.sol')) + sourceASTs[filename] = asts[filename].ast + } + try { + compilationData = compilationResult + deployAll(compilationResult, web3, accounts, false, null, next) + } catch (e) { + throw e + } + } + ], function (_err: Error | null | undefined, contracts: any): void { + callback(null, compilationData, contracts, sourceASTs, accounts, web3) + }) +} + +// Use `export NODE_OPTIONS="--max-old-space-size=4096"` if there is a JavaScript heap out of memory issue + +describe('testRunner', () => { + let tests: any[] = [], results: ResultsInterface; + + const testCallback: TestCbInterface = (err, test) => { + if (err) { throw err } + + if (test.type === 'testPass' || test.type === 'testFailure') { + assert.ok(test.time, 'test time not reported') + assert.ok(!Number.isInteger(test.time || 0), 'test time should not be an integer') + } + + tests.push(test) + } + + const resultsCallback: Function = (done) => { + return (err, _results) => { + if (err) { throw err } + results = _results + done() + } + } + + describe('#runTest', () => { + + describe('assert library OK method tests', () => { + const filename: string = __dirname + '/examples_0/assert_ok_test.sol' + + beforeAll((done) => { + compileAndDeploy(filename, (_err: Error | null | undefined, compilationData: object, contracts: any, asts: any, accounts: string[], web3: any) => { + runTest('AssertOkTest', contracts.AssertOkTest, compilationData[filename]['AssertOkTest'], asts[filename], { accounts, web3 }, testCallback, resultsCallback(done)) + }) + }) + + afterAll(() => { tests = [] }) + + it('should have 1 passing test', () => { + assert.equal(results.passingNum, 1) + }) + + it('should have 1 failing test', () => { + assert.equal(results.failureNum, 1) + }) + + const hhLogs1 = [ [ "AssertOkTest", "okPassTest"]] + const hhLogs2 = [ [ "AssertOkTest", "okFailTest"]] + it('should return', () => { + deepEqualExcluding(tests, [ + { type: 'accountList', value: accounts }, + { type: 'contract', value: 'AssertOkTest', filename: __dirname + '/examples_0/assert_ok_test.sol' }, + { type: 'testPass', debugTxHash: '0x5b665752a4faf83229259b9b2811d3295be0af633b0051d4b90042283ef55707', value: 'Ok pass test', filename: __dirname + '/examples_0/assert_ok_test.sol', context: 'AssertOkTest', hhLogs: hhLogs1 }, + { type: 'testFailure', debugTxHash: '0xa0a30ad042a7fc3495f72be7ba788d705888ffbbec7173f60bb27e07721510f2',value: 'Ok fail test', filename: __dirname + '/examples_0/assert_ok_test.sol', errMsg: 'okFailTest fails', context: 'AssertOkTest', hhLogs: hhLogs2, assertMethod: 'ok', location: '366:36:0', expected: 'true', returned: 'false'}, + + ], ['time', 'web3']) + }) + }) + + describe('assert library EQUAL method tests', () => { + const filename: string = __dirname + '/examples_0/assert_equal_test.sol' + + beforeAll((done) => { + compileAndDeploy(filename, (_err: Error | null | undefined, compilationData: object, contracts: any, asts: any, accounts: string[], web3: any) => { + runTest('AssertEqualTest', contracts.AssertEqualTest, compilationData[filename]['AssertEqualTest'], asts[filename], { accounts }, testCallback, resultsCallback(done)) + }) + }) + + afterAll(() => { tests = [] }) + + it('should have 6 passing test', () => { + assert.equal(results.passingNum, 6) + }) + + it('should have 6 failing test', () => { + assert.equal(results.failureNum, 6) + }) + + it('should return', () => { + deepEqualExcluding(tests, [ + { type: 'accountList', value: accounts }, + { type: 'contract', value: 'AssertEqualTest', filename: __dirname + '/examples_0/assert_equal_test.sol' }, + { type: 'testPass', debugTxHash: '0xbe77baee10f8a044a68fbb83abf57ce1ca63b8739f3b2dcd122dab0ee44fd0e9', value: 'Equal uint pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, + { type: 'testFailure', debugTxHash: '0xfc9691b0cd0a49dbefed5cd3fad32158dd229e5bb7b0eb11da3c72054eafae8b', value: 'Equal uint fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalUintFailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '273:57:0', expected: '2', returned: '1'}, + { type: 'testPass', debugTxHash: '0xbc142789e5a51841781536a9291c9022896b0c7453140fdc98996638c0d76045', value: 'Equal int pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, + { type: 'testFailure', debugTxHash: '0xcc28211a4ab3149b1122fb47f45529a4edddbafa076d2338cc3754ab0629eaa1', value: 'Equal int fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalIntFailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '493:45:0', expected: '2', returned: '-1'}, + { type: 'testPass', debugTxHash: '0x4f5570fc7da86f09aafb436ff3b4c46aa885f71680a233234433d0ef0346206b', value: 'Equal bool pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, + { type: 'testFailure', debugTxHash: '0x28dc2d146dee77a2d6446efb088e5f9d008a3c7a14116e798401b68470da017f', value: 'Equal bool fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalBoolFailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '708:52:0', expected: false, returned: true}, + { type: 'testPass', debugTxHash: '0x0abc8fa8831efa3a8c82c758d045c1382f71b6a7f7e9135ffbe9e40059f84617', value: 'Equal address pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, + { type: 'testFailure', debugTxHash: '0x5ec200fb053539b8165a6b6ab36e9229a870c4752b0d6ff2786c4d5a66d5b35d', value: 'Equal address fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalAddressFailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '1015:130:0', expected: '0x1c6637567229159d1eFD45f95A6675e77727E013', returned: '0x7994f14563F39875a2F934Ce42cAbF48a93FdDA9'}, + { type: 'testPass', debugTxHash: '0xb6c34f5baa6916569d122bcb1210fcd07fb126f4b859fea58db5328e5f1dab85', value: 'Equal bytes32 pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, + { type: 'testFailure', debugTxHash: '0xb3af74a384b8b6ddacbc03a480ae48e233415b1570717ca7023530023a871be6', value: 'Equal bytes32 fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalBytes32FailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '1670:48:0', expected: '0x72656d6978000000000000000000000000000000000000000000000000000000', returned: '0x72656d6979000000000000000000000000000000000000000000000000000000'}, + { type: 'testPass', debugTxHash: '0x8537e74941b511b5c745b398e55435748adcdf637659247e0d573fb681ee4833', value: 'Equal string pass test', filename: __dirname + '/examples_0/assert_equal_test.sol', context: 'AssertEqualTest' }, + { type: 'testFailure', debugTxHash: '0x30d44498e63ac51f1412062b849144c103e19a4dc9daf81c5e84bd984ef738a6', value: 'Equal string fail test', filename: __dirname + '/examples_0/assert_equal_test.sol', errMsg: 'equalStringFailTest fails', context: 'AssertEqualTest', assertMethod: 'equal', location: '1916:81:0', expected: 'remix-tests', returned: 'remix'} + ], ['time', 'web3']) + }) + }) + + describe('assert library NOTEQUAL method tests', () => { + const filename: string = __dirname + '/examples_0/assert_notEqual_test.sol' + + beforeAll((done) => { + compileAndDeploy(filename, (_err: Error | null | undefined, compilationData: object, contracts: any, asts: any, accounts: string[], web3: any) => { + runTest('AssertNotEqualTest', contracts.AssertNotEqualTest, compilationData[filename]['AssertNotEqualTest'], asts[filename], { accounts }, testCallback, resultsCallback(done)) + }) + }) + + afterAll(() => { tests = [] }) + + it('should have 6 passing test', () => { + assert.equal(results.passingNum, 6) + }) + + it('should have 6 failing test', () => { + assert.equal(results.failureNum, 6) + }) + + it('should return', () => { + deepEqualExcluding(tests, [ + { type: 'accountList', value: accounts }, + { type: 'contract', value: 'AssertNotEqualTest', filename: __dirname + '/examples_0/assert_notEqual_test.sol' }, + { type: 'testPass', debugTxHash: '0xb0ac5cde13a5005dc1b4efbb66fb3a5d6f0697467aedd6165ed1c8ee552939bc', value: 'Not equal uint pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, + { type: 'testFailure', debugTxHash: '0x69d25ed9b9a010e97e0f7282313d4018c77a8873fd5f2e0b12483701febdd6b4', value: 'Not equal uint fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualUintFailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '288:63:0', expected: '1', returned: '1'}, + { type: 'testPass', debugTxHash: '0x19a743cfb44b273c78b3271d603412d31087974309a70595bc5d15097a52c5c5', value: 'Not equal int pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, + { type: 'testFailure', debugTxHash: '0x8dfce61b71a9ea65fb00c471370413779626f290b71d41f8be8408674e64f4d2', value: 'Not equal int fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualIntFailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '525:52:0', expected: '-2', returned: '-2'}, + { type: 'testPass', debugTxHash: '0x12bc9eb3a653ebe4c7ab954c144dab4848295c88d71d17cb86a41e8a004419ba', value: 'Not equal bool pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, + { type: 'testFailure', debugTxHash: '0x901b9cd631f8f29841243a257d1914060b9c36d88ee7d8b624de76dad4a34c85', value: 'Not equal bool fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualBoolFailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '760:57:0', expected: true, returned: true}, + { type: 'testPass', debugTxHash: '0xf9e48bac26d3a2871ceb92974b897cae61e60bbc4db115b7e8eff4ac0390e4a5', value: 'Not equal address pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, + { type: 'testFailure', debugTxHash: '0x4e83a17426bc79b147ddd30a5434a2430a8302b3ce78b6979088ac5eceac5d3c', value: 'Not equal address fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualAddressFailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '1084:136:0', expected: 0x7994f14563F39875a2F934Ce42cAbF48a93FdDA9, returned: 0x7994f14563F39875a2F934Ce42cAbF48a93FdDA9}, + { type: 'testPass', debugTxHash: '0xfb4b30bb8373eeb6b09e38ad07880b86654f72780b41d7cf7554a112a04a0f53', value: 'Not equal bytes32 pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, + { type: 'testFailure', debugTxHash: '0x43edf8bc68229415ee63f63f5303351a0dfecf9f3eb2ec35ffd2c10c60cf7005', value: 'Not equal bytes32 fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualBytes32FailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '1756:54:0', expected: '0x72656d6978000000000000000000000000000000000000000000000000000000', returned: '0x72656d6978000000000000000000000000000000000000000000000000000000'}, + { type: 'testPass', debugTxHash: '0x712932edc040596e2b02ddc06a48b773f5fccc7346d55cefc5d1c52528ce3168', value: 'Not equal string pass test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', context: 'AssertNotEqualTest' }, + { type: 'testFailure', debugTxHash: '0x961ce7425fdd4049aeb678ea20a0441eb837c1fe26b0d010593fc07d668321e6', value: 'Not equal string fail test', filename: __dirname + '/examples_0/assert_notEqual_test.sol', errMsg: 'notEqualStringFailTest fails', context: 'AssertNotEqualTest', assertMethod: 'notEqual', location: '2026:81:0', expected: 'remix', returned: 'remix'}, + ], ['time', 'web3']) + }) + }) + + describe('assert library GREATERTHAN method tests', () => { + const filename: string = __dirname + '/examples_0/assert_greaterThan_test.sol' + + beforeAll((done) => { + compileAndDeploy(filename, (_err: Error | null | undefined, compilationData: object, contracts: any, asts: any, accounts: string[], web3: any) => { + runTest('AssertGreaterThanTest', contracts.AssertGreaterThanTest, compilationData[filename]['AssertGreaterThanTest'], asts[filename], { accounts }, testCallback, resultsCallback(done)) + }) + }) + + afterAll(() => { tests = [] }) + + it('should have 4 passing test', () => { + assert.equal(results.passingNum, 4) + }) + + it('should have 4 failing test', () => { + assert.equal(results.failureNum, 4) + }) + it('should return', () => { + deepEqualExcluding(tests, [ + { type: 'accountList', value: accounts }, + { type: 'contract', value: 'AssertGreaterThanTest', filename: __dirname + '/examples_0/assert_greaterThan_test.sol' }, + { type: 'testPass', debugTxHash: '0x81cf46560b522280ac60bd5c8efedad4598957d33435a898c23eefb13ca6104f', value: 'Greater than uint pass test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', context: 'AssertGreaterThanTest' }, + { type: 'testFailure', debugTxHash: '0x7090dc27ac06e1afd66963992bdd9188200d0404d43b95cfa5d925bbe6eba3ed', value: 'Greater than uint fail test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', errMsg: 'greaterThanUintFailTest fails', context: 'AssertGreaterThanTest', assertMethod: 'greaterThan', location: '303:69:0', expected: '4', returned: '1'}, + { type: 'testPass', debugTxHash: '0xd57b40ed464763baf128f8a72efcd198e825e0b8f498cef90aed23045d0196db', value: 'Greater than int pass test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', context: 'AssertGreaterThanTest' }, + { type: 'testFailure', debugTxHash: '0x6c7b84bd8fc452b7839e11129d3818fa69dfd9b914e55556b39fdc68b70fc1b5', value: 'Greater than int fail test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', errMsg: 'greaterThanIntFailTest fails', context: 'AssertGreaterThanTest', assertMethod: 'greaterThan', location: '569:67:0', expected: '1', returned: '-1'}, + { type: 'testPass', debugTxHash: '0xc5883db70b83a1d3afff24a9f0555de3edd7776e5ec157cc2110e426e5be2594', value: 'Greater than uint int pass test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', context: 'AssertGreaterThanTest' }, + { type: 'testFailure', debugTxHash: '0xa534085a6bbdcf73a68bdef6a1421218c11ac0ec1af398f9445defeea31cea6e', value: 'Greater than uint int fail test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', errMsg: 'greaterThanUintIntFailTest fails', context: 'AssertGreaterThanTest', assertMethod: 'greaterThan', location: '845:71:0', expected: '2', returned: '1'}, + { type: 'testPass', debugTxHash: '0x36a7139445d76f6072fab4cc0717461068276748622c0dfc3f092d548b197de8', value: 'Greater than int uint pass test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', context: 'AssertGreaterThanTest' }, + { type: 'testFailure', debugTxHash: '0x7890f7b8f2eabae581b6f70d55d2d3bfa64ddd7753d5f892dbdf86ced96945fe', value: 'Greater than int uint fail test', filename: __dirname + '/examples_0/assert_greaterThan_test.sol', errMsg: 'greaterThanIntUintFailTest fails', context: 'AssertGreaterThanTest', assertMethod: 'greaterThan', location: '1125:76:0', expected: '115792089237316195423570985008687907853269984665640564039457584007913129639836', returned: '100'} + ], ['time', 'web3']) + }) + }) + + describe('assert library LESSERTHAN method tests', () => { + const filename: string = __dirname + '/examples_0/assert_lesserThan_test.sol' + + beforeAll((done) => { + compileAndDeploy(filename, (_err: Error | null | undefined, compilationData: object, contracts: any, asts: any, accounts: string[], web3: any) => { + runTest('AssertLesserThanTest', contracts.AssertLesserThanTest, compilationData[filename]['AssertLesserThanTest'], asts[filename], { accounts }, testCallback, resultsCallback(done)) + }) + }) + + afterAll(() => { tests = [] }) + + it('should have 4 passing test', () => { + assert.equal(results.passingNum, 4) + }) + + it('should have 4 failing test', () => { + assert.equal(results.failureNum, 4) + }) + + it('should return', () => { + deepEqualExcluding(tests, [ + { type: 'accountList', value: accounts }, + { type: 'contract', value: 'AssertLesserThanTest', filename: __dirname + '/examples_0/assert_lesserThan_test.sol' }, + { type: 'testPass', debugTxHash: '0x47875047c1fff8a7b1cc1603418960cd2a3afe8a9c59337b19fb463a85d6e47e', value: 'Lesser than uint pass test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', context: 'AssertLesserThanTest' }, + { type: 'testFailure', debugTxHash: '0xf6fd459d0b28d0d85c56dd69d953331291e1c234c8a263150252e35da0ed6671', value: 'Lesser than uint fail test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', errMsg: 'lesserThanUintFailTest fails', context: 'AssertLesserThanTest', assertMethod: 'lesserThan', location: '298:67:0', expected: '2', returned: '4'}, + { type: 'testPass', debugTxHash: '0x761d95111764af396634474899ff1db218d5e514d6de6bc3260af15b1f5b929f', value: 'Lesser than int pass test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', context: 'AssertLesserThanTest' }, + { type: 'testFailure', debugTxHash: '0xc17697ef2df92c22707639caa9355bdf0d98dbb18157e72b8b257bb0eb2beb4e', value: 'Lesser than int fail test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', errMsg: 'lesserThanIntFailTest fails', context: 'AssertLesserThanTest', assertMethod: 'lesserThan', location: '557:65:0', expected: '-1', returned: '1'}, + { type: 'testPass', debugTxHash: '0xf0721b28c547c1c64948661d677cf6afc10d139315726280162a984f2f7e5d9c', value: 'Lesser than uint int pass test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', context: 'AssertLesserThanTest' }, + { type: 'testFailure', debugTxHash: '0x0757289229b58043c101cb311df8db16d1b30944747493e1491daa9aca6aa30e', value: 'Lesser than uint int fail test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', errMsg: 'lesserThanUintIntFailTest fails', context: 'AssertLesserThanTest', assertMethod: 'lesserThan', location: '826:71:0', expected: '-1', returned: '115792089237316195423570985008687907853269984665640564039457584007913129639935'}, + { type: 'testPass', debugTxHash: '0x316feb8f80c04b12194262dd80b6b004232eab00d7f7c3846badf6e92684e177', value: 'Lesser than int uint pass test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', context: 'AssertLesserThanTest' }, + { type: 'testFailure', debugTxHash: '0x65c5ab3cb85f163eefe3321cc4444defa99154d3cbe415b9384bbd2627449b6a', value: 'Lesser than int uint fail test', filename: __dirname + '/examples_0/assert_lesserThan_test.sol', errMsg: 'lesserThanIntUintFailTest fails', context: 'AssertLesserThanTest', assertMethod: 'lesserThan', location: '1105:69:0', expected: '1', returned: '1'}, + ], ['time', 'web3']) + }) + }) + + describe('test with beforeAll', () => { + const filename: string = __dirname + '/examples_1/simple_storage_test.sol' + + beforeAll((done) => { + compileAndDeploy(filename, (_err: Error | null | undefined, compilationData: object, contracts: any, asts: any, accounts: string[], web3: any) => { + runTest('MyTest', contracts.MyTest, compilationData[filename]['MyTest'], asts[filename], { accounts }, testCallback, resultsCallback(done)) + }) + }) + + afterAll(() => { tests = [] }) + + it('should have 3 passing test', () => { + assert.equal(results.passingNum, 3) + }) + + it('should have 1 failing test', () => { + assert.equal(results.failureNum, 1) + }) + + it('should return 6 messages', () => { + deepEqualExcluding(tests, [ + { type: 'accountList', value: accounts }, + { type: 'contract', value: 'MyTest', filename: __dirname + '/examples_1/simple_storage_test.sol' }, + { type: 'testPass', debugTxHash: '0x5a805403a12f0431c5dd190d31a87eb62758f09dddc0c6ee7ee6899c5e7eba71', value: 'Initial value should be100', filename: __dirname + '/examples_1/simple_storage_test.sol', context: 'MyTest' }, + { type: 'testPass', debugTxHash: '0xd0ae7cb5a3a0f5e8f7bf90129e3daba36f649a5c1176ad54609f7b7615bef7dd', value: 'Initial value should not be200', filename: __dirname + '/examples_1/simple_storage_test.sol', context: 'MyTest' }, + { type: 'testFailure', debugTxHash: '0xb0d613434f2fd7060f97d4ca8bbfd8f2deeebed83062a25044f0237bd38b3229', value: 'Should trigger one fail', filename: __dirname + '/examples_1/simple_storage_test.sol', errMsg: 'uint test 1 fails', context: 'MyTest', assertMethod: 'equal', location: '532:51:1', expected: '2', returned: '1'}, + { type: 'testPass', debugTxHash: '0x5ee675ec81b550386b2fdd359ae3530e49dd3e02145e877a4a5f68753ac4e341', value: 'Should trigger one pass', filename: __dirname + '/examples_1/simple_storage_test.sol', context: 'MyTest' } + ], ['time', 'web3']) + }) + }) + + describe('test with beforeEach', () => { + const filename: string = __dirname + '/examples_2/simple_storage_test.sol' + + beforeAll(done => { + compileAndDeploy(filename, function (_err: Error | null | undefined, compilationData: object, contracts: any, asts: any, accounts: string[], web3: any) { + runTest('MyTest', contracts.MyTest, compilationData[filename]['MyTest'], asts[filename], { accounts }, testCallback, resultsCallback(done)) + }) + }) + + afterAll(() => { tests = [] }) + + it('should have 2 passing tests', () => { + assert.equal(results.passingNum, 2) + }) + + it('should 0 failing tests', () => { + assert.equal(results.failureNum, 0) + }) + + it('should return 4 messages', () => { + deepEqualExcluding(tests, [ + { type: 'accountList', value: accounts }, + { type: 'contract', value: 'MyTest', filename: __dirname + '/examples_2/simple_storage_test.sol' }, + { type: 'testPass', debugTxHash: '0xa700d29204f1ddb40ef66f151c44387f905d405b6da10380111a751451af2fe1', value: 'Initial value should be100', filename: __dirname + '/examples_2/simple_storage_test.sol', context: 'MyTest' }, + { type: 'testPass', debugTxHash: '0x2c037b78a435e5964615f838ea65f077f3b15d8552d514b3551d0fb87419e444', value: 'Value is set200', filename: __dirname + '/examples_2/simple_storage_test.sol', context: 'MyTest' } + ], ['time', 'web3']) + }) + }) + + // Test string equality + describe('test string equality', () => { + const filename: string = __dirname + '/examples_3/simple_string_test.sol' + + beforeAll(done => { + compileAndDeploy(filename, function (_err: Error | null | undefined, compilationData: object, contracts: any, asts: any, accounts: string[], web3: any) { + runTest('StringTest', contracts.StringTest, compilationData[filename]['StringTest'], asts[filename], { accounts }, testCallback, resultsCallback(done)) + }) + }) + + afterAll(() => { tests = [] }) + + it('should 2 passing tests', () => { + assert.equal(results.passingNum, 2) + }) + + it('should return 4 messages', () => { + deepEqualExcluding(tests, [ + { type: 'accountList', value: accounts }, + { type: 'contract', value: 'StringTest', filename: __dirname + '/examples_3/simple_string_test.sol' }, + { type: 'testPass', debugTxHash: '0x4e160dbc81f88d3d87b39d81651c42b0ea8e3aaa10c1a57394467e073bbcb2a4', value: 'Initial value should be hello world', filename: __dirname + '/examples_3/simple_string_test.sol', context: 'StringTest' }, + { type: 'testPass', debugTxHash: '0x47030578c5bcb990d837356430697d061a02813e3322fa3323f6b5f78176eea6', value: 'Value should not be hello wordl', filename: __dirname + '/examples_3/simple_string_test.sol', context: 'StringTest' } + ], ['time', 'web3']) + }) + }) + + // Test multiple directory import in test contract + describe('test multiple directory import in test contract', () => { + const filename: string = __dirname + '/examples_5/test/simple_storage_test.sol' + + beforeAll(done => { + compileAndDeploy(filename, function (_err: Error | null | undefined, compilationData: object, contracts: any, asts: any, accounts: string[], web3: any) { + runTest('StorageResolveTest', contracts.StorageResolveTest, compilationData[filename]['StorageResolveTest'], asts[filename], { accounts }, testCallback, resultsCallback(done)) + }) + }) + + afterAll(() => { tests = [] }) + + it('should 3 passing tests', () => { + assert.equal(results.passingNum, 3) + }) + + it('should return 4 messages', () => { + deepEqualExcluding(tests, [ + { type: 'accountList', value: accounts }, + { type: 'contract', value: 'StorageResolveTest', filename: __dirname + '/examples_5/test/simple_storage_test.sol' }, + { type: 'testPass', debugTxHash: '0x85e901e9160c4a17725d020f030c7cbb020d36da1fda8422d990391df3cbfcbb', value: 'Initial value should be100', filename: __dirname + '/examples_5/test/simple_storage_test.sol', context: 'StorageResolveTest' }, + { type: 'testPass', debugTxHash: '0x1abb2456746b416cddcaf2f3fe960103e740e9772c47a0f1d65d48394facb21a', value: 'Check if even', filename: __dirname + '/examples_5/test/simple_storage_test.sol', context: 'StorageResolveTest' }, + { type: 'testPass', debugTxHash: '0xfcc2332a24d2780390e85a06343fab81c4dc20c12cf5455d746641a9c3e8db03', value: 'Check if odd', filename: __dirname + '/examples_5/test/simple_storage_test.sol', context: 'StorageResolveTest' } + ], ['time', 'web3']) + }) + }) + + //Test SafeMath library methods + describe('test SafeMath library', () => { + const filename: string = __dirname + '/examples_4/SafeMath_test.sol' + + beforeAll(done => { + compileAndDeploy(filename, function (_err: Error | null | undefined, compilationData: object, contracts: any, asts: any, accounts: string[], web3: any) { + runTest('SafeMathTest', contracts.SafeMathTest, compilationData[filename]['SafeMathTest'], asts[filename], { accounts }, testCallback, resultsCallback(done)) + }) + }) + + afterAll(() => { tests = [] }) + + it('should have 10 passing tests', () => { + assert.equal(results.passingNum, 10) + }) + it('should have 0 failing tests', () => { + assert.equal(results.failureNum, 0) + }) + }) + + //Test signed/unsigned integer weight + describe('test number weight', () => { + const filename: string = __dirname + '/number/number_test.sol' + + beforeAll(done => { + compileAndDeploy(filename, function (_err: Error | null | undefined, compilationData: object, contracts: any, asts: any, accounts: string[], web3: any) { + runTest('IntegerTest', contracts.IntegerTest, compilationData[filename]['IntegerTest'], asts[filename], { accounts }, testCallback, resultsCallback(done)) + }) + }) + + afterAll(() => { tests = [] }) + + it('should have 6 passing tests', () => { + assert.equal(results.passingNum, 6) + }) + it('should have 2 failing tests', () => { + assert.equal(results.failureNum, 2) + }) + }) + + // Test Transaction with custom sender & value + describe('various sender', () => { + const filename: string = __dirname + '/various_sender/sender_and_value_test.sol' + + beforeAll(done => { + compileAndDeploy(filename, function (_err: Error | null | undefined, compilationData: object, contracts: any, asts: any, accounts: string[], web3: any) { + runTest('SenderAndValueTest', contracts.SenderAndValueTest, compilationData[filename]['SenderAndValueTest'], asts[filename], { accounts }, testCallback, resultsCallback(done)) + }) + }) + + afterAll(() => { tests = [] }) + + it('should have 17 passing tests', () => { + assert.equal(results.passingNum, 17) + }) + it('should have 0 failing tests', () => { + assert.equal(results.failureNum, 0) + }) + }) + + // Test `runTest` method without sending contract object (should throw error) + describe('runTest method without contract json interface', () => { + const filename: string = __dirname + '/various_sender/sender_and_value_test.sol' + const errorCallback: Function = (done) => { + return (err, _results) => { + if (err && err.message.includes('Contract interface not available')) { + results = _results + done() + } + else throw err + } + } + beforeAll(done => { + compileAndDeploy(filename, function (_err: Error | null | undefined, compilationData: object, contracts: any, asts: any, accounts: string[], web3: any) { + runTest('SenderAndValueTest', undefined, compilationData[filename]['SenderAndValueTest'], asts[filename], { accounts }, testCallback, errorCallback(done)) + }) + }) + + it('should have 0 passing tests', () => { + assert.equal(results.passingNum, 0) + }) + it('should have 0 failing tests', () => { + assert.equal(results.failureNum, 0) + }) + }) + }) +}) diff --git a/libs/remix-ui/app/src/lib/remix-app/components/modals/matomo.tsx b/libs/remix-ui/app/src/lib/remix-app/components/modals/matomo.tsx index 4d4daaed3e..276a3ee47b 100644 --- a/libs/remix-ui/app/src/lib/remix-app/components/modals/matomo.tsx +++ b/libs/remix-ui/app/src/lib/remix-app/components/modals/matomo.tsx @@ -1,6 +1,11 @@ import React, { useContext, useEffect, useState } from 'react' import { AppContext } from '../../context/context' import { useDialogDispatchers } from '../../context/provider' +declare global { + interface Window { + _paq: any + } +} const _paq = window._paq = window._paq || [] const MatomoDialog = (props) => { diff --git a/libs/remix-ui/debugger-ui/src/lib/debugger-ui.tsx b/libs/remix-ui/debugger-ui/src/lib/debugger-ui.tsx index f7135f74bb..3233461935 100644 --- a/libs/remix-ui/debugger-ui/src/lib/debugger-ui.tsx +++ b/libs/remix-ui/debugger-ui/src/lib/debugger-ui.tsx @@ -121,7 +121,7 @@ export const DebuggerUI = (props: DebuggerUIProps) => { }) }) - debuggerInstance.event.register('newSourceLocation', async (lineColumnPos, rawLocation, generatedSources, address) => { + debuggerInstance.event.register('newSourceLocation', async (lineColumnPos, rawLocation, generatedSources, address, stepDetail, lineGasCost) => { if (!lineColumnPos) { await debuggerModule.discardHighlight() setState(prevState => { @@ -158,7 +158,7 @@ export const DebuggerUI = (props: DebuggerUIProps) => { return { ...prevState, sourceLocationStatus: '' } }) await debuggerModule.discardHighlight() - await debuggerModule.highlight(lineColumnPos, path) + await debuggerModule.highlight(lineColumnPos, path, rawLocation, stepDetail, lineGasCost) } } }) @@ -266,13 +266,14 @@ export const DebuggerUI = (props: DebuggerUIProps) => { console.log(e.message) } + const localCache = {} const debuggerInstance = new Debugger({ web3, offsetToLineColumnConverter: debuggerModule.offsetToLineColumnConverter, compilationResult: async (address) => { try { - const ret = await debuggerModule.fetchContractAndCompile(address, currentReceipt) - return ret + if (!localCache[address]) localCache[address] = await debuggerModule.fetchContractAndCompile(address, currentReceipt) + return localCache[address] } catch (e) { // debuggerModule.showMessage('Debugging error', 'Unable to fetch a transaction.') console.error(e) @@ -395,8 +396,8 @@ export const DebuggerUI = (props: DebuggerUIProps) => { { state.debugging && }
- { state.debugging && } - { state.debugging && } + { state.debugging && } + { state.debugging && }
) diff --git a/libs/remix-ui/debugger-ui/src/lib/idebugger-api.ts b/libs/remix-ui/debugger-ui/src/lib/idebugger-api.ts index 3b3fa55d11..1b0d19c3d4 100644 --- a/libs/remix-ui/debugger-ui/src/lib/idebugger-api.ts +++ b/libs/remix-ui/debugger-ui/src/lib/idebugger-api.ts @@ -44,7 +44,7 @@ export interface IDebuggerApi { onEditorContentChanged: (listener: onEditorContentChanged) => void onEnvChanged: (listener: onEnvChangedListener) => void discardHighlight: () => Promise - highlight: (lineColumnPos: LineColumnLocation, path: string) => Promise + highlight: (lineColumnPos: LineColumnLocation, path: string, rawLocation: any, stepDetail: any, highlight: any) => Promise fetchContractAndCompile: (address: string, currentReceipt: TransactionReceipt) => Promise getFile: (path: string) => Promise setFile: (path: string, content: string) => Promise diff --git a/libs/remix-ui/debugger-ui/src/lib/vm-debugger/assembly-items.tsx b/libs/remix-ui/debugger-ui/src/lib/vm-debugger/assembly-items.tsx index 815681529b..b5fb288c48 100644 --- a/libs/remix-ui/debugger-ui/src/lib/vm-debugger/assembly-items.tsx +++ b/libs/remix-ui/debugger-ui/src/lib/vm-debugger/assembly-items.tsx @@ -1,3 +1,4 @@ +import { stateDecoder } from 'dist/libs/remix-debug/src/solidity-decoder' import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line import { initialState, reducer } from '../../reducers/assembly-items' import './styles/assembly-items.css' @@ -9,6 +10,7 @@ export const AssemblyItems = ({ registerEvent }) => { const [nextSelectedItems, setNextSelectedItems] = useState([1]) const [returnInstructionIndexes, setReturnInstructionIndexes] = useState([]) const [outOfGasInstructionIndexes, setOutOfGasInstructionIndexes] = useState([]) + const [opcodeTooltipText, setOpcodeTooltipText] = useState('') const refs = useRef({}) const asmItemsRef = useRef(null) @@ -16,6 +18,10 @@ export const AssemblyItems = ({ registerEvent }) => { registerEvent && registerEvent('codeManagerChanged', (code, address, index, nextIndexes, returnInstructionIndexes, outOfGasInstructionIndexes) => { dispatch({ type: 'FETCH_OPCODES_SUCCESS', payload: { code, address, index, nextIndexes, returnInstructionIndexes, outOfGasInstructionIndexes } }) }) + + registerEvent && registerEvent('lineGasCostChanged', (instructionsIndexes: number[], line: []) => { + dispatch({ type: 'FETCH_INDEXES_FOR_NEW_LINE', payload: { currentLineIndexes: instructionsIndexes || [], line } }) + }) }, []) useEffect(() => { @@ -129,7 +135,9 @@ export const AssemblyItems = ({ registerEvent }) => {
{ assemblyItems.display.map((item, i) => { - return
{ refs.current[i] = ref }}>{item}
+ return
{ refs.current[i] = ref }}> + {item}{assemblyItems.currentLineIndexes.includes(i) ? - LINE {assemblyItems.line + 1} : ' - '} +
}) }
diff --git a/libs/remix-ui/debugger-ui/src/lib/vm-debugger/vm-debugger-head.tsx b/libs/remix-ui/debugger-ui/src/lib/vm-debugger/vm-debugger-head.tsx index 4402e7a57c..669a5cf89c 100644 --- a/libs/remix-ui/debugger-ui/src/lib/vm-debugger/vm-debugger-head.tsx +++ b/libs/remix-ui/debugger-ui/src/lib/vm-debugger/vm-debugger-head.tsx @@ -5,7 +5,7 @@ import StepDetail from './step-detail' // eslint-disable-line import SolidityState from './solidity-state' // eslint-disable-line import SolidityLocals from './solidity-locals' // eslint-disable-line -export const VmDebuggerHead = ({ vmDebugger: { registerEvent, triggerEvent } }) => { +export const VmDebuggerHead = ({ vmDebugger: { registerEvent, triggerEvent }, debugging }) => { const [functionPanel, setFunctionPanel] = useState(null) const [stepDetail, setStepDetail] = useState({ 'vm trace step': '-', @@ -31,7 +31,7 @@ export const VmDebuggerHead = ({ vmDebugger: { registerEvent, triggerEvent } }) const functions = [] for (const func of stack) { - functions.push(func.functionDefinition.name + '(' + func.inputs.join(', ') + ')') + functions.push((func.functionDefinition.name || func.functionDefinition.kind) + '(' + func.inputs.join(', ') + ')' + ' - ' + func.gasCost + ' gas') } setFunctionPanel(() => functions) }) @@ -95,7 +95,7 @@ export const VmDebuggerHead = ({ vmDebugger: { registerEvent, triggerEvent } }) return { ...solidityLocals, message } }) }) - }, [registerEvent]) + }, [debugging]) return (
diff --git a/libs/remix-ui/debugger-ui/src/lib/vm-debugger/vm-debugger.tsx b/libs/remix-ui/debugger-ui/src/lib/vm-debugger/vm-debugger.tsx index 8bf8307f2a..508097d5df 100644 --- a/libs/remix-ui/debugger-ui/src/lib/vm-debugger/vm-debugger.tsx +++ b/libs/remix-ui/debugger-ui/src/lib/vm-debugger/vm-debugger.tsx @@ -8,7 +8,7 @@ import ReturnValuesPanel from './dropdown-panel' // eslint-disable-line import FullStoragesChangesPanel from './full-storages-changes' // eslint-disable-line import GlobalVariables from './global-variables' // eslint-disable-line -export const VmDebugger = ({ vmDebugger: { registerEvent }, currentBlock, currentReceipt, currentTransaction }) => { +export const VmDebugger = ({ vmDebugger: { registerEvent }, currentBlock, currentReceipt, currentTransaction, debugging }) => { const [calldataPanel, setCalldataPanel] = useState(null) const [memoryPanel, setMemoryPanel] = useState(null) const [callStackPanel, setCallStackPanel] = useState(null) @@ -49,7 +49,7 @@ export const VmDebugger = ({ vmDebugger: { registerEvent }, currentBlock, curren registerEvent && registerEvent('traceStorageUpdate', (calldata) => { setFullStoragesChangesPanel(() => calldata) }) - }, [registerEvent]) + }, [debugging]) return (
diff --git a/libs/remix-ui/debugger-ui/src/reducers/assembly-items.ts b/libs/remix-ui/debugger-ui/src/reducers/assembly-items.ts index a86554f297..4f1e9c307a 100644 --- a/libs/remix-ui/debugger-ui/src/reducers/assembly-items.ts +++ b/libs/remix-ui/debugger-ui/src/reducers/assembly-items.ts @@ -13,6 +13,7 @@ export const initialState = { }, display: [], index: 0, + initialIndex: 0, nextIndexes: [-1], returnInstructionIndexes: [], outOfGasInstructionIndexes: [], @@ -20,7 +21,10 @@ export const initialState = { bottom: 0, isRequesting: false, isSuccessful: false, - hasError: null + hasError: null, + absoluteCurrentLineIndexes: [], + currentLineIndexes: [], + line: -1 } const reducedOpcode = (opCodes, payload) => { @@ -31,6 +35,7 @@ const reducedOpcode = (opCodes, payload) => { return { index: opCodes.index - bottom, nextIndexes: opCodes.nextIndexes.map(index => index - bottom), + currentLineIndexes: (opCodes.absoluteCurrentLineIndexes && opCodes.absoluteCurrentLineIndexes.map(index => index - bottom)) || [], display: opCodes.code.slice(bottom, top), returnInstructionIndexes: payload.returnInstructionIndexes.map((index) => index.instructionIndex - bottom), outOfGasInstructionIndexes: payload.outOfGasInstructionIndexes.map((index) => index.instructionIndex - bottom) @@ -49,20 +54,23 @@ export const reducer = (state = initialState, action: Action) => { } case 'FETCH_OPCODES_SUCCESS': { const opCodes = action.payload.address === state.opCodes.address ? { - ...state.opCodes, index: action.payload.index, nextIndexes: action.payload.nextIndexes + ...state.opCodes, index: action.payload.index, nextIndexes: action.payload.nextIndexes, absoluteCurrentLineIndexes: state.absoluteCurrentLineIndexes } : deepEqual(action.payload.code, state.opCodes.code) ? state.opCodes : action.payload const reduced = reducedOpcode(opCodes, action.payload) return { + ...state, opCodes, display: reduced.display, + initialIndex: action.payload.index, index: reduced.index, nextIndexes: reduced.nextIndexes, isRequesting: false, isSuccessful: true, hasError: null, returnInstructionIndexes: reduced.returnInstructionIndexes, - outOfGasInstructionIndexes: reduced.outOfGasInstructionIndexes + outOfGasInstructionIndexes: reduced.outOfGasInstructionIndexes, + currentLineIndexes: reduced.currentLineIndexes } } case 'FETCH_OPCODES_ERROR': { @@ -73,6 +81,16 @@ export const reducer = (state = initialState, action: Action) => { hasError: action.payload } } + case 'FETCH_INDEXES_FOR_NEW_LINE': { + let bottom = state.initialIndex - 10 + bottom = bottom < 0 ? 0 : bottom + return { + ...state, + absoluteCurrentLineIndexes: action.payload.currentLineIndexes, + currentLineIndexes: action.payload.currentLineIndexes.map(index => index - bottom), + line: action.payload.line + } + } default: throw new Error() } diff --git a/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx b/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx index 9adc0a64c5..7c02ed4308 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx +++ b/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx @@ -5,6 +5,7 @@ import { CompilerContainerProps } from './types' import { ConfigurationSettings } from '@remix-project/remix-lib-ts' import { checkSpecialChars, CustomTooltip, extractNameFromKey } from '@remix-ui/helper' import { canUseWorker, baseURLBin, baseURLWasm, urlFromVersion, pathToURL, promisedMiniXhr } from '@remix-project/remix-solidity' + import { compilerReducer, compilerInitialState } from './reducers/compiler' import { resetEditorMode, listenToEvents } from './actions/compiler' import { getValidLanguage } from '@remix-project/remix-solidity' @@ -732,6 +733,7 @@ export const CompilerContainer = (props: CompilerContainerProps) => { setToggleExpander(!toggleExpander) } + return (
@@ -944,20 +946,26 @@ export const CompilerContainer = (props: CompilerContainerProps) => { {(configFilePath === '' && state.useFileConfiguration) &&
No config file selected
}
} > - - { } - - {typeof state.compiledFileName === 'string' - ? extractNameFromKey(state.compiledFileName) || - `<${intl.formatMessage({ - id: 'solidity.noFileSelected', - defaultMessage: 'no file selected', - })}>` - : `<${intl.formatMessage({ - id: 'solidity.noFileSelected', - defaultMessage: 'no file selected', - })}>`} - +
+ { } +
+ + + + + {typeof state.compiledFileName === 'string' + ? extractNameFromKey(state.compiledFileName) || + `<${intl.formatMessage({ + id: 'solidity.noFileSelected', + defaultMessage: 'no file selected', + })}>` + : `<${intl.formatMessage({ + id: 'solidity.noFileSelected', + defaultMessage: 'no file selected', + })}>`} + +
+
diff --git a/libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx b/libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx index d92383a02b..c84beaf3ad 100644 --- a/libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx +++ b/libs/remix-ui/solidity-unit-testing/src/lib/solidity-unit-testing.tsx @@ -259,7 +259,7 @@ export const SolidityUnitTesting = (props: Record) => { // eslint-d finalLogs = finalLogs + ' ' + formattedLog + '\n' } _paq.push(['trackEvent', 'solidityUnitTesting', 'hardhat', 'console.log']) - testTab.call('terminal', 'log', { type: 'log', value: finalLogs }) + testTab.call('terminal', 'logHtml', { type: 'log', value: finalLogs }) } const discardHighlight = async () => { diff --git a/libs/remix-url-resolver/package.json b/libs/remix-url-resolver/package.json index 7981f78c80..5acac15755 100644 --- a/libs/remix-url-resolver/package.json +++ b/libs/remix-url-resolver/package.json @@ -1,6 +1,6 @@ { "name": "@remix-project/remix-url-resolver", - "version": "0.0.40", + "version": "0.0.41", "description": "Solidity import url resolver engine", "main": "src/index.js", "types": "src/index.d.ts", @@ -41,5 +41,5 @@ "typescript": "^3.1.6" }, "typings": "src/index.d.ts", - "gitHead": "2cec195f5a3678b17745155536a1714d9b1a5694" + "gitHead": "569f7d53f6f218d99aa8281929b541ab7e00058f" } \ No newline at end of file diff --git a/libs/remix-ws-templates/package.json b/libs/remix-ws-templates/package.json index b6ee54b787..bb3e274521 100644 --- a/libs/remix-ws-templates/package.json +++ b/libs/remix-ws-templates/package.json @@ -1,6 +1,6 @@ { "name": "@remix-project/remix-ws-templates", - "version": "1.0.6", + "version": "1.0.7", "description": "Create a Remix IDE workspace using different templates", "main": "src/index.js", "types": "src/index.d.ts", @@ -27,5 +27,5 @@ "ethers": "^5.4.2", "web3": "^1.5.1" }, - "gitHead": "2cec195f5a3678b17745155536a1714d9b1a5694" + "gitHead": "569f7d53f6f218d99aa8281929b541ab7e00058f" } \ No newline at end of file