Skip to content

Commit 7a94d33

Browse files
authoredAug 17, 2020
feat(server): allow 'exit' listeners to set exit code (#3541)
Currently it seems that it's not possible for reporters to set the exit code asynchronously. Within the "onRunComplete" event, the results.exitCode must be modified synchronously, otherwise the updated value is not taken into account. With this change, the reporters (or any other plugin) can pass an exit code to the callback of the 'exit' event.
1 parent 8bc5b46 commit 7a94d33

File tree

2 files changed

+155
-20
lines changed

2 files changed

+155
-20
lines changed
 

‎lib/server.js

+51-20
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

+104
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)
Please sign in to comment.