Skip to content

Commit 80febfb

Browse files
nicojsjohnjbarton
authored andcommitted
fix(middleware/runner): handle file list rejections (#3400)
Add error handling for rejections on the file list methods in the `runner` middleware. As discussed in #3396 it does this by handling an error the same way an error in a run is handled. Fixes #3396
1 parent adc6a66 commit 80febfb

File tree

4 files changed

+231
-106
lines changed

4 files changed

+231
-106
lines changed

lib/executor.js

+33
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class Executor {
99
this.emitter = emitter
1010

1111
this.executionScheduled = false
12+
this.errorsScheduled = []
1213
this.pendingCount = 0
1314
this.runningBrowsers = null
1415

@@ -37,10 +38,42 @@ class Executor {
3738
}
3839
}
3940

41+
/**
42+
* Schedule an error to be reported
43+
* @param {string} errorMessage
44+
* @returns {boolean} a boolean indicating whether or not the error was handled synchronously
45+
*/
46+
scheduleError (errorMessage) {
47+
// We don't want to interfere with any running test.
48+
// Verify that no test is running before reporting the error.
49+
if (this.capturedBrowsers.areAllReady()) {
50+
log.warn(errorMessage)
51+
const errorResult = {
52+
success: 0,
53+
failed: 0,
54+
skipped: 0,
55+
error: errorMessage,
56+
exitCode: 1
57+
}
58+
const noBrowsersStartedTests = []
59+
this.emitter.emit('run_start', noBrowsersStartedTests) // A run cannot complete without being started
60+
this.emitter.emit('run_complete', noBrowsersStartedTests, errorResult)
61+
return true
62+
} else {
63+
this.errorsScheduled.push(errorMessage)
64+
return false
65+
}
66+
}
67+
4068
onRunComplete () {
4169
if (this.executionScheduled) {
4270
this.schedule()
4371
}
72+
if (this.errorsScheduled.length) {
73+
const errorsToReport = this.errorsScheduled
74+
this.errorsScheduled = []
75+
errorsToReport.forEach((error) => this.scheduleError(error))
76+
}
4477
}
4578

4679
onBrowserComplete () {

lib/middleware/runner.js

+37-29
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,18 @@ function createRunnerMiddleware (emitter, fileList, capturedBrowsers, reporter,
3535
}
3636

3737
const data = request.body
38-
emitter.once('run_start', function () {
39-
const responseWrite = response.write.bind(response)
40-
responseWrite.colors = data.colors
41-
reporter.addAdapter(responseWrite)
4238

43-
// clean up, close runner response
44-
emitter.once('run_complete', function (browsers, results) {
45-
reporter.removeAdapter(responseWrite)
46-
const emptyTestSuite = (results.failed + results.success) === 0 ? 0 : 1
47-
response.end(constant.EXIT_CODE + emptyTestSuite + results.exitCode)
48-
})
39+
updateClientArgs(data)
40+
handleRun(data)
41+
refreshFileList(data).then(() => {
42+
executor.schedule()
43+
}).catch((error) => {
44+
const errorMessage = `Error during refresh file list. ${error.stack || error}`
45+
executor.scheduleError(errorMessage)
4946
})
47+
})
5048

49+
function updateClientArgs (data) {
5150
helper.restoreOriginalArgs(config)
5251
if (_.isEmpty(data.args)) {
5352
log.debug('Ignoring empty client.args from run command')
@@ -59,43 +58,52 @@ function createRunnerMiddleware (emitter, fileList, capturedBrowsers, reporter,
5958
log.warn('Replacing client.args with ', data.args, ' as their types do not match.')
6059
config.client.args = data.args
6160
}
61+
}
6262

63+
async function refreshFileList (data) {
6364
let fullRefresh = true
6465

6566
if (helper.isArray(data.changedFiles)) {
66-
data.changedFiles.forEach(function (filepath) {
67-
fileList.changeFile(path.resolve(config.basePath, filepath))
67+
await Promise.all(data.changedFiles.map(async function (filepath) {
68+
await fileList.changeFile(path.resolve(config.basePath, filepath))
6869
fullRefresh = false
69-
})
70+
}))
7071
}
7172

7273
if (helper.isArray(data.addedFiles)) {
73-
data.addedFiles.forEach(function (filepath) {
74-
fileList.addFile(path.resolve(config.basePath, filepath))
74+
await Promise.all(data.addedFiles.map(async function (filepath) {
75+
await fileList.addFile(path.resolve(config.basePath, filepath))
7576
fullRefresh = false
76-
})
77+
}))
7778
}
7879

7980
if (helper.isArray(data.removedFiles)) {
80-
data.removedFiles.forEach(function (filepath) {
81-
fileList.removeFile(path.resolve(config.basePath, filepath))
81+
await Promise.all(data.removedFiles.map(async function (filepath) {
82+
await fileList.removeFile(path.resolve(config.basePath, filepath))
8283
fullRefresh = false
83-
})
84+
}))
8485
}
8586

8687
if (fullRefresh && data.refresh !== false) {
8788
log.debug('Refreshing all the files / patterns')
88-
fileList.refresh().then(function () {
89-
// Wait for the file list refresh to complete before starting test run,
90-
// otherwise the context.html generation might not see new/updated files.
91-
if (!config.autoWatch) {
92-
executor.schedule()
93-
}
94-
})
95-
} else {
96-
executor.schedule()
89+
await fileList.refresh()
9790
}
98-
})
91+
}
92+
93+
function handleRun (data) {
94+
emitter.once('run_start', function () {
95+
const responseWrite = response.write.bind(response)
96+
responseWrite.colors = data.colors
97+
reporter.addAdapter(responseWrite)
98+
99+
// clean up, close runner response
100+
emitter.once('run_complete', function (_browsers, results) {
101+
reporter.removeAdapter(responseWrite)
102+
const emptyTestSuite = (results.failed + results.success) === 0 ? 0 : 1
103+
response.end(constant.EXIT_CODE + emptyTestSuite + results.exitCode)
104+
})
105+
})
106+
}
99107
}
100108
}
101109

