Skip to content

Commit faf8cb5

Browse files
authored
Add ABI diff logic (#598)
1 parent ecde305 commit faf8cb5

20 files changed

+713
-12
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ module.exports = {
127127
| measureModifierCoverage | *boolean* | `true` | Computes each modifier invocation as a code branch. [More...][34] |
128128
| modifierWhitelist | *String[]* | `[]` | List of modifier names (ex: "onlyOwner") to exclude from branch measurement. (Useful for modifiers which prepare something instead of acting as a gate.)) |
129129
| matrixOutputPath | *String* | `./testMatrix.json` | Relative path to write test matrix JSON object to. [More...][38] |
130+
| abiOutputPath | *String* | `./humanReadableAbis.json` | Relative path to write diff-able ABI data to. [More...][38] |
130131
| istanbulFolder | *String* | `./coverage` | Folder location for Istanbul coverage reports. |
131132
| istanbulReporter | *Array* | `['html', 'lcov', 'text', 'json']` | [Istanbul coverage reporters][2] |
132133
| 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/abi.js

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
const ethersABI = require("@ethersproject/abi");
2+
const difflib = require('difflib');
3+
4+
class AbiUtils {
5+
6+
diff(orig={}, cur={}){
7+
let plus = 0;
8+
let minus = 0;
9+
10+
const unifiedDiff = difflib.unifiedDiff(
11+
orig.humanReadableAbiList,
12+
cur.humanReadableAbiList,
13+
{
14+
fromfile: orig.contractName,
15+
tofile: cur.contractName,
16+
fromfiledate: `sha: ${orig.sha}`,
17+
tofiledate: `sha: ${cur.sha}`,
18+
lineterm: ''
19+
}
20+
);
21+
22+
// Count changes (unified diff always has a plus & minus in header);
23+
if (unifiedDiff.length){
24+
plus = -1;
25+
minus = -1;
26+
}
27+
28+
unifiedDiff.forEach(line => {
29+
if (line[0] === `+`) plus++;
30+
if (line[0] === `-`) minus++;
31+
})
32+
33+
return {
34+
plus,
35+
minus,
36+
unifiedDiff
37+
}
38+
}
39+
40+
toHumanReadableFunctions(contract){
41+
const human = [];
42+
const ethersOutput = new ethersABI.Interface(contract.abi).functions;
43+
const signatures = Object.keys(ethersOutput);
44+
45+
for (const sig of signatures){
46+
const method = ethersOutput[sig];
47+
let returns = '';
48+
49+
method.outputs.forEach(output => {
50+
(returns.length)
51+
? returns += `, ${output.type}`
52+
: returns += output.type;
53+
});
54+
55+
let readable = `${method.type} ${sig} ${method.stateMutability}`;
56+
57+
if (returns.length){
58+
readable += ` returns (${returns})`
59+
}
60+
61+
human.push(readable);
62+
}
63+
64+
return human;
65+
}
66+
67+
toHumanReadableEvents(contract){
68+
const human = [];
69+
const ethersOutput = new ethersABI.Interface(contract.abi).events;
70+
const signatures = Object.keys(ethersOutput);
71+
72+
for (const sig of signatures){
73+
const method = ethersOutput[sig];
74+
const readable = `${ethersOutput[sig].type} ${sig}`;
75+
human.push(readable);
76+
}
77+
78+
return human;
79+
}
80+
81+
generateHumanReadableAbiList(_artifacts, sha){
82+
const list = [];
83+
if (_artifacts.length){
84+
for (const item of _artifacts){
85+
const fns = this.toHumanReadableFunctions(item);
86+
const evts = this.toHumanReadableEvents(item);
87+
const all = fns.concat(evts);
88+
list.push({
89+
contractName: item.contractName,
90+
sha: sha,
91+
humanReadableAbiList: all
92+
})
93+
}
94+
}
95+
return list;
96+
}
97+
}
98+
99+
module.exports = AbiUtils;

lib/api.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ const Instrumenter = require('./instrumenter');
1212
const Coverage = require('./coverage');
1313
const DataCollector = require('./collector');
1414
const AppUI = require('./ui').AppUI;
15+
const AbiUtils = require('./abi');
1516

1617
/**
1718
* Coverage Runner
1819
*/
1920
class API {
2021
constructor(config={}) {
21-
this.validator = new ConfigValidator()
22+
this.validator = new ConfigValidator();
23+
this.abiUtils = new AbiUtils();
2224
this.config = config || {};
2325
this.testMatrix = {};
2426

@@ -31,6 +33,7 @@ class API {
3133
this.testsErrored = false;
3234

3335
this.cwd = config.cwd || process.cwd();
36+
this.abiOutputPath = config.abiOutputPath || "humanReadableAbis.json";
3437
this.matrixOutputPath = config.matrixOutputPath || "testMatrix.json";
3538
this.matrixReporterPath = config.matrixReporterPath || "solidity-coverage/plugins/resources/matrix.js"
3639

@@ -357,6 +360,11 @@ class API {
357360
fs.writeFileSync(matrixPath, JSON.stringify(mapping, null, ' '));
358361
}
359362

363+
saveHumanReadableAbis(data){
364+
const abiPath = path.join(this.cwd, this.abiOutputPath);
365+
fs.writeFileSync(abiPath, JSON.stringify(data, null, ' '));
366+
}
367+
360368
// =====
361369
// Paths
362370
// =====

lib/validator.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ const configSchema = {
1414
client: {type: "object"},
1515
cwd: {type: "string"},
1616
host: {type: "string"},
17-
17+
abiOutputPath: {type: "string"},
18+
matrixOutputPath: {type: "string"},
19+
matrixReporterPath: {type: "string"},
1820
port: {type: "number"},
1921
providerOptions: {type: "object"},
2022
silent: {type: "boolean"},

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424
"author": "",
2525
"license": "ISC",
2626
"dependencies": {
27+
"@ethersproject/abi": "^5.0.9",
2728
"@solidity-parser/parser": "^0.10.1",
2829
"@truffle/provider": "^0.2.24",
2930
"chalk": "^2.4.2",
3031
"death": "^1.1.0",
3132
"detect-port": "^1.3.0",
33+
"difflib": "^0.2.4",
3234
"fs-extra": "^8.1.0",
3335
"ganache-cli": "^6.11.0",
3436
"ghost-testrpc": "^0.0.2",

plugins/resources/nomiclabs.utils.js

+31-1
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,35 @@ function collectTestMatrixData(args, env, api){
180180
}
181181
}
182182

183+
/**
184+
* Returns all Hardhat artifacts.
185+
* @param {HRE} env
186+
* @return {Artifact[]}
187+
*/
188+
async function getAllArtifacts(env){
189+
const all = [];
190+
const qualifiedNames = await env.artifacts.getArtifactPaths();
191+
for (const name of qualifiedNames){
192+
all.push(await env.artifacts.readArtifact(name));
193+
}
194+
return all;
195+
}
196+
197+
/**
198+
* Compiles project
199+
* Collects all artifacts from Hardhat project,
200+
* Converts them to a format that can be consumed by api.abiUtils.diff
201+
* Saves them to `api.abiOutputPath`
202+
* @param {HRE} env
203+
* @param {SolidityCoverageAPI} api
204+
*/
205+
async function generateHumanReadableAbiList(env, api){
206+
await env.run(TASK_COMPILE);
207+
const _artifacts = getAllArtifacts(env);
208+
const list = api.abiUtils.generateHumanReadableAbiList(_artifacts)
209+
api.saveHumanReadableAbis(list);
210+
}
211+
183212
/**
184213
* Sets the default `from` account field in the network that will be used.
185214
* This needs to be done after accounts are fetched from the launched client.
@@ -231,6 +260,7 @@ module.exports = {
231260
setupHardhatNetwork,
232261
getTestFilePaths,
233262
setNetworkFrom,
234-
collectTestMatrixData
263+
collectTestMatrixData,
264+
getAllArtifacts
235265
}
236266

plugins/resources/truffle.utils.js

+33
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,39 @@ async function getTestFilePaths(config){
3535
return target.filter(f => f.match(testregex) != null);
3636
}
3737

38+
/**
39+
* Returns all Truffle artifacts.
40+
* @param {TruffleConfig} config
41+
* @return {Artifact[]}
42+
*/
43+
function getAllArtifacts(config){
44+
const all = [];
45+
const artifactsGlob = path.join(config.artifactsDir, '/**/*.json');
46+
const files = globby.sync([artifactsGlob])
47+
for (const file of files){
48+
const candidate = require(file);
49+
if (candidate.contractName && candidate.abi){
50+
all.push(candidate);
51+
}
52+
}
53+
return all;
54+
}
55+
56+
/**
57+
* Compiles project
58+
* Collects all artifacts from Truffle project,
59+
* Converts them to a format that can be consumed by api.abiUtils.diff
60+
* Saves them to `api.abiOutputPath`
61+
* @param {TruffleConfig} config
62+
* @param {TruffleAPI} truffle
63+
* @param {SolidityCoverageAPI} api
64+
*/
65+
async function generateHumanReadableAbiList(config, truffle, api){
66+
await truffle.compile(config);
67+
const _artifacts = getAllArtifacts(config);
68+
const list = api.abiUtils.generateHumanReadableAbiList(_artifacts)
69+
api.saveHumanReadableAbis(list);
70+
}
3871

3972
/**
4073
* Configures the network. Runs before the server is launched.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
pragma solidity ^0.7.0;
2+
3+
contract Old {
4+
uint public y;
5+
6+
function a() public {
7+
bool x = true;
8+
}
9+
10+
function b() external {
11+
bool x = true;
12+
}
13+
14+
function c() external {
15+
bool x = true;
16+
}
17+
}
18+
19+
contract New {
20+
uint public y;
21+
22+
function a() public {
23+
bool x = true;
24+
}
25+
26+
function b() external {
27+
bool x = true;
28+
}
29+
30+
function c() external {
31+
bool x = true;
32+
}
33+
34+
function d() external {
35+
bool x = true;
36+
}
37+
}
38+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
pragma solidity ^0.7.0;
2+
3+
contract Old {
4+
uint y;
5+
6+
event Evt(uint x, bytes8 y);
7+
8+
function a() public {
9+
bool x = true;
10+
}
11+
}
12+
13+
contract New {
14+
uint y;
15+
16+
function a() public {
17+
bool x = true;
18+
}
19+
20+
event aEvt(bytes8);
21+
event _Evt(bytes8 x, bytes8 y);
22+
}
23+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
pragma solidity ^0.7.0;
2+
3+
contract Old {
4+
uint y;
5+
6+
function a() public {
7+
bool x = true;
8+
}
9+
10+
function b() external {
11+
bool x = true;
12+
}
13+
14+
function c() external {
15+
bool x = true;
16+
}
17+
}
18+
19+
contract New {
20+
uint y;
21+
22+
function a() public {
23+
bool x = true;
24+
}
25+
26+
function b() external {
27+
bool x = true;
28+
}
29+
30+
function c() external {
31+
bool x = true;
32+
}
33+
}
34+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
pragma solidity ^0.7.0;
2+
3+
contract Old {
4+
uint y;
5+
6+
function a() public {
7+
bool x = true;
8+
}
9+
10+
function b() external {
11+
bool x = true;
12+
}
13+
14+
function c() external {
15+
bool x = true;
16+
}
17+
}
18+
19+
contract New {
20+
uint y;
21+
22+
function a() public {
23+
bool x = true;
24+
}
25+
26+
function b(bytes8 z) external {
27+
bool x = true;
28+
}
29+
30+
function c(uint q, uint r) external {
31+
bool x = true;
32+
}
33+
}

0 commit comments

Comments
 (0)