run.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. 'use strict';
  2. /**
  3. * Definition for Mocha's default ("run tests") command
  4. *
  5. * @module
  6. * @private
  7. */
  8. const symbols = require('log-symbols');
  9. const pc = require('picocolors');
  10. const Mocha = require('../mocha');
  11. const {
  12. createUnsupportedError,
  13. createInvalidArgumentValueError,
  14. createMissingArgumentError
  15. } = require('../errors');
  16. const {
  17. list,
  18. handleRequires,
  19. validateLegacyPlugin,
  20. runMocha
  21. } = require('./run-helpers');
  22. const {ONE_AND_DONES, ONE_AND_DONE_ARGS} = require('./one-and-dones');
  23. const debug = require('debug')('mocha:cli:run');
  24. const defaults = require('../mocharc.json');
  25. const {types, aliases} = require('./run-option-metadata');
  26. /**
  27. * Logical option groups
  28. * @constant
  29. */
  30. const GROUPS = {
  31. FILES: 'File Handling',
  32. FILTERS: 'Test Filters',
  33. NODEJS: 'Node.js & V8',
  34. OUTPUT: 'Reporting & Output',
  35. RULES: 'Rules & Behavior',
  36. CONFIG: 'Configuration'
  37. };
  38. exports.command = ['$0 [spec..]', 'inspect'];
  39. exports.describe = 'Run tests with Mocha';
  40. exports.builder = yargs =>
  41. yargs
  42. .options({
  43. 'allow-uncaught': {
  44. description: 'Allow uncaught errors to propagate',
  45. group: GROUPS.RULES
  46. },
  47. 'async-only': {
  48. description:
  49. 'Require all tests to use a callback (async) or return a Promise',
  50. group: GROUPS.RULES
  51. },
  52. bail: {
  53. description: 'Abort ("bail") after first test failure',
  54. group: GROUPS.RULES
  55. },
  56. 'check-leaks': {
  57. description: 'Check for global variable leaks',
  58. group: GROUPS.RULES
  59. },
  60. color: {
  61. description: 'Force-enable color output',
  62. group: GROUPS.OUTPUT
  63. },
  64. config: {
  65. config: true,
  66. defaultDescription: '(nearest rc file)',
  67. description: 'Path to config file',
  68. group: GROUPS.CONFIG
  69. },
  70. delay: {
  71. description: 'Delay initial execution of root suite',
  72. group: GROUPS.RULES
  73. },
  74. diff: {
  75. default: true,
  76. description: 'Show diff on failure',
  77. group: GROUPS.OUTPUT
  78. },
  79. 'dry-run': {
  80. description: 'Report tests without executing them',
  81. group: GROUPS.RULES
  82. },
  83. exit: {
  84. description: 'Force Mocha to quit after tests complete',
  85. group: GROUPS.RULES
  86. },
  87. extension: {
  88. default: defaults.extension,
  89. description: 'File extension(s) to load',
  90. group: GROUPS.FILES,
  91. requiresArg: true,
  92. coerce: list
  93. },
  94. 'pass-on-failing-test-suite': {
  95. default: false,
  96. description: 'Not fail test run if tests were failed',
  97. group: GROUPS.RULES
  98. },
  99. 'fail-zero': {
  100. description: 'Fail test run if no test(s) encountered',
  101. group: GROUPS.RULES
  102. },
  103. fgrep: {
  104. conflicts: 'grep',
  105. description: 'Only run tests containing this string',
  106. group: GROUPS.FILTERS,
  107. requiresArg: true
  108. },
  109. file: {
  110. defaultDescription: '(none)',
  111. description:
  112. 'Specify file(s) to be loaded prior to root suite execution',
  113. group: GROUPS.FILES,
  114. normalize: true,
  115. requiresArg: true
  116. },
  117. 'forbid-only': {
  118. description: 'Fail if exclusive test(s) encountered',
  119. group: GROUPS.RULES
  120. },
  121. 'forbid-pending': {
  122. description: 'Fail if pending test(s) encountered',
  123. group: GROUPS.RULES
  124. },
  125. 'full-trace': {
  126. description: 'Display full stack traces',
  127. group: GROUPS.OUTPUT
  128. },
  129. global: {
  130. coerce: list,
  131. description: 'List of allowed global variables',
  132. group: GROUPS.RULES,
  133. requiresArg: true
  134. },
  135. grep: {
  136. coerce: value => (!value ? null : value),
  137. conflicts: 'fgrep',
  138. description: 'Only run tests matching this string or regexp',
  139. group: GROUPS.FILTERS,
  140. requiresArg: true
  141. },
  142. ignore: {
  143. defaultDescription: '(none)',
  144. description: 'Ignore file(s) or glob pattern(s)',
  145. group: GROUPS.FILES,
  146. requiresArg: true
  147. },
  148. 'inline-diffs': {
  149. description:
  150. 'Display actual/expected differences inline within each string',
  151. group: GROUPS.OUTPUT
  152. },
  153. invert: {
  154. description: 'Inverts --grep and --fgrep matches',
  155. group: GROUPS.FILTERS
  156. },
  157. jobs: {
  158. description:
  159. 'Number of concurrent jobs for --parallel; use 1 to run in serial',
  160. defaultDescription: '(number of CPU cores - 1)',
  161. requiresArg: true,
  162. group: GROUPS.RULES
  163. },
  164. 'list-interfaces': {
  165. conflicts: Array.from(ONE_AND_DONE_ARGS).filter(arg => arg !== "list-interfaces"),
  166. description: 'List built-in user interfaces & exit'
  167. },
  168. 'list-reporters': {
  169. conflicts: Array.from(ONE_AND_DONE_ARGS).filter(arg => arg !== "list-reporters"),
  170. description: 'List built-in reporters & exit'
  171. },
  172. 'no-colors': {
  173. description: 'Force-disable color output',
  174. group: GROUPS.OUTPUT,
  175. hidden: true
  176. },
  177. 'node-option': {
  178. description: 'Node or V8 option (no leading "--")',
  179. group: GROUPS.CONFIG
  180. },
  181. package: {
  182. description: 'Path to package.json for config',
  183. group: GROUPS.CONFIG,
  184. normalize: true,
  185. requiresArg: true
  186. },
  187. parallel: {
  188. description: 'Run tests in parallel',
  189. group: GROUPS.RULES
  190. },
  191. 'posix-exit-codes': {
  192. description: 'Use POSIX and UNIX shell exit codes as Mocha\'s return value',
  193. group: GROUPS.RULES
  194. },
  195. recursive: {
  196. description: 'Look for tests in subdirectories',
  197. group: GROUPS.FILES
  198. },
  199. reporter: {
  200. default: defaults.reporter,
  201. description: 'Specify reporter to use',
  202. group: GROUPS.OUTPUT,
  203. requiresArg: true
  204. },
  205. 'reporter-option': {
  206. coerce: opts =>
  207. list(opts).reduce((acc, opt) => {
  208. const pair = opt.split('=');
  209. if (pair.length > 2 || !pair.length) {
  210. throw createInvalidArgumentValueError(
  211. `invalid reporter option '${opt}'`,
  212. '--reporter-option',
  213. opt,
  214. 'expected "key=value" format'
  215. );
  216. }
  217. acc[pair[0]] = pair.length === 2 ? pair[1] : true;
  218. return acc;
  219. }, {}),
  220. description: 'Reporter-specific options (<k=v,[k1=v1,..]>)',
  221. group: GROUPS.OUTPUT,
  222. requiresArg: true
  223. },
  224. require: {
  225. defaultDescription: '(none)',
  226. description: 'Require module',
  227. group: GROUPS.FILES,
  228. requiresArg: true
  229. },
  230. retries: {
  231. description: 'Retry failed tests this many times',
  232. group: GROUPS.RULES
  233. },
  234. slow: {
  235. default: defaults.slow,
  236. description: 'Specify "slow" test threshold (in milliseconds)',
  237. group: GROUPS.RULES
  238. },
  239. sort: {
  240. description: 'Sort test files',
  241. group: GROUPS.FILES
  242. },
  243. timeout: {
  244. default: defaults.timeout,
  245. description: 'Specify test timeout threshold (in milliseconds)',
  246. group: GROUPS.RULES
  247. },
  248. ui: {
  249. default: defaults.ui,
  250. description: 'Specify user interface',
  251. group: GROUPS.RULES,
  252. requiresArg: true
  253. },
  254. watch: {
  255. description: 'Watch files in the current working directory for changes',
  256. group: GROUPS.FILES
  257. },
  258. 'watch-files': {
  259. description: 'List of paths or globs to watch',
  260. group: GROUPS.FILES,
  261. requiresArg: true,
  262. coerce: list
  263. },
  264. 'watch-ignore': {
  265. description: 'List of paths or globs to exclude from watching',
  266. group: GROUPS.FILES,
  267. requiresArg: true,
  268. coerce: list,
  269. default: defaults['watch-ignore']
  270. }
  271. })
  272. .positional('spec', {
  273. default: ['test'],
  274. description: 'One or more files, directories, or globs to test',
  275. type: 'array'
  276. })
  277. .check(argv => {
  278. // "one-and-dones"; let yargs handle help and version
  279. Object.keys(ONE_AND_DONES).forEach(opt => {
  280. if (argv[opt]) {
  281. ONE_AND_DONES[opt].call(null, yargs);
  282. process.exit();
  283. }
  284. });
  285. // yargs.implies() isn't flexible enough to handle this
  286. if (argv.invert && !('fgrep' in argv || 'grep' in argv)) {
  287. throw createMissingArgumentError(
  288. '"--invert" requires one of "--fgrep <str>" or "--grep <regexp>"',
  289. '--fgrep|--grep',
  290. 'string|regexp'
  291. );
  292. }
  293. if (argv.parallel) {
  294. // yargs.conflicts() can't deal with `--file foo.js --no-parallel`, either
  295. if (argv.file) {
  296. throw createUnsupportedError(
  297. '--parallel runs test files in a non-deterministic order, and is mutually exclusive with --file'
  298. );
  299. }
  300. // or this
  301. if (argv.sort) {
  302. throw createUnsupportedError(
  303. '--parallel runs test files in a non-deterministic order, and is mutually exclusive with --sort'
  304. );
  305. }
  306. if (argv.reporter === 'progress') {
  307. throw createUnsupportedError(
  308. '--reporter=progress is mutually exclusive with --parallel'
  309. );
  310. }
  311. if (argv.reporter === 'markdown') {
  312. throw createUnsupportedError(
  313. '--reporter=markdown is mutually exclusive with --parallel'
  314. );
  315. }
  316. if (argv.reporter === 'json-stream') {
  317. throw createUnsupportedError(
  318. '--reporter=json-stream is mutually exclusive with --parallel'
  319. );
  320. }
  321. }
  322. if (argv.compilers) {
  323. throw createUnsupportedError(
  324. `--compilers is DEPRECATED and no longer supported.
  325. See https://github.com/mochajs/mocha/wiki/compilers-deprecation for migration information.`
  326. );
  327. }
  328. if (argv.opts) {
  329. throw createUnsupportedError(
  330. `--opts: configuring Mocha via 'mocha.opts' is DEPRECATED and no longer supported.
  331. Please use a configuration file instead.`
  332. );
  333. }
  334. return true;
  335. })
  336. .middleware(async (argv, yargs) => {
  337. // currently a failing middleware does not work nicely with yargs' `fail()`.
  338. try {
  339. // load requires first, because it can impact "plugin" validation
  340. const plugins = await handleRequires(argv.require);
  341. validateLegacyPlugin(argv, 'reporter', Mocha.reporters);
  342. validateLegacyPlugin(argv, 'ui', Mocha.interfaces);
  343. Object.assign(argv, plugins);
  344. } catch (err) {
  345. // this could be a bad --require, bad reporter, ui, etc.
  346. console.error(`\n${symbols.error} ${pc.red('ERROR:')}`, err);
  347. yargs.exit(1);
  348. }
  349. })
  350. .array(types.array)
  351. .boolean(types.boolean)
  352. .string(types.string)
  353. .number(types.number)
  354. .alias(aliases);
  355. exports.handler = async function (argv) {
  356. debug('post-yargs config', argv);
  357. const mocha = new Mocha(argv);
  358. try {
  359. await runMocha(mocha, argv);
  360. } catch (err) {
  361. console.error('\n Exception during run:', err);
  362. process.exit(1);
  363. }
  364. };