config.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. 'use strict'
  2. const path = require('path')
  3. const assert = require('assert')
  4. const logger = require('./logger')
  5. const log = logger.create('config')
  6. const helper = require('./helper')
  7. const constant = require('./constants')
  8. const _ = require('lodash')
  9. let COFFEE_SCRIPT_AVAILABLE = false
  10. let LIVE_SCRIPT_AVAILABLE = false
  11. let TYPE_SCRIPT_AVAILABLE = false
  12. try {
  13. require('coffeescript').register()
  14. COFFEE_SCRIPT_AVAILABLE = true
  15. } catch {}
  16. // LiveScript is required here to enable config files written in LiveScript.
  17. // It's not directly used in this file.
  18. try {
  19. require('LiveScript')
  20. LIVE_SCRIPT_AVAILABLE = true
  21. } catch {}
  22. try {
  23. require('ts-node')
  24. TYPE_SCRIPT_AVAILABLE = true
  25. } catch {}
  26. class Pattern {
  27. constructor (pattern, served, included, watched, nocache, type, isBinary, integrity) {
  28. this.pattern = pattern
  29. this.served = helper.isDefined(served) ? served : true
  30. this.included = helper.isDefined(included) ? included : true
  31. this.watched = helper.isDefined(watched) ? watched : true
  32. this.nocache = helper.isDefined(nocache) ? nocache : false
  33. this.weight = helper.mmPatternWeight(pattern)
  34. this.type = type
  35. this.isBinary = isBinary
  36. this.integrity = integrity
  37. }
  38. compare (other) {
  39. return helper.mmComparePatternWeights(this.weight, other.weight)
  40. }
  41. }
  42. class UrlPattern extends Pattern {
  43. constructor (url, type, integrity) {
  44. super(url, false, true, false, false, type, undefined, integrity)
  45. }
  46. }
  47. function createPatternObject (pattern) {
  48. if (pattern && helper.isString(pattern)) {
  49. return helper.isUrlAbsolute(pattern)
  50. ? new UrlPattern(pattern)
  51. : new Pattern(pattern)
  52. } else if (helper.isObject(pattern) && pattern.pattern && helper.isString(pattern.pattern)) {
  53. return helper.isUrlAbsolute(pattern.pattern)
  54. ? new UrlPattern(pattern.pattern, pattern.type, pattern.integrity)
  55. : new Pattern(pattern.pattern, pattern.served, pattern.included, pattern.watched, pattern.nocache, pattern.type)
  56. } else {
  57. log.warn(`Invalid pattern ${pattern}!\n\tExpected string or object with "pattern" property.`)
  58. return new Pattern(null, false, false, false, false)
  59. }
  60. }
  61. function normalizeUrl (url) {
  62. if (!url.startsWith('/')) {
  63. url = `/${url}`
  64. }
  65. if (!url.endsWith('/')) {
  66. url = url + '/'
  67. }
  68. return url
  69. }
  70. function normalizeUrlRoot (urlRoot) {
  71. const normalizedUrlRoot = normalizeUrl(urlRoot)
  72. if (normalizedUrlRoot !== urlRoot) {
  73. log.warn(`urlRoot normalized to "${normalizedUrlRoot}"`)
  74. }
  75. return normalizedUrlRoot
  76. }
  77. function normalizeProxyPath (proxyPath) {
  78. const normalizedProxyPath = normalizeUrl(proxyPath)
  79. if (normalizedProxyPath !== proxyPath) {
  80. log.warn(`proxyPath normalized to "${normalizedProxyPath}"`)
  81. }
  82. return normalizedProxyPath
  83. }
  84. function normalizeConfig (config, configFilePath) {
  85. function basePathResolve (relativePath) {
  86. if (helper.isUrlAbsolute(relativePath)) {
  87. return relativePath
  88. } else if (helper.isDefined(config.basePath) && helper.isDefined(relativePath)) {
  89. return path.resolve(config.basePath, relativePath)
  90. } else {
  91. return ''
  92. }
  93. }
  94. function createPatternMapper (resolve) {
  95. return (objectPattern) => Object.assign(objectPattern, { pattern: resolve(objectPattern.pattern) })
  96. }
  97. if (helper.isString(configFilePath)) {
  98. config.basePath = path.resolve(path.dirname(configFilePath), config.basePath) // resolve basePath
  99. config.exclude.push(configFilePath) // always ignore the config file itself
  100. } else {
  101. config.basePath = path.resolve(config.basePath || '.')
  102. }
  103. config.files = config.files.map(createPatternObject).map(createPatternMapper(basePathResolve))
  104. config.exclude = config.exclude.map(basePathResolve)
  105. config.customContextFile = config.customContextFile && basePathResolve(config.customContextFile)
  106. config.customDebugFile = config.customDebugFile && basePathResolve(config.customDebugFile)
  107. config.customClientContextFile = config.customClientContextFile && basePathResolve(config.customClientContextFile)
  108. // normalize paths on windows
  109. config.basePath = helper.normalizeWinPath(config.basePath)
  110. config.files = config.files.map(createPatternMapper(helper.normalizeWinPath))
  111. config.exclude = config.exclude.map(helper.normalizeWinPath)
  112. config.customContextFile = helper.normalizeWinPath(config.customContextFile)
  113. config.customDebugFile = helper.normalizeWinPath(config.customDebugFile)
  114. config.customClientContextFile = helper.normalizeWinPath(config.customClientContextFile)
  115. // normalize urlRoot
  116. config.urlRoot = normalizeUrlRoot(config.urlRoot)
  117. // normalize and default upstream proxy settings if given
  118. if (config.upstreamProxy) {
  119. const proxy = config.upstreamProxy
  120. proxy.path = helper.isDefined(proxy.path) ? normalizeProxyPath(proxy.path) : '/'
  121. proxy.hostname = helper.isDefined(proxy.hostname) ? proxy.hostname : 'localhost'
  122. proxy.port = helper.isDefined(proxy.port) ? proxy.port : 9875
  123. // force protocol to end with ':'
  124. proxy.protocol = (proxy.protocol || 'http').split(':')[0] + ':'
  125. if (proxy.protocol.match(/https?:/) === null) {
  126. log.warn(`"${proxy.protocol}" is not a supported upstream proxy protocol, defaulting to "http:"`)
  127. proxy.protocol = 'http:'
  128. }
  129. }
  130. // force protocol to end with ':'
  131. config.protocol = (config.protocol || 'http').split(':')[0] + ':'
  132. if (config.protocol.match(/https?:/) === null) {
  133. log.warn(`"${config.protocol}" is not a supported protocol, defaulting to "http:"`)
  134. config.protocol = 'http:'
  135. }
  136. if (config.proxies && Object.prototype.hasOwnProperty.call(config.proxies, config.urlRoot)) {
  137. log.warn(`"${config.urlRoot}" is proxied, you should probably change urlRoot to avoid conflicts`)
  138. }
  139. if (config.singleRun && config.autoWatch) {
  140. log.debug('autoWatch set to false, because of singleRun')
  141. config.autoWatch = false
  142. }
  143. if (config.runInParent) {
  144. log.debug('useIframe set to false, because using runInParent')
  145. config.useIframe = false
  146. }
  147. if (!config.singleRun && !config.useIframe && config.runInParent) {
  148. log.debug('singleRun set to true, because using runInParent')
  149. config.singleRun = true
  150. }
  151. if (helper.isString(config.reporters)) {
  152. config.reporters = config.reporters.split(',')
  153. }
  154. if (config.client && config.client.args) {
  155. assert(Array.isArray(config.client.args), 'Invalid configuration: client.args must be an array of strings')
  156. }
  157. if (config.browsers) {
  158. assert(Array.isArray(config.browsers), 'Invalid configuration: browsers option must be an array')
  159. }
  160. if (config.formatError) {
  161. assert(helper.isFunction(config.formatError), 'Invalid configuration: formatError option must be a function.')
  162. }
  163. if (config.processKillTimeout) {
  164. assert(helper.isNumber(config.processKillTimeout), 'Invalid configuration: processKillTimeout option must be a number.')
  165. }
  166. if (config.browserSocketTimeout) {
  167. assert(helper.isNumber(config.browserSocketTimeout), 'Invalid configuration: browserSocketTimeout option must be a number.')
  168. }
  169. if (config.pingTimeout) {
  170. assert(helper.isNumber(config.pingTimeout), 'Invalid configuration: pingTimeout option must be a number.')
  171. }
  172. const defaultClient = config.defaultClient || {}
  173. Object.keys(defaultClient).forEach(function (key) {
  174. const option = config.client[key]
  175. config.client[key] = helper.isDefined(option) ? option : defaultClient[key]
  176. })
  177. // normalize preprocessors
  178. const preprocessors = config.preprocessors || {}
  179. const normalizedPreprocessors = config.preprocessors = Object.create(null)
  180. Object.keys(preprocessors).forEach(function (pattern) {
  181. const normalizedPattern = helper.normalizeWinPath(basePathResolve(pattern))
  182. normalizedPreprocessors[normalizedPattern] = helper.isString(preprocessors[pattern])
  183. ? [preprocessors[pattern]] : preprocessors[pattern]
  184. })
  185. // define custom launchers/preprocessors/reporters - create a new plugin
  186. const module = Object.create(null)
  187. let hasSomeInlinedPlugin = false
  188. const types = ['launcher', 'preprocessor', 'reporter']
  189. types.forEach(function (type) {
  190. const definitions = config[`custom${helper.ucFirst(type)}s`] || {}
  191. Object.keys(definitions).forEach(function (name) {
  192. const definition = definitions[name]
  193. if (!helper.isObject(definition)) {
  194. return log.warn(`Can not define ${type} ${name}. Definition has to be an object.`)
  195. }
  196. if (!helper.isString(definition.base)) {
  197. return log.warn(`Can not define ${type} ${name}. Missing base ${type}.`)
  198. }
  199. const token = type + ':' + definition.base
  200. const locals = {
  201. args: ['value', definition]
  202. }
  203. module[type + ':' + name] = ['factory', function (injector) {
  204. const plugin = injector.createChild([locals], [token]).get(token)
  205. if (type === 'launcher' && helper.isDefined(definition.displayName)) {
  206. plugin.displayName = definition.displayName
  207. }
  208. return plugin
  209. }]
  210. hasSomeInlinedPlugin = true
  211. })
  212. })
  213. if (hasSomeInlinedPlugin) {
  214. config.plugins.push(module)
  215. }
  216. return config
  217. }
  218. /**
  219. * @class
  220. */
  221. class Config {
  222. constructor () {
  223. this.LOG_DISABLE = constant.LOG_DISABLE
  224. this.LOG_ERROR = constant.LOG_ERROR
  225. this.LOG_WARN = constant.LOG_WARN
  226. this.LOG_INFO = constant.LOG_INFO
  227. this.LOG_DEBUG = constant.LOG_DEBUG
  228. // DEFAULT CONFIG
  229. this.frameworks = []
  230. this.protocol = 'http:'
  231. this.port = constant.DEFAULT_PORT
  232. this.listenAddress = constant.DEFAULT_LISTEN_ADDR
  233. this.hostname = constant.DEFAULT_HOSTNAME
  234. this.httpsServerConfig = {}
  235. this.basePath = ''
  236. this.files = []
  237. this.browserConsoleLogOptions = {
  238. level: 'debug',
  239. format: '%b %T: %m',
  240. terminal: true
  241. }
  242. this.customContextFile = null
  243. this.customDebugFile = null
  244. this.customClientContextFile = null
  245. this.exclude = []
  246. this.logLevel = constant.LOG_INFO
  247. this.colors = true
  248. this.autoWatch = true
  249. this.autoWatchBatchDelay = 250
  250. this.restartOnFileChange = false
  251. this.usePolling = process.platform === 'linux'
  252. this.reporters = ['progress']
  253. this.singleRun = false
  254. this.browsers = []
  255. this.captureTimeout = 60000
  256. this.pingTimeout = 5000
  257. this.proxies = {}
  258. this.proxyValidateSSL = true
  259. this.preprocessors = {}
  260. this.preprocessor_priority = {}
  261. this.urlRoot = '/'
  262. this.upstreamProxy = undefined
  263. this.reportSlowerThan = 0
  264. this.loggers = [constant.CONSOLE_APPENDER]
  265. this.transports = ['polling', 'websocket']
  266. this.forceJSONP = false
  267. this.plugins = ['karma-*']
  268. this.defaultClient = this.client = {
  269. args: [],
  270. useIframe: true,
  271. runInParent: false,
  272. captureConsole: true,
  273. clearContext: true,
  274. allowedReturnUrlPatterns: ['^https?://']
  275. }
  276. this.browserDisconnectTimeout = 2000
  277. this.browserDisconnectTolerance = 0
  278. this.browserNoActivityTimeout = 30000
  279. this.processKillTimeout = 2000
  280. this.concurrency = Infinity
  281. this.failOnEmptyTestSuite = true
  282. this.retryLimit = 2
  283. this.detached = false
  284. this.crossOriginAttribute = true
  285. this.browserSocketTimeout = 20000
  286. }
  287. set (newConfig) {
  288. _.mergeWith(this, newConfig, (obj, src) => {
  289. // Overwrite arrays to keep consistent with #283
  290. if (Array.isArray(src)) {
  291. return src
  292. }
  293. })
  294. }
  295. }
  296. const CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' +
  297. ' config.set({\n' +
  298. ' // your config\n' +
  299. ' });\n' +
  300. ' };\n'
  301. /**
  302. * Retrieve a parsed and finalized Karma `Config` instance. This `karmaConfig`
  303. * object may be used to configure public API methods such a `Server`,
  304. * `runner.run`, and `stopper.stop`.
  305. *
  306. * @param {?string} [configFilePath=null]
  307. * A string representing a file system path pointing to the config file
  308. * whose default export is a function that will be used to set Karma
  309. * configuration options. This function will be passed an instance of the
  310. * `Config` class as its first argument. If this option is not provided,
  311. * then only the options provided by the `cliOptions` argument will be
  312. * set.
  313. * @param {Object} cliOptions
  314. * An object whose values will take priority over options set in the
  315. * config file. The config object passed to function exported by the
  316. * config file will already have these options applied. Any changes the
  317. * config file makes to these options will effectively be ignored in the
  318. * final configuration.
  319. *
  320. * `cliOptions` all the same options as the config file and is applied
  321. * using the same `config.set()` method.
  322. * @param {Object} parseOptions
  323. * @param {boolean} [parseOptions.promiseConfig=false]
  324. * When `true`, a promise that resolves to a `Config` object will be
  325. * returned. This also allows the function exported by config files (if
  326. * provided) to be asynchronous by returning a promise. Resolving this
  327. * promise indicates that all async activity has completed. The resolution
  328. * value itself is ignored, all configuration must be done with
  329. * `config.set`.
  330. * @param {boolean} [parseOptions.throwErrors=false]
  331. * When `true`, process exiting on critical failures will be disabled. In
  332. * The error will be thrown as an exception. If
  333. * `parseOptions.promiseConfig` is also `true`, then the error will
  334. * instead be used as the promise's reject reason.
  335. * @returns {Config|Promise<Config>}
  336. */
  337. function parseConfig (configFilePath, cliOptions, parseOptions) {
  338. const promiseConfig = parseOptions && parseOptions.promiseConfig === true
  339. const throwErrors = parseOptions && parseOptions.throwErrors === true
  340. const shouldSetupLoggerEarly = promiseConfig
  341. if (shouldSetupLoggerEarly) {
  342. // `setupFromConfig` provides defaults for `colors` and `logLevel`.
  343. // `setup` provides defaults for `appenders`
  344. // The first argument MUST BE an object
  345. logger.setupFromConfig({})
  346. }
  347. function fail () {
  348. log.error(...arguments)
  349. if (throwErrors) {
  350. const errorMessage = Array.from(arguments).join(' ')
  351. const err = new Error(errorMessage)
  352. if (promiseConfig) {
  353. return Promise.reject(err)
  354. }
  355. throw err
  356. } else {
  357. const warningMessage =
  358. 'The `parseConfig()` function historically called `process.exit(1)`' +
  359. ' when it failed. This behavior is now deprecated and function will' +
  360. ' throw an error in the next major release. To suppress this warning' +
  361. ' pass `throwErrors: true` as a third argument to opt-in into the new' +
  362. ' behavior and adjust your code to respond to the exception' +
  363. ' accordingly.' +
  364. ' Example: `parseConfig(path, cliOptions, { throwErrors: true })`'
  365. log.warn(warningMessage)
  366. process.exit(1)
  367. }
  368. }
  369. let configModule
  370. if (configFilePath) {
  371. try {
  372. if (path.extname(configFilePath) === '.ts' && TYPE_SCRIPT_AVAILABLE) {
  373. require('ts-node').register()
  374. }
  375. configModule = require(configFilePath)
  376. if (typeof configModule === 'object' && typeof configModule.default !== 'undefined') {
  377. configModule = configModule.default
  378. }
  379. } catch (e) {
  380. const extension = path.extname(configFilePath)
  381. if (extension === '.coffee' && !COFFEE_SCRIPT_AVAILABLE) {
  382. log.error('You need to install CoffeeScript.\n npm install coffeescript --save-dev')
  383. } else if (extension === '.ls' && !LIVE_SCRIPT_AVAILABLE) {
  384. log.error('You need to install LiveScript.\n npm install LiveScript --save-dev')
  385. } else if (extension === '.ts' && !TYPE_SCRIPT_AVAILABLE) {
  386. log.error('You need to install TypeScript.\n npm install typescript ts-node --save-dev')
  387. }
  388. return fail('Error in config file!\n ' + e.stack || e)
  389. }
  390. if (!helper.isFunction(configModule)) {
  391. return fail('Config file must export a function!\n' + CONFIG_SYNTAX_HELP)
  392. }
  393. } else {
  394. configModule = () => {} // if no config file path is passed, we define a dummy config module.
  395. }
  396. const config = new Config()
  397. // save and reset hostname and listenAddress so we can detect if the user
  398. // changed them
  399. const defaultHostname = config.hostname
  400. config.hostname = null
  401. const defaultListenAddress = config.listenAddress
  402. config.listenAddress = null
  403. // add the user's configuration in
  404. config.set(cliOptions)
  405. let configModuleReturn
  406. try {
  407. configModuleReturn = configModule(config)
  408. } catch (e) {
  409. return fail('Error in config file!\n', e)
  410. }
  411. function finalizeConfig (config) {
  412. // merge the config from config file and cliOptions (precedence)
  413. config.set(cliOptions)
  414. // if the user changed listenAddress, but didn't set a hostname, warn them
  415. if (config.hostname === null && config.listenAddress !== null) {
  416. log.warn(`ListenAddress was set to ${config.listenAddress} but hostname was left as the default: ` +
  417. `${defaultHostname}. If your browsers fail to connect, consider changing the hostname option.`)
  418. }
  419. // restore values that weren't overwritten by the user
  420. if (config.hostname === null) {
  421. config.hostname = defaultHostname
  422. }
  423. if (config.listenAddress === null) {
  424. config.listenAddress = defaultListenAddress
  425. }
  426. // configure the logger as soon as we can
  427. logger.setup(config.logLevel, config.colors, config.loggers)
  428. log.debug(configFilePath ? `Loading config ${configFilePath}` : 'No config file specified.')
  429. return normalizeConfig(config, configFilePath)
  430. }
  431. /**
  432. * Return value is a function or (non-null) object that has a `then` method.
  433. *
  434. * @type {boolean}
  435. * @see {@link https://promisesaplus.com/}
  436. */
  437. const returnIsThenable = (
  438. (
  439. (configModuleReturn != null && typeof configModuleReturn === 'object') ||
  440. typeof configModuleReturn === 'function'
  441. ) && typeof configModuleReturn.then === 'function'
  442. )
  443. if (returnIsThenable) {
  444. if (promiseConfig !== true) {
  445. const errorMessage =
  446. 'The `parseOptions.promiseConfig` option must be set to `true` to ' +
  447. 'enable promise return values from configuration files. ' +
  448. 'Example: `parseConfig(path, cliOptions, { promiseConfig: true })`'
  449. return fail(errorMessage)
  450. }
  451. return configModuleReturn.then(
  452. function onKarmaConfigModuleFulfilled (/* ignoredResolutionValue */) {
  453. return finalizeConfig(config)
  454. },
  455. function onKarmaConfigModuleRejected (reason) {
  456. return fail('Error in config file!\n', reason)
  457. }
  458. )
  459. } else {
  460. if (promiseConfig) {
  461. try {
  462. return Promise.resolve(finalizeConfig(config))
  463. } catch (exception) {
  464. return Promise.reject(exception)
  465. }
  466. } else {
  467. return finalizeConfig(config)
  468. }
  469. }
  470. }
  471. // PUBLIC API
  472. exports.parseConfig = parseConfig
  473. exports.Pattern = Pattern
  474. exports.createPatternObject = createPatternObject
  475. exports.Config = Config