Add sol-cover implementation

This commit is contained in:
Leonid Logvinov
2018-03-04 19:05:26 -08:00
parent a6571b09d2
commit 13299158d1
110 changed files with 3704 additions and 1570 deletions

View File

@@ -0,0 +1,5 @@
.*
yarn-error.log
/src/
/scripts/
tsconfig.json

View File

@@ -0,0 +1,5 @@
# CHANGELOG
## v0.0.1 - _TBD, 2018_
* Initial implementation (#TBD)

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,42 @@
{
"name": "@0xproject/sol-cov",
"version": "0.0.1",
"description": "Generate coverage reports for solidity code",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"scripts": {
"build:watch": "tsc -w",
"lint": "tslint --project . 'src/**/*.ts'",
"clean": "shx rm -rf lib",
"build": "tsc"
},
"repository": {
"type": "git",
"url": "https://github.com/0xProject/0x.js.git"
},
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/0xProject/0x.js/issues"
},
"homepage": "https://github.com/0xProject/0x.js/packages/sol-cov/README.md",
"dependencies": {
"@0xproject/subproviders": "^0.7.0",
"@0xproject/utils": "^0.3.4",
"glob": "^7.1.2",
"istanbul": "^0.4.5",
"lodash": "^4.17.4",
"solidity-coverage": "^0.4.10",
"solidity-parser-sc": "^0.4.4",
"web3": "^0.20.0"
},
"devDependencies": {
"@0xproject/tslint-config": "^0.4.9",
"@types/istanbul": "^0.4.29",
"@types/node": "^8.0.53",
"npm-run-all": "^4.1.2",
"shx": "^0.2.2",
"tslint": "5.8.0",
"typescript": "2.7.1",
"web3-typescript-typings": "^0.9.11"
}
}

View File

@@ -0,0 +1,5 @@
const postpublish_utils = require('../../../scripts/postpublish_utils');
const packageJSON = require('../package.json');
const subPackageName = packageJSON.name;
postpublish_utils.standardPostPublishAsync(subPackageName);

View File

@@ -0,0 +1,115 @@
import * as _ from 'lodash';
import * as SolidityParser from 'solidity-parser-sc';
import { BranchMap, FnMap, LocationByOffset, SingleFileSourceRange, StatementMap } from './types';
export interface CoverageEntriesDescription {
fnMap: FnMap;
branchMap: BranchMap;
statementMap: StatementMap;
}
export class ASTVisitor {
private _entryId = 0;
private _fnMap: FnMap = {};
private _branchMap: BranchMap = {};
private _statementMap: StatementMap = {};
private _locationByOffset: LocationByOffset;
private static _doesLookLikeAnASTNode(ast: any): boolean {
const isAST = _.isObject(ast) && _.isString(ast.type) && _.isNumber(ast.start) && _.isNumber(ast.end);
return isAST;
}
constructor(locationByOffset: LocationByOffset) {
this._locationByOffset = locationByOffset;
}
public walkAST(astNode: SolidityParser.AST): void {
if (_.isArray(astNode) || _.isObject(astNode)) {
if (ASTVisitor._doesLookLikeAnASTNode(astNode)) {
const nodeType = astNode.type;
const visitorFunctionName = `_visit${nodeType}`;
// tslint:disable-next-line:no-this-assignment
const self: { [visitorFunctionName: string]: (ast: SolidityParser.AST) => void } = this as any;
if (_.isFunction(self[visitorFunctionName])) {
self[visitorFunctionName](astNode);
}
}
_.forEach(astNode, subtree => {
this.walkAST(subtree);
});
}
}
public getCollectedCoverageEntries(): CoverageEntriesDescription {
const coverageEntriesDescription = {
fnMap: this._fnMap,
branchMap: this._branchMap,
statementMap: this._statementMap,
};
return coverageEntriesDescription;
}
private _visitConditionalExpression(ast: SolidityParser.AST): void {
this._visitBinaryBranch(ast, ast.consequent, ast.alternate, 'cond-expr');
}
private _visitFunctionDeclaration(ast: SolidityParser.AST): void {
const loc = this._getExpressionRange(ast);
this._fnMap[this._entryId++] = {
name: ast.name,
line: loc.start.line,
loc,
};
}
private _visitBinaryExpression(ast: SolidityParser.AST): void {
this._visitBinaryBranch(ast, ast.left, ast.right, 'binary-expr');
}
private _visitIfStatement(ast: SolidityParser.AST): void {
this._visitStatement(ast);
this._visitBinaryBranch(ast, ast.consequent, ast.alternate || ast, 'if');
}
private _visitBreakStatement(ast: SolidityParser.AST): void {
this._visitStatement(ast);
}
private _visitContractStatement(ast: SolidityParser.AST): void {
this._visitStatement(ast);
}
private _visitExpressionStatement(ast: SolidityParser.AST): void {
this._visitStatement(ast);
}
private _visitForStatement(ast: SolidityParser.AST): void {
this._visitStatement(ast);
}
private _visitPlaceholderStatement(ast: SolidityParser.AST): void {
this._visitStatement(ast);
}
private _visitReturnStatement(ast: SolidityParser.AST): void {
this._visitStatement(ast);
}
private _visitModifierArgument(ast: SolidityParser.AST): void {
const BUILTIN_MODIFIERS = ['public', 'view', 'payable', 'external', 'internal', 'pure'];
if (!_.includes(BUILTIN_MODIFIERS, ast.name)) {
this._visitStatement(ast);
}
}
private _visitBinaryBranch(
ast: SolidityParser.AST,
left: SolidityParser.AST,
right: SolidityParser.AST,
type: 'if' | 'cond-expr' | 'binary-expr',
): void {
this._branchMap[this._entryId++] = {
line: this._getExpressionRange(ast).start.line,
type,
locations: [this._getExpressionRange(left), this._getExpressionRange(right)],
};
}
private _visitStatement(ast: SolidityParser.AST): void {
this._statementMap[this._entryId++] = this._getExpressionRange(ast);
}
private _getExpressionRange(ast: SolidityParser.AST): SingleFileSourceRange {
const start = this._locationByOffset[ast.start - 1];
const end = this._locationByOffset[ast.end - 1];
const range = {
start,
end,
};
return range;
}
}

View File

@@ -0,0 +1,40 @@
import * as fs from 'fs';
import * as glob from 'glob';
import * as _ from 'lodash';
import * as path from 'path';
import { ContractData } from './types';
export const collectContractsData = (artifactsPath: string, sourcesPath: string, networkId: number) => {
const sourcesGlob = `${sourcesPath}/**/*.sol`;
const sourceFileNames = glob.sync(sourcesGlob, { absolute: true });
const contractsDataIfExists: Array<ContractData | {}> = _.map(sourceFileNames, sourceFileName => {
const baseName = path.basename(sourceFileName, '.sol');
const artifactFileName = path.join(artifactsPath, `${baseName}.json`);
if (!fs.existsSync(artifactFileName)) {
// If the contract isn't directly compiled, but is imported as the part of the other contract - we don't have an artifact for it and therefore can't do anything usefull with it
return {};
}
const artifact = JSON.parse(fs.readFileSync(artifactFileName).toString());
const sources = _.map(artifact.networks[networkId].sources, source => {
const includedFileName = glob.sync(`${sourcesPath}/**/${source}`, { absolute: true })[0];
return includedFileName;
});
const sourceCodes = _.map(sources, source => {
const includedSourceCode = fs.readFileSync(source).toString();
return includedSourceCode;
});
const contractData = {
baseName,
sourceCodes,
sources,
sourceMap: artifact.networks[networkId].source_map,
sourceMapRuntime: artifact.networks[networkId].source_map_runtime,
runtimeBytecode: artifact.networks[networkId].runtime_bytecode,
bytecode: artifact.networks[networkId].bytecode,
};
return contractData;
});
const contractsData = _.filter(contractsDataIfExists, contractData => !_.isEmpty(contractData)) as ContractData[];
return contractsData;
};

View File

@@ -0,0 +1,3 @@
export const constants = {
NEW_CONTRACT: 'NEW_CONTRACT',
};

View File

@@ -0,0 +1,166 @@
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();
}
}

