Make sol-cov work with truffle and other artifact adapters
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { CoverageSubprovider } from '@0xproject/sol-cov';
|
import { CoverageSubprovider, ZeroExArtifactAdapter } from '@0xproject/sol-cov';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import { constants } from './constants';
|
import { constants } from './constants';
|
||||||
@@ -16,6 +16,7 @@ export const coverage = {
|
|||||||
const artifactsPath = '../migrations/artifacts/1.0.0';
|
const artifactsPath = '../migrations/artifacts/1.0.0';
|
||||||
const contractsPath = 'src/contracts';
|
const contractsPath = 'src/contracts';
|
||||||
const defaultFromAddress = constants.TESTRPC_FIRST_ADDRESS;
|
const defaultFromAddress = constants.TESTRPC_FIRST_ADDRESS;
|
||||||
return new CoverageSubprovider(artifactsPath, contractsPath, defaultFromAddress);
|
const zeroExArtifactsAdapter = new ZeroExArtifactAdapter(artifactsPath, contractsPath);
|
||||||
|
return new CoverageSubprovider(zeroExArtifactsAdapter, defaultFromAddress);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import * as path from 'path';
|
|||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
networkId: 50,
|
networkId: 50,
|
||||||
artifactsDir: path.resolve(__dirname, '../../artifacts'),
|
artifactsDir: 'artifacts',
|
||||||
contractsDir: path.resolve(__dirname, '../../contracts'),
|
contractsDir: 'contracts',
|
||||||
ganacheLogFile: 'ganache.log',
|
ganacheLogFile: 'ganache.log',
|
||||||
txDefaults: {
|
txDefaults: {
|
||||||
from: devConstants.TESTRPC_FIRST_ADDRESS,
|
from: devConstants.TESTRPC_FIRST_ADDRESS,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { devConstants } from '@0xproject/dev-utils';
|
import { devConstants } from '@0xproject/dev-utils';
|
||||||
import { CoverageSubprovider } from '@0xproject/sol-cov';
|
import { CoverageSubprovider, ZeroExArtifactAdapter } from '@0xproject/sol-cov';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import { config } from './config';
|
import { config } from './config';
|
||||||
@@ -15,6 +15,7 @@ export const coverage = {
|
|||||||
},
|
},
|
||||||
_getCoverageSubprovider(): CoverageSubprovider {
|
_getCoverageSubprovider(): CoverageSubprovider {
|
||||||
const defaultFromAddress = devConstants.TESTRPC_FIRST_ADDRESS;
|
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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,9 +48,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@0xproject/subproviders": "^0.10.1",
|
"@0xproject/subproviders": "^0.10.1",
|
||||||
"@0xproject/types": "^0.6.3",
|
"@0xproject/types": "^0.6.3",
|
||||||
|
"@0xproject/utils": "^0.6.1",
|
||||||
|
"@0xproject/sol-compiler": "^0.4.3",
|
||||||
"@0xproject/typescript-typings": "^0.3.1",
|
"@0xproject/typescript-typings": "^0.3.1",
|
||||||
"@0xproject/utils": "^0.6.1",
|
"@0xproject/utils": "^0.6.1",
|
||||||
"ethereumjs-util": "^5.1.1",
|
"ethereumjs-util": "^5.1.1",
|
||||||
|
"rimraf": "^2.6.2",
|
||||||
"glob": "^7.1.2",
|
"glob": "^7.1.2",
|
||||||
"istanbul": "^0.4.5",
|
"istanbul": "^0.4.5",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
@@ -62,6 +65,7 @@
|
|||||||
"@0xproject/monorepo-scripts": "^0.1.19",
|
"@0xproject/monorepo-scripts": "^0.1.19",
|
||||||
"@0xproject/tslint-config": "^0.4.17",
|
"@0xproject/tslint-config": "^0.4.17",
|
||||||
"@types/istanbul": "^0.4.30",
|
"@types/istanbul": "^0.4.30",
|
||||||
|
"@types/rimraf": "^2.0.2",
|
||||||
"@types/mkdirp": "^0.5.1",
|
"@types/mkdirp": "^0.5.1",
|
||||||
"@types/mocha": "^2.2.42",
|
"@types/mocha": "^2.2.42",
|
||||||
"@types/node": "^8.0.53",
|
"@types/node": "^8.0.53",
|
||||||
|
|||||||
41
packages/sol-cov/src/artifact_adapters/0x.ts
Normal file
41
packages/sol-cov/src/artifact_adapters/0x.ts
Normal file
@@ -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<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());
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/sol-cov/src/artifact_adapters/abstract.ts
Normal file
5
packages/sol-cov/src/artifact_adapters/abstract.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { ContractData } from '../types';
|
||||||
|
|
||||||
|
export abstract class AbstractArtifactAdapter {
|
||||||
|
public abstract async collectContractsDataAsync(): Promise<ContractData[]>;
|
||||||
|
}
|
||||||
43
packages/sol-cov/src/artifact_adapters/truffle.ts
Normal file
43
packages/sol-cov/src/artifact_adapters/truffle.ts
Normal file
@@ -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<ContractData[]> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -6,7 +6,7 @@ import * as _ from 'lodash';
|
|||||||
import * as mkdirp from 'mkdirp';
|
import * as mkdirp from 'mkdirp';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { collectContractsData } from './collect_contract_data';
|
import { AbstractArtifactAdapter } from './artifact_adapters/abstract';
|
||||||
import { collectCoverageEntries } from './collect_coverage_entries';
|
import { collectCoverageEntries } from './collect_coverage_entries';
|
||||||
import { constants } from './constants';
|
import { constants } from './constants';
|
||||||
import { parseSourceMap } from './source_maps';
|
import { parseSourceMap } from './source_maps';
|
||||||
@@ -34,41 +34,23 @@ import { utils } from './utils';
|
|||||||
const mkdirpAsync = promisify<undefined>(mkdirp);
|
const mkdirpAsync = promisify<undefined>(mkdirp);
|
||||||
|
|
||||||
export class CoverageManager {
|
export class CoverageManager {
|
||||||
private _sourcesPath: string;
|
private _artifactAdapter: AbstractArtifactAdapter;
|
||||||
|
private _verbose: boolean;
|
||||||
private _traceInfos: TraceInfo[] = [];
|
private _traceInfos: TraceInfo[] = [];
|
||||||
private _contractsData: ContractData[] = [];
|
|
||||||
private _getContractCodeAsync: (address: string) => Promise<string>;
|
private _getContractCodeAsync: (address: string) => Promise<string>;
|
||||||
constructor(
|
private static _getSingleFileCoverageForTrace(
|
||||||
artifactsPath: string,
|
|
||||||
sourcesPath: string,
|
|
||||||
getContractCodeAsync: (address: string) => Promise<string>,
|
|
||||||
) {
|
|
||||||
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<void> {
|
|
||||||
const finalCoverage = await this._computeCoverageAsync();
|
|
||||||
const stringifiedCoverage = JSON.stringify(finalCoverage, null, '\t');
|
|
||||||
await mkdirpAsync('coverage');
|
|
||||||
fs.writeFileSync('coverage/coverage.json', stringifiedCoverage);
|
|
||||||
}
|
|
||||||
private _getSingleFileCoverageForTrace(
|
|
||||||
contractData: ContractData,
|
contractData: ContractData,
|
||||||
coveredPcs: number[],
|
coveredPcs: number[],
|
||||||
pcToSourceRange: { [programCounter: number]: SourceRange },
|
pcToSourceRange: { [programCounter: number]: SourceRange },
|
||||||
fileIndex: number,
|
fileIndex: number,
|
||||||
): Coverage {
|
): Coverage {
|
||||||
const fileName = contractData.sources[fileIndex];
|
const absoluteFileName = contractData.sources[fileIndex];
|
||||||
const coverageEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex]);
|
const coverageEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex]);
|
||||||
let sourceRanges = _.map(coveredPcs, coveredPc => pcToSourceRange[coveredPc]);
|
let sourceRanges = _.map(coveredPcs, coveredPc => pcToSourceRange[coveredPc]);
|
||||||
sourceRanges = _.compact(sourceRanges); // Some PC's don't map to a source range and we just ignore them.
|
sourceRanges = _.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.
|
// 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 = _.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 branchCoverage: BranchCoverage = {};
|
||||||
const branchIds = _.keys(coverageEntriesDescription.branchMap);
|
const branchIds = _.keys(coverageEntriesDescription.branchMap);
|
||||||
for (const branchId of branchIds) {
|
for (const branchId of branchIds) {
|
||||||
@@ -118,7 +100,6 @@ export class CoverageManager {
|
|||||||
);
|
);
|
||||||
statementCoverage[modifierStatementId] = isModifierCovered;
|
statementCoverage[modifierStatementId] = isModifierCovered;
|
||||||
}
|
}
|
||||||
const absoluteFileName = path.join(this._sourcesPath, fileName);
|
|
||||||
const partialCoverage = {
|
const partialCoverage = {
|
||||||
[absoluteFileName]: {
|
[absoluteFileName]: {
|
||||||
...coverageEntriesDescription,
|
...coverageEntriesDescription,
|
||||||
@@ -131,18 +112,53 @@ export class CoverageManager {
|
|||||||
};
|
};
|
||||||
return partialCoverage;
|
return partialCoverage;
|
||||||
}
|
}
|
||||||
|
constructor(
|
||||||
|
artifactAdapter: AbstractArtifactAdapter,
|
||||||
|
getContractCodeAsync: (address: string) => Promise<string>,
|
||||||
|
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<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> {
|
private async _computeCoverageAsync(): Promise<Coverage> {
|
||||||
|
const contractsData = await this._artifactAdapter.collectContractsDataAsync();
|
||||||
const collector = new Collector();
|
const collector = new Collector();
|
||||||
for (const traceInfo of this._traceInfos) {
|
for (const traceInfo of this._traceInfos) {
|
||||||
if (traceInfo.address !== constants.NEW_CONTRACT) {
|
if (traceInfo.address !== constants.NEW_CONTRACT) {
|
||||||
// Runtime transaction
|
// Runtime transaction
|
||||||
let runtimeBytecode = (traceInfo as TraceInfoExistingContract).runtimeBytecode;
|
let runtimeBytecode = (traceInfo as TraceInfoExistingContract).runtimeBytecode;
|
||||||
runtimeBytecode = addHexPrefix(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)) {
|
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 sourceMap = contractData.sourceMapRuntime;
|
||||||
const pcToSourceRange = parseSourceMap(
|
const pcToSourceRange = parseSourceMap(
|
||||||
contractData.sourceCodes,
|
contractData.sourceCodes,
|
||||||
@@ -151,7 +167,7 @@ export class CoverageManager {
|
|||||||
contractData.sources,
|
contractData.sources,
|
||||||
);
|
);
|
||||||
for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
|
for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
|
||||||
const singleFileCoverageForTrace = this._getSingleFileCoverageForTrace(
|
const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace(
|
||||||
contractData,
|
contractData,
|
||||||
traceInfo.coveredPcs,
|
traceInfo.coveredPcs,
|
||||||
pcToSourceRange,
|
pcToSourceRange,
|
||||||
@@ -163,13 +179,26 @@ export class CoverageManager {
|
|||||||
// Contract creation transaction
|
// Contract creation transaction
|
||||||
let bytecode = (traceInfo as TraceInfoNewContract).bytecode;
|
let bytecode = (traceInfo as TraceInfoNewContract).bytecode;
|
||||||
bytecode = addHexPrefix(bytecode);
|
bytecode = addHexPrefix(bytecode);
|
||||||
const contractData = _.find(this._contractsData, contractDataCandidate =>
|
const contractData = _.find(contractsData, contractDataCandidate => {
|
||||||
bytecode.startsWith(contractDataCandidate.bytecode),
|
// Library linking placeholder: __ConvertLib____________________________
|
||||||
) as ContractData;
|
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)) {
|
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 sourceMap = contractData.sourceMap;
|
||||||
const pcToSourceRange = parseSourceMap(
|
const pcToSourceRange = parseSourceMap(
|
||||||
contractData.sourceCodes,
|
contractData.sourceCodes,
|
||||||
@@ -178,7 +207,7 @@ export class CoverageManager {
|
|||||||
contractData.sources,
|
contractData.sources,
|
||||||
);
|
);
|
||||||
for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
|
for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
|
||||||
const singleFileCoverageForTrace = this._getSingleFileCoverageForTrace(
|
const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace(
|
||||||
contractData,
|
contractData,
|
||||||
traceInfo.coveredPcs,
|
traceInfo.coveredPcs,
|
||||||
pcToSourceRange,
|
pcToSourceRange,
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { Callback, ErrorCallback, NextCallback, Subprovider } from '@0xproject/subproviders';
|
import { Callback, ErrorCallback, NextCallback, Subprovider } from '@0xproject/subproviders';
|
||||||
import { BlockParam, CallData, JSONRPCRequestPayload, TransactionTrace, TxData } from '@0xproject/types';
|
import { BlockParam, CallData, JSONRPCRequestPayload, TransactionTrace, TxData } from '@0xproject/types';
|
||||||
|
import * as fs from 'fs';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import { Lock } from 'semaphore-async-await';
|
import { Lock } from 'semaphore-async-await';
|
||||||
|
|
||||||
|
import { AbstractArtifactAdapter } from './artifact_adapters/abstract';
|
||||||
import { constants } from './constants';
|
import { constants } from './constants';
|
||||||
import { CoverageManager } from './coverage_manager';
|
import { CoverageManager } from './coverage_manager';
|
||||||
|
import { getTracesByContractAddress } from './trace';
|
||||||
import { TraceInfoExistingContract, TraceInfoNewContract } from './types';
|
import { TraceInfoExistingContract, TraceInfoNewContract } from './types';
|
||||||
|
|
||||||
interface MaybeFakeTxData extends TxData {
|
interface MaybeFakeTxData extends TxData {
|
||||||
@@ -26,15 +29,15 @@ export class CoverageSubprovider extends Subprovider {
|
|||||||
private _defaultFromAddress: string;
|
private _defaultFromAddress: string;
|
||||||
/**
|
/**
|
||||||
* Instantiates a CoverageSubprovider instance
|
* Instantiates a CoverageSubprovider instance
|
||||||
* @param artifactsPath Path to the smart contract artifacts
|
* @param artifactAdapter Adapter for used artifacts format (0x, truffle, giveth, etc.)
|
||||||
* @param sourcesPath Path to the smart contract source files
|
|
||||||
* @param defaultFromAddress default from address to use when sending transactions
|
* @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();
|
super();
|
||||||
this._lock = new Lock();
|
this._lock = new Lock();
|
||||||
this._defaultFromAddress = defaultFromAddress;
|
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.
|
* 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 jsonRPCResponsePayload = await this.emitPayloadAsync(payload);
|
||||||
const trace: TransactionTrace = jsonRPCResponsePayload.result;
|
const trace: TransactionTrace = jsonRPCResponsePayload.result;
|
||||||
const coveredPcs = _.map(trace.structLogs, log => log.pc);
|
|
||||||
if (address === constants.NEW_CONTRACT) {
|
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 = {
|
const traceInfo: TraceInfoNewContract = {
|
||||||
coveredPcs,
|
coveredPcs,
|
||||||
txHash,
|
txHash,
|
||||||
@@ -129,15 +134,20 @@ export class CoverageSubprovider extends Subprovider {
|
|||||||
};
|
};
|
||||||
this._coverageManager.appendTraceInfo(traceInfo);
|
this._coverageManager.appendTraceInfo(traceInfo);
|
||||||
} else {
|
} else {
|
||||||
payload = { method: 'eth_getCode', params: [address, 'latest'] };
|
const tracesByContractAddress = getTracesByContractAddress(trace.structLogs, address);
|
||||||
const runtimeBytecode = (await this.emitPayloadAsync(payload)).result;
|
for (const subcallAddress of _.keys(tracesByContractAddress)) {
|
||||||
const traceInfo: TraceInfoExistingContract = {
|
payload = { method: 'eth_getCode', params: [subcallAddress, 'latest'] };
|
||||||
coveredPcs,
|
const runtimeBytecode = (await this.emitPayloadAsync(payload)).result;
|
||||||
txHash,
|
const traceForThatSubcall = tracesByContractAddress[subcallAddress];
|
||||||
address,
|
const coveredPcs = _.map(traceForThatSubcall, log => log.pc);
|
||||||
runtimeBytecode,
|
const traceInfo: TraceInfoExistingContract = {
|
||||||
};
|
coveredPcs,
|
||||||
this._coverageManager.appendTraceInfo(traceInfo);
|
txHash,
|
||||||
|
address: subcallAddress,
|
||||||
|
runtimeBytecode,
|
||||||
|
};
|
||||||
|
this._coverageManager.appendTraceInfo(traceInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private async _recordCallTraceAsync(callData: Partial<CallData>, blockNumber: BlockParam): Promise<void> {
|
private async _recordCallTraceAsync(callData: Partial<CallData>, blockNumber: BlockParam): Promise<void> {
|
||||||
|
|||||||
@@ -1 +1,5 @@
|
|||||||
export { CoverageSubprovider } from './coverage_subprovider';
|
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';
|
||||||
|
|||||||
100
packages/sol-cov/src/trace.ts
Normal file
100
packages/sol-cov/src/trace.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -4,16 +4,17 @@ import 'make-promises-safe';
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { collectContractsData } from '../src/collect_contract_data';
|
import { ZeroExArtifactAdapter } from '../src/artifact_adapters/0x';
|
||||||
|
|
||||||
const expect = chai.expect;
|
const expect = chai.expect;
|
||||||
|
|
||||||
describe('Collect contracts data', () => {
|
describe('Collect contracts data', () => {
|
||||||
describe('#collectContractsData', () => {
|
describe('#collectContractsData', () => {
|
||||||
it('correctly collects contracts data', () => {
|
it('correctly collects contracts data', async () => {
|
||||||
const artifactsPath = path.resolve(__dirname, 'fixtures/artifacts');
|
const artifactsPath = path.resolve(__dirname, 'fixtures/artifacts');
|
||||||
const sourcesPath = path.resolve(__dirname, 'fixtures/contracts');
|
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 => {
|
_.forEach(contractsData, contractData => {
|
||||||
expect(contractData).to.have.keys([
|
expect(contractData).to.have.keys([
|
||||||
'sourceCodes',
|
'sourceCodes',
|
||||||
|
|||||||
57
packages/sol-cov/test/trace_test.ts
Normal file
57
packages/sol-cov/test/trace_test.ts
Normal file
@@ -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<StructLog> & { 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user