1 | var path = require('path')
|
2 |
|
3 | var logger = require('./logger')
|
4 | var log = logger.create('config')
|
5 | var helper = require('./helper')
|
6 | var constant = require('./constants')
|
7 |
|
8 | var _ = require('lodash')
|
9 |
|
10 | var COFFEE_SCRIPT_AVAILABLE = false
|
11 | var LIVE_SCRIPT_AVAILABLE = false
|
12 | var TYPE_SCRIPT_AVAILABLE = false
|
13 |
|
14 |
|
15 |
|
16 | try {
|
17 | require('coffee-script').register()
|
18 | COFFEE_SCRIPT_AVAILABLE = true
|
19 | } catch (e) {}
|
20 |
|
21 |
|
22 | try {
|
23 | require('coffeescript').register()
|
24 | COFFEE_SCRIPT_AVAILABLE = true
|
25 | } catch (e) {}
|
26 |
|
27 |
|
28 |
|
29 | try {
|
30 | require('LiveScript')
|
31 | LIVE_SCRIPT_AVAILABLE = true
|
32 | } catch (e) {}
|
33 |
|
34 | try {
|
35 | require('ts-node').register()
|
36 | TYPE_SCRIPT_AVAILABLE = true
|
37 | } catch (e) {}
|
38 |
|
39 | var Pattern = function (pattern, served, included, watched, nocache, type) {
|
40 | this.pattern = pattern
|
41 | this.served = helper.isDefined(served) ? served : true
|
42 | this.included = helper.isDefined(included) ? included : true
|
43 | this.watched = helper.isDefined(watched) ? watched : true
|
44 | this.nocache = helper.isDefined(nocache) ? nocache : false
|
45 | this.weight = helper.mmPatternWeight(pattern)
|
46 | this.type = type
|
47 | }
|
48 |
|
49 | Pattern.prototype.compare = function (other) {
|
50 | return helper.mmComparePatternWeights(this.weight, other.weight)
|
51 | }
|
52 |
|
53 | var UrlPattern = function (url) {
|
54 | Pattern.call(this, url, false, true, false, false)
|
55 | }
|
56 |
|
57 | var createPatternObject = function (pattern) {
|
58 | if (pattern && helper.isString(pattern)) {
|
59 | return helper.isUrlAbsolute(pattern) ? new UrlPattern(pattern) : new Pattern(pattern)
|
60 | }
|
61 |
|
62 | if (helper.isObject(pattern)) {
|
63 | if (pattern.pattern && helper.isString(pattern.pattern)) {
|
64 | return helper.isUrlAbsolute(pattern.pattern)
|
65 | ? new UrlPattern(pattern.pattern)
|
66 | : new Pattern(
|
67 | pattern.pattern,
|
68 | pattern.served,
|
69 | pattern.included,
|
70 | pattern.watched,
|
71 | pattern.nocache,
|
72 | pattern.type)
|
73 | }
|
74 |
|
75 | log.warn('Invalid pattern %s!\n\tObject is missing "pattern" property.', pattern)
|
76 | return new Pattern(null, false, false, false, false)
|
77 | }
|
78 |
|
79 | log.warn('Invalid pattern %s!\n\tExpected string or object with "pattern" property.', pattern)
|
80 | return new Pattern(null, false, false, false, false)
|
81 | }
|
82 |
|
83 | var normalizeUrl = function (url) {
|
84 | if (url.charAt(0) !== '/') {
|
85 | url = '/' + url
|
86 | }
|
87 |
|
88 | if (url.charAt(url.length - 1) !== '/') {
|
89 | url = url + '/'
|
90 | }
|
91 |
|
92 | return url
|
93 | }
|
94 |
|
95 | var normalizeUrlRoot = function (urlRoot) {
|
96 | var normalizedUrlRoot = normalizeUrl(urlRoot)
|
97 |
|
98 | if (normalizedUrlRoot !== urlRoot) {
|
99 | log.warn('urlRoot normalized to "%s"', normalizedUrlRoot)
|
100 | }
|
101 |
|
102 | return normalizedUrlRoot
|
103 | }
|
104 |
|
105 | var normalizeProxyPath = function (proxyPath) {
|
106 | var normalizedProxyPath = normalizeUrl(proxyPath)
|
107 |
|
108 | if (normalizedProxyPath !== proxyPath) {
|
109 | log.warn('proxyPath normalized to "%s"', normalizedProxyPath)
|
110 | }
|
111 |
|
112 | return normalizedProxyPath
|
113 | }
|
114 |
|
115 | var normalizeConfig = function (config, configFilePath) {
|
116 | var basePathResolve = function (relativePath) {
|
117 | if (helper.isUrlAbsolute(relativePath)) {
|
118 | return relativePath
|
119 | }
|
120 |
|
121 | if (!helper.isDefined(config.basePath) || !helper.isDefined(relativePath)) {
|
122 | return ''
|
123 | }
|
124 | return path.resolve(config.basePath, relativePath)
|
125 | }
|
126 |
|
127 | var createPatternMapper = function (resolve) {
|
128 | return function (objectPattern) {
|
129 | objectPattern.pattern = resolve(objectPattern.pattern)
|
130 |
|
131 | return objectPattern
|
132 | }
|
133 | }
|
134 |
|
135 | if (helper.isString(configFilePath)) {
|
136 |
|
137 | config.basePath = path.resolve(path.dirname(configFilePath), config.basePath)
|
138 |
|
139 |
|
140 | config.exclude.push(configFilePath)
|
141 | } else {
|
142 | config.basePath = path.resolve(config.basePath || '.')
|
143 | }
|
144 |
|
145 | config.files = config.files.map(createPatternObject).map(createPatternMapper(basePathResolve))
|
146 | config.exclude = config.exclude.map(basePathResolve)
|
147 | config.customContextFile = config.customContextFile && basePathResolve(config.customContextFile)
|
148 | config.customDebugFile = config.customDebugFile && basePathResolve(config.customDebugFile)
|
149 | config.customClientContextFile = config.customClientContextFile && basePathResolve(config.customClientContextFile)
|
150 |
|
151 |
|
152 | config.basePath = helper.normalizeWinPath(config.basePath)
|
153 | config.files = config.files.map(createPatternMapper(helper.normalizeWinPath))
|
154 | config.exclude = config.exclude.map(helper.normalizeWinPath)
|
155 | config.customContextFile = helper.normalizeWinPath(config.customContextFile)
|
156 | config.customDebugFile = helper.normalizeWinPath(config.customDebugFile)
|
157 | config.customClientContextFile = helper.normalizeWinPath(config.customClientContextFile)
|
158 |
|
159 |
|
160 | config.urlRoot = normalizeUrlRoot(config.urlRoot)
|
161 |
|
162 |
|
163 | if (config.upstreamProxy) {
|
164 | var proxy = config.upstreamProxy
|
165 | proxy.path = _.isUndefined(proxy.path) ? '/' : normalizeProxyPath(proxy.path)
|
166 | proxy.hostname = _.isUndefined(proxy.hostname) ? 'localhost' : proxy.hostname
|
167 | proxy.port = _.isUndefined(proxy.port) ? 9875 : proxy.port
|
168 |
|
169 |
|
170 | proxy.protocol = (proxy.protocol || 'http').split(':')[0] + ':'
|
171 | if (proxy.protocol.match(/https?:/) === null) {
|
172 | log.warn('"%s" is not a supported upstream proxy protocol, defaulting to "http:"',
|
173 | proxy.protocol)
|
174 | proxy.protocol = 'http:'
|
175 | }
|
176 | }
|
177 |
|
178 |
|
179 | config.protocol = (config.protocol || 'http').split(':')[0] + ':'
|
180 | if (config.protocol.match(/https?:/) === null) {
|
181 | log.warn('"%s" is not a supported protocol, defaulting to "http:"',
|
182 | config.protocol)
|
183 | config.protocol = 'http:'
|
184 | }
|
185 |
|
186 | if (config.proxies && config.proxies.hasOwnProperty(config.urlRoot)) {
|
187 | log.warn('"%s" is proxied, you should probably change urlRoot to avoid conflicts',
|
188 | config.urlRoot)
|
189 | }
|
190 |
|
191 | if (config.singleRun && config.autoWatch) {
|
192 | log.debug('autoWatch set to false, because of singleRun')
|
193 | config.autoWatch = false
|
194 | }
|
195 |
|
196 | if (config.runInParent) {
|
197 | log.debug('useIframe set to false, because using runInParent')
|
198 | config.useIframe = false
|
199 | }
|
200 |
|
201 | if (!config.singleRun && !config.useIframe && config.runInParent) {
|
202 | log.debug('singleRun set to true, because using runInParent')
|
203 | config.singleRun = true
|
204 | }
|
205 |
|
206 | if (helper.isString(config.reporters)) {
|
207 | config.reporters = config.reporters.split(',')
|
208 | }
|
209 |
|
210 | if (config.client && config.client.args && !Array.isArray(config.client.args)) {
|
211 | throw new Error('Invalid configuration: client.args must be an array of strings')
|
212 | }
|
213 |
|
214 | if (config.browsers && Array.isArray(config.browsers) === false) {
|
215 | throw new TypeError('Invalid configuration: browsers option must be an array')
|
216 | }
|
217 |
|
218 | if (config.formatError && !helper.isFunction(config.formatError)) {
|
219 | throw new TypeError('Invalid configuration: formatError option must be a function.')
|
220 | }
|
221 |
|
222 | if (config.processKillTimeout && !helper.isNumber(config.processKillTimeout)) {
|
223 | throw new TypeError('Invalid configuration: processKillTimeout option must be a number.')
|
224 | }
|
225 |
|
226 | var defaultClient = config.defaultClient || {}
|
227 | Object.keys(defaultClient).forEach(function (key) {
|
228 | var option = config.client[key]
|
229 | config.client[key] = helper.isDefined(option) ? option : defaultClient[key]
|
230 | })
|
231 |
|
232 |
|
233 | var preprocessors = config.preprocessors || {}
|
234 | var normalizedPreprocessors = config.preprocessors = Object.create(null)
|
235 |
|
236 | Object.keys(preprocessors).forEach(function (pattern) {
|
237 | var normalizedPattern = helper.normalizeWinPath(basePathResolve(pattern))
|
238 |
|
239 | normalizedPreprocessors[normalizedPattern] = helper.isString(preprocessors[pattern])
|
240 | ? [preprocessors[pattern]] : preprocessors[pattern]
|
241 | })
|
242 |
|
243 |
|
244 | var module = Object.create(null)
|
245 | var hasSomeInlinedPlugin = false
|
246 | var types = ['launcher', 'preprocessor', 'reporter']
|
247 |
|
248 | types.forEach(function (type) {
|
249 | var definitions = config['custom' + helper.ucFirst(type) + 's'] || {}
|
250 |
|
251 | Object.keys(definitions).forEach(function (name) {
|
252 | var definition = definitions[name]
|
253 |
|
254 | if (!helper.isObject(definition)) {
|
255 | return log.warn('Can not define %s %s. Definition has to be an object.', type, name)
|
256 | }
|
257 |
|
258 | if (!helper.isString(definition.base)) {
|
259 | return log.warn('Can not define %s %s. Missing base %s.', type, name, type)
|
260 | }
|
261 |
|
262 | var token = type + ':' + definition.base
|
263 | var locals = {
|
264 | args: ['value', definition]
|
265 | }
|
266 |
|
267 | module[type + ':' + name] = ['factory', function (injector) {
|
268 | var plugin = injector.createChild([locals], [token]).get(token)
|
269 | if (type === 'launcher' && helper.isDefined(definition.displayName)) {
|
270 | plugin.displayName = definition.displayName
|
271 | }
|
272 | return plugin
|
273 | }]
|
274 | hasSomeInlinedPlugin = true
|
275 | })
|
276 | })
|
277 |
|
278 | if (hasSomeInlinedPlugin) {
|
279 | config.plugins.push(module)
|
280 | }
|
281 |
|
282 | return config
|
283 | }
|
284 |
|
285 | var Config = function () {
|
286 | var config = this
|
287 |
|
288 | this.LOG_DISABLE = constant.LOG_DISABLE
|
289 | this.LOG_ERROR = constant.LOG_ERROR
|
290 | this.LOG_WARN = constant.LOG_WARN
|
291 | this.LOG_INFO = constant.LOG_INFO
|
292 | this.LOG_DEBUG = constant.LOG_DEBUG
|
293 |
|
294 | this.set = function (newConfig) {
|
295 | _.mergeWith(config, newConfig, function (obj, src) {
|
296 |
|
297 | if (_.isArray(src)) {
|
298 | return src
|
299 | }
|
300 | })
|
301 | }
|
302 |
|
303 |
|
304 | this.frameworks = []
|
305 | this.protocol = 'http:'
|
306 | this.port = constant.DEFAULT_PORT
|
307 | this.listenAddress = constant.DEFAULT_LISTEN_ADDR
|
308 | this.hostname = constant.DEFAULT_HOSTNAME
|
309 | this.httpsServerConfig = {}
|
310 | this.basePath = ''
|
311 | this.files = []
|
312 | this.browserConsoleLogOptions = {
|
313 | level: 'debug',
|
314 | format: '%b %T: %m',
|
315 | terminal: true
|
316 | }
|
317 | this.customContextFile = null
|
318 | this.customDebugFile = null
|
319 | this.customClientContextFile = null
|
320 | this.exclude = []
|
321 | this.logLevel = constant.LOG_INFO
|
322 | this.colors = true
|
323 | this.autoWatch = true
|
324 | this.autoWatchBatchDelay = 250
|
325 | this.restartOnFileChange = false
|
326 | this.usePolling = process.platform === 'linux'
|
327 | this.reporters = ['progress']
|
328 | this.singleRun = false
|
329 | this.browsers = []
|
330 | this.captureTimeout = 60000
|
331 | this.proxies = {}
|
332 | this.proxyValidateSSL = true
|
333 | this.preprocessors = {}
|
334 | this.urlRoot = '/'
|
335 | this.upstreamProxy = undefined
|
336 | this.reportSlowerThan = 0
|
337 | this.loggers = [constant.CONSOLE_APPENDER]
|
338 | this.transports = ['polling', 'websocket']
|
339 | this.forceJSONP = false
|
340 | this.plugins = ['karma-*']
|
341 | this.defaultClient = this.client = {
|
342 | args: [],
|
343 | useIframe: true,
|
344 | runInParent: false,
|
345 | captureConsole: true,
|
346 | clearContext: true
|
347 | }
|
348 | this.browserDisconnectTimeout = 2000
|
349 | this.browserDisconnectTolerance = 0
|
350 | this.browserNoActivityTimeout = 10000
|
351 | this.processKillTimeout = 2000
|
352 | this.concurrency = Infinity
|
353 | this.failOnEmptyTestSuite = true
|
354 | this.retryLimit = 2
|
355 | this.detached = false
|
356 | this.crossOriginAttribute = true
|
357 | }
|
358 |
|
359 | var CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' +
|
360 | ' config.set({\n' +
|
361 | ' // your config\n' +
|
362 | ' });\n' +
|
363 | ' };\n'
|
364 |
|
365 | var parseConfig = function (configFilePath, cliOptions) {
|
366 | var configModule
|
367 | if (configFilePath) {
|
368 |
|
369 | try {
|
370 | configModule = require(configFilePath)
|
371 | if (typeof configModule === 'object' && typeof configModule.default !== 'undefined') {
|
372 | configModule = configModule.default
|
373 | }
|
374 | } catch (e) {
|
375 | if (e.code === 'MODULE_NOT_FOUND' && e.message.indexOf(configFilePath) !== -1) {
|
376 | log.error('File %s does not exist!', configFilePath)
|
377 | } else {
|
378 | log.error('Invalid config file!\n ' + e.stack)
|
379 |
|
380 | var extension = path.extname(configFilePath)
|
381 | if (extension === '.coffee' && !COFFEE_SCRIPT_AVAILABLE) {
|
382 | log.error('You need to install CoffeeScript.\n' +
|
383 | ' npm install coffee-script --save-dev')
|
384 | } else if (extension === '.ls' && !LIVE_SCRIPT_AVAILABLE) {
|
385 | log.error('You need to install LiveScript.\n' +
|
386 | ' npm install LiveScript --save-dev')
|
387 | } else if (extension === '.ts' && !TYPE_SCRIPT_AVAILABLE) {
|
388 | log.error('You need to install TypeScript.\n' +
|
389 | ' npm install typescript ts-node --save-dev')
|
390 | }
|
391 | }
|
392 | return process.exit(1)
|
393 | }
|
394 | if (!helper.isFunction(configModule)) {
|
395 | log.error('Config file must export a function!\n' + CONFIG_SYNTAX_HELP)
|
396 | return process.exit(1)
|
397 | }
|
398 | } else {
|
399 |
|
400 | configModule = function () {}
|
401 | }
|
402 |
|
403 | var config = new Config()
|
404 |
|
405 |
|
406 |
|
407 | var defaultHostname = config.hostname
|
408 | config.hostname = null
|
409 | var defaultListenAddress = config.listenAddress
|
410 | config.listenAddress = null
|
411 |
|
412 |
|
413 | config.set(cliOptions)
|
414 |
|
415 | try {
|
416 | configModule(config)
|
417 | } catch (e) {
|
418 | log.error('Error in config file!\n', e)
|
419 | return process.exit(1)
|
420 | }
|
421 |
|
422 |
|
423 | config.set(cliOptions)
|
424 |
|
425 |
|
426 | if (config.hostname === null && config.listenAddress !== null) {
|
427 | log.warn('ListenAddress was set to %s but hostname was left as the default: ' +
|
428 | '%s. If your browsers fail to connect, consider changing the hostname option.',
|
429 | config.listenAddress, defaultHostname)
|
430 | }
|
431 |
|
432 | if (config.hostname === null) {
|
433 | config.hostname = defaultHostname
|
434 | }
|
435 | if (config.listenAddress === null) {
|
436 | config.listenAddress = defaultListenAddress
|
437 | }
|
438 |
|
439 |
|
440 | logger.setup(config.logLevel, config.colors, config.loggers)
|
441 |
|
442 | if (configFilePath) {
|
443 | log.debug('Loading config %s', configFilePath)
|
444 | } else {
|
445 | log.debug('No config file specified.')
|
446 | }
|
447 |
|
448 | return normalizeConfig(config, configFilePath)
|
449 | }
|
450 |
|
451 |
|
452 | exports.parseConfig = parseConfig
|
453 | exports.Pattern = Pattern
|
454 | exports.createPatternObject = createPatternObject
|
455 | exports.Config = Config
|