Add sol-cover implementation
This commit is contained in:
5
packages/sol-cov/.npmignore
Normal file
5
packages/sol-cov/.npmignore
Normal file
@@ -0,0 +1,5 @@
|
||||
.*
|
||||
yarn-error.log
|
||||
/src/
|
||||
/scripts/
|
||||
tsconfig.json
|
||||
5
packages/sol-cov/CHANGELOG.md
Normal file
5
packages/sol-cov/CHANGELOG.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.0.1 - _TBD, 2018_
|
||||
|
||||
* Initial implementation (#TBD)
|
||||
1
packages/sol-cov/README.md
Normal file
1
packages/sol-cov/README.md
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
42
packages/sol-cov/package.json
Normal file
42
packages/sol-cov/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
5
packages/sol-cov/scripts/postpublish.js
Normal file
5
packages/sol-cov/scripts/postpublish.js
Normal 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);
|
||||
115
packages/sol-cov/src/ast_visitor.ts
Normal file
115
packages/sol-cov/src/ast_visitor.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
40
packages/sol-cov/src/collect_contract_data.ts
Normal file
40
packages/sol-cov/src/collect_contract_data.ts
Normal 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;
|
||||
};
|
||||
3
packages/sol-cov/src/constants.ts
Normal file
3
packages/sol-cov/src/constants.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const constants = {
|
||||
NEW_CONTRACT: 'NEW_CONTRACT',
|
||||
};
|
||||
166
packages/sol-cov/src/coverage_manager.ts
Normal file
166
packages/sol-cov/src/coverage_manager.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
124
packages/sol-cov/src/coverage_subprovider.ts
Normal file
124
packages/sol-cov/src/coverage_subprovider.ts
Normal 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
6
packages/sol-cov/src/globals.d.ts
vendored
Normal 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;
|
||||
}
|
||||
1
packages/sol-cov/src/index.ts
Normal file
1
packages/sol-cov/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { CoverageSubprovider } from './coverage_subprovider';
|
||||
24
packages/sol-cov/src/instructions.ts
Normal file
24
packages/sol-cov/src/instructions.ts
Normal 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;
|
||||
};
|
||||
16
packages/sol-cov/src/instrument_solidity.ts
Normal file
16
packages/sol-cov/src/instrument_solidity.ts
Normal 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;
|
||||
};
|
||||
77
packages/sol-cov/src/source_maps.ts
Normal file
77
packages/sol-cov/src/source_maps.ts
Normal 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;
|
||||
};
|
||||
89
packages/sol-cov/src/types.ts
Normal file
89
packages/sol-cov/src/types.ts
Normal 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;
|
||||
}
|
||||
13
packages/sol-cov/src/utils.ts
Normal file
13
packages/sol-cov/src/utils.ts
Normal 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
|
||||
);
|
||||
},
|
||||
};
|
||||
7
packages/sol-cov/tsconfig.json
Normal file
7
packages/sol-cov/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": ["./src/**/*", "../../node_modules/web3-typescript-typings/index.d.ts"]
|
||||
}
|
||||
3
packages/sol-cov/tslint.json
Normal file
3
packages/sol-cov/tslint.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["@0xproject/tslint-config"]
|
||||
}
|
||||
Reference in New Issue
Block a user