Skip to content
This repository has been archived by the owner on Jul 9, 2021. It is now read-only.

Allow project specific coverage ignore paths by specifying config.ignoreFilesGlobs #1656

Merged
merged 5 commits into from
Feb 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions contracts/test-utils/src/coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,15 @@ export const coverage = {
_getCoverageSubprovider(): CoverageSubprovider {
const defaultFromAddress = devConstants.TESTRPC_FIRST_ADDRESS;
const solCompilerArtifactAdapter = new SolCompilerArtifactAdapter();
const isVerbose = true;
const subprovider = new CoverageSubprovider(solCompilerArtifactAdapter, defaultFromAddress, isVerbose);
const coverageSubproviderConfig = {
isVerbose: true,
ignoreFilesGlobs: ['**/node_modules/**', '**/interfaces/**', '**/test/**'],
};
const subprovider = new CoverageSubprovider(
solCompilerArtifactAdapter,
defaultFromAddress,
coverageSubproviderConfig,
);
return subprovider;
},
};
17 changes: 17 additions & 0 deletions packages/sol-coverage/CHANGELOG.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
[
{
"version": "3.0.0",
"changes": [
{
"note": "Change the interface to accept a configuration object instead of `isVerbose`",
"pr": 1656
},
{
"note": "Add `ignoreFilesGlobs` property on a config object",
"pr": 1656
},
{
"note": "Allow project specific coverage ignore paths by specifying `config.ignoreFilesGlobs`",
"pr": 1656
}
]
},
{
"timestamp": 1551299797,
"version": "2.0.6",
Expand Down
2 changes: 2 additions & 0 deletions packages/sol-coverage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
"@0x/sol-tracing-utils": "^6.0.6",
"@0x/subproviders": "^4.0.1",
"@0x/typescript-typings": "^4.1.0",
"@types/minimatch": "^3.0.3",
"ethereum-types": "^2.1.0",
"lodash": "^4.17.11",
"minimatch": "^3.0.4",
"web3-provider-engine": "14.0.6"
},
"devDependencies": {
Expand Down
234 changes: 137 additions & 97 deletions packages/sol-coverage/src/coverage_subprovider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
Coverage,
FunctionCoverage,
FunctionDescription,
SingleFileSubtraceHandler,
SourceRange,
StatementCoverage,
StatementDescription,
Expand All @@ -17,27 +16,58 @@ import {
utils,
} from '@0x/sol-tracing-utils';
import * as _ from 'lodash';
import * as minimatch from 'minimatch';

/**
* This type defines the schema of the config object that could be passed to CoverageSubprovider
* isVerbose: If true - will log any unknown transactions. Defaults to true.
* ignoreFilesGlobs: The list of globs matching the file names of the files we want to ignore coverage for. Defaults to [].
*/
export interface CoverageSubproviderConfig {
isVerbose: boolean;
ignoreFilesGlobs: string[];
}

export type CoverageSubproviderPartialConfig = Partial<CoverageSubproviderConfig>;

export const DEFAULT_COVERAGE_SUBPROVIDER_CONFIG = {
isVerbose: true,
ignoreFilesGlobs: [],
};

/**
* 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;
private readonly _coverageSubproviderCnfig: CoverageSubproviderConfig;
/**
* 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
* @param partialConfig Partial configuration object
*/
constructor(artifactAdapter: AbstractArtifactAdapter, defaultFromAddress: string, isVerbose: boolean = true) {
constructor(
artifactAdapter: AbstractArtifactAdapter,
defaultFromAddress: string,
partialConfig: CoverageSubproviderPartialConfig = {},
) {
const traceCollectionSubproviderConfig = {
shouldCollectTransactionTraces: true,
shouldCollectGasEstimateTraces: true,
shouldCollectCallTraces: true,
};
super(defaultFromAddress, traceCollectionSubproviderConfig);
this._coverageCollector = new TraceCollector(artifactAdapter, isVerbose, coverageHandler);
this._coverageSubproviderCnfig = {
...DEFAULT_COVERAGE_SUBPROVIDER_CONFIG,
...partialConfig,
};
this._coverageCollector = new TraceCollector(
artifactAdapter,
this._coverageSubproviderCnfig.isVerbose,
this._coverageHandler.bind(this),
);
}
protected async _handleSubTraceInfoAsync(subTraceInfo: SubTraceInfo): Promise<void> {
await this._coverageCollector.computeSingleTraceCoverageAsync(subTraceInfo);
Expand All @@ -48,101 +78,111 @@ export class CoverageSubprovider extends TraceInfoSubprovider {
public async writeCoverageAsync(): Promise<void> {
await this._coverageCollector.writeOutputAsync();
}
}

const IGNORE_REGEXP = /\/\*\s*solcov\s+ignore\s+next\s*\*\/\s*/gm;

/**
* 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], IGNORE_REGEXP);

// if the source wasn't provided for the fileIndex, we can't cover the file
if (_.isUndefined(coverageEntriesDescription)) {
return {};
private _isFileIgnored(absoluteFileName: string): boolean {
for (const ignoreFilesGlob of this._coverageSubproviderCnfig.ignoreFilesGlobs) {
if (minimatch(absoluteFileName, ignoreFilesGlob)) {
return true;
}
}
return false;
}
/**
* Computes 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 _coverageHandler(
contractData: ContractData,
subtrace: Subtrace,
pcToSourceRange: { [programCounter: number]: SourceRange },
fileIndex: number,
): Coverage {
const absoluteFileName = contractData.sources[fileIndex];
if (this._isFileIgnored(absoluteFileName)) {
return {};
}
const coverageEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex], IGNORE_REGEXP);

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;
// 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 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 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,
},
);
const timesModifierCovered = Number(isModifierCovered);
statementCoverage[modifierStatementId] = timesModifierCovered;
};
return partialCoverage;
}
const partialCoverage = {
[absoluteFileName]: {
...coverageEntriesDescription,
path: absoluteFileName,
f: functionCoverage,
s: statementCoverage,
b: branchCoverage,
},
};
return partialCoverage;
};
}

const IGNORE_REGEXP = /\/\*\s*solcov\s+ignore\s+next\s*\*\/\s*/gm;
7 changes: 6 additions & 1 deletion packages/sol-coverage/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export { CoverageSubprovider } from './coverage_subprovider';
export {
CoverageSubprovider,
CoverageSubproviderConfig,
DEFAULT_COVERAGE_SUBPROVIDER_CONFIG,
CoverageSubproviderPartialConfig,
} from './coverage_subprovider';
export {
SolCompilerArtifactAdapter,
TruffleArtifactAdapter,
Expand Down
3 changes: 1 addition & 2 deletions packages/website/md/docs/sol_coverage/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ import ProviderEngine = require('web3-provider-engine');
const provider = new ProviderEngine();
// Some calls might not have `from` address specified. Nevertheless - transactions need to be submitted from an address with at least some funds. defaultFromAddress is the address that will be used to submit those calls as transactions from.
const defaultFromAddress = '0x5409ed021d9299bf6814279a6a1411a7e866a631';
const isVerbose = true;
const coverageSubprovider = new CoverageSubprovider(artifactsAdapter, defaultFromAddress, isVerbose);
const coverageSubprovider = new CoverageSubprovider(artifactsAdapter, defaultFromAddress);

provider.addProvider(coverageSubprovider);
// Add all your other providers
Expand Down
18 changes: 16 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1569,7 +1569,7 @@
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b"

"@types/minimatch@*", "@types/[email protected]":
"@types/minimatch@*", "@types/[email protected]", "@types/minimatch@^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"

Expand Down Expand Up @@ -7925,10 +7925,20 @@ got@^6.7.1:
unzip-response "^2.0.1"
url-parse-lax "^1.0.0"

[email protected], graceful-fs@^3.0.0, graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@~1.2.0:
graceful-fs@^3.0.0:
version "3.0.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-3.0.11.tgz#7613c778a1afea62f25c630a086d7f3acbbdd818"
dependencies:
natives "^1.1.0"

graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
version "4.1.15"
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"

graceful-fs@~1.2.0:
version "1.2.3"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-1.2.3.tgz#15a4806a57547cb2d2dbf27f42e89a8c3451b364"

"graceful-readlink@>= 1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
Expand Down Expand Up @@ -11362,6 +11372,10 @@ nanomatch@^1.2.9:
snapdragon "^0.8.1"
to-regex "^3.0.1"

natives@^1.1.0:
version "1.1.6"
resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.6.tgz#a603b4a498ab77173612b9ea1acdec4d980f00bb"

natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
Expand Down