aboutsummaryrefslogtreecommitdiffstats
path: root/packages/sol-cov
diff options
context:
space:
mode:
authorJacob Evans <dekz@dekz.net>2018-06-18 19:50:35 +0800
committerGitHub <noreply@github.com>2018-06-18 19:50:35 +0800
commit190eafc30e2e444ed15b76217a6162ec04b33f73 (patch)
treeb20cbad73ff7a069dc0f0ef43ebc0373c714ad02 /packages/sol-cov
parentd4ee0e862297c16f8ee62efccd31f1193052c64e (diff)
parent0c238448fda99c4d7997901d0fe4d72cb06b79cc (diff)
downloaddexon-sol-tools-190eafc30e2e444ed15b76217a6162ec04b33f73.tar
dexon-sol-tools-190eafc30e2e444ed15b76217a6162ec04b33f73.tar.gz
dexon-sol-tools-190eafc30e2e444ed15b76217a6162ec04b33f73.tar.bz2
dexon-sol-tools-190eafc30e2e444ed15b76217a6162ec04b33f73.tar.lz
dexon-sol-tools-190eafc30e2e444ed15b76217a6162ec04b33f73.tar.xz
dexon-sol-tools-190eafc30e2e444ed15b76217a6162ec04b33f73.tar.zst
dexon-sol-tools-190eafc30e2e444ed15b76217a6162ec04b33f73.zip
Merge branch 'v2-prototype' into bug/contracts/eip712-191-prefix
Diffstat (limited to 'packages/sol-cov')
-rw-r--r--packages/sol-cov/CHANGELOG.json53
-rw-r--r--packages/sol-cov/package.json15
-rw-r--r--packages/sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter.ts17
-rw-r--r--packages/sol-cov/src/artifact_adapters/truffle_artifact_adapter.ts4
-rw-r--r--packages/sol-cov/src/ast_visitor.ts3
-rw-r--r--packages/sol-cov/src/collect_coverage_entries.ts2
-rw-r--r--packages/sol-cov/src/coverage_manager.ts218
-rw-r--r--packages/sol-cov/src/coverage_subprovider.ts293
-rw-r--r--packages/sol-cov/src/index.ts3
-rw-r--r--packages/sol-cov/src/profiler_subprovider.ts91
-rw-r--r--packages/sol-cov/src/revert_trace.ts90
-rw-r--r--packages/sol-cov/src/revert_trace_subprovider.ts114
-rw-r--r--packages/sol-cov/src/source_maps.ts2
-rw-r--r--packages/sol-cov/src/trace.ts68
-rw-r--r--packages/sol-cov/src/trace_collection_subprovider.ts185
-rw-r--r--packages/sol-cov/src/trace_collector.ts93
-rw-r--r--packages/sol-cov/src/trace_info_subprovider.ts59
-rw-r--r--packages/sol-cov/src/types.ts24
-rw-r--r--packages/sol-cov/src/utils.ts54
-rw-r--r--packages/sol-cov/test/instructions_test.ts2
-rw-r--r--packages/sol-cov/test/trace_test.ts2
21 files changed, 930 insertions, 462 deletions
diff --git a/packages/sol-cov/CHANGELOG.json b/packages/sol-cov/CHANGELOG.json
index 0d3303231..7e934ad6e 100644
--- a/packages/sol-cov/CHANGELOG.json
+++ b/packages/sol-cov/CHANGELOG.json
@@ -1,5 +1,58 @@
[
{
+ "version": "0.2.0",
+ "changes": [
+ {
+ "note": "Fixed a bug causing RegExp to crash if contract code is longer that 32767 characters",
+ "pr": 675
+ },
+ {
+ "note": "Fixed a bug caused by Geth debug trace depth being 1-indexed",
+ "pr": 675
+ },
+ {
+ "note": "Fixed a bug when the tool crashed on empty traces",
+ "pr": 675
+ },
+ {
+ "note": "Use `BlockchainLifecycle` to support reverts on Geth",
+ "pr": 675
+ },
+ {
+ "note": "Add `ProfilerSubprovider` as a hacky way to profile code using coverage tools",
+ "pr": 675
+ },
+ {
+ "note": "Collect traces from `estimate_gas` calls",
+ "pr": 675
+ },
+ {
+ "note": "Fix a race condition caused by not awaiting the transaction before getting a trace",
+ "pr": 675
+ },
+ {
+ "note": "Add `start`/`stop` functionality to `CoverageSubprovider` and `ProfilerSubprovider`",
+ "pr": 675
+ },
+ {
+ "note": "Skip interface artifacts with a warning instead of failing",
+ "pr": 675
+ },
+ {
+ "note": "Fix solcVersion regex in parameter validation",
+ "pr": 690
+ },
+ {
+ "note": "Fix a bug when in TruffleArtifactsAdapter causing it to throw if compiler.json is not there",
+ "pr": 690
+ },
+ {
+ "note": "HUGE perf improvements",
+ "pr": 690
+ }
+ ]
+ },
+ {
"timestamp": 1527009134,
"version": "0.1.0",
"changes": [
diff --git a/packages/sol-cov/package.json b/packages/sol-cov/package.json
index 4d92a610b..1d9d7f94d 100644
--- a/packages/sol-cov/package.json
+++ b/packages/sol-cov/package.json
@@ -8,15 +8,18 @@
"main": "lib/src/index.js",
"types": "lib/src/index.d.ts",
"scripts": {
- "watch": "tsc -w",
+ "watch_without_deps": "yarn pre_build && tsc -w",
+ "build": "yarn pre_build && tsc && copyfiles -u 3 './lib/src/monorepo_scripts/**/*' ./scripts",
+ "pre_build": "run-s copy_test_fixtures",
"lint": "tslint --project .",
- "test": "run-s clean build compile_test run_mocha",
+ "test": "run-s compile_test run_mocha",
+ "rebuild_and_test": "run-s clean build test",
"test:coverage": "nyc npm run test --all && yarn coverage:report:lcov",
"coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info",
"test:circleci": "yarn test:coverage",
- "run_mocha": "mocha lib/test/**/*_test.js --exit",
+ "run_mocha": "mocha --require source-map-support/register lib/test/**/*_test.js --exit",
"clean": "shx rm -rf lib scripts test/fixtures/artifacts src/artifacts",
- "build": "copyfiles 'test/fixtures/**/*' ./lib && tsc && copyfiles -u 3 './lib/src/monorepo_scripts/**/*' ./scripts",
+ "copy_test_fixtures": "copyfiles 'test/fixtures/**/*' ./lib",
"compile_test": "sol-compiler compile",
"manual:postpublish": "yarn build; node ./scripts/postpublish.js",
"docs:stage": "node scripts/stage_docs.js",
@@ -51,6 +54,8 @@
"@0xproject/subproviders": "^0.10.2",
"@0xproject/typescript-typings": "^0.3.2",
"@0xproject/utils": "^0.6.2",
+ "@0xproject/web3-wrapper": "^0.6.4",
+ "@0xproject/dev-utils": "^0.4.2",
"ethereum-types": "^0.0.1",
"ethereumjs-util": "^5.1.1",
"glob": "^7.1.2",
@@ -60,7 +65,7 @@
"mkdirp": "^0.5.1",
"rimraf": "^2.6.2",
"semaphore-async-await": "^1.5.1",
- "solidity-parser-antlr": "^0.2.11"
+ "solidity-parser-antlr": "^0.2.12"
},
"devDependencies": {
"@0xproject/monorepo-scripts": "^0.1.20",
diff --git a/packages/sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter.ts b/packages/sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter.ts
index d08828bf6..220a9f98c 100644
--- a/packages/sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter.ts
+++ b/packages/sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter.ts
@@ -1,3 +1,5 @@
+import { CompilerOptions, ContractArtifact } from '@0xproject/sol-compiler';
+import { logUtils } from '@0xproject/utils';
import * as fs from 'fs';
import * as glob from 'glob';
import * as _ from 'lodash';
@@ -14,25 +16,30 @@ export class SolCompilerArtifactAdapter extends AbstractArtifactAdapter {
private _sourcesPath: string;
constructor(artifactsPath?: string, sourcesPath?: string) {
super();
- const config = JSON.parse(fs.readFileSync(CONFIG_FILE).toString());
+ const config: CompilerOptions = fs.existsSync(CONFIG_FILE)
+ ? JSON.parse(fs.readFileSync(CONFIG_FILE).toString())
+ : {};
if (_.isUndefined(artifactsPath) && _.isUndefined(config.artifactsDir)) {
throw new Error(`artifactsDir not found in ${CONFIG_FILE}`);
}
- this._artifactsPath = config.artifactsDir;
+ this._artifactsPath = (artifactsPath || config.artifactsDir) as string;
if (_.isUndefined(sourcesPath) && _.isUndefined(config.contractsDir)) {
throw new Error(`contractsDir not found in ${CONFIG_FILE}`);
}
- this._sourcesPath = config.contractsDir;
+ this._sourcesPath = (sourcesPath || config.contractsDir) as string;
}
public async collectContractsDataAsync(): Promise<ContractData[]> {
const artifactsGlob = `${this._artifactsPath}/**/*.json`;
const artifactFileNames = glob.sync(artifactsGlob, { absolute: true });
const contractsData: ContractData[] = [];
for (const artifactFileName of artifactFileNames) {
- const artifact = JSON.parse(fs.readFileSync(artifactFileName).toString());
+ const artifact: ContractArtifact = JSON.parse(fs.readFileSync(artifactFileName).toString());
+ if (_.isUndefined(artifact.compilerOutput.evm)) {
+ logUtils.warn(`${artifactFileName} doesn't contain bytecode. Skipping...`);
+ continue;
+ }
let sources = _.keys(artifact.sources);
sources = _.map(sources, relativeFilePath => path.resolve(this._sourcesPath, relativeFilePath));
- const contractName = artifact.contractName;
const sourceCodes = _.map(sources, (source: string) => fs.readFileSync(source).toString());
const contractData = {
sourceCodes,
diff --git a/packages/sol-cov/src/artifact_adapters/truffle_artifact_adapter.ts b/packages/sol-cov/src/artifact_adapters/truffle_artifact_adapter.ts
index c7f21b6eb..53449e5e1 100644
--- a/packages/sol-cov/src/artifact_adapters/truffle_artifact_adapter.ts
+++ b/packages/sol-cov/src/artifact_adapters/truffle_artifact_adapter.ts
@@ -1,8 +1,4 @@
import { Compiler, CompilerOptions } from '@0xproject/sol-compiler';
-import * as fs from 'fs';
-import * as glob from 'glob';
-import * as _ from 'lodash';
-import * as path from 'path';
import * as rimraf from 'rimraf';
import { ContractData } from '../types';
diff --git a/packages/sol-cov/src/ast_visitor.ts b/packages/sol-cov/src/ast_visitor.ts
index 88309d3eb..16984b5ec 100644
--- a/packages/sol-cov/src/ast_visitor.ts
+++ b/packages/sol-cov/src/ast_visitor.ts
@@ -57,6 +57,9 @@ export class ASTVisitor {
public ContinueStatement(ast: Parser.ContinueStatement): void {
this._visitStatement(ast);
}
+ public EmitStatement(ast: any /* TODO: Parser.EmitStatement */): void {
+ this._visitStatement(ast);
+ }
public VariableDeclarationStatement(ast: Parser.VariableDeclarationStatement): void {
this._visitStatement(ast);
}
diff --git a/packages/sol-cov/src/collect_coverage_entries.ts b/packages/sol-cov/src/collect_coverage_entries.ts
index 97218616c..b145f044e 100644
--- a/packages/sol-cov/src/collect_coverage_entries.ts
+++ b/packages/sol-cov/src/collect_coverage_entries.ts
@@ -1,7 +1,5 @@
import * as ethUtil from 'ethereumjs-util';
-import * as fs from 'fs';
import * as _ from 'lodash';
-import * as path from 'path';
import * as parser from 'solidity-parser-antlr';
import { ASTVisitor, CoverageEntriesDescription } from './ast_visitor';
diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts
deleted file mode 100644
index 31b0e6fbc..000000000
--- a/packages/sol-cov/src/coverage_manager.ts
+++ /dev/null
@@ -1,218 +0,0 @@
-import { promisify } from '@0xproject/utils';
-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 { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter';
-import { collectCoverageEntries } from './collect_coverage_entries';
-import { constants } from './constants';
-import { parseSourceMap } from './source_maps';
-import {
- BranchCoverage,
- BranchDescription,
- BranchMap,
- ContractData,
- Coverage,
- FnMap,
- FunctionCoverage,
- FunctionDescription,
- LineColumn,
- SingleFileSourceRange,
- SourceRange,
- StatementCoverage,
- StatementDescription,
- StatementMap,
- TraceInfo,
- TraceInfoExistingContract,
- TraceInfoNewContract,
-} from './types';
-import { utils } from './utils';
-
-const mkdirpAsync = promisify<undefined>(mkdirp);
-
-export class CoverageManager {
- private _artifactAdapter: AbstractArtifactAdapter;
- private _logger: Logger;
- private _traceInfos: TraceInfo[] = [];
- private _getContractCodeAsync: (address: string) => Promise<string>;
- private static _getSingleFileCoverageForTrace(
- contractData: ContractData,
- coveredPcs: number[],
- pcToSourceRange: { [programCounter: number]: SourceRange },
- fileIndex: number,
- ): Coverage {
- 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 === absoluteFileName);
- const branchCoverage: BranchCoverage = {};
- const branchIds = _.keys(coverageEntriesDescription.branchMap);
- for (const branchId of branchIds) {
- const branchDescription = coverageEntriesDescription.branchMap[branchId];
- const isCoveredByBranchIndex = _.map(branchDescription.locations, location =>
- _.some(sourceRanges, range => utils.isRangeInside(range.location, location)),
- );
- branchCoverage[branchId] = isCoveredByBranchIndex;
- }
- const statementCoverage: StatementCoverage = {};
- const statementIds = _.keys(coverageEntriesDescription.statementMap);
- for (const statementId of statementIds) {
- const statementDescription = coverageEntriesDescription.statementMap[statementId];
- const isCovered = _.some(sourceRanges, range => utils.isRangeInside(range.location, statementDescription));
- statementCoverage[statementId] = isCovered;
- }
- const functionCoverage: FunctionCoverage = {};
- const functionIds = _.keys(coverageEntriesDescription.fnMap);
- for (const fnId of functionIds) {
- const functionDescription = coverageEntriesDescription.fnMap[fnId];
- const isCovered = _.some(sourceRanges, range =>
- utils.isRangeInside(range.location, functionDescription.loc),
- );
- functionCoverage[fnId] = isCovered;
- }
- // 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;
- },
- );
- statementCoverage[modifierStatementId] = isModifierCovered;
- }
- const partialCoverage = {
- [absoluteFileName]: {
- ...coverageEntriesDescription,
- l: {}, // It's able to derive it from statement coverage
- path: absoluteFileName,
- f: functionCoverage,
- s: statementCoverage,
- b: branchCoverage,
- },
- };
- 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
- const runtimeBytecode = (traceInfo as TraceInfoExistingContract).runtimeBytecode;
- const contractData = CoverageManager._getContractDataIfExists(contractsData, runtimeBytecode);
- if (_.isUndefined(contractData)) {
- this._logger.warn(`Transaction to an unknown address: ${traceInfo.address}`);
- continue;
- }
- const bytecodeHex = stripHexPrefix(runtimeBytecode);
- const sourceMap = contractData.sourceMapRuntime;
- const pcToSourceRange = parseSourceMap(
- contractData.sourceCodes,
- sourceMap,
- bytecodeHex,
- contractData.sources,
- );
- for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
- const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace(
- contractData,
- traceInfo.coveredPcs,
- pcToSourceRange,
- fileIndex,
- );
- collector.add(singleFileCoverageForTrace);
- }
- } else {
- // Contract creation transaction
- const bytecode = (traceInfo as TraceInfoNewContract).bytecode;
- const contractData = CoverageManager._getContractDataIfExists(contractsData, bytecode);
- if (_.isUndefined(contractData)) {
- this._logger.warn(`Unknown contract creation transaction`);
- continue;
- }
- const bytecodeHex = stripHexPrefix(bytecode);
- const sourceMap = contractData.sourceMap;
- const pcToSourceRange = parseSourceMap(
- contractData.sourceCodes,
- sourceMap,
- bytecodeHex,
- contractData.sources,
- );
- for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
- const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace(
- contractData,
- traceInfo.coveredPcs,
- pcToSourceRange,
- fileIndex,
- );
- collector.add(singleFileCoverageForTrace);
- }
- }
- }
- return collector.getFinalCoverage();
- }
-}
diff --git a/packages/sol-cov/src/coverage_subprovider.ts b/packages/sol-cov/src/coverage_subprovider.ts
index ca7f4aed2..065a48434 100644
--- a/packages/sol-cov/src/coverage_subprovider.ts
+++ b/packages/sol-cov/src/coverage_subprovider.ts
@@ -1,32 +1,29 @@
-import { Callback, ErrorCallback, NextCallback, Subprovider } from '@0xproject/subproviders';
-import { BlockParam, CallData, JSONRPCRequestPayload, TransactionTrace, TxData } from 'ethereum-types';
-import * as fs from 'fs';
import * as _ from 'lodash';
-import { Lock } from 'semaphore-async-await';
import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter';
-import { constants } from './constants';
-import { CoverageManager } from './coverage_manager';
-import { getTracesByContractAddress } from './trace';
-import { BlockParamLiteral, TraceInfoExistingContract, TraceInfoNewContract } from './types';
-
-interface MaybeFakeTxData extends TxData {
- isFakeTransaction?: boolean;
-}
-
-// Because there is no notion of a call trace in the Ethereum rpc - we collect them in a rather non-obvious/hacky way.
-// On each call - we create a snapshot, execute the call as a transaction, get the trace, revert the snapshot.
-// That allows us to avoid influencing test behaviour.
+import { collectCoverageEntries } from './collect_coverage_entries';
+import { SingleFileSubtraceHandler, TraceCollector } from './trace_collector';
+import { TraceInfoSubprovider } from './trace_info_subprovider';
+import {
+ BranchCoverage,
+ ContractData,
+ Coverage,
+ FunctionCoverage,
+ FunctionDescription,
+ SourceRange,
+ StatementCoverage,
+ StatementDescription,
+ Subtrace,
+ TraceInfo,
+} from './types';
+import { utils } from './utils';
/**
* This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface.
- * It collects traces of all transactions that were sent and all calls that were executed through JSON RPC.
+ * It's used to compute your code coverage while running solidity tests.
*/
-export class CoverageSubprovider extends Subprovider {
- // Lock is used to not accept normal transactions while doing call/snapshot magic because they'll be reverted later otherwise
- private _lock: Lock;
- private _coverageManager: CoverageManager;
- private _defaultFromAddress: string;
+export class CoverageSubprovider extends TraceInfoSubprovider {
+ private _coverageCollector: TraceCollector;
/**
* Instantiates a CoverageSubprovider instance
* @param artifactAdapter Adapter for used artifacts format (0x, truffle, giveth, etc.)
@@ -34,172 +31,110 @@ export class CoverageSubprovider extends Subprovider {
* @param isVerbose If true, we will log any unknown transactions. Otherwise we will ignore them
*/
constructor(artifactAdapter: AbstractArtifactAdapter, defaultFromAddress: string, isVerbose: boolean = true) {
- super();
- this._lock = new Lock();
- this._defaultFromAddress = defaultFromAddress;
- this._coverageManager = new CoverageManager(artifactAdapter, this._getContractCodeAsync.bind(this), isVerbose);
+ 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._coverageManager.writeCoverageAsync();
+ await this._coverageCollector.writeOutputAsync();
}
- /**
- * This method conforms to the web3-provider-engine interface.
- * It is called internally by the ProviderEngine when it is this subproviders
- * turn to handle a JSON RPC request.
- * @param payload JSON RPC payload
- * @param next Callback to call if this subprovider decides not to handle the request
- * @param end Callback to call if subprovider handled the request and wants to pass back the request.
- */
- // tslint:disable-next-line:prefer-function-over-method async-suffix
- public async handleRequest(payload: JSONRPCRequestPayload, next: NextCallback, end: ErrorCallback): Promise<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;
- }
- }
- private async _onTransactionSentAsync(
- txData: MaybeFakeTxData,
- err: Error | null,
- txHash: string | undefined,
- cb: Callback,
- ): Promise<void> {
- if (!txData.isFakeTransaction) {
- // This transaction is a usual ttransaction. Not a call executed as one.
- // And we don't want it to be executed within a snapshotting period
- await this._lock.acquire();
- }
- if (_.isNull(err)) {
- const toAddress = _.isUndefined(txData.to) || txData.to === '0x0' ? constants.NEW_CONTRACT : txData.to;
- await this._recordTxTraceAsync(toAddress, txData.data, txHash as string);
- } else {
- const payload = {
- method: 'eth_getBlockByNumber',
- params: [BlockParamLiteral.Latest, true],
- };
- const jsonRPCResponsePayload = await this.emitPayloadAsync(payload);
- const transactions = jsonRPCResponsePayload.result.transactions;
- for (const transaction of transactions) {
- const toAddress = _.isUndefined(txData.to) || txData.to === '0x0' ? constants.NEW_CONTRACT : txData.to;
- await this._recordTxTraceAsync(toAddress, transaction.data, transaction.hash);
- }
- }
- if (!txData.isFakeTransaction) {
- // This transaction is a usual ttransaction. Not a call executed as one.
- // And we don't want it to be executed within a snapshotting period
- this._lock.release();
- }
- cb();
+/**
+ * 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]);
+ 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 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 === absoluteFileName);
+ const branchCoverage: BranchCoverage = {};
+ const branchIds = _.keys(coverageEntriesDescription.branchMap);
+ for (const branchId of branchIds) {
+ const branchDescription = coverageEntriesDescription.branchMap[branchId];
+ const isBranchCoveredByBranchIndex = _.map(branchDescription.locations, location => {
+ const isBranchCovered = _.some(sourceRanges, range => utils.isRangeInside(range.location, location));
+ const timesBranchCovered = Number(isBranchCovered);
+ return timesBranchCovered;
+ });
+ branchCoverage[branchId] = isBranchCoveredByBranchIndex;
}
- private async _onCallExecutedAsync(
- callData: Partial<CallData>,
- blockNumber: BlockParam,
- err: Error | null,
- callResult: string,
- cb: Callback,
- ): Promise<void> {
- await this._recordCallTraceAsync(callData, blockNumber);
- cb();
+ 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;
}
- private async _recordTxTraceAsync(address: string, data: string | undefined, txHash: string): Promise<void> {
- let payload = {
- method: 'debug_traceTransaction',
- params: [txHash, { disableMemory: true, disableStack: false, disableStorage: true }],
- };
- let jsonRPCResponsePayload = await this.emitPayloadAsync(payload);
- const trace: TransactionTrace = jsonRPCResponsePayload.result;
- const tracesByContractAddress = getTracesByContractAddress(trace.structLogs, address);
- const subcallAddresses = _.keys(tracesByContractAddress);
- if (address === constants.NEW_CONTRACT) {
- for (const subcallAddress of subcallAddresses) {
- let traceInfo: TraceInfoNewContract | TraceInfoExistingContract;
- if (subcallAddress === 'NEW_CONTRACT') {
- const traceForThatSubcall = tracesByContractAddress[subcallAddress];
- const coveredPcs = _.map(traceForThatSubcall, log => log.pc);
- traceInfo = {
- coveredPcs,
- txHash,
- address: constants.NEW_CONTRACT,
- bytecode: data as string,
- };
- } else {
- payload = { method: 'eth_getCode', params: [subcallAddress, BlockParamLiteral.Latest] };
- jsonRPCResponsePayload = await this.emitPayloadAsync(payload);
- const runtimeBytecode = jsonRPCResponsePayload.result;
- const traceForThatSubcall = tracesByContractAddress[subcallAddress];
- const coveredPcs = _.map(traceForThatSubcall, log => log.pc);
- traceInfo = {
- coveredPcs,
- txHash,
- address: subcallAddress,
- runtimeBytecode,
- };
- }
- this._coverageManager.appendTraceInfo(traceInfo);
- }
- } else {
- for (const subcallAddress of subcallAddresses) {
- payload = { method: 'eth_getCode', params: [subcallAddress, BlockParamLiteral.Latest] };
- jsonRPCResponsePayload = await this.emitPayloadAsync(payload);
- const runtimeBytecode = jsonRPCResponsePayload.result;
- const traceForThatSubcall = tracesByContractAddress[subcallAddress];
- const coveredPcs = _.map(traceForThatSubcall, log => log.pc);
- const traceInfo: TraceInfoExistingContract = {
- coveredPcs,
- txHash,
- address: subcallAddress,
- runtimeBytecode,
- };
- this._coverageManager.appendTraceInfo(traceInfo);
- }
- }
+ 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;
}
- private async _recordCallTraceAsync(callData: Partial<CallData>, blockNumber: BlockParam): Promise<void> {
- // We don't want other transactions to be exeucted during snashotting period, that's why we lock the
- // transaction execution for all transactions except our fake ones.
- await this._lock.acquire();
- const snapshotId = Number((await this.emitPayloadAsync({ method: 'evm_snapshot' })).result);
- const fakeTxData: MaybeFakeTxData = {
- isFakeTransaction: true, // This transaction (and only it) is allowed to come through when the lock is locked
- ...callData,
- from: callData.from || this._defaultFromAddress,
- };
- try {
- await this.emitPayloadAsync({
- method: 'eth_sendTransaction',
- params: [fakeTxData],
- });
- } catch (err) {
- // Even if this transaction failed - we've already recorded it's trace.
- }
- const jsonRPCResponse = await this.emitPayloadAsync({ method: 'evm_revert', params: [snapshotId] });
- this._lock.release();
- const didRevert = jsonRPCResponse.result;
- if (!didRevert) {
- throw new Error('Failed to revert the snapshot');
+ // 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;
}
- private async _getContractCodeAsync(address: string): Promise<string> {
- const payload = {
- method: 'eth_getCode',
- params: [address, BlockParamLiteral.Latest],
- };
- const jsonRPCResponsePayload = await this.emitPayloadAsync(payload);
- const contractCode: string = jsonRPCResponsePayload.result;
- return contractCode;
- }
-}
+ const partialCoverage = {
+ [absoluteFileName]: {
+ ...coverageEntriesDescription,
+ path: absoluteFileName,
+ f: functionCoverage,
+ s: statementCoverage,
+ b: branchCoverage,
+ },
+ };
+ return partialCoverage;
+};
diff --git a/packages/sol-cov/src/index.ts b/packages/sol-cov/src/index.ts
index 7a2afbe80..003a27374 100644
--- a/packages/sol-cov/src/index.ts
+++ b/packages/sol-cov/src/index.ts
@@ -1,4 +1,7 @@
export { CoverageSubprovider } from './coverage_subprovider';
+// HACK: ProfilerSubprovider is a hacky way to do profiling using coverage tools. Not production ready
+export { ProfilerSubprovider } from './profiler_subprovider';
+export { RevertTraceSubprovider } from './revert_trace_subprovider';
export { SolCompilerArtifactAdapter } from './artifact_adapters/sol_compiler_artifact_adapter';
export { TruffleArtifactAdapter } from './artifact_adapters/truffle_artifact_adapter';
export { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter';
diff --git a/packages/sol-cov/src/profiler_subprovider.ts b/packages/sol-cov/src/profiler_subprovider.ts
new file mode 100644
index 000000000..9f98da524
--- /dev/null
+++ b/packages/sol-cov/src/profiler_subprovider.ts
@@ -0,0 +1,91 @@
+import * as _ from 'lodash';
+
+import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter';
+import { collectCoverageEntries } from './collect_coverage_entries';
+import { SingleFileSubtraceHandler, TraceCollector } from './trace_collector';
+import { TraceInfoSubprovider } from './trace_info_subprovider';
+import { ContractData, Coverage, SourceRange, Subtrace, TraceInfo } from './types';
+import { utils } from './utils';
+
+/**
+ * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface.
+ * ProfilerSubprovider is used to profile Solidity code while running tests.
+ */
+export class ProfilerSubprovider extends TraceInfoSubprovider {
+ private _profilerCollector: TraceCollector;
+ /**
+ * Instantiates a ProfilerSubprovider 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: false,
+ shouldCollectCallTraces: false,
+ };
+ super(defaultFromAddress, traceCollectionSubproviderConfig);
+ this._profilerCollector = new TraceCollector(artifactAdapter, isVerbose, profilerHandler);
+ }
+ protected async _handleTraceInfoAsync(traceInfo: TraceInfo): Promise<void> {
+ await this._profilerCollector.computeSingleTraceCoverageAsync(traceInfo);
+ }
+ /**
+ * Write the test profiler results to a file in Istanbul format.
+ */
+ public async writeProfilerOutputAsync(): Promise<void> {
+ await this._profilerCollector.writeOutputAsync();
+ }
+}
+
+/**
+ * Computed partial coverage for a single file & subtrace for the purposes of
+ * gas profiling.
+ * @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 profilerHandler: SingleFileSubtraceHandler = (
+ contractData: ContractData,
+ subtrace: Subtrace,
+ pcToSourceRange: { [programCounter: number]: SourceRange },
+ fileIndex: number,
+): Coverage => {
+ const absoluteFileName = contractData.sources[fileIndex];
+ const profilerEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex]);
+ const gasConsumedByStatement: { [statementId: string]: number } = {};
+ const statementIds = _.keys(profilerEntriesDescription.statementMap);
+ for (const statementId of statementIds) {
+ const statementDescription = profilerEntriesDescription.statementMap[statementId];
+ const totalGasCost = _.sum(
+ _.map(subtrace, structLog => {
+ const sourceRange = pcToSourceRange[structLog.pc];
+ if (_.isUndefined(sourceRange)) {
+ return 0;
+ }
+ if (sourceRange.fileName !== absoluteFileName) {
+ return 0;
+ }
+ if (utils.isRangeInside(sourceRange.location, statementDescription)) {
+ return structLog.gasCost;
+ } else {
+ return 0;
+ }
+ }),
+ );
+ gasConsumedByStatement[statementId] = totalGasCost;
+ }
+ const partialProfilerOutput = {
+ [absoluteFileName]: {
+ ...profilerEntriesDescription,
+ path: absoluteFileName,
+ f: {}, // I's meaningless in profiling context
+ s: gasConsumedByStatement,
+ b: {}, // I's meaningless in profiling context
+ },
+ };
+ return partialProfilerOutput;
+};
diff --git a/packages/sol-cov/src/revert_trace.ts b/packages/sol-cov/src/revert_trace.ts
new file mode 100644
index 000000000..a78d1afa8
--- /dev/null
+++ b/packages/sol-cov/src/revert_trace.ts
@@ -0,0 +1,90 @@
+import { logUtils } from '@0xproject/utils';
+import { OpCode, StructLog } from 'ethereum-types';
+
+import * as _ from 'lodash';
+
+import { EvmCallStack } from './types';
+import { utils } from './utils';
+
+export function getRevertTrace(structLogs: StructLog[], startAddress: string): EvmCallStack {
+ const evmCallStack: EvmCallStack = [];
+ const addressStack = [startAddress];
+ if (_.isEmpty(structLogs)) {
+ return [];
+ }
+ const normalizedStructLogs = utils.normalizeStructLogs(structLogs);
+ // tslint:disable-next-line:prefer-for-of
+ for (let i = 0; i < normalizedStructLogs.length; i++) {
+ const structLog = normalizedStructLogs[i];
+ if (structLog.depth !== addressStack.length - 1) {
+ throw new Error("Malformed trace. Trace depth doesn't match call stack depth");
+ }
+ // After that check we have a guarantee that call stack is never empty
+ // If it would: callStack.length - 1 === structLog.depth === -1
+ // That means that we can always safely pop from it
+
+ if (utils.isCallLike(structLog.op)) {
+ const currentAddress = _.last(addressStack) as string;
+ const jumpAddressOffset = 1;
+ const newAddress = utils.getAddressFromStackEntry(
+ structLog.stack[structLog.stack.length - jumpAddressOffset - 1],
+ );
+
+ // Sometimes calls don't change the execution context (current address). When we do a transfer to an
+ // externally owned account - it does the call and immediately returns because there is no fallback
+ // function. We manually check if the call depth had changed to handle that case.
+ const nextStructLog = normalizedStructLogs[i + 1];
+ if (nextStructLog.depth !== structLog.depth) {
+ addressStack.push(newAddress);
+ evmCallStack.push({
+ address: currentAddress,
+ structLog,
+ });
+ }
+ } else if (utils.isEndOpcode(structLog.op) && structLog.op !== OpCode.Revert) {
+ // Just like with calls, sometimes returns/stops don't change the execution context (current address).
+ const nextStructLog = normalizedStructLogs[i + 1];
+ if (_.isUndefined(nextStructLog) || nextStructLog.depth !== structLog.depth) {
+ evmCallStack.pop();
+ addressStack.pop();
+ }
+ if (structLog.op === OpCode.SelfDestruct) {
+ // After contract execution, we look at all sub-calls to external contracts, and for each one, fetch
+ // the bytecode and compute the coverage for the call. If the contract is destroyed with a call
+ // to `selfdestruct`, we are unable to fetch it's bytecode and compute coverage.
+ // TODO: Refactor this logic to fetch the sub-called contract bytecode before the selfdestruct is called
+ // in order to handle this edge-case.
+ logUtils.warn(
+ "Detected a selfdestruct. Sol-cov currently doesn't support that scenario. We'll just skip the trace part for a destructed contract",
+ );
+ }
+ } else if (structLog.op === OpCode.Revert) {
+ evmCallStack.push({
+ address: _.last(addressStack) as string,
+ structLog,
+ });
+ return evmCallStack;
+ } else if (structLog.op === OpCode.Create) {
+ // TODO: Extract the new contract address from the stack and handle that scenario
+ logUtils.warn(
+ "Detected a contract created from within another contract. Sol-cov currently doesn't support that scenario. We'll just skip that trace",
+ );
+ return [];
+ } else {
+ if (structLog !== _.last(normalizedStructLogs)) {
+ const nextStructLog = normalizedStructLogs[i + 1];
+ if (nextStructLog.depth === structLog.depth) {
+ continue;
+ } else if (nextStructLog.depth === structLog.depth - 1) {
+ addressStack.pop();
+ } else {
+ throw new Error('Malformed trace. Unexpected call depth change');
+ }
+ }
+ }
+ }
+ if (evmCallStack.length !== 0) {
+ logUtils.warn('Malformed trace. Call stack non empty at the end. (probably out of gas)');
+ }
+ return [];
+}
diff --git a/packages/sol-cov/src/revert_trace_subprovider.ts b/packages/sol-cov/src/revert_trace_subprovider.ts
new file mode 100644
index 000000000..b1d4da10c
--- /dev/null
+++ b/packages/sol-cov/src/revert_trace_subprovider.ts
@@ -0,0 +1,114 @@
+import { stripHexPrefix } from 'ethereumjs-util';
+import * as _ from 'lodash';
+import { getLogger, levels, Logger } from 'loglevel';
+
+import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter';
+import { constants } from './constants';
+import { getRevertTrace } from './revert_trace';
+import { parseSourceMap } from './source_maps';
+import { TraceCollectionSubprovider } from './trace_collection_subprovider';
+import { ContractData, EvmCallStack, SourceRange } from './types';
+import { utils } from './utils';
+
+/**
+ * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface.
+ * It is used to report call stack traces whenever a revert occurs.
+ */
+export class RevertTraceSubprovider extends TraceCollectionSubprovider {
+ // Lock is used to not accept normal transactions while doing call/snapshot magic because they'll be reverted later otherwise
+ private _contractsData!: ContractData[];
+ private _artifactAdapter: AbstractArtifactAdapter;
+ private _logger: Logger;
+
+ /**
+ * Instantiates a RevertTraceSubprovider 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._artifactAdapter = artifactAdapter;
+ this._logger = getLogger('sol-cov');
+ this._logger.setLevel(isVerbose ? levels.TRACE : levels.ERROR);
+ }
+ // tslint:disable-next-line:no-unused-variable
+ protected async _recordTxTraceAsync(address: string, data: string | undefined, txHash: string): Promise<void> {
+ await this._web3Wrapper.awaitTransactionMinedAsync(txHash, 0);
+ const trace = await this._web3Wrapper.getTransactionTraceAsync(txHash, {
+ disableMemory: true,
+ disableStack: false,
+ disableStorage: true,
+ });
+ const evmCallStack = getRevertTrace(trace.structLogs, address);
+ if (evmCallStack.length > 0) {
+ // if getRevertTrace returns a call stack it means there was a
+ // revert.
+ await this._printStackTraceAsync(evmCallStack);
+ }
+ }
+ private async _printStackTraceAsync(evmCallStack: EvmCallStack): Promise<void> {
+ const sourceRanges: SourceRange[] = [];
+ if (_.isUndefined(this._contractsData)) {
+ this._contractsData = await this._artifactAdapter.collectContractsDataAsync();
+ }
+ for (const evmCallStackEntry of evmCallStack) {
+ const isContractCreation = evmCallStackEntry.address === constants.NEW_CONTRACT;
+ if (isContractCreation) {
+ this._logger.error('Contract creation not supported');
+ continue;
+ }
+ const bytecode = await this._web3Wrapper.getContractCodeAsync(evmCallStackEntry.address);
+ const contractData = utils.getContractDataIfExists(this._contractsData, bytecode);
+ if (_.isUndefined(contractData)) {
+ const errMsg = isContractCreation
+ ? `Unknown contract creation transaction`
+ : `Transaction to an unknown address: ${evmCallStackEntry.address}`;
+ this._logger.warn(errMsg);
+ continue;
+ }
+ const bytecodeHex = stripHexPrefix(bytecode);
+ const sourceMap = isContractCreation ? contractData.sourceMap : contractData.sourceMapRuntime;
+ const pcToSourceRange = parseSourceMap(
+ contractData.sourceCodes,
+ sourceMap,
+ bytecodeHex,
+ contractData.sources,
+ );
+ // tslint:disable-next-line:no-unnecessary-initializer
+ let sourceRange: SourceRange | undefined = undefined;
+ let pc = evmCallStackEntry.structLog.pc;
+ // Sometimes there is not a mapping for this pc (e.g. if the revert
+ // actually happens in assembly). In that case, we want to keep
+ // searching backwards by decrementing the pc until we find a
+ // mapped source range.
+ while (_.isUndefined(sourceRange)) {
+ sourceRange = pcToSourceRange[pc];
+ pc -= 1;
+ if (pc <= 0) {
+ this._logger.warn(
+ `could not find matching sourceRange for structLog: ${evmCallStackEntry.structLog}`,
+ );
+ continue;
+ }
+ }
+ sourceRanges.push(sourceRange);
+ }
+ if (sourceRanges.length > 0) {
+ this._logger.error('\n\nStack trace for REVERT:\n');
+ _.forEach(_.reverse(sourceRanges), sourceRange => {
+ this._logger.error(
+ `${sourceRange.fileName}:${sourceRange.location.start.line}:${sourceRange.location.start.column}`,
+ );
+ });
+ this._logger.error('\n');
+ } else {
+ this._logger.error('Could not determine stack trace');
+ }
+ }
+}
diff --git a/packages/sol-cov/src/source_maps.ts b/packages/sol-cov/src/source_maps.ts
index 694171442..f9503e16c 100644
--- a/packages/sol-cov/src/source_maps.ts
+++ b/packages/sol-cov/src/source_maps.ts
@@ -38,10 +38,10 @@ export function parseSourceMap(
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) => {
+ // tslint:disable-next-line:no-unused-variable
const [instructionIndexStrIfExists, lengthStrIfExists, fileIndexStrIfExists, jumpTypeStrIfExists] = entry.split(
':',
);
diff --git a/packages/sol-cov/src/trace.ts b/packages/sol-cov/src/trace.ts
index 1c21c8e5c..635019fc0 100644
--- a/packages/sol-cov/src/trace.ts
+++ b/packages/sol-cov/src/trace.ts
@@ -1,26 +1,25 @@
-import { addressUtils, BigNumber, logUtils } from '@0xproject/utils';
-import { OpCode, StructLog, TransactionTrace } from 'ethereum-types';
-import { addHexPrefix, stripHexPrefix } from 'ethereumjs-util';
-import * as fs from 'fs';
+import { logUtils } from '@0xproject/utils';
+import { OpCode, StructLog } from 'ethereum-types';
import * as _ from 'lodash';
+import { utils } from './utils';
+
export interface TraceByContractAddress {
[contractAddress: string]: StructLog[];
}
-function getAddressFromStackEntry(stackEntry: string): string {
- const hexBase = 16;
- return addressUtils.padZeros(new BigNumber(addHexPrefix(stackEntry)).toString(hexBase));
-}
-
export function getTracesByContractAddress(structLogs: StructLog[], startAddress: string): TraceByContractAddress {
const traceByContractAddress: TraceByContractAddress = {};
let currentTraceSegment = [];
- const callStack = [startAddress];
+ const addressStack = [startAddress];
+ if (_.isEmpty(structLogs)) {
+ return traceByContractAddress;
+ }
+ const normalizedStructLogs = utils.normalizeStructLogs(structLogs);
// tslint:disable-next-line:prefer-for-of
- for (let i = 0; i < structLogs.length; i++) {
- const structLog = structLogs[i];
- if (structLog.depth !== callStack.length - 1) {
+ for (let i = 0; i < normalizedStructLogs.length; i++) {
+ const structLog = normalizedStructLogs[i];
+ if (structLog.depth !== addressStack.length - 1) {
throw new Error("Malformed trace. Trace depth doesn't match call stack depth");
}
// After that check we have a guarantee that call stack is never empty
@@ -28,36 +27,26 @@ export function getTracesByContractAddress(structLogs: StructLog[], startAddress
// That means that we can always safely pop from it
currentTraceSegment.push(structLog);
- const isCallLike = _.includes(
- [OpCode.CallCode, OpCode.StaticCall, OpCode.Call, OpCode.DelegateCall],
- structLog.op,
- );
- const isEndOpcode = _.includes(
- [OpCode.Return, OpCode.Stop, OpCode.Revert, OpCode.Invalid, OpCode.SelfDestruct],
- structLog.op,
- );
- if (isCallLike) {
- const currentAddress = _.last(callStack) as string;
+ if (utils.isCallLike(structLog.op)) {
+ const currentAddress = _.last(addressStack) as string;
const jumpAddressOffset = 1;
- const newAddress = getAddressFromStackEntry(
+ const newAddress = utils.getAddressFromStackEntry(
structLog.stack[structLog.stack.length - jumpAddressOffset - 1],
);
- if (structLog === _.last(structLogs)) {
- throw new Error('Malformed trace. CALL-like opcode can not be the last one');
- }
+
// Sometimes calls don't change the execution context (current address). When we do a transfer to an
// externally owned account - it does the call and immediately returns because there is no fallback
// function. We manually check if the call depth had changed to handle that case.
- const nextStructLog = structLogs[i + 1];
+ const nextStructLog = normalizedStructLogs[i + 1];
if (nextStructLog.depth !== structLog.depth) {
- callStack.push(newAddress);
+ addressStack.push(newAddress);
traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat(
currentTraceSegment,
);
currentTraceSegment = [];
}
- } else if (isEndOpcode) {
- const currentAddress = callStack.pop() as string;
+ } else if (utils.isEndOpcode(structLog.op)) {
+ const currentAddress = addressStack.pop() as string;
traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat(
currentTraceSegment,
);
@@ -79,12 +68,12 @@ export function getTracesByContractAddress(structLogs: StructLog[], startAddress
);
return traceByContractAddress;
} else {
- if (structLog !== _.last(structLogs)) {
- const nextStructLog = structLogs[i + 1];
+ if (structLog !== _.last(normalizedStructLogs)) {
+ const nextStructLog = normalizedStructLogs[i + 1];
if (nextStructLog.depth === structLog.depth) {
continue;
} else if (nextStructLog.depth === structLog.depth - 1) {
- const currentAddress = callStack.pop() as string;
+ const currentAddress = addressStack.pop() as string;
traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat(
currentTraceSegment,
);
@@ -95,11 +84,16 @@ export function getTracesByContractAddress(structLogs: StructLog[], startAddress
}
}
}
- if (callStack.length !== 0) {
- throw new Error('Malformed trace. Call stack non empty at the end');
+ if (addressStack.length !== 0) {
+ logUtils.warn('Malformed trace. Call stack non empty at the end');
}
if (currentTraceSegment.length !== 0) {
- throw new Error('Malformed trace. Current trace segment non empty at the end');
+ const currentAddress = addressStack.pop() as string;
+ traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat(
+ currentTraceSegment,
+ );
+ currentTraceSegment = [];
+ logUtils.warn('Malformed trace. Current trace segment non empty at the end');
}
return traceByContractAddress;
}
diff --git a/packages/sol-cov/src/trace_collection_subprovider.ts b/packages/sol-cov/src/trace_collection_subprovider.ts
new file mode 100644
index 000000000..9866472b9
--- /dev/null
+++ b/packages/sol-cov/src/trace_collection_subprovider.ts
@@ -0,0 +1,185 @@
+import { BlockchainLifecycle } from '@0xproject/dev-utils';
+import { Callback, ErrorCallback, NextCallback, Subprovider } from '@0xproject/subproviders';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import { CallData, JSONRPCRequestPayload, Provider, TxData } from 'ethereum-types';
+import * as _ from 'lodash';
+import { Lock } from 'semaphore-async-await';
+
+import { constants } from './constants';
+import { BlockParamLiteral } from './types';
+
+interface MaybeFakeTxData extends TxData {
+ isFakeTransaction?: boolean;
+}
+
+const BLOCK_GAS_LIMIT = 6000000;
+
+export interface TraceCollectionSubproviderConfig {
+ shouldCollectTransactionTraces: boolean;
+ shouldCollectCallTraces: boolean;
+ shouldCollectGasEstimateTraces: boolean;
+}
+
+// Because there is no notion of a call trace in the Ethereum rpc - we collect them in a rather non-obvious/hacky way.
+// On each call - we create a snapshot, execute the call as a transaction, get the trace, revert the snapshot.
+// That allows us to avoid influencing test behaviour.
+
+/**
+ * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface.
+ * It collects traces of all transactions that were sent and all calls that were executed through JSON RPC. It must
+ * be extended by implementing the _recordTxTraceAsync method which is called for every transaction.
+ */
+export abstract class TraceCollectionSubprovider extends Subprovider {
+ protected _web3Wrapper!: Web3Wrapper;
+ // Lock is used to not accept normal transactions while doing call/snapshot magic because they'll be reverted later otherwise
+ private _lock = new Lock();
+ private _defaultFromAddress: string;
+ private _isEnabled = true;
+ private _config: TraceCollectionSubproviderConfig;
+ /**
+ * Instantiates a TraceCollectionSubprovider instance
+ * @param defaultFromAddress default from address to use when sending transactions
+ */
+ constructor(defaultFromAddress: string, config: TraceCollectionSubproviderConfig) {
+ super();
+ this._defaultFromAddress = defaultFromAddress;
+ this._config = config;
+ }
+ /**
+ * Starts trace collection
+ */
+ public start(): void {
+ this._isEnabled = true;
+ }
+ /**
+ * Stops trace collection
+ */
+ public stop(): void {
+ this._isEnabled = false;
+ }
+ /**
+ * This method conforms to the web3-provider-engine interface.
+ * It is called internally by the ProviderEngine when it is this subproviders
+ * turn to handle a JSON RPC request.
+ * @param payload JSON RPC payload
+ * @param next Callback to call if this subprovider decides not to handle the request
+ * @param end Callback to call if subprovider handled the request and wants to pass back the request.
+ */
+ // tslint:disable-next-line:prefer-function-over-method async-suffix
+ public async handleRequest(payload: JSONRPCRequestPayload, next: NextCallback, _end: ErrorCallback): Promise<void> {
+ if (this._isEnabled) {
+ switch (payload.method) {
+ case 'eth_sendTransaction':
+ if (!this._config.shouldCollectTransactionTraces) {
+ next();
+ } else {
+ const txData = payload.params[0];
+ next(this._onTransactionSentAsync.bind(this, txData));
+ }
+ return;
+
+ case 'eth_call':
+ if (!this._config.shouldCollectCallTraces) {
+ next();
+ } else {
+ const callData = payload.params[0];
+ next(this._onCallOrGasEstimateExecutedAsync.bind(this, callData));
+ }
+ return;
+
+ case 'eth_estimateGas':
+ if (!this._config.shouldCollectGasEstimateTraces) {
+ next();
+ } else {
+ const estimateGasData = payload.params[0];
+ next(this._onCallOrGasEstimateExecutedAsync.bind(this, estimateGasData));
+ }
+ return;
+
+ default:
+ next();
+ return;
+ }
+ } else {
+ next();
+ return;
+ }
+ }
+ /**
+ * Set's the subprovider's engine to the ProviderEngine it is added to.
+ * This is only called within the ProviderEngine source code, do not call
+ * directly.
+ */
+ public setEngine(engine: Provider): void {
+ super.setEngine(engine);
+ this._web3Wrapper = new Web3Wrapper(engine);
+ }
+ protected abstract async _recordTxTraceAsync(
+ address: string,
+ data: string | undefined,
+ txHash: string,
+ ): Promise<void>;
+ private async _onTransactionSentAsync(
+ txData: MaybeFakeTxData,
+ err: Error | null,
+ txHash: string | undefined,
+ cb: Callback,
+ ): Promise<void> {
+ if (!txData.isFakeTransaction) {
+ // This transaction is a usual transaction. Not a call executed as one.
+ // And we don't want it to be executed within a snapshotting period
+ await this._lock.acquire();
+ }
+ const NULL_ADDRESS = '0x0';
+ if (_.isNull(err)) {
+ const toAddress =
+ _.isUndefined(txData.to) || txData.to === NULL_ADDRESS ? constants.NEW_CONTRACT : txData.to;
+ await this._recordTxTraceAsync(toAddress, txData.data, txHash as string);
+ } else {
+ const latestBlock = await this._web3Wrapper.getBlockWithTransactionDataAsync(BlockParamLiteral.Latest);
+ const transactions = latestBlock.transactions;
+ for (const transaction of transactions) {
+ const toAddress =
+ _.isUndefined(txData.to) || txData.to === NULL_ADDRESS ? constants.NEW_CONTRACT : txData.to;
+ await this._recordTxTraceAsync(toAddress, transaction.input, transaction.hash);
+ }
+ }
+ if (!txData.isFakeTransaction) {
+ // This transaction is a usual transaction. Not a call executed as one.
+ // And we don't want it to be executed within a snapshotting period
+ this._lock.release();
+ }
+ cb();
+ }
+ private async _onCallOrGasEstimateExecutedAsync(
+ callData: Partial<CallData>,
+ _err: Error | null,
+ _callResult: string,
+ cb: Callback,
+ ): Promise<void> {
+ await this._recordCallOrGasEstimateTraceAsync(callData);
+ cb();
+ }
+ private async _recordCallOrGasEstimateTraceAsync(callData: Partial<CallData>): Promise<void> {
+ // We don't want other transactions to be exeucted during snashotting period, that's why we lock the
+ // transaction execution for all transactions except our fake ones.
+ await this._lock.acquire();
+ const blockchainLifecycle = new BlockchainLifecycle(this._web3Wrapper);
+ await blockchainLifecycle.startAsync();
+ const fakeTxData: MaybeFakeTxData = {
+ gas: BLOCK_GAS_LIMIT,
+ isFakeTransaction: true, // This transaction (and only it) is allowed to come through when the lock is locked
+ ...callData,
+ from: callData.from || this._defaultFromAddress,
+ };
+ try {
+ const txHash = await this._web3Wrapper.sendTransactionAsync(fakeTxData);
+ await this._web3Wrapper.awaitTransactionMinedAsync(txHash, 0);
+ } catch (err) {
+ // Even if this transaction failed - we've already recorded it's trace.
+ _.noop();
+ }
+ await blockchainLifecycle.revertAsync();
+ this._lock.release();
+ }
+}
diff --git a/packages/sol-cov/src/trace_collector.ts b/packages/sol-cov/src/trace_collector.ts
new file mode 100644
index 000000000..1b458edec
--- /dev/null
+++ b/packages/sol-cov/src/trace_collector.ts
@@ -0,0 +1,93 @@
+import { promisify } from '@0xproject/utils';
+import { stripHexPrefix } from 'ethereumjs-util';
+import * as fs from 'fs';
+import { Collector } from 'istanbul';
+import * as _ from 'lodash';
+import { getLogger, levels, Logger } from 'loglevel';
+import * as mkdirp from 'mkdirp';
+
+import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter';
+import { constants } from './constants';
+import { parseSourceMap } from './source_maps';
+import {
+ ContractData,
+ Coverage,
+ SourceRange,
+ Subtrace,
+ TraceInfo,
+ TraceInfoExistingContract,
+ TraceInfoNewContract,
+} from './types';
+import { utils } from './utils';
+
+const mkdirpAsync = promisify<undefined>(mkdirp);
+
+export type SingleFileSubtraceHandler = (
+ contractData: ContractData,
+ subtrace: Subtrace,
+ pcToSourceRange: { [programCounter: number]: SourceRange },
+ fileIndex: number,
+) => Coverage;
+
+/**
+ * TraceCollector is used by CoverageSubprovider to compute code coverage based on collected trace data.
+ */
+export class TraceCollector {
+ private _artifactAdapter: AbstractArtifactAdapter;
+ private _logger: Logger;
+ private _contractsData!: ContractData[];
+ private _collector = new Collector();
+ private _singleFileSubtraceHandler: SingleFileSubtraceHandler;
+
+ /**
+ * Instantiates a TraceCollector instance
+ * @param artifactAdapter Adapter for used artifacts format (0x, truffle, giveth, etc.)
+ * @param isVerbose If true, we will log any unknown transactions. Otherwise we will ignore them
+ * @param singleFileSubtraceHandler A handler function for computing partial coverage for a single file & subtrace
+ */
+ constructor(
+ artifactAdapter: AbstractArtifactAdapter,
+ isVerbose: boolean,
+ singleFileSubtraceHandler: SingleFileSubtraceHandler,
+ ) {
+ this._artifactAdapter = artifactAdapter;
+ this._logger = getLogger('sol-cov');
+ this._logger.setLevel(isVerbose ? levels.TRACE : levels.ERROR);
+ this._singleFileSubtraceHandler = singleFileSubtraceHandler;
+ }
+ public async writeOutputAsync(): Promise<void> {
+ const finalCoverage = this._collector.getFinalCoverage();
+ const stringifiedCoverage = JSON.stringify(finalCoverage, null, '\t');
+ await mkdirpAsync('coverage');
+ fs.writeFileSync('coverage/coverage.json', stringifiedCoverage);
+ }
+ public async computeSingleTraceCoverageAsync(traceInfo: TraceInfo): Promise<void> {
+ if (_.isUndefined(this._contractsData)) {
+ this._contractsData = await this._artifactAdapter.collectContractsDataAsync();
+ }
+ const isContractCreation = traceInfo.address === constants.NEW_CONTRACT;
+ const bytecode = isContractCreation
+ ? (traceInfo as TraceInfoNewContract).bytecode
+ : (traceInfo as TraceInfoExistingContract).runtimeBytecode;
+ const contractData = utils.getContractDataIfExists(this._contractsData, bytecode);
+ if (_.isUndefined(contractData)) {
+ const errMsg = isContractCreation
+ ? `Unknown contract creation transaction`
+ : `Transaction to an unknown address: ${traceInfo.address}`;
+ this._logger.warn(errMsg);
+ return;
+ }
+ const bytecodeHex = stripHexPrefix(bytecode);
+ const sourceMap = isContractCreation ? contractData.sourceMap : contractData.sourceMapRuntime;
+ const pcToSourceRange = parseSourceMap(contractData.sourceCodes, sourceMap, bytecodeHex, contractData.sources);
+ for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
+ const singleFileCoverageForTrace = this._singleFileSubtraceHandler(
+ contractData,
+ traceInfo.subtrace,
+ pcToSourceRange,
+ fileIndex,
+ );
+ this._collector.add(singleFileCoverageForTrace);
+ }
+ }
+}
diff --git a/packages/sol-cov/src/trace_info_subprovider.ts b/packages/sol-cov/src/trace_info_subprovider.ts
new file mode 100644
index 000000000..635a68f58
--- /dev/null
+++ b/packages/sol-cov/src/trace_info_subprovider.ts
@@ -0,0 +1,59 @@
+import * as _ from 'lodash';
+
+import { constants } from './constants';
+import { getTracesByContractAddress } from './trace';
+import { TraceCollectionSubprovider } from './trace_collection_subprovider';
+import { TraceInfo, TraceInfoExistingContract, TraceInfoNewContract } from './types';
+
+// TraceInfoSubprovider is extended by subproviders which need to work with one
+// TraceInfo at a time. It has one abstract method: _handleTraceInfoAsync, which
+// is called for each TraceInfo.
+export abstract class TraceInfoSubprovider extends TraceCollectionSubprovider {
+ protected abstract _handleTraceInfoAsync(traceInfo: TraceInfo): Promise<void>;
+ protected async _recordTxTraceAsync(address: string, data: string | undefined, txHash: string): Promise<void> {
+ await this._web3Wrapper.awaitTransactionMinedAsync(txHash, 0);
+ const trace = await this._web3Wrapper.getTransactionTraceAsync(txHash, {
+ disableMemory: true,
+ disableStack: false,
+ disableStorage: true,
+ });
+ const tracesByContractAddress = getTracesByContractAddress(trace.structLogs, address);
+ const subcallAddresses = _.keys(tracesByContractAddress);
+ if (address === constants.NEW_CONTRACT) {
+ for (const subcallAddress of subcallAddresses) {
+ let traceInfo: TraceInfoNewContract | TraceInfoExistingContract;
+ if (subcallAddress === 'NEW_CONTRACT') {
+ const traceForThatSubcall = tracesByContractAddress[subcallAddress];
+ traceInfo = {
+ subtrace: traceForThatSubcall,
+ txHash,
+ address: subcallAddress,
+ bytecode: data as string,
+ };
+ } else {
+ const runtimeBytecode = await this._web3Wrapper.getContractCodeAsync(subcallAddress);
+ const traceForThatSubcall = tracesByContractAddress[subcallAddress];
+ traceInfo = {
+ subtrace: traceForThatSubcall,
+ txHash,
+ address: subcallAddress,
+ runtimeBytecode,
+ };
+ }
+ await this._handleTraceInfoAsync(traceInfo);
+ }
+ } else {
+ for (const subcallAddress of subcallAddresses) {
+ const runtimeBytecode = await this._web3Wrapper.getContractCodeAsync(subcallAddress);
+ const traceForThatSubcall = tracesByContractAddress[subcallAddress];
+ const traceInfo: TraceInfoExistingContract = {
+ subtrace: traceForThatSubcall,
+ txHash,
+ address: subcallAddress,
+ runtimeBytecode,
+ };
+ await this._handleTraceInfoAsync(traceInfo);
+ }
+ }
+ }
+}
diff --git a/packages/sol-cov/src/types.ts b/packages/sol-cov/src/types.ts
index 4c3de55a1..cef7141cb 100644
--- a/packages/sol-cov/src/types.ts
+++ b/packages/sol-cov/src/types.ts
@@ -1,3 +1,5 @@
+import { StructLog } from 'ethereum-types';
+
export interface LineColumn {
line: number;
column: number;
@@ -45,24 +47,24 @@ export interface StatementMap {
}
export interface LineCoverage {
- [lineNo: number]: boolean;
+ [lineNo: number]: number;
}
export interface FunctionCoverage {
- [functionId: string]: boolean;
+ [functionId: string]: number;
}
export interface StatementCoverage {
- [statementId: string]: boolean;
+ [statementId: string]: number;
}
export interface BranchCoverage {
- [branchId: string]: boolean[];
+ [branchId: string]: number[];
}
export interface Coverage {
[fineName: string]: {
- l: LineCoverage;
+ l?: LineCoverage;
f: FunctionCoverage;
s: StatementCoverage;
b: BranchCoverage;
@@ -82,8 +84,11 @@ export interface ContractData {
sources: string[];
}
+// Part of the trace executed within the same context
+export type Subtrace = StructLog[];
+
export interface TraceInfoBase {
- coveredPcs: number[];
+ subtrace: Subtrace;
txHash: string;
}
@@ -102,3 +107,10 @@ export type TraceInfo = TraceInfoNewContract | TraceInfoExistingContract;
export enum BlockParamLiteral {
Latest = 'latest',
}
+
+export interface EvmCallStackEntry {
+ structLog: StructLog;
+ address: string;
+}
+
+export type EvmCallStack = EvmCallStackEntry[];
diff --git a/packages/sol-cov/src/utils.ts b/packages/sol-cov/src/utils.ts
index d970c42ee..4f16a1cda 100644
--- a/packages/sol-cov/src/utils.ts
+++ b/packages/sol-cov/src/utils.ts
@@ -1,4 +1,9 @@
-import { LineColumn, SingleFileSourceRange } from './types';
+import { addressUtils, BigNumber } from '@0xproject/utils';
+import { OpCode, StructLog } from 'ethereum-types';
+import { addHexPrefix } from 'ethereumjs-util';
+import * as _ from 'lodash';
+
+import { ContractData, LineColumn, SingleFileSourceRange } from './types';
export const utils = {
compareLineColumn(lhs: LineColumn, rhs: LineColumn): number {
@@ -14,4 +19,51 @@ export const utils = {
utils.compareLineColumn(childRange.end, parentRange.end) <= 0
);
},
+ 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........................................');
+ // HACK: Node regexes can't be longer that 32767 characters. Contracts bytecode can. We just truncate the regexes. It's safe in practice.
+ const MAX_REGEX_LENGTH = 32767;
+ const truncatedBytecodeRegex = bytecodeRegex.slice(0, MAX_REGEX_LENGTH);
+ return truncatedBytecodeRegex;
+ },
+ 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 = utils.bytecodeToBytecodeRegex(contractDataCandidate.bytecode);
+ const runtimeBytecodeRegex = utils.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;
+ },
+ isCallLike(op: OpCode): boolean {
+ return _.includes([OpCode.CallCode, OpCode.StaticCall, OpCode.Call, OpCode.DelegateCall], op);
+ },
+ isEndOpcode(op: OpCode): boolean {
+ return _.includes([OpCode.Return, OpCode.Stop, OpCode.Revert, OpCode.Invalid, OpCode.SelfDestruct], op);
+ },
+ getAddressFromStackEntry(stackEntry: string): string {
+ const hexBase = 16;
+ return addressUtils.padZeros(new BigNumber(addHexPrefix(stackEntry)).toString(hexBase));
+ },
+ normalizeStructLogs(structLogs: StructLog[]): StructLog[] {
+ if (structLogs[0].depth === 1) {
+ // Geth uses 1-indexed depth counter whilst ganache starts from 0
+ const newStructLogs = _.map(structLogs, structLog => ({
+ ...structLog,
+ depth: structLog.depth - 1,
+ }));
+ return newStructLogs;
+ }
+ return structLogs;
+ },
};
diff --git a/packages/sol-cov/test/instructions_test.ts b/packages/sol-cov/test/instructions_test.ts
index 8012674f5..02f30a5ca 100644
--- a/packages/sol-cov/test/instructions_test.ts
+++ b/packages/sol-cov/test/instructions_test.ts
@@ -1,8 +1,6 @@
import * as chai from 'chai';
-import * as fs from 'fs';
import 'make-promises-safe';
import 'mocha';
-import * as path from 'path';
import { constants } from '../src/constants';
import { getPcToInstructionIndexMapping } from '../src/instructions';
diff --git a/packages/sol-cov/test/trace_test.ts b/packages/sol-cov/test/trace_test.ts
index 07e270b71..7a034362c 100644
--- a/packages/sol-cov/test/trace_test.ts
+++ b/packages/sol-cov/test/trace_test.ts
@@ -1,9 +1,7 @@
import * as chai from 'chai';
import { OpCode, StructLog } from 'ethereum-types';
-import * as fs from 'fs';
import * as _ from 'lodash';
import 'mocha';
-import * as path from 'path';
import { getTracesByContractAddress } from '../src/trace';