Skip to content

Commit 235866f

Browse files
committed
Add modifier-invocation-as-branch coverage (#588)
1 parent d3c7469 commit 235866f

26 files changed

+668
-7
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ module.exports = {
122122
| skipFiles | *Array* | `['Migrations.sol']` | Array of contracts or folders (with paths expressed relative to the `contracts` directory) that should be skipped when doing instrumentation. |
123123
| measureStatementCoverage | *boolean* | `true` | Computes statement (in addition to line) coverage. [More...][34] |
124124
| measureFunctionCoverage | *boolean* | `true` | Computes function coverage. [More...][34] |
125+
| measureModifierCoverage | *boolean* | `true` | Computes each modifier invocation as a code branch. [More...][34] |
125126
| istanbulFolder | *String* | `./coverage` | Folder location for Istanbul coverage reports. |
126127
| istanbulReporter | *Array* | `['html', 'lcov', 'text', 'json']` | [Istanbul coverage reporters][2] |
127128
| mocha | *Object* | `{ }` | [Mocha options][3] to merge into existing mocha config. `grep` and `invert` are useful for skipping certain tests under coverage using tags in the test descriptions.|

lib/injector.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const web3Utils = require("web3-utils");
33
class Injector {
44
constructor(){
55
this.hashCounter = 0;
6+
this.modifiers = {};
67
}
78

89
_split(contract, injectionPoint){
@@ -18,6 +19,8 @@ class Injector {
1819
return ` && ${this._getTrueMethodIdentifier(id)}(${hash}))`;
1920
case 'or-false':
2021
return ` || ${this._getFalseMethodIdentifier(id)}(${hash}))`;
22+
case 'modifier':
23+
return ` ${this._getModifierIdentifier(id)} `;
2124
default:
2225
return `${this._getDefaultMethodIdentifier(id)}(${hash}); /* ${type} */ \n`;
2326
}
@@ -43,6 +46,10 @@ class Injector {
4346
return `c_false${web3Utils.keccak256(id).slice(0,10)}`
4447
}
4548

49+
_getModifierIdentifier(id){
50+
return `c_mod${web3Utils.keccak256(id).slice(0,10)}`
51+
}
52+
4653
_getInjectionComponents(contract, injectionPoint, id, type){
4754
const { start, end } = this._split(contract, injectionPoint);
4855
const hash = this._getHash(id)
@@ -92,6 +99,57 @@ class Injector {
9299
return `function ${method}(bytes32 c__${hash}) public pure returns (bool){ return false; }\n`;
93100
}
94101

102+
_getModifierDefinitions(contractId, instrumentation){
103+
let injection = '';
104+
105+
if (this.modifiers[contractId]){
106+
107+
for (const item of this.modifiers[contractId]){
108+
injection += `modifier ${this._getModifierIdentifier(item.modifierId)}{ `;
109+
110+
let hash = this._getHash(item.modifierId);
111+
let injectable = this._getInjectable(item.contractId, hash, 'modifier-pre');
112+
113+
instrumentation[hash] = {
114+
id: item.branchId,
115+
type: 'requirePre',
116+
contractPath: item.fileName,
117+
hits: 0
118+
}
119+
120+
injection += injectable;
121+
injection += `_;`
122+
123+
hash = this._getHash(item.modifierId);
124+
injectable = this._getInjectable(item.contractId, hash, 'modifier-post');
125+
126+
instrumentation[hash] = {
127+
id: item.branchId,
128+
type: 'requirePost',
129+
contractPath: item.fileName,
130+
hits: 0
131+
}
132+
133+
injection += injectable;
134+
injection += ` }\n`;
135+
}
136+
}
137+
138+
return injection;
139+
}
140+
141+
_cacheModifier(injection){
142+
if (!this.modifiers[injection.contractId]) {
143+
this.modifiers[injection.contractId] = [];
144+
}
145+
146+
this.modifiers[injection.contractId].push(injection);
147+
}
148+
149+
resetModifierMapping(){
150+
this.modifiers = {};
151+
}
152+
95153
injectLine(contract, fileName, injectionPoint, injection, instrumentation){
96154
const type = 'line';
97155
const { start, end } = this._split(contract, injectionPoint);
@@ -250,6 +308,7 @@ class Injector {
250308
`${this._getDefaultMethodDefinition(id)}` +
251309
`${this._getTrueMethodDefinition(id)}` +
252310
`${this._getFalseMethodDefinition(id)}` +
311+
`${this._getModifierDefinitions(id, instrumentation)}` +
253312
`${end}`;
254313
}
255314

@@ -302,6 +361,29 @@ class Injector {
302361

303362
contract.instrumented = `${start}${injectable}${end}`;
304363
}
364+
365+
injectModifier(contract, fileName, injectionPoint, injection, instrumentation){
366+
const type = 'modifier';
367+
const contractId = `${fileName}:${injection.contractName}`;
368+
const modifierId = `${fileName}:${injection.contractName}:` +
369+
`${injection.modifierName}:${injection.fnId}`;
370+
371+
const {
372+
start,
373+
end,
374+
hash,
375+
injectable
376+
} = this._getInjectionComponents(contract, injectionPoint, modifierId, type);
377+
378+
this._cacheModifier({
379+
contractId,
380+
modifierId,
381+
fileName,
382+
...injection
383+
});
384+
385+
contract.instrumented = `${start}${injectable}${end}`;
386+
}
305387
};
306388

307389
module.exports = Injector;

lib/instrumenter.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class Instrumenter {
1717
this.injector = new Injector();
1818
this.measureStatementCoverage = (config.measureStatementCoverage === false) ? false : true;
1919
this.measureFunctionCoverage = (config.measureFunctionCoverage === false) ? false: true;
20+
this.measureModifierCoverage = (config.measureModifierCoverage === false) ? false: true;
2021
}
2122

2223
_isRootNode(node){
@@ -56,15 +57,19 @@ class Instrumenter {
5657
instrument(contractSource, fileName) {
5758
const contract = {};
5859

60+
this.injector.resetModifierMapping();
61+
parse.configureStatementCoverage(this.measureStatementCoverage)
62+
parse.configureFunctionCoverage(this.measureFunctionCoverage)
63+
parse.configureModifierCoverage(this.measureModifierCoverage)
64+
5965
contract.source = contractSource;
6066
contract.instrumented = contractSource;
6167

6268
this._initializeCoverageFields(contract);
63-
parse.configureStatementCoverage(this.measureStatementCoverage)
64-
parse.configureFunctionCoverage(this.measureFunctionCoverage)
6569

6670
// First, we run over the original contract to get the source mapping.
6771
let ast = SolidityParser.parse(contract.source, {loc: true, range: true});
72+
6873
parse[ast.type](contract, ast);
6974
const retValue = JSON.parse(JSON.stringify(contract)); // Possibly apotropaic.
7075

lib/parse.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ parse.configureFunctionCoverage = function(val){
1717
register.measureFunctionCoverage = val;
1818
}
1919

20+
parse.configureModifierCoverage = function(val){
21+
register.measureModifierCoverage = val;
22+
}
23+
2024
// Nodes
2125
parse.AssignmentExpression = function(contract, expression) {
2226
register.statement(contract, expression);

lib/registrar.js

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class Registrar {
1414
// These are set by user option and enable/disable the measurement completely
1515
this.measureStatementCoverage = true;
1616
this.measureFunctionCoverage = true;
17+
this.measureModifierCoverage = true;
1718
}
1819

1920
/**
@@ -110,14 +111,31 @@ class Registrar {
110111
if (!this.measureFunctionCoverage) return;
111112

112113
let start = 0;
114+
contract.fnId += 1;
113115

114-
// It's possible functions will have modifiers that take string args
115-
// which contains an open curly brace. Skip ahead...
116116
if (expression.modifiers && expression.modifiers.length){
117117
for (let modifier of expression.modifiers ){
118+
119+
// It's possible functions will have modifiers that take string args
120+
// which contains an open curly brace. Skip ahead...
118121
if (modifier.range[1] > start){
119122
start = modifier.range[1];
120123
}
124+
125+
// Add modifier branch coverage
126+
if (!this.measureModifierCoverage) continue;
127+
128+
this.addNewModifierBranch(contract, modifier);
129+
this._createInjectionPoint(
130+
contract,
131+
modifier.range[0],
132+
{
133+
type: 'injectModifier',
134+
branchId: contract.branchId,
135+
modifierName: modifier.name,
136+
fnId: contract.fnId
137+
}
138+
);
121139
}
122140
} else {
123141
start = expression.range[0];
@@ -133,7 +151,6 @@ class Registrar {
133151
start + endlineDelta
134152
);
135153

136-
contract.fnId += 1;
137154
contract.fnMap[contract.fnId] = {
138155
name: expression.isConstructor ? 'constructor' : expression.name,
139156
line: startline,
@@ -186,6 +203,41 @@ class Registrar {
186203
};
187204
};
188205

206+
/**
207+
* Registers injections for modifier branch measurements.
208+
* @param {Object} contract instrumentation target
209+
* @param {Object} expression AST node
210+
*/
211+
addNewModifierBranch(contract, expression) {
212+
const startContract = contract.instrumented.slice(0, expression.range[0]);
213+
const startline = ( startContract.match(/\n/g) || [] ).length + 1;
214+
const startcol = expression.range[0] - startContract.lastIndexOf('\n') - 1;
215+
216+
contract.branchId += 1;
217+
218+
// NB locations for if branches in istanbul are zero
219+
// length and associated with the start of the if.
220+
contract.branchMap[contract.branchId] = {
221+
line: startline,
222+
type: 'if',
223+
locations: [{
224+
start: {
225+
line: startline, column: startcol,
226+
},
227+
end: {
228+
line: startline, column: startcol,
229+
},
230+
}, {
231+
start: {
232+
line: startline, column: startcol,
233+
},
234+
end: {
235+
line: startline, column: startcol,
236+
},
237+
}],
238+
};
239+
};
240+
189241
addNewConditionalBranch(contract, expression){
190242
let start;
191243
// Instabul HTML highlighting location data...

lib/validator.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const configSchema = {
2222
istanbulFolder: {type: "string"},
2323
measureStatementCoverage: {type: "boolean"},
2424
measureFunctionCoverage: {type: "boolean"},
25+
measureModifierCoverage: {type: "boolean"},
2526

2627
// Hooks:
2728
onServerReady: {type: "function", format: "isFunction"},
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: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
pragma solidity ^0.6.0;
2+
3+
import "./ModifiersB.sol";
4+
5+
/**
6+
* New syntaxes in solc 0.6.x
7+
*/
8+
contract ModifiersA is ModifiersB {
9+
uint counter;
10+
bool flag = true;
11+
12+
modifier flippable {
13+
require(flag);
14+
_;
15+
}
16+
17+
modifier overridden() override {
18+
require(true);
19+
_;
20+
}
21+
22+
function flip() public {
23+
flag = !flag;
24+
}
25+
26+
function simpleSet(uint i)
27+
public
28+
override(ModifiersB)
29+
{
30+
counter = counter + i;
31+
}
32+
33+
function simpleView(uint i)
34+
view
35+
overridden
36+
external
37+
returns (uint, bool)
38+
{
39+
return (counter + i, true);
40+
}
41+
42+
function simpleSetFlip(uint i) flippable public {
43+
counter = counter + i;
44+
}
45+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
pragma solidity ^0.6.0;
2+
3+
4+
contract ModifiersB {
5+
uint value;
6+
uint b;
7+
8+
constructor() public {
9+
}
10+
11+
modifier overridden() virtual {
12+
require(true);
13+
_;
14+
}
15+
16+
function simpleSet(uint i) public virtual {
17+
value = 5;
18+
}
19+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
pragma solidity ^0.6.0;
2+
3+
import "./ModifiersB.sol";
4+
5+
/**
6+
* New syntaxes in solc 0.6.x
7+
*/
8+
contract ModifiersC {
9+
uint counter;
10+
address owner;
11+
bool flag = true;
12+
13+
constructor() public {
14+
owner = msg.sender;
15+
}
16+
17+
modifier flippable {
18+
require(flag);
19+
_;
20+
}
21+
22+
function flip() public {
23+
flag = !flag;
24+
}
25+
26+
function simpleSetFlip(uint i) flippable public {
27+
counter = counter + i;
28+
}
29+
30+
modifier onlyOwner {
31+
require(msg.sender == owner);
32+
_;
33+
}
34+
35+
function set(uint i)
36+
onlyOwner
37+
public
38+
payable
39+
virtual
40+
{
41+
counter = counter + i;
42+
}
43+
}

0 commit comments

Comments
 (0)