diff --git a/lib/mocha.js b/lib/mocha.js index c6ee248561..afda6e4aa4 100644 --- a/lib/mocha.js +++ b/lib/mocha.js @@ -184,6 +184,7 @@ function Mocha(options = {}) { options = {...mocharc, ...options}; this.files = []; this.options = options; + // root suite this.suite = new exports.Suite('', new exports.Context(), true); this._cleanReferencesAfterRun = true; @@ -987,6 +988,7 @@ Mocha.prototype.run = function (fn) { if (this.files.length && !this._lazyLoadFiles) { this.loadFiles(); } + var suite = this.suite; var options = this.options; options.files = this.files; @@ -1190,7 +1192,7 @@ Mocha.prototype.runGlobalSetup = async function runGlobalSetup(context = {}) { const {globalSetup} = this.options; if (globalSetup && globalSetup.length) { debug('run(): global setup starting'); - await this._runGlobalFixtures(globalSetup, context); + await this._runGlobalFixtures(globalSetup, context, "Global Setup"); debug('run(): global setup complete'); } return context; @@ -1212,29 +1214,48 @@ Mocha.prototype.runGlobalTeardown = async function runGlobalTeardown( const {globalTeardown} = this.options; if (globalTeardown && globalTeardown.length) { debug('run(): global teardown starting'); - await this._runGlobalFixtures(globalTeardown, context); + await this._runGlobalFixtures(globalTeardown, context, "Global Teardown"); } debug('run(): global teardown complete'); return context; }; /** - * Run global fixtures sequentially with context `context` + * Run global fixtures sequentially with context `context`. * @private * @param {MochaGlobalFixture[]} [fixtureFns] - Fixtures to run * @param {object} [context] - context object + * @params {string} [phase] - phase of the fixture * @returns {Promise<object>} context object */ Mocha.prototype._runGlobalFixtures = async function _runGlobalFixtures( fixtureFns = [], - context = {} + context = {}, + phase ) { - for await (const fixtureFn of fixtureFns) { - await fixtureFn.call(context); + for (const fixtureFn of fixtureFns) { + try { + await fixtureFn.call(context); + } catch (err) { + + context.stats.failures++; + context.failures++; + + context.emit('fail', { + title: phase, + err: err + }); + + if (phase === "Global Setup") { + throw err; + } + console.error(err); + } } return context; }; + /** * Toggle execution of any global setup fixture(s) * @@ -1330,3 +1351,4 @@ Mocha.prototype.hasGlobalTeardownFixtures = * @param {Array<*>} impls - User-supplied implementations * @returns {Promise<*>|*} */ + diff --git a/test/integration/fixtures/global-fixtures/failing-test.fixture.js b/test/integration/fixtures/global-fixtures/failing-test.fixture.js new file mode 100644 index 0000000000..2b0eaf20f4 --- /dev/null +++ b/test/integration/fixtures/global-fixtures/failing-test.fixture.js @@ -0,0 +1,9 @@ +'use strict'; + + +describe('Test Suite', function() { + it('failing test', function() { + throw new Error('Test failure'); + }); +}); + \ No newline at end of file diff --git a/test/integration/fixtures/global-fixtures/global-setup.fixture.js b/test/integration/fixtures/global-fixtures/global-setup.fixture.js new file mode 100644 index 0000000000..401a529b3d --- /dev/null +++ b/test/integration/fixtures/global-fixtures/global-setup.fixture.js @@ -0,0 +1,5 @@ +'use strict'; + +exports.mochaGlobalSetup = async function () { + throw new Error('Setup problem'); +} \ No newline at end of file diff --git a/test/integration/fixtures/global-fixtures/global-teardown.fixture.js b/test/integration/fixtures/global-fixtures/global-teardown.fixture.js new file mode 100644 index 0000000000..483022a3a7 --- /dev/null +++ b/test/integration/fixtures/global-fixtures/global-teardown.fixture.js @@ -0,0 +1,5 @@ +'use strict'; + +exports.mochaGlobalTeardown = async function () { + throw new Error('Teardown problem'); +} \ No newline at end of file diff --git a/test/integration/fixtures/global-fixtures/passing-setup.fixture.js b/test/integration/fixtures/global-fixtures/passing-setup.fixture.js new file mode 100644 index 0000000000..8c14b6e34a --- /dev/null +++ b/test/integration/fixtures/global-fixtures/passing-setup.fixture.js @@ -0,0 +1,6 @@ +'use strict'; + +exports.mochaGlobalSetup = async function () { + // Success case + }; + \ No newline at end of file diff --git a/test/integration/fixtures/global-fixtures/passing-teardown.fixture.js b/test/integration/fixtures/global-fixtures/passing-teardown.fixture.js new file mode 100644 index 0000000000..0751bf9bc3 --- /dev/null +++ b/test/integration/fixtures/global-fixtures/passing-teardown.fixture.js @@ -0,0 +1,6 @@ +'use strict'; + +exports.mochaGlobalTeardown = async function () { + // Success case + }; + \ No newline at end of file diff --git a/test/integration/fixtures/global-fixtures/test.fixture.js b/test/integration/fixtures/global-fixtures/test.fixture.js new file mode 100644 index 0000000000..2f4c6da2c6 --- /dev/null +++ b/test/integration/fixtures/global-fixtures/test.fixture.js @@ -0,0 +1,7 @@ +'use strict'; + +describe('Test Suite', function() { + it('should pass', function() { + // This test passes + }); +}); \ No newline at end of file diff --git a/test/integration/fixtures/global-teardown-error.js b/test/integration/fixtures/global-teardown-error.js new file mode 100644 index 0000000000..b958030f1b --- /dev/null +++ b/test/integration/fixtures/global-teardown-error.js @@ -0,0 +1,9 @@ +'use strict' + +const { it } = require('../../../lib/mocha'); + +it('should pass', () => {}); + +exports.mochaGlobalTeardown = async function () { + throw new Error('Teardown problem') +} \ No newline at end of file diff --git a/test/integration/global-fixtures.spec.js b/test/integration/global-fixtures.spec.js new file mode 100644 index 0000000000..a384c95525 --- /dev/null +++ b/test/integration/global-fixtures.spec.js @@ -0,0 +1,56 @@ +'use strict'; + +const assert = require('assert'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +describe('Run Global Fixtures Error Handling', function () { + + const FIXTURES_DIR = 'test/integration/fixtures/global-fixtures/'; + + function runMochaWithFixture(setupFile, testFile) { + return spawnSync('node', [ + 'bin/mocha', + '--require', path.join(FIXTURES_DIR, setupFile), + path.join(FIXTURES_DIR, testFile) + ], { encoding: 'utf-8' }); + } + + function assertFailure(result, expectedMessage) { + assert.strictEqual(result.status, 1, 'Process should exit with 1'); + assert( + result.stderr.includes(expectedMessage) || result.stdout.includes(expectedMessage), + `Should show error message: ${expectedMessage}` + ); + } + + it('should fail with non-zero exit code when global setup fails', function () { + const result = runMochaWithFixture('global-setup.fixture.js', 'failing-test.fixture.js'); + assertFailure(result, 'Setup problem'); + }); + + it('should fail with non-zero exit code when global teardown fails', function () { + const result = runMochaWithFixture('global-teardown.fixture.js', 'failing-test.fixture.js'); + assertFailure(result, 'Teardown problem'); + }); + + it('should combine failures with setup failures', function () { + const result = runMochaWithFixture('global-setup.fixture.js', 'failing-test.fixture.js'); + assertFailure(result, 'Setup problem'); + }); + + it('should combine failures with teardown failures', function () { + const result = runMochaWithFixture('global-teardown.fixture.js', 'failing-test.fixture.js'); + assertFailure(result, 'Teardown problem'); + }); + + it('should pass with zero exit code when no errors occur in setup', function () { + const result = runMochaWithFixture('passing-setup.fixture.js', 'test.fixture.js'); + assert.strictEqual(result.status, 0, 'Process should exit with 0'); + }); + + it('should pass with zero exit code when no errors occur in teardown', function () { + const result = runMochaWithFixture('passing-teardown.fixture.js', 'test.fixture.js'); + assert.strictEqual(result.status, 0, 'Process should exit with 0'); + }); +}); diff --git a/test/integration/global-teardown-errors.spec.js b/test/integration/global-teardown-errors.spec.js new file mode 100644 index 0000000000..981f657e1b --- /dev/null +++ b/test/integration/global-teardown-errors.spec.js @@ -0,0 +1,102 @@ +'use strict' + +const assert = require('assert'); +const path = require('path'); +const { spawnSync } = require('child_process'); +const fs = require('fs').promises; + +describe('Global Teardown Error Handling', function() { + this.timeout(5000); + + const setupFile = 'test/fixtures/global-teardown/setup.js'; + const testFile = 'test/fixtures/global-teardown/test.js'; + + before(async function() { + await fs.mkdir(path.dirname(setupFile), { recursive: true }); + + await fs.writeFile(setupFile, ` + exports.mochaGlobalTeardown = async function () { + throw new Error('Teardown failure'); + }; + `); + + await fs.writeFile(testFile, ` + describe('Test Suite', function() { + it('passing test', function() { + // This test passes + }); + }); + `); + }); + + after(async function() { + await fs.rm(path.dirname(setupFile), { recursive: true, force: true }); + }); + + it('should fail with non-zero exit code when global teardown fails', function() { + const result = spawnSync('node', [ + 'bin/mocha', + '--require', setupFile, + testFile + ], { + encoding: 'utf8' + }); + + assert.strictEqual(result.status, 1, 'Process should exit with code 1'); + + assert(result.stderr.includes('Teardown failure') || + result.stdout.includes('Teardown failure'), + 'Should show teardown error message'); + }); + + it('should combine test failures with teardown failures', async function() { + + await fs.writeFile(testFile, ` + describe('Test Suite', function() { + it('failing test', function() { + throw new Error('Test failure'); + }); + }); + `); + + const result = spawnSync('node', [ + 'bin/mocha', + '--require', setupFile, + testFile + ], { + encoding: 'utf8' + }); + + assert.strictEqual(result.status, 1, 'Process should exit with code 1'); + + const output = result.stdout + result.stderr; + assert(output.includes('Test failure'), 'Should show test error'); + assert(output.includes('Teardown failure'), 'Should show teardown error'); + }); + + it('should pass with zero exit code when no errors occur', async function() { + await fs.writeFile(setupFile, ` + exports.mochaGlobalTeardown = async function () { + // Success case + }; + `); + + await fs.writeFile(testFile, ` + describe('Test Suite', function() { + it('passing test', function() { + // This test passes + }); + }); + `); + + const result = spawnSync('node', [ + 'bin/mocha', + '--require', setupFile, + testFile + ], { + encoding: 'utf8' + }); + + assert.strictEqual(result.status, 0, 'Process should exit with code 0'); + }); +}); \ No newline at end of file diff --git a/test/unit/mocha.spec.js b/test/unit/mocha.spec.js index e0f603574e..56d478704b 100644 --- a/test/unit/mocha.spec.js +++ b/test/unit/mocha.spec.js @@ -1072,7 +1072,8 @@ describe('Mocha', function () { await mocha.runGlobalSetup(context); expect(mocha._runGlobalFixtures, 'to have a call satisfying', [ mocha.options.globalSetup, - context + context, + 'Global Setup' ]); }); }); @@ -1102,7 +1103,8 @@ describe('Mocha', function () { await mocha.runGlobalTeardown(); expect(mocha._runGlobalFixtures, 'to have a call satisfying', [ mocha.options.globalTeardown, - context + context, + 'Global Teardown' ]); }); });