aboutsummaryrefslogtreecommitdiffstats
path: root/packages/sol-cov/src/coverage_manager.ts
diff options
context:
space:
mode:
authorLeonid Logvinov <logvinov.leon@gmail.com>2018-03-05 11:05:26 +0800
committerLeonid Logvinov <logvinov.leon@gmail.com>2018-03-12 10:37:27 +0800
commit13299158d1e22d1af1cd36434fc403a74743ecb1 (patch)
tree9b35435c6f8641d2dc3d7bfd530c7c4f040a1f51 /packages/sol-cov/src/coverage_manager.ts
parenta6571b09d2087ffb9a4860c0db3d7344321fe2c3 (diff)
downloaddexon-sol-tools-13299158d1e22d1af1cd36434fc403a74743ecb1.tar
dexon-sol-tools-13299158d1e22d1af1cd36434fc403a74743ecb1.tar.gz
dexon-sol-tools-13299158d1e22d1af1cd36434fc403a74743ecb1.tar.bz2
dexon-sol-tools-13299158d1e22d1af1cd36434fc403a74743ecb1.tar.lz
dexon-sol-tools-13299158d1e22d1af1cd36434fc403a74743ecb1.tar.xz
dexon-sol-tools-13299158d1e22d1af1cd36434fc403a74743ecb1.tar.zst
dexon-sol-tools-13299158d1e22d1af1cd36434fc403a74743ecb1.zip
Add sol-cover implementation
Diffstat (limited to 'packages/sol-cov/src/coverage_manager.ts')
-rw-r--r--packages/sol-cov/src/coverage_manager.ts166
1 files changed, 166 insertions, 0 deletions
diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts
new file mode 100644
index 000000000..aced9208f
--- /dev/null
+++ b/packages/sol-cov/src/coverage_manager.ts
@@ -0,0 +1,166 @@
+import * as fs from 'fs';
+import { Collector } from 'istanbul';
+import * as _ from 'lodash';
+import * as path from 'path';
+
+import { collectContractsData } from './collect_contract_data';
+import { constants } from './constants';
+import { collectCoverageEntries } from './instrument_solidity';
+import { parseSourceMap } from './source_maps';
+import {
+ BranchCoverage,
+ BranchDescription,
+ BranchMap,
+ ContractData,
+ Coverage,
+ FnMap,
+ FunctionCoverage,
+ FunctionDescription,
+ LineColumn,
+ SingleFileSourceRange,
+ SourceRange,
+ StatementCoverage,
+ StatementDescription,
+ StatementMap,
+ TraceInfo,
+} from './types';
+import { utils } from './utils';
+
+function getSingleFileCoverageForTrace(
+ contractData: ContractData,
+ coveredPcs: number[],
+ pcToSourceRange: { [programCounter: number]: SourceRange },
+ fileIndex: number,
+): Coverage {
+ const fileName = contractData.sources[fileIndex];
+ const coverageEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex], fileName);
+ let sourceRanges = _.map(coveredPcs, coveredPc => pcToSourceRange[coveredPc]);
+ sourceRanges = _.compact(sourceRanges); // Some PC's don't map to a source range and we just ignore them.
+ sourceRanges = _.uniqBy(sourceRanges, s => JSON.stringify(s)); // We don't care if one PC was covered multiple times within a single transaction
+ sourceRanges = _.filter(sourceRanges, sourceRange => sourceRange.fileName === fileName);
+ const branchCoverage: BranchCoverage = {};
+ for (const branchId of _.keys(coverageEntriesDescription.branchMap)) {
+ const branchDescription = coverageEntriesDescription.branchMap[branchId];
+ const isCovered = _.map(branchDescription.locations, location =>
+ _.some(sourceRanges, range => utils.isRangeInside(range.location, location)),
+ );
+ branchCoverage[branchId] = isCovered;
+ }
+ const statementCoverage: StatementCoverage = {};
+ for (const statementId of _.keys(coverageEntriesDescription.statementMap)) {
+ const statementDescription = coverageEntriesDescription.statementMap[statementId];
+ const isCovered = _.some(sourceRanges, range => utils.isRangeInside(range.location, statementDescription));
+ statementCoverage[statementId] = isCovered;
+ }
+ const functionCoverage: FunctionCoverage = {};
+ for (const fnId of _.keys(coverageEntriesDescription.fnMap)) {
+ const functionDescription = coverageEntriesDescription.fnMap[fnId];
+ const isCovered = _.some(sourceRanges, range => utils.isRangeInside(range.location, functionDescription.loc));
+ functionCoverage[fnId] = isCovered;
+ }
+ const partialCoverage = {
+ [contractData.sources[fileIndex]]: {
+ ...coverageEntriesDescription,
+ l: {}, // It's able to derive it from statement coverage
+ path: fileName,
+ f: functionCoverage,
+ s: statementCoverage,
+ b: branchCoverage,
+ },
+ };
+ return partialCoverage;
+}
+
+export class CoverageManager {
+ private _traceInfoByAddress: { [address: string]: TraceInfo[] } = {};
+ private _contractsData: ContractData[] = [];
+ private _txDataByHash: { [txHash: string]: string } = {};
+ private _getContractCodeAsync: (address: string) => Promise<string>;
+ constructor(
+ artifactsPath: string,
+ sourcesPath: string,
+ networkId: number,
+ getContractCodeAsync: (address: string) => Promise<string>,
+ ) {
+ this._getContractCodeAsync = getContractCodeAsync;
+ this._contractsData = collectContractsData(artifactsPath, sourcesPath, networkId);
+ }
+ public setTxDataByHash(txHash: string, data: string): void {
+ this._txDataByHash[txHash] = data;
+ }
+ public appendTraceInfo(address: string, traceInfo: TraceInfo): void {
+ if (_.isUndefined(this._traceInfoByAddress[address])) {
+ this._traceInfoByAddress[address] = [];
+ }
+ this._traceInfoByAddress[address].push(traceInfo);
+ }
+ public async writeCoverageAsync(): Promise<void> {
+ const finalCoverage = await this._computeCoverageAsync();
+ const jsonReplacer: null = null;
+ const numberOfJsonSpaces = 4;
+ const stringifiedCoverage = JSON.stringify(finalCoverage, jsonReplacer, numberOfJsonSpaces);
+ fs.writeFileSync('coverage/coverage.json', stringifiedCoverage);
+ }
+ private async _computeCoverageAsync(): Promise<Coverage> {
+ const collector = new Collector();
+ for (const address of _.keys(this._traceInfoByAddress)) {
+ if (address !== constants.NEW_CONTRACT) {
+ // Runtime transaction
+ const runtimeBytecode = await this._getContractCodeAsync(address);
+ const contractData = _.find(this._contractsData, { runtimeBytecode }) as ContractData;
+ if (_.isUndefined(contractData)) {
+ throw new Error(`Transaction to an unknown address: ${address}`);
+ }
+ const bytecodeHex = contractData.runtimeBytecode.slice(2);
+ const sourceMap = contractData.sourceMapRuntime;
+ const pcToSourceRange = parseSourceMap(
+ contractData.sourceCodes,
+ sourceMap,
+ bytecodeHex,
+ contractData.sources,
+ );
+ for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
+ _.forEach(this._traceInfoByAddress[address], (traceInfo: TraceInfo) => {
+ const singleFileCoverageForTrace = getSingleFileCoverageForTrace(
+ contractData,
+ traceInfo.coveredPcs,
+ pcToSourceRange,
+ fileIndex,
+ );
+ collector.add(singleFileCoverageForTrace);
+ });
+ }
+ } else {
+ // Contract creation transaction
+ _.forEach(this._traceInfoByAddress[address], (traceInfo: TraceInfo) => {
+ const bytecode = this._txDataByHash[traceInfo.txHash];
+ const contractData = _.find(this._contractsData, contractDataCandidate =>
+ bytecode.startsWith(contractDataCandidate.bytecode),
+ ) as ContractData;
+ if (_.isUndefined(contractData)) {
+ throw new Error(`Unknown contract creation transaction`);
+ }
+ const bytecodeHex = contractData.bytecode.slice(2);
+ const sourceMap = contractData.sourceMap;
+ const pcToSourceRange = parseSourceMap(
+ contractData.sourceCodes,
+ sourceMap,
+ bytecodeHex,
+ contractData.sources,
+ );
+ for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) {
+ const singleFileCoverageForTrace = getSingleFileCoverageForTrace(
+ contractData,
+ traceInfo.coveredPcs,
+ pcToSourceRange,
+ fileIndex,
+ );
+ collector.add(singleFileCoverageForTrace);
+ }
+ });
+ }
+ }
+ // TODO: Submit a PR to DT
+ return (collector as any).getFinalCoverage();
+ }
+}