From 9def3eb13d51ff5fee85efb2019cfaddb72a32c0 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Mon, 14 May 2018 16:24:42 -0700 Subject: [PATCH] feat: merge together multiple istanbul format reports (#840) --- bin/nyc.js | 10 +- index.js | 20 +-- lib/commands/merge.js | 51 +++++++ lib/config-util.js | 1 + lib/process-args.js | 3 +- test/fixtures/cli/merge-input/a.json | 200 +++++++++++++++++++++++++++ test/fixtures/cli/merge-input/b.json | 200 +++++++++++++++++++++++++++ test/nyc-bin.js | 75 ++++++++++ 8 files changed, 544 insertions(+), 16 deletions(-) create mode 100644 lib/commands/merge.js create mode 100644 test/fixtures/cli/merge-input/a.json create mode 100644 test/fixtures/cli/merge-input/b.json diff --git a/bin/nyc.js b/bin/nyc.js index d28d6e510..d7c070a81 100755 --- a/bin/nyc.js +++ b/bin/nyc.js @@ -23,12 +23,10 @@ const config = configUtil.loadConfig(yargs.parse(instrumenterArgs)) configUtil.addCommandsAndHelp(yargs) const argv = yargs.config(config).parse(instrumenterArgs) -if (argv._[0] === 'report') { - // look in lib/commands/report.js for logic. -} else if (argv._[0] === 'check-coverage') { - // look in lib/commands/check-coverage.js for logic. -} else if (argv._[0] === 'instrument') { - // look in lib/commands/instrument.js for logic. +if ([ + 'check-coverage', 'report', 'instrument', 'merge' +].indexOf(argv._[0]) !== -1) { + // look in lib/commands for logic. } else if (argv._.length) { // if instrument is set to false, // enable a noop instrumenter. diff --git a/index.js b/index.js index 3bf3fdaeb..dd1428a82 100755 --- a/index.js +++ b/index.js @@ -421,13 +421,13 @@ function coverageFinder () { return coverage } -NYC.prototype._getCoverageMapFromAllCoverageFiles = function () { +NYC.prototype.getCoverageMapFromAllCoverageFiles = function (baseDirectory) { var _this = this var map = libCoverage.createCoverageMap({}) - this.eachReport(function (report) { + this.eachReport(undefined, (report) => { map.merge(report) - }) + }, baseDirectory) // depending on whether source-code is pre-instrumented // or instrumented using a JIT plugin like babel-require // you may opt to exclude files after applying @@ -443,7 +443,7 @@ NYC.prototype._getCoverageMapFromAllCoverageFiles = function () { NYC.prototype.report = function () { var tree - var map = this._getCoverageMapFromAllCoverageFiles() + var map = this.getCoverageMapFromAllCoverageFiles() var context = libReport.createContext({ dir: this.reportDirectory(), watermarks: this.config.watermarks @@ -469,7 +469,7 @@ NYC.prototype.showProcessTree = function () { } NYC.prototype.checkCoverage = function (thresholds, perFile) { - var map = this._getCoverageMapFromAllCoverageFiles() + var map = this.getCoverageMapFromAllCoverageFiles() var nyc = this if (perFile) { @@ -516,20 +516,22 @@ NYC.prototype._loadProcessInfos = function () { }) } -NYC.prototype.eachReport = function (filenames, iterator) { +NYC.prototype.eachReport = function (filenames, iterator, baseDirectory) { + baseDirectory = baseDirectory || this.tempDirectory() + if (typeof filenames === 'function') { iterator = filenames filenames = undefined } var _this = this - var files = filenames || fs.readdirSync(this.tempDirectory()) + var files = filenames || fs.readdirSync(baseDirectory) files.forEach(function (f) { var report try { report = JSON.parse(fs.readFileSync( - path.resolve(_this.tempDirectory(), f), + path.resolve(baseDirectory, f), 'utf-8' )) @@ -545,7 +547,7 @@ NYC.prototype.eachReport = function (filenames, iterator) { NYC.prototype.loadReports = function (filenames) { var reports = [] - this.eachReport(filenames, function (report) { + this.eachReport(filenames, (report) => { reports.push(report) }) diff --git a/lib/commands/merge.js b/lib/commands/merge.js new file mode 100644 index 000000000..f660092d1 --- /dev/null +++ b/lib/commands/merge.js @@ -0,0 +1,51 @@ +'use strict' +const fs = require('fs') + +var NYC +try { + NYC = require('../../index.covered.js') +} catch (e) { + NYC = require('../../index.js') +} + +exports.command = 'merge [output-file]' + +exports.describe = 'merge istanbul format coverage output in a given folder' + +exports.builder = function (yargs) { + return yargs + .positional('input-directory', { + describe: 'directory containing multiple istanbul coverage files', + type: 'text', + default: './.nyc_output' + }) + .positional('output-file', { + describe: 'file to output combined istanbul format coverage to', + type: 'text', + default: 'coverage.json' + }) + .option('temp-directory', { + describe: 'directory to read raw coverage information from', + default: './.nyc_output' + }) + .example('$0 merge ./out coverage.json', 'merge together reports in ./out and output as coverage.json') +} + +exports.handler = function (argv) { + process.env.NYC_CWD = process.cwd() + const nyc = new NYC(argv) + let inputStat + try { + inputStat = fs.statSync(argv.inputDirectory) + if (!inputStat.isDirectory()) { + console.error(`${argv.inputDirectory} was not a directory`) + process.exit(1) + } + } catch (err) { + console.error(`failed access input directory ${argv.inputDirectory} with error:\n\n${err.message}`) + process.exit(1) + } + const map = nyc.getCoverageMapFromAllCoverageFiles(argv.inputDirectory) + fs.writeFileSync(argv.outputFile, JSON.stringify(map, null, 2), 'utf8') + console.info(`coverage files in ${argv.inputDirectory} merged into ${argv.outputFile}`) +} diff --git a/lib/config-util.js b/lib/config-util.js index 170e1d701..b202ab445 100644 --- a/lib/config-util.js +++ b/lib/config-util.js @@ -243,6 +243,7 @@ Config.addCommandsAndHelp = function (yargs) { .command(require('../lib/commands/check-coverage')) .command(require('../lib/commands/instrument')) .command(require('../lib/commands/report')) + .command(require('../lib/commands/merge')) } module.exports = Config diff --git a/lib/process-args.js b/lib/process-args.js index df6bcaac1..695f8e62a 100644 --- a/lib/process-args.js +++ b/lib/process-args.js @@ -2,7 +2,8 @@ const parser = require('yargs-parser') const commands = [ 'report', 'check-coverage', - 'instrument' + 'instrument', + 'merge' ] module.exports = { diff --git a/test/fixtures/cli/merge-input/a.json b/test/fixtures/cli/merge-input/a.json new file mode 100644 index 000000000..81f0ba143 --- /dev/null +++ b/test/fixtures/cli/merge-input/a.json @@ -0,0 +1,200 @@ +{ + "/private/tmp/contrived/library.js": { + "path": "/private/tmp/contrived/library.js", + "statementMap": { + "0": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 15, + "column": 1 + } + }, + "1": { + "start": { + "line": 3, + "column": 4 + }, + "end": { + "line": 3, + "column": 16 + } + }, + "2": { + "start": { + "line": 6, + "column": 4 + }, + "end": { + "line": 6, + "column": 16 + } + }, + "3": { + "start": { + "line": 9, + "column": 4 + }, + "end": { + "line": 13, + "column": 5 + } + }, + "4": { + "start": { + "line": 10, + "column": 6 + }, + "end": { + "line": 10, + "column": 14 + } + }, + "5": { + "start": { + "line": 12, + "column": 6 + }, + "end": { + "line": 12, + "column": 14 + } + } + }, + "fnMap": { + "0": { + "name": "(anonymous_0)", + "decl": { + "start": { + "line": 2, + "column": 11 + }, + "end": { + "line": 2, + "column": 12 + } + }, + "loc": { + "start": { + "line": 2, + "column": 21 + }, + "end": { + "line": 4, + "column": 3 + } + }, + "line": 2 + }, + "1": { + "name": "(anonymous_1)", + "decl": { + "start": { + "line": 5, + "column": 11 + }, + "end": { + "line": 5, + "column": 12 + } + }, + "loc": { + "start": { + "line": 5, + "column": 21 + }, + "end": { + "line": 7, + "column": 3 + } + }, + "line": 5 + }, + "2": { + "name": "(anonymous_2)", + "decl": { + "start": { + "line": 8, + "column": 11 + }, + "end": { + "line": 8, + "column": 12 + } + }, + "loc": { + "start": { + "line": 8, + "column": 18 + }, + "end": { + "line": 14, + "column": 3 + } + }, + "line": 8 + } + }, + "branchMap": { + "0": { + "loc": { + "start": { + "line": 9, + "column": 4 + }, + "end": { + "line": 13, + "column": 5 + } + }, + "type": "if", + "locations": [ + { + "start": { + "line": 9, + "column": 4 + }, + "end": { + "line": 13, + "column": 5 + } + }, + { + "start": { + "line": 9, + "column": 4 + }, + "end": { + "line": 13, + "column": 5 + } + } + ], + "line": 9 + } + }, + "s": { + "0": 1, + "1": 1, + "2": 0, + "3": 1, + "4": 0, + "5": 1 + }, + "f": { + "0": 1, + "1": 0, + "2": 1 + }, + "b": { + "0": [ + 0, + 1 + ] + }, + "_coverageSchema": "332fd63041d2c1bcb487cc26dd0d5f7d97098a6c", + "hash": "e86c0c0fa7c4fadac81e2479bfba3c0d59b657aa" + } +} \ No newline at end of file diff --git a/test/fixtures/cli/merge-input/b.json b/test/fixtures/cli/merge-input/b.json new file mode 100644 index 000000000..bbef81594 --- /dev/null +++ b/test/fixtures/cli/merge-input/b.json @@ -0,0 +1,200 @@ +{ + "/private/tmp/contrived/library.js": { + "path": "/private/tmp/contrived/library.js", + "statementMap": { + "0": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 15, + "column": 1 + } + }, + "1": { + "start": { + "line": 3, + "column": 4 + }, + "end": { + "line": 3, + "column": 16 + } + }, + "2": { + "start": { + "line": 6, + "column": 4 + }, + "end": { + "line": 6, + "column": 16 + } + }, + "3": { + "start": { + "line": 9, + "column": 4 + }, + "end": { + "line": 13, + "column": 5 + } + }, + "4": { + "start": { + "line": 10, + "column": 6 + }, + "end": { + "line": 10, + "column": 14 + } + }, + "5": { + "start": { + "line": 12, + "column": 6 + }, + "end": { + "line": 12, + "column": 14 + } + } + }, + "fnMap": { + "0": { + "name": "(anonymous_0)", + "decl": { + "start": { + "line": 2, + "column": 11 + }, + "end": { + "line": 2, + "column": 12 + } + }, + "loc": { + "start": { + "line": 2, + "column": 21 + }, + "end": { + "line": 4, + "column": 3 + } + }, + "line": 2 + }, + "1": { + "name": "(anonymous_1)", + "decl": { + "start": { + "line": 5, + "column": 11 + }, + "end": { + "line": 5, + "column": 12 + } + }, + "loc": { + "start": { + "line": 5, + "column": 21 + }, + "end": { + "line": 7, + "column": 3 + } + }, + "line": 5 + }, + "2": { + "name": "(anonymous_2)", + "decl": { + "start": { + "line": 8, + "column": 11 + }, + "end": { + "line": 8, + "column": 12 + } + }, + "loc": { + "start": { + "line": 8, + "column": 18 + }, + "end": { + "line": 14, + "column": 3 + } + }, + "line": 8 + } + }, + "branchMap": { + "0": { + "loc": { + "start": { + "line": 9, + "column": 4 + }, + "end": { + "line": 13, + "column": 5 + } + }, + "type": "if", + "locations": [ + { + "start": { + "line": 9, + "column": 4 + }, + "end": { + "line": 13, + "column": 5 + } + }, + { + "start": { + "line": 9, + "column": 4 + }, + "end": { + "line": 13, + "column": 5 + } + } + ], + "line": 9 + } + }, + "s": { + "0": 1, + "1": 0, + "2": 1, + "3": 1, + "4": 1, + "5": 0 + }, + "f": { + "0": 0, + "1": 1, + "2": 1 + }, + "b": { + "0": [ + 1, + 0 + ] + }, + "_coverageSchema": "332fd63041d2c1bcb487cc26dd0d5f7d97098a6c", + "hash": "e86c0c0fa7c4fadac81e2479bfba3c0d59b657aa" + } +} \ No newline at end of file diff --git a/test/nyc-bin.js b/test/nyc-bin.js index fb7752328..5a2cb937f 100644 --- a/test/nyc-bin.js +++ b/test/nyc-bin.js @@ -972,6 +972,81 @@ describe('the nyc cli', function () { }) }) }) + + describe('merge', () => { + it('combines multiple coverage reports', (done) => { + const args = [ + bin, + 'merge', + './merge-input' + ] + + const proc = spawn(process.execPath, args, { + cwd: fixturesCLI, + env: env + }) + + proc.on('close', function (code) { + const mergedCoverage = require('./fixtures/cli/coverage') + // the combined reports should have 100% function + // branch and statement coverage. + mergedCoverage['/private/tmp/contrived/library.js'] + .s.should.eql({'0': 2, '1': 1, '2': 1, '3': 2, '4': 1, '5': 1}) + mergedCoverage['/private/tmp/contrived/library.js'] + .f.should.eql({'0': 1, '1': 1, '2': 2}) + mergedCoverage['/private/tmp/contrived/library.js'] + .b.should.eql({'0': [1, 1]}) + rimraf.sync(path.resolve(fixturesCLI, 'coverage.json')) + return done() + }) + }) + + it('reports error if input directory is missing', (done) => { + const args = [ + bin, + 'merge', + './DIRECTORY_THAT_IS_MISSING' + ] + + const proc = spawn(process.execPath, args, { + cwd: fixturesCLI, + env: env + }) + + var stderr = '' + proc.stderr.on('data', function (chunk) { + stderr += chunk + }) + + proc.on('close', function (code) { + stderr.should.match(/failed access input directory/) + return done() + }) + }) + + it('reports error if input is not a directory', (done) => { + const args = [ + bin, + 'merge', + './package.json' + ] + + const proc = spawn(process.execPath, args, { + cwd: fixturesCLI, + env: env + }) + + var stderr = '' + proc.stderr.on('data', function (chunk) { + stderr += chunk + }) + + proc.on('close', function (code) { + stderr.should.match(/was not a directory/) + return done() + }) + }) + }) }) function stdoutShouldEqual (stdout, expected) {