Skip to content

Commit

Permalink
parallel: use ipc for communication
Browse files Browse the repository at this point in the history
  • Loading branch information
charlierudolph committed Oct 4, 2018
1 parent 4ae6f25 commit 0c76b0c
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 67 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ Please see [CONTRIBUTING.md](https://github.com/cucumber/cucumber/blob/master/CO

### [Unreleased](https://github.com/cucumber/cucumber-js/compare/v5.0.1...master) (In Git)

#### Bug Fixes

* Allow writing to stdout when running in parallel

### [5.0.1](https://github.com/cucumber/cucumber-js/compare/v5.0.0...v5.0.1) (2018-04-09)

#### Bug Fixes
Expand Down
25 changes: 25 additions & 0 deletions features/parallel.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Feature: Running scenarios in parallel

Background:
Given a file named "features/step_definitions/cucumber_steps.js" with:
"""
import {Given} from 'cucumber'
import Promise from 'bluebird'
Given(/^a slow step$/, function(callback) {
setTimeout(callback, 1000)
})
"""

Scenario: running in parallel can improve speed if there are async operations
Given a file named "features/a.feature" with:
"""
Feature: slow
Scenario: a
Given a slow step
Scenario: b
Given a slow step
"""
When I run cucumber-js with `--parallel 2`
Then it passes
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
"cli-table3": "^0.5.1",
"colors": "^1.1.2",
"commander": "^2.9.0",
"cross-spawn": "^6.0.5",
"cucumber-expressions": "^6.0.0",
"cucumber-tag-expressions": "^1.1.1",
"duration": "^0.2.1",
Expand Down
51 changes: 21 additions & 30 deletions src/runtime/parallel/master.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import _ from 'lodash'
import childProcess from 'child_process'
import crossSpawn from 'cross-spawn'
import commandTypes from './command_types'
import path from 'path'
import readline from 'readline'
import Status from '../../status'

const slaveCommand = path.resolve(
Expand Down Expand Up @@ -37,51 +36,47 @@ export default class Master {
this.slaves = {}
}

parseSlaveLine(slave, line) {
const input = JSON.parse(line)
switch (input.command) {
parseSlaveMessage(slave, message) {
switch (message.command) {
case commandTypes.READY:
this.giveSlaveWork(slave)
break
case commandTypes.EVENT:
this.eventBroadcaster.emit(input.name, input.data)
if (input.name === 'test-case-finished') {
this.parseTestCaseResult(input.data.result)
this.eventBroadcaster.emit(message.name, message.data)
if (message.name === 'test-case-finished') {
this.parseTestCaseResult(message.data.result)
}
break
default:
throw new Error(`Unexpected message from slave: ${line}`)
throw new Error(`Unexpected message from slave: ${message}`)
}
}

startSlave(id, total) {
const slaveProcess = childProcess.spawn(slaveCommand, [], {
const slaveProcess = crossSpawn(slaveCommand, [], {
env: _.assign({}, process.env, {
CUCUMBER_PARALLEL: 'true',
CUCUMBER_TOTAL_SLAVES: total,
CUCUMBER_SLAVE_ID: id,
}),
stdio: ['pipe', 'pipe', process.stderr],
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
})
const rl = readline.createInterface({ input: slaveProcess.stdout })
const slave = { process: slaveProcess }
this.slaves[id] = slave
rl.on('line', line => {
this.parseSlaveLine(slave, line)
slave.process.on('message', message => {
this.parseSlaveMessage(slave, message)
})
rl.on('close', () => {
slave.process.on('close', () => {
slave.closed = true
this.onSlaveClose()
})
slave.process.stdin.write(
JSON.stringify({
command: commandTypes.INITIALIZE,
filterStacktraces: this.options.filterStacktraces,
supportCodePaths: this.supportCodePaths,
supportCodeRequiredModules: this.supportCodeRequiredModules,
worldParameters: this.options.worldParameters,
}) + '\n'
)
slave.process.send({
command: commandTypes.INITIALIZE,
filterStacktraces: this.options.filterStacktraces,
supportCodePaths: this.supportCodePaths,
supportCodeRequiredModules: this.supportCodeRequiredModules,
worldParameters: this.options.worldParameters,
})
}

onSlaveClose() {
Expand Down Expand Up @@ -109,18 +104,14 @@ export default class Master {

giveSlaveWork(slave) {
if (this.nextTestCaseIndex === this.testCases.length) {
slave.process.stdin.write(
JSON.stringify({ command: commandTypes.FINALIZE }) + '\n'
)
slave.process.send({ command: commandTypes.FINALIZE })
return
}
const testCase = this.testCases[this.nextTestCaseIndex]
this.nextTestCaseIndex += 1
const skip =
this.options.dryRun || (this.options.failFast && !this.result.success)
slave.process.stdin.write(
JSON.stringify({ command: commandTypes.RUN, skip, testCase }) + '\n'
)
slave.process.send({ command: commandTypes.RUN, skip, testCase })
}

shouldCauseFailure(status) {
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/parallel/run_slave.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import Slave from './slave'

export default async function run() {
const slave = new Slave({
stdin: process.stdin,
stdout: process.stdout,
sendMessage: m => process.send(m),
cwd: process.cwd(),
exit: () => process.exit(),
})
await slave.run()
process.on('message', m => slave.receiveMessage(m))
}
56 changes: 23 additions & 33 deletions src/runtime/parallel/slave.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { formatLocation } from '../../formatter/helpers'
import commandTypes from './command_types'
import EventEmitter from 'events'
import Promise from 'bluebird'
import readline from 'readline'
import serializeError from 'serialize-error'
import StackTraceFilter from '../stack_trace_filter'
import supportCodeLibraryBuilder from '../../support_code_library_builder'
Expand All @@ -20,30 +19,29 @@ const EVENTS = [
'test-case-finished',
]

function replacerSerializeErrors(key, value) {
if (_.isError(value)) {
return serializeError(value)
function serializeResultExceptionIfNecessary(data) {
if (
data.result &&
data.result.exception &&
_.isError(data.result.exception)
) {
data.result.exception = serializeError(data.result.exception)
}
return value
}

export default class Slave {
constructor({ cwd, stdin, stdout }) {
constructor({ cwd, exit, sendMessage }) {
this.initialized = false
this.stdin = stdin
this.stdout = stdout
this.cwd = cwd
this.exit = exit
this.sendMessage = sendMessage
this.eventBroadcaster = new EventEmitter()
this.stackTraceFilter = new StackTraceFilter()
EVENTS.forEach(name => {
this.eventBroadcaster.on(name, data =>
this.stdout.write(
JSON.stringify(
{ command: commandTypes.EVENT, name, data },
replacerSerializeErrors
) + '\n'
)
)
this.eventBroadcaster.on(name, data => {
serializeResultExceptionIfNecessary(data)
this.sendMessage({ command: commandTypes.EVENT, name, data })
})
})
}

Expand All @@ -63,35 +61,27 @@ export default class Slave {
this.stackTraceFilter.filter()
}
await this.runTestRunHooks('beforeTestRunHookDefinitions', 'a BeforeAll')
this.stdout.write(JSON.stringify({ command: commandTypes.READY }) + '\n')
this.sendMessage({ command: commandTypes.READY })
}

async finalize() {
await this.runTestRunHooks('afterTestRunHookDefinitions', 'an AfterAll')
if (this.filterStacktraces) {
this.stackTraceFilter.unfilter()
}
process.exit()
this.exit()
}

parseMasterLine(line) {
const input = JSON.parse(line)
if (input.command === 'initialize') {
this.initialize(input)
} else if (input.command === 'finalize') {
receiveMessage(message) {
if (message.command === 'initialize') {
this.initialize(message)
} else if (message.command === 'finalize') {
this.finalize()
} else if (input.command === 'run') {
this.runTestCase(input)
} else if (message.command === 'run') {
this.runTestCase(message)
}
}

async run() {
this.rl = readline.createInterface({ input: this.stdin })
this.rl.on('line', line => {
this.parseMasterLine(line)
})
}

async runTestCase({ testCase, skip }) {
const testCaseRunner = new TestCaseRunner({
eventBroadcaster: this.eventBroadcaster,
Expand All @@ -101,7 +91,7 @@ export default class Slave {
worldParameters: this.worldParameters,
})
await testCaseRunner.run()
this.stdout.write(JSON.stringify({ command: commandTypes.READY }) + '\n')
this.sendMessage({ command: commandTypes.READY })
}

async runTestRunHooks(key, name) {
Expand Down
16 changes: 15 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1523,6 +1523,16 @@ cross-spawn@^5.0.1, cross-spawn@^5.1.0:
shebang-command "^1.2.0"
which "^1.2.9"

cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
dependencies:
nice-try "^1.0.4"
path-key "^2.0.1"
semver "^5.5.0"
shebang-command "^1.2.0"
which "^1.2.9"

cryptiles@2.x.x:
version "2.0.5"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
Expand Down Expand Up @@ -3439,6 +3449,10 @@ next-tick@1:
version "1.0.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"

nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"

nise@^1.3.3:
version "1.4.2"
resolved "https://registry.yarnpkg.com/nise/-/nise-1.4.2.tgz#a9a3800e3994994af9e452333d549d60f72b8e8c"
Expand Down Expand Up @@ -3740,7 +3754,7 @@ path-is-inside@^1.0.1, path-is-inside@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"

path-key@^2.0.0:
path-key@^2.0.0, path-key@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"

Expand Down

0 comments on commit 0c76b0c

Please sign in to comment.