aboutsummaryrefslogtreecommitdiffstats
path: root/packages/sol-cov/src/coverage_manager.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/sol-cov/src/coverage_manager.ts')
-rw-r--r--packages/sol-cov/src/coverage_manager.ts105
1 files changed, 65 insertions, 40 deletions
diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts
index 800ca96dd..31b0e6fbc 100644
--- a/packages/sol-cov/src/coverage_manager.ts
+++ b/packages/sol-cov/src/coverage_manager.ts
@@ -1,12 +1,13 @@
import { promisify } from '@0xproject/utils';
-import { addHexPrefix } from 'ethereumjs-util';
+import { addHexPrefix, stripHexPrefix } from 'ethereumjs-util';
import * as fs from 'fs';
import { Collector } from 'istanbul';
import * as _ from 'lodash';
+import { getLogger, levels, Logger, LogLevel } from 'loglevel';
import * as mkdirp from 'mkdirp';
import * as path from 'path';
-import { collectContractsData } from './collect_contract_data';
+import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter';
import { collectCoverageEntries } from './collect_coverage_entries';
import { constants } from './constants';
import { parseSourceMap } from './source_maps';
@@ -34,41 +35,23 @@ import { utils } from './utils';
const mkdirpAsync = promisify<undefined>(mkdirp);
export class CoverageManager {
- private _sourcesPath: string;
+ private _artifactAdapter: AbstractArtifactAdapter;
+ private _logger: Logger;
private _traceInfos: TraceInfo[] = [];
- private _contractsData: ContractData[] = [];
private _getContractCodeAsync: (address: string) => Promise<string>;
- constructor(
- artifactsPath: string,
- sourcesPath: string,
- getContractCodeAsync: (address: string) => Promise<string>,
- ) {
- this._getContractCodeAsync = getContractCodeAsync;
- this._sourcesPath = sourcesPath;
- this._contractsData = collectContractsData(artifactsPath, this._sourcesPath);
- }
- public appendTraceInfo(traceInfo: TraceInfo): void {
- this._traceInfos.push(traceInfo);
- }
- public async writeCoverageAsync(): Promise<void> {
- const finalCoverage = await this._computeCoverageAsync();
- const stringifiedCoverage = JSON.stringify(finalCoverage, null, '\t');
- await mkdirpAsync('coverage');
- fs.writeFileSync('coverage/coverage.json', stringifiedCoverage);
- }
- private _getSingleFileCoverageForTrace(
+ private static _getSingleFileCoverageForTrace(
contractData: ContractData,
coveredPcs: number[],
pcToSourceRange: { [programCounter: number]: SourceRange },
fileIndex: number,
): Coverage {
- const fileName = contractData.sources[fileIndex];
+ const absoluteFileName = contractData.sources[fileIndex];
const coverageEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex]);
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.
// By default lodash does a shallow object comparasion. 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 === fileName);
+ sourceRanges = _.filter(sourceRanges, sourceRange => sourceRange.fileName === absoluteFileName);
const branchCoverage: BranchCoverage = {};
const branchIds = _.keys(coverageEntriesDescription.branchMap);
for (const branchId of branchIds) {
@@ -118,7 +101,6 @@ export class CoverageManager {
);
statementCoverage[modifierStatementId] = isModifierCovered;
}
- const absoluteFileName = path.join(this._sourcesPath, fileName);
const partialCoverage = {
[absoluteFileName]: {
...coverageEntriesDescription,
@@ -131,18 +113,63 @@ export class CoverageManager {
};
return partialCoverage;
}
+ private static _bytecodeToBytecodeRegex(bytecode: string): string {
+ const bytecodeRegex = bytecode
+ // Library linking placeholder: __ConvertLib____________________________
+ .replace(/_.*_/, '.*')
+ // Last 86 characters is solidity compiler metadata that's different between compilations
+ .replace(/.{86}$/, '')
+ // Libraries contain their own address at the beginning of the code and it's impossible to know it in advance
+ .replace(/^0x730000000000000000000000000000000000000000/, '0x73........................................');
+ return bytecodeRegex;
+ }
+ private static _getContractDataIfExists(contractsData: ContractData[], bytecode: string): ContractData | undefined {
+ if (!bytecode.startsWith('0x')) {
+ throw new Error(`0x hex prefix missing: ${bytecode}`);
+ }
+ const contractData = _.find(contractsData, contractDataCandidate => {
+ const bytecodeRegex = CoverageManager._bytecodeToBytecodeRegex(contractDataCandidate.bytecode);
+ const runtimeBytecodeRegex = CoverageManager._bytecodeToBytecodeRegex(
+ contractDataCandidate.runtimeBytecode,
+ );
+ // We use that function to find by bytecode or runtimeBytecode. Those are quasi-random strings so
+ // collisions are practically impossible and it allows us to reuse that code
+ return !_.isNull(bytecode.match(bytecodeRegex)) || !_.isNull(bytecode.match(runtimeBytecodeRegex));
+ });
+ return contractData;
+ }
+ constructor(
+ artifactAdapter: AbstractArtifactAdapter,
+ getContractCodeAsync: (address: string) => Promise<string>,
+ isVerbose: boolean,
+ ) {
+ this._getContractCodeAsync = getContractCodeAsync;
+ this._artifactAdapter = artifactAdapter;
+ this._logger = getLogger('sol-cov');
+ this._logger.setLevel(isVerbose ? levels.TRACE : levels.ERROR);
+ }
+ public appendTraceInfo(traceInfo: TraceInfo): void {
+ this._traceInfos.push(traceInfo);
+ }
+ public async writeCoverageAsync(): Promise<void> {
+ const finalCoverage = await this._computeCoverageAsync();
+ const stringifiedCoverage = JSON.stringify(finalCoverage, null, '\t');
+ await mkdirpAsync('coverage');
+ fs.writeFileSync('coverage/coverage.json', stringifiedCoverage);
+ }
private async _computeCoverageAsync(): Promise<Coverage> {
+ const contractsData = await this._artifactAdapter.collectContractsDataAsync();
const collector = new Collector();
for (const traceInfo of this._traceInfos) {
if (traceInfo.address !== constants.NEW_CONTRACT) {
// Runtime transaction
- let runtimeBytecode = (traceInfo as TraceInfoExistingContract).runtimeBytecode;
- runtimeBytecode = addHexPrefix(runtimeBytecode);
- const contractData = _.find(this._contractsData, { runtimeBytecode }) as ContractData;
+ const runtimeBytecode = (traceInfo as TraceInfoExistingContract).runtimeBytecode;
+ const contractData = CoverageManager._getContractDataIfExists(contractsData, runtimeBytecode);
if (_.isUndefined(contractData)) {
- throw new Error(`Transaction to an unknown address: ${traceInfo.address}`);
+ this._logger.warn(`Transaction to an unknown address: ${traceInfo.address}`);
+ continue;
}
- const bytecodeHex = contractData.runtimeBytecode.slice(2);
+ const bytecodeHex = stripHexPrefix(runtimeBytecode);
const sourceMap = contractData.sourceMapRuntime;
const pcToSourceRange = parseSourceMap(
contractData.sourceCodes,
@@ -151,7 +178,7 @@ export class CoverageManager {
contractData.sources,
);
for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
- const singleFileCoverageForTrace = this._getSingleFileCoverageForTrace(
+ const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace(
contractData,
traceInfo.coveredPcs,
pcToSourceRange,
@@ -161,15 +188,13 @@ export class CoverageManager {
}
} else {
// Contract creation transaction
- let bytecode = (traceInfo as TraceInfoNewContract).bytecode;
- bytecode = addHexPrefix(bytecode);
- const contractData = _.find(this._contractsData, contractDataCandidate =>
- bytecode.startsWith(contractDataCandidate.bytecode),
- ) as ContractData;
+ const bytecode = (traceInfo as TraceInfoNewContract).bytecode;
+ const contractData = CoverageManager._getContractDataIfExists(contractsData, bytecode);
if (_.isUndefined(contractData)) {
- throw new Error(`Unknown contract creation transaction`);
+ this._logger.warn(`Unknown contract creation transaction`);
+ continue;
}
- const bytecodeHex = contractData.bytecode.slice(2);
+ const bytecodeHex = stripHexPrefix(bytecode);
const sourceMap = contractData.sourceMap;
const pcToSourceRange = parseSourceMap(
contractData.sourceCodes,
@@ -178,7 +203,7 @@ export class CoverageManager {
contractData.sources,
);
for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
- const singleFileCoverageForTrace = this._getSingleFileCoverageForTrace(
+ const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace(
contractData,
traceInfo.coveredPcs,
pcToSourceRange,