Skip to content

Commit

Permalink
Generate mocha JSON output with --matrix (#601)
Browse files Browse the repository at this point in the history
  • Loading branch information
cgewecke committed Apr 26, 2022
1 parent adcaab5 commit e9133d7
Show file tree
Hide file tree
Showing 12 changed files with 243 additions and 30 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ module.exports = {
| measureFunctionCoverage | *boolean* | `true` | Computes function coverage. [More...][34] |
| measureModifierCoverage | *boolean* | `true` | Computes each modifier invocation as a code branch. [More...][34] |
| 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.)) |
| matrixOutputPath | *String* | `./testMatrix.json` | Relative path to write test matrix JSON object to. [More...][39] |
| matrixOutputPath | *String* | `./testMatrix.json` | Relative path to write test matrix JSON object to. [More...][39]|
| mochaJsonOutputPath | *String* | `./mochaOutput.json` | Relative path to write mocha JSON reporter object to. [More...][39]|
| abiOutputPath | *String* | `./humanReadableAbis.json` | Relative path to write diff-able ABI data to |
| istanbulFolder | *String* | `./coverage` | Folder location for Istanbul coverage reports. |
| istanbulReporter | *Array* | `['html', 'lcov', 'text', 'json']` | [Istanbul coverage reporters][2] |
Expand Down
9 changes: 9 additions & 0 deletions docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ to guess where bugs might exist in a given codebase.
Running the coverage command with `--matrix` will write [a JSON test matrix][25] which maps greppable
test names to each line of code to a file named `testMatrix.json` in your project's root.

It also generates a `mochaOutput.json` file which contains test run data similar to that
generated by mocha's built-in [JSON reporter][27].

In combination these data sets can be passed to Joram's Honig's [tarantula][29] tool which uses
a fault localization algorithm to generate 'suspiciousness' ratings for each line of
Solidity code in your project.

