123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543 |
- 'use strict'
- const path = require('path')
- const assert = require('assert')
- const logger = require('./logger')
- const log = logger.create('config')
- const helper = require('./helper')
- const constant = require('./constants')
- const _ = require('lodash')
- let COFFEE_SCRIPT_AVAILABLE = false
- let LIVE_SCRIPT_AVAILABLE = false
- let TYPE_SCRIPT_AVAILABLE = false
- try {
- require('coffeescript').register()
- COFFEE_SCRIPT_AVAILABLE = true
- } catch {}
- // LiveScript is required here to enable config files written in LiveScript.
- // It's not directly used in this file.
- try {
- require('LiveScript')
- LIVE_SCRIPT_AVAILABLE = true
- } catch {}
- try {
- require('ts-node')
- TYPE_SCRIPT_AVAILABLE = true
- } catch {}
- class Pattern {
- constructor (pattern, served, included, watched, nocache, type, isBinary) {
- this.pattern = pattern
- this.served = helper.isDefined(served) ? served : true
- this.included = helper.isDefined(included) ? included : true
- this.watched = helper.isDefined(watched) ? watched : true
- this.nocache = helper.isDefined(nocache) ? nocache : false
- this.weight = helper.mmPatternWeight(pattern)
- this.type = type
- this.isBinary = isBinary
- }
- compare (other) {
- return helper.mmComparePatternWeights(this.weight, other.weight)
- }
- }
- class UrlPattern extends Pattern {
- constructor (url, type) {
- super(url, false, true, false, false, type)
- }
- }
- function createPatternObject (pattern) {
- if (pattern && helper.isString(pattern)) {
- return helper.isUrlAbsolute(pattern)
- ? new UrlPattern(pattern)
- : new Pattern(pattern)
- } else if (helper.isObject(pattern) && pattern.pattern && helper.isString(pattern.pattern)) {
- return helper.isUrlAbsolute(pattern.pattern)
- ? new UrlPattern(pattern.pattern, pattern.type)
- : new Pattern(pattern.pattern, pattern.served, pattern.included, pattern.watched, pattern.nocache, pattern.type)
- } else {
- log.warn(`Invalid pattern ${pattern}!\n\tExpected string or object with "pattern" property.`)
- return new Pattern(null, false, false, false, false)
- }
- }
- function normalizeUrl (url) {
- if (!url.startsWith('/')) {
- url = `/${url}`
- }
- if (!url.endsWith('/')) {
- url = url + '/'
- }
- return url
- }
- function normalizeUrlRoot (urlRoot) {
- const normalizedUrlRoot = normalizeUrl(urlRoot)
- if (normalizedUrlRoot !== urlRoot) {
- log.warn(`urlRoot normalized to "${normalizedUrlRoot}"`)
- }
- return normalizedUrlRoot
- }
- function normalizeProxyPath (proxyPath) {
- const normalizedProxyPath = normalizeUrl(proxyPath)
- if (normalizedProxyPath !== proxyPath) {
- log.warn(`proxyPath normalized to "${normalizedProxyPath}"`)
- }
- return normalizedProxyPath
- }
- function normalizeConfig (config, configFilePath) {
- function basePathResolve (relativePath) {
- if (helper.isUrlAbsolute(relativePath)) {
- return relativePath
- } else if (helper.isDefined(config.basePath) && helper.isDefined(relativePath)) {
- return path.resolve(config.basePath, relativePath)
- } else {
- return ''
- }
- }
- function createPatternMapper (resolve) {
- return (objectPattern) => Object.assign(objectPattern, { pattern: resolve(objectPattern.pattern) })
- }
- if (helper.isString(configFilePath)) {
- config.basePath = path.resolve(path.dirname(configFilePath), config.basePath) // resolve basePath
- config.exclude.push(configFilePath) // always ignore the config file itself
- } else {
- config.basePath = path.resolve(config.basePath || '.')
- }
- config.files = config.files.map(createPatternObject).map(createPatternMapper(basePathResolve))
- config.exclude = config.exclude.map(basePathResolve)
- config.customContextFile = config.customContextFile && basePathResolve(config.customContextFile)
- config.customDebugFile = config.customDebugFile && basePathResolve(config.customDebugFile)
- config.customClientContextFile = config.customClientContextFile && basePathResolve(config.customClientContextFile)
- // normalize paths on windows
- config.basePath = helper.normalizeWinPath(config.basePath)
- config.files = config.files.map(createPatternMapper(helper.normalizeWinPath))
- config.exclude = config.exclude.map(helper.normalizeWinPath)
- config.customContextFile = helper.normalizeWinPath(config.customContextFile)
- config.customDebugFile = helper.normalizeWinPath(config.customDebugFile)
- config.customClientContextFile = helper.normalizeWinPath(config.customClientContextFile)
- // normalize urlRoot
- config.urlRoot = normalizeUrlRoot(config.urlRoot)
- // normalize and default upstream proxy settings if given
- if (config.upstreamProxy) {
- const proxy = config.upstreamProxy
- proxy.path = helper.isDefined(proxy.path) ? normalizeProxyPath(proxy.path) : '/'
- proxy.hostname = helper.isDefined(proxy.hostname) ? proxy.hostname : 'localhost'
- proxy.port = helper.isDefined(proxy.port) ? proxy.port : 9875
- // force protocol to end with ':'
- proxy.protocol = (proxy.protocol || 'http').split(':')[0] + ':'
- if (proxy.protocol.match(/https?:/) === null) {
- log.warn(`"${proxy.protocol}" is not a supported upstream proxy protocol, defaulting to "http:"`)
- proxy.protocol = 'http:'
- }
- }
- // force protocol to end with ':'
- config.protocol = (config.protocol || 'http').split(':')[0] + ':'
- if (config.protocol.match(/https?:/) === null) {
- log.warn(`"${config.protocol}" is not a supported protocol, defaulting to "http:"`)
- config.protocol = 'http:'
- }
- if (config.proxies && Object.prototype.hasOwnProperty.call(config.proxies, config.urlRoot)) {
- log.warn(`"${config.urlRoot}" is proxied, you should probably change urlRoot to avoid conflicts`)
- }
- if (config.singleRun && config.autoWatch) {
- log.debug('autoWatch set to false, because of singleRun')
- config.autoWatch = false
- }
- if (config.runInParent) {
- log.debug('useIframe set to false, because using runInParent')
- config.useIframe = false
- }
- if (!config.singleRun && !config.useIframe && config.runInParent) {
- log.debug('singleRun set to true, because using runInParent')
- config.singleRun = true
- }
- if (helper.isString(config.reporters)) {
- config.reporters = config.reporters.split(',')
- }
- if (config.client && config.client.args) {
- assert(Array.isArray(config.client.args), 'Invalid configuration: client.args must be an array of strings')
- }
- if (config.browsers) {
- assert(Array.isArray(config.browsers), 'Invalid configuration: browsers option must be an array')
- }
- if (config.formatError) {
- assert(helper.isFunction(config.formatError), 'Invalid configuration: formatError option must be a function.')
- }
- if (config.processKillTimeout) {
- assert(helper.isNumber(config.processKillTimeout), 'Invalid configuration: processKillTimeout option must be a number.')
- }
- if (config.browserSocketTimeout) {
- assert(helper.isNumber(config.browserSocketTimeout), 'Invalid configuration: browserSocketTimeout option must be a number.')
- }
- if (config.pingTimeout) {
- assert(helper.isNumber(config.pingTimeout), 'Invalid configuration: pingTimeout option must be a number.')
- }
- const defaultClient = config.defaultClient || {}
- Object.keys(defaultClient).forEach(function (key) {
- const option = config.client[key]
- config.client[key] = helper.isDefined(option) ? option : defaultClient[key]
- })
- // normalize preprocessors
- const preprocessors = config.preprocessors || {}
- const normalizedPreprocessors = config.preprocessors = Object.create(null)
- Object.keys(preprocessors).forEach(function (pattern) {
- const normalizedPattern = helper.normalizeWinPath(basePathResolve(pattern))
- normalizedPreprocessors[normalizedPattern] = helper.isString(preprocessors[pattern])
- ? [preprocessors[pattern]] : preprocessors[pattern]
- })
- // define custom launchers/preprocessors/reporters - create a new plugin
- const module = Object.create(null)
- let hasSomeInlinedPlugin = false
- const types = ['launcher', 'preprocessor', 'reporter']
- types.forEach(function (type) {
- const definitions = config[`custom${helper.ucFirst(type)}s`] || {}
- Object.keys(definitions).forEach(function (name) {
- const definition = definitions[name]
- if (!helper.isObject(definition)) {
- return log.warn(`Can not define ${type} ${name}. Definition has to be an object.`)
- }
- if (!helper.isString(definition.base)) {
- return log.warn(`Can not define ${type} ${name}. Missing base ${type}.`)
- }
- const token = type + ':' + definition.base
- const locals = {
- args: ['value', definition]
- }
- module[type + ':' + name] = ['factory', function (injector) {
- const plugin = injector.createChild([locals], [token]).get(token)
- if (type === 'launcher' && helper.isDefined(definition.displayName)) {
- plugin.displayName = definition.displayName
- }
- return plugin
- }]
- hasSomeInlinedPlugin = true
- })
- })
- if (hasSomeInlinedPlugin) {
- config.plugins.push(module)
- }
- return config
- }
- /**
- * @class
- */
- class Config {
- constructor () {
- this.LOG_DISABLE = constant.LOG_DISABLE
- this.LOG_ERROR = constant.LOG_ERROR
- this.LOG_WARN = constant.LOG_WARN
- this.LOG_INFO = constant.LOG_INFO
- this.LOG_DEBUG = constant.LOG_DEBUG
- // DEFAULT CONFIG
- this.frameworks = []
- this.protocol = 'http:'
- this.port = constant.DEFAULT_PORT
- this.listenAddress = constant.DEFAULT_LISTEN_ADDR
- this.hostname = constant.DEFAULT_HOSTNAME
- this.httpsServerConfig = {}
- this.basePath = ''
- this.files = []
- this.browserConsoleLogOptions = {
- level: 'debug',
- format: '%b %T: %m',
- terminal: true
- }
- this.customContextFile = null
- this.customDebugFile = null
- this.customClientContextFile = null
- this.exclude = []
- this.logLevel = constant.LOG_INFO
- this.colors = true
- this.autoWatch = true
- this.autoWatchBatchDelay = 250
- this.restartOnFileChange = false
- this.usePolling = process.platform === 'linux'
- this.reporters = ['progress']
- this.singleRun = false
- this.browsers = []
- this.captureTimeout = 60000
- this.pingTimeout = 5000
- this.proxies = {}
- this.proxyValidateSSL = true
- this.preprocessors = {}
- this.preprocessor_priority = {}
- this.urlRoot = '/'
- this.upstreamProxy = undefined
- this.reportSlowerThan = 0
- this.loggers = [constant.CONSOLE_APPENDER]
- this.transports = ['polling', 'websocket']
- this.forceJSONP = false
- this.plugins = ['karma-*']
- this.defaultClient = this.client = {
- args: [],
- useIframe: true,
- runInParent: false,
- captureConsole: true,
- clearContext: true,
- allowedReturnUrlPatterns: ['^https?://']
- }
- this.browserDisconnectTimeout = 2000
- this.browserDisconnectTolerance = 0
- this.browserNoActivityTimeout = 30000
- this.processKillTimeout = 2000
- this.concurrency = Infinity
- this.failOnEmptyTestSuite = true
- this.retryLimit = 2
- this.detached = false
- this.crossOriginAttribute = true
- this.browserSocketTimeout = 20000
- }
- set (newConfig) {
- _.mergeWith(this, newConfig, (obj, src) => {
- // Overwrite arrays to keep consistent with #283
- if (Array.isArray(src)) {
- return src
- }
- })
- }
- }
- const CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' +
- ' config.set({\n' +
- ' // your config\n' +
- ' });\n' +
- ' };\n'
- /**
- * Retrieve a parsed and finalized Karma `Config` instance. This `karmaConfig`
- * object may be used to configure public API methods such a `Server`,
- * `runner.run`, and `stopper.stop`.
- *
- * @param {?string} [configFilePath=null]
- * A string representing a file system path pointing to the config file
- * whose default export is a function that will be used to set Karma
- * configuration options. This function will be passed an instance of the
- * `Config` class as its first argument. If this option is not provided,
- * then only the options provided by the `cliOptions` argument will be
- * set.
- * @param {Object} cliOptions
- * An object whose values will take priority over options set in the
- * config file. The config object passed to function exported by the
- * config file will already have these options applied. Any changes the
- * config file makes to these options will effectively be ignored in the
- * final configuration.
- *
- * `cliOptions` all the same options as the config file and is applied
- * using the same `config.set()` method.
- * @param {Object} parseOptions
- * @param {boolean} [parseOptions.promiseConfig=false]
- * When `true`, a promise that resolves to a `Config` object will be
- * returned. This also allows the function exported by config files (if
- * provided) to be asynchronous by returning a promise. Resolving this
- * promise indicates that all async activity has completed. The resolution
- * value itself is ignored, all configuration must be done with
- * `config.set`.
- * @param {boolean} [parseOptions.throwErrors=false]
- * When `true`, process exiting on critical failures will be disabled. In
- * The error will be thrown as an exception. If
- * `parseOptions.promiseConfig` is also `true`, then the error will
- * instead be used as the promise's reject reason.
- * @returns {Config|Promise<Config>}
- */
- function parseConfig (configFilePath, cliOptions, parseOptions) {
- const promiseConfig = parseOptions && parseOptions.promiseConfig === true
- const throwErrors = parseOptions && parseOptions.throwErrors === true
- const shouldSetupLoggerEarly = promiseConfig
- if (shouldSetupLoggerEarly) {
- // `setupFromConfig` provides defaults for `colors` and `logLevel`.
- // `setup` provides defaults for `appenders`
- // The first argument MUST BE an object
- logger.setupFromConfig({})
- }
- function fail () {
- log.error(...arguments)
- if (throwErrors) {
- const errorMessage = Array.from(arguments).join(' ')
- const err = new Error(errorMessage)
- if (promiseConfig) {
- return Promise.reject(err)
- }
- throw err
- } else {
- const warningMessage =
- 'The `parseConfig()` function historically called `process.exit(1)`' +
- ' when it failed. This behavior is now deprecated and function will' +
- ' throw an error in the next major release. To suppress this warning' +
- ' pass `throwErrors: true` as a third argument to opt-in into the new' +
- ' behavior and adjust your code to respond to the exception' +
- ' accordingly.' +
- ' Example: `parseConfig(path, cliOptions, { throwErrors: true })`'
- log.warn(warningMessage)
- process.exit(1)
- }
- }
- let configModule
- if (configFilePath) {
- try {
- if (path.extname(configFilePath) === '.ts' && TYPE_SCRIPT_AVAILABLE) {
- require('ts-node').register()
- }
- configModule = require(configFilePath)
- if (typeof configModule === 'object' && typeof configModule.default !== 'undefined') {
- configModule = configModule.default
- }
- } catch (e) {
- const extension = path.extname(configFilePath)
- if (extension === '.coffee' && !COFFEE_SCRIPT_AVAILABLE) {
- log.error('You need to install CoffeeScript.\n npm install coffeescript --save-dev')
- } else if (extension === '.ls' && !LIVE_SCRIPT_AVAILABLE) {
- log.error('You need to install LiveScript.\n npm install LiveScript --save-dev')
- } else if (extension === '.ts' && !TYPE_SCRIPT_AVAILABLE) {
- log.error('You need to install TypeScript.\n npm install typescript ts-node --save-dev')
- }
- return fail('Error in config file!\n ' + e.stack || e)
- }
- if (!helper.isFunction(configModule)) {
- return fail('Config file must export a function!\n' + CONFIG_SYNTAX_HELP)
- }
- } else {
- configModule = () => {} // if no config file path is passed, we define a dummy config module.
- }
- const config = new Config()
- // save and reset hostname and listenAddress so we can detect if the user
- // changed them
- const defaultHostname = config.hostname
- config.hostname = null
- const defaultListenAddress = config.listenAddress
- config.listenAddress = null
- // add the user's configuration in
- config.set(cliOptions)
- let configModuleReturn
- try {
- 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)
- // 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
- }
- // 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.')
- return normalizeConfig(config, configFilePath)
- }
- /**
- * Return value is a function or (non-null) object that has a `then` method.
- *
- * @type {boolean}
- * @see {@link https://promisesaplus.com/}
- */
- const returnIsThenable = (
- (
- (configModuleReturn != null && typeof configModuleReturn === 'object') ||
- typeof configModuleReturn === 'function'
- ) && 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)
- },
- function onKarmaConfigModuleRejected (reason) {
- return fail('Error in config file!\n', reason)
- }
- )
- } else {
- if (promiseConfig) {
- try {
- return Promise.resolve(finalizeConfig(config))
- } catch (exception) {
- return Promise.reject(exception)
- }
- } else {
- return finalizeConfig(config)
- }
- }
- }
- // PUBLIC API
- exports.parseConfig = parseConfig
- exports.Pattern = Pattern
- exports.createPatternObject = createPatternObject
- exports.Config = Config
|