run-helpers.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. 'use strict';
  2. /**
  3. * Helper scripts for the `run` command
  4. * @see module:lib/cli/run
  5. * @module
  6. * @private
  7. */
  8. const fs = require('fs');
  9. const path = require('path');
  10. const ansi = require('ansi-colors');
  11. const debug = require('debug')('mocha:cli:run:helpers');
  12. const minimatch = require('minimatch');
  13. const Context = require('../context');
  14. const Mocha = require('../mocha');
  15. const utils = require('../utils');
  16. const cwd = (exports.cwd = process.cwd());
  17. /**
  18. * Exits Mocha when tests + code under test has finished execution (default)
  19. * @param {number} code - Exit code; typically # of failures
  20. * @ignore
  21. * @private
  22. */
  23. const exitMochaLater = code => {
  24. process.on('exit', () => {
  25. process.exitCode = Math.min(code, 255);
  26. });
  27. };
  28. /**
  29. * Exits Mocha when Mocha itself has finished execution, regardless of
  30. * what the tests or code under test is doing.
  31. * @param {number} code - Exit code; typically # of failures
  32. * @ignore
  33. * @private
  34. */
  35. const exitMocha = code => {
  36. const clampedCode = Math.min(code, 255);
  37. let draining = 0;
  38. // Eagerly set the process's exit code in case stream.write doesn't
  39. // execute its callback before the process terminates.
  40. process.exitCode = clampedCode;
  41. // flush output for Node.js Windows pipe bug
  42. // https://github.com/joyent/node/issues/6247 is just one bug example
  43. // https://github.com/visionmedia/mocha/issues/333 has a good discussion
  44. const done = () => {
  45. if (!draining--) {
  46. process.exit(clampedCode);
  47. }
  48. };
  49. const streams = [process.stdout, process.stderr];
  50. streams.forEach(stream => {
  51. // submit empty write request and wait for completion
  52. draining += 1;
  53. stream.write('', done);
  54. });
  55. done();
  56. };
  57. /**
  58. * Hide the cursor.
  59. * @ignore
  60. * @private
  61. */
  62. const hideCursor = () => {
  63. process.stdout.write('\u001b[?25l');
  64. };
  65. /**
  66. * Show the cursor.
  67. * @ignore
  68. * @private
  69. */
  70. const showCursor = () => {
  71. process.stdout.write('\u001b[?25h');
  72. };
  73. /**
  74. * Stop cursor business
  75. * @private
  76. */
  77. const stop = () => {
  78. process.stdout.write('\u001b[2K');
  79. };
  80. /**
  81. * Coerce a comma-delimited string (or array thereof) into a flattened array of
  82. * strings
  83. * @param {string|string[]} str - Value to coerce
  84. * @returns {string[]} Array of strings
  85. * @private
  86. */
  87. exports.list = str =>
  88. Array.isArray(str) ? exports.list(str.join(',')) : str.split(/ *, */);
  89. /**
  90. * `require()` the modules as required by `--require <require>`
  91. * @param {string[]} requires - Modules to require
  92. * @private
  93. */
  94. exports.handleRequires = (requires = []) => {
  95. requires.forEach(mod => {
  96. let modpath = mod;
  97. if (fs.existsSync(mod, {cwd}) || fs.existsSync(`${mod}.js`, {cwd})) {
  98. modpath = path.resolve(mod);
  99. debug(`resolved ${mod} to ${modpath}`);
  100. }
  101. require(modpath);
  102. debug(`loaded require "${mod}"`);
  103. });
  104. };
  105. /**
  106. * Smash together an array of test files in the correct order
  107. * @param {Object} [opts] - Options
  108. * @param {string[]} [opts.extension] - File extensions to use
  109. * @param {string[]} [opts.spec] - Files, dirs, globs to run
  110. * @param {string[]} [opts.exclude] - Files, dirs, globs to exclude
  111. * @param {boolean} [opts.recursive=false] - Find files recursively
  112. * @param {boolean} [opts.sort=false] - Sort test files
  113. * @returns {string[]} List of files to test
  114. * @private
  115. */
  116. exports.handleFiles = ({
  117. exclude = [],
  118. extension = [],
  119. file = [],
  120. recursive = false,
  121. sort = false,
  122. spec = []
  123. } = {}) => {
  124. let files = [];
  125. const unmatched = [];
  126. spec.forEach(arg => {
  127. let newFiles;
  128. try {
  129. newFiles = utils.lookupFiles(arg, extension, recursive);
  130. } catch (err) {
  131. if (err.code === 'ERR_MOCHA_NO_FILES_MATCH_PATTERN') {
  132. unmatched.push({message: err.message, pattern: err.pattern});
  133. return;
  134. }
  135. throw err;
  136. }
  137. if (typeof newFiles !== 'undefined') {
  138. if (typeof newFiles === 'string') {
  139. newFiles = [newFiles];
  140. }
  141. newFiles = newFiles.filter(fileName =>
  142. exclude.every(pattern => !minimatch(fileName, pattern))
  143. );
  144. }
  145. files = files.concat(newFiles);
  146. });
  147. if (!files.length) {
  148. // give full message details when only 1 file is missing
  149. const noneFoundMsg =
  150. unmatched.length === 1
  151. ? `Error: No test files found: ${JSON.stringify(unmatched[0].pattern)}` // stringify to print escaped characters raw
  152. : 'Error: No test files found';
  153. console.error(ansi.red(noneFoundMsg));
  154. process.exit(1);
  155. } else {
  156. // print messages as an warning
  157. unmatched.forEach(warning => {
  158. console.warn(ansi.yellow(`Warning: ${warning.message}`));
  159. });
  160. }
  161. const fileArgs = file.map(filepath => path.resolve(filepath));
  162. files = files.map(filepath => path.resolve(filepath));
  163. // ensure we don't sort the stuff from fileArgs; order is important!
  164. if (sort) {
  165. files.sort();
  166. }
  167. // add files given through --file to be ran first
  168. files = fileArgs.concat(files);
  169. debug('files (in order): ', files);
  170. return files;
  171. };
  172. /**
  173. * Give Mocha files and tell it to run
  174. * @param {Mocha} mocha - Mocha instance
  175. * @param {Options} [opts] - Options
  176. * @param {string[]} [opts.files] - List of test files
  177. * @param {boolean} [opts.exit] - Whether or not to force-exit after tests are complete
  178. * @returns {Runner}
  179. * @private
  180. */
  181. exports.singleRun = (mocha, {files = [], exit = false} = {}) => {
  182. mocha.files = files;
  183. return mocha.run(exit ? exitMocha : exitMochaLater);
  184. };
  185. /**
  186. * Run Mocha in "watch" mode
  187. * @param {Mocha} mocha - Mocha instance
  188. * @param {Object} [opts] - Options
  189. * @param {string[]} [opts.extension] - List of extensions to watch
  190. * @param {string|RegExp} [opts.grep] - Grep for test titles
  191. * @param {string} [opts.ui=bdd] - User interface
  192. * @param {string[]} [files] - Array of test files
  193. * @private
  194. */
  195. exports.watchRun = (
  196. mocha,
  197. {extension = ['js'], grep = '', ui = 'bdd', files = []} = {}
  198. ) => {
  199. let runner;
  200. console.log();
  201. hideCursor();
  202. process.on('SIGINT', () => {
  203. showCursor();
  204. console.log('\n');
  205. process.exit(130);
  206. });
  207. const watchFiles = utils.files(cwd, extension);
  208. let runAgain = false;
  209. const loadAndRun = () => {
  210. try {
  211. mocha.files = files;
  212. runAgain = false;
  213. runner = mocha.run(() => {
  214. runner = null;
  215. if (runAgain) {
  216. rerun();
  217. }
  218. });
  219. } catch (e) {
  220. console.log(e.stack);
  221. }
  222. };
  223. const purge = () => {
  224. watchFiles.forEach(Mocha.unloadFile);
  225. };
  226. loadAndRun();
  227. const rerun = () => {
  228. purge();
  229. stop();
  230. if (!grep) {
  231. mocha.grep(null);
  232. }
  233. mocha.suite = mocha.suite.clone();
  234. mocha.suite.ctx = new Context();
  235. mocha.ui(ui);
  236. loadAndRun();
  237. };
  238. utils.watch(watchFiles, () => {
  239. runAgain = true;
  240. if (runner) {
  241. runner.abort();
  242. } else {
  243. rerun();
  244. }
  245. });
  246. };
  247. /**
  248. * Actually run tests
  249. * @param {Mocha} mocha - Mocha instance
  250. * @param {Object} [opts] - Options
  251. * @param {boolean} [opts.watch=false] - Enable watch mode
  252. * @param {string[]} [opts.extension] - List of extensions to watch
  253. * @param {string|RegExp} [opts.grep] - Grep for test titles
  254. * @param {string} [opts.ui=bdd] - User interface
  255. * @param {boolean} [opts.exit=false] - Force-exit Mocha when tests done
  256. * @param {string[]} [files] - Array of test files
  257. * @private
  258. */
  259. exports.runMocha = (
  260. mocha,
  261. {watch = false, extension = ['js'], grep = '', ui = 'bdd', exit = false} = {},
  262. files = []
  263. ) => {
  264. if (watch) {
  265. exports.watchRun(mocha, {extension, grep, ui, files});
  266. } else {
  267. exports.singleRun(mocha, {files, exit});
  268. }
  269. };
  270. /**
  271. * Used for `--reporter` and `--ui`. Ensures there's only one, and asserts
  272. * that it actually exists.
  273. * @todo XXX This must get run after requires are processed, as it'll prevent
  274. * interfaces from loading.
  275. * @param {Object} opts - Options object
  276. * @param {string} key - Resolvable module name or path
  277. * @param {Object} [map] - An object perhaps having key `key`
  278. * @private
  279. */
  280. exports.validatePlugin = (opts, key, map = {}) => {
  281. if (Array.isArray(opts[key])) {
  282. throw new TypeError(`"--${key} <${key}>" can only be specified once`);
  283. }
  284. const unknownError = () => new Error(`Unknown "${key}": ${opts[key]}`);
  285. if (!map[opts[key]]) {
  286. try {
  287. opts[key] = require(opts[key]);
  288. } catch (err) {
  289. if (err.code === 'MODULE_NOT_FOUND') {
  290. // Try to load reporters from a path (absolute or relative)
  291. try {
  292. opts[key] = require(path.resolve(process.cwd(), opts[key]));
  293. } catch (err) {
  294. throw unknownError();
  295. }
  296. } else {
  297. throw unknownError();
  298. }
  299. }
  300. }
  301. };