Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/parallel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
40 changes: 27 additions & 13 deletions lib/workers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
32 changes: 32 additions & 0 deletions test/runner/run_workers_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading