Skip to content

Commit bf77884

Browse files
committed
Add modifier-invocation-as-branch coverage (#588)
1 parent 7403f3f commit bf77884

26 files changed

+668
-8
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ module.exports = {
9898
| skipFiles | *Array* | `['Migrations.sol']` | Array of contracts or folders (with paths expressed relative to the `contracts` directory) that should be skipped when doing instrumentation. |
9999
| measureStatementCoverage | *boolean* | `true` | Computes statement (in addition to line) coverage. [More...][34] |
100100
| measureFunctionCoverage | *boolean* | `true` | Computes function coverage. [More...][34] |
101+
| measureModifierCoverage | *boolean* | `true` | Computes each modifier invocation as a code branch. [More...][34] |
101102
| istanbulFolder | *String* | `./coverage` | Folder location for Istanbul coverage reports. |
102103
| istanbulReporter | *Array* | `['html', 'lcov', 'text', 'json']` | [Istanbul coverage reporters][2] |
103104
| 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)
@@ -104,6 +111,57 @@ class Injector {
104111
return `function ${method}(bytes32 c__${hash}) public pure returns (bool){ return false; }\n`;
105112
}
106113

114+
_getModifierDefinitions(contractId, instrumentation){
115+
let injection = '';
116+
117+
if (this.modifiers[contractId]){
118+
119+
for (const item of this.modifiers[contractId]){
120+
injection += `modifier ${this._getModifierIdentifier(item.modifierId)}{ `;
121+
122+
let hash = this._getHash(item.modifierId);
123+
let injectable = this._getInjectable(item.contractId, hash, 'modifier-pre');
124+
125+
instrumentation[hash] = {
126+
id: item.branchId,
127+
type: 'requirePre',
128+
contractPath: item.fileName,
129+
hits: 0
130+
}
131+
132+
injection += injectable;
133+
injection += `_;`
134+
135+
hash = this._getHash(item.modifierId);
136+
injectable = this._getInjectable(item.contractId, hash, 'modifier-post');
137+
138+
instrumentation[hash] = {
139+
id: item.branchId,
140+
type: 'requirePost',
141+
contractPath: item.fileName,
142+
hits: 0
143+
}
144+
145+
injection += injectable;
146+
injection += ` }\n`;
147+
}
148+
}
149+
150+
return injection;
151+
}
152+
153+
_cacheModifier(injection){
154+
if (!this.modifiers[injection.contractId]) {
155+
this.modifiers[injection.contractId] = [];
156+
}
157+
158+
this.modifiers[injection.contractId].push(injection);
159+
}
160+
161+
resetModifierMapping(){
162+
this.modifiers = {};
163+
}
164+
107165
injectLine(contract, fileName, injectionPoint, injection, instrumentation){
108166
const type = 'line';
109167
const { start, end } = this._split(contract, injectionPoint);
@@ -267,6 +325,7 @@ class Injector {
267325
`${defaultMethodDefinition}` +
268326
`${this._getTrueMethodDefinition(id)}` +
269327
`${this._getFalseMethodDefinition(id)}` +
328+
`${this._getModifierDefinitions(id, instrumentation)}` +
270329
`${end}`;
271330
}
272331

@@ -319,6 +378,29 @@ class Injector {
319378

320379
contract.instrumented = `${start}${injectable}${end}`;
321380
}
381+
382+
injectModifier(contract, fileName, injectionPoint, injection, instrumentation){
383+
const type = 'modifier';
384+
const contractId = `${fileName}:${injection.contractName}`;
385+
const modifierId = `${fileName}:${injection.contractName}:` +
386+
`${injection.modifierName}:${injection.fnId}`;
387+
388+
const {
389+
start,
390+
end,
391+
hash,
392+
injectable
393+
} = this._getInjectionComponents(contract, injectionPoint, modifierId, type);
394+
395+
this._cacheModifier({
396+
contractId,
397+
modifierId,
398+
fileName,
399+
...injection
400+
});
401+
402+
contract.instrumented = `${start}${injectable}${end}`;
403+
}
322404
};
323405

324406
module.exports = Injector;

lib/instrumenter.js

Lines changed: 7 additions & 3 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,16 +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});
68-
//console.log(JSON.stringify(ast, null, ' '))
72+
6973
parse[ast.type](contract, ast);
7074
const retValue = JSON.parse(JSON.stringify(contract)); // Possibly apotropaic.
7175

lib/parse.js

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

22+
parse.configureModifierCoverage = function(val){
23+
register.measureModifierCoverage = val;
24+
}
25+
2226
// Nodes
2327
parse.AssignmentExpression = function(contract, expression) {
2428
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)