test/unit/executor.spec.js

+71-20
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const BrowserCollection = require('../../lib/browser_collection')
55
const EventEmitter = require('../../lib/events').EventEmitter
66
const Executor = require('../../lib/executor')
77

8+
const log = require('../../lib/logger').create()
9+
810
describe('executor', () => {
911
let emitter
1012
let capturedBrowsers
@@ -21,36 +23,85 @@ describe('executor', () => {
2123
executor.socketIoSockets = new EventEmitter()
2224

2325
spy = {
24-
onRunStart: () => null,
25-
onSocketsExecute: () => null
26+
onRunStart: sinon.stub(),
27+
onSocketsExecute: sinon.stub(),
28+
onRunComplete: sinon.stub()
2629
}
27-
28-
sinon.spy(spy, 'onRunStart')
29-
sinon.spy(spy, 'onSocketsExecute')
30+
sinon.stub(log, 'warn')
3031

3132
emitter.on('run_start', spy.onRunStart)
33+
emitter.on('run_complete', spy.onRunComplete)
3234
executor.socketIoSockets.on('execute', spy.onSocketsExecute)
3335
})
3436

35-
it('should start the run and pass client config', () => {
36-
capturedBrowsers.areAllReady = () => true
37+
describe('schedule', () => {
38+
it('should start the run and pass client config', () => {
39+
capturedBrowsers.areAllReady = () => true
40+
41+
executor.schedule()
42+
expect(spy.onRunStart).to.have.been.called
43+
expect(spy.onSocketsExecute).to.have.been.calledWith(config.client)
44+
})
45+
46+
it('should wait for all browsers to finish', () => {
47+
capturedBrowsers.areAllReady = () => false
3748

38-
executor.schedule()
39-
expect(spy.onRunStart).to.have.been.called
40-
expect(spy.onSocketsExecute).to.have.been.calledWith(config.client)
49+
// they are not ready yet
50+
executor.schedule()
51+
expect(spy.onRunStart).not.to.have.been.called
52+
expect(spy.onSocketsExecute).not.to.have.been.called
53+
54+
capturedBrowsers.areAllReady = () => true
55+
emitter.emit('run_complete')
56+
expect(spy.onRunStart).to.have.been.called
57+
expect(spy.onSocketsExecute).to.have.been.called
58+
})
4159
})
4260

43-
it('should wait for all browsers to finish', () => {
44-
capturedBrowsers.areAllReady = () => false
61+
describe('scheduleError', () => {
62+
it('should return `true` if scheduled synchronously', () => {
63+
const result = executor.scheduleError('expected error')
64+
expect(result).to.be.true
65+
})
66+
67+
it('should emit both "run_start" and "run_complete"', () => {
68+
executor.scheduleError('expected error')
69+
expect(spy.onRunStart).to.have.been.called
70+
expect(spy.onRunComplete).to.have.been.called
71+
expect(spy.onRunStart).to.have.been.calledBefore(spy.onRunComplete)
72+
})
73+
74+
it('should report the error', () => {
75+
const expectedError = 'expected error'
76+
executor.scheduleError(expectedError)
77+
expect(spy.onRunComplete).to.have.been.calledWith([], {
78+
success: 0,
79+
failed: 0,
80+
skipped: 0,
81+
error: expectedError,
82+
exitCode: 1
83+
})
84+
})
85+
86+
it('should wait for scheduled runs to end before reporting the error', () => {
87+
// Arrange
88+
let browsersAreReady = true
89+
const expectedError = 'expected error'
90+
capturedBrowsers.areAllReady = () => browsersAreReady
91+
executor.schedule()
92+
browsersAreReady = false
4593

46-
// they are not ready yet
47-
executor.schedule()
48-
expect(spy.onRunStart).not.to.have.been.called
49-
expect(spy.onSocketsExecute).not.to.have.been.called
94+
// Act
95+
const result = executor.scheduleError(expectedError)
96+
browsersAreReady = true
5097

51-
capturedBrowsers.areAllReady = () => true
52-
emitter.emit('run_complete')
53-
expect(spy.onRunStart).to.have.been.called
54-
expect(spy.onSocketsExecute).to.have.been.called
98+
// Assert
99+
expect(result).to.be.false
100+
expect(spy.onRunComplete).to.not.have.been.called
101+
emitter.emit('run_complete')
102+
expect(spy.onRunComplete).to.have.been.calledWith([], sinon.match({
103+
error: expectedError
104+
}))
105+
})
55106
})
56107
})

0 commit comments

Comments
 (0)