var path = require('path') var logger = require('./logger') var log = logger.create('config') var helper = require('./helper') var constant = require('./constants') var _ = require('lodash') var COFFEE_SCRIPT_AVAILABLE = false var LIVE_SCRIPT_AVAILABLE = false var TYPE_SCRIPT_AVAILABLE = false // Coffee is required here to enable config files written in coffee-script. // It's not directly used in this file. try { require('coffee-script').register() COFFEE_SCRIPT_AVAILABLE = true } catch (e) {} // 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 (e) {} try { require('ts-node').register() TYPE_SCRIPT_AVAILABLE = true } catch (e) {} var Pattern = function (pattern, served, included, watched, nocache) { 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) } Pattern.prototype.compare = function (other) { return helper.mmComparePatternWeights(this.weight, other.weight) } var UrlPattern = function (url) { Pattern.call(this, url, false, true, false, false) } var createPatternObject = function (pattern) { if (pattern && helper.isString(pattern)) { return helper.isUrlAbsolute(pattern) ? new UrlPattern(pattern) : new Pattern(pattern) } if (helper.isObject(pattern)) { if (pattern.pattern && helper.isString(pattern.pattern)) { return helper.isUrlAbsolute(pattern.pattern) ? new UrlPattern(pattern.pattern) : new Pattern( pattern.pattern, pattern.served, pattern.included, pattern.watched, pattern.nocache) } log.warn('Invalid pattern %s!\n\tObject is missing "pattern" property.', pattern) return new Pattern(null, false, false, false, false) } log.warn('Invalid pattern %s!\n\tExpected string or object with "pattern" property.', pattern) return new Pattern(null, false, false, false, false) } var normalizeUrl = function (url) { if (url.charAt(0) !== '/') { url = '/' + url } if (url.charAt(url.length - 1) !== '/') { url = url + '/' } return url } var normalizeUrlRoot = function (urlRoot) { var normalizedUrlRoot = normalizeUrl(urlRoot) if (normalizedUrlRoot !== urlRoot) { log.warn('urlRoot normalized to "%s"', normalizedUrlRoot) } return normalizedUrlRoot } var normalizeProxyPath = function (proxyPath) { var normalizedProxyPath = normalizeUrl(proxyPath) if (normalizedProxyPath !== proxyPath) { log.warn('proxyPath normalized to "%s"', normalizedProxyPath) } return normalizedProxyPath } var normalizeConfig = function (config, configFilePath) { var basePathResolve = function (relativePath) { if (helper.isUrlAbsolute(relativePath)) { return relativePath } if (!helper.isDefined(config.basePath) || !helper.isDefined(relativePath)) { return '' } return path.resolve(config.basePath, relativePath) } var createPatternMapper = function (resolve) { return function (objectPattern) { objectPattern.pattern = resolve(objectPattern.pattern) return objectPattern } } if (helper.isString(configFilePath)) { // resolve basePath config.basePath = path.resolve(path.dirname(configFilePath), config.basePath) // always ignore the config file itself config.exclude.push(configFilePath) } 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) // 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) // normalize urlRoot config.urlRoot = normalizeUrlRoot(config.urlRoot) // normalize and default upstream proxy settings if given if (config.upstreamProxy) { var proxy = config.upstreamProxy proxy.path = _.isUndefined(proxy.path) ? '/' : normalizeProxyPath(proxy.path) proxy.hostname = _.isUndefined(proxy.hostname) ? 'localhost' : proxy.hostname proxy.port = _.isUndefined(proxy.port) ? 9875 : proxy.port // force protocol to end with ':' proxy.protocol = (proxy.protocol || 'http').split(':')[0] + ':' if (proxy.protocol.match(/https?:/) === null) { log.warn('"%s" is not a supported upstream proxy protocol, defaulting to "http:"', proxy.protocol) proxy.protocol = 'http:' } } // force protocol to end with ':' config.protocol = (config.protocol || 'http').split(':')[0] + ':' if (config.protocol.match(/https?:/) === null) { log.warn('"%s" is not a supported protocol, defaulting to "http:"', config.protocol) config.protocol = 'http:' } if (config.proxies && config.proxies.hasOwnProperty(config.urlRoot)) { log.warn('"%s" is proxied, you should probably change urlRoot to avoid conflicts', config.urlRoot) } if (config.singleRun && config.autoWatch) { log.debug('autoWatch set to false, because of singleRun') config.autoWatch = false } if (helper.isString(config.reporters)) { config.reporters = config.reporters.split(',') } if (config.client && config.client.args && !Array.isArray(config.client.args)) { throw new Error('Invalid configuration: client.args must be an array of strings') } if (config.browsers && Array.isArray(config.browsers) === false) { throw new TypeError('Invalid configuration: browsers option must be an array') } if (config.formatError && !helper.isFunction(config.formatError)) { throw new TypeError('Invalid configuration: formatError option must be a function.') } if (config.processKillTimeout && !helper.isNumber(config.processKillTimeout)) { throw new TypeError('Invalid configuration: processKillTimeout option must be a number.') } var defaultClient = config.defaultClient || {} Object.keys(defaultClient).forEach(function (key) { var option = config.client[key] config.client[key] = helper.isDefined(option) ? option : defaultClient[key] }) // normalize preprocessors var preprocessors = config.preprocessors || {} var normalizedPreprocessors = config.preprocessors = Object.create(null) Object.keys(preprocessors).forEach(function (pattern) { var normalizedPattern = helper.normalizeWinPath(basePathResolve(pattern)) normalizedPreprocessors[normalizedPattern] = helper.isString(preprocessors[pattern]) ? [preprocessors[pattern]] : preprocessors[pattern] }) // define custom launchers/preprocessors/reporters - create an inlined plugin var module = Object.create(null) var hasSomeInlinedPlugin = false var types = ['launcher', 'preprocessor', 'reporter'] types.forEach(function (type) { var definitions = config['custom' + helper.ucFirst(type) + 's'] || {} Object.keys(definitions).forEach(function (name) { var definition = definitions[name] if (!helper.isObject(definition)) { return log.warn('Can not define %s %s. Definition has to be an object.', type, name) } if (!helper.isString(definition.base)) { return log.warn('Can not define %s %s. Missing base %s.', type, name, type) } var token = type + ':' + definition.base var locals = { args: ['value', definition] } module[type + ':' + name] = ['factory', function (injector) { var 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 } var Config = function () { var config = this 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 this.set = function (newConfig) { _.merge(config, newConfig, function (obj, src) { // Overwrite arrays to keep consistent with #283 if (_.isArray(src)) { return src } }) } // 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.exclude = [] this.logLevel = constant.LOG_INFO this.colors = true this.autoWatch = true this.autoWatchBatchDelay = 250 this.restartOnFileChange = false this.usePolling = process.platform === 'darwin' || process.platform === 'linux' this.reporters = ['progress'] this.singleRun = false this.browsers = [] this.captureTimeout = 60000 this.proxies = {} this.proxyValidateSSL = true this.preprocessors = {} 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, captureConsole: true, clearContext: true } this.browserDisconnectTimeout = 2000 this.browserDisconnectTolerance = 0 this.browserNoActivityTimeout = 10000 this.processKillTimeout = 2000 this.concurrency = Infinity this.failOnEmptyTestSuite = true this.retryLimit = 2 this.detached = false this.crossOriginAttribute = true } var CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' + ' config.set({\n' + ' // your config\n' + ' });\n' + ' };\n' var parseConfig = function (configFilePath, cliOptions) { var configModule if (configFilePath) { log.debug('Loading config %s', configFilePath) try { configModule = require(configFilePath) if (typeof configModule === 'object' && typeof configModule.default !== 'undefined') { configModule = configModule.default } } catch (e) { if (e.code === 'MODULE_NOT_FOUND' && e.message.indexOf(configFilePath) !== -1) { log.error('File %s does not exist!', configFilePath) } else { log.error('Invalid config file!\n ' + e.stack) var extension = path.extname(configFilePath) if (extension === '.coffee' && !COFFEE_SCRIPT_AVAILABLE) { log.error('You need to install CoffeeScript.\n' + ' npm install coffee-script --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 process.exit(1) } if (!helper.isFunction(configModule)) { log.error('Config file must export a function!\n' + CONFIG_SYNTAX_HELP) return process.exit(1) } } else { log.debug('No config file specified.') // if no config file path is passed, we define a dummy config module. configModule = function () {} } var config = new Config() // save and reset hostname and listenAddress so we can detect if the user // changed them var defaultHostname = config.hostname config.hostname = null var defaultListenAddress = config.listenAddress config.listenAddress = null // add the user's configuration in config.set(cliOptions) try { configModule(config) } catch (e) { log.error('Error in config file!\n', e) return process.exit(1) } // 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 %s but hostname was left as the default: ' + '%s. If your browsers fail to connect, consider changing the hostname option.', config.listenAddress, defaultHostname) } // 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) return normalizeConfig(config, configFilePath) } // PUBLIC API exports.parseConfig = parseConfig exports.Pattern = Pattern exports.createPatternObject = createPatternObject exports.Config = Config