From 974575b695108dd70f4b165f6789f71c3647c2b1 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 14 May 2018 20:01:18 +0200 Subject: Make sol-cov work with truffle and other artifact adapters --- packages/dev-utils/src/coverage.ts | 5 +- packages/metacoin/test/utils/config.ts | 4 +- packages/metacoin/test/utils/coverage.ts | 5 +- packages/sol-cov/package.json | 4 + packages/sol-cov/src/artifact_adapters/0x.ts | 41 +++++++++ packages/sol-cov/src/artifact_adapters/abstract.ts | 5 ++ packages/sol-cov/src/artifact_adapters/truffle.ts | 43 +++++++++ packages/sol-cov/src/collect_contract_data.ts | 31 ------- packages/sol-cov/src/coverage_manager.ts | 99 ++++++++++++-------- packages/sol-cov/src/coverage_subprovider.ts | 38 +++++--- packages/sol-cov/src/index.ts | 4 + packages/sol-cov/src/trace.ts | 100 +++++++++++++++++++++ .../sol-cov/test/collect_contracts_data_test.ts | 7 +- packages/sol-cov/test/trace_test.ts | 57 ++++++++++++ 14 files changed, 354 insertions(+), 89 deletions(-) create mode 100644 packages/sol-cov/src/artifact_adapters/0x.ts create mode 100644 packages/sol-cov/src/artifact_adapters/abstract.ts create mode 100644 packages/sol-cov/src/artifact_adapters/truffle.ts delete mode 100644 packages/sol-cov/src/collect_contract_data.ts create mode 100644 packages/sol-cov/src/trace.ts create mode 100644 packages/sol-cov/test/trace_test.ts (limited to 'packages') diff --git a/packages/dev-utils/src/coverage.ts b/packages/dev-utils/src/coverage.ts index 6f7640835..caf04672f 100644 --- a/packages/dev-utils/src/coverage.ts +++ b/packages/dev-utils/src/coverage.ts @@ -1,4 +1,4 @@ -import { CoverageSubprovider } from '@0xproject/sol-cov'; +import { CoverageSubprovider, ZeroExArtifactAdapter } from '@0xproject/sol-cov'; import * as _ from 'lodash'; import { constants } from './constants'; @@ -16,6 +16,7 @@ export const coverage = { const artifactsPath = '../migrations/artifacts/1.0.0'; const contractsPath = 'src/contracts'; const defaultFromAddress = constants.TESTRPC_FIRST_ADDRESS; - return new CoverageSubprovider(artifactsPath, contractsPath, defaultFromAddress); + const zeroExArtifactsAdapter = new ZeroExArtifactAdapter(artifactsPath, contractsPath); + return new CoverageSubprovider(zeroExArtifactsAdapter, defaultFromAddress); }, }; diff --git a/packages/metacoin/test/utils/config.ts b/packages/metacoin/test/utils/config.ts index 389edb388..ef4932845 100644 --- a/packages/metacoin/test/utils/config.ts +++ b/packages/metacoin/test/utils/config.ts @@ -3,8 +3,8 @@ import * as path from 'path'; export const config = { networkId: 50, - artifactsDir: path.resolve(__dirname, '../../artifacts'), - contractsDir: path.resolve(__dirname, '../../contracts'), + artifactsDir: 'artifacts', + contractsDir: 'contracts', ganacheLogFile: 'ganache.log', txDefaults: { from: devConstants.TESTRPC_FIRST_ADDRESS, diff --git a/packages/metacoin/test/utils/coverage.ts b/packages/metacoin/test/utils/coverage.ts index debd544ed..83b56596f 100644 --- a/packages/metacoin/test/utils/coverage.ts +++ b/packages/metacoin/test/utils/coverage.ts @@ -1,5 +1,5 @@ import { devConstants } from '@0xproject/dev-utils'; -import { CoverageSubprovider } from '@0xproject/sol-cov'; +import { CoverageSubprovider, ZeroExArtifactAdapter } from '@0xproject/sol-cov'; import * as _ from 'lodash'; import { config } from './config'; @@ -15,6 +15,7 @@ export const coverage = { }, _getCoverageSubprovider(): CoverageSubprovider { const defaultFromAddress = devConstants.TESTRPC_FIRST_ADDRESS; - return new CoverageSubprovider(config.artifactsDir, config.contractsDir, defaultFromAddress); + const zeroExArtifactsAdapter = new ZeroExArtifactAdapter(config.artifactsDir, config.contractsDir); + return new CoverageSubprovider(zeroExArtifactsAdapter, defaultFromAddress); }, }; diff --git a/packages/sol-cov/package.json b/packages/sol-cov/package.json index 28ceae0fa..593ff8356 100644 --- a/packages/sol-cov/package.json +++ b/packages/sol-cov/package.json @@ -48,9 +48,12 @@ "dependencies": { "@0xproject/subproviders": "^0.10.1", "@0xproject/types": "^0.6.3", + "@0xproject/utils": "^0.6.1", + "@0xproject/sol-compiler": "^0.4.3", "@0xproject/typescript-typings": "^0.3.1", "@0xproject/utils": "^0.6.1", "ethereumjs-util": "^5.1.1", + "rimraf": "^2.6.2", "glob": "^7.1.2", "istanbul": "^0.4.5", "lodash": "^4.17.4", @@ -62,6 +65,7 @@ "@0xproject/monorepo-scripts": "^0.1.19", "@0xproject/tslint-config": "^0.4.17", "@types/istanbul": "^0.4.30", + "@types/rimraf": "^2.0.2", "@types/mkdirp": "^0.5.1", "@types/mocha": "^2.2.42", "@types/node": "^8.0.53", diff --git a/packages/sol-cov/src/artifact_adapters/0x.ts b/packages/sol-cov/src/artifact_adapters/0x.ts new file mode 100644 index 000000000..87d23b0aa --- /dev/null +++ b/packages/sol-cov/src/artifact_adapters/0x.ts @@ -0,0 +1,41 @@ +import * as fs from 'fs'; +import * as glob from 'glob'; +import * as _ from 'lodash'; +import * as path from 'path'; + +import { ContractData } from '../types'; + +import { AbstractArtifactAdapter } from './abstract'; + +export class ZeroExArtifactAdapter extends AbstractArtifactAdapter { + private _artifactsPath: string; + private _sourcesPath: string; + constructor(artifactsPath: string, sourcesPath: string) { + super(); + this._artifactsPath = artifactsPath; + this._sourcesPath = sourcesPath; + } + public async collectContractsDataAsync(): Promise { + 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()); + let sources = _.keys(artifact.sources); + sources = _.map(sources, relativeFilePath => path.resolve(this._sourcesPath, relativeFilePath)); + const contractName = artifact.contractName; + // We don't compute coverage for dependencies + const sourceCodes = _.map(sources, (source: string) => fs.readFileSync(source).toString()); + const contractData = { + sourceCodes, + sources, + bytecode: artifact.compilerOutput.evm.bytecode.object, + sourceMap: artifact.compilerOutput.evm.bytecode.sourceMap, + runtimeBytecode: artifact.compilerOutput.evm.deployedBytecode.object, + sourceMapRuntime: artifact.compilerOutput.evm.deployedBytecode.sourceMap, + }; + contractsData.push(contractData); + } + return contractsData; + } +} diff --git a/packages/sol-cov/src/artifact_adapters/abstract.ts b/packages/sol-cov/src/artifact_adapters/abstract.ts new file mode 100644 index 000000000..fcc6562ad --- /dev/null +++ b/packages/sol-cov/src/artifact_adapters/abstract.ts @@ -0,0 +1,5 @@ +import { ContractData } from '../types'; + +export abstract class AbstractArtifactAdapter { + public abstract async collectContractsDataAsync(): Promise; +} diff --git a/packages/sol-cov/src/artifact_adapters/truffle.ts b/packages/sol-cov/src/artifact_adapters/truffle.ts new file mode 100644 index 000000000..e891bb464 --- /dev/null +++ b/packages/sol-cov/src/artifact_adapters/truffle.ts @@ -0,0 +1,43 @@ +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'; + +import { ZeroExArtifactAdapter } from './0x'; +import { AbstractArtifactAdapter } from './abstract'; + +export class TruffleArtifactAdapter extends AbstractArtifactAdapter { + private _solcVersion: string; + private _sourcesPath: string; + constructor(sourcesPath: string, solcVersion: string) { + super(); + this._solcVersion = solcVersion; + this._sourcesPath = sourcesPath; + } + public async collectContractsDataAsync(): Promise { + const artifactsDir = '0x-artifacts'; + const compilerOptions: CompilerOptions = { + contractsDir: this._sourcesPath, + artifactsDir, + compilerSettings: { + outputSelection: { + ['*']: { + ['*']: ['abi', 'evm.bytecode.object', 'evm.deployedBytecode.object'], + }, + }, + }, + contracts: '*', + solcVersion: this._solcVersion, + }; + const compiler = new Compiler(compilerOptions); + await compiler.compileAsync(); + const zeroExArtifactAdapter = new ZeroExArtifactAdapter(artifactsDir, this._sourcesPath); + const contractsDataFrom0xArtifacts = await zeroExArtifactAdapter.collectContractsDataAsync(); + rimraf.sync(artifactsDir); + return contractsDataFrom0xArtifacts; + } +} diff --git a/packages/sol-cov/src/collect_contract_data.ts b/packages/sol-cov/src/collect_contract_data.ts deleted file mode 100644 index 2c2a12835..000000000 --- a/packages/sol-cov/src/collect_contract_data.ts +++ /dev/null @@ -1,31 +0,0 @@ -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) => { - const artifactsGlob = `${artifactsPath}/**/*.json`; - const artifactFileNames = glob.sync(artifactsGlob, { absolute: true }); - const contractsData: ContractData[] = []; - _.forEach(artifactFileNames, artifactFileName => { - const artifact = JSON.parse(fs.readFileSync(artifactFileName).toString()); - const sources = _.keys(artifact.sources); - const contractName = artifact.contractName; - // We don't compute coverage for dependencies - const sourceCodes = _.map(sources, (source: string) => - fs.readFileSync(path.join(sourcesPath, source)).toString(), - ); - const contractData = { - sourceCodes, - sources, - bytecode: artifact.compilerOutput.evm.bytecode.object, - sourceMap: artifact.compilerOutput.evm.bytecode.sourceMap, - runtimeBytecode: artifact.compilerOutput.evm.deployedBytecode.object, - sourceMapRuntime: artifact.compilerOutput.evm.deployedBytecode.sourceMap, - }; - contractsData.push(contractData); - }); - return contractsData; -}; diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts index 800ca96dd..65a6086c0 100644 --- a/packages/sol-cov/src/coverage_manager.ts +++ b/packages/sol-cov/src/coverage_manager.ts @@ -6,7 +6,7 @@ import * as _ from 'lodash'; import * as mkdirp from 'mkdirp'; import * as path from 'path'; -import { collectContractsData } from './collect_contract_data'; +import { AbstractArtifactAdapter } from './artifact_adapters/abstract'; import { collectCoverageEntries } from './collect_coverage_entries'; import { constants } from './constants'; import { parseSourceMap } from './source_maps'; @@ -34,41 +34,23 @@ import { utils } from './utils'; const mkdirpAsync = promisify(mkdirp); export class CoverageManager { - private _sourcesPath: string; + private _artifactAdapter: AbstractArtifactAdapter; + private _verbose: boolean; private _traceInfos: TraceInfo[] = []; - private _contractsData: ContractData[] = []; private _getContractCodeAsync: (address: string) => Promise; - constructor( - artifactsPath: string, - sourcesPath: string, - getContractCodeAsync: (address: string) => Promise, - ) { - this._getContractCodeAsync = getContractCodeAsync; - this._sourcesPath = sourcesPath; - this._contractsData = collectContractsData(artifactsPath, this._sourcesPath); - } - public appendTraceInfo(traceInfo: TraceInfo): void { - this._traceInfos.push(traceInfo); - } - public async writeCoverageAsync(): Promise { - const finalCoverage = await this._computeCoverageAsync(); - const stringifiedCoverage = JSON.stringify(finalCoverage, null, '\t'); - await mkdirpAsync('coverage'); - fs.writeFileSync('coverage/coverage.json', stringifiedCoverage); - } - private _getSingleFileCoverageForTrace( + private static _getSingleFileCoverageForTrace( contractData: ContractData, coveredPcs: number[], pcToSourceRange: { [programCounter: number]: SourceRange }, fileIndex: number, ): Coverage { - const fileName = contractData.sources[fileIndex]; + 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 === fileName); + sourceRanges = _.filter(sourceRanges, sourceRange => sourceRange.fileName === absoluteFileName); const branchCoverage: BranchCoverage = {}; const branchIds = _.keys(coverageEntriesDescription.branchMap); for (const branchId of branchIds) { @@ -118,7 +100,6 @@ export class CoverageManager { ); statementCoverage[modifierStatementId] = isModifierCovered; } - const absoluteFileName = path.join(this._sourcesPath, fileName); const partialCoverage = { [absoluteFileName]: { ...coverageEntriesDescription, @@ -131,18 +112,53 @@ export class CoverageManager { }; return partialCoverage; } + constructor( + artifactAdapter: AbstractArtifactAdapter, + getContractCodeAsync: (address: string) => Promise, + verbose: boolean, + ) { + this._getContractCodeAsync = getContractCodeAsync; + this._artifactAdapter = artifactAdapter; + this._verbose = verbose; + } + public appendTraceInfo(traceInfo: TraceInfo): void { + // console.log(JSON.stringify(traceInfo, null, '\n')); + this._traceInfos.push(traceInfo); + } + public async writeCoverageAsync(): Promise { + 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 { + const contractsData = await this._artifactAdapter.collectContractsDataAsync(); const collector = new Collector(); for (const traceInfo of this._traceInfos) { if (traceInfo.address !== constants.NEW_CONTRACT) { // Runtime transaction let runtimeBytecode = (traceInfo as TraceInfoExistingContract).runtimeBytecode; runtimeBytecode = addHexPrefix(runtimeBytecode); - const contractData = _.find(this._contractsData, { runtimeBytecode }) as ContractData; + const contractData = _.find(contractsData, contractDataCandidate => { + // Library linking placeholder: __ConvertLib____________________________ + let runtimeBytecodeRegex = contractDataCandidate.runtimeBytecode.replace(/_.*_/, '.*'); + // Last 86 characters is solidity compiler metadata that's different between compilations + runtimeBytecodeRegex = runtimeBytecodeRegex.replace(/.{86}$/, ''); + // Libraries contain their own address at the beginning of the code and it's impossible to know it in advance + runtimeBytecodeRegex = runtimeBytecodeRegex.replace( + /^0x730000000000000000000000000000000000000000/, + '0x73........................................', + ); + return !_.isNull(runtimeBytecode.match(runtimeBytecodeRegex)); + }) as ContractData; if (_.isUndefined(contractData)) { - throw new Error(`Transaction to an unknown address: ${traceInfo.address}`); + if (this._verbose) { + // tslint:disable-next-line:no-console + console.warn(`Transaction to an unknown address: ${traceInfo.address}`); + } + continue; } - const bytecodeHex = contractData.runtimeBytecode.slice(2); + const bytecodeHex = runtimeBytecode.slice(2); const sourceMap = contractData.sourceMapRuntime; const pcToSourceRange = parseSourceMap( contractData.sourceCodes, @@ -151,7 +167,7 @@ export class CoverageManager { contractData.sources, ); for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) { - const singleFileCoverageForTrace = this._getSingleFileCoverageForTrace( + const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace( contractData, traceInfo.coveredPcs, pcToSourceRange, @@ -163,13 +179,26 @@ export class CoverageManager { // Contract creation transaction let bytecode = (traceInfo as TraceInfoNewContract).bytecode; bytecode = addHexPrefix(bytecode); - const contractData = _.find(this._contractsData, contractDataCandidate => - bytecode.startsWith(contractDataCandidate.bytecode), - ) as ContractData; + const contractData = _.find(contractsData, contractDataCandidate => { + // Library linking placeholder: __ConvertLib____________________________ + let bytecodeRegex = contractDataCandidate.bytecode.replace(/_.*_/, '.*'); + // Last 86 characters is solidity compiler metadata that's different between compilations + bytecodeRegex = bytecodeRegex.replace(/.{86}$/, ''); + // Libraries contain their own address at the beginning of the code and it's impossible to know it in advance + bytecodeRegex = bytecodeRegex.replace( + /^0x730000000000000000000000000000000000000000/, + '0x73........................................', + ); + return !_.isNull(bytecode.match(bytecodeRegex)); + }) as ContractData; if (_.isUndefined(contractData)) { - throw new Error(`Unknown contract creation transaction`); + if (this._verbose) { + // tslint:disable-next-line:no-console + console.warn(`Unknown contract creation transaction`); + } + continue; } - const bytecodeHex = contractData.bytecode.slice(2); + const bytecodeHex = bytecode.slice(2); const sourceMap = contractData.sourceMap; const pcToSourceRange = parseSourceMap( contractData.sourceCodes, @@ -178,7 +207,7 @@ export class CoverageManager { contractData.sources, ); for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) { - const singleFileCoverageForTrace = this._getSingleFileCoverageForTrace( + const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace( contractData, traceInfo.coveredPcs, pcToSourceRange, diff --git a/packages/sol-cov/src/coverage_subprovider.ts b/packages/sol-cov/src/coverage_subprovider.ts index 08efeaa24..f421a17fd 100644 --- a/packages/sol-cov/src/coverage_subprovider.ts +++ b/packages/sol-cov/src/coverage_subprovider.ts @@ -1,10 +1,13 @@ import { Callback, ErrorCallback, NextCallback, Subprovider } from '@0xproject/subproviders'; import { BlockParam, CallData, JSONRPCRequestPayload, TransactionTrace, TxData } from '@0xproject/types'; +import * as fs from 'fs'; import * as _ from 'lodash'; import { Lock } from 'semaphore-async-await'; +import { AbstractArtifactAdapter } from './artifact_adapters/abstract'; import { constants } from './constants'; import { CoverageManager } from './coverage_manager'; +import { getTracesByContractAddress } from './trace'; import { TraceInfoExistingContract, TraceInfoNewContract } from './types'; interface MaybeFakeTxData extends TxData { @@ -26,15 +29,15 @@ export class CoverageSubprovider extends Subprovider { private _defaultFromAddress: string; /** * Instantiates a CoverageSubprovider instance - * @param artifactsPath Path to the smart contract artifacts - * @param sourcesPath Path to the smart contract source files + * @param artifactAdapter Adapter for used artifacts format (0x, truffle, giveth, etc.) * @param defaultFromAddress default from address to use when sending transactions + * @param verbose If true, we will log any unknown transactions. Otherwise we will ignore them */ - constructor(artifactsPath: string, sourcesPath: string, defaultFromAddress: string) { + constructor(artifactAdapter: AbstractArtifactAdapter, defaultFromAddress: string, verbose: boolean = true) { super(); this._lock = new Lock(); this._defaultFromAddress = defaultFromAddress; - this._coverageManager = new CoverageManager(artifactsPath, sourcesPath, this._getContractCodeAsync.bind(this)); + this._coverageManager = new CoverageManager(artifactAdapter, this._getContractCodeAsync.bind(this), verbose); } /** * Write the test coverage results to a file in Istanbul format. @@ -119,8 +122,10 @@ export class CoverageSubprovider extends Subprovider { }; const jsonRPCResponsePayload = await this.emitPayloadAsync(payload); const trace: TransactionTrace = jsonRPCResponsePayload.result; - const coveredPcs = _.map(trace.structLogs, log => log.pc); if (address === constants.NEW_CONTRACT) { + // TODO handle calls to external contracts and contract creations from within the constructor + const structLogsOnDepth0 = _.filter(trace.structLogs, { depth: 0 }); + const coveredPcs = _.map(structLogsOnDepth0, log => log.pc); const traceInfo: TraceInfoNewContract = { coveredPcs, txHash, @@ -129,15 +134,20 @@ export class CoverageSubprovider extends Subprovider { }; 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); + const tracesByContractAddress = getTracesByContractAddress(trace.structLogs, address); + for (const subcallAddress of _.keys(tracesByContractAddress)) { + payload = { method: 'eth_getCode', params: [subcallAddress, 'latest'] }; + const runtimeBytecode = (await this.emitPayloadAsync(payload)).result; + const traceForThatSubcall = tracesByContractAddress[subcallAddress]; + const coveredPcs = _.map(traceForThatSubcall, log => log.pc); + const traceInfo: TraceInfoExistingContract = { + coveredPcs, + txHash, + address: subcallAddress, + runtimeBytecode, + }; + this._coverageManager.appendTraceInfo(traceInfo); + } } } private async _recordCallTraceAsync(callData: Partial, blockNumber: BlockParam): Promise { diff --git a/packages/sol-cov/src/index.ts b/packages/sol-cov/src/index.ts index e5c5e5be3..18031372b 100644 --- a/packages/sol-cov/src/index.ts +++ b/packages/sol-cov/src/index.ts @@ -1 +1,5 @@ export { CoverageSubprovider } from './coverage_subprovider'; +export { ZeroExArtifactAdapter } from './artifact_adapters/0x'; +export { TruffleArtifactAdapter } from './artifact_adapters/truffle'; +export { AbstractArtifactAdapter } from './artifact_adapters/abstract'; +export { ContractData } from './types'; diff --git a/packages/sol-cov/src/trace.ts b/packages/sol-cov/src/trace.ts new file mode 100644 index 000000000..6bc28989d --- /dev/null +++ b/packages/sol-cov/src/trace.ts @@ -0,0 +1,100 @@ +import { StructLog, TransactionTrace } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { addHexPrefix, stripHexPrefix } from 'ethereumjs-util'; +import * as fs from 'fs'; +import * as _ from 'lodash'; + +export interface TraceByContractAddress { + [contractAddress: string]: StructLog[]; +} +function padZeros(address: string) { + return addHexPrefix(_.padStart(stripHexPrefix(address), 40, '0')); +} + +export function getTracesByContractAddress(structLogs: StructLog[], startAddress: string): TraceByContractAddress { + const traceByContractAddress: TraceByContractAddress = {}; + let currentTraceSegment = []; + const callStack = [startAddress]; + // 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) { + 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 + currentTraceSegment.push(structLog); + + if (structLog.op === 'DELEGATECALL') { + const currentAddress = _.last(callStack) as string; + const jumpAddressOffset = 4; + const newAddress = padZeros(new BigNumber(addHexPrefix(structLog.stack[jumpAddressOffset])).toString(16)); + callStack.push(newAddress); + traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat( + currentTraceSegment, + ); + currentTraceSegment = []; + } else if (structLog.op === 'RETURN') { + const currentAddress = callStack.pop() as string; + traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat( + currentTraceSegment, + ); + currentTraceSegment = []; + } else if (structLog.op === 'STOP') { + const currentAddress = callStack.pop() as string; + traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat( + currentTraceSegment, + ); + currentTraceSegment = []; + if (i !== structLogs.length - 1) { + throw new Error('Malformed trace. STOP is not the last opcode executed'); + } + } else if (structLog.op === 'CREATE') { + console.warn( + "Detected a contract created from within another contract. Sol-cov currently doesn't support that scenario. We'll just skip that trace", + ); + return traceByContractAddress; + } else if (structLog.op === 'CALL') { + const currentAddress = _.last(callStack) as string; + const jumpAddressOffset = 2; + const newAddress = padZeros(new BigNumber(addHexPrefix(structLog.stack[jumpAddressOffset])).toString(16)); + callStack.push(newAddress); + traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat( + currentTraceSegment, + ); + currentTraceSegment = []; + } else if (structLog.op === 'CALLCODE') { + throw new Error('CALLCODE opcode unsupported by coverage'); + } else if (structLog.op === 'STATICCALL') { + throw new Error('STATICCALL opcode unsupported by coverage'); + } else if (structLog.op === 'REVERT') { + const currentAddress = callStack.pop() as string; + traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat( + currentTraceSegment, + ); + currentTraceSegment = []; + if (i !== structLogs.length - 1) { + throw new Error('Malformed trace. REVERT is not the last opcode executed'); + } + } else if (structLog.op === 'INVALID') { + const currentAddress = callStack.pop() as string; + traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat( + currentTraceSegment, + ); + currentTraceSegment = []; + if (i !== structLogs.length - 1) { + throw new Error('Malformed trace. INVALID is not the last opcode executed'); + } + } else if (structLog.op === 'SELFDESTRUCT') { + throw new Error('SELFDESTRUCT opcode unsupported by coverage'); + } + } + if (callStack.length !== 0) { + throw new Error('Malformed trace. Call stack non empty at the end'); + } + if (currentTraceSegment.length !== 0) { + throw new Error('Malformed trace. currentTraceSegment non empty at the end'); + } + return traceByContractAddress; +} diff --git a/packages/sol-cov/test/collect_contracts_data_test.ts b/packages/sol-cov/test/collect_contracts_data_test.ts index d84ac5a39..906b7f4e2 100644 --- a/packages/sol-cov/test/collect_contracts_data_test.ts +++ b/packages/sol-cov/test/collect_contracts_data_test.ts @@ -4,16 +4,17 @@ import 'make-promises-safe'; import 'mocha'; import * as path from 'path'; -import { collectContractsData } from '../src/collect_contract_data'; +import { ZeroExArtifactAdapter } from '../src/artifact_adapters/0x'; const expect = chai.expect; describe('Collect contracts data', () => { describe('#collectContractsData', () => { - it('correctly collects contracts data', () => { + it('correctly collects contracts data', async () => { const artifactsPath = path.resolve(__dirname, 'fixtures/artifacts'); const sourcesPath = path.resolve(__dirname, 'fixtures/contracts'); - const contractsData = collectContractsData(artifactsPath, sourcesPath); + const zeroExArtifactsAdapter = new ZeroExArtifactAdapter(artifactsPath, sourcesPath); + const contractsData = await zeroExArtifactsAdapter.collectContractsDataAsync(); _.forEach(contractsData, contractData => { expect(contractData).to.have.keys([ 'sourceCodes', diff --git a/packages/sol-cov/test/trace_test.ts b/packages/sol-cov/test/trace_test.ts new file mode 100644 index 000000000..b9d846732 --- /dev/null +++ b/packages/sol-cov/test/trace_test.ts @@ -0,0 +1,57 @@ +import { StructLog } from '@0xproject/types'; +import * as chai from 'chai'; +import * as fs from 'fs'; +import * as _ from 'lodash'; +import 'mocha'; +import * as path from 'path'; + +import { getTracesByContractAddress } from '../src/trace'; + +const expect = chai.expect; + +const DEFAULT_STRUCT_LOG: StructLog = { + depth: 0, + error: '', + gas: 0, + gasCost: 0, + memory: [], + op: 'DEFAULT', + pc: 0, + stack: [], + storage: {}, +}; + +function addDefaultStructLogFields(compactStructLog: Partial & { op: string; depth: number }): StructLog { + return { ...DEFAULT_STRUCT_LOG, ...compactStructLog }; +} + +describe('Trace', () => { + describe('#getTracesByContractAddress', () => { + it('correctly splits trace by contract address', () => { + const delegateCallAddress = '0x0000000000000000000000000000000000000002'; + const trace = [ + { + op: 'DELEGATECALL', + stack: ['0x', '0x', delegateCallAddress], + depth: 0, + }, + { + op: 'RETURN', + depth: 1, + }, + { + op: 'RETURN', + depth: 0, + }, + ]; + const fullTrace = _.map(trace, compactStructLog => addDefaultStructLogFields(compactStructLog)); + const startAddress = '0x0000000000000000000000000000000000000001'; + const traceByContractAddress = getTracesByContractAddress(fullTrace, startAddress); + const expectedTraceByContractAddress = { + [startAddress]: [fullTrace[0], fullTrace[2]], + [delegateCallAddress]: [fullTrace[1]], + }; + expect(traceByContractAddress).to.be.deep.equal(expectedTraceByContractAddress); + }); + }); +}); -- cgit