diff options
Diffstat (limited to 'packages/sol-cov/src/coverage_manager.ts')
-rw-r--r-- | packages/sol-cov/src/coverage_manager.ts | 105 |
1 files changed, 65 insertions, 40 deletions
diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts index 800ca96dd..31b0e6fbc 100644 --- a/packages/sol-cov/src/coverage_manager.ts +++ b/packages/sol-cov/src/coverage_manager.ts @@ -1,12 +1,13 @@ import { promisify } from '@0xproject/utils'; -import { addHexPrefix } from 'ethereumjs-util'; +import { addHexPrefix, stripHexPrefix } from 'ethereumjs-util'; import * as fs from 'fs'; import { Collector } from 'istanbul'; import * as _ from 'lodash'; +import { getLogger, levels, Logger, LogLevel } from 'loglevel'; import * as mkdirp from 'mkdirp'; import * as path from 'path'; -import { collectContractsData } from './collect_contract_data'; +import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter'; import { collectCoverageEntries } from './collect_coverage_entries'; import { constants } from './constants'; import { parseSourceMap } from './source_maps'; @@ -34,41 +35,23 @@ import { utils } from './utils'; const mkdirpAsync = promisify<undefined>(mkdirp); export class CoverageManager { - private _sourcesPath: string; + private _artifactAdapter: AbstractArtifactAdapter; + private _logger: Logger; private _traceInfos: TraceInfo[] = []; - private _contractsData: ContractData[] = []; private _getContractCodeAsync: (address: string) => Promise<string>; - constructor( - artifactsPath: string, - sourcesPath: string, - getContractCodeAsync: (address: string) => Promise<string>, - ) { - this._getContractCodeAsync = getContractCodeAsync; - this._sourcesPath = sourcesPath; - this._contractsData = collectContractsData(artifactsPath, this._sourcesPath); - } - 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 _getSingleFileCoverageForTrace( + private static _getSingleFileCoverageForTrace( contractData: ContractData, coveredPcs: number[], pcToSourceRange: { [programCounter: number]: SourceRange }, fileIndex: number, ): Coverage { - const fileName = contractData.sources[fileIndex]; + const absoluteFileName = contractData.sources[fileIndex]; const coverageEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex]); let sourceRanges = _.map(coveredPcs, coveredPc => pcToSourceRange[coveredPc]); 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 === fileName); + sourceRanges = _.filter(sourceRanges, sourceRange => sourceRange.fileName === absoluteFileName); const branchCoverage: BranchCoverage = {}; const branchIds = _.keys(coverageEntriesDescription.branchMap); for (const branchId of branchIds) { @@ -118,7 +101,6 @@ export class CoverageManager { ); statementCoverage[modifierStatementId] = isModifierCovered; } - const absoluteFileName = path.join(this._sourcesPath, fileName); const partialCoverage = { [absoluteFileName]: { ...coverageEntriesDescription, @@ -131,18 +113,63 @@ export class CoverageManager { }; return partialCoverage; } + private static _bytecodeToBytecodeRegex(bytecode: string): string { + const bytecodeRegex = bytecode + // Library linking placeholder: __ConvertLib____________________________ + .replace(/_.*_/, '.*') + // Last 86 characters is solidity compiler metadata that's different between compilations + .replace(/.{86}$/, '') + // Libraries contain their own address at the beginning of the code and it's impossible to know it in advance + .replace(/^0x730000000000000000000000000000000000000000/, '0x73........................................'); + return bytecodeRegex; + } + private static _getContractDataIfExists(contractsData: ContractData[], bytecode: string): ContractData | undefined { + if (!bytecode.startsWith('0x')) { + throw new Error(`0x hex prefix missing: ${bytecode}`); + } + const contractData = _.find(contractsData, contractDataCandidate => { + const bytecodeRegex = CoverageManager._bytecodeToBytecodeRegex(contractDataCandidate.bytecode); + const runtimeBytecodeRegex = CoverageManager._bytecodeToBytecodeRegex( + contractDataCandidate.runtimeBytecode, + ); + // We use that function to find by bytecode or runtimeBytecode. Those are quasi-random strings so + // collisions are practically impossible and it allows us to reuse that code + return !_.isNull(bytecode.match(bytecodeRegex)) || !_.isNull(bytecode.match(runtimeBytecodeRegex)); + }); + return contractData; + } + constructor( + artifactAdapter: AbstractArtifactAdapter, + getContractCodeAsync: (address: string) => Promise<string>, + isVerbose: boolean, + ) { + this._getContractCodeAsync = getContractCodeAsync; + 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) { if (traceInfo.address !== constants.NEW_CONTRACT) { // Runtime transaction - let runtimeBytecode = (traceInfo as TraceInfoExistingContract).runtimeBytecode; - runtimeBytecode = addHexPrefix(runtimeBytecode); - const contractData = _.find(this._contractsData, { runtimeBytecode }) as ContractData; + const runtimeBytecode = (traceInfo as TraceInfoExistingContract).runtimeBytecode; + const contractData = CoverageManager._getContractDataIfExists(contractsData, runtimeBytecode); if (_.isUndefined(contractData)) { - throw new Error(`Transaction to an unknown address: ${traceInfo.address}`); + this._logger.warn(`Transaction to an unknown address: ${traceInfo.address}`); + continue; } - const bytecodeHex = contractData.runtimeBytecode.slice(2); + const bytecodeHex = stripHexPrefix(runtimeBytecode); const sourceMap = contractData.sourceMapRuntime; const pcToSourceRange = parseSourceMap( contractData.sourceCodes, @@ -151,7 +178,7 @@ export class CoverageManager { contractData.sources, ); for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) { - const singleFileCoverageForTrace = this._getSingleFileCoverageForTrace( + const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace( contractData, traceInfo.coveredPcs, pcToSourceRange, @@ -161,15 +188,13 @@ export class CoverageManager { } } else { // Contract creation transaction - let bytecode = (traceInfo as TraceInfoNewContract).bytecode; - bytecode = addHexPrefix(bytecode); - const contractData = _.find(this._contractsData, contractDataCandidate => - bytecode.startsWith(contractDataCandidate.bytecode), - ) as ContractData; + const bytecode = (traceInfo as TraceInfoNewContract).bytecode; + const contractData = CoverageManager._getContractDataIfExists(contractsData, bytecode); if (_.isUndefined(contractData)) { - throw new Error(`Unknown contract creation transaction`); + this._logger.warn(`Unknown contract creation transaction`); + continue; } - const bytecodeHex = contractData.bytecode.slice(2); + const bytecodeHex = stripHexPrefix(bytecode); const sourceMap = contractData.sourceMap; const pcToSourceRange = parseSourceMap( contractData.sourceCodes, @@ -178,7 +203,7 @@ export class CoverageManager { contractData.sources, ); for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) { - const singleFileCoverageForTrace = this._getSingleFileCoverageForTrace( + const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace( contractData, traceInfo.coveredPcs, pcToSourceRange, |