Skip to content

Commit 1741deb

Browse files
feat(launcher): Add concurrency limit
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
1 parent b138619 commit 1741deb

9 files changed

Lines changed: 97 additions & 21 deletions

File tree

config.tpl.coffee

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,7 @@ module.exports = (config) ->
6464
# Continuous Integration mode
6565
# if true, Karma captures browsers, runs the tests and exits
6666
singleRun: false
67+
68+
# Concurrency level
69+
# how many browser should be started simultanous
70+
concurrency: Infinity

config.tpl.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ module.exports = function(config) {
5858

5959
// Continuous Integration mode
6060
// if true, Karma captures browsers, runs the tests and exits
61-
singleRun: false
61+
singleRun: false,
62+
63+
// Concurrency level
64+
// how many browser should be started simultanous
65+
concurrency: Infinity
6266
})
6367
}

config.tpl.ls

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,7 @@ module.exports = (config) ->
6464
# Continuous Integration mode
6565
# if true, Karma captures browsers, runs the tests and exits
6666
singleRun: false
67+
68+
# Concurrency level
69+
# how many browser should be started simultanous
70+
concurrency: Infinity

docs/config/01-configuration-file.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ Click <a href="preprocessors.html">here</a> for more information.
369369
**Possible Values:**
370370

371371
* `http:`
372-
* `https:`
372+
* `https:`
373373

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

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

481+
## concurrency
482+
**Type:** Number
483+
484+
**Default:** `Infinity`
485+
486+
**Description:** How many browser Karma launches in parallel.
487+
488+
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.
481489

482490
[plugins]: plugins.html
483491
[config/files]: files.html

lib/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ var Config = function () {
258258
this.browserDisconnectTimeout = 2000
259259
this.browserDisconnectTolerance = 0
260260
this.browserNoActivityTimeout = 10000
261+
this.concurrency = Infinity
261262
}
262263

263264
var CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' +

lib/launcher.js

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
var log = require('./logger').create('launcher')
21
var Promise = require('bluebird')
2+
var Batch = require('batch')
3+
4+
var log = require('./logger').create('launcher')
35

46
var baseDecorator = require('./launchers/base').decoratorFactory
57
var captureTimeoutDecorator = require('./launchers/capture_timeout').decoratorFactory
@@ -31,9 +33,10 @@ var Launcher = function (emitter, injector) {
3133
return null
3234
}
3335

34-
this.launch = function (names, protocol, hostname, port, urlRoot) {
35-
var browser
36+
this.launch = function (names, protocol, hostname, port, urlRoot, concurrency) {
3637
var url = protocol + '//' + hostname + ':' + port + urlRoot
38+
var batch = new Batch()
39+
batch.concurrency(concurrency)
3740

3841
lastStartTime = Date.now()
3942

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

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

87-
log.info('Starting browser %s', browser.name)
88-
browser.start(url)
90+
batch.push(function (done) {
91+
log.info('Starting browser %s', browser.name)
92+
93+
browser.start(url)
94+
browser.on('browser_process_failure', function () {
95+
done(browser.error)
96+
})
97+
98+
browser.on('done', function () {
99+
// We are not done if there was an error as first the retry takes
100+
// place which we catch with `browser_process_failure` if it fails
101+
if (browser.error) return
102+
103+
done(null, browser)
104+
})
105+
})
106+
89107
browsers.push(browser)
90108
})
91109

110+
batch.end(function (err) {
111+
log.debug('Finished all browsers')
112+
113+
if (err) {
114+
log.error(err)
115+
}
116+
})
117+
92118
return browsers
93119
}
94120

95-
this.launch.$inject = ['config.browsers', 'config.protocol', 'config.hostname', 'config.port', 'config.urlRoot']
121+
this.launch.$inject = [
122+
'config.browsers',
123+
'config.protocol',
124+
'config.hostname',
125+
'config.port',
126+
'config.urlRoot',
127+
'config.concurrency'
128+
]
96129

97130
this.kill = function (id, callback) {
98131
var browser = getBrowserById(id)

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@
232232
"Jerry Reptak <[email protected]>"
233233
],
234234
"dependencies": {
235+
"batch": "^0.5.3",
235236
"bluebird": "^2.9.27",
236237
"body-parser": "^1.12.4",
237238
"chokidar": "^1.0.1",

test/e2e/support/world.js

Whitespace-only changes.

test/unit/launcher.spec.js

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ describe('launcher', () => {
8585

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

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

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

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

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

107107
var browser = FakeBrowser._instances.pop()
108108
expect(browser.start).to.have.been.calledWith('http://whatever:1234/root/')
109109
})
110+
111+
it('should only launch the specified number of browsers at once', () => {
112+
l.launch([
113+
'Fake',
114+
'Fake',
115+
'Fake'
116+
], 'http:', 'whatever', 1234, '/root/', 2)
117+
118+
var b1 = FakeBrowser._instances.pop()
119+
var b2 = FakeBrowser._instances.pop()
120+
var b3 = FakeBrowser._instances.pop()
121+
122+
expect(b1.start).to.not.have.been.called
123+
expect(b2.start).to.have.been.calledOnce
124+
expect(b3.start).to.have.been.calledOnce
125+
126+
b1._done()
127+
b2._done()
128+
129+
expect(b1.start).to.have.been.calledOnce
130+
})
110131
})
111132

112133
describe('restart', () => {
113134
it('should restart the browser', () => {
114-
l.launch(['Fake'], 'http:', 'localhost', 1234, '/root/')
135+
l.launch(['Fake'], 'http:', 'localhost', 1234, '/root/', 1)
115136
var browser = FakeBrowser._instances.pop()
116137

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

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

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

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

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

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

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

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

157178
describe('killAll', () => {
158179
it('should kill all running processe', () => {
159-
l.launch(['Fake', 'Fake'], 'http:', 'localhost', 1234)
180+
l.launch(['Fake', 'Fake'], 'http:', 'localhost', 1234, '/', 1)
160181
l.killAll()
161182

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

172-
l.launch(['Fake', 'Fake'], 'http:', 'localhost', 1234)
193+
l.launch(['Fake', 'Fake'], 'http:', 'localhost', 1234, '/', 1)
173194
l.killAll(exitSpy)
174195

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

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

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

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

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

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

0 commit comments

Comments
 (0)