diff options
author | Fabio Berger <me@fabioberger.com> | 2018-03-14 21:16:08 +0800 |
---|---|---|
committer | Fabio Berger <me@fabioberger.com> | 2018-03-14 21:16:08 +0800 |
commit | 009b70f5b218a1ccb154034936256308131b7d9c (patch) | |
tree | cd9642a8d3323c9e80da54ac4e8fc40ade9f9020 /packages/sol-cov | |
parent | f7c1e10b5ac112866ee55e7fededdb37c890d30f (diff) | |
parent | 3f3e8be004818ddaa1921b3dff12bdd46052278b (diff) | |
download | dexon-sol-tools-009b70f5b218a1ccb154034936256308131b7d9c.tar dexon-sol-tools-009b70f5b218a1ccb154034936256308131b7d9c.tar.gz dexon-sol-tools-009b70f5b218a1ccb154034936256308131b7d9c.tar.bz2 dexon-sol-tools-009b70f5b218a1ccb154034936256308131b7d9c.tar.lz dexon-sol-tools-009b70f5b218a1ccb154034936256308131b7d9c.tar.xz dexon-sol-tools-009b70f5b218a1ccb154034936256308131b7d9c.tar.zst dexon-sol-tools-009b70f5b218a1ccb154034936256308131b7d9c.zip |
Merge branch 'development' into convertScriptsToTs
* development: (71 commits)
Transform input data before encoding for callAsync and getABIEncodedTransactionData
Update coverage badge to show development coverage
Configure post build hook
Notify coveralls after all tasks have finished
Address feedback
Revert "Report all coverage reports together"
Separate published packages and typescript typings on README
Report all coverage reports together
Add other statement types
Properly and consistently parse ENV vars
Add forgotten file
Start using solidity-parser-antlr
Fix the default always overriding to address
Submit a TD PR
Add an explanatory comment for making ranges unique
Fix a typo in handling env variables
Introduce TESTRPC_FIRST_ADDRESS
Make BlockchainLifecycle accept only web3Wrapper
Fix comments
Fix deployer CHANGELOG
...
# Conflicts:
# README.md
# packages/deployer/package.json
# packages/subproviders/src/globals.d.ts
# yarn.lock
Diffstat (limited to 'packages/sol-cov')
-rw-r--r-- | packages/sol-cov/.npmignore | 5 | ||||
-rw-r--r-- | packages/sol-cov/CHANGELOG.md | 1 | ||||
-rw-r--r-- | packages/sol-cov/README.md | 64 | ||||
-rw-r--r-- | packages/sol-cov/package.json | 45 | ||||
-rw-r--r-- | packages/sol-cov/scripts/postpublish.js | 5 | ||||
-rw-r--r-- | packages/sol-cov/src/ast_visitor.ts | 133 | ||||
-rw-r--r-- | packages/sol-cov/src/collect_contract_data.ts | 44 | ||||
-rw-r--r-- | packages/sol-cov/src/collect_coverage_entries.ts | 24 | ||||
-rw-r--r-- | packages/sol-cov/src/constants.ts | 3 | ||||
-rw-r--r-- | packages/sol-cov/src/coverage_manager.ts | 186 | ||||
-rw-r--r-- | packages/sol-cov/src/coverage_subprovider.ts | 168 | ||||
-rw-r--r-- | packages/sol-cov/src/globals.d.ts | 315 | ||||
-rw-r--r-- | packages/sol-cov/src/index.ts | 1 | ||||
-rw-r--r-- | packages/sol-cov/src/instructions.ts | 24 | ||||
-rw-r--r-- | packages/sol-cov/src/source_maps.ts | 82 | ||||
-rw-r--r-- | packages/sol-cov/src/types.ts | 101 | ||||
-rw-r--r-- | packages/sol-cov/src/utils.ts | 13 | ||||
-rw-r--r-- | packages/sol-cov/tsconfig.json | 12 | ||||
-rw-r--r-- | packages/sol-cov/tslint.json | 6 |
19 files changed, 1232 insertions, 0 deletions
diff --git a/packages/sol-cov/.npmignore b/packages/sol-cov/.npmignore new file mode 100644 index 000000000..87bc30436 --- /dev/null +++ b/packages/sol-cov/.npmignore @@ -0,0 +1,5 @@ +.* +yarn-error.log +/src/ +/scripts/ +tsconfig.json diff --git a/packages/sol-cov/CHANGELOG.md b/packages/sol-cov/CHANGELOG.md new file mode 100644 index 000000000..a0cf709bc --- /dev/null +++ b/packages/sol-cov/CHANGELOG.md @@ -0,0 +1 @@ +# CHANGELOG diff --git a/packages/sol-cov/README.md b/packages/sol-cov/README.md new file mode 100644 index 000000000..6a98346f9 --- /dev/null +++ b/packages/sol-cov/README.md @@ -0,0 +1,64 @@ +## @0xproject/sol-cov + +A Solidity code coverage tool. + +## Installation + +```bash +yarn add -D @0xproject/sol-cov +``` + +## Usage + +Sol-cov uses transaction traces in order to figure out which lines of Solidity source code have been covered by your tests. In order for it to gather these traces, you must add the `CoverageSubprovider` to the [ProviderEngine](https://github.com/MetaMask/provider-engine) instance you use when running your Solidity tests. If you're unfamiliar with ProviderEngine, please read the [Web3 Provider explained](https://0xproject.com/wiki#Web3-Provider-Explained) wiki article. + +The CoverageSubprovider eavesdrops on the `eth_sendTransaction` and `eth_call` RPC calls and collects traces after each call using `debug_traceTransaction`. `eth_call`'s' don't generate traces - so we take a snapshot, re-submit it as a transaction, get the trace and then revert the snapshot. + +```typescript +import { CoverageSubprovider } from '@0xproject/sol-cov'; + +const provider = new ProviderEngine(); + +const artifactsPath = 'src/artifacts'; +const contractsPath = 'src/contracts'; +const networkId = 50; +// Some calls might not have `from` address specified. Nevertheless - transactions need to be submitted from an address with at least some funds. defaultFromAddress is the address that will be used to submit those calls as transactions from. +const defaultFromAddress = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; +const coverageSubprovider = new CoverageSubprovider(artifactsPath, contractsPath, networkId, defaultFromAddress); + +provider.addProvider(coverageSubprovider); +``` + +After your test suite is complete (e.g global `after` hook), you'll need to call: + +```typescript +await coverageSubprovider.writeCoverageAsync(); +``` + +This will create a `coverage.json` file in the `coverage` directory. This file has an [Istanbul format](https://github.com/gotwarlost/istanbul/blob/master/coverage.json.md) - so you can use any of the existing Instanbul reporters. + +## Contributing + +We strongly encourage the community to help us make improvements. To report bugs within this package, please create an issue in this repository. + +Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting started. + +### Install Dependencies + +If you don't have yarn workspaces enabled (Yarn < v1.0) - enable them: + +```bash +yarn config set workspaces-experimental true +``` + +Then install dependencies + +```bash +yarn install +``` + +### Lint + +```bash +yarn lint +``` diff --git a/packages/sol-cov/package.json b/packages/sol-cov/package.json new file mode 100644 index 000000000..af6be13f2 --- /dev/null +++ b/packages/sol-cov/package.json @@ -0,0 +1,45 @@ +{ + "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", + "ethereumjs-util": "^5.1.1", + "glob": "^7.1.2", + "istanbul": "^0.4.5", + "lodash": "^4.17.4", + "semaphore-async-await": "^1.5.1", + "solidity-coverage": "^0.4.10", + "solidity-parser-antlr": "^0.2.7", + "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" + } +} diff --git a/packages/sol-cov/scripts/postpublish.js b/packages/sol-cov/scripts/postpublish.js new file mode 100644 index 000000000..b3e5407c8 --- /dev/null +++ b/packages/sol-cov/scripts/postpublish.js @@ -0,0 +1,5 @@ +const postpublish_utils = require('../../../scripts/postpublish_utils'); +const packageJSON = require('../package.json'); + +const subPackageName = packageJSON.name; +postpublish_utils.standardPostPublishAsync(subPackageName);
\ No newline at end of file diff --git a/packages/sol-cov/src/ast_visitor.ts b/packages/sol-cov/src/ast_visitor.ts new file mode 100644 index 000000000..66190afec --- /dev/null +++ b/packages/sol-cov/src/ast_visitor.ts @@ -0,0 +1,133 @@ +import * as _ from 'lodash'; +import * as Parser from 'solidity-parser-antlr'; + +import { BranchMap, FnMap, LocationByOffset, SingleFileSourceRange, StatementMap } from './types'; + +export interface CoverageEntriesDescription { + fnMap: FnMap; + branchMap: BranchMap; + statementMap: StatementMap; + modifiersStatementIds: number[]; +} + +enum BranchType { + If = 'if', + ConditionalExpression = 'cond-expr', + BinaryExpression = 'binary-expr', +} + +export class ASTVisitor { + private _entryId = 0; + private _fnMap: FnMap = {}; + private _branchMap: BranchMap = {}; + private _modifiersStatementIds: number[] = []; + private _statementMap: StatementMap = {}; + private _locationByOffset: LocationByOffset; + constructor(locationByOffset: LocationByOffset) { + this._locationByOffset = locationByOffset; + } + public getCollectedCoverageEntries(): CoverageEntriesDescription { + const coverageEntriesDescription = { + fnMap: this._fnMap, + branchMap: this._branchMap, + statementMap: this._statementMap, + modifiersStatementIds: this._modifiersStatementIds, + }; + return coverageEntriesDescription; + } + public IfStatement(ast: Parser.IfStatement): void { + this._visitStatement(ast); + this._visitBinaryBranch(ast, ast.trueBody, ast.falseBody || ast, BranchType.If); + } + public FunctionDefinition(ast: Parser.FunctionDefinition): void { + this._visitFunctionLikeDefinition(ast); + } + public ModifierDefinition(ast: Parser.ModifierDefinition): void { + this._visitFunctionLikeDefinition(ast); + } + public ForStatement(ast: Parser.ForStatement): void { + this._visitStatement(ast); + } + public ReturnStatement(ast: Parser.ReturnStatement): void { + this._visitStatement(ast); + } + public BreakStatement(ast: Parser.BreakStatement): void { + this._visitStatement(ast); + } + public ContinueStatement(ast: Parser.ContinueStatement): void { + this._visitStatement(ast); + } + public VariableDeclarationStatement(ast: Parser.VariableDeclarationStatement): void { + this._visitStatement(ast); + } + public Statement(ast: Parser.Statement): void { + this._visitStatement(ast); + } + public WhileStatement(ast: Parser.WhileStatement): void { + this._visitStatement(ast); + } + public SimpleStatement(ast: Parser.SimpleStatement): void { + this._visitStatement(ast); + } + public ThrowStatement(ast: Parser.ThrowStatement): void { + this._visitStatement(ast); + } + public DoWhileStatement(ast: Parser.DoWhileStatement): void { + this._visitStatement(ast); + } + public ExpressionStatement(ast: Parser.ExpressionStatement): void { + this._visitStatement(ast.expression); + } + public InlineAssemblyStatement(ast: Parser.InlineAssemblyStatement): void { + this._visitStatement(ast); + } + public BinaryOperation(ast: Parser.BinaryOperation): void { + const BRANCHING_BIN_OPS = ['&&', '||']; + if (_.includes(BRANCHING_BIN_OPS, ast.operator)) { + this._visitBinaryBranch(ast, ast.left, ast.right, BranchType.BinaryExpression); + } + } + public Conditional(ast: Parser.Conditional): void { + this._visitBinaryBranch(ast, ast.trueExpression, ast.falseExpression, BranchType.ConditionalExpression); + } + public ModifierInvocation(ast: Parser.ModifierInvocation): void { + const BUILTIN_MODIFIERS = ['public', 'view', 'payable', 'external', 'internal', 'pure', 'constant']; + if (!_.includes(BUILTIN_MODIFIERS, ast.name)) { + this._modifiersStatementIds.push(this._entryId); + this._visitStatement(ast); + } + } + private _visitBinaryBranch( + ast: Parser.ASTNode, + left: Parser.ASTNode, + right: Parser.ASTNode, + type: BranchType, + ): void { + this._branchMap[this._entryId++] = { + line: this._getExpressionRange(ast).start.line, + type, + locations: [this._getExpressionRange(left), this._getExpressionRange(right)], + }; + } + private _visitStatement(ast: Parser.ASTNode): void { + this._statementMap[this._entryId++] = this._getExpressionRange(ast); + } + private _getExpressionRange(ast: Parser.ASTNode): SingleFileSourceRange { + const start = this._locationByOffset[ast.range[0] - 1]; + const end = this._locationByOffset[ast.range[1]]; + const range = { + start, + end, + }; + return range; + } + private _visitFunctionLikeDefinition(ast: Parser.ModifierDefinition | Parser.FunctionDefinition): void { + const loc = this._getExpressionRange(ast); + this._fnMap[this._entryId++] = { + name: ast.name, + line: loc.start.line, + loc, + }; + this._visitStatement(ast); + } +} diff --git a/packages/sol-cov/src/collect_contract_data.ts b/packages/sol-cov/src/collect_contract_data.ts new file mode 100644 index 000000000..a0ce2640f --- /dev/null +++ b/packages/sol-cov/src/collect_contract_data.ts @@ -0,0 +1,44 @@ +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 useful 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; + }); + if (_.isUndefined(artifact.networks[networkId])) { + throw new Error(`No ${baseName} artifacts found for networkId ${networkId}`); + } + 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; +}; diff --git a/packages/sol-cov/src/collect_coverage_entries.ts b/packages/sol-cov/src/collect_coverage_entries.ts new file mode 100644 index 000000000..6da81fbfc --- /dev/null +++ b/packages/sol-cov/src/collect_coverage_entries.ts @@ -0,0 +1,24 @@ +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'; +import { getLocationByOffset } from './source_maps'; + +// Parsing source code for each transaction/code is slow and therefore we cache it +const coverageEntriesBySourceHash: { [sourceHash: string]: CoverageEntriesDescription } = {}; + +export const collectCoverageEntries = (contractSource: string, fileName: string) => { + const sourceHash = ethUtil.sha3(contractSource).toString('hex'); + if (_.isUndefined(coverageEntriesBySourceHash[sourceHash])) { + const ast = parser.parse(contractSource, { range: true }); + const locationByOffset = getLocationByOffset(contractSource); + const visitor = new ASTVisitor(locationByOffset); + parser.visit(ast, visitor); + coverageEntriesBySourceHash[sourceHash] = visitor.getCollectedCoverageEntries(); + } + const coverageEntriesDescription = coverageEntriesBySourceHash[sourceHash]; + return coverageEntriesDescription; +}; diff --git a/packages/sol-cov/src/constants.ts b/packages/sol-cov/src/constants.ts new file mode 100644 index 000000000..970734f2d --- /dev/null +++ b/packages/sol-cov/src/constants.ts @@ -0,0 +1,3 @@ +export const constants = { + NEW_CONTRACT: 'NEW_CONTRACT', +}; diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts new file mode 100644 index 000000000..b1ba8b22b --- /dev/null +++ b/packages/sol-cov/src/coverage_manager.ts @@ -0,0 +1,186 @@ +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 { 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'; + +export class CoverageManager { + private _traceInfos: TraceInfo[] = []; + private _contractsData: ContractData[] = []; + private _getContractCodeAsync: (address: string) => Promise<string>; + private static _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. + // By default lodash does a shallow object comparasion. We JSON.stringify them and compare as strings. + sourceRanges = _.uniqBy(sourceRanges, s => JSON.stringify(s)); // We don't care if one PC was covered multiple times within a single transaction + sourceRanges = _.filter(sourceRanges, sourceRange => sourceRange.fileName === fileName); + 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 = { + [contractData.sources[fileIndex]]: { + ...coverageEntriesDescription, + l: {}, // It's able to derive it from statement coverage + path: fileName, + f: functionCoverage, + s: statementCoverage, + b: branchCoverage, + }, + }; + return partialCoverage; + } + constructor( + artifactsPath: string, + sourcesPath: string, + networkId: number, + getContractCodeAsync: (address: string) => Promise<string>, + ) { + this._getContractCodeAsync = getContractCodeAsync; + this._contractsData = collectContractsData(artifactsPath, sourcesPath, networkId); + } + public appendTraceInfo(traceInfo: TraceInfo): void { + this._traceInfos.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 traceInfo of this._traceInfos) { + if (traceInfo.address !== constants.NEW_CONTRACT) { + // Runtime transaction + const runtimeBytecode = (traceInfo as TraceInfoExistingContract).runtimeBytecode; + const contractData = _.find(this._contractsData, { runtimeBytecode }) as ContractData; + if (_.isUndefined(contractData)) { + throw new Error(`Transaction to an unknown address: ${traceInfo.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++) { + const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace( + contractData, + traceInfo.coveredPcs, + pcToSourceRange, + fileIndex, + ); + collector.add(singleFileCoverageForTrace); + } + } else { + // Contract creation transaction + const bytecode = (traceInfo as TraceInfoNewContract).bytecode; + 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 = CoverageManager._getSingleFileCoverageForTrace( + contractData, + traceInfo.coveredPcs, + pcToSourceRange, + fileIndex, + ); + collector.add(singleFileCoverageForTrace); + } + } + } + // TODO: Remove any cast as soon as https://github.com/DefinitelyTyped/DefinitelyTyped/pull/24233 gets merged + return (collector as any).getFinalCoverage(); + } +} diff --git a/packages/sol-cov/src/coverage_subprovider.ts b/packages/sol-cov/src/coverage_subprovider.ts new file mode 100644 index 000000000..71d90bba7 --- /dev/null +++ b/packages/sol-cov/src/coverage_subprovider.ts @@ -0,0 +1,168 @@ +import { Callback, NextCallback, Subprovider } from '@0xproject/subproviders'; +import { promisify } from '@0xproject/utils'; +import * as _ from 'lodash'; +import { Lock } from 'semaphore-async-await'; +import * as Web3 from 'web3'; + +import { constants } from './constants'; +import { CoverageManager } from './coverage_manager'; +import { TraceInfoExistingContract, TraceInfoNewContract } from './types'; + +interface MaybeFakeTxData extends Web3.TxData { + isFakeTransaction?: boolean; +} + +/* + * This class implements the web3-provider-engine subprovider interface and collects traces of all transactions that were sent and all calls that were executed. + * Because there is no notion of call trace in the rpc - we collect them in 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 not influence the test behaviour. + * Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js + */ +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; + constructor(artifactsPath: string, sourcesPath: string, networkId: number, defaultFromAddress: string) { + super(); + this._lock = new Lock(); + this._defaultFromAddress = defaultFromAddress; + 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: 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: ['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(); + } + 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> { + let 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); + if (address === constants.NEW_CONTRACT) { + const traceInfo: TraceInfoNewContract = { + coveredPcs, + txHash, + address: address as 'NEW_CONTRACT', + bytecode: data as string, + }; + this._coverageManager.appendTraceInfo(traceInfo); + } else { + payload = { method: 'eth_getCode', params: [address, 'latest'] }; + const runtimeBytecode = (await this.emitPayloadAsync(payload)).result; + const traceInfo: TraceInfoExistingContract = { + coveredPcs, + txHash, + address, + runtimeBytecode, + }; + this._coverageManager.appendTraceInfo(traceInfo); + } + } + private async _recordCallTraceAsync(callData: Partial<Web3.CallData>, blockNumber: Web3.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'); + } + } + 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; + } +} diff --git a/packages/sol-cov/src/globals.d.ts b/packages/sol-cov/src/globals.d.ts new file mode 100644 index 000000000..54ee64684 --- /dev/null +++ b/packages/sol-cov/src/globals.d.ts @@ -0,0 +1,315 @@ +// tslint:disable:completed-docs +declare module 'solidity-parser-antlr' { + export interface BaseASTNode { + range: [number, number]; + } + export interface SourceUnit extends BaseASTNode {} + export interface PragmaDirective extends BaseASTNode {} + export interface PragmaName extends BaseASTNode {} + export interface PragmaValue extends BaseASTNode {} + export interface Version extends BaseASTNode {} + export interface VersionOperator extends BaseASTNode {} + export interface VersionConstraint extends BaseASTNode {} + export interface ImportDeclaration extends BaseASTNode {} + export interface ImportDirective extends BaseASTNode {} + export interface ContractDefinition extends BaseASTNode {} + export interface InheritanceSpecifier extends BaseASTNode {} + export interface ContractPart extends BaseASTNode {} + export interface StateVariableDeclaration extends BaseASTNode { + variables: VariableDeclaration[]; + } + export interface UsingForDeclaration extends BaseASTNode {} + export interface StructDefinition extends BaseASTNode {} + export interface ModifierDefinition extends BaseASTNode { + name: string; + } + export interface ModifierInvocation extends BaseASTNode { + name: string; + } + export interface FunctionDefinition extends BaseASTNode { + name: string; + } + export interface ReturnParameters extends BaseASTNode {} + export interface ModifierList extends BaseASTNode {} + export interface EventDefinition extends BaseASTNode {} + export interface EnumValue extends BaseASTNode {} + export interface EnumDefinition extends BaseASTNode {} + export interface ParameterList extends BaseASTNode {} + export interface Parameter extends BaseASTNode {} + export interface EventParameterList extends BaseASTNode {} + export interface EventParameter extends BaseASTNode {} + export interface FunctionTypeParameterList extends BaseASTNode {} + export interface FunctionTypeParameter extends BaseASTNode {} + export interface VariableDeclaration extends BaseASTNode { + visibility: 'public' | 'private'; + isStateVar: boolean; + } + export interface TypeName extends BaseASTNode {} + export interface UserDefinedTypeName extends BaseASTNode {} + export interface Mapping extends BaseASTNode {} + export interface FunctionTypeName extends BaseASTNode {} + export interface StorageLocation extends BaseASTNode {} + export interface StateMutability extends BaseASTNode {} + export interface Block extends BaseASTNode {} + export interface Statement extends BaseASTNode {} + export interface ExpressionStatement extends BaseASTNode { + expression: ASTNode; + } + export interface IfStatement extends BaseASTNode { + trueBody: ASTNode; + falseBody: ASTNode; + } + export interface WhileStatement extends BaseASTNode {} + export interface SimpleStatement extends BaseASTNode {} + export interface ForStatement extends BaseASTNode {} + export interface InlineAssemblyStatement extends BaseASTNode {} + export interface DoWhileStatement extends BaseASTNode {} + export interface ContinueStatement extends BaseASTNode {} + export interface BreakStatement extends BaseASTNode {} + export interface ReturnStatement extends BaseASTNode {} + export interface ThrowStatement extends BaseASTNode {} + export interface VariableDeclarationStatement extends BaseASTNode {} + export interface IdentifierList extends BaseASTNode {} + export interface ElementaryTypeName extends BaseASTNode {} + export interface Expression extends BaseASTNode {} + export interface PrimaryExpression extends BaseASTNode {} + export interface ExpressionList extends BaseASTNode {} + export interface NameValueList extends BaseASTNode {} + export interface NameValue extends BaseASTNode {} + export interface FunctionCallArguments extends BaseASTNode {} + export interface AssemblyBlock extends BaseASTNode {} + export interface AssemblyItem extends BaseASTNode {} + export interface AssemblyExpression extends BaseASTNode {} + export interface AssemblyCall extends BaseASTNode {} + export interface AssemblyLocalDefinition extends BaseASTNode {} + export interface AssemblyAssignment extends BaseASTNode {} + export interface AssemblyIdentifierOrList extends BaseASTNode {} + export interface AssemblyIdentifierList extends BaseASTNode {} + export interface AssemblyStackAssignment extends BaseASTNode {} + export interface LabelDefinition extends BaseASTNode {} + export interface AssemblySwitch extends BaseASTNode {} + export interface AssemblyCase extends BaseASTNode {} + export interface AssemblyFunctionDefinition extends BaseASTNode {} + export interface AssemblyFunctionReturns extends BaseASTNode {} + export interface AssemblyFor extends BaseASTNode {} + export interface AssemblyIf extends BaseASTNode {} + export interface AssemblyLiteral extends BaseASTNode {} + export interface SubAssembly extends BaseASTNode {} + export interface TupleExpression extends BaseASTNode {} + export interface ElementaryTypeNameExpression extends BaseASTNode {} + export interface NumberLiteral extends BaseASTNode {} + export interface Identifier extends BaseASTNode {} + export type BinOp = + | '+' + | '-' + | '*' + | '/' + | '**' + | '%' + | '<<' + | '>>' + | '&&' + | '||' + | '&' + | '|' + | '^' + | '<' + | '>' + | '<=' + | '>=' + | '==' + | '!=' + | '=' + | '|=' + | '^=' + | '&=' + | '<<=' + | '>>=' + | '+=' + | '-=' + | '*=' + | '/=' + | '%='; + export interface BinaryOperation extends BaseASTNode { + left: ASTNode; + right: ASTNode; + operator: BinOp; + } + export interface Conditional extends BaseASTNode { + trueExpression: ASTNode; + falseExpression: ASTNode; + } + + export type ASTNode = + | SourceUnit + | PragmaDirective + | PragmaName + | PragmaValue + | Version + | VersionOperator + | VersionConstraint + | ImportDeclaration + | ImportDirective + | ContractDefinition + | InheritanceSpecifier + | ContractPart + | StateVariableDeclaration + | UsingForDeclaration + | StructDefinition + | ModifierDefinition + | ModifierInvocation + | FunctionDefinition + | ReturnParameters + | ModifierList + | EventDefinition + | EnumValue + | EnumDefinition + | ParameterList + | Parameter + | EventParameterList + | EventParameter + | FunctionTypeParameterList + | FunctionTypeParameter + | VariableDeclaration + | TypeName + | UserDefinedTypeName + | Mapping + | FunctionTypeName + | StorageLocation + | StateMutability + | Block + | Statement + | ExpressionStatement + | IfStatement + | WhileStatement + | SimpleStatement + | ForStatement + | InlineAssemblyStatement + | DoWhileStatement + | ContinueStatement + | BreakStatement + | ReturnStatement + | ThrowStatement + | VariableDeclarationStatement + | IdentifierList + | ElementaryTypeName + | Expression + | PrimaryExpression + | ExpressionList + | NameValueList + | NameValue + | FunctionCallArguments + | AssemblyBlock + | AssemblyItem + | AssemblyExpression + | AssemblyCall + | AssemblyLocalDefinition + | AssemblyAssignment + | AssemblyIdentifierOrList + | AssemblyIdentifierList + | AssemblyStackAssignment + | LabelDefinition + | AssemblySwitch + | AssemblyCase + | AssemblyFunctionDefinition + | AssemblyFunctionReturns + | AssemblyFor + | AssemblyIf + | AssemblyLiteral + | SubAssembly + | TupleExpression + | ElementaryTypeNameExpression + | NumberLiteral + | Identifier + | BinaryOperation + | Conditional; + export interface Visitor { + SourceUnit?: (node: SourceUnit) => void; + PragmaDirective?: (node: PragmaDirective) => void; + PragmaName?: (node: PragmaName) => void; + PragmaValue?: (node: PragmaValue) => void; + Version?: (node: Version) => void; + VersionOperator?: (node: VersionOperator) => void; + VersionConstraint?: (node: VersionConstraint) => void; + ImportDeclaration?: (node: ImportDeclaration) => void; + ImportDirective?: (node: ImportDirective) => void; + ContractDefinition?: (node: ContractDefinition) => void; + InheritanceSpecifier?: (node: InheritanceSpecifier) => void; + ContractPart?: (node: ContractPart) => void; + StateVariableDeclaration?: (node: StateVariableDeclaration) => void; + UsingForDeclaration?: (node: UsingForDeclaration) => void; + StructDefinition?: (node: StructDefinition) => void; + ModifierDefinition?: (node: ModifierDefinition) => void; + ModifierInvocation?: (node: ModifierInvocation) => void; + FunctionDefinition?: (node: FunctionDefinition) => void; + ReturnParameters?: (node: ReturnParameters) => void; + ModifierList?: (node: ModifierList) => void; + EventDefinition?: (node: EventDefinition) => void; + EnumValue?: (node: EnumValue) => void; + EnumDefinition?: (node: EnumDefinition) => void; + ParameterList?: (node: ParameterList) => void; + Parameter?: (node: Parameter) => void; + EventParameterList?: (node: EventParameterList) => void; + EventParameter?: (node: EventParameter) => void; + FunctionTypeParameterList?: (node: FunctionTypeParameterList) => void; + FunctionTypeParameter?: (node: FunctionTypeParameter) => void; + VariableDeclaration?: (node: VariableDeclaration) => void; + TypeName?: (node: TypeName) => void; + UserDefinedTypeName?: (node: UserDefinedTypeName) => void; + Mapping?: (node: Mapping) => void; + FunctionTypeName?: (node: FunctionTypeName) => void; + StorageLocation?: (node: StorageLocation) => void; + StateMutability?: (node: StateMutability) => void; + Block?: (node: Block) => void; + Statement?: (node: Statement) => void; + ExpressionStatement?: (node: ExpressionStatement) => void; + IfStatement?: (node: IfStatement) => void; + WhileStatement?: (node: WhileStatement) => void; + SimpleStatement?: (node: SimpleStatement) => void; + ForStatement?: (node: ForStatement) => void; + InlineAssemblyStatement?: (node: InlineAssemblyStatement) => void; + DoWhileStatement?: (node: DoWhileStatement) => void; + ContinueStatement?: (node: ContinueStatement) => void; + BreakStatement?: (node: BreakStatement) => void; + ReturnStatement?: (node: ReturnStatement) => void; + ThrowStatement?: (node: ThrowStatement) => void; + VariableDeclarationStatement?: (node: VariableDeclarationStatement) => void; + IdentifierList?: (node: IdentifierList) => void; + ElementaryTypeName?: (node: ElementaryTypeName) => void; + Expression?: (node: Expression) => void; + PrimaryExpression?: (node: PrimaryExpression) => void; + ExpressionList?: (node: ExpressionList) => void; + NameValueList?: (node: NameValueList) => void; + NameValue?: (node: NameValue) => void; + FunctionCallArguments?: (node: FunctionCallArguments) => void; + AssemblyBlock?: (node: AssemblyBlock) => void; + AssemblyItem?: (node: AssemblyItem) => void; + AssemblyExpression?: (node: AssemblyExpression) => void; + AssemblyCall?: (node: AssemblyCall) => void; + AssemblyLocalDefinition?: (node: AssemblyLocalDefinition) => void; + AssemblyAssignment?: (node: AssemblyAssignment) => void; + AssemblyIdentifierOrList?: (node: AssemblyIdentifierOrList) => void; + AssemblyIdentifierList?: (node: AssemblyIdentifierList) => void; + AssemblyStackAssignment?: (node: AssemblyStackAssignment) => void; + LabelDefinition?: (node: LabelDefinition) => void; + AssemblySwitch?: (node: AssemblySwitch) => void; + AssemblyCase?: (node: AssemblyCase) => void; + AssemblyFunctionDefinition?: (node: AssemblyFunctionDefinition) => void; + AssemblyFunctionReturns?: (node: AssemblyFunctionReturns) => void; + AssemblyFor?: (node: AssemblyFor) => void; + AssemblyIf?: (node: AssemblyIf) => void; + AssemblyLiteral?: (node: AssemblyLiteral) => void; + SubAssembly?: (node: SubAssembly) => void; + TupleExpression?: (node: TupleExpression) => void; + ElementaryTypeNameExpression?: (node: ElementaryTypeNameExpression) => void; + NumberLiteral?: (node: NumberLiteral) => void; + Identifier?: (node: Identifier) => void; + BinaryOperation?: (node: BinaryOperation) => void; + Conditional?: (node: Conditional) => void; + } + export interface ParserOpts { + range?: boolean; + } + export function parse(sourceCode: string, parserOpts: ParserOpts): ASTNode; + export function visit(ast: ASTNode, visitor: Visitor): void; +} diff --git a/packages/sol-cov/src/index.ts b/packages/sol-cov/src/index.ts new file mode 100644 index 000000000..e5c5e5be3 --- /dev/null +++ b/packages/sol-cov/src/index.ts @@ -0,0 +1 @@ +export { CoverageSubprovider } from './coverage_subprovider'; diff --git a/packages/sol-cov/src/instructions.ts b/packages/sol-cov/src/instructions.ts new file mode 100644 index 000000000..c6506e58d --- /dev/null +++ b/packages/sol-cov/src/instructions.ts @@ -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; +}; diff --git a/packages/sol-cov/src/source_maps.ts b/packages/sol-cov/src/source_maps.ts new file mode 100644 index 000000000..9b3ea9e24 --- /dev/null +++ b/packages/sol-cov/src/source_maps.ts @@ -0,0 +1,82 @@ +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 function getLocationByOffset(str: string): LocationByOffset { + 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 function parseSourceMap( + sourceCodes: string[], + srcMap: string, + bytecodeHex: string, + sources: string[], +): { [programCounter: number]: SourceRange } { + 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; +} diff --git a/packages/sol-cov/src/types.ts b/packages/sol-cov/src/types.ts new file mode 100644 index 000000000..d6491100b --- /dev/null +++ b/packages/sol-cov/src/types.ts @@ -0,0 +1,101 @@ +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 TraceInfoBase { + coveredPcs: number[]; + txHash: string; +} + +export interface TraceInfoNewContract extends TraceInfoBase { + address: 'NEW_CONTRACT'; + bytecode: string; +} + +export interface TraceInfoExistingContract extends TraceInfoBase { + address: string; + runtimeBytecode: string; +} + +export type TraceInfo = TraceInfoNewContract | TraceInfoExistingContract; diff --git a/packages/sol-cov/src/utils.ts b/packages/sol-cov/src/utils.ts new file mode 100644 index 000000000..f155043a1 --- /dev/null +++ b/packages/sol-cov/src/utils.ts @@ -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 + ); + }, +}; diff --git a/packages/sol-cov/tsconfig.json b/packages/sol-cov/tsconfig.json new file mode 100644 index 000000000..bdf315d59 --- /dev/null +++ b/packages/sol-cov/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "outDir": "lib" + }, + "include": [ + "./src/**/*", + "../../node_modules/types-bn/index.d.ts", + "../../node_modules/web3-typescript-typings/index.d.ts", + "../../node_modules/types-ethereumjs-util/index.d.ts" + ] +} diff --git a/packages/sol-cov/tslint.json b/packages/sol-cov/tslint.json new file mode 100644 index 000000000..015dce851 --- /dev/null +++ b/packages/sol-cov/tslint.json @@ -0,0 +1,6 @@ +{ + "extends": ["@0xproject/tslint-config"], + "rules": { + "completed-docs": false + } +} |