Skip to content

Commit cbe2851

Browse files
committed
feat: allow browser to reconnect during the test run
When a browser disconnects during a test run, Karma waits for reconnecting (configurable by `browserDisconnectTimeout`). If the browser reconnects in this timeout frame, nothing happpens - Karma replies the events (results) and the test run continues. If the browser does not reconnect in the timeout frame, Karma fails the build. This should solve the connection issues with IE on polling. - add browserDisconnectTimeout config property (defaults to 2000) Internal changes: - `Browser.isReady` is a function now, as browser has multiple states - `BrowserCollection.setAllIsReadyTo` -> `setAllToExecuting` - remove `Browser.launchId`, we use `Browser.id` instead Closes #82 Closes #590
1 parent 09866f9 commit cbe2851

File tree

8 files changed

+424
-127
lines changed

8 files changed

+424
-127
lines changed

lib/browser.js

Lines changed: 101 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,57 @@ var Result = function() {
1616
};
1717

1818

19-
var Browser = function(id, collection, emitter) {
20-
var log = logger.create(id);
19+
// The browser is ready to execute tests.
20+
var READY = 1;
21+
22+
// The browser is executing the tests/
23+
var EXECUTING = 2;
24+
25+
// The browser is not executing, but temporarily disconnected (waiting for reconnecting).
26+
var READY_DISCONNECTED = 3;
27+
28+
// The browser is executing the tests, but temporarily disconnect (waiting for reconnecting).
29+
var EXECUTING_DISCONNECTED = 4;
30+
31+
// The browser got permanently disconnected (being removed from the collection and destroyed).
32+
var DISCONNECTED = 5;
33+
34+
35+
var Browser = function(id, fullName, /* capturedBrowsers */ collection, emitter, socket, timer,
36+
/* config.browserDisconnectTimeout */ disconnectDelay) {
37+
38+
var name = helper.browserFullNameToShort(fullName);
39+
var log = logger.create(name);
2140

2241
this.id = id;
23-
this.name = id;
24-
this.fullName = null;
25-
this.isReady = true;
42+
this.fullName = fullName;
43+
this.name = name;
44+
this.state = READY;
2645
this.lastResult = new Result();
2746

47+
this.init = function() {
48+
collection.add(this);
2849

29-
this.toString = function() {
30-
return this.name;
31-
};
50+
events.bindAll(this, socket);
3251

33-
this.onRegister = function(info) {
34-
this.launchId = info.id;
35-
this.fullName = info.name;
36-
this.name = helper.browserFullNameToShort(this.fullName);
37-
log = logger.create(this.name);
38-
log.info('Connected on socket id ' + this.id);
52+
log.info('Connected on socket %s', socket.id);
3953

40-
emitter.emit('browser_register', this);
54+
// TODO(vojta): move to collection
4155
emitter.emit('browsers_change', collection);
56+
57+
emitter.emit('browser_register', this);
58+
};
59+
60+
this.isReady = function() {
61+
return this.state === READY;
62+
};
63+
64+
this.toString = function() {
65+
return this.name;
4266
};
4367

4468
this.onError = function(error) {
45-
if (this.isReady) {
69+
if (this.isReady()) {
4670
return;
4771
}
4872

@@ -51,7 +75,7 @@ var Browser = function(id, collection, emitter) {
5175
};
5276

5377
this.onInfo = function(info) {
54-
if (this.isReady) {
78+
if (this.isReady()) {
5579
return;
5680
}
5781

@@ -70,11 +94,11 @@ var Browser = function(id, collection, emitter) {
7094
};
7195

7296
this.onComplete = function(result) {
73-
if (this.isReady) {
97+
if (this.isReady()) {
7498
return;
7599
}
76100

77-
this.isReady = true;
101+
this.state = READY;
78102
this.lastResult.totalTimeEnd();
79103

80104
if (!this.lastResult.success) {
@@ -85,21 +109,50 @@ var Browser = function(id, collection, emitter) {
85109
emitter.emit('browser_complete', this, result);
86110
};
87111

112+
var self = this;
113+
var disconnect = function() {
114+
self.state = DISCONNECTED;
115+
log.warn('Disconnected');
116+
collection.remove(self);
117+
};
118+
119+
var pendingDisconnect;
88120
this.onDisconnect = function() {
89-
if (!this.isReady) {
90-
this.isReady = true;
91-
this.lastResult.totalTimeEnd();
92-
this.lastResult.disconnected = true;
93-
emitter.emit('browser_complete', this);
121+
if (this.state === READY) {
122+
disconnect();
123+
} else if (this.state === EXECUTING) {
124+
log.debug('Disconnected during run, waiting for reconnecting.');
125+
this.state = EXECUTING_DISCONNECTED;
126+
127+
pendingDisconnect = timer.setTimeout(function() {
128+
self.lastResult.totalTimeEnd();
129+
self.lastResult.disconnected = true;
130+
disconnect();
131+
emitter.emit('browser_complete', self);
132+
}, disconnectDelay);
94133
}
134+
};
95135

96-
log.warn('Disconnected');
97-
collection.remove(this);
136+
this.onReconnect = function(newSocket) {
137+
if (this.state === EXECUTING_DISCONNECTED) {
138+
this.state = EXECUTING;
139+
log.debug('Reconnected.');
140+
} else if (this.state === EXECUTING || this.state === READY) {
141+
log.debug('New connection, forgetting the old one.');
142+
// TODO(vojta): this should only remove this browser.onDisconnect listener
143+
socket.removeAllListeners('disconnect');
144+
}
145+
146+
socket = newSocket;
147+
events.bindAll(this, newSocket);
148+
if (pendingDisconnect) {
149+
timer.clearTimeout(pendingDisconnect);
150+
}
98151
};
99152

100153
this.onResult = function(result) {
101154
// ignore - probably results from last run (after server disconnecting)
102-
if (this.isReady) {
155+
if (this.isReady()) {
103156
return;
104157
}
105158

@@ -119,11 +172,17 @@ var Browser = function(id, collection, emitter) {
119172
return {
120173
id: this.id,
121174
name: this.name,
122-
isReady: this.isReady
175+
isReady: this.state === READY
123176
};
124177
};
125178
};
126179

180+
Browser.STATE_READY = READY;
181+
Browser.STATE_EXECUTING = EXECUTING;
182+
Browser.STATE_READY_DISCONNECTED = READY_DISCONNECTED;
183+
Browser.STATE_EXECUTING_DISCONNECTED = EXECUTING_DISCONNECTED;
184+
Browser.STATE_DISCONNECTED = DISCONNECTED;
185+
127186

128187
var Collection = function(emitter, browsers) {
129188
browsers = browsers || [];
@@ -146,23 +205,29 @@ var Collection = function(emitter, browsers) {
146205
return true;
147206
};
148207

149-
this.setAllIsReadyTo = function(value) {
150-
var change = false;
208+
this.getById = function(browserId) {
209+
for (var i = 0; i < browsers.length; i++) {
210+
if (browsers[i].id === browserId) {
211+
return browsers[i];
212+
}
213+
}
214+
215+
return null;
216+
};
217+
218+
this.setAllToExecuting = function() {
151219
browsers.forEach(function(browser) {
152-
change = change || browser.isReady !== value;
153-
browser.isReady = value;
220+
browser.state = EXECUTING;
154221
});
155222

156-
if (change) {
157-
emitter.emit('browsers_change', this);
158-
}
223+
emitter.emit('browsers_change', this);
159224
};
160225

161226
this.areAllReady = function(nonReadyList) {
162227
nonReadyList = nonReadyList || [];
163228

164229
browsers.forEach(function(browser) {
165-
if (!browser.isReady) {
230+
if (!browser.isReady()) {
166231
nonReadyList.push(browser);
167232
}
168233
});
@@ -222,12 +287,3 @@ Collection.$inject = ['emitter'];
222287
exports.Result = Result;
223288
exports.Browser = Browser;
224289
exports.Collection = Collection;
225-
226-
exports.createBrowser = function(socket, collection, emitter) {
227-
var browser = new Browser(socket.id, collection, emitter);
228-
229-
events.bindAll(browser, socket);
230-
collection.add(browser);
231-
232-
return browser;
233-
};

lib/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ var Config = function() {
271271
this.client = {
272272
args: []
273273
};
274+
this.browserDisconnectTimeout = 2000;
274275

275276
// TODO(vojta): remove in 0.10
276277
this.junitReporter = {

lib/events.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,41 @@ var bindAllEvents = function(object, context) {
1515
}
1616
};
1717

18+
19+
var bufferEvents = function(emitter, eventsToBuffer) {
20+
var listeners = [];
21+
var eventsToReply = [];
22+
var genericListener = function() {
23+
eventsToReply.push(Array.prototype.slice.call(arguments));
24+
};
25+
26+
eventsToBuffer.forEach(function(eventName) {
27+
var listener = genericListener.bind(null, eventName);
28+
listeners.push(listener);
29+
emitter.on(eventName, listener);
30+
});
31+
32+
return function() {
33+
if (!eventsToReply) {
34+
return;
35+
}
36+
37+
// remove all buffering listeners
38+
listeners.forEach(function(listener, i) {
39+
emitter.removeListener(eventsToBuffer[i], listener);
40+
});
41+
42+
// reply
43+
eventsToReply.forEach(function(args) {
44+
events.EventEmitter.prototype.emit.apply(emitter, args);
45+
});
46+
47+
// free-up
48+
listeners = eventsToReply = null;
49+
};
50+
};
51+
52+
1853
// TODO(vojta): log.debug all events
1954
var EventEmitter = function() {
2055
this.bind = bindAllEvents;
@@ -38,6 +73,8 @@ var EventEmitter = function() {
3873

3974
util.inherits(EventEmitter, events.EventEmitter);
4075

76+
4177
// PUBLISH
4278
exports.EventEmitter = EventEmitter;
4379
exports.bindAll = bindAllEvents;
80+
exports.bufferEvents = bufferEvents;

lib/executor.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ var Executor = function(capturedBrowsers, config, emitter) {
1818
if (capturedBrowsers.areAllReady(nonReady)) {
1919
log.debug('All browsers are ready, executing');
2020
executionScheduled = false;
21-
capturedBrowsers.setAllIsReadyTo(false);
2221
capturedBrowsers.clearResults();
22+
capturedBrowsers.setAllToExecuting();
2323
pendingCount = capturedBrowsers.length;
2424
runningBrowsers = capturedBrowsers.clone();
2525
emitter.emit('run_start', runningBrowsers);

lib/server.js

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ var Launcher = require('./launcher').Launcher;
1414
var FileList = require('./file-list').List;
1515
var reporter = require('./reporter');
1616
var helper = require('./helper');
17-
var EventEmitter = require('./events').EventEmitter;
17+
var events = require('./events');
18+
var EventEmitter = events.EventEmitter;
1819
var Executor = require('./executor');
1920

2021
var log = logger.create();
@@ -60,8 +61,8 @@ var start = function(injector, config, launcher, globalEmitter, preprocess, file
6061
});
6162

6263
globalEmitter.on('browser_register', function(browser) {
63-
if (browser.launchId) {
64-
launcher.markCaptured(browser.launchId);
64+
if (browser.id) {
65+
launcher.markCaptured(browser.id);
6566
}
6667

6768
// TODO(vojta): This is lame, browser can get captured and then crash (before other browsers get
@@ -78,8 +79,31 @@ var start = function(injector, config, launcher, globalEmitter, preprocess, file
7879
});
7980

8081
socketServer.sockets.on('connection', function (socket) {
81-
log.debug('New browser has connected on socket ' + socket.id);
82-
browser.createBrowser(socket, capturedBrowsers, globalEmitter);
82+
log.debug('A browser has connected on socket ' + socket.id);
83+
84+
var replySocketEvents = events.bufferEvents(socket, ['info', 'error', 'result', 'complete']);
85+
86+
socket.on('register', function(info) {
87+
var newBrowser;
88+
89+
if (info.id) {
90+
newBrowser = capturedBrowsers.getById(info.id);
91+
}
92+
93+
if (newBrowser) {
94+
newBrowser.onReconnect(socket);
95+
} else {
96+
newBrowser = injector.createChild([{
97+
id: ['value', info.id || null],
98+
fullName: ['value', info.name],
99+
socket: ['value', socket]
100+
}]).instantiate(browser.Browser);
101+
102+
newBrowser.init();
103+
}
104+
105+
replySocketEvents();
106+
});
83107
});
84108

85109
if (config.autoWatch) {
@@ -167,7 +191,8 @@ exports.start = function(cliOptions, done) {
167191
customScriptTypes: ['value', []],
168192
reporter: ['factory', reporter.createReporters],
169193
capturedBrowsers: ['type', browser.Collection],
170-
args: ['value', {}]
194+
args: ['value', {}],
195+
timer: ['value', {setTimeout: setTimeout, clearTimeout: clearTimeout}]
171196
}];
172197

173198
// load the plugins

static/karma.src.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,9 +250,6 @@ var Karma = function(socket, context, navigator, location) {
250250
}
251251
});
252252

253-
// cancel execution
254-
socket.on('disconnect', clearContext);
255-
256253
// report browser name, id
257254
socket.on('connect', function() {
258255
socket.emit('register', {

0 commit comments

Comments
 (0)