mirror of https://github.com/embarklabs/embark.git
feat(coverage): gas usage improvements
Do not submit event for function and branch but detect where the statement is
This commit is contained in:
parent
8a6d075cd1
commit
0118b1a409
|
@ -1,5 +1,5 @@
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import parser, { Location } from "solidity-parser-antlr";
|
import parser, { LineColumn, Location } from "solidity-parser-antlr";
|
||||||
import { EventLog } from "web3/types";
|
import { EventLog } from "web3/types";
|
||||||
|
|
||||||
import { decrypt } from "./eventId";
|
import { decrypt } from "./eventId";
|
||||||
|
@ -12,11 +12,8 @@ import { BranchType, Coverage } from "./types";
|
||||||
|
|
||||||
const fs = require("../../core/fs");
|
const fs = require("../../core/fs");
|
||||||
|
|
||||||
enum EventEnum {
|
const STATEMENT_EVENT = "__StatementCoverage";
|
||||||
statement = "__StatementCoverage",
|
const POINT_FACTOR = 1000000000;
|
||||||
branch = "__BranchCoverage",
|
|
||||||
function = "__FunctionCoverage",
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = 0;
|
let id = 0;
|
||||||
function nextId() {
|
function nextId() {
|
||||||
|
@ -31,6 +28,7 @@ export class ContractEnhanced {
|
||||||
public source: string;
|
public source: string;
|
||||||
private ast: parser.ASTNode;
|
private ast: parser.ASTNode;
|
||||||
private coverageFilepath: string;
|
private coverageFilepath: string;
|
||||||
|
private functionsBodyLocation: {[id: number]: Location} = {};
|
||||||
|
|
||||||
constructor(public filepath: string) {
|
constructor(public filepath: string) {
|
||||||
this.id = nextId();
|
this.id = nextId();
|
||||||
|
@ -71,29 +69,24 @@ export class ContractEnhanced {
|
||||||
public updateCoverage(events: EventLog[]) {
|
public updateCoverage(events: EventLog[]) {
|
||||||
events.filter(this.filterCoverageEvent).forEach((event) => {
|
events.filter(this.filterCoverageEvent).forEach((event) => {
|
||||||
const value = parseInt(event.returnValues[0], 10);
|
const value = parseInt(event.returnValues[0], 10);
|
||||||
const {contractId, injectionPointId, locationIdx} = decrypt(value);
|
const {contractId, injectionPointId} = decrypt(value);
|
||||||
|
|
||||||
if (contractId !== this.id) {
|
if (contractId !== this.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (event.event) {
|
|
||||||
case "__StatementCoverage": {
|
|
||||||
this.coverage.s[injectionPointId] += 1;
|
this.coverage.s[injectionPointId] += 1;
|
||||||
const statement = this.coverage.statementMap[injectionPointId];
|
const location = this.coverage.statementMap[injectionPointId];
|
||||||
this.coverage.l[statement.start.line] += 1;
|
this.coverage.l[location.start.line] += 1;
|
||||||
break;
|
|
||||||
}
|
const fnMapId = this.findFnMapId(location);
|
||||||
case "__FunctionCoverage": {
|
if (fnMapId) {
|
||||||
this.coverage.f[injectionPointId] += 1;
|
this.coverage.f[fnMapId] += 1;
|
||||||
const fn = this.coverage.fnMap[injectionPointId];
|
|
||||||
this.coverage.l[fn.line] += 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "__BranchCoverage": {
|
|
||||||
this.coverage.b[injectionPointId][locationIdx] += 1;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [fnBranchId, locationId] = this.findFnBranchIdAndLocationId(location);
|
||||||
|
if (fnBranchId) {
|
||||||
|
this.coverage.b[fnBranchId][locationId] += 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -118,7 +111,7 @@ export class ContractEnhanced {
|
||||||
return coverageId;
|
return coverageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public addFunction(location: Location, name: string) {
|
public addFunction(location: Location, name: string, bodyLocation: Location) {
|
||||||
const coverageId = this.getNewCoverageId(this.coverage.fnMap);
|
const coverageId = this.getNewCoverageId(this.coverage.fnMap);
|
||||||
const line = location.start.line;
|
const line = location.start.line;
|
||||||
this.coverage.fnMap[coverageId] = {
|
this.coverage.fnMap[coverageId] = {
|
||||||
|
@ -127,7 +120,7 @@ export class ContractEnhanced {
|
||||||
name,
|
name,
|
||||||
};
|
};
|
||||||
this.coverage.f[coverageId] = 0;
|
this.coverage.f[coverageId] = 0;
|
||||||
this.coverage.l[line] = 0;
|
this.functionsBodyLocation[coverageId] = bodyLocation;
|
||||||
return coverageId;
|
return coverageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +130,51 @@ export class ContractEnhanced {
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterCoverageEvent(event: EventLog) {
|
private filterCoverageEvent(event: EventLog) {
|
||||||
return [EventEnum.function, EventEnum.branch, EventEnum.statement].includes(event.event as EventEnum);
|
return STATEMENT_EVENT === event.event;
|
||||||
|
}
|
||||||
|
|
||||||
|
private findFnMapId(location: Location) {
|
||||||
|
const result = Object.keys(this.functionsBodyLocation).find((value: string) => {
|
||||||
|
const bodyLocation = this.functionsBodyLocation[parseInt(value, 10)];
|
||||||
|
|
||||||
|
return this.isWithin(location.start, bodyLocation);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
return parseInt(result, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private findFnBranchIdAndLocationId(location: Location) {
|
||||||
|
let fnBranchId = 0;
|
||||||
|
let locationId = 0;
|
||||||
|
Object.keys(this.coverage.branchMap).forEach((value: string) => {
|
||||||
|
if (fnBranchId && locationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const branch = this.coverage.branchMap[parseInt(value, 10)];
|
||||||
|
|
||||||
|
branch.locations.forEach((branchLocation, index) => {
|
||||||
|
if (this.isWithin(location.start, branchLocation)) {
|
||||||
|
fnBranchId = parseInt(value, 10);
|
||||||
|
locationId = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return [fnBranchId, locationId];
|
||||||
|
}
|
||||||
|
|
||||||
|
private isWithin(point: LineColumn, location: Location) {
|
||||||
|
const toCompare = this.getComparablePoint(point);
|
||||||
|
return toCompare >= this.getComparablePoint(location.start) && toCompare <= this.getComparablePoint(location.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getComparablePoint(point: LineColumn) {
|
||||||
|
return point.line * POINT_FACTOR + point.column;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
const CONTRACT_ID_FACTOR = 100000000;
|
const CONTRACT_ID_FACTOR = 1000000;
|
||||||
const INJECTION_POINT_ID_FACTOR = 10000;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the 3 params as uint32 where the first 2 digits are for contractsId,
|
* Convert the 2 params as uint32 where the first 4 digits are for contractsId,
|
||||||
* the next 3 digit are for injectionPoint id and the rest if for the locationIds
|
* the followings one are for injectionPoint id
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
* @param {number} contractId
|
* @param {number} contractId
|
||||||
* @param {number} injectionPointId
|
* @param {number} injectionPointId
|
||||||
* @param {number} [locationIdx]
|
* @returns a number representing the 2 params as uint32
|
||||||
* @returns a number representing the 3 params as uint32
|
|
||||||
*/
|
*/
|
||||||
export function encrypt(contractId: number, injectionPointId: number, locationIdx?: number) {
|
export function encrypt(contractId: number, injectionPointId: number) {
|
||||||
return contractId * CONTRACT_ID_FACTOR + injectionPointId * INJECTION_POINT_ID_FACTOR + (locationIdx || 0);
|
return contractId * CONTRACT_ID_FACTOR + injectionPointId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Explore the uint32 into contractId, injectionPointId and locationIds where
|
* Explore the uint32 into contractId, injectionPointId and locationIds where
|
||||||
* the first 2 digits are for contractsId,
|
* the first 4 digits are for contractsId,
|
||||||
* the next 3 digit are for injectionPoint id and the rest if for the locationIds
|
* the rest are for injectionPoint id
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
* @param {number} value
|
* @param {number} value
|
||||||
|
@ -26,8 +24,7 @@ export function encrypt(contractId: number, injectionPointId: number, locationId
|
||||||
*/
|
*/
|
||||||
export function decrypt(value: number) {
|
export function decrypt(value: number) {
|
||||||
const contractId = Math.floor(value / CONTRACT_ID_FACTOR);
|
const contractId = Math.floor(value / CONTRACT_ID_FACTOR);
|
||||||
const injectionPointId = Math.floor(value / INJECTION_POINT_ID_FACTOR) - contractId * INJECTION_POINT_ID_FACTOR;
|
const injectionPointId = value - contractId * CONTRACT_ID_FACTOR;
|
||||||
const locationIdx = value - contractId * CONTRACT_ID_FACTOR - injectionPointId * INJECTION_POINT_ID_FACTOR;
|
|
||||||
|
|
||||||
return {contractId, injectionPointId, locationIdx};
|
return {contractId, injectionPointId};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as globule from "globule";
|
import * as globule from "globule";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { Contract as Web3Contract } from "web3/types";
|
import Web3Contract from "web3/eth/contract";
|
||||||
|
|
||||||
import { Contract } from "../../../typings/contract";
|
import { Contract } from "../../../typings/contract";
|
||||||
import { Embark } from "../../../typings/embark";
|
import { Embark } from "../../../typings/embark";
|
||||||
|
|
|
@ -11,10 +11,6 @@ export class Injector {
|
||||||
switch (injectionPoint.type) {
|
switch (injectionPoint.type) {
|
||||||
case "statement":
|
case "statement":
|
||||||
return this.statement(injectionPoint);
|
return this.statement(injectionPoint);
|
||||||
case "function":
|
|
||||||
return this.function(injectionPoint);
|
|
||||||
case "branch":
|
|
||||||
return this.branch(injectionPoint);
|
|
||||||
case "contractDefinition":
|
case "contractDefinition":
|
||||||
return this.contractDefinition(injectionPoint);
|
return this.contractDefinition(injectionPoint);
|
||||||
}
|
}
|
||||||
|
@ -25,21 +21,9 @@ export class Injector {
|
||||||
this.insertAt(injectionPoint.location.start.line - 1, data);
|
this.insertAt(injectionPoint.location.start.line - 1, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function(injectionPoint: InjectionPoint) {
|
|
||||||
const data = `emit __FunctionCoverage(${encrypt(this.contract.id, injectionPoint.id)});`;
|
|
||||||
this.insertAt(injectionPoint.location.start.line, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private branch(injectionPoint: InjectionPoint) {
|
|
||||||
const data = `emit __BranchCoverage(${encrypt(this.contract.id, injectionPoint.id, injectionPoint.locationIdx)});`;
|
|
||||||
this.insertAt(injectionPoint.location.start.line, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private contractDefinition(injectionPoint: InjectionPoint) {
|
private contractDefinition(injectionPoint: InjectionPoint) {
|
||||||
const data = [
|
const data = [
|
||||||
"event __FunctionCoverage(uint32 value);",
|
|
||||||
"event __StatementCoverage(uint32 value);",
|
"event __StatementCoverage(uint32 value);",
|
||||||
"event __BranchCoverage(uint32 value);",
|
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
this.insertAt(injectionPoint.location.start.line, data);
|
this.insertAt(injectionPoint.location.start.line, data);
|
||||||
|
|
|
@ -44,8 +44,7 @@ export class Instrumenter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = this.contract.addFunction(node.loc, node.name);
|
this.contract.addFunction(node.loc, node.name, node.body.loc);
|
||||||
this.addInjectionPoints("function", id, node.body.loc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public instrumentStatement(node: Statement) {
|
public instrumentStatement(node: Statement) {
|
||||||
|
@ -69,10 +68,7 @@ export class Instrumenter {
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const id = this.contract.addBranch(node.loc.start.line, "if", locations);
|
this.contract.addBranch(node.loc.start.line, "if", locations);
|
||||||
locations.forEach((location, index) => {
|
|
||||||
this.addInjectionPoints("branch", id, location, index);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private addInjectionPoints(type: InjectionPointType, id: number, location: Location, locationIdx?: number) {
|
private addInjectionPoints(type: InjectionPointType, id: number, location: Location, locationIdx?: number) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Location } from "solidity-parser-antlr";
|
import { Location } from "solidity-parser-antlr";
|
||||||
|
|
||||||
export type InjectionPointType = "statement" | "branch" | "function" | "contractDefinition";
|
export type InjectionPointType = "statement" | "contractDefinition";
|
||||||
export type BranchType = "if" | "switch";
|
export type BranchType = "if" | "switch";
|
||||||
|
|
||||||
export interface InjectionPoint {
|
export interface InjectionPoint {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Handlebars from "handlebars";
|
import Handlebars from "handlebars";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { ABIDefinition } from "web3/types";
|
import { ABIDefinition } from "web3/eth/abi";
|
||||||
|
|
||||||
import { Contract } from "../../../../../typings/contract";
|
import { Contract } from "../../../../../typings/contract";
|
||||||
import { Embark } from "../../../../../typings/embark";
|
import { Embark } from "../../../../../typings/embark";
|
||||||
|
|
Loading…
Reference in New Issue