Skip to content

Commit 9dba1e2

Browse files
authoredFeb 3, 2021
feat(config): improve karma.config.parseConfig error handling (#3635)
1 parent a14a24e commit 9dba1e2

File tree

3 files changed

+75
-10
lines changed

3 files changed

+75
-10
lines changed
 

‎lib/config.js

+23-8
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,26 @@ const CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' +
351351
' });\n' +
352352
' };\n'
353353

354-
function parseConfig (configFilePath, cliOptions) {
354+
function parseConfig (configFilePath, cliOptions, parseOptions) {
355+
function fail () {
356+
log.error(...arguments)
357+
if (parseOptions && parseOptions.throwErrors === true) {
358+
const errorMessage = Array.from(arguments).join(' ')
359+
throw new Error(errorMessage)
360+
} else {
361+
const warningMessage =
362+
'The `parseConfig()` function historically called `process.exit(1)`' +
363+
' when it failed. This behavior is now deprecated and function will' +
364+
' throw an error in the next major release. To suppress this warning' +
365+
' pass `throwErrors: true` as a third argument to opt-in into the new' +
366+
' behavior and adjust your code to respond to the exception' +
367+
' accordingly.' +
368+
' Example: `parseConfig(path, cliOptions, { throwErrors: true })`'
369+
log.warn(warningMessage)
370+
process.exit(1)
371+
}
372+
}
373+
355374
let configModule
356375
if (configFilePath) {
357376
try {
@@ -360,8 +379,6 @@ function parseConfig (configFilePath, cliOptions) {
360379
configModule = configModule.default
361380
}
362381
} catch (e) {
363-
log.error('Error in config file!\n ' + e.stack || e)
364-
365382
const extension = path.extname(configFilePath)
366383
if (extension === '.coffee' && !COFFEE_SCRIPT_AVAILABLE) {
367384
log.error('You need to install CoffeeScript.\n npm install coffeescript --save-dev')
@@ -370,11 +387,10 @@ function parseConfig (configFilePath, cliOptions) {
370387
} else if (extension === '.ts' && !TYPE_SCRIPT_AVAILABLE) {
371388
log.error('You need to install TypeScript.\n npm install typescript ts-node --save-dev')
372389
}
373-
return process.exit(1)
390+
return fail('Error in config file!\n ' + e.stack || e)
374391
}
375392
if (!helper.isFunction(configModule)) {
376-
log.error('Config file must export a function!\n' + CONFIG_SYNTAX_HELP)
377-
return process.exit(1)
393+
return fail('Config file must export a function!\n' + CONFIG_SYNTAX_HELP)
378394
}
379395
} else {
380396
configModule = () => {} // if no config file path is passed, we define a dummy config module.
@@ -395,8 +411,7 @@ function parseConfig (configFilePath, cliOptions) {
395411
try {
396412
configModule(config)
397413
} catch (e) {
398-
log.error('Error in config file!\n', e)
399-
return process.exit(1)
414+
return fail('Error in config file!\n', e)
400415
}
401416

402417
// merge the config from config file and cliOptions (precedence)

‎lib/server.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,15 @@ class Server extends KarmaEventEmitter {
6363

6464
this.loadErrors = []
6565

66-
const config = cfg.parseConfig(cliOptions.configFile, cliOptions)
66+
let config
67+
try {
68+
config = cfg.parseConfig(cliOptions.configFile, cliOptions, { throwErrors: true })
69+
} catch (parseConfigError) {
70+
// TODO: change how `done` falls back to exit in next major version
71+
// SEE: https://github.com/karma-runner/karma/pull/3635#discussion_r565399378
72+
(done || process.exit)(1)
73+
return
74+
}
6775

6876
this.log.debug('Final config', util.inspect(config, false, /** depth **/ null))
6977

‎test/unit/config.spec.js

+43-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ describe('config', () => {
4646
'/conf/invalid.js': () => {
4747
throw new SyntaxError('Unexpected token =')
4848
},
49+
'/conf/export-not-function.js': 'not-a-function',
50+
// '/conf/export-null.js': null, // Same as `/conf/not-exist.js`?
4951
'/conf/exclude.js': wrapCfg({ exclude: ['one.js', 'sub/two.js'] }),
5052
'/conf/absolute.js': wrapCfg({ files: ['http://some.com', 'https://more.org/file.js'] }),
5153
'/conf/both.js': wrapCfg({ files: ['one.js', 'two.js'], exclude: ['third.js'] }),
@@ -57,6 +59,7 @@ describe('config', () => {
5759
m = loadFile(path.join(__dirname, '/../../lib/config.js'), mocks, {
5860
global: {},
5961
process: mocks.process,
62+
Error: Error, // Without this, chai's `.throw()` assertion won't correctly check against constructors.
6063
require (path) {
6164
if (mockConfigs[path]) {
6265
return mockConfigs[path]
@@ -123,7 +126,20 @@ describe('config', () => {
123126
expect(mocks.process.exit).to.have.been.calledWith(1)
124127
})
125128

126-
it('should throw and log error if invalid file', () => {
129+
it('should log error and throw if file does not exist AND throwErrors is true', () => {
130+
function parseConfig () {
131+
e.parseConfig('/conf/not-exist.js', {}, { throwErrors: true })
132+
}
133+
134+
expect(parseConfig).to.throw(Error, 'Error in config file!\n Error: Cannot find module \'/conf/not-exist.js\'')
135+
expect(logSpy).to.have.been.called
136+
const event = logSpy.lastCall.args
137+
expect(event.toString().split('\n').slice(0, 2)).to.be.deep.equal(
138+
['Error in config file!', ' Error: Cannot find module \'/conf/not-exist.js\''])
139+
expect(mocks.process.exit).not.to.have.been.called
140+
})
141+
142+
it('should log an error and exit if invalid file', () => {
127143
e.parseConfig('/conf/invalid.js', {})
128144

129145
expect(logSpy).to.have.been.called
@@ -133,6 +149,32 @@ describe('config', () => {
133149
expect(mocks.process.exit).to.have.been.calledWith(1)
134150
})
135151

152+
it('should log an error and throw if invalid file AND throwErrors is true', () => {
153+
function parseConfig () {
154+
e.parseConfig('/conf/invalid.js', {}, { throwErrors: true })
155+
}
156+
157+
expect(parseConfig).to.throw(Error, 'Error in config file!\n SyntaxError: Unexpected token =')
158+
expect(logSpy).to.have.been.called
159+
const event = logSpy.lastCall.args
160+
expect(event[0]).to.eql('Error in config file!\n')
161+
expect(event[1].message).to.eql('Unexpected token =')
162+
expect(mocks.process.exit).not.to.have.been.called
163+
})
164+
165+
it('should log error and throw if file does not export a function AND throwErrors is true', () => {
166+
function parseConfig () {
167+
e.parseConfig('/conf/export-not-function.js', {}, { throwErrors: true })
168+
}
169+
170+
expect(parseConfig).to.throw(Error, 'Config file must export a function!\n')
171+
expect(logSpy).to.have.been.called
172+
const event = logSpy.lastCall.args
173+
expect(event.toString().split('\n').slice(0, 1)).to.be.deep.equal(
174+
['Config file must export a function!'])
175+
expect(mocks.process.exit).not.to.have.been.called
176+
})
177+
136178
it('should override config with given cli options', () => {
137179
const config = e.parseConfig('/home/config4.js', { port: 456, autoWatch: false })
138180

0 commit comments

Comments
 (0)
Please sign in to comment.