Skip to content

Commit

Permalink
feat: add browserDisconnectTolerance config option
Browse files Browse the repository at this point in the history
This adds a new config option `browserDisconnectTolerance`.
docs/config/01-configuration-file.md for more information.

When `browserDisconnectTolerance` is set to 1 and a browser disconnects during a test run, Karma will restart the browser and run the tests again. Only if the browser disconnects again, it will be treated as a failure.

Based on #777.
  • Loading branch information
vojtajina committed Dec 1, 2013
1 parent 763ae2a commit 19590e1
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 7 deletions.
12 changes: 12 additions & 0 deletions docs/config/01-configuration-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ paths defined in `files` and `exclude`. If the `basePath` configuration is a
relative path then it will be resolved to the `__dirname` of the configuration file.


## browserDisconnectTolerance
**Type:** Number

**Default:** `0`

**Description:** The number of disconnections tolerated.

The `disconnectTolerance` value represents the maximum number of tries a browser will attempt in case of disconnection.
Usually any disconnection is considered as a failure, but this option allows to define a tolerance level when there is
a flaky network link between the karma server and the browsers.


## browsers
**Type:** Array

Expand Down
12 changes: 12 additions & 0 deletions lib/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ var Browser = function(id, fullName, /* capturedBrowsers */ collection, emitter,
this.name = name;
this.state = READY;
this.lastResult = new Result();
this.disconnectsCount = 0;

this.init = function() {
collection.add(this);
Expand Down Expand Up @@ -90,6 +91,7 @@ var Browser = function(id, fullName, /* capturedBrowsers */ collection, emitter,
};

this.onStart = function(info) {
this.lastResult = new Result();
this.lastResult.total = info.total;

if (info.total === null) {
Expand Down Expand Up @@ -118,6 +120,7 @@ var Browser = function(id, fullName, /* capturedBrowsers */ collection, emitter,
var self = this;
var disconnect = function() {
self.state = DISCONNECTED;
self.disconnectsCount++;
log.warn('Disconnected');
collection.remove(self);
};
Expand Down Expand Up @@ -147,6 +150,15 @@ var Browser = function(id, fullName, /* capturedBrowsers */ collection, emitter,
log.debug('New connection %s, forgetting %s.', newSocket.id, socket.id);
// TODO(vojta): this should only remove this browser.onDisconnect listener
socket.removeAllListeners('disconnect');
} else if (this.state === DISCONNECTED) {
this.state = READY;
log.info('Connected on socket %s with id %s', newSocket.id, this.id);
collection.add(this);

// TODO(vojta): move to collection
emitter.emit('browsers_change', collection);

emitter.emit('browser_register', this);
}

socket = newSocket;
Expand Down
6 changes: 6 additions & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ var normalizeConfig = function(config, configFilePath) {
config.autoWatch = false;
}

if (!config.singleRun && config.browserDisconnectTolerance) {
log.debug('browserDisconnectTolerance set to 0, because of singleRun');
config.browserDisconnectTolerance = 0;
}

if (helper.isString(config.reporters)) {
config.reporters = config.reporters.split(',');
}
Expand Down Expand Up @@ -208,6 +213,7 @@ var Config = function() {
args: []
};
this.browserDisconnectTimeout = 2000;
this.browserDisconnectTolerance = 0;
};

var CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' +
Expand Down
17 changes: 16 additions & 1 deletion lib/launcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ var baseBrowserDecoratorFactory = require('./launchers/Base').decoratorFactory;

var Launcher = function(emitter, injector) {
var browsers = [];
var lastUrl;

this.launch = function(names, hostname, port, urlRoot) {
var url = 'http://' + hostname + ':' + port + urlRoot;
var browser;
var url = lastUrl = 'http://' + hostname + ':' + port + urlRoot;

names.forEach(function(name) {
var locals = {
Expand Down Expand Up @@ -60,6 +61,20 @@ var Launcher = function(emitter, injector) {
};


this.restart = function(id) {
for (var i = 0; i < browsers.length; i++) {
if (browsers[i].id === id) {
browsers[i].kill(function() {
browsers[i].start(lastUrl);
});
return true;
}
}

return false;
};


this.killAll = function(callback) {
log.debug('Disconnecting all browsers');

Expand Down
20 changes: 14 additions & 6 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,22 @@ var start = function(injector, config, launcher, globalEmitter, preprocess, file

if (config.singleRun) {
globalEmitter.on('browser_complete', function(completedBrowser) {
singleRunDoneBrowsers[completedBrowser.id] = true;
if (completedBrowser.lastResult.disconnected && completedBrowser.disconnectsCount <= config.browserDisconnectTolerance) {
log.info('Restarting %s (%d of %d attempts)', completedBrowser.name, completedBrowser.disconnectsCount, config.browserDisconnectTolerance);
if (!launcher.restart(completedBrowser.id)) {
singleRunDoneBrowsers[completedBrowser.id] = true;
emitRunCompleteIfAllBrowsersDone();
}
} else {
singleRunDoneBrowsers[completedBrowser.id] = true;

if (launcher.kill(completedBrowser.id)) {
// workaround to supress "disconnect" warning
completedBrowser.state = browser.Browser.STATE_DISCONNECTED;
}
if (launcher.kill(completedBrowser.id)) {
// workaround to supress "disconnect" warning
completedBrowser.state = browser.Browser.STATE_DISCONNECTED;
}

emitRunCompleteIfAllBrowsersDone();
emitRunCompleteIfAllBrowsersDone();
}
});

globalEmitter.on('browser_process_failure', function(browserLauncher) {
Expand Down
40 changes: 40 additions & 0 deletions test/unit/browser.spec.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,15 @@ describe 'browser', ->
expect(browser.state).to.equal b.Browser.STATE_EXECUTING


it 'should reconnect a disconnected browser', ->
browser = new b.Browser 'id', 'Chrome 25.0', collection, emitter, socket, null, 10
browser.state = b.Browser.STATE_DISCONNECTED

browser.onReconnect new e.EventEmitter

expect(browser.isReady()).to.equal true


#==========================================================================
# browser.Browser.onResult
#==========================================================================
Expand Down Expand Up @@ -427,6 +436,37 @@ describe 'browser', ->
expect(spy).to.have.been.calledWith browser


it 'restarting a disconnected browser', ->
timer = createMockTimer()
browser = new b.Browser 'fake-id', 'Chrome 31.0', collection, emitter, socket, timer, 10
browser.init()

browser.execute()
socket.emit 'start', {total: 10}
socket.emit 'result', {success: true, suite: [], log: []}
socket.emit 'result', {success: false, suite: [], log: []}
socket.emit 'result', {skipped: true, suite: [], log: []}
socket.emit 'disconnect'
timer.wind 10 # wait-for reconnecting delay
expect(browser.state).to.equal b.Browser.STATE_DISCONNECTED
expect(browser.disconnectsCount).to.equal 1

newSocket = new e.EventEmitter
emitter.on 'browser_register', -> browser.execute()

# reconnect on a new socket (which triggers re-execution)
browser.onReconnect newSocket
expect(browser.state).to.equal b.Browser.STATE_EXECUTING
newSocket.emit 'start', {total: 11}
socket.emit 'result', {success: true, suite: [], log: []}

# expected cleared last result (should not include the results from previous run)
expect(browser.lastResult.total).to.equal 11
expect(browser.lastResult.success).to.equal 1
expect(browser.lastResult.failed).to.equal 0
expect(browser.lastResult.skipped).to.equal 0


#============================================================================
# browser.Collection
#============================================================================
Expand Down
18 changes: 18 additions & 0 deletions test/unit/launcher.spec.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ describe 'launcher', ->
expect(browser.start).to.have.been.calledWith 'http://whatever:1234/root/'


describe 'restart', ->
it 'should kill and start the browser with the original url', ->
l.launch ['Fake'], 'localhost', 1234, '/root/'
browser = FakeBrowser._instances.pop()
browser.start.reset()

returnedValue = l.restart lastGeneratedId
expect(returnedValue).to.equal true
expect(browser.kill).to.have.been.called
browser.kill.callArg 0 # killing is done
expect(browser.start).to.have.been.calledWith 'http://localhost:1234/root/'


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


describe 'kill', ->
it 'should kill browser with given id', ->
killSpy = sinon.spy()
Expand Down

0 comments on commit 19590e1

Please sign in to comment.