From 6ceca82fdfd51341796caa523d696fa7621639cc Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Mon, 5 Oct 2020 13:22:04 -0700 Subject: [PATCH] make guarantees about orphaned processes - updates workerpool to v6.0.2, which guarantees child processes exit before `Pool#terminate()` resolves - cleanup `test/integration/options/parallel.spec.js` --- lib/nodejs/buffered-worker-pool.js | 4 +- package-lock.json | 12 +- package.json | 3 +- test/integration/options/parallel.spec.js | 641 ++++++++++++---------- 4 files changed, 369 insertions(+), 291 deletions(-) diff --git a/lib/nodejs/buffered-worker-pool.js b/lib/nodejs/buffered-worker-pool.js index 144333ef1a..9b0d4516e5 100644 --- a/lib/nodejs/buffered-worker-pool.js +++ b/lib/nodejs/buffered-worker-pool.js @@ -75,9 +75,7 @@ class BufferedWorkerPool { process.execArgv.join(' ') ); - this.options = Object.assign({}, WORKER_POOL_DEFAULT_OPTS, opts, { - maxWorkers - }); + this.options = {...WORKER_POOL_DEFAULT_OPTS, opts, maxWorkers}; this._pool = workerpool.pool(WORKER_PATH, this.options); } diff --git a/package-lock.json b/package-lock.json index e9635e37cd..95f411ced2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17401,6 +17401,12 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" }, + "pidtree": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.5.0.tgz", + "integrity": "sha512-9nxspIM7OpZuhBxPg73Zvyq7j1QMPMPsGKTqRc2XOaFQauDvoNz9fM1Wdkjmeo7l9GXOZiRs97sPkuayl39wjA==", + "dev": true + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -25216,9 +25222,9 @@ } }, "workerpool": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.0.tgz", - "integrity": "sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA==" + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.2.tgz", + "integrity": "sha512-DSNyvOpFKrNusaaUwk+ej6cBj1bmhLcBfj80elGk+ZIo5JSkq+unB1dLKEOcNfJDZgjGICfhQ0Q5TbP0PvF4+Q==" }, "wrap-ansi": { "version": "5.1.0", diff --git a/package.json b/package.json index edbab206cb..a63e22b343 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "supports-color": "7.1.0", "which": "2.0.2", "wide-align": "1.1.3", - "workerpool": "6.0.0", + "workerpool": "6.0.2", "yargs": "13.3.2", "yargs-parser": "13.1.2", "yargs-unparser": "1.6.1" @@ -130,6 +130,7 @@ "needle": "^2.5.0", "nps": "^5.10.0", "nyc": "^15.1.0", + "pidtree": "^0.5.0", "prettier": "^1.19.1", "remark": "^12.0.1", "remark-github": "^9.0.1", diff --git a/test/integration/options/parallel.spec.js b/test/integration/options/parallel.spec.js index 8fc5389943..8bd3729f23 100644 --- a/test/integration/options/parallel.spec.js +++ b/test/integration/options/parallel.spec.js @@ -1,47 +1,71 @@ 'use strict'; -var Mocha = require('../../../lib/mocha'); -var path = require('path'); -var helpers = require('../helpers'); -var runMochaAsync = helpers.runMochaAsync; -var invokeMochaAsync = helpers.invokeMochaAsync; -var getSummary = helpers.getSummary; -var utils = require('../../../lib/utils'); - -function compareReporters(reporter) { - return runMochaAsync(path.join('options', 'parallel', 'test-a.fixture.js'), [ - '--reporter', - reporter, - '--no-parallel' - ]).then(function(expected) { - expected.output = expected.output.replace(/\d+m?s/g, '100ms'); - return runMochaAsync( - path.join('options', 'parallel', 'test-a.fixture.js'), - ['--reporter', reporter, '--parallel'] - ).then(function(actual) { - actual.output = actual.output.replace(/\d+m?s/g, '100ms'); - return [actual, expected]; - }); - }); +const Mocha = require('../../../lib/mocha'); +const { + runMochaAsync, + invokeMochaAsync, + getSummary, + resolveFixturePath +} = require('../helpers'); +const utils = require('../../../lib/utils'); +const pidtree = require('pidtree'); + +const REPORTER_FIXTURE_PATH = resolveFixturePath('options/parallel/test-a'); + +/** + * Run a test fixture with the same reporter in both parallel and serial modes, + * returning both outputs for comparison + * @param {string} reporter - Reporter name + * @returns {{actual: import('../helpers').Summary, expected: import('../helpers').Summary}} + */ +async function compareReporters(reporter) { + const [actual, expected] = await Promise.all([ + runMochaAsync(REPORTER_FIXTURE_PATH, [ + '--reporter', + reporter, + '--no-parallel' + ]), + runMochaAsync(REPORTER_FIXTURE_PATH, ['--reporter', reporter, '--parallel']) + ]); + + // the test duration is non-deterministic, so we just fudge it + actual.output = expected.output = expected.output.replace(/\d+m?s/g, '100ms'); + + return {actual, expected}; } -function runGenericReporterTest(reporter) { - return compareReporters.call(this, reporter).then(function(result) { - var expected = result.shift(); - var actual = result.shift(); - return expect(actual, 'to satisfy', { - passing: expected.passing, - failing: expected.failing, - pending: expected.pending, - code: expected.code, - output: expected.output - }); +/** + * Many (but not all) reporters can use this assertion to compare output of serial vs. parallel + * @param {string} reporter - Reporter name + */ +async function assertReporterOutputEquality(reporter) { + const {actual, expected} = await compareReporters(reporter); + return expect(actual, 'to satisfy', { + passing: expected.passing, + failing: expected.failing, + pending: expected.pending, + code: expected.code, + output: expected.output }); } +/** + * Polls a process for its list of children PIDs. Returns the first non-empty list found + * @param {number} pid - Process PID + * @returns {number[]} Child PIDs + */ +async function waitForChildPids(pid) { + let childPids = []; + while (!childPids.length) { + childPids = await pidtree(pid); + await new Promise(resolve => setTimeout(resolve, 100)); + } + return childPids; +} + describe('--parallel', function() { describe('when a test has a syntax error', function() { describe('when there is only a single test file', function() { - it('should fail gracefully', function() { + it('should fail gracefully', async function() { return expect( runMochaAsync('options/parallel/syntax-err', ['--parallel']), 'when fulfilled', @@ -52,15 +76,10 @@ describe('--parallel', function() { }); describe('when there are multiple test files', function() { - it('should fail gracefully', function() { + it('should fail gracefully', async function() { return expect( invokeMochaAsync( - [ - require.resolve( - '../fixtures/options/parallel/syntax-err.fixture.js' - ), - '--parallel' - ], + [resolveFixturePath('options/parallel/syntax-err'), '--parallel'], 'pipe' )[1], 'when fulfilled', @@ -71,29 +90,25 @@ describe('--parallel', function() { }); describe('when used with CJS tests', function() { - it('should have the same result as with --no-parallel', function() { - return runMochaAsync( - path.join('options', 'parallel', 'test-*.fixture.js'), - ['--no-parallel'] - ).then(function(expected) { - return expect( - runMochaAsync(path.join('options', 'parallel', 'test-*.fixture.js'), [ - '--parallel' - ]), - 'to be fulfilled with value satisfying', - { - passing: expected.passing, - failing: expected.failing, - pending: expected.pending, - code: expected.code - } - ); - }); + it('should have the same result as with --no-parallel', async function() { + const expected = await runMochaAsync('options/parallel/test-*', [ + '--no-parallel' + ]); + return expect( + runMochaAsync('options/parallel/test-*', ['--parallel']), + 'to be fulfilled with value satisfying', + { + passing: expected.passing, + failing: expected.failing, + pending: expected.pending, + code: expected.code + } + ); }); }); describe('when used with ESM tests', function() { - var esmArgs = + const esmArgs = Number(process.versions.node.split('.')[0]) >= 13 ? [] : ['--experimental-modules']; @@ -102,68 +117,74 @@ describe('--parallel', function() { if (!utils.supportsEsModules()) this.skip(); }); - it('should have the same result as with --no-parallel', function() { - var glob = path.join(__dirname, '..', 'fixtures', 'esm', '*.fixture.mjs'); - return invokeMochaAsync(esmArgs.concat('--no-parallel', glob))[1].then( - function(expected) { - expected = getSummary(expected); - return invokeMochaAsync(esmArgs.concat('--parallel', glob))[1].then( - function(actual) { - actual = getSummary(actual); - expect(actual, 'to satisfy', { - pending: expected.pending, - passing: expected.passing, - failing: expected.failing - }); - } - ); - } + it('should have the same result as with --no-parallel', async function() { + const expected = getSummary( + await invokeMochaAsync([ + ...esmArgs, + '--no-parallel', + resolveFixturePath('esm/*.fixture.mjs') + ])[1] + ); + + const actual = getSummary( + await invokeMochaAsync([ + ...esmArgs, + '--parallel', + resolveFixturePath('esm/*.fixture.mjs') + ])[1] ); + + return expect(actual, 'to satisfy', { + pending: expected.pending, + passing: expected.passing, + failing: expected.failing + }); }); }); describe('when used with --retries', function() { - it('should retry tests appropriately', function() { + it('should retry tests appropriately', async function() { return expect( - runMochaAsync( - path.join('options', 'parallel', 'retries-*.fixture.js'), - ['--parallel'] - ), + runMochaAsync('options/parallel/retries-*', ['--parallel']), 'when fulfilled', - 'to have failed' - ) - .and('when fulfilled', 'to have passed test count', 1) - .and('when fulfilled', 'to have pending test count', 0) - .and('when fulfilled', 'to have failed test count', 1) - .and('when fulfilled', 'to contain output', /count: 3/); + 'to satisfy', + expect + .it('to have failed') + .and('to have passed test count', 1) + .and('to have pending test count', 0) + .and('to have failed test count', 1) + .and('to contain output', /count: 3/) + ); }); }); describe('when used with --allow-uncaught', function() { - it('should bubble up an exception', function() { + it('should bubble up an exception', async function() { return expect( invokeMochaAsync( [ - require.resolve('../fixtures/options/parallel/uncaught.fixture.js'), + resolveFixturePath('options/parallel/uncaught'), '--parallel', '--allow-uncaught' ], 'pipe' )[1], 'when fulfilled', - 'to contain output', - /Error: existential isolation/i - ).and('when fulfilled', 'to have exit code', 1); + 'to satisfy', + expect + .it('to contain output', /Error: existential isolation/i) + .and('to have exit code', 1) + ); }); }); describe('when used with --file', function() { - it('should error out', function() { + it('should error out', async function() { return expect( invokeMochaAsync( [ '--file', - path.join('options', 'parallel', 'test-a.fixture.js'), + resolveFixturePath('options/parallel/test-a'), '--parallel' ], 'pipe' @@ -176,19 +197,12 @@ describe('--parallel', function() { }); describe('when used with --sort', function() { - it('should error out', function() { + it('should error out', async function() { return expect( invokeMochaAsync( [ '--sort', - path.join( - __dirname, - '..', - 'fixtures', - 'options', - 'parallel', - 'test-*.fixture.js' - ), + resolveFixturePath('options/parallel/test-*'), '--parallel' ], 'pipe' @@ -201,18 +215,11 @@ describe('--parallel', function() { }); describe('when used with exclusive tests', function() { - it('should error out', function() { + it('should error out', async function() { return expect( invokeMochaAsync( [ - path.join( - __dirname, - '..', - 'fixtures', - 'options', - 'parallel', - 'exclusive-test-*.fixture.js' - ), + resolveFixturePath('options/parallel/exclusive-test-*'), '--parallel' ], 'pipe' @@ -225,28 +232,24 @@ describe('--parallel', function() { }); describe('when used with --bail', function() { - it('should skip some tests', function() { - return runMochaAsync( - path.join('options', 'parallel', 'test-*.fixture.js'), - ['--parallel', '--bail'] - ).then(function(result) { - // we don't know _exactly_ how many tests will be skipped here - // due to the --bail, but the number of tests completed should be - // less than the total, which is 5. - return expect( - result.passing + result.pending + result.failing, - 'to be less than', - 5 - ); - }); + it('should skip some tests', async function() { + const result = await runMochaAsync('options/parallel/test-*', [ + '--parallel', + '--bail' + ]); + // we don't know _exactly_ how many tests will be skipped here + // due to the --bail, but the number of tests completed should be + // less than the total, which is 5. + return expect( + result.passing + result.pending + result.failing, + 'to be less than', + 5 + ); }); - it('should fail', function() { + it('should fail', async function() { return expect( - runMochaAsync(path.join('options', 'parallel', 'test-*.fixture.js'), [ - '--parallel', - '--bail' - ]), + runMochaAsync('options/parallel/test-*', ['--parallel', '--bail']), 'when fulfilled', 'to have failed' ); @@ -254,19 +257,18 @@ describe('--parallel', function() { }); describe('when encountering a "bail" in context', function() { - it('should skip some tests', function() { - return runMochaAsync('options/parallel/bail', ['--parallel']).then( - function(result) { - return expect( - result.passing + result.pending + result.failing, - 'to be less than', - 2 - ); - } + it('should skip some tests', async function() { + const result = await runMochaAsync('options/parallel/bail', [ + '--parallel' + ]); + return expect( + result.passing + result.pending + result.failing, + 'to be less than', + 2 ); }); - it('should fail', function() { + it('should fail', async function() { return expect( runMochaAsync('options/parallel/bail', ['--parallel', '--bail']), 'when fulfilled', @@ -276,173 +278,174 @@ describe('--parallel', function() { }); describe('when used with "grep"', function() { - it('should be equivalent to running in serial', function() { - return runMochaAsync( - path.join('options', 'parallel', 'test-*.fixture.js'), - ['--no-parallel', '--grep="suite d"'] - ).then(function(expected) { - return expect( - runMochaAsync(path.join('options', 'parallel', 'test-*.fixture.js'), [ - '--parallel', - '--grep="suite d"' - ]), - 'to be fulfilled with value satisfying', - { - passing: expected.passing, - failing: expected.failing, - pending: expected.pending, - code: expected.code - } - ); - }); + it('should be equivalent to running in serial', async function() { + const expected = await runMochaAsync('options/parallel/test-*', [ + '--no-parallel', + '--grep="suite d"' + ]); + return expect( + runMochaAsync('options/parallel/test-*', [ + '--parallel', + '--grep="suite d"' + ]), + 'to be fulfilled with value satisfying', + { + passing: expected.passing, + failing: expected.failing, + pending: expected.pending, + code: expected.code + } + ); }); }); describe('reporter equivalence', function() { // each reporter name is duplicated; one is in all lower-case // 'base' is abstract, 'html' is browser-only, others are incompatible - var DENY = ['progress', 'base', 'html', 'markdown', 'json-stream']; + const DENY = ['progress', 'base', 'html', 'markdown', 'json-stream']; Object.keys(Mocha.reporters) - .filter(function(name) { - return /^[a-z]/.test(name) && DENY.indexOf(name) === -1; - }) - .forEach(function(reporter) { - describe( - 'when multiple test files run with --reporter=' + reporter, - function() { - it('should have the same result as when run with --no-parallel', function() { - // note that the output may not be in the same order, as running file - // order is non-deterministic in parallel mode - return runMochaAsync( - path.join('options', 'parallel', 'test-*.fixture.js'), - ['--reporter', reporter, '--no-parallel'] - ).then(function(expected) { - return expect( - runMochaAsync( - path.join('options', 'parallel', 'test-*.fixture.js'), - ['--reporter', reporter, '--parallel'] - ), - 'to be fulfilled with value satisfying', - { - passing: expected.passing, - failing: expected.failing, - pending: expected.pending, - code: expected.code - } - ); - }); - }); - } - ); + .filter(name => /^[a-z]/.test(name) && DENY.indexOf(name) === -1) + .forEach(reporter => { + describe(`when multiple test files run with --reporter=${reporter}`, function() { + it('should have the same result as when run with --no-parallel', async function() { + // note that the output may not be in the same order, as running file + // order is non-deterministic in parallel mode + const expected = await runMochaAsync('options/parallel/test-*', [ + '--reporter', + reporter, + '--no-parallel' + ]); + return expect( + runMochaAsync('options/parallel/test-*', [ + '--reporter', + reporter, + '--parallel' + ]), + 'to be fulfilled with value satisfying', + { + passing: expected.passing, + failing: expected.failing, + pending: expected.pending, + code: expected.code + } + ); + }); + }); }); - }); - describe('when a single test file is run with --reporter=dot', function() { - it('should have the same output as when run with --no-parallel', function() { - return runGenericReporterTest.call(this, 'dot'); + describe('when a single test file is run with --reporter=dot', function() { + it('should have the same output as when run with --no-parallel', async function() { + return assertReporterOutputEquality.call(this, 'dot'); + }); }); - }); - describe('when a single test file is run with --reporter=doc', function() { - it('should have the same output as when run with --no-parallel', function() { - return runGenericReporterTest.call(this, 'doc'); + describe('when a single test file is run with --reporter=doc', function() { + it('should have the same output as when run with --no-parallel', async function() { + return assertReporterOutputEquality.call(this, 'doc'); + }); }); - }); - describe('when a single test file is run with --reporter=tap', function() { - it('should have the same output as when run with --no-parallel', function() { - return runGenericReporterTest.call(this, 'tap'); + describe('when a single test file is run with --reporter=tap', function() { + it('should have the same output as when run with --no-parallel', async function() { + return assertReporterOutputEquality.call(this, 'tap'); + }); }); - }); - describe('when a single test file is run with --reporter=list', function() { - it('should have the same output as when run with --no-parallel', function() { - return runGenericReporterTest.call(this, 'list'); + describe('when a single test file is run with --reporter=list', function() { + it('should have the same output as when run with --no-parallel', async function() { + return assertReporterOutputEquality.call(this, 'list'); + }); }); - }); - describe('when a single test file is run with --reporter=min', function() { - it('should have the same output as when run with --no-parallel', function() { - return runGenericReporterTest.call(this, 'min'); + describe('when a single test file is run with --reporter=min', function() { + it('should have the same output as when run with --no-parallel', async function() { + return assertReporterOutputEquality.call(this, 'min'); + }); }); - }); - describe('when a single test file is run with --reporter=spec', function() { - it('should have the same output as when run with --no-parallel', function() { - return runGenericReporterTest.call(this, 'spec'); + describe('when a single test file is run with --reporter=spec', function() { + it('should have the same output as when run with --no-parallel', async function() { + return assertReporterOutputEquality.call(this, 'spec'); + }); }); - }); - describe('when a single test file is run with --reporter=nyan', function() { - it('should have the same output as when run with --no-parallel', function() { - return runGenericReporterTest.call(this, 'nyan'); + describe('when a single test file is run with --reporter=nyan', function() { + it('should have the same output as when run with --no-parallel', async function() { + return assertReporterOutputEquality.call(this, 'nyan'); + }); }); - }); - describe('when a single test file is run with --reporter=landing', function() { - it('should have the same output as when run with --no-parallel', function() { - return runGenericReporterTest.call(this, 'landing'); + describe('when a single test file is run with --reporter=landing', function() { + it('should have the same output as when run with --no-parallel', async function() { + return assertReporterOutputEquality.call(this, 'landing'); + }); }); - }); - describe('when a single test file is run with --reporter=progress', function() { - it('should fail due to incompatibility', function() { - return expect( - invokeMochaAsync( - [ - require.resolve('../fixtures/options/parallel/test-a.fixture.js'), - '--reporter=progress', - '--parallel' - ], - 'pipe' - )[1], - 'when fulfilled', - 'to have failed' - ).and('when fulfilled', 'to contain output', /mutually exclusive/); + describe('when a single test file is run with --reporter=progress', function() { + it('should fail due to incompatibility', async function() { + return expect( + invokeMochaAsync( + [ + resolveFixturePath('options/parallel/test-a'), + '--reporter=progress', + '--parallel' + ], + 'pipe' + )[1], + 'when fulfilled', + 'to satisfy', + expect + .it('to have failed') + .and('to contain output', /mutually exclusive/) + ); + }); }); - }); - describe('when a single test file is run with --reporter=markdown', function() { - it('should fail due to incompatibility', function() { - return expect( - invokeMochaAsync( - [ - require.resolve('../fixtures/options/parallel/test-a.fixture.js'), - '--reporter=markdown', - '--parallel' - ], - 'pipe' - )[1], - 'when fulfilled', - 'to have failed' - ).and('when fulfilled', 'to contain output', /mutually exclusive/); + describe('when a single test file is run with --reporter=markdown', function() { + it('should fail due to incompatibility', async function() { + return expect( + invokeMochaAsync( + [ + resolveFixturePath('options/parallel/test-a'), + '--reporter=markdown', + '--parallel' + ], + 'pipe' + )[1], + 'when fulfilled', + 'to satisfy', + expect + .it('to have failed') + .and('to contain output', /mutually exclusive/) + ); + }); }); - }); - describe('when a single test file is run with --reporter=json-stream', function() { - it('should fail due to incompatibility', function() { - return expect( - invokeMochaAsync( - [ - require.resolve('../fixtures/options/parallel/test-a.fixture.js'), - '--reporter=json-stream', - '--parallel' - ], - 'pipe' - )[1], - 'when fulfilled', - 'to have failed' - ).and('when fulfilled', 'to contain output', /mutually exclusive/); + describe('when a single test file is run with --reporter=json-stream', function() { + it('should fail due to incompatibility', async function() { + return expect( + invokeMochaAsync( + [ + resolveFixturePath('options/parallel/test-a'), + '--reporter=json-stream', + '--parallel' + ], + 'pipe' + )[1], + 'when fulfilled', + 'to satisfy', + expect + .it('to have failed') + .and('to contain output', /mutually exclusive/) + ); + }); }); - }); - describe('when a single test file is run with --reporter=json', function() { - it('should have the same output as when run with --no-parallel', function() { - // this one has some timings/durations that we can safely ignore - return compareReporters.call(this, 'json').then(function(result) { - var expected = result.shift(); + describe('when a single test file is run with --reporter=json', function() { + it('should have the same output as when run with --no-parallel', async function() { + // this one has some timings/durations that we can safely ignore + const {expected, actual} = await compareReporters('json'); expected.output = JSON.parse(expected.output); - var actual = result.shift(); actual.output = JSON.parse(actual.output); return expect(actual, 'to satisfy', { passing: expected.passing, @@ -462,17 +465,14 @@ describe('--parallel', function() { }); }); }); - }); - describe('when a single test file is run with --reporter=xunit', function() { - it('should have the same output as when run with --no-parallel', function() { - // durations need replacing - return compareReporters.call(this, 'xunit').then(function(result) { - var expected = result.shift(); + describe('when a single test file is run with --reporter=xunit', function() { + it('should have the same output as when run with --no-parallel', async function() { + // durations need replacing + const {expected, actual} = await compareReporters('xunit'); expected.output = expected.output .replace(/time=".+?"/g, 'time="0.5"') .replace(/timestamp=".+?"/g, 'timestamp="some-timestamp'); - var actual = result.shift(); actual.output = actual.output .replace(/time=".+?"/g, 'time="0.5"') .replace(/timestamp=".+?"/g, 'timestamp="some-timestamp'); @@ -486,4 +486,77 @@ describe('--parallel', function() { }); }); }); + + describe('pool shutdown', function() { + // these are unusual and deserve some explanation. we start our mocha + // subprocess, and in parallel mode, that subprocess spawns more + // subprocesses. `invokeMochaAsync` returns a tuple of a `mocha` + // `ChildProcess` object and a `Promise` (which will resolve/reject when the + // subprocess finishes its test run). we use the `pid` from the `mocha` + // `ChildProcess` to ask `pidtree` (https://npm.im/pidtree) for the + // children, which are the worker processes. the `mocha` subprocess does + // _not_ immediately spawn worker processes, so we need to _poll_ for child + // processes. when we have them, we record them. once the `Promise` + // resolves, we then attempt to get pid information for our `mocha` process + // and _each_ worker process we retrieved earlier. if a process does not + // exist, `pidtree` will reject--this is what we _want_ to happen. we check + // each explicitly in case the child processes are somehow orphaned. + // + // we return `null` for each in the case of the expected rejection--if the + // `Promise.all()` call results in an array containing anything _except_ a + // bunch of `null`s, then this test fails, because one of the processes is + // still running. this behavior is dependent on `workerpool@6.0.2`, which + // added a guarantee that terminating the pool will _wait_ until all child + // processes have actually exited. + describe('during normal operation', function() { + it('should not leave orphaned processes around', async function() { + const [{pid}, promise] = invokeMochaAsync([ + resolveFixturePath('options/parallel/test-*'), + '--parallel' + ]); + const childPids = await waitForChildPids(pid); + await promise; + return expect( + Promise.all( + [pid, ...childPids].map(async childPid => { + let pids = null; + try { + pids = await pidtree(childPid, {root: true}); + } catch (ignored) {} + return pids; + }) + ), + 'when fulfilled', + 'to have items satisfying', + null + ); + }); + }); + + describe('during operation with --bail', function() { + it('should not leave orphaned processes around', async function() { + const [{pid}, promise] = invokeMochaAsync([ + resolveFixturePath('options/parallel/test-*'), + '--bail', + '--parallel' + ]); + const childPids = await waitForChildPids(pid); + await promise; + return expect( + Promise.all( + [pid, ...childPids].map(async childPid => { + let pids = null; + try { + pids = await pidtree(childPid, {root: true}); + } catch (ignored) {} + return pids; + }) + ), + 'when fulfilled', + 'to have items satisfying', + null + ); + }); + }); + }); });