aboutsummaryrefslogtreecommitdiffstats
path: root/packages/sol-coverage/src/coverage_subprovider.ts
blob: d03963ed6ae4567b8857d94d643fde670d4ed4fa (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import {
    AbstractArtifactAdapter,
    BranchCoverage,
    collectCoverageEntries,
    ContractData,
    Coverage,
    FunctionCoverage,
    FunctionDescription,
    SingleFileSubtraceHandler,
    SourceRange,
    StatementCoverage,
    StatementDescription,
    Subtrace,
    TraceCollector,
    TraceInfo,
    TraceInfoSubprovider,
    utils,
} from '@0x/sol-tracing-utils';
import * as _ from 'lodash';

/**
 * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface.
 * It's used to compute your code coverage while running solidity tests.
 */
export class CoverageSubprovider extends TraceInfoSubprovider {
    private readonly _coverageCollector: TraceCollector;
    /**
     * Instantiates a CoverageSubprovider instance
     * @param artifactAdapter Adapter for used artifacts format (0x, truffle, giveth, etc.)
     * @param defaultFromAddress default from address to use when sending transactions
     * @param isVerbose If true, we will log any unknown transactions. Otherwise we will ignore them
     */
    constructor(artifactAdapter: AbstractArtifactAdapter, defaultFromAddress: string, isVerbose: boolean = true) {
        const traceCollectionSubproviderConfig = {
            shouldCollectTransactionTraces: true,
            shouldCollectGasEstimateTraces: true,
            shouldCollectCallTraces: true,
        };
        super(defaultFromAddress, traceCollectionSubproviderConfig);
        this._coverageCollector = new TraceCollector(artifactAdapter, isVerbose, coverageHandler);
    }
    protected async _handleTraceInfoAsync(traceInfo: TraceInfo): Promise<void> {
        await this._coverageCollector.computeSingleTraceCoverageAsync(traceInfo);
    }
    /**
     * Write the test coverage results to a file in Istanbul format.
     */
    public async writeCoverageAsync(): Promise<void> {
        await this._coverageCollector.writeOutputAsync();
    }
}

/**
 * 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
 */
export const coverageHandler: SingleFileSubtraceHandler = (
    contractData: ContractData,
    subtrace: Subtrace,
    pcToSourceRange: { [programCounter: number]: SourceRange },
    fileIndex: number,
): Coverage => {
    const absoluteFileName = contractData.sources[fileIndex];
    const coverageEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex]);

    // if the source wasn't provided for the fileIndex, we can't cover the file
    if (_.isUndefined(coverageEntriesDescription)) {
        return {};
    }

    let sourceRanges = _.map(subtrace, structLog => pcToSourceRange[structLog.pc]);
    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 comparison. 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 === absoluteFileName);
    const branchCoverage: BranchCoverage = {};
    const branchIds = _.keys(coverageEntriesDescription.branchMap);
    for (const branchId of branchIds) {
        const branchDescription = coverageEntriesDescription.branchMap[branchId];
        const branchIndexToIsBranchCovered = _.map(branchDescription.locations, location => {
            const isBranchCovered = _.some(sourceRanges, range => utils.isRangeInside(range.location, location));
            const timesBranchCovered = Number(isBranchCovered);
            return timesBranchCovered;
        });
        branchCoverage[branchId] = branchIndexToIsBranchCovered;
    }
    const statementCoverage: StatementCoverage = {};
    const statementIds = _.keys(coverageEntriesDescription.statementMap);
    for (const statementId of statementIds) {
        const statementDescription = coverageEntriesDescription.statementMap[statementId];
        const isStatementCovered = _.some(sourceRanges, range =>
            utils.isRangeInside(range.location, statementDescription),
        );
        const timesStatementCovered = Number(isStatementCovered);
        statementCoverage[statementId] = timesStatementCovered;
    }
    const functionCoverage: FunctionCoverage = {};
    const functionIds = _.keys(coverageEntriesDescription.fnMap);
    for (const fnId of functionIds) {
        const functionDescription = coverageEntriesDescription.fnMap[fnId];
        const isFunctionCovered = _.some(sourceRanges, range =>
            utils.isRangeInside(range.location, functionDescription.loc),
        );
        const timesFunctionCovered = Number(isFunctionCovered);
        functionCoverage[fnId] = timesFunctionCovered;
    }
    // HACK: Solidity doesn't emit any opcodes that map back to modifiers with no args, that's why we map back to the
    // function range and check if there is any covered statement within that range.
    for (const modifierStatementId of coverageEntriesDescription.modifiersStatementIds) {
        if (statementCoverage[modifierStatementId]) {
            // Already detected as covered
            continue;
        }
        const modifierDescription = coverageEntriesDescription.statementMap[modifierStatementId];
        const enclosingFunction = _.find(coverageEntriesDescription.fnMap, functionDescription =>
            utils.isRangeInside(modifierDescription, functionDescription.loc),
        ) as FunctionDescription;
        const isModifierCovered = _.some(
            coverageEntriesDescription.statementMap,
            (statementDescription: StatementDescription, statementId: number) => {
                const isInsideTheModifierEnclosingFunction = utils.isRangeInside(
                    statementDescription,
                    enclosingFunction.loc,
                );
                const isCovered = statementCoverage[statementId];
                return isInsideTheModifierEnclosingFunction && isCovered;
            },
        );
        const timesModifierCovered = Number(isModifierCovered);
        statementCoverage[modifierStatementId] = timesModifierCovered;
    }
    const partialCoverage = {
        [absoluteFileName]: {
            ...coverageEntriesDescription,
            path: absoluteFileName,
            f: functionCoverage,
            s: statementCoverage,
            b: branchCoverage,
        },
    };
    return partialCoverage;
};