import { promisify } from '@0x/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(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 readonly _artifactAdapter: AbstractArtifactAdapter; private readonly _logger: Logger; private _contractsData!: ContractData[]; private readonly _collector = new Collector(); private readonly _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-tracing-utils'); this._logger.setLevel(isVerbose ? levels.TRACE : levels.ERROR); this._singleFileSubtraceHandler = singleFileSubtraceHandler; } public async writeOutputAsync(): Promise { const finalCoverage: Coverage = 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 { 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); _.map(contractData.sources, (_sourcePath: string, fileIndex: string) => { const singleFileCoverageForTrace = this._singleFileSubtraceHandler( contractData, traceInfo.subtrace, pcToSourceRange, _.parseInt(fileIndex), ); this._collector.add(singleFileCoverageForTrace); }); } }