diff --git a/docs/parallel.md b/docs/parallel.md index d6ed1e080..ebefbe27c 100644 --- a/docs/parallel.md +++ b/docs/parallel.md @@ -20,7 +20,7 @@ For anything more specific — your own grouping, a config per group, several br npx codeceptjs run-workers 4 ``` -Steps are not streamed to the console in this mode — output from separate threads can't be interleaved cleanly. While workers run, CodeceptJS sets `process.env.RUNS_WITH_WORKERS=true`, so plugins and helpers can branch on it. All `run` options work here too: `--grep "@smoke"`, `-c codecept.conf.js`, `--debug`, and the rest. +Steps are not streamed to the console in this mode — output from separate threads can't be interleaved cleanly. While workers run, CodeceptJS sets `process.env.RUNS_WITH_WORKERS=true`, so plugins and helpers can branch on it. All `run` options work here too: `--grep "@smoke"`, `-c codecept.conf.js`, `--debug`, and the rest. With `--grep`, only matching tests are spread across workers, so a narrow pattern may start fewer workers than requested. By default, workers are created with a staggered delay of 200ms to prevent CPU spikes and stagger browser initializations. You can adjust this via `workerInitializationDelay` in your configuration. diff --git a/lib/workers.js b/lib/workers.js index 84a96b02d..1957bb8a9 100644 --- a/lib/workers.js +++ b/lib/workers.js @@ -57,6 +57,17 @@ const populateGroups = numberOfWorkers => { return groups } +const matchesGrep = (mocha, test) => { + const { grep, invert } = mocha.options + if (!grep) return true + return grep.test(test.fullTitle()) !== Boolean(invert) +} + +const withoutEmptyGroups = groups => { + const nonEmpty = groups.filter(group => group.length) + return nonEmpty.length ? nonEmpty : groups.slice(0, 1) +} + const createWorker = (workerObject, isPoolMode = false) => { const worker = new Worker(pathToWorker, { workerData: { @@ -345,7 +356,8 @@ class Workers extends EventEmitter { if (config.by === 'pool') { this.createTestPool(numberOfWorkers) } else { - this.testGroups = config.by === 'suite' ? await this.createGroupsOfSuites(numberOfWorkers) : await this.createGroupsOfTests(numberOfWorkers) + const groups = config.by === 'suite' ? await this.createGroupsOfSuites(numberOfWorkers) : await this.createGroupsOfTests(numberOfWorkers) + this.testGroups = withoutEmptyGroups(groups) } } } @@ -379,13 +391,16 @@ class Workers extends EventEmitter { const groups = populateGroups(numberOfWorkers) let groupCounter = 0 + const tests = [] mocha.suite.eachTest(test => { - const i = groupCounter % groups.length - if (test) { - groups[i].push(test.uid) - groupCounter++ - } + if (test) tests.push(test) }) + const selected = tests.filter(test => matchesGrep(mocha, test)) + + for (const test of selected.length ? selected : tests) { + groups[groupCounter % groups.length].push(test.uid) + groupCounter++ + } // Clean up after collecting test UIDs mocha.unloadFiles() @@ -464,14 +479,13 @@ class Workers extends EventEmitter { mocha.files = files await loadTests(mocha) - mocha.suite.suites.forEach(suite => { + const suites = mocha.suite.suites.map(suite => suite.tests.filter(test => test)) + const selected = suites.map(tests => tests.filter(test => matchesGrep(mocha, test))).filter(tests => tests.length) + + for (const tests of selected.length ? selected : suites) { const i = indexOfSmallestElement(groups) - suite.tests.forEach(test => { - if (test) { - groups[i].push(test.uid) - } - }) - }) + tests.forEach(test => groups[i].push(test.uid)) + } // Clean up after collecting test UIDs mocha.unloadFiles() diff --git a/test/runner/run_workers_test.js b/test/runner/run_workers_test.js index fe805602a..9781e9446 100644 --- a/test/runner/run_workers_test.js +++ b/test/runner/run_workers_test.js @@ -95,6 +95,38 @@ describe('CodeceptJS Workers Runner', function () { }) }) + it('should distribute only tests matching grep', function (done) { + exec(`${codecept_run} 3 --grep "grep"`, (err, stdout) => { + expect(stdout).toContain('Worker 1: 1 test') + expect(stdout).toContain('Worker 2: 1 test') + expect(stdout).not.toContain('Worker 3:') + expect(stdout).not.toContain('No tests found by pattern') + expect(stdout).toContain('OK | 2 passed') + expect(err).toEqual(null) + done() + }) + }) + + it('should distribute only tests not matching inverted grep', function (done) { + exec(`${codecept_run} 2 --grep "Workers" --invert`, (err, stdout) => { + expect(stdout).toContain('Worker 1: 1 test') + expect(stdout).toContain('Worker 2: 1 test') + expect(stdout).toContain('From worker @1_grep print message 1') + expect(stdout).toContain('From worker @2_grep print message 2') + expect(stdout).toContain('OK | 2 passed') + expect(err).toEqual(null) + done() + }) + }) + + it('should suggest tests when grep matches nothing', function (done) { + exec(`${codecept_run} 2 --grep "nonexistent"`, (err, stdout) => { + expect(stdout).toContain('No tests found by pattern: /nonexistent/') + expect(stdout).toContain('OK | 0 passed') + done() + }) + }) + it('should use suites', function (done) { if (!semver.satisfies(process.version, '>=11.7.0')) this.skip('not for node version') exec(`${codecept_run} 2 --suites`, (err, stdout) => {