From 6a93d47baa9b256a4ce57c6d034e7ec04b75f58c Mon Sep 17 00:00:00 2001 From: Michal Dorner Date: Sun, 14 Mar 2021 22:57:57 +0100 Subject: [PATCH 1/2] Add output option to JSON reporter (#4131) --- docs/index.md | 2 + lib/reporters/json.js | 20 ++- test/reporters/json.spec.js | 254 ++++++++++++++++++++++-------------- 3 files changed, 178 insertions(+), 98 deletions(-) diff --git a/docs/index.md b/docs/index.md index 7300faf9e9..94fbe29089 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1933,6 +1933,8 @@ Alias: `JSON`, `json` The JSON reporter outputs a single large JSON object when the tests have completed (failures or not). +By default, it will output to the console. To write directly to a file, use `--reporter-option output=filename.json`. + ![json reporter](images/reporter-json.png?withoutEnlargement&resize=920,9999){:class="screenshot" loading="lazy"} ### JSON Stream diff --git a/lib/reporters/json.js b/lib/reporters/json.js index a314cd3805..ead181b4e8 100644 --- a/lib/reporters/json.js +++ b/lib/reporters/json.js @@ -7,6 +7,10 @@ */ var Base = require('./base'); +var fs = require('fs'); +var path = require('path'); +var errors = require('../errors'); +var createUnsupportedError = errors.createUnsupportedError; var constants = require('../runner').constants; var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; @@ -38,6 +42,14 @@ function JSONReporter(runner, options) { var pending = []; var failures = []; var passes = []; + var output; + + if (options && options.reporterOptions && options.reporterOptions.output) { + if (!fs || !fs.writeFileSync) { + throw createUnsupportedError('file output not supported in browser'); + } + output = options.reporterOptions.output; + } runner.on(EVENT_TEST_END, function(test) { tests.push(test); @@ -66,7 +78,13 @@ function JSONReporter(runner, options) { runner.testResults = obj; - process.stdout.write(JSON.stringify(obj, null, 2)); + var json = JSON.stringify(obj, null, 2); + if (output) { + fs.mkdirSync(path.dirname(output), {recursive: true}); + fs.writeFileSync(output, json); + } else { + process.stdout.write(json); + } }); } diff --git a/test/reporters/json.spec.js b/test/reporters/json.spec.js index 3eca1e9175..97f9019732 100644 --- a/test/reporters/json.spec.js +++ b/test/reporters/json.spec.js @@ -1,12 +1,15 @@ 'use strict'; +var fs = require('fs'); var sinon = require('sinon'); +var JSONReporter = require('../../lib/reporters/json'); var Mocha = require('../../'); var Suite = Mocha.Suite; var Runner = Mocha.Runner; var Test = Mocha.Test; describe('JSON reporter', function() { + var mocha; var suite; var runner; var testTitle = 'json test 1'; @@ -14,131 +17,188 @@ describe('JSON reporter', function() { var noop = function() {}; beforeEach(function() { - var mocha = new Mocha({ + mocha = new Mocha({ reporter: 'json' }); suite = new Suite('JSON suite', 'root'); runner = new Runner(suite); - var options = {}; - /* eslint no-unused-vars: off */ - var mochaReporter = new mocha._reporter(runner, options); - }); - - beforeEach(function() { - sinon.stub(process.stdout, 'write').callsFake(noop); }); afterEach(function() { sinon.restore(); }); - it('should have 1 test failure', function(done) { - var error = {message: 'oh shit'}; + describe('test results', function() { + beforeEach(function() { + var options = {}; + /* eslint no-unused-vars: off */ + var mochaReporter = new mocha._reporter(runner, options); + }); - var test = new Test(testTitle, function(done) { - done(new Error(error.message)); + beforeEach(function() { + sinon.stub(process.stdout, 'write').callsFake(noop); }); - test.file = testFile; - suite.addTest(test); - - runner.run(function(failureCount) { - sinon.restore(); - expect(runner, 'to satisfy', { - testResults: { - failures: [ - { - title: testTitle, - file: testFile, - err: { - message: error.message + it('should have 1 test failure', function(done) { + var error = {message: 'oh shit'}; + + var test = new Test(testTitle, function(done) { + done(new Error(error.message)); + }); + + test.file = testFile; + suite.addTest(test); + + runner.run(function(failureCount) { + sinon.restore(); + expect(runner, 'to satisfy', { + testResults: { + failures: [ + { + title: testTitle, + file: testFile, + err: { + message: error.message + } } - } - ] - } + ] + } + }); + expect(failureCount, 'to be', 1); + done(); }); - expect(failureCount, 'to be', 1); - done(); }); - }); - it('should have 1 test pending', function(done) { - var test = new Test(testTitle); - test.file = testFile; - suite.addTest(test); - - runner.run(function(failureCount) { - sinon.restore(); - expect(runner, 'to satisfy', { - testResults: { - pending: [ - { - title: testTitle, - file: testFile - } - ] - } + it('should have 1 test pending', function(done) { + var test = new Test(testTitle); + test.file = testFile; + suite.addTest(test); + + runner.run(function(failureCount) { + sinon.restore(); + expect(runner, 'to satisfy', { + testResults: { + pending: [ + { + title: testTitle, + file: testFile + } + ] + } + }); + expect(failureCount, 'to be', 0); + done(); }); - expect(failureCount, 'to be', 0); - done(); }); - }); - it('should have 1 test pass', function(done) { - const test = new Test(testTitle, () => {}); - - test.file = testFile; - suite.addTest(test); - - runner.run(function(failureCount) { - expect(runner, 'to satisfy', { - testResults: { - passes: [ - { - title: testTitle, - file: testFile, - speed: /(slow|medium|fast)/ - } - ] - } + it('should have 1 test pass', function(done) { + const test = new Test(testTitle, () => {}); + + test.file = testFile; + suite.addTest(test); + + runner.run(function(failureCount) { + expect(runner, 'to satisfy', { + testResults: { + passes: [ + { + title: testTitle, + file: testFile, + speed: /(slow|medium|fast)/ + } + ] + } + }); + expect(failureCount, 'to be', 0); + done(); + }); + }); + + it('should handle circular objects in errors', function(done) { + var testTitle = 'json test 1'; + function CircleError() { + this.message = 'oh shit'; + this.circular = this; + } + var error = new CircleError(); + + var test = new Test(testTitle, function(done) { + throw error; + }); + + test.file = testFile; + suite.addTest(test); + + runner.run(function(failureCount) { + sinon.restore(); + expect(runner, 'to satisfy', { + testResults: { + failures: [ + { + title: testTitle, + file: testFile, + err: { + message: error.message + } + } + ] + } + }); + expect(failureCount, 'to be', 1); + done(); }); - expect(failureCount, 'to be', 0); - done(); }); }); - it('should handle circular objects in errors', function(done) { - var testTitle = 'json test 1'; - function CircleError() { - this.message = 'oh shit'; - this.circular = this; - } - var error = new CircleError(); + describe("when 'reporterOptions.output' is provided", function() { + var expectedDirName = 'reports'; + var expectedFileName = 'reports/test-results.json'; + var options = { + reporterOptions: { + output: expectedFileName + } + }; + + beforeEach(function() { + /* eslint no-unused-vars: off */ + var mochaReporter = new mocha._reporter(runner, options); + }); - var test = new Test(testTitle, function(done) { - throw error; + beforeEach(function() { + // Add one test to suite to avoid assertions against empty test results + var test = new Test(testTitle, () => {}); + test.file = testFile; + suite.addTest(test); }); - test.file = testFile; - suite.addTest(test); - - runner.run(function(failureCount) { - sinon.restore(); - expect(runner, 'to satisfy', { - testResults: { - failures: [ - { - title: testTitle, - file: testFile, - err: { - message: error.message - } - } - ] - } + describe('when file can be created', function() { + it('should write test results to file', function(done) { + var fsMkdirSync = sinon.stub(fs, 'mkdirSync'); + var fsWriteFileSync = sinon.stub(fs, 'writeFileSync'); + + fsWriteFileSync.callsFake(function(filename, content) { + var expectedJson = JSON.stringify(runner.testResults, null, 2); + expect(expectedFileName, 'to be', filename); + expect(content, 'to be', expectedJson); + }); + + runner.run(function() { + fsMkdirSync.calledWith(expectedDirName, {recursive: true}); + expect(fsWriteFileSync.calledOnce, 'to be true'); + done(); + }); + }); + }); + + describe('when run in browser', function() { + it('should throw unsupported error', function() { + sinon.stub(fs, 'writeFileSync').value(false); + expect( + () => new JSONReporter(runner, options), + 'to throw', + 'file output not supported in browser' + ); }); - expect(failureCount, 'to be', 1); - done(); }); }); }); From eff4daaddb44295895b5debca511d25b3c7fe6b0 Mon Sep 17 00:00:00 2001 From: juergba Date: Tue, 17 Aug 2021 20:14:31 +0200 Subject: [PATCH 2/2] try/catch plus test --- example/config/.mocharc.js | 2 +- example/config/.mocharc.yml | 2 +- lib/reporters/base.js | 2 +- lib/reporters/json.js | 25 ++++++++----- test/reporters/json.spec.js | 73 ++++++++++++++++++++++++++----------- 5 files changed, 70 insertions(+), 34 deletions(-) diff --git a/example/config/.mocharc.js b/example/config/.mocharc.js index b730b4d399..e40bea1b71 100644 --- a/example/config/.mocharc.js +++ b/example/config/.mocharc.js @@ -33,7 +33,7 @@ module.exports = { parallel: false, recursive: false, reporter: 'spec', - 'reporter-option': ['foo=bar', 'baz=quux'], + 'reporter-option': ['foo=bar', 'baz=quux'], // array, not object require: '@babel/register', retries: 1, slow: '75', diff --git a/example/config/.mocharc.yml b/example/config/.mocharc.yml index 456b351332..e132b7a99d 100644 --- a/example/config/.mocharc.yml +++ b/example/config/.mocharc.yml @@ -35,7 +35,7 @@ package: './package.json' parallel: false recursive: false reporter: 'spec' -reporter-option: +reporter-option: # array, not object - 'foo=bar' - 'baz=quux' require: '@babel/register' diff --git a/lib/reporters/base.js b/lib/reporters/base.js index 20d1ccec11..f2f2bc65fc 100644 --- a/lib/reporters/base.js +++ b/lib/reporters/base.js @@ -90,7 +90,7 @@ exports.colors = { exports.symbols = { ok: symbols.success, - err: symbols.err, + err: symbols.error, dot: '.', comma: ',', bang: '!' diff --git a/lib/reporters/json.js b/lib/reporters/json.js index ead181b4e8..05e6269361 100644 --- a/lib/reporters/json.js +++ b/lib/reporters/json.js @@ -9,14 +9,14 @@ var Base = require('./base'); var fs = require('fs'); var path = require('path'); -var errors = require('../errors'); -var createUnsupportedError = errors.createUnsupportedError; +const createUnsupportedError = require('../errors').createUnsupportedError; +const utils = require('../utils'); var constants = require('../runner').constants; var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; +var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING; var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; var EVENT_TEST_END = constants.EVENT_TEST_END; var EVENT_RUN_END = constants.EVENT_RUN_END; -var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING; /** * Expose `JSON`. @@ -34,7 +34,7 @@ exports = module.exports = JSONReporter; * @param {Runner} runner - Instance triggers reporter actions. * @param {Object} [options] - runner options */ -function JSONReporter(runner, options) { +function JSONReporter(runner, options = {}) { Base.call(this, runner, options); var self = this; @@ -44,11 +44,11 @@ function JSONReporter(runner, options) { var passes = []; var output; - if (options && options.reporterOptions && options.reporterOptions.output) { - if (!fs || !fs.writeFileSync) { + if (options.reporterOption && options.reporterOption.output) { + if (utils.isBrowser()) { throw createUnsupportedError('file output not supported in browser'); } - output = options.reporterOptions.output; + output = options.reporterOption.output; } runner.on(EVENT_TEST_END, function(test) { @@ -80,8 +80,15 @@ function JSONReporter(runner, options) { var json = JSON.stringify(obj, null, 2); if (output) { - fs.mkdirSync(path.dirname(output), {recursive: true}); - fs.writeFileSync(output, json); + try { + fs.mkdirSync(path.dirname(output), {recursive: true}); + fs.writeFileSync(output, json); + } catch (err) { + console.error( + `${Base.symbols.err} [mocha] writing output to "${output}" failed: ${err.message}\n` + ); + process.stdout.write(json); + } } else { process.stdout.write(json); } diff --git a/test/reporters/json.spec.js b/test/reporters/json.spec.js index 97f9019732..8a44bc4e2a 100644 --- a/test/reporters/json.spec.js +++ b/test/reporters/json.spec.js @@ -3,6 +3,7 @@ var fs = require('fs'); var sinon = require('sinon'); var JSONReporter = require('../../lib/reporters/json'); +var utils = require('../../lib/utils'); var Mocha = require('../../'); var Suite = Mocha.Suite; var Runner = Mocha.Runner; @@ -98,6 +99,7 @@ describe('JSON reporter', function() { suite.addTest(test); runner.run(function(failureCount) { + sinon.restore(); expect(runner, 'to satisfy', { testResults: { passes: [ @@ -150,11 +152,11 @@ describe('JSON reporter', function() { }); }); - describe("when 'reporterOptions.output' is provided", function() { + describe('when "reporterOption.output" is provided', function() { var expectedDirName = 'reports'; var expectedFileName = 'reports/test-results.json'; var options = { - reporterOptions: { + reporterOption: { output: expectedFileName } }; @@ -171,34 +173,61 @@ describe('JSON reporter', function() { suite.addTest(test); }); - describe('when file can be created', function() { - it('should write test results to file', function(done) { - var fsMkdirSync = sinon.stub(fs, 'mkdirSync'); - var fsWriteFileSync = sinon.stub(fs, 'writeFileSync'); + it('should write test results to file', function(done) { + const fsMkdirSync = sinon.stub(fs, 'mkdirSync'); + const fsWriteFileSync = sinon.stub(fs, 'writeFileSync'); - fsWriteFileSync.callsFake(function(filename, content) { - var expectedJson = JSON.stringify(runner.testResults, null, 2); - expect(expectedFileName, 'to be', filename); - expect(content, 'to be', expectedJson); - }); + fsWriteFileSync.callsFake(function(filename, content) { + const expectedJson = JSON.stringify(runner.testResults, null, 2); + expect(expectedFileName, 'to be', filename); + expect(content, 'to be', expectedJson); + }); - runner.run(function() { - fsMkdirSync.calledWith(expectedDirName, {recursive: true}); - expect(fsWriteFileSync.calledOnce, 'to be true'); - done(); - }); + runner.run(function() { + expect( + fsMkdirSync.calledWith(expectedDirName, {recursive: true}), + 'to be true' + ); + expect(fsWriteFileSync.calledOnce, 'to be true'); + done(); }); }); - describe('when run in browser', function() { - it('should throw unsupported error', function() { - sinon.stub(fs, 'writeFileSync').value(false); + it('should warn and write test results to console', function(done) { + const fsMkdirSync = sinon.stub(fs, 'mkdirSync'); + const fsWriteFileSync = sinon.stub(fs, 'writeFileSync'); + + fsWriteFileSync.throws('unable to write file'); + + const outLog = []; + const fake = chunk => outLog.push(chunk); + sinon.stub(process.stderr, 'write').callsFake(fake); + sinon.stub(process.stdout, 'write').callsFake(fake); + + runner.run(function() { + sinon.restore(); + expect( + fsMkdirSync.calledWith(expectedDirName, {recursive: true}), + 'to be true' + ); + expect(fsWriteFileSync.calledOnce, 'to be true'); expect( - () => new JSONReporter(runner, options), - 'to throw', - 'file output not supported in browser' + outLog[0], + 'to contain', + `[mocha] writing output to "${expectedFileName}" failed:` ); + expect(outLog[1], 'to match', /"fullTitle": "JSON suite json test 1"/); + done(); }); }); + + it('should throw "unsupported error" in browser', function() { + sinon.stub(utils, 'isBrowser').callsFake(() => true); + expect( + () => new JSONReporter(runner, options), + 'to throw', + 'file output not supported in browser' + ); + }); }); });