@christian-bromann/webdriverio
Version:
A nodejs bindings implementation for selenium 2.0/webdriver
444 lines (372 loc) • 13.8 kB
JavaScript
import path from 'path'
import child from 'child_process'
import ConfigParser from './utils/ConfigParser'
import BaseReporter from './utils/BaseReporter'
class Launcher {
constructor (configFile, argv) {
this.configParser = new ConfigParser()
this.configParser.addConfigFile(configFile)
this.configParser.merge(argv)
this.reporters = this.initReporters()
this.argv = argv
this.configFile = configFile
this.exitCode = 0
this.hasTriggeredExitRoutine = false
this.hasStartedAnyProcess = false
this.processes = []
this.schedule = []
this.rid = []
this.processesStarted = 0
}
/**
* check if multiremote or wdio test
*/
isMultiremote () {
let caps = this.configParser.getCapabilities()
return !Array.isArray(caps)
}
/**
* initialise reporters
*/
initReporters () {
let reporter = new BaseReporter()
let config = this.configParser.getConfig()
/**
* if no reporter is set or config property is in a wrong format
* just use the dot reporter
*/
if (!config.reporters || !Array.isArray(config.reporters) || !config.reporters.length) {
config.reporters = ['dot']
}
const reporters = {}
for (let reporterName of config.reporters) {
let Reporter
if (typeof reporterName === 'function') {
Reporter = reporterName
if (!Reporter.reporterName) {
throw new Error('Custom reporters must export a unique \'reporterName\' property')
}
reporters[Reporter.reporterName] = Reporter
} else if (typeof reporterName === 'string') {
try {
Reporter = require(`wdio-${reporterName}-reporter`)
} catch (e) {
throw new Error(`reporter "wdio-${reporterName}-reporter" is not installed. Error: ${e.stack}`)
}
reporters[reporterName] = Reporter
}
if (!Reporter) {
throw new Error(`config.reporters must be an array of strings or functions, but got '${typeof reporterName}': ${reporterName}`)
}
}
/**
* if no reporter options are set or property is in a wrong format default to
* empty object
*/
if (!config.reporterOptions || typeof config.reporterOptions !== 'object') {
config.reporterOptions = {}
}
for (let reporterName in reporters) {
const Reporter = reporters[reporterName]
let reporterOptions = {}
for (let option of Object.keys(config.reporterOptions)) {
if (option === reporterName && typeof config.reporterOptions[reporterName] === 'object') {
// Copy over options specifically for this reporter type
reporterOptions = Object.assign(reporterOptions, config.reporterOptions[reporterName])
} else if (reporters[option]) {
// Don't copy options for other reporters
continue
} else {
// Copy over generic options
reporterOptions[option] = config.reporterOptions[option]
}
}
reporter.add(new Reporter(reporter, config, reporterOptions))
}
return reporter
}
/**
* run sequence
* @return {Promise} that only gets resolves with either an exitCode or an error
*/
async run () {
let config = this.configParser.getConfig()
let caps = this.configParser.getCapabilities()
let launcher = this.getLauncher(config)
this.reporters.handleEvent('start')
/**
* run onPrepare hook
*/
await config.onPrepare(config, caps)
await this.runServiceHook(launcher, 'onPrepare', config, caps)
/**
* if it is an object run multiremote test
*/
if (this.isMultiremote()) {
let exitCode = await new Promise((resolve) => {
this.resolve = resolve
this.startInstance(this.configParser.getSpecs(), caps, 0)
})
/**
* run onComplete hook for multiremote
*/
await this.runServiceHook(launcher, 'onComplete', exitCode)
await config.onComplete(exitCode)
return exitCode
}
/**
* schedule test runs
*/
let cid = 0
for (let capabilities of caps) {
this.schedule.push({
cid: cid++,
caps: capabilities,
specs: this.configParser.getSpecs(capabilities.specs, capabilities.exclude),
availableInstances: capabilities.maxInstances || config.maxInstancesPerCapability,
runningInstances: 0,
seleniumServer: { host: config.host, port: config.port }
})
}
/**
* catches ctrl+c event
*/
process.on('SIGINT', this.exitHandler.bind(this))
/**
* make sure the program will not close instantly
*/
if (process.stdin.isPaused()) {
process.stdin.resume()
}
let exitCode = await new Promise((resolve) => {
this.resolve = resolve
this.runSpecs()
})
/**
* run onComplete hook
*/
await this.runServiceHook(launcher, 'onComplete', exitCode)
await config.onComplete(exitCode)
return exitCode
}
/**
* run service launch sequences
*/
async runServiceHook (launcher, hookName, ...args) {
try {
return await Promise.all(launcher.map((service) => {
if (typeof service[hookName] === 'function') {
return service[hookName](...args)
}
}))
} catch (e) {
console.error(`A service failed in the '${hookName}' hook\n${e.stack}\n\nContinue...`)
}
}
/**
* run multiple single remote tests
* @return {Boolean} true if all specs have been run and all instances have finished
*/
runSpecs () {
let config = this.configParser.getConfig()
while (this.getNumberOfRunningInstances() < config.maxInstances) {
let schedulableCaps = this.schedule
/**
* make sure complete number of running instances is not higher than general maxInstances number
*/
.filter((a) => this.getNumberOfRunningInstances() < config.maxInstances)
/**
* make sure the capabiltiy has available capacities
*/
.filter((a) => a.availableInstances > 0)
/**
* make sure capabiltiy has still caps to run
*/
.filter((a) => a.specs.length > 0)
/**
* make sure we are running caps with less running instances first
*/
.sort((a, b) => a.runningInstances > b.runningInstances)
/**
* continue if no capabiltiy were schedulable
*/
if (schedulableCaps.length === 0) {
break
}
this.startInstance(
[schedulableCaps[0].specs.pop()],
schedulableCaps[0].caps,
schedulableCaps[0].cid,
schedulableCaps[0].seleniumServer
)
schedulableCaps[0].availableInstances--
schedulableCaps[0].runningInstances++
}
return this.getNumberOfRunningInstances() === 0 && this.getNumberOfSpecsLeft() === 0
}
/**
* gets number of all running instances
* @return {number} number of running instances
*/
getNumberOfRunningInstances () {
return this.schedule.map((a) => a.runningInstances).reduce((a, b) => a + b)
}
/**
* get number of total specs left to complete whole suites
* @return {number} specs left to complete suite
*/
getNumberOfSpecsLeft () {
return this.schedule.map((a) => a.specs.length).reduce((a, b) => a + b)
}
/**
* Start instance in a child process.
* @param {Array} specs Specs to run
* @param {Number} cid Capabilities ID
*/
startInstance (specs, caps, cid, server) {
let config = this.configParser.getConfig()
let debug = caps.debug || config.debug
let rid = this.getRunnerId(cid)
let processNumber = this.processesStarted + 1
// process.debugPort defaults to 5858 and is set even when process
// is not being debugged.
let debugArgs = (debug)
? [`--debug=${(process.debugPort + processNumber)}`]
: []
// if you would like to add --debug-brk, use a different port, etc...
let capExecArgs = [
...(config.execArgv || []),
...(caps.execArgv || [])
]
// The default value for child.fork execArgs is process.execArgs,
// so continue to use this unless another value is specified in config.
let defaultArgs = (capExecArgs.length) ? process.execArgv : []
// If an arg appears multiple times the last occurence is used
let execArgv = [ ...defaultArgs, ...debugArgs, ...capExecArgs ]
let childProcess = child.fork(path.join(__dirname, '/runner.js'), process.argv.slice(2), {
cwd: process.cwd(),
execArgv
})
this.processes.push(childProcess)
childProcess
.on('message', this.messageHandler.bind(this))
.on('exit', this.endHandler.bind(this, rid))
childProcess.send({
cid: rid,
command: 'run',
configFile: this.configFile,
argv: this.argv,
caps,
processNumber,
specs,
server,
isMultiremote: this.isMultiremote()
})
this.processesStarted++
}
/**
* generates a runner id
* @param {Number} cid capability id (unique identifier for a capability)
* @return {String} runner id (combination of cid and test id e.g. 0a, 0b, 1a, 1b ...)
*/
getRunnerId (cid) {
if (!this.rid[cid]) {
this.rid[cid] = 'a'
return cid + this.rid[cid]
}
this.rid[cid] = String.fromCharCode(this.rid[cid].charCodeAt(0) + 1)
return cid + this.rid[cid]
}
/**
* emit event from child process to reporter
* @param {Object} m event object
*/
messageHandler (m) {
this.hasStartedAnyProcess = true
if (m.event === 'runner:error') {
this.reporters.handleEvent('error', m)
}
this.reporters.handleEvent(m.event, m)
}
/**
* Close test runner process once all child processes have exited
* @param {Number} cid Capabilities ID
* @param {Number} childProcessExitCode exit code of child process
*/
endHandler (rid, childProcessExitCode) {
this.exitCode = this.exitCode || childProcessExitCode
// Update schedule now this process has ended
if (!this.isMultiremote()) {
// get cid (capability id) from rid (runner id)
let cid = parseInt(rid, 10)
this.schedule[cid].availableInstances++
this.schedule[cid].runningInstances--
}
if (!this.isMultiremote() && !this.runSpecs()) {
return
}
this.reporters.handleEvent('end', {
sigint: this.hasTriggeredExitRoutine,
exitCode: this.exitCode
})
if (this.exitCode === 0) {
return this.resolve(this.exitCode)
}
/**
* finish with exit code 1
*/
return this.resolve(1)
}
/**
* Make sure all started selenium sessions get closed properly and prevent
* having dead driver processes. To do so let the runner end its Selenium
* session first before killing
*/
exitHandler () {
if (this.hasTriggeredExitRoutine || !this.hasStartedAnyProcess) {
console.log('\nKilling process, bye!')
/**
* finish with exit code 1
*/
return this.resolve(1)
}
// When spawned as a subprocess,
// SIGINT will not be forwarded to childs.
// Thus for the child to exit cleanly, we must force send SIGINT
if (!process.stdin.isTTY) {
this.processes.forEach(p => p.kill('SIGINT'))
}
console.log(`
End selenium sessions properly ...
(press crtl+c again to hard kill the runner)
`)
this.hasTriggeredExitRoutine = true
}
/**
* loads launch services
*/
getLauncher (config) {
let launchServices = []
if (!Array.isArray(config.services)) {
return launchServices
}
for (let serviceName of config.services) {
let service
/**
* allow custom services
*/
if (typeof serviceName === 'object') {
launchServices.push(serviceName)
continue
}
try {
service = require(`wdio-${serviceName}-service/launcher`)
} catch (e) {}
if (service && (typeof service.onPrepare === 'function' || typeof service.onPrepare === 'function')) {
launchServices.push(service)
}
}
return launchServices
}
}
export default Launcher