Files
Jack Franklin 8f8ec22bb3 Update karma to 6.4.0
No changes in the changelog
(https://github.com/karma-runner/karma/blob/master/CHANGELOG.md) to
concern ourselves about, but one useful change is they upgraded the
log4js dependency which was spitting out console messages on each test
run. This is fixed with this version of Karma.

Bug: none
Change-Id: Ic49618cbda7a079cacb9813f5129692a83508a92
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3745621
Commit-Queue: Jack Franklin <jacktfranklin@chromium.org>
Auto-Submit: Jack Franklin <jacktfranklin@chromium.org>
Reviewed-by: Philip Pfaffe <pfaffe@chromium.org>
Commit-Queue: Philip Pfaffe <pfaffe@chromium.org>
2022-07-05 10:46:37 +00:00

545 lines
18 KiB
JavaScript

'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, integrity) {
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
this.integrity = integrity
}
compare (other) {
return helper.mmComparePatternWeights(this.weight, other.weight)
}
}
class UrlPattern extends Pattern {
constructor (url, type, integrity) {
super(url, false, true, false, false, type, undefined, integrity)
}
}
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