diff --git a/lib/cli.js b/lib/cli.js index 6e7fdac4f..d9854cfa5 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -7,6 +7,7 @@ const fs = require('graceful-fs') const Server = require('./server') const helper = require('./helper') const constant = require('./constants') +const cfg = require('./config') function processArgs (argv, options, fs, path) { Object.getOwnPropertyNames(argv).forEach(function (name) { @@ -278,26 +279,47 @@ exports.process = () => { } exports.run = () => { - const config = exports.process() - - switch (config.cmd) { - case 'start': - new Server(config).start() - break - case 'run': - require('./runner') - .run(config) - .on('progress', printRunnerProgress) - break - case 'stop': - require('./stopper').stop(config) - break - case 'init': - require('./init').init(config) - break - case 'completion': - require('./completion').completion(config) - break + const cliOptions = exports.process() + const cmd = cliOptions.cmd // prevent config from changing the command + const cmdNeedsConfig = cmd === 'start' || cmd === 'run' || cmd === 'stop' + if (cmdNeedsConfig) { + cfg.parseConfig( + cliOptions.configFile, + cliOptions, + { + promiseConfig: true, + throwErrors: true + } + ).then(function onKarmaConfigFulfilled (config) { + switch (cmd) { + case 'start': + new Server(config).start() + break + case 'run': + require('./runner') + .run(config) + .on('progress', printRunnerProgress) + break + case 'stop': + require('./stopper').stop(config) + break + } + }, function onKarmaConfigRejected (reason) { + console.error(reason) // TODO: configure a CLI Logger? + + // The `run` function is a private application, not a public API. We don't + // need to worry about process.exit vs throw vs promise rejection here. + process.exit(1) + }) + } else { + switch (cmd) { + case 'init': + require('./init').init(cliOptions) + break + case 'completion': + require('./completion').completion(cliOptions) + break + } } } diff --git a/lib/config.js b/lib/config.js index 1192afbac..b18c9d95b 100644 --- a/lib/config.js +++ b/lib/config.js @@ -352,11 +352,17 @@ const CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' + ' };\n' function parseConfig (configFilePath, cliOptions, parseOptions) { + const promiseConfig = parseOptions && parseOptions.promiseConfig === true + const throwErrors = parseOptions && parseOptions.throwErrors === true function fail () { log.error(...arguments) - if (parseOptions && parseOptions.throwErrors === true) { + if (throwErrors) { const errorMessage = Array.from(arguments).join(' ') - throw new Error(errorMessage) + const err = new Error(errorMessage) + if (promiseConfig) { + return Promise.reject(err) + } + throw err } else { const warningMessage = 'The `parseConfig()` function historically called `process.exit(1)`' + @@ -411,34 +417,59 @@ function parseConfig (configFilePath, cliOptions, parseOptions) { // add the user's configuration in config.set(cliOptions) + let configModuleReturn try { - configModule(config) + configModuleReturn = configModule(config) } catch (e) { return fail('Error in config file!\n', e) } + function finalizeConfig (config) { + // merge the config from config file and cliOptions (precedence) + config.set(cliOptions) - // merge the config from config file and cliOptions (precedence) - config.set(cliOptions) - - // if the user changed listenAddress, but didn't set a hostname, warn them - if (config.hostname === null && config.listenAddress !== null) { - log.warn(`ListenAddress was set to ${config.listenAddress} but hostname was left as the default: ` + + // if the user changed listenAddress, but didn't set a hostname, warn them + if (config.hostname === null && config.listenAddress !== null) { + log.warn(`ListenAddress was set to ${config.listenAddress} but hostname was left as the default: ` + `${defaultHostname}. If your browsers fail to connect, consider changing the hostname option.`) - } - // restore values that weren't overwritten by the user - if (config.hostname === null) { - config.hostname = defaultHostname - } - if (config.listenAddress === null) { - config.listenAddress = defaultListenAddress - } + } + // restore values that weren't overwritten by the user + if (config.hostname === null) { + config.hostname = defaultHostname + } + if (config.listenAddress === null) { + config.listenAddress = defaultListenAddress + } - // configure the logger as soon as we can - logger.setup(config.logLevel, config.colors, config.loggers) + // configure the logger as soon as we can + logger.setup(config.logLevel, config.colors, config.loggers) - log.debug(configFilePath ? `Loading config ${configFilePath}` : 'No config file specified.') + log.debug(configFilePath ? `Loading config ${configFilePath}` : 'No config file specified.') - return normalizeConfig(config, configFilePath) + return normalizeConfig(config, configFilePath) + } + const returnIsThenable = configModuleReturn != null && typeof configModuleReturn === 'object' && typeof configModuleReturn.then === 'function' + if (returnIsThenable) { + if (promiseConfig !== true) { + const errorMessage = + 'The `parseOptions.promiseConfig` option must be set to `true` to ' + + 'enable promise return values from configuration files. ' + + 'Example: `parseConfig(path, cliOptions, { promiseConfig: true })`' + return fail(errorMessage) + } + return configModuleReturn.then(function onKarmaConfigModuleFulfilled (/* ignoredResolutionValue */) { + return finalizeConfig(config) + }) + } else { + if (promiseConfig) { + try { + return Promise.resolve(finalizeConfig(config)) + } catch (exception) { + return Promise.reject(exception) + } + } else { + return finalizeConfig(config) + } + } } // PUBLIC API diff --git a/lib/runner.js b/lib/runner.js index e98a39da1..81ce9e1ff 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -32,14 +32,44 @@ function parseExitCode (buffer, defaultExitCode, failOnEmptyTestSuite) { } // TODO(vojta): read config file (port, host, urlRoot) -function run (config, done) { - config = config || {} - - logger.setupFromConfig(config) +function run (cliOptionsOrConfig, done) { + cliOptionsOrConfig = cliOptionsOrConfig || {} + + // TODO: Should `const log = logger.create('runner')` be moved into the + // : function for consistency with server.js and stopper.js? or the + // : reverse (make server and stopper consistent with runner?) + logger.setupFromConfig({ + colors: cliOptionsOrConfig.colors, + logLevel: cliOptionsOrConfig.logLevel + }) done = helper.isFunction(done) ? done : process.exit - config = cfg.parseConfig(config.configFile, config) + let config + if (cliOptionsOrConfig instanceof cfg.Config) { + config = cliOptionsOrConfig + } else { + const deprecatedCliOptionsMessage = + 'Passing raw CLI options to `runner(config, done)` is deprecated. Use ' + + '`parseConfig(configFilePath, cliOptions, {promiseConfig: true, throwErrors: true})` ' + + 'to prepare a processed `Config` instance and pass that as the ' + + '`config` argument instead.' + log.warn(deprecatedCliOptionsMessage) + try { + config = cfg.parseConfig( + cliOptionsOrConfig.configFile, + cliOptionsOrConfig, + { + promiseConfig: false, + throwErrors: true + } + ) + } catch (parseConfigError) { + // TODO: change how `done` falls back to exit in next major version + // SEE: https://github.com/karma-runner/karma/pull/3635#discussion_r565399378 + done(1) + } + } let exitCode = 1 const emitter = new EventEmitter() const options = { diff --git a/lib/server.js b/lib/server.js index a6ae81dab..07fabc32f 100644 --- a/lib/server.js +++ b/lib/server.js @@ -55,22 +55,43 @@ function createSocketIoServer (webServer, executor, config) { } class Server extends KarmaEventEmitter { - constructor (cliOptions, done) { + constructor (cliOptionsOrConfig, done) { super() - logger.setupFromConfig(cliOptions) - + cliOptionsOrConfig = cliOptionsOrConfig || {} + logger.setupFromConfig({ + colors: cliOptionsOrConfig.colors, + logLevel: cliOptionsOrConfig.logLevel + }) this.log = logger.create('karma-server') - + done = helper.isFunction(done) ? done : process.exit this.loadErrors = [] let config - try { - config = cfg.parseConfig(cliOptions.configFile, cliOptions, { throwErrors: true }) - } catch (parseConfigError) { - // TODO: change how `done` falls back to exit in next major version - // SEE: https://github.com/karma-runner/karma/pull/3635#discussion_r565399378 - (done || process.exit)(1) - return + if (cliOptionsOrConfig instanceof cfg.Config) { + config = cliOptionsOrConfig + } else { + const deprecatedCliOptionsMessage = + 'Passing raw CLI options to `new Server(config, done)` is ' + + 'deprecated. Use ' + + '`parseConfig(configFilePath, cliOptions, {promiseConfig: true, throwErrors: true})` ' + + 'to prepare a processed `Config` instance and pass that as the ' + + '`config` argument instead.' + this.log.warn(deprecatedCliOptionsMessage) + try { + config = cfg.parseConfig( + cliOptionsOrConfig.configFile, + cliOptionsOrConfig, + { + promiseConfig: false, + throwErrors: true + } + ) + } catch (parseConfigError) { + // TODO: change how `done` falls back to exit in next major version + // SEE: https://github.com/karma-runner/karma/pull/3635#discussion_r565399378 + done(1) + return + } } this.log.debug('Final config', util.inspect(config, false, /** depth **/ null)) diff --git a/lib/stopper.js b/lib/stopper.js index 5e87be595..b03fb046d 100644 --- a/lib/stopper.js +++ b/lib/stopper.js @@ -3,11 +3,40 @@ const cfg = require('./config') const logger = require('./logger') const helper = require('./helper') -exports.stop = function (config, done) { - config = config || {} - logger.setupFromConfig(config) +exports.stop = function (cliOptionsOrConfig, done) { + cliOptionsOrConfig = cliOptionsOrConfig || {} + logger.setupFromConfig({ + colors: cliOptionsOrConfig.colors, + logLevel: cliOptionsOrConfig.logLevel + }) const log = logger.create('stopper') done = helper.isFunction(done) ? done : process.exit + + let config + if (cliOptionsOrConfig instanceof cfg.Config) { + config = cliOptionsOrConfig + } else { + const deprecatedCliOptionsMessage = + 'Passing raw CLI options to `stopper(config, done)` is deprecated. Use ' + + '`parseConfig(configFilePath, cliOptions, {promiseConfig: true, throwErrors: true})` ' + + 'to prepare a processed `Config` instance and pass that as the ' + + '`config` argument instead.' + log.warn(deprecatedCliOptionsMessage) + try { + config = cfg.parseConfig( + cliOptionsOrConfig.configFile, + cliOptionsOrConfig, + { + promiseConfig: false, + throwErrors: true + } + ) + } catch (parseConfigError) { + // TODO: change how `done` falls back to exit in next major version + // SEE: https://github.com/karma-runner/karma/pull/3635#discussion_r565399378 + done(1) + } + } config = cfg.parseConfig(config.configFile, config) const request = http.request({