From 13299158d1e22d1af1cd36434fc403a74743ecb1 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 4 Mar 2018 19:05:26 -0800 Subject: Add sol-cover implementation --- packages/sol-cov/src/coverage_manager.ts | 166 +++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 packages/sol-cov/src/coverage_manager.ts (limited to 'packages/sol-cov/src/coverage_manager.ts') diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts new file mode 100644 index 000000000..aced9208f --- /dev/null +++ b/packages/sol-cov/src/coverage_manager.ts @@ -0,0 +1,166 @@ +import * as fs from 'fs'; +import { Collector } from 'istanbul'; +import * as _ from 'lodash'; +import * as path from 'path'; + +import { collectContractsData } from './collect_contract_data'; +import { constants } from './constants'; +import { collectCoverageEntries } from './instrument_solidity'; +import { parseSourceMap } from './source_maps'; +import { + BranchCoverage, + BranchDescription, + BranchMap, + ContractData, + Coverage, + FnMap, + FunctionCoverage, + FunctionDescription, + LineColumn, + SingleFileSourceRange, + SourceRange, + StatementCoverage, + StatementDescription, + StatementMap, + TraceInfo, +} from './types'; +import { utils } from './utils'; + +function getSingleFileCoverageForTrace( + contractData: ContractData, + coveredPcs: number[], + pcToSourceRange: { [programCounter: number]: SourceRange }, + fileIndex: number, +): Coverage { + const fileName = contractData.sources[fileIndex]; + const coverageEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex], fileName); + 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. + 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); + const branchCoverage: BranchCoverage = {}; + for (const branchId of _.keys(coverageEntriesDescription.branchMap)) { + const branchDescription = coverageEntriesDescription.branchMap[branchId]; + const isCovered = _.map(branchDescription.locations, location => + _.some(sourceRanges, range => utils.isRangeInside(range.location, location)), + ); + branchCoverage[branchId] = isCovered; + } + const statementCoverage: StatementCoverage = {}; + for (const statementId of _.keys(coverageEntriesDescription.statementMap)) { + const statementDescription = coverageEntriesDescription.statementMap[statementId]; + const isCovered = _.some(sourceRanges, range => utils.isRangeInside(range.location, statementDescription)); + statementCoverage[statementId] = isCovered; + } + const functionCoverage: FunctionCoverage = {}; + for (const fnId of _.keys(coverageEntriesDescription.fnMap)) { + const functionDescription = coverageEntriesDescription.fnMap[fnId]; + const isCovered = _.some(sourceRanges, range => utils.isRangeInside(range.location, functionDescription.loc)); + functionCoverage[fnId] = isCovered; + } + const partialCoverage = { + [contractData.sources[fileIndex]]: { + ...coverageEntriesDescription, + l: {}, // It's able to derive it from statement coverage + path: fileName, + f: functionCoverage, + s: statementCoverage, + b: branchCoverage, + }, + }; + return partialCoverage; +} + +export class CoverageManager { + private _traceInfoByAddress: { [address: string]: TraceInfo[] } = {}; + private _contractsData: ContractData[] = []; + private _txDataByHash: { [txHash: string]: string } = {}; + private _getContractCodeAsync: (address: string) => Promise; + constructor( + artifactsPath: string, + sourcesPath: string, + networkId: number, + getContractCodeAsync: (address: string) => Promise, + ) { + this._getContractCodeAsync = getContractCodeAsync; + this._contractsData = collectContractsData(artifactsPath, sourcesPath, networkId); + } + public setTxDataByHash(txHash: string, data: string): void { + this._txDataByHash[txHash] = data; + } + public appendTraceInfo(address: string, traceInfo: TraceInfo): void { + if (_.isUndefined(this._traceInfoByAddress[address])) { + this._traceInfoByAddress[address] = []; + } + this._traceInfoByAddress[address].push(traceInfo); + } + public async writeCoverageAsync(): Promise { + const finalCoverage = await this._computeCoverageAsync(); + const jsonReplacer: null = null; + const numberOfJsonSpaces = 4; + const stringifiedCoverage = JSON.stringify(finalCoverage, jsonReplacer, numberOfJsonSpaces); + fs.writeFileSync('coverage/coverage.json', stringifiedCoverage); + } + private async _computeCoverageAsync(): Promise { + const collector = new Collector(); + for (const address of _.keys(this._traceInfoByAddress)) { + if (address !== constants.NEW_CONTRACT) { + // Runtime transaction + const runtimeBytecode = await this._getContractCodeAsync(address); + const contractData = _.find(this._contractsData, { runtimeBytecode }) as ContractData; + if (_.isUndefined(contractData)) { + throw new Error(`Transaction to an unknown address: ${address}`); + } + const bytecodeHex = contractData.runtimeBytecode.slice(2); + const sourceMap = contractData.sourceMapRuntime; + const pcToSourceRange = parseSourceMap( + contractData.sourceCodes, + sourceMap, + bytecodeHex, + contractData.sources, + ); + for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) { + _.forEach(this._traceInfoByAddress[address], (traceInfo: TraceInfo) => { + const singleFileCoverageForTrace = getSingleFileCoverageForTrace( + contractData, + traceInfo.coveredPcs, + pcToSourceRange, + fileIndex, + ); + collector.add(singleFileCoverageForTrace); + }); + } + } else { + // Contract creation transaction + _.forEach(this._traceInfoByAddress[address], (traceInfo: TraceInfo) => { + const bytecode = this._txDataByHash[traceInfo.txHash]; + const contractData = _.find(this._contractsData, contractDataCandidate => + bytecode.startsWith(contractDataCandidate.bytecode), + ) as ContractData; + if (_.isUndefined(contractData)) { + throw new Error(`Unknown contract creation transaction`); + } + const bytecodeHex = contractData.bytecode.slice(2); + const sourceMap = contractData.sourceMap; + const pcToSourceRange = parseSourceMap( + contractData.sourceCodes, + sourceMap, + bytecodeHex, + contractData.sources, + ); + for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) { + const singleFileCoverageForTrace = getSingleFileCoverageForTrace( + contractData, + traceInfo.coveredPcs, + pcToSourceRange, + fileIndex, + ); + collector.add(singleFileCoverageForTrace); + } + }); + } + } + // TODO: Submit a PR to DT + return (collector as any).getFinalCoverage(); + } +} -- cgit v1.2.3