Skip to content

Commit e9a0868

Browse files
committed
Add branch coverage for logical OR conditions (#499)
* Add initial OR coverage test cases * Add logicalOR coverage for "require" conditions * Add logicalOR coverage for "while" conditions * Add logicalOR coverage for "return" conditions * Add logicalOR coverage for "if" conditions * Add logicalOR branch highlighting for html report
1 parent 1ed5228 commit e9a0868

29 files changed

+934
-20
lines changed

lib/coverage.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,17 @@ class Coverage {
6969
const data = collectedData[hash];
7070
const contractPath = collectedData[hash].contractPath;
7171
const id = collectedData[hash].id;
72+
73+
// NB: Any branch using the injected fn which returns boolean will have artificially
74+
// doubled hits (because of something internal to Solidity about how the stack is managed)
7275
const hits = collectedData[hash].hits;
7376

7477
switch(collectedData[hash].type){
75-
case 'line': this.data[contractPath].l[id] = hits; break;
76-
case 'function': this.data[contractPath].f[id] = hits; break;
77-
case 'statement': this.data[contractPath].s[id] = hits; break;
78-
case 'branch': this.data[contractPath].b[id][data.locationIdx] = hits; break;
78+
case 'line': this.data[contractPath].l[id] = hits; break;
79+
case 'function': this.data[contractPath].f[id] = hits; break;
80+
case 'statement': this.data[contractPath].s[id] = hits; break;
81+
case 'branch': this.data[contractPath].b[id][data.locationIdx] = hits; break;
82+
case 'logicalOR': this.data[contractPath].b[id][data.locationIdx] = hits / 2; break;
7983
case 'requirePre': this.requireData[contractPath][id].preEvents = hits; break;
8084
case 'requirePost': this.requireData[contractPath][id].postEvents = hits; break;
8185
}

lib/injector.js

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,29 @@ class Injector {
1313
}
1414

1515
_getInjectable(id, hash, type){
16-
return `${this._getMethodIdentifier(id)}(${hash}); /* ${type} */ \n`;
16+
switch(type){
17+
case 'logicalOR':
18+
return ` && ${this._getTrueMethodIdentifier(id)}(${hash}))`;
19+
default:
20+
return `${this._getDefaultMethodIdentifier(id)}(${hash}); /* ${type} */ \n`;
21+
}
1722
}
1823

1924
_getHash(id) {
2025
this.hashCounter++;
2126
return web3Utils.keccak256(`${id}:${this.hashCounter}`);
2227
}
2328

24-
_getMethodIdentifier(id){
29+
// Method returns void
30+
_getDefaultMethodIdentifier(id){
2531
return `c_${web3Utils.keccak256(id).slice(0,10)}`
2632
}
2733

34+
// Method returns boolean true
35+
_getTrueMethodIdentifier(id){
36+
return `c_true${web3Utils.keccak256(id).slice(0,10)}`
37+
}
38+
2839
_getInjectionComponents(contract, injectionPoint, id, type){
2940
const { start, end } = this._split(contract, injectionPoint);
3041
const hash = this._getHash(id)
@@ -39,17 +50,29 @@ class Injector {
3950
}
4051

4152
/**
42-
* Generates a solidity statement injection. Declared once per fn.
43-
* Definition is the same for every fn in file.
53+
* Generates a solidity statement injection defining a method
54+
* *which returns void* to pass instrumentation hash to.
4455
* @param {String} id
4556
* @return {String} ex: bytes32[1] memory _sc_82e0891
4657
*/
47-
_getHashMethodDefinition(id, contract){
58+
_getDefaultMethodDefinition(id){
4859
const hash = web3Utils.keccak256(id).slice(0,10);
49-
const method = this._getMethodIdentifier(id);
60+
const method = this._getDefaultMethodIdentifier(id);
5061
return `\nfunction ${method}(bytes32 c__${hash}) public pure {}\n`;
5162
}
5263

64+
/**
65+
* Generates a solidity statement injection defining a method
66+
* *which returns boolean true* to pass instrumentation hash to.
67+
* @param {String} fileName
68+
* @return {String} ex: bytes32[1] memory _sc_82e0891
69+
*/
70+
_getTrueMethodDefinition(id){
71+
const hash = web3Utils.keccak256(id).slice(0,10);
72+
const method = this._getTrueMethodIdentifier(id);
73+
return `\nfunction ${method}(bytes32 c__${hash}) public pure returns (bool){ return true; }\n`;
74+
}
75+
5376
injectLine(contract, fileName, injectionPoint, injection, instrumentation){
5477
const type = 'line';
5578
const { start, end } = this._split(contract, injectionPoint);
@@ -204,7 +227,38 @@ class Injector {
204227
const start = contract.instrumented.slice(0, injectionPoint);
205228
const end = contract.instrumented.slice(injectionPoint);
206229
const id = `${fileName}:${injection.contractName}`;
207-
contract.instrumented = `${start}${this._getHashMethodDefinition(id)}${end}`;
230+
contract.instrumented = `${start}` +
231+
`${this._getDefaultMethodDefinition(id)}` +
232+
`${this._getTrueMethodDefinition(id)}` +
233+
`${end}`;
234+
}
235+
236+
injectOpenParen(contract, fileName, injectionPoint, injection, instrumentation){
237+
const start = contract.instrumented.slice(0, injectionPoint);
238+
const end = contract.instrumented.slice(injectionPoint);
239+
contract.instrumented = `${start}(${end}`;
240+
}
241+
242+
injectLogicalOR(contract, fileName, injectionPoint, injection, instrumentation){
243+
const type = 'logicalOR';
244+
const id = `${fileName}:${injection.contractName}`;
245+
246+
const {
247+
start,
248+
end,
249+
hash,
250+
injectable
251+
} = this._getInjectionComponents(contract, injectionPoint, id, type);
252+
253+
instrumentation[hash] = {
254+
id: injection.branchId,
255+
locationIdx: injection.locationIdx,
256+
type: type,
257+
contractPath: fileName,
258+
hits: 0
259+
}
260+
261+
contract.instrumented = `${start}${injectable}${end}`;
208262
}
209263
};
210264

lib/instrumenter.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,12 @@ class Instrumenter {
8888

8989
// Line instrumentation has to happen first
9090
contract.injectionPoints[injectionPoint].sort((a, b) => {
91-
const injections = ['injectBranch', 'injectEmptyBranch', 'injectLine'];
91+
const injections = [
92+
'injectLogicalOR',
93+
'injectBranch',
94+
'injectEmptyBranch',
95+
'injectLine'
96+
];
9297
return injections.indexOf(b.type) - injections.indexOf(a.type);
9398
});
9499

lib/parse.js

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,47 @@ parse.Block = function(contract, expression) {
3030
}
3131
};
3232

33-
parse.BinaryOperation = function(contract, expression) {
34-
register.statement(contract, expression);
33+
parse.BinaryOperation = function(contract, expression, skipStatementRegistry) {
34+
// Regular binary operation
35+
if (!skipStatementRegistry){
36+
register.statement(contract, expression);
37+
38+
// LogicalOR conditional search...
39+
} else {
40+
parse[expression.left.type] &&
41+
parse[expression.left.type](contract, expression.left, true);
42+
43+
parse[expression.right.type] &&
44+
parse[expression.right.type](contract, expression.right, true);
45+
46+
if (expression.operator === '||'){
47+
register.logicalOR(contract, expression);
48+
}
49+
}
50+
}
51+
52+
parse.TupleExpression = function(contract, expression, skipStatementRegistry) {
53+
expression.components.forEach(component => {
54+
parse[component.type] &&
55+
parse[component.type](contract, component, skipStatementRegistry);
56+
});
3557
}
3658

37-
parse.FunctionCall = function(contract, expression) {
59+
parse.FunctionCall = function(contract, expression, skipStatementRegistry) {
3860
// In any given chain of call expressions, only the last one will fail this check.
3961
// This makes sure we don't instrument a chain of expressions multiple times.
4062
if (expression.expression.type !== 'FunctionCall') {
41-
register.statement(contract, expression);
63+
64+
// Don't register sub-expressions (like intermediate method calls)
65+
if (!skipStatementRegistry){
66+
register.statement(contract, expression);
67+
}
68+
4269
if (expression.expression.name === 'require') {
4370
register.requireBranch(contract, expression);
71+
expression.arguments.forEach(arg => {
72+
parse[arg.type] && parse[arg.type](contract, arg, true);
73+
});
4474
}
4575
parse[expression.expression.type] &&
4676
parse[expression.expression.type](contract, expression.expression);
@@ -50,10 +80,13 @@ parse.FunctionCall = function(contract, expression) {
5080
}
5181
};
5282

53-
parse.Conditional = function(contract, expression) {
54-
register.statement(contract, expression);
55-
// TODO: Investigate node structure
56-
// There are potential substatements here we aren't measuring
83+
parse.Conditional = function(contract, expression, skipStatementRegistry) {
84+
if (!skipStatementRegistry){
85+
register.statement(contract, expression);
86+
}
87+
88+
parse[expression.condition.type] &&
89+
parse[expression.condition.type](contract, expression.condition, skipStatementRegistry);
5790
};
5891

5992
parse.ContractDefinition = function(contract, expression) {
@@ -130,6 +163,10 @@ parse.FunctionDefinition = function(contract, expression) {
130163
parse.IfStatement = function(contract, expression) {
131164
register.statement(contract, expression);
132165
register.ifStatement(contract, expression);
166+
167+
parse[expression.condition.type] &&
168+
parse[expression.condition.type](contract, expression.condition, true);
169+
133170
parse[expression.trueBody.type] &&
134171
parse[expression.trueBody.type](contract, expression.trueBody);
135172

@@ -173,6 +210,10 @@ parse.SourceUnit = function(contract, expression) {
173210

174211
parse.ReturnStatement = function(contract, expression) {
175212
register.statement(contract, expression);
213+
214+
expression.expression &&
215+
parse[expression.expression.type] &&
216+
parse[expression.expression.type](contract, expression.expression, true);
176217
};
177218

178219
// TODO:Investigate node structure
@@ -203,6 +244,10 @@ parse.VariableDeclarationStatement = function (contract, expression) {
203244

204245
parse.WhileStatement = function (contract, expression) {
205246
register.statement(contract, expression);
247+
248+
parse[expression.condition.type] &&
249+
parse[expression.condition.type](contract, expression.condition, true);
250+
206251
parse[expression.body.type] &&
207252
parse[expression.body.type](contract, expression.body);
208253
};

lib/registrar.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,107 @@ class Registrar {
186186
};
187187
};
188188

189+
/**
190+
* Registers injections for branch measurements. Used by logicalOR registration method.
191+
* @param {Object} contract instrumentation target
192+
* @param {Object} expression AST node
193+
*/
194+
addNewLogicalORBranch(contract, expression) {
195+
let start;
196+
197+
// Instabul HTML highlighting location data...
198+
const leftZero = expression.left.range[0];
199+
const leftOne = expression.left.range[1];
200+
const rightZero = expression.right.range[0];
201+
const rightOne = expression.right.range[1];
202+
203+
start = contract.instrumented.slice(0, leftZero);
204+
const leftStartLine = ( start.match(/\n/g) || [] ).length + 1;
205+
const leftStartCol = leftZero - start.lastIndexOf('\n') - 1;
206+
207+
start = contract.instrumented.slice(0, leftOne);
208+
const leftEndLine = ( start.match(/\n/g) || [] ).length + 1;
209+
const leftEndCol = leftOne - start.lastIndexOf('\n') - 1;
210+
211+
start = contract.instrumented.slice(0, rightZero);
212+
const rightStartLine = ( start.match(/\n/g) || [] ).length + 1;
213+
const rightStartCol = rightZero - start.lastIndexOf('\n') - 1;
214+
215+
start = contract.instrumented.slice(0, rightOne);
216+
const rightEndLine = ( start.match(/\n/g) || [] ).length + 1;
217+
const rightEndCol = rightOne - start.lastIndexOf('\n') - 1;
218+
219+
contract.branchId += 1;
220+
221+
// NB locations for if branches in istanbul are zero
222+
// length and associated with the start of the if.
223+
contract.branchMap[contract.branchId] = {
224+
line: leftStartLine,
225+
type: 'cond-expr',
226+
locations: [{
227+
start: {
228+
line: leftStartLine, column: leftStartCol,
229+
},
230+
end: {
231+
line: leftEndLine, column: leftEndCol,
232+
},
233+
}, {
234+
start: {
235+
line: rightStartLine, column: rightStartCol,
236+
},
237+
end: {
238+
line: rightEndLine, column: rightEndCol,
239+
},
240+
}],
241+
};
242+
};
243+
244+
/**
245+
* Registers injections for logicalOR clause measurements (branches)
246+
* @param {Object} contract instrumentation target
247+
* @param {Object} expression AST node
248+
* @param {Number} injectionIdx pre/post branch index (left=0, right=1)
249+
*/
250+
logicalOR(contract, expression) {
251+
this.addNewLogicalORBranch(contract, expression);
252+
253+
// Left
254+
this._createInjectionPoint(
255+
contract,
256+
expression.left.range[0],
257+
{
258+
type: 'injectOpenParen',
259+
}
260+
);
261+
this._createInjectionPoint(
262+
contract,
263+
expression.left.range[1] + 1,
264+
{
265+
type: 'injectLogicalOR',
266+
branchId: contract.branchId,
267+
locationIdx: 0
268+
}
269+
);
270+
271+
// Right
272+
this._createInjectionPoint(
273+
contract,
274+
expression.right.range[0],
275+
{
276+
type: 'injectOpenParen',
277+
}
278+
);
279+
this._createInjectionPoint(
280+
contract,
281+
expression.right.range[1] + 1,
282+
{
283+
type: 'injectLogicalOR',
284+
branchId: contract.branchId,
285+
locationIdx: 1
286+
}
287+
);
288+
}
289+
189290
/**
190291
* Registers injections for require statement measurements (branches)
191292
* @param {Object} contract instrumentation target
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Testing hooks
2+
const fn = (msg, config) => config.logger.log(msg);
3+
4+
module.exports = {
5+
skipFiles: ['Migrations.sol'],
6+
silent: process.env.SILENT ? true : false,
7+
istanbulReporter: ['json-summary', 'text'],
8+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const { loadPluginFile } = require("@nomiclabs/buidler/plugins-testing");
2+
loadPluginFile(__dirname + "/../plugins/buidler.plugin");
3+
usePlugin("@nomiclabs/buidler-truffle5");
4+
5+
module.exports={
6+
defaultNetwork: "buidlerevm"
7+
};

0 commit comments

Comments
 (0)