diff options
author | Fabio Berger <me@fabioberger.com> | 2018-06-13 22:10:05 +0800 |
---|---|---|
committer | Fabio Berger <me@fabioberger.com> | 2018-06-13 22:10:05 +0800 |
commit | b4fead9606ecc9655b2038fcef1c75217752f67d (patch) | |
tree | b363eee11085850328837004b41ff23c5d31efb6 /packages/sol-cov | |
parent | 61243b418e4d962cd8d8a1d7a49f04510b3c1c7f (diff) | |
parent | 4efd28c092e74b438d0397069c0c55cc90c537f2 (diff) | |
download | dexon-sol-tools-b4fead9606ecc9655b2038fcef1c75217752f67d.tar dexon-sol-tools-b4fead9606ecc9655b2038fcef1c75217752f67d.tar.gz dexon-sol-tools-b4fead9606ecc9655b2038fcef1c75217752f67d.tar.bz2 dexon-sol-tools-b4fead9606ecc9655b2038fcef1c75217752f67d.tar.lz dexon-sol-tools-b4fead9606ecc9655b2038fcef1c75217752f67d.tar.xz dexon-sol-tools-b4fead9606ecc9655b2038fcef1c75217752f67d.tar.zst dexon-sol-tools-b4fead9606ecc9655b2038fcef1c75217752f67d.zip |
Merge branch 'v2-prototype' into feature/combinatorial-testing
* v2-prototype: (26 commits)
Rename _coverageCollector -> _profilerCollector in TraceCollectionSubprovider
Refactor sol-cov to de-duplicate code for coverage and profiling
Rename popByte and popAddress
Hard code test addresses/bytes32 instead of generating pseudorandom ones
Update artifacts
Rename computeCoverageAsync -> computeSingleTraceCoverageAsync
Fix linter errors
Refactor sol-cov to avoid keeping traceInfo in memory
Unpop byte rather than making deep copy
Pass gas in to marketBuyOrdersNoThrow
Looks up the memory location of makerAssetData/takerAssetData
Make ZRX_PROXY_ID constant rather than popping it from ZRX_ASSET_DATA
Add tests for deepCopyBytes and missing write methods from LibBytes
Pop id from assetData before dispatching to AssetProxies
Upgrade solidity-parser-entlr 0.2.11 => 0.2.12
Fix import order
Fix typos
Add CHANGELOGs
Speed-up sol-cov
Increase delay when sending transactions during devnet startup
...
Diffstat (limited to 'packages/sol-cov')
-rw-r--r-- | packages/sol-cov/CHANGELOG.json | 12 | ||||
-rw-r--r-- | packages/sol-cov/package.json | 2 | ||||
-rw-r--r-- | packages/sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter.ts | 10 | ||||
-rw-r--r-- | packages/sol-cov/src/coverage_manager.ts | 178 | ||||
-rw-r--r-- | packages/sol-cov/src/coverage_subprovider.ts | 116 | ||||
-rw-r--r-- | packages/sol-cov/src/profiler_manager.ts | 134 | ||||
-rw-r--r-- | packages/sol-cov/src/profiler_subprovider.ts | 67 | ||||
-rw-r--r-- | packages/sol-cov/src/trace_collection_subprovider.ts | 22 | ||||
-rw-r--r-- | packages/sol-cov/src/trace_collector.ts | 93 |
9 files changed, 293 insertions, 341 deletions
diff --git a/packages/sol-cov/CHANGELOG.json b/packages/sol-cov/CHANGELOG.json index 36f8e1a7d..7e934ad6e 100644 --- a/packages/sol-cov/CHANGELOG.json +++ b/packages/sol-cov/CHANGELOG.json @@ -37,6 +37,18 @@ { "note": "Skip interface artifacts with a warning instead of failing", "pr": 675 + }, + { + "note": "Fix solcVersion regex in parameter validation", + "pr": 690 + }, + { + "note": "Fix a bug when in TruffleArtifactsAdapter causing it to throw if compiler.json is not there", + "pr": 690 + }, + { + "note": "HUGE perf improvements", + "pr": 690 } ] }, diff --git a/packages/sol-cov/package.json b/packages/sol-cov/package.json index 02235915a..1d9d7f94d 100644 --- a/packages/sol-cov/package.json +++ b/packages/sol-cov/package.json @@ -65,7 +65,7 @@ "mkdirp": "^0.5.1", "rimraf": "^2.6.2", "semaphore-async-await": "^1.5.1", - "solidity-parser-antlr": "^0.2.11" + "solidity-parser-antlr": "^0.2.12" }, "devDependencies": { "@0xproject/monorepo-scripts": "^0.1.20", diff --git a/packages/sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter.ts b/packages/sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter.ts index 051782ba4..220a9f98c 100644 --- a/packages/sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter.ts +++ b/packages/sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter.ts @@ -1,4 +1,4 @@ -import { ContractArtifact } from '@0xproject/sol-compiler'; +import { CompilerOptions, ContractArtifact } from '@0xproject/sol-compiler'; import { logUtils } from '@0xproject/utils'; import * as fs from 'fs'; import * as glob from 'glob'; @@ -16,15 +16,17 @@ export class SolCompilerArtifactAdapter extends AbstractArtifactAdapter { private _sourcesPath: string; constructor(artifactsPath?: string, sourcesPath?: string) { super(); - const config = JSON.parse(fs.readFileSync(CONFIG_FILE).toString()); + const config: CompilerOptions = fs.existsSync(CONFIG_FILE) + ? JSON.parse(fs.readFileSync(CONFIG_FILE).toString()) + : {}; if (_.isUndefined(artifactsPath) && _.isUndefined(config.artifactsDir)) { throw new Error(`artifactsDir not found in ${CONFIG_FILE}`); } - this._artifactsPath = artifactsPath || config.artifactsDir; + this._artifactsPath = (artifactsPath || config.artifactsDir) as string; if (_.isUndefined(sourcesPath) && _.isUndefined(config.contractsDir)) { throw new Error(`contractsDir not found in ${CONFIG_FILE}`); } - this._sourcesPath = sourcesPath || config.contractsDir; + this._sourcesPath = (sourcesPath || config.contractsDir) as string; } public async collectContractsDataAsync(): Promise<ContractData[]> { const artifactsGlob = `${this._artifactsPath}/**/*.json`; diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts deleted file mode 100644 index 3ab363b52..000000000 --- a/packages/sol-cov/src/coverage_manager.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { promisify } from '@0xproject/utils'; -import { stripHexPrefix } from 'ethereumjs-util'; -import * as fs from 'fs'; -import { Collector } from 'istanbul'; -import * as _ from 'lodash'; -import { getLogger, levels, Logger } from 'loglevel'; -import * as mkdirp from 'mkdirp'; - -import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter'; -import { collectCoverageEntries } from './collect_coverage_entries'; -import { constants } from './constants'; -import { parseSourceMap } from './source_maps'; -import { - BranchCoverage, - BranchDescription, - ContractData, - Coverage, - FunctionCoverage, - FunctionDescription, - SingleFileSourceRange, - SourceRange, - StatementCoverage, - StatementDescription, - Subtrace, - TraceInfo, - TraceInfoExistingContract, - TraceInfoNewContract, -} from './types'; -import { utils } from './utils'; - -const mkdirpAsync = promisify<undefined>(mkdirp); - -/** - * CoverageManager is used by CoverageSubprovider to compute code coverage based on collected trace data. - */ -export class CoverageManager { - private _artifactAdapter: AbstractArtifactAdapter; - private _logger: Logger; - private _traceInfos: TraceInfo[] = []; - /** - * Computed partial coverage for a single file & subtrace - * @param contractData Contract metadata (source, srcMap, bytecode) - * @param subtrace A subset of a transcation/call trace that was executed within that contract - * @param pcToSourceRange A mapping from program counters to source ranges - * @param fileIndex Index of a file to compute coverage for - * @return Partial istanbul coverage for that file & subtrace - */ - private static _getSingleFileCoverageForSubtrace( - contractData: ContractData, - subtrace: Subtrace, - pcToSourceRange: { [programCounter: number]: SourceRange }, - fileIndex: number, - ): Coverage { - const absoluteFileName = contractData.sources[fileIndex]; - const coverageEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex]); - let sourceRanges = _.map(subtrace, structLog => pcToSourceRange[structLog.pc]); - sourceRanges = _.compact(sourceRanges); // Some PC's don't map to a source range and we just ignore them. - // By default lodash does a shallow object comparasion. We JSON.stringify them and compare as strings. - sourceRanges = _.uniqBy(sourceRanges, s => JSON.stringify(s)); // We don't care if one PC was covered multiple times within a single transaction - sourceRanges = _.filter(sourceRanges, sourceRange => sourceRange.fileName === absoluteFileName); - const branchCoverage: BranchCoverage = {}; - const branchIds = _.keys(coverageEntriesDescription.branchMap); - for (const branchId of branchIds) { - const branchDescription = coverageEntriesDescription.branchMap[branchId]; - const isBranchCoveredByBranchIndex = _.map(branchDescription.locations, location => { - const isBranchCovered = _.some(sourceRanges, range => utils.isRangeInside(range.location, location)); - const timesBranchCovered = Number(isBranchCovered); - return timesBranchCovered; - }); - branchCoverage[branchId] = isBranchCoveredByBranchIndex; - } - const statementCoverage: StatementCoverage = {}; - const statementIds = _.keys(coverageEntriesDescription.statementMap); - for (const statementId of statementIds) { - const statementDescription = coverageEntriesDescription.statementMap[statementId]; - const isStatementCovered = _.some(sourceRanges, range => - utils.isRangeInside(range.location, statementDescription), - ); - const timesStatementCovered = Number(isStatementCovered); - statementCoverage[statementId] = timesStatementCovered; - } - const functionCoverage: FunctionCoverage = {}; - const functionIds = _.keys(coverageEntriesDescription.fnMap); - for (const fnId of functionIds) { - const functionDescription = coverageEntriesDescription.fnMap[fnId]; - const isFunctionCovered = _.some(sourceRanges, range => - utils.isRangeInside(range.location, functionDescription.loc), - ); - const timesFunctionCovered = Number(isFunctionCovered); - functionCoverage[fnId] = timesFunctionCovered; - } - // HACK: Solidity doesn't emit any opcodes that map back to modifiers with no args, that's why we map back to the - // function range and check if there is any covered statement within that range. - for (const modifierStatementId of coverageEntriesDescription.modifiersStatementIds) { - if (statementCoverage[modifierStatementId]) { - // Already detected as covered - continue; - } - const modifierDescription = coverageEntriesDescription.statementMap[modifierStatementId]; - const enclosingFunction = _.find(coverageEntriesDescription.fnMap, functionDescription => - utils.isRangeInside(modifierDescription, functionDescription.loc), - ) as FunctionDescription; - const isModifierCovered = _.some( - coverageEntriesDescription.statementMap, - (statementDescription: StatementDescription, statementId: number) => { - const isInsideTheModifierEnclosingFunction = utils.isRangeInside( - statementDescription, - enclosingFunction.loc, - ); - const isCovered = statementCoverage[statementId]; - return isInsideTheModifierEnclosingFunction && isCovered; - }, - ); - const timesModifierCovered = Number(isModifierCovered); - statementCoverage[modifierStatementId] = timesModifierCovered; - } - const partialCoverage = { - [absoluteFileName]: { - ...coverageEntriesDescription, - path: absoluteFileName, - f: functionCoverage, - s: statementCoverage, - b: branchCoverage, - }, - }; - return partialCoverage; - } - constructor(artifactAdapter: AbstractArtifactAdapter, isVerbose: boolean) { - this._artifactAdapter = artifactAdapter; - this._logger = getLogger('sol-cov'); - this._logger.setLevel(isVerbose ? levels.TRACE : levels.ERROR); - } - public appendTraceInfo(traceInfo: TraceInfo): void { - this._traceInfos.push(traceInfo); - } - public async writeCoverageAsync(): Promise<void> { - const finalCoverage = await this._computeCoverageAsync(); - const stringifiedCoverage = JSON.stringify(finalCoverage, null, '\t'); - await mkdirpAsync('coverage'); - fs.writeFileSync('coverage/coverage.json', stringifiedCoverage); - } - private async _computeCoverageAsync(): Promise<Coverage> { - const contractsData = await this._artifactAdapter.collectContractsDataAsync(); - const collector = new Collector(); - for (const traceInfo of this._traceInfos) { - const isContractCreation = traceInfo.address === constants.NEW_CONTRACT; - const bytecode = isContractCreation - ? (traceInfo as TraceInfoNewContract).bytecode - : (traceInfo as TraceInfoExistingContract).runtimeBytecode; - const contractData = utils.getContractDataIfExists(contractsData, bytecode); - if (_.isUndefined(contractData)) { - const errMsg = isContractCreation - ? `Unknown contract creation transaction` - : `Transaction to an unknown address: ${traceInfo.address}`; - this._logger.warn(errMsg); - continue; - } - const bytecodeHex = stripHexPrefix(bytecode); - const sourceMap = isContractCreation ? contractData.sourceMap : contractData.sourceMapRuntime; - const pcToSourceRange = parseSourceMap( - contractData.sourceCodes, - sourceMap, - bytecodeHex, - contractData.sources, - ); - for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) { - const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForSubtrace( - contractData, - traceInfo.subtrace, - pcToSourceRange, - fileIndex, - ); - collector.add(singleFileCoverageForTrace); - } - } - return collector.getFinalCoverage(); - } -} diff --git a/packages/sol-cov/src/coverage_subprovider.ts b/packages/sol-cov/src/coverage_subprovider.ts index 174b7c6ac..0fa7f873e 100644 --- a/packages/sol-cov/src/coverage_subprovider.ts +++ b/packages/sol-cov/src/coverage_subprovider.ts @@ -1,15 +1,29 @@ import * as _ from 'lodash'; import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter'; -import { CoverageManager } from './coverage_manager'; +import { collectCoverageEntries } from './collect_coverage_entries'; import { TraceCollectionSubprovider } from './trace_collection_subprovider'; +import { SingleFileSubtraceHandler, TraceCollector } from './trace_collector'; +import { + BranchCoverage, + ContractData, + Coverage, + FunctionCoverage, + FunctionDescription, + SourceRange, + StatementCoverage, + StatementDescription, + Subtrace, + TraceInfo, +} from './types'; +import { utils } from './utils'; /** * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface. * It's used to compute your code coverage while running solidity tests. */ export class CoverageSubprovider extends TraceCollectionSubprovider { - private _coverageManager: CoverageManager; + private _coverageCollector: TraceCollector; /** * Instantiates a CoverageSubprovider instance * @param artifactAdapter Adapter for used artifacts format (0x, truffle, giveth, etc.) @@ -23,14 +37,104 @@ export class CoverageSubprovider extends TraceCollectionSubprovider { shouldCollectCallTraces: true, }; super(defaultFromAddress, traceCollectionSubproviderConfig); - this._coverageManager = new CoverageManager(artifactAdapter, isVerbose); + this._coverageCollector = new TraceCollector(artifactAdapter, isVerbose, coverageHandler); + } + public async handleTraceInfoAsync(traceInfo: TraceInfo): Promise<void> { + await this._coverageCollector.computeSingleTraceCoverageAsync(traceInfo); } /** * Write the test coverage results to a file in Istanbul format. */ public async writeCoverageAsync(): Promise<void> { - const traceInfos = this.getCollectedTraceInfos(); - _.forEach(traceInfos, traceInfo => this._coverageManager.appendTraceInfo(traceInfo)); - await this._coverageManager.writeCoverageAsync(); + await this._coverageCollector.writeOutputAsync(); } } + +/** + * Computed partial coverage for a single file & subtrace. + * @param contractData Contract metadata (source, srcMap, bytecode) + * @param subtrace A subset of a transcation/call trace that was executed within that contract + * @param pcToSourceRange A mapping from program counters to source ranges + * @param fileIndex Index of a file to compute coverage for + * @return Partial istanbul coverage for that file & subtrace + */ +export const coverageHandler: SingleFileSubtraceHandler = ( + contractData: ContractData, + subtrace: Subtrace, + pcToSourceRange: { [programCounter: number]: SourceRange }, + fileIndex: number, +): Coverage => { + const absoluteFileName = contractData.sources[fileIndex]; + const coverageEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex]); + let sourceRanges = _.map(subtrace, structLog => pcToSourceRange[structLog.pc]); + sourceRanges = _.compact(sourceRanges); // Some PC's don't map to a source range and we just ignore them. + // By default lodash does a shallow object comparasion. We JSON.stringify them and compare as strings. + sourceRanges = _.uniqBy(sourceRanges, s => JSON.stringify(s)); // We don't care if one PC was covered multiple times within a single transaction + sourceRanges = _.filter(sourceRanges, sourceRange => sourceRange.fileName === absoluteFileName); + const branchCoverage: BranchCoverage = {}; + const branchIds = _.keys(coverageEntriesDescription.branchMap); + for (const branchId of branchIds) { + const branchDescription = coverageEntriesDescription.branchMap[branchId]; + const isBranchCoveredByBranchIndex = _.map(branchDescription.locations, location => { + const isBranchCovered = _.some(sourceRanges, range => utils.isRangeInside(range.location, location)); + const timesBranchCovered = Number(isBranchCovered); + return timesBranchCovered; + }); + branchCoverage[branchId] = isBranchCoveredByBranchIndex; + } + const statementCoverage: StatementCoverage = {}; + const statementIds = _.keys(coverageEntriesDescription.statementMap); + for (const statementId of statementIds) { + const statementDescription = coverageEntriesDescription.statementMap[statementId]; + const isStatementCovered = _.some(sourceRanges, range => + utils.isRangeInside(range.location, statementDescription), + ); + const timesStatementCovered = Number(isStatementCovered); + statementCoverage[statementId] = timesStatementCovered; + } + const functionCoverage: FunctionCoverage = {}; + const functionIds = _.keys(coverageEntriesDescription.fnMap); + for (const fnId of functionIds) { + const functionDescription = coverageEntriesDescription.fnMap[fnId]; + const isFunctionCovered = _.some(sourceRanges, range => + utils.isRangeInside(range.location, functionDescription.loc), + ); + const timesFunctionCovered = Number(isFunctionCovered); + functionCoverage[fnId] = timesFunctionCovered; + } + // HACK: Solidity doesn't emit any opcodes that map back to modifiers with no args, that's why we map back to the + // function range and check if there is any covered statement within that range. + for (const modifierStatementId of coverageEntriesDescription.modifiersStatementIds) { + if (statementCoverage[modifierStatementId]) { + // Already detected as covered + continue; + } + const modifierDescription = coverageEntriesDescription.statementMap[modifierStatementId]; + const enclosingFunction = _.find(coverageEntriesDescription.fnMap, functionDescription => + utils.isRangeInside(modifierDescription, functionDescription.loc), + ) as FunctionDescription; + const isModifierCovered = _.some( + coverageEntriesDescription.statementMap, + (statementDescription: StatementDescription, statementId: number) => { + const isInsideTheModifierEnclosingFunction = utils.isRangeInside( + statementDescription, + enclosingFunction.loc, + ); + const isCovered = statementCoverage[statementId]; + return isInsideTheModifierEnclosingFunction && isCovered; + }, + ); + const timesModifierCovered = Number(isModifierCovered); + statementCoverage[modifierStatementId] = timesModifierCovered; + } + const partialCoverage = { + [absoluteFileName]: { + ...coverageEntriesDescription, + path: absoluteFileName, + f: functionCoverage, + s: statementCoverage, + b: branchCoverage, + }, + }; + return partialCoverage; +}; diff --git a/packages/sol-cov/src/profiler_manager.ts b/packages/sol-cov/src/profiler_manager.ts deleted file mode 100644 index 0ab0ea544..000000000 --- a/packages/sol-cov/src/profiler_manager.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { promisify } from '@0xproject/utils'; -import { stripHexPrefix } from 'ethereumjs-util'; -import * as fs from 'fs'; -import { Collector } from 'istanbul'; -import * as _ from 'lodash'; -import { getLogger, levels, Logger } from 'loglevel'; -import * as mkdirp from 'mkdirp'; - -import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter'; -import { collectCoverageEntries } from './collect_coverage_entries'; -import { constants } from './constants'; -import { parseSourceMap } from './source_maps'; -import { - ContractData, - Coverage, - SingleFileSourceRange, - SourceRange, - Subtrace, - TraceInfo, - TraceInfoExistingContract, - TraceInfoNewContract, -} from './types'; -import { utils } from './utils'; - -const mkdirpAsync = promisify<undefined>(mkdirp); - -/** - * ProfilerManager is used by ProfilerSubprovider to profile code while running Solidity tests based on collected trace data. - * HACK: It's almost the exact copy of CoverageManager but instead of reporting how much times was each statement executed - it reports - how expensive it was gaswise. - */ -export class ProfilerManager { - private _artifactAdapter: AbstractArtifactAdapter; - private _logger: Logger; - private _traceInfos: TraceInfo[] = []; - /** - * Computed partial coverage for a single file & subtrace - * @param contractData Contract metadata (source, srcMap, bytecode) - * @param subtrace A subset of a transcation/call trace that was executed within that contract - * @param pcToSourceRange A mapping from program counters to source ranges - * @param fileIndex Index of a file to compute coverage for - * @return Partial istanbul coverage for that file & subtrace - */ - private static _getSingleFileCoverageForSubtrace( - contractData: ContractData, - subtrace: Subtrace, - pcToSourceRange: { [programCounter: number]: SourceRange }, - fileIndex: number, - ): Coverage { - const absoluteFileName = contractData.sources[fileIndex]; - const profilerEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex]); - const gasConsumedByStatement: { [statementId: string]: number } = {}; - const statementIds = _.keys(profilerEntriesDescription.statementMap); - for (const statementId of statementIds) { - const statementDescription = profilerEntriesDescription.statementMap[statementId]; - const totalGasCost = _.sum( - _.map(subtrace, structLog => { - const sourceRange = pcToSourceRange[structLog.pc]; - if (_.isUndefined(sourceRange)) { - return 0; - } - if (sourceRange.fileName !== absoluteFileName) { - return 0; - } - if (utils.isRangeInside(sourceRange.location, statementDescription)) { - return structLog.gasCost; - } else { - return 0; - } - }), - ); - gasConsumedByStatement[statementId] = totalGasCost; - } - const partialProfilerOutput = { - [absoluteFileName]: { - ...profilerEntriesDescription, - path: absoluteFileName, - f: {}, // I's meaningless in profiling context - s: gasConsumedByStatement, - b: {}, // I's meaningless in profiling context - }, - }; - return partialProfilerOutput; - } - constructor(artifactAdapter: AbstractArtifactAdapter, isVerbose: boolean) { - this._artifactAdapter = artifactAdapter; - this._logger = getLogger('sol-cov'); - this._logger.setLevel(isVerbose ? levels.TRACE : levels.ERROR); - } - public appendTraceInfo(traceInfo: TraceInfo): void { - this._traceInfos.push(traceInfo); - } - public async writeProfilerOutputAsync(): Promise<void> { - const finalCoverage = await this._computeCoverageAsync(); - const stringifiedCoverage = JSON.stringify(finalCoverage, null, '\t'); - await mkdirpAsync('coverage'); - fs.writeFileSync('coverage/coverage.json', stringifiedCoverage); - } - private async _computeCoverageAsync(): Promise<Coverage> { - const contractsData = await this._artifactAdapter.collectContractsDataAsync(); - const collector = new Collector(); - for (const traceInfo of this._traceInfos) { - const isContractCreation = traceInfo.address === constants.NEW_CONTRACT; - const bytecode = isContractCreation - ? (traceInfo as TraceInfoNewContract).bytecode - : (traceInfo as TraceInfoExistingContract).runtimeBytecode; - const contractData = utils.getContractDataIfExists(contractsData, bytecode); - if (_.isUndefined(contractData)) { - const errMsg = isContractCreation - ? `Unknown contract creation transaction` - : `Transaction to an unknown address: ${traceInfo.address}`; - this._logger.warn(errMsg); - continue; - } - const bytecodeHex = stripHexPrefix(bytecode); - const sourceMap = isContractCreation ? contractData.sourceMap : contractData.sourceMapRuntime; - const pcToSourceRange = parseSourceMap( - contractData.sourceCodes, - sourceMap, - bytecodeHex, - contractData.sources, - ); - for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) { - const singleFileCoverageForTrace = ProfilerManager._getSingleFileCoverageForSubtrace( - contractData, - traceInfo.subtrace, - pcToSourceRange, - fileIndex, - ); - collector.add(singleFileCoverageForTrace); - } - } - return collector.getFinalCoverage(); - } -} diff --git a/packages/sol-cov/src/profiler_subprovider.ts b/packages/sol-cov/src/profiler_subprovider.ts index ac878c070..62ed1b472 100644 --- a/packages/sol-cov/src/profiler_subprovider.ts +++ b/packages/sol-cov/src/profiler_subprovider.ts @@ -1,15 +1,18 @@ import * as _ from 'lodash'; import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter'; -import { ProfilerManager } from './profiler_manager'; +import { collectCoverageEntries } from './collect_coverage_entries'; import { TraceCollectionSubprovider } from './trace_collection_subprovider'; +import { SingleFileSubtraceHandler, TraceCollector } from './trace_collector'; +import { ContractData, Coverage, SourceRange, Subtrace, TraceInfo } from './types'; +import { utils } from './utils'; /** * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface. * ProfilerSubprovider is used to profile Solidity code while running tests. */ export class ProfilerSubprovider extends TraceCollectionSubprovider { - private _profilerManager: ProfilerManager; + private _profilerCollector: TraceCollector; /** * Instantiates a ProfilerSubprovider instance * @param artifactAdapter Adapter for used artifacts format (0x, truffle, giveth, etc.) @@ -23,14 +26,66 @@ export class ProfilerSubprovider extends TraceCollectionSubprovider { shouldCollectCallTraces: false, }; super(defaultFromAddress, traceCollectionSubproviderConfig); - this._profilerManager = new ProfilerManager(artifactAdapter, isVerbose); + this._profilerCollector = new TraceCollector(artifactAdapter, isVerbose, profilerHandler); + } + public async handleTraceInfoAsync(traceInfo: TraceInfo): Promise<void> { + await this._profilerCollector.computeSingleTraceCoverageAsync(traceInfo); } /** * Write the test profiler results to a file in Istanbul format. */ public async writeProfilerOutputAsync(): Promise<void> { - const traceInfos = this.getCollectedTraceInfos(); - _.forEach(traceInfos, traceInfo => this._profilerManager.appendTraceInfo(traceInfo)); - await this._profilerManager.writeProfilerOutputAsync(); + await this._profilerCollector.writeOutputAsync(); } } + +/** + * Computed partial coverage for a single file & subtrace for the purposes of + * gas profiling. + * @param contractData Contract metadata (source, srcMap, bytecode) + * @param subtrace A subset of a transcation/call trace that was executed within that contract + * @param pcToSourceRange A mapping from program counters to source ranges + * @param fileIndex Index of a file to compute coverage for + * @return Partial istanbul coverage for that file & subtrace + */ +export const profilerHandler: SingleFileSubtraceHandler = ( + contractData: ContractData, + subtrace: Subtrace, + pcToSourceRange: { [programCounter: number]: SourceRange }, + fileIndex: number, +): Coverage => { + const absoluteFileName = contractData.sources[fileIndex]; + const profilerEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex]); + const gasConsumedByStatement: { [statementId: string]: number } = {}; + const statementIds = _.keys(profilerEntriesDescription.statementMap); + for (const statementId of statementIds) { + const statementDescription = profilerEntriesDescription.statementMap[statementId]; + const totalGasCost = _.sum( + _.map(subtrace, structLog => { + const sourceRange = pcToSourceRange[structLog.pc]; + if (_.isUndefined(sourceRange)) { + return 0; + } + if (sourceRange.fileName !== absoluteFileName) { + return 0; + } + if (utils.isRangeInside(sourceRange.location, statementDescription)) { + return structLog.gasCost; + } else { + return 0; + } + }), + ); + gasConsumedByStatement[statementId] = totalGasCost; + } + const partialProfilerOutput = { + [absoluteFileName]: { + ...profilerEntriesDescription, + path: absoluteFileName, + f: {}, // I's meaningless in profiling context + s: gasConsumedByStatement, + b: {}, // I's meaningless in profiling context + }, + }; + return partialProfilerOutput; +}; diff --git a/packages/sol-cov/src/trace_collection_subprovider.ts b/packages/sol-cov/src/trace_collection_subprovider.ts index 8b5168c45..742735935 100644 --- a/packages/sol-cov/src/trace_collection_subprovider.ts +++ b/packages/sol-cov/src/trace_collection_subprovider.ts @@ -29,12 +29,11 @@ export interface TraceCollectionSubproviderConfig { * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface. * It collects traces of all transactions that were sent and all calls that were executed through JSON RPC. */ -export class TraceCollectionSubprovider extends Subprovider { +export abstract class TraceCollectionSubprovider extends Subprovider { // Lock is used to not accept normal transactions while doing call/snapshot magic because they'll be reverted later otherwise private _lock = new Lock(); private _defaultFromAddress: string; private _web3Wrapper!: Web3Wrapper; - private _traceInfos: TraceInfo[] = []; private _isEnabled = true; private _config: TraceCollectionSubproviderConfig; /** @@ -47,12 +46,6 @@ export class TraceCollectionSubprovider extends Subprovider { this._config = config; } /** - * Returns all trace infos collected by the subprovider so far - */ - public getCollectedTraceInfos(): TraceInfo[] { - return this._traceInfos; - } - /** * Starts trace collection */ public start(): void { @@ -65,6 +58,11 @@ export class TraceCollectionSubprovider extends Subprovider { this._isEnabled = false; } /** + * Called for each subtrace. + * @param traceInfo Trace info for this subtrace. + */ + public abstract handleTraceInfoAsync(traceInfo: TraceInfo): Promise<void>; + /** * This method conforms to the web3-provider-engine interface. * It is called internally by the ProviderEngine when it is this subproviders * turn to handle a JSON RPC request. @@ -163,7 +161,7 @@ export class TraceCollectionSubprovider extends Subprovider { cb(); } private async _recordTxTraceAsync(address: string, data: string | undefined, txHash: string): Promise<void> { - await this._web3Wrapper.awaitTransactionMinedAsync(txHash); + await this._web3Wrapper.awaitTransactionMinedAsync(txHash, 0); const trace = await this._web3Wrapper.getTransactionTraceAsync(txHash, { disableMemory: true, disableStack: false, @@ -192,7 +190,7 @@ export class TraceCollectionSubprovider extends Subprovider { runtimeBytecode, }; } - this._traceInfos.push(traceInfo); + await this.handleTraceInfoAsync(traceInfo); } } else { for (const subcallAddress of subcallAddresses) { @@ -204,7 +202,7 @@ export class TraceCollectionSubprovider extends Subprovider { address: subcallAddress, runtimeBytecode, }; - this._traceInfos.push(traceInfo); + await this.handleTraceInfoAsync(traceInfo); } } } @@ -222,7 +220,7 @@ export class TraceCollectionSubprovider extends Subprovider { }; try { const txHash = await this._web3Wrapper.sendTransactionAsync(fakeTxData); - await this._web3Wrapper.awaitTransactionMinedAsync(txHash); + await this._web3Wrapper.awaitTransactionMinedAsync(txHash, 0); } catch (err) { // Even if this transaction failed - we've already recorded it's trace. _.noop(); diff --git a/packages/sol-cov/src/trace_collector.ts b/packages/sol-cov/src/trace_collector.ts new file mode 100644 index 000000000..1b458edec --- /dev/null +++ b/packages/sol-cov/src/trace_collector.ts @@ -0,0 +1,93 @@ +import { promisify } from '@0xproject/utils'; +import { stripHexPrefix } from 'ethereumjs-util'; +import * as fs from 'fs'; +import { Collector } from 'istanbul'; +import * as _ from 'lodash'; +import { getLogger, levels, Logger } from 'loglevel'; +import * as mkdirp from 'mkdirp'; + +import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter'; +import { constants } from './constants'; +import { parseSourceMap } from './source_maps'; +import { + ContractData, + Coverage, + SourceRange, + Subtrace, + TraceInfo, + TraceInfoExistingContract, + TraceInfoNewContract, +} from './types'; +import { utils } from './utils'; + +const mkdirpAsync = promisify<undefined>(mkdirp); + +export type SingleFileSubtraceHandler = ( + contractData: ContractData, + subtrace: Subtrace, + pcToSourceRange: { [programCounter: number]: SourceRange }, + fileIndex: number, +) => Coverage; + +/** + * TraceCollector is used by CoverageSubprovider to compute code coverage based on collected trace data. + */ +export class TraceCollector { + private _artifactAdapter: AbstractArtifactAdapter; + private _logger: Logger; + private _contractsData!: ContractData[]; + private _collector = new Collector(); + private _singleFileSubtraceHandler: SingleFileSubtraceHandler; + + /** + * Instantiates a TraceCollector instance + * @param artifactAdapter Adapter for used artifacts format (0x, truffle, giveth, etc.) + * @param isVerbose If true, we will log any unknown transactions. Otherwise we will ignore them + * @param singleFileSubtraceHandler A handler function for computing partial coverage for a single file & subtrace + */ + constructor( + artifactAdapter: AbstractArtifactAdapter, + isVerbose: boolean, + singleFileSubtraceHandler: SingleFileSubtraceHandler, + ) { + this._artifactAdapter = artifactAdapter; + this._logger = getLogger('sol-cov'); + this._logger.setLevel(isVerbose ? levels.TRACE : levels.ERROR); + this._singleFileSubtraceHandler = singleFileSubtraceHandler; + } + public async writeOutputAsync(): Promise<void> { + const finalCoverage = this._collector.getFinalCoverage(); + const stringifiedCoverage = JSON.stringify(finalCoverage, null, '\t'); + await mkdirpAsync('coverage'); + fs.writeFileSync('coverage/coverage.json', stringifiedCoverage); + } + public async computeSingleTraceCoverageAsync(traceInfo: TraceInfo): Promise<void> { + if (_.isUndefined(this._contractsData)) { + this._contractsData = await this._artifactAdapter.collectContractsDataAsync(); + } + const isContractCreation = traceInfo.address === constants.NEW_CONTRACT; + const bytecode = isContractCreation + ? (traceInfo as TraceInfoNewContract).bytecode + : (traceInfo as TraceInfoExistingContract).runtimeBytecode; + const contractData = utils.getContractDataIfExists(this._contractsData, bytecode); + if (_.isUndefined(contractData)) { + const errMsg = isContractCreation + ? `Unknown contract creation transaction` + : `Transaction to an unknown address: ${traceInfo.address}`; + this._logger.warn(errMsg); + return; + } + const bytecodeHex = stripHexPrefix(bytecode); + const sourceMap = isContractCreation ? contractData.sourceMap : contractData.sourceMapRuntime; + const pcToSourceRange = parseSourceMap(contractData.sourceCodes, sourceMap, bytecodeHex, contractData.sources); + for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) { + const singleFileCoverageForTrace = this._singleFileSubtraceHandler( + contractData, + traceInfo.subtrace, + pcToSourceRange, + fileIndex, + ); + this._collector.add(singleFileCoverageForTrace); + } + } +} |