Skip to content

Generate mocha JSON output with --matrix #601

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,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...][38] |
| matrixOutputPath | *String* | `./testMatrix.json` | Relative path to write test matrix JSON object to. [More...][38]|
| mochaJsonOutputPath | *String* | `./mochaOutput.json` | Relative path to write mocha JSON reporter object to. [More...][38]|
| abiOutputPath | *String* | `./humanReadableAbis.json` | Relative path to write diff-able ABI data to. [More...][38] |
| 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 @@ -109,6 +109,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 @@ -318,29 +319,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 @@ -360,6 +363,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 @@ -174,7 +174,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 @@ -240,7 +240,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
Loading