aboutsummaryrefslogblamecommitdiffstats
path: root/packages/sol-cov/src/coverage_manager.ts
blob: aced9208f2326ed71e0eaa829b4f8bd4029a39ee (plain) (tree)





































































































































































                                                                                                                                                    
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<string>;
    constructor(
        artifactsPath: string,
        sourcesPath: string,
        networkId: number,
        getContractCodeAsync: (address: string) => Promise<string>,
    ) {
        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<void> {
        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<Coverage> {
        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();
    }
}