cli.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. 'use strict'
  2. const path = require('path')
  3. const yargs = require('yargs')
  4. const fs = require('graceful-fs')
  5. const Server = require('./server')
  6. const helper = require('./helper')
  7. const constant = require('./constants')
  8. const cfg = require('./config')
  9. function processArgs (argv, options, fs, path) {
  10. Object.getOwnPropertyNames(argv).forEach(function (name) {
  11. let argumentValue = argv[name]
  12. if (name !== '_' && name !== '$0') {
  13. if (Array.isArray(argumentValue)) {
  14. argumentValue = argumentValue.pop() // If the same argument is defined multiple times, override.
  15. }
  16. options[helper.dashToCamel(name)] = argumentValue
  17. }
  18. })
  19. if (helper.isString(options.autoWatch)) {
  20. options.autoWatch = options.autoWatch === 'true'
  21. }
  22. if (helper.isString(options.colors)) {
  23. options.colors = options.colors === 'true'
  24. }
  25. if (helper.isString(options.failOnEmptyTestSuite)) {
  26. options.failOnEmptyTestSuite = options.failOnEmptyTestSuite === 'true'
  27. }
  28. if (helper.isString(options.failOnFailingTestSuite)) {
  29. options.failOnFailingTestSuite = options.failOnFailingTestSuite === 'true'
  30. }
  31. if (helper.isString(options.formatError)) {
  32. let required
  33. try {
  34. required = require(options.formatError)
  35. } catch (err) {
  36. console.error('Could not require formatError: ' + options.formatError, err)
  37. }
  38. // support exports.formatError and module.exports = function
  39. options.formatError = required.formatError || required
  40. if (!helper.isFunction(options.formatError)) {
  41. console.error(`Format error must be a function, got: ${typeof options.formatError}`)
  42. process.exit(1)
  43. }
  44. }
  45. if (helper.isString(options.logLevel)) {
  46. const logConstant = constant['LOG_' + options.logLevel.toUpperCase()]
  47. if (helper.isDefined(logConstant)) {
  48. options.logLevel = logConstant
  49. } else {
  50. console.error('Log level must be one of disable, error, warn, info, or debug.')
  51. process.exit(1)
  52. }
  53. } else if (helper.isDefined(options.logLevel)) {
  54. console.error('Log level must be one of disable, error, warn, info, or debug.')
  55. process.exit(1)
  56. }
  57. if (helper.isString(options.singleRun)) {
  58. options.singleRun = options.singleRun === 'true'
  59. }
  60. if (helper.isString(options.browsers)) {
  61. options.browsers = options.browsers.split(',')
  62. }
  63. if (options.reportSlowerThan === false) {
  64. options.reportSlowerThan = 0
  65. }
  66. if (helper.isString(options.reporters)) {
  67. options.reporters = options.reporters.split(',')
  68. }
  69. if (helper.isString(options.removedFiles)) {
  70. options.removedFiles = options.removedFiles.split(',')
  71. }
  72. if (helper.isString(options.addedFiles)) {
  73. options.addedFiles = options.addedFiles.split(',')
  74. }
  75. if (helper.isString(options.changedFiles)) {
  76. options.changedFiles = options.changedFiles.split(',')
  77. }
  78. if (helper.isString(options.refresh)) {
  79. options.refresh = options.refresh === 'true'
  80. }
  81. let configFile = argv.configFile
  82. if (!configFile) {
  83. // default config file (if exists)
  84. if (fs.existsSync('./karma.conf.js')) {
  85. configFile = './karma.conf.js'
  86. } else if (fs.existsSync('./karma.conf.coffee')) {
  87. configFile = './karma.conf.coffee'
  88. } else if (fs.existsSync('./karma.conf.ts')) {
  89. configFile = './karma.conf.ts'
  90. } else if (fs.existsSync('./.config/karma.conf.js')) {
  91. configFile = './.config/karma.conf.js'
  92. } else if (fs.existsSync('./.config/karma.conf.coffee')) {
  93. configFile = './.config/karma.conf.coffee'
  94. } else if (fs.existsSync('./.config/karma.conf.ts')) {
  95. configFile = './.config/karma.conf.ts'
  96. }
  97. }
  98. options.configFile = configFile ? path.resolve(configFile) : null
  99. if (options.cmd === 'run') {
  100. options.clientArgs = parseClientArgs(process.argv)
  101. }
  102. return options
  103. }
  104. function parseClientArgs (argv) {
  105. // extract any args after '--' as clientArgs
  106. let clientArgs = []
  107. argv = argv.slice(2)
  108. const idx = argv.indexOf('--')
  109. if (idx !== -1) {
  110. clientArgs = argv.slice(idx + 1)
  111. }
  112. return clientArgs
  113. }
  114. // return only args that occur before `--`
  115. function argsBeforeDoubleDash (argv) {
  116. const idx = argv.indexOf('--')
  117. return idx === -1 ? argv : argv.slice(0, idx)
  118. }
  119. function describeRoot () {
  120. return yargs
  121. .usage('Karma - Spectacular Test Runner for JavaScript.\n\n' +
  122. 'Run --help with particular command to see its description and available options.\n\n' +
  123. 'Usage:\n' +
  124. ' $0 <command>')
  125. .command('init [configFile]', 'Initialize a config file.', describeInit)
  126. .command('start [configFile]', 'Start the server / do a single run.', describeStart)
  127. .command('run [configFile]', 'Trigger a test run.', describeRun)
  128. .command('stop [configFile]', 'Stop the server.', describeStop)
  129. .command('completion', 'Shell completion for karma.', describeCompletion)
  130. .demandCommand(1, 'Command not specified.')
  131. .strictCommands()
  132. .describe('help', 'Print usage and options.')
  133. .describe('version', 'Print current version.')
  134. }
  135. function describeInit (yargs) {
  136. yargs
  137. .usage('Karma - Spectacular Test Runner for JavaScript.\n\n' +
  138. 'INIT - Initialize a config file.\n\n' +
  139. 'Usage:\n' +
  140. ' $0 init [configFile]')
  141. .strictCommands(false)
  142. .version(false)
  143. .positional('configFile', {
  144. describe: 'Name of the generated Karma configuration file',
  145. type: 'string'
  146. })
  147. .describe('log-level', '<disable | error | warn | info | debug> Level of logging.')
  148. .describe('colors', 'Use colors when reporting and printing logs.')
  149. .describe('no-colors', 'Do not use colors when reporting or printing logs.')
  150. }
  151. function describeStart (yargs) {
  152. yargs
  153. .usage('Karma - Spectacular Test Runner for JavaScript.\n\n' +
  154. 'START - Start the server / do a single run.\n\n' +
  155. 'Usage:\n' +
  156. ' $0 start [configFile]')
  157. .strictCommands(false)
  158. .version(false)
  159. .positional('configFile', {
  160. describe: 'Path to the Karma configuration file',
  161. type: 'string'
  162. })
  163. .describe('port', '<integer> Port where the server is running.')
  164. .describe('auto-watch', 'Auto watch source files and run on change.')
  165. .describe('detached', 'Detach the server.')
  166. .describe('no-auto-watch', 'Do not watch source files.')
  167. .describe('log-level', '<disable | error | warn | info | debug> Level of logging.')
  168. .describe('colors', 'Use colors when reporting and printing logs.')
  169. .describe('no-colors', 'Do not use colors when reporting or printing logs.')
  170. .describe('reporters', 'List of reporters (available: dots, progress, junit, growl, coverage).')
  171. .describe('browsers', 'List of browsers to start (eg. --browsers Chrome,ChromeCanary,Firefox).')
  172. .describe('capture-timeout', '<integer> Kill browser if does not capture in given time [ms].')
  173. .describe('single-run', 'Run the test when browsers captured and exit.')
  174. .describe('no-single-run', 'Disable single-run.')
  175. .describe('report-slower-than', '<integer> Report tests that are slower than given time [ms].')
  176. .describe('fail-on-empty-test-suite', 'Fail on empty test suite.')
  177. .describe('no-fail-on-empty-test-suite', 'Do not fail on empty test suite.')
  178. .describe('fail-on-failing-test-suite', 'Fail on failing test suite.')
  179. .describe('no-fail-on-failing-test-suite', 'Do not fail on failing test suite.')
  180. .option('format-error', {
  181. describe: 'A path to a file that exports the format function.',
  182. type: 'string'
  183. })
  184. }
  185. function describeRun (yargs) {
  186. yargs
  187. .usage('Karma - Spectacular Test Runner for JavaScript.\n\n' +
  188. 'RUN - Run the tests (requires running server).\n\n' +
  189. 'Usage:\n' +
  190. ' $0 run [configFile] [-- <clientArgs>]')
  191. .strictCommands(false)
  192. .version(false)
  193. .positional('configFile', {
  194. describe: 'Path to the Karma configuration file',
  195. type: 'string'
  196. })
  197. .describe('port', '<integer> Port where the server is listening.')
  198. .describe('no-refresh', 'Do not re-glob all the patterns.')
  199. .describe('fail-on-empty-test-suite', 'Fail on empty test suite.')
  200. .describe('no-fail-on-empty-test-suite', 'Do not fail on empty test suite.')
  201. .describe('log-level', '<disable | error | warn | info | debug> Level of logging.')
  202. .describe('colors', 'Use colors when reporting and printing logs.')
  203. .describe('no-colors', 'Do not use colors when reporting or printing logs.')
  204. .option('removed-files', {
  205. describe: 'Comma-separated paths to removed files. Useful when automatic file watching is disabled.',
  206. type: 'string'
  207. })
  208. .option('changed-files', {
  209. describe: 'Comma-separated paths to changed files. Useful when automatic file watching is disabled.',
  210. type: 'string'
  211. })
  212. .option('added-files', {
  213. describe: 'Comma-separated paths to added files. Useful when automatic file watching is disabled.',
  214. type: 'string'
  215. })
  216. }
  217. function describeStop (yargs) {
  218. yargs
  219. .usage('Karma - Spectacular Test Runner for JavaScript.\n\n' +
  220. 'STOP - Stop the server (requires running server).\n\n' +
  221. 'Usage:\n' +
  222. ' $0 stop [configFile]')
  223. .strictCommands(false)
  224. .version(false)
  225. .positional('configFile', {
  226. describe: 'Path to the Karma configuration file',
  227. type: 'string'
  228. })
  229. .describe('port', '<integer> Port where the server is listening.')
  230. .describe('log-level', '<disable | error | warn | info | debug> Level of logging.')
  231. }
  232. function describeCompletion (yargs) {
  233. yargs
  234. .usage('Karma - Spectacular Test Runner for JavaScript.\n\n' +
  235. 'COMPLETION - Bash/ZSH completion for karma.\n\n' +
  236. 'Installation:\n' +
  237. ' $0 completion >> ~/.bashrc')
  238. .version(false)
  239. }
  240. function printRunnerProgress (data) {
  241. process.stdout.write(data)
  242. }
  243. exports.process = () => {
  244. const argv = describeRoot().parse(argsBeforeDoubleDash(process.argv.slice(2)))
  245. return processArgs(argv, { cmd: argv._.shift() }, fs, path)
  246. }
  247. exports.run = async () => {
  248. const cliOptions = exports.process()
  249. const cmd = cliOptions.cmd // prevent config from changing the command
  250. const cmdNeedsConfig = cmd === 'start' || cmd === 'run' || cmd === 'stop'
  251. if (cmdNeedsConfig) {
  252. let config
  253. try {
  254. config = await cfg.parseConfig(
  255. cliOptions.configFile,
  256. cliOptions,
  257. {
  258. promiseConfig: true,
  259. throwErrors: true
  260. }
  261. )
  262. } catch (karmaConfigException) {
  263. // The reject reason/exception isn't used to log a message since
  264. // parseConfig already calls a configured logger method with an almost
  265. // identical message.
  266. // The `run` function is a private application, not a public API. We don't
  267. // need to worry about process.exit vs throw vs promise rejection here.
  268. process.exit(1)
  269. }
  270. switch (cmd) {
  271. case 'start': {
  272. const server = new Server(config)
  273. await server.start()
  274. return server
  275. }
  276. case 'run':
  277. return require('./runner')
  278. .run(config)
  279. .on('progress', printRunnerProgress)
  280. case 'stop':
  281. return require('./stopper').stop(config)
  282. }
  283. } else {
  284. switch (cmd) {
  285. case 'init':
  286. return require('./init').init(cliOptions)
  287. case 'completion':
  288. return require('./completion').completion(cliOptions)
  289. }
  290. }
  291. }
  292. // just for testing
  293. exports.processArgs = processArgs
  294. exports.parseClientArgs = parseClientArgs
  295. exports.argsBeforeDoubleDash = argsBeforeDoubleDash