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
































                                                                                                                                                                       

                                            





















































                                                                                                           
                                                            
                                                                 



                                                                              
























                                                                                                                       
              
                                                            
         

     
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 _contractsData!: ContractData[];
    private _collector = new Collector();
    /**
     * 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 async writeProfilerOutputAsync(): 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 computeCoverageAsync(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 = ProfilerManager._getSingleFileCoverageForSubtrace(
                contractData,
                traceInfo.subtrace,
                pcToSourceRange,
                fileIndex,
            );
            this._collector.add(singleFileCoverageForTrace);
        }
    }
}