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..362dee054 100644 --- a/lib/launcher.js +++ b/lib/launcher.js @@ -1,5 +1,7 @@ -var log = require('./logger').create('launcher') var Promise = require('bluebird') +var cq = require('concurrent-queue') + +var log = require('./logger').create('launcher') var baseDecorator = require('./launchers/base').decoratorFactory var captureTimeoutDecorator = require('./launchers/capture_timeout').decoratorFactory @@ -31,9 +33,22 @@ 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 queue = cq().limit({concurrency: concurrency}).process(function (browser, done) { + log.info('Starting browser %s', browser.name) + + browser.start(url) + browser.on('browser_process_failure', function () { + done(browser.error) + }) + + browser.on('done', function () { + if (browser.error) return + + done(null, browser) + }) + }) lastStartTime = Date.now() @@ -54,7 +69,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 +99,26 @@ var Launcher = function (emitter, injector) { } } - log.info('Starting browser %s', browser.name) - browser.start(url) + queue(browser, function (err) { + if (err) { + log.error(err) + } + }) + browsers.push(browser) }) 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..759e21bac 100644 --- a/package.json +++ b/package.json @@ -236,6 +236,7 @@ "body-parser": "^1.12.4", "chokidar": "^1.0.1", "colors": "^1.1.0", + "concurrent-queue": "^7.0.1", "connect": "^3.3.5", "core-js": "^1.2.2", "di": "^0.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..619b5a0f3 100644 --- a/test/unit/launcher.spec.js +++ b/test/unit/launcher.spec.js @@ -3,6 +3,7 @@ import di from 'di' import events from '../../lib/events' import launcher from '../../lib/launcher' import createMockTimer from './mocks/timer' +import _ from 'lodash' // promise mock var stubPromise = (obj, method, stubAction) => { @@ -84,34 +85,70 @@ describe('launcher', () => { }) describe('launch', () => { - it('should inject and start all browsers', () => { - l.launch(['Fake'], 'http:', 'localhost', 1234, '/root/') + it('should inject and start all browsers', done => { + l.launch(['Fake'], 'http:', 'localhost', 1234, '/root/', 1) var browser = FakeBrowser._instances.pop() - expect(browser.start).to.have.been.calledWith('http://localhost:1234/root/') - expect(browser.id).to.equal(lastGeneratedId) - expect(browser.name).to.equal('Fake') + browser.start = url => { + expect(url).to.equal('http://localhost:1234/root/') + expect(browser.id).to.equal(lastGeneratedId) + expect(browser.name).to.equal('Fake') + done() + } }) - it('should allow launching a script', () => { - l.launch(['/usr/local/bin/special-browser'], 'http:', 'localhost', 1234, '/') + it('should allow launching a script', done => { + 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') + script.start = url => { + expect(url).to.equal('http://localhost:1234/') + expect(script.name).to.equal('/usr/local/bin/special-browser') + done() + } }) - it('should use the non default host', () => { - l.launch(['Fake'], 'http:', 'whatever', 1234, '/root/') + it('should use the non default host', done => { + l.launch(['Fake'], 'http:', 'whatever', 1234, '/root/', 1) var browser = FakeBrowser._instances.pop() - expect(browser.start).to.have.been.calledWith('http://whatever:1234/root/') + browser.start = url => { + expect(url).to.equal('http://whatever:1234/root/') + done() + } + }) + + it('should only launch the specified number of browsers at once', done => { + l.launch([ + 'Fake', + 'Fake', + 'Fake' + ], 'http:', 'whatever', 1234, '/root/', 2) + + var start = sinon.stub() + var b1 = FakeBrowser._instances.pop() + var b2 = FakeBrowser._instances.pop() + + b1.start = start + b2.start = start + FakeBrowser._instances.pop().start = start + + _.defer(() => { + expect(start).to.have.been.calledTwice + b1._done() + b2._done() + + _.defer(() => { + expect(start).to.have.been.calledThrice + done() + }) + }) }) }) 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 +157,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 +174,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 +183,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 +193,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 +206,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 @@ -198,23 +235,26 @@ describe('launcher', () => { }) }) - describe('areAllCaptured', () => { + describe('areAllCaptured', done => { 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) + _.defer(() => { + expect(l.areAllCaptured()).to.equal(false) - l.markCaptured(1) - expect(l.areAllCaptured()).to.equal(false) + l.markCaptured(1) + expect(l.areAllCaptured()).to.equal(false) - l.markCaptured(2) - expect(l.areAllCaptured()).to.equal(true) + l.markCaptured(2) + expect(l.areAllCaptured()).to.equal(true) + done() + }) }) }) 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)