View File

@@ -0,0 +1,124 @@
import { Callback, NextCallback, Subprovider } from '@0xproject/subproviders';
import { promisify } from '@0xproject/utils';
import * as _ from 'lodash';
import * as Web3 from 'web3';
import { constants } from './constants';
import { CoverageManager } from './coverage_manager';
/*
* This class implements the web3-provider-engine subprovider interface and collects traces of all transactions that were sent and all calls that were executed.
* Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js
*/
export class CoverageSubprovider extends Subprovider {
private _coverageManager: CoverageManager;
constructor(artifactsPath: string, sourcesPath: string, networkId: number) {
super();
this._coverageManager = new CoverageManager(
artifactsPath,
sourcesPath,
networkId,
this._getContractCodeAsync.bind(this),
);
}
public handleRequest(
payload: Web3.JSONRPCRequestPayload,
next: NextCallback,
end: (err: Error | null, result: any) => void,
) {
switch (payload.method) {
case 'eth_sendTransaction':
const txData = payload.params[0];
next(this._onTransactionSentAsync.bind(this, txData));
return;
case 'eth_call':
const callData = payload.params[0];
const blockNumber = payload.params[1];
next(this._onCallExecutedAsync.bind(this, callData, blockNumber));
return;
default:
next();
return;
}
}
public async writeCoverageAsync(): Promise<void> {
await this._coverageManager.writeCoverageAsync();
}
private async _onTransactionSentAsync(
txData: Web3.TxData,
err: Error | null,
txHash?: string,
cb?: Callback,
): Promise<void> {
if (_.isNull(err)) {
await this._recordTxTraceAsync(txData.to || constants.NEW_CONTRACT, txData.data, txHash as string);
} else {
const payload = {
method: 'eth_getBlockByNumber',
params: ['latest', true],
};
const jsonRPCResponsePayload = await this.emitPayloadAsync(payload);
const transactions = jsonRPCResponsePayload.result.transactions;
for (const transaction of transactions) {
await this._recordTxTraceAsync(
transaction.to || constants.NEW_CONTRACT,
transaction.data,
transaction.hash,
);
}
}
if (!_.isUndefined(cb)) {
cb();
}
}
private async _onCallExecutedAsync(
callData: Partial<Web3.CallData>,
blockNumber: Web3.BlockParam,
err: Error | null,
callResult: string,
cb: Callback,
): Promise<void> {
await this._recordCallTraceAsync(callData, blockNumber);
cb();
}
private async _recordTxTraceAsync(address: string, data: string | undefined, txHash: string): Promise<void> {
this._coverageManager.setTxDataByHash(txHash, data || '');
const payload = {
method: 'debug_traceTransaction',
params: [txHash, { disableMemory: true, disableStack: true, disableStorage: true }], // TODO For now testrpc just ignores those parameters https://github.com/trufflesuite/ganache-cli/issues/489
};
const jsonRPCResponsePayload = await this.emitPayloadAsync(payload);
const trace: Web3.TransactionTrace = jsonRPCResponsePayload.result;
const coveredPcs = _.map(trace.structLogs, log => log.pc);
this._coverageManager.appendTraceInfo(address, { coveredPcs, txHash });
}
private async _recordCallTraceAsync(callData: Partial<Web3.CallData>, blockNumber: Web3.BlockParam): Promise<void> {
const snapshotId = Number((await this.emitPayloadAsync({ method: 'evm_snapshot' })).result);
const txData = callData;
if (_.isUndefined(txData.from)) {
txData.from = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; // TODO
}
const txDataWithFromAddress = txData as Web3.TxData & { from: string };
try {
const txHash = (await this.emitPayloadAsync({
method: 'eth_sendTransaction',
params: [txDataWithFromAddress],
})).result;
await this._onTransactionSentAsync(txDataWithFromAddress, null, txHash);
} catch (err) {
await this._onTransactionSentAsync(txDataWithFromAddress, err, undefined);
}
const didRevert = (await this.emitPayloadAsync({ method: 'evm_revert', params: [snapshotId] })).result;
}
private async _getContractCodeAsync(address: string): Promise<string> {
const payload = {
method: 'eth_getCode',
params: [address, 'latest'],
};
const jsonRPCResponsePayload = await this.emitPayloadAsync(payload);
const contractCode: string = jsonRPCResponsePayload.result;
return contractCode;
}
}