[22]: https://github.com/JoranHonig/vertigo#vertigo
[23]: http://spideruci.org/papers/jones05.pdf
[25]: https://github.com/sc-forks/solidity-coverage/blob/master/docs/matrix.md
[27]: https://mochajs.org/api/reporters_json.js.html
[29]: https://github.com/JoranHonig/tarantula
34 changes: 21 additions & 13 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class API {
this.cwd = config.cwd || process.cwd();
this.abiOutputPath = config.abiOutputPath || "humanReadableAbis.json";
this.matrixOutputPath = config.matrixOutputPath || "testMatrix.json";
this.mochaJsonOutputPath = config.mochaJsonOutputPath || "mochaOutput.json";
this.matrixReporterPath = config.matrixReporterPath || "solidity-coverage/plugins/resources/matrix.js"

this.defaultHook = () => {};
Expand Down Expand Up @@ -321,29 +322,31 @@ class API {
id
} = this.instrumenter.instrumentationData[hash];

if (type === 'line' && hits > 0){
if (type === 'line'){
if (!this.testMatrix[contractPath]){
this.testMatrix[contractPath] = {};
}
if (!this.testMatrix[contractPath][id]){
this.testMatrix[contractPath][id] = [];
}

// Search for and exclude duplicate entries
let duplicate = false;
for (const item of this.testMatrix[contractPath][id]){
if (item.title === title && item.file === file){
duplicate = true;
break;
if (hits > 0){
// Search for and exclude duplicate entries
let duplicate = false;
for (const item of this.testMatrix[contractPath][id]){
if (item.title === title && item.file === file){
duplicate = true;
break;
}
}
}

if (!duplicate) {
this.testMatrix[contractPath][id].push({title, file});
}
if (!duplicate) {
this.testMatrix[contractPath][id].push({title, file});
}

// Reset line data
this.instrumenter.instrumentationData[hash].hits = 0;
// Reset line data
this.instrumenter.instrumentationData[hash].hits = 0;
}
}
}
}
Expand All @@ -363,6 +366,11 @@ class API {
fs.writeFileSync(matrixPath, JSON.stringify(mapping, null, ' '));
}

saveMochaJsonOutput(data){
const outputPath = path.join(this.cwd, this.mochaJsonOutputPath);
fs.writeFileSync(outputPath, JSON.stringify(data, null, ' '));
}

saveHumanReadableAbis(data){
const abiPath = path.join(this.cwd, this.abiOutputPath);
fs.writeFileSync(abiPath, JSON.stringify(data, null, ' '));
Expand Down
82 changes: 78 additions & 4 deletions plugins/resources/matrix.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const mocha = require("mocha");
const inherits = require("util").inherits;
const Spec = mocha.reporters.Spec;

const path = require('path');

/**
* This file adapted from mocha's stats-collector
Expand Down Expand Up @@ -40,10 +40,13 @@ function mochaStats(runner) {
}

/**
* Based on the Mocha 'Spec' reporter. Watches an Ethereum test suite run
* and collects data about which tests hit which lines of code.
* This "test matrix" can be used as an input to
* Based on the Mocha 'Spec' reporter.
*
* Watches an Ethereum test suite run and collects data about which tests hit
* which lines of code. This "test matrix" can be used as an input to fault localization tools
* like: https://github.com/JoranHonig/tarantula
*
* Mocha's JSON reporter output is also generated and saved to a separate file
*
* @param {Object} runner mocha's runner
* @param {Object} options reporter.options (see README example usage)
Expand All @@ -52,6 +55,11 @@ function Matrix(runner, options) {
// Spec reporter
Spec.call(this, runner, options);

const self = this;
const tests = [];
const failures = [];
const passes = [];

// Initialize stats for Mocha 6+ epilogue
if (!runner.stats) {
mochaStats(runner);
Expand All @@ -60,7 +68,73 @@ function Matrix(runner, options) {

runner.on("test end", (info) => {
options.reporterOptions.collectTestMatrixData(info);
tests.push(info);
});

runner.on('pass', function(info) {
passes.push(info)
})
runner.on('fail', function(info) {
failures.push(info)
});

runner.once('end', function() {
delete self.stats.start;
delete self.stats.end;
delete self.stats.duration;

var obj = {
stats: self.stats,
tests: tests.map(clean),
failures: failures.map(clean),
passes: passes.map(clean)
};
runner.testResults = obj;
options.reporterOptions.saveMochaJsonOutput(obj)
});

// >>>>>>>>>>>>>>>>>>>>>>>>>
// Mocha JSON Reporter Utils
// Code taken from:
// https://mochajs.org/api/reporters_json.js.html
// >>>>>>>>>>>>>>>>>>>>>>>>>
function clean(info) {
var err = info.err || {};
if (err instanceof Error) {
err = errorJSON(err);
}
return {
title: info.title,
fullTitle: info.fullTitle(),
file: path.relative(options.reporterOptions.cwd, info.file),
currentRetry: info.currentRetry(),
err: cleanCycles(err)
};
}

function cleanCycles(obj) {
var cache = [];
return JSON.parse(
JSON.stringify(obj, function(key, value) {
if (typeof value === 'object' && value !== null) {
if (cache.indexOf(value) !== -1) {
// Instead of going in a circle, we'll print [object Object]
return '' + value;
}
cache.push(value);
}
return value;
})
);
}

function errorJSON(err) {
var res = {};
Object.getOwnPropertyNames(err).forEach(function(key) {
res[key] = err[key];
}, err);
return res;
}
}

/**
Expand Down
4 changes: 3 additions & 1 deletion plugins/resources/nomiclabs.utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ function collectTestMatrixData(args, env, api){
mochaConfig = env.config.mocha || {};
mochaConfig.reporter = api.matrixReporterPath;
mochaConfig.reporterOptions = {
collectTestMatrixData: api.collectTestMatrixData.bind(api)
collectTestMatrixData: api.collectTestMatrixData.bind(api),
saveMochaJsonOutput: api.saveMochaJsonOutput.bind(api),
cwd: api.cwd
}
env.config.mocha = mochaConfig;
}
Expand Down
4 changes: 3 additions & 1 deletion plugins/resources/truffle.utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,9 @@ function collectTestMatrixData(config, api){
config.mocha = config.mocha || {};
config.mocha.reporter = api.matrixReporterPath;
config.mocha.reporterOptions = {
collectTestMatrixData: api.collectTestMatrixData.bind(api)
collectTestMatrixData: api.collectTestMatrixData.bind(api),
saveMochaJsonOutput: api.saveMochaJsonOutput.bind(api),
cwd: api.cwd
}
}
}
Expand Down
1 change: 1 addition & 0 deletions test/integration/projects/matrix/.solcover.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = {
// "solidity-coverage/plugins/resources/matrix.js"
matrixReporterPath: reporterPath,
matrixOutputPath: "alternateTestMatrix.json",
mochaJsonOutputPath: "alternateMochaOutput.json",

skipFiles: ['Migrations.sol'],
silent: process.env.SILENT ? true : false,
Expand Down
4 changes: 4 additions & 0 deletions test/integration/projects/matrix/contracts/MatrixA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ contract MatrixA {
uint y = 5;
return y;
}

function unhit() public {
uint z = 7;
}
}
99 changes: 99 additions & 0 deletions test/integration/projects/matrix/expectedMochaOutput.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{
"stats": {
"suites": 2,
"tests": 6,
"passes": 6,
"pending": 0,
"failures": 0
},
"tests": [
{
"title": "sends to A",
"fullTitle": "Contract: Matrix A and B sends to A",
"file": "test/matrix_a_b.js",
"currentRetry": 0,
"err": {}
},
{
"title": "sends to A",
"fullTitle": "Contract: Matrix A and B sends to A",
"file": "test/matrix_a_b.js",
"currentRetry": 0,
"err": {}
},
{
"title": "calls B",
"fullTitle": "Contract: Matrix A and B calls B",
"file": "test/matrix_a_b.js",
"currentRetry": 0,
"err": {}
},
{
"title": "sends to B",
"fullTitle": "Contract: Matrix A and B sends to B",
"file": "test/matrix_a_b.js",
"currentRetry": 0,
"err": {}
},
{
"title": "sends",
"fullTitle": "Contract: MatrixA sends",
"file": "test/matrix_a.js",
"currentRetry": 0,
"err": {}
},
{
"title": "calls",
"fullTitle": "Contract: MatrixA calls",
"file": "test/matrix_a.js",
"currentRetry": 0,
"err": {}
}
],
"failures": [],
"passes": [
{
"title": "sends to A",
"fullTitle": "Contract: Matrix A and B sends to A",
"file": "test/matrix_a_b.js",
"currentRetry": 0,
"err": {}
},
{
"title": "sends to A",
"fullTitle": "Contract: Matrix A and B sends to A",
"file": "test/matrix_a_b.js",
"currentRetry": 0,
"err": {}
},
{
"title": "calls B",
"fullTitle": "Contract: Matrix A and B calls B",
"file": "test/matrix_a_b.js",
"currentRetry": 0,
"err": {}
},
{
"title": "sends to B",
"fullTitle": "Contract: Matrix A and B sends to B",
"file": "test/matrix_a_b.js",
"currentRetry": 0,
"err": {}
},
{
"title": "sends",
"fullTitle": "Contract: MatrixA sends",
"file": "test/matrix_a.js",
"currentRetry": 0,
"err": {}
},
{
"title": "calls",
"fullTitle": "Contract: MatrixA calls",
"file": "test/matrix_a.js",
"currentRetry": 0,
"err": {}
}
]
}

Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"title": "calls",
"file": "test/matrix_a.js"
}
]
],
"19": []
},
"contracts/MatrixB.sol": {
"10": [
Expand All @@ -43,4 +44,5 @@
}
]
}
}
}

14 changes: 10 additions & 4 deletions test/units/hardhat/flags.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,18 @@ describe('Hardhat Plugin: command line options', function() {
await this.env.run("coverage", taskArgs);

// Integration test checks output path configurabililty
const altPath = path.join(process.cwd(), './alternateTestMatrix.json');
const expPath = path.join(process.cwd(), './expectedTestMatrixHardhat.json');
const producedMatrix = require(altPath)
const expectedMatrix = require(expPath);
const altMatrixPath = path.join(process.cwd(), './alternateTestMatrix.json');
const expMatrixPath = path.join(process.cwd(), './expectedTestMatrixHardhat.json');
const altMochaPath = path.join(process.cwd(), './alternateMochaOutput.json');
const expMochaPath = path.join(process.cwd(), './expectedMochaOutput.json');

const producedMatrix = require(altMatrixPath)
const expectedMatrix = require(expMatrixPath);
const producedMochaOutput = require(altMochaPath);
const expectedMochaOutput = require(expMochaPath);

assert.deepEqual(producedMatrix, expectedMatrix);
assert.deepEqual(producedMochaOutput, expectedMochaOutput);
});

it('--abi', async function(){
Expand Down

0 comments on commit e9133d7

Please sign in to comment.