Skip to content

Commit

Permalink
feat(launcher): Add concurrency limit
Browse files Browse the repository at this point in the history
Especially services like Browserstack and SauceLabs have limitations on
how many browsers
can be launched at the same time. The new config option `concurrency`
allows to
specify an upper limit of how many browsers are allowed to run at the
same time.

Ref: karma-runner/karma-sauce-launcher#40

Closes #1465
  • Loading branch information
dignifiedquire committed Oct 21, 2015
1 parent b138619 commit 1741deb
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 21 deletions.
4 changes: 4 additions & 0 deletions config.tpl.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,7 @@ module.exports = (config) ->
# Continuous Integration mode
# if true, Karma captures browsers, runs the tests and exits
singleRun: false

# Concurrency level
# how many browser should be started simultanous
concurrency: Infinity
6 changes: 5 additions & 1 deletion config.tpl.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ module.exports = function(config) {

// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false
singleRun: false,

// Concurrency level
// how many browser should be started simultanous
concurrency: Infinity
})
}
4 changes: 4 additions & 0 deletions config.tpl.ls
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,7 @@ module.exports = (config) ->
# Continuous Integration mode
# if true, Karma captures browsers, runs the tests and exits
singleRun: false

# Concurrency level
# how many browser should be started simultanous
concurrency: Infinity
10 changes: 9 additions & 1 deletion docs/config/01-configuration-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ Click <a href="preprocessors.html">here</a> for more information.
**Possible Values:**

* `http:`
* `https:`
* `https:`

**Description:** Protocol used for running the Karma webserver.

Expand Down Expand Up @@ -478,6 +478,14 @@ iFrame and may need a new window to run.
All of Karma's urls get prefixed with the `urlRoot`. This is helpful when using proxies, as
sometimes you might want to proxy a url that is already taken by Karma.

## concurrency
**Type:** Number

**Default:** `Infinity`

**Description:** How many browser Karma launches in parallel.

Especially on sevices like SauceLabs and Browserstack it makes sense to only launch a limited amount of browsers at once, and only start more when those have finished. Using this configuration you can sepcify how many browsers should be running at once at any given point in time.

[plugins]: plugins.html
[config/files]: files.html
Expand Down
1 change: 1 addition & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ var Config = function () {
this.browserDisconnectTimeout = 2000
this.browserDisconnectTolerance = 0
this.browserNoActivityTimeout = 10000
this.concurrency = Infinity
}

var CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' +
Expand Down
47 changes: 40 additions & 7 deletions lib/launcher.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
var log = require('./logger').create('launcher')
var Promise = require('bluebird')
var Batch = require('batch')

var log = require('./logger').create('launcher')

var baseDecorator = require('./launchers/base').decoratorFactory
var captureTimeoutDecorator = require('./launchers/capture_timeout').decoratorFactory
Expand Down Expand Up @@ -31,9 +33,10 @@ var Launcher = function (emitter, injector) {
return null
}