6
packages/sol-cov/src/globals.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
// tslint:disable:completed-docs
declare module 'solidity-parser-sc' {
// This is too time-consuming to define and we don't rely on it anyway
export type AST = any;
export function parse(sourceCode: string): AST;
}

View File

@@ -0,0 +1 @@
export { CoverageSubprovider } from './coverage_subprovider';

View File

@@ -0,0 +1,24 @@
// tslint:disable:number-literal-format
const PUSH1 = 0x60;
const PUSH32 = 0x7f;
const isPush = (inst: number) => inst >= PUSH1 && inst <= PUSH32;
const pushDataLength = (inst: number) => inst - PUSH1 + 1;
const instructionLength = (inst: number) => (isPush(inst) ? pushDataLength(inst) + 1 : 1);
export const getPcToInstructionIndexMapping = (bytecode: Uint8Array) => {
const result: {
[programCounter: number]: number;
} = {};
let byteIndex = 0;
let instructionIndex = 0;
while (byteIndex < bytecode.length) {
const instruction = bytecode[byteIndex];
const length = instructionLength(instruction);
result[byteIndex] = instructionIndex;
byteIndex += length;
instructionIndex += 1;
}
return result;
};

View File

@@ -0,0 +1,16 @@
import * as fs from 'fs';
import * as _ from 'lodash';
import * as path from 'path';
import * as SolidityParser from 'solidity-parser-sc';
import { ASTVisitor, CoverageEntriesDescription } from './ast_visitor';
import { getLocationByOffset } from './source_maps';
export const collectCoverageEntries = (contractSource: string, fileName: string) => {
const ast = SolidityParser.parse(contractSource);
const locationByOffset = getLocationByOffset(contractSource);
const astVisitor = new ASTVisitor(locationByOffset);
astVisitor.walkAST(ast);
const coverageEntries = astVisitor.getCollectedCoverageEntries();
return coverageEntries;
};

View File

@@ -0,0 +1,77 @@
import * as _ from 'lodash';
import { getPcToInstructionIndexMapping } from './instructions';
import { LineColumn, LocationByOffset, SourceRange } from './types';
const RADIX = 10;
export interface SourceLocation {
offset: number;
length: number;
fileIndex: number;
}
export const getLocationByOffset = (str: string) => {
const locationByOffset: LocationByOffset = {};
let currentOffset = 0;
for (const char of str.split('')) {
const location = locationByOffset[currentOffset - 1] || { line: 1, column: 0 };
const isNewline = char === '\n';
locationByOffset[currentOffset] = {
line: location.line + (isNewline ? 1 : 0),
column: isNewline ? 0 : location.column + 1,
};
currentOffset++;
}
return locationByOffset;
};
// Parses a sourcemap string
// The solidity sourcemap format is documented here: https://github.com/ethereum/solidity/blob/develop/docs/miscellaneous.rst#source-mappings
export const parseSourceMap = (sourceCodes: string[], srcMap: string, bytecodeHex: string, sources: string[]) => {
const bytecode = Uint8Array.from(Buffer.from(bytecodeHex, 'hex'));
const pcToInstructionIndex: { [programCounter: number]: number } = getPcToInstructionIndexMapping(bytecode);
const locationByOffsetByFileIndex = _.map(sourceCodes, getLocationByOffset);
const entries = srcMap.split(';');
const parsedEntries: SourceLocation[] = [];
let lastParsedEntry: SourceLocation = {} as any;
const instructionIndexToSourceRange: { [instructionIndex: number]: SourceRange } = {};
_.each(entries, (entry: string, i: number) => {
const [instructionIndexStrIfExists, lengthStrIfExists, fileIndexStrIfExists, jumpTypeStrIfExists] = entry.split(
':',
);
const instructionIndexIfExists = parseInt(instructionIndexStrIfExists, RADIX);
const lengthIfExists = parseInt(lengthStrIfExists, RADIX);
const fileIndexIfExists = parseInt(fileIndexStrIfExists, RADIX);
const offset = _.isNaN(instructionIndexIfExists) ? lastParsedEntry.offset : instructionIndexIfExists;
const length = _.isNaN(lengthIfExists) ? lastParsedEntry.length : lengthIfExists;
const fileIndex = _.isNaN(fileIndexIfExists) ? lastParsedEntry.fileIndex : fileIndexIfExists;
const parsedEntry = {
offset,
length,
fileIndex,
};
if (parsedEntry.fileIndex !== -1) {
const sourceRange = {
location: {
start: locationByOffsetByFileIndex[parsedEntry.fileIndex][parsedEntry.offset - 1],
end:
locationByOffsetByFileIndex[parsedEntry.fileIndex][parsedEntry.offset + parsedEntry.length - 1],
},
fileName: sources[parsedEntry.fileIndex],
};
instructionIndexToSourceRange[i] = sourceRange;
} else {
// Some assembly code generated by Solidity can't be mapped back to a line of source code.
// Source: https://github.com/ethereum/solidity/issues/3629
}
lastParsedEntry = parsedEntry;
});
const pcsToSourceRange: { [programCounter: number]: SourceRange } = {};
for (const programCounterKey of _.keys(pcToInstructionIndex)) {
const pc = parseInt(programCounterKey, RADIX);
const instructionIndex: number = pcToInstructionIndex[pc];
pcsToSourceRange[pc] = instructionIndexToSourceRange[instructionIndex];
}
return pcsToSourceRange;
};

