diff --git a/config.tpl.coffee b/config.tpl.coffee
index 05c39992c..e632a9620 100644
--- a/config.tpl.coffee
+++ b/config.tpl.coffee
@@ -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
diff --git a/config.tpl.js b/config.tpl.js
index 69d19c17c..2a5d076ad 100644
--- a/config.tpl.js
+++ b/config.tpl.js
@@ -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
})
}
diff --git a/config.tpl.ls b/config.tpl.ls
index b15e7842c..ecba166bd 100644
--- a/config.tpl.ls
+++ b/config.tpl.ls
@@ -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
diff --git a/docs/config/01-configuration-file.md b/docs/config/01-configuration-file.md
index 862d1e67e..2888cb30d 100644
--- a/docs/config/01-configuration-file.md
+++ b/docs/config/01-configuration-file.md
@@ -369,7 +369,7 @@ Click here for more information.
**Possible Values:**
* `http:`
-* `https:`
+* `https:`
**Description:** Protocol used for running the Karma webserver.
@@ -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
diff --git a/lib/config.js b/lib/config.js
index c1ea2c0ba..c32036519 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -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' +
diff --git a/lib/launcher.js b/lib/launcher.js
index ff1f752df..b5996279b 100644
--- a/lib/launcher.js
+++ b/lib/launcher.js
@@ -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
@@ -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()
@@ -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 ' +
@@ -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)
diff --git a/package.json b/package.json
index 7f3b10f58..754a18848 100644
--- a/package.json
+++ b/package.json
@@ -232,6 +232,7 @@
"Jerry Reptak "
],
"dependencies": {
+ "batch": "^0.5.3",
"bluebird": "^2.9.27",
"body-parser": "^1.12.4",
"chokidar": "^1.0.1",
diff --git a/test/e2e/support/world.js b/test/e2e/support/world.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/unit/launcher.spec.js b/test/unit/launcher.spec.js
index a114954b1..c81ddef4b 100644
--- a/test/unit/launcher.spec.js
+++ b/test/unit/launcher.spec.js
@@ -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/')
@@ -94,7 +94,7 @@ 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/')
@@ -102,16 +102,37 @@ describe('launcher', () => {
})
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)
@@ -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)
@@ -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)
@@ -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')
@@ -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()
@@ -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
@@ -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)
@@ -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)