this.launch = function (names, protocol, hostname, port, urlRoot) {
var browser
this.launch = function (names, protocol, hostname, port, urlRoot, concurrency) {
var url = protocol + '//' + hostname + ':' + port + urlRoot
var batch = new Batch()
batch.concurrency(concurrency)

lastStartTime = Date.now()

Expand All @@ -54,7 +57,7 @@ var Launcher = function (emitter, injector) {
}

try {
browser = injector.createChild([locals], ['launcher:' + name]).get('launcher:' + name)
var browser = injector.createChild([locals], ['launcher:' + name]).get('launcher:' + name)
} catch (e) {
if (e.message.indexOf('No provider for "launcher:' + name + '"') !== -1) {
log.warn('Can not load "%s", it is not registered!\n ' +
Expand Down Expand Up @@ -84,15 +87,45 @@ var Launcher = function (emitter, injector) {
}
}

log.info('Starting browser %s', browser.name)
browser.start(url)
batch.push(function (done) {
log.info('Starting browser %s', browser.name)

browser.start(url)
browser.on('browser_process_failure', function () {
done(browser.error)
})

browser.on('done', function () {
// We are not done if there was an error as first the retry takes
// place which we catch with `browser_process_failure` if it fails
if (browser.error) return

done(null, browser)
})
})

browsers.push(browser)
})

batch.end(function (err) {
log.debug('Finished all browsers')

if (err) {
log.error(err)
}
})

return browsers
}

this.launch.$inject = ['config.browsers', 'config.protocol', 'config.hostname', 'config.port', 'config.urlRoot']
this.launch.$inject = [
'config.browsers',
'config.protocol',
'config.hostname',
'config.port',
'config.urlRoot',
'config.concurrency'
]

this.kill = function (id, callback) {
var browser = getBrowserById(id)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@
"Jerry Reptak <jreptak@gmail.com>"
],
"dependencies": {
"batch": "^0.5.3",
"bluebird": "^2.9.27",
"body-parser": "^1.12.4",
"chokidar": "^1.0.1",
Expand Down
Empty file added test/e2e/support/world.js
Empty file.
45 changes: 33 additions & 12 deletions test/unit/launcher.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ describe('launcher', () => {

describe('launch', () => {
it('should inject and start all browsers', () => {
l.launch(['Fake'], 'http:', 'localhost', 1234, '/root/')
l.launch(['Fake'], 'http:', 'localhost', 1234, '/root/', 1)

var browser = FakeBrowser._instances.pop()
expect(browser.start).to.have.been.calledWith('http://localhost:1234/root/')
Expand All @@ -94,24 +94,45 @@ describe('launcher', () => {
})

it('should allow launching a script', () => {
l.launch(['/usr/local/bin/special-browser'], 'http:', 'localhost', 1234, '/')
l.launch(['/usr/local/bin/special-browser'], 'http:', 'localhost', 1234, '/', 1)

var script = ScriptBrowser._instances.pop()
expect(script.start).to.have.been.calledWith('http://localhost:1234/')
expect(script.name).to.equal('/usr/local/bin/special-browser')
})

it('should use the non default host', () => {
l.launch(['Fake'], 'http:', 'whatever', 1234, '/root/')
l.launch(['Fake'], 'http:', 'whatever', 1234, '/root/', 1)

var browser = FakeBrowser._instances.pop()
expect(browser.start).to.have.been.calledWith('http://whatever:1234/root/')
})

it('should only launch the specified number of browsers at once', () => {
l.launch([
'Fake',
'Fake',
'Fake'
], 'http:', 'whatever', 1234, '/root/', 2)

var b1 = FakeBrowser._instances.pop()
var b2 = FakeBrowser._instances.pop()
var b3 = FakeBrowser._instances.pop()

expect(b1.start).to.not.have.been.called
expect(b2.start).to.have.been.calledOnce
expect(b3.start).to.have.been.calledOnce

b1._done()
b2._done()

expect(b1.start).to.have.been.calledOnce
})
})

describe('restart', () => {
it('should restart the browser', () => {
l.launch(['Fake'], 'http:', 'localhost', 1234, '/root/')
l.launch(['Fake'], 'http:', 'localhost', 1234, '/root/', 1)
var browser = FakeBrowser._instances.pop()

var returnedValue = l.restart(lastGeneratedId)
Expand All @@ -120,14 +141,14 @@ describe('launcher', () => {
})

it('should return false if the browser was not launched by launcher (manual)', () => {
l.launch([], 'http:', 'localhost', 1234, '/')
l.launch([], 'http:', 'localhost', 1234, '/', 1)
expect(l.restart('manual-id')).to.equal(false)
})
})

describe('kill', () => {
it('should kill browser with given id', done => {
l.launch(['Fake'])
l.launch(['Fake'], 'http:', 'localhost', 1234, '/', 1)
var browser = FakeBrowser._instances.pop()

l.kill(browser.id, done)
Expand All @@ -137,7 +158,7 @@ describe('launcher', () => {
})

it('should return false if browser does not exist, but still resolve the callback', done => {
l.launch(['Fake'])
l.launch(['Fake'], 'http:', 'localhost', 1234, '/', 1)
var browser = FakeBrowser._instances.pop()

var returnedValue = l.kill('weird-id', done)
Expand All @@ -146,7 +167,7 @@ describe('launcher', () => {
})

it('should not require a callback', done => {
l.launch(['Fake'])
l.launch(['Fake'], 'http:', 'localhost', 1234, '/', 1)
FakeBrowser._instances.pop()

l.kill('weird-id')
Expand All @@ -156,7 +177,7 @@ describe('launcher', () => {

describe('killAll', () => {
it('should kill all running processe', () => {
l.launch(['Fake', 'Fake'], 'http:', 'localhost', 1234)
l.launch(['Fake', 'Fake'], 'http:', 'localhost', 1234, '/', 1)
l.killAll()

var browser = FakeBrowser._instances.pop()
Expand All @@ -169,7 +190,7 @@ describe('launcher', () => {
it('should call callback when all processes killed', () => {
var exitSpy = sinon.spy()

l.launch(['Fake', 'Fake'], 'http:', 'localhost', 1234)
l.launch(['Fake', 'Fake'], 'http:', 'localhost', 1234, '/', 1)
l.killAll(exitSpy)

expect(exitSpy).not.to.have.been.called
Expand Down Expand Up @@ -200,7 +221,7 @@ describe('launcher', () => {

describe('areAllCaptured', () => {
it('should return true if only if all browsers captured', () => {
l.launch(['Fake', 'Fake'], 'http:', 'localhost', 1234)
l.launch(['Fake', 'Fake'], 'http:', 'localhost', 1234, '/', 2)

expect(l.areAllCaptured()).to.equal(false)

Expand All @@ -214,7 +235,7 @@ describe('launcher', () => {

describe('onExit', () => {
it('should kill all browsers', done => {
l.launch(['Fake', 'Fake'], 'http:', 'localhost', 1234, '/', 0, 1)
l.launch(['Fake', 'Fake'], 'http:', 'localhost', 1234, '/', 1)

emitter.emitAsync('exit').then(done)

Expand Down

0 comments on commit 1741deb

Please sign in to comment.