View File

@@ -0,0 +1,89 @@
export interface LineColumn {
line: number;
column: number;
}
export interface SourceRange {
location: SingleFileSourceRange;
fileName: string;
}
export interface SingleFileSourceRange {
start: LineColumn;
end: LineColumn;
}
export interface LocationByOffset {
[offset: number]: LineColumn;
}
export interface FunctionDescription {
name: string;
line: number;
loc: SingleFileSourceRange;
skip?: boolean;
}
export type StatementDescription = SingleFileSourceRange;
export interface BranchDescription {
line: number;
type: 'if' | 'switch' | 'cond-expr' | 'binary-expr';
locations: SingleFileSourceRange[];
}
export interface FnMap {
[functionId: string]: FunctionDescription;
}
export interface BranchMap {
[branchId: string]: BranchDescription;
}
export interface StatementMap {
[statementId: string]: StatementDescription;
}
export interface LineCoverage {
[lineNo: number]: boolean;
}
export interface FunctionCoverage {
[functionId: string]: boolean;
}
export interface StatementCoverage {
[statementId: string]: boolean;
}
export interface BranchCoverage {
[branchId: string]: boolean[];
}
export interface Coverage {
[fineName: string]: {
l: LineCoverage;
f: FunctionCoverage;
s: StatementCoverage;
b: BranchCoverage;
fnMap: FnMap;
branchMap: BranchMap;
statementMap: StatementMap;
path: string;
};
}
export interface ContractData {
bytecode: string;
sourceMap: string;
runtimeBytecode: string;
sourceMapRuntime: string;
sourceCodes: string[];
baseName: string;
sources: string[];
}
export interface TraceInfo {
coveredPcs: number[];
txHash: string;
}

View File

@@ -0,0 +1,13 @@
import { LineColumn, SingleFileSourceRange } from './types';
export const utils = {
compareLineColumn(lhs: LineColumn, rhs: LineColumn): number {
return lhs.line !== rhs.line ? lhs.line - rhs.line : lhs.column - rhs.column;
},
isRangeInside(childRange: SingleFileSourceRange, parentRange: SingleFileSourceRange): boolean {
return (
utils.compareLineColumn(parentRange.start, childRange.start) <= 0 &&
utils.compareLineColumn(childRange.end, parentRange.end) <= 0
);
},
};

View File

@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "lib"
},
"include": ["./src/**/*", "../../node_modules/web3-typescript-typings/index.d.ts"]
}

View File

@@ -0,0 +1,3 @@
{
"extends": ["@0xproject/tslint-config"]
}