Skip to content

Commit e1234ad

Browse files
authored
Merge b7f7dea into 8bc5b46
2 parents 8bc5b46 + b7f7dea commit e1234ad

File tree

2 files changed

+155
-20
lines changed

2 files changed

+155
-20
lines changed

lib/server.js

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,35 @@ class Server extends KarmaEventEmitter {
142142
return this._fileList ? this._fileList.changeFile(path) : Promise.resolve()
143143
}
144144

145+
emitExitAsync (code) {
146+
const name = 'exit'
147+
let pending = this.listeners(name).length
148+
const deferred = helper.defer()
149+
150+
function resolve () {
151+
deferred.resolve(code)
152+
}
153+
154+
try {
155+
this.emit(name, (newCode) => {
156+
if (newCode && typeof newCode === 'number') {
157+
// Only update code if it is given and not zero
158+
code = newCode
159+
}
160+
if (!--pending) {
161+
resolve()
162+
}
163+
})
164+
165+
if (!pending) {
166+
resolve()
167+
}
168+
} catch (err) {
169+
deferred.reject(err)
170+
}
171+
return deferred.promise
172+
}
173+
145174
async _start (config, launcher, preprocess, fileList, capturedBrowsers, executor, done) {
146175
if (config.detached) {
147176
this._detach(config, done)
@@ -296,7 +325,8 @@ class Server extends KarmaEventEmitter {
296325

297326
this.on('stop', function (done) {
298327
this.log.debug('Received stop event, exiting.')
299-
return disconnectBrowsers().then(done)
328+
disconnectBrowsers()
329+
done()
300330
})
301331

302332
if (config.singleRun) {
@@ -354,28 +384,29 @@ class Server extends KarmaEventEmitter {
354384
}
355385
})
356386

357-
let removeAllListenersDone = false
358-
const removeAllListeners = () => {
359-
if (removeAllListenersDone) {
360-
return
387+
this.emitExitAsync(code).catch((err) => {
388+
this.log.error('Error while calling exit event listeners\n' + err.stack || err)
389+
return 1
390+
}).then((code) => {
391+
socketServer.sockets.removeAllListeners()
392+
socketServer.close()
393+
394+
let removeAllListenersDone = false
395+
const removeAllListeners = () => {
396+
if (removeAllListenersDone) {
397+
return
398+
}
399+
removeAllListenersDone = true
400+
webServer.removeAllListeners()
401+
processWrapper.removeAllListeners()
402+
done(code || 0)
361403
}
362-
removeAllListenersDone = true
363-
webServer.removeAllListeners()
364-
processWrapper.removeAllListeners()
365-
done(code || 0)
366-
}
367404

368-
return this.emitAsync('exit').then(() => {
369-
return new Promise((resolve, reject) => {
370-
socketServer.sockets.removeAllListeners()
371-
socketServer.close()
372-
const closeTimeout = setTimeout(removeAllListeners, webServerCloseTimeout)
405+
const closeTimeout = setTimeout(removeAllListeners, webServerCloseTimeout)
373406

374-
webServer.close(() => {
375-
clearTimeout(closeTimeout)
376-
removeAllListeners()
377-
resolve()
378-
})
407+
webServer.close(() => {
408+
clearTimeout(closeTimeout)
409+
removeAllListeners()
379410
})
380411
})
381412
}

test/unit/server.spec.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,110 @@ describe('server', () => {
306306
expect(await exitCode()).to.have.equal(15)
307307
})
308308

309+
it('given on run_complete with exit event listener (15)', async () => {
310+
mockProcess(process)
311+
312+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, (exitCode) => {
313+
resolveExitCode(exitCode)
314+
})
315+
316+
// last non-zero exit code will be taken
317+
server.on('exit', (done) => {
318+
setTimeout(() => done(30))
319+
})
320+
server.on('exit', (done) => {
321+
setTimeout(() => done(15))
322+
})
323+
server.on('exit', (done) => {
324+
setTimeout(() => done(0))
325+
})
326+
327+
// Provided run_complete exitCode will be overridden by exit listeners
328+
server.emit('run_complete', browserCollection, { exitCode: 5 })
329+
330+
function mockProcess (process) {
331+
sinon.stub(process, 'kill').callsFake((pid, ev) => process.emit(ev))
332+
}
333+
expect(await exitCode()).to.have.equal(15)
334+
})
335+
336+
it('given on run_complete with exit event listener (0)', async () => {
337+
mockProcess(process)
338+
339+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, (exitCode) => {
340+
resolveExitCode(exitCode)
341+
})
342+
343+
// exit listeners can't set exit code back to 0
344+
server.on('exit', (done) => {
345+
setTimeout(() => done(0))
346+
})
347+
348+
server.emit('run_complete', browserCollection, { exitCode: 15 })
349+
350+
function mockProcess (process) {
351+
sinon.stub(process, 'kill').callsFake((pid, ev) => process.emit(ev))
352+
}
353+
expect(await exitCode()).to.have.equal(15)
354+
})
355+
356+
it('1 on run_complete with exit event listener throws', async () => {
357+
mockProcess(process)
358+
359+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, (exitCode) => {
360+
resolveExitCode(exitCode)
361+
})
362+
363+
server.on('exit', (done) => {
364+
throw new Error('async error from exit event listener')
365+
})
366+
367+
server.emit('run_complete', browserCollection, { exitCode: 0 })
368+
369+
function mockProcess (process) {
370+
sinon.stub(process, 'kill').callsFake((pid, ev) => process.emit(ev))
371+
}
372+
expect(await exitCode()).to.have.equal(1)
373+
})
374+
375+
it('1 on run_complete with exit event listener rejects', async () => {
376+
mockProcess(process)
377+
378+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, (exitCode) => {
379+
resolveExitCode(exitCode)
380+
})
381+
382+
function onExit (done) {
383+
// Need to remove listener to prevent endless loop via unhandledRejection handler
384+
// which again calls disconnectBrowsers to fire the 'exit' event
385+
server.off('exit', onExit)
386+
return Promise.reject(new Error('async error from exit event listener'))
387+
}
388+
server.on('exit', onExit)
389+
390+
server.emit('run_complete', browserCollection, { exitCode: 0 })
391+
392+
function mockProcess (process) {
393+
sinon.stub(process, 'kill').callsFake((pid, ev) => process.emit(ev))
394+
}
395+
expect(await exitCode()).to.have.equal(1)
396+
})
397+
398+
it('0 on server stop', async () => {
399+
mockProcess(process)
400+
401+
await server._start(mockConfig, mockLauncher, null, mockFileList, browserCollection, mockExecutor, (exitCode) => {
402+
resolveExitCode(exitCode)
403+
})
404+
405+
server.stop()
406+
407+
function mockProcess (process) {
408+
sinon.stub(process, 'kill').callsFake((pid, ev) => process.emit(ev))
409+
}
410+
expect(await exitCode()).to.have.equal(0)
411+
})
412+
309413
it('1 on browser_process_failure (singleRunBrowserNotCaptured)', async () => {
310414
mockProcess(process)
311415

0 commit comments

Comments
 (0)