runnable.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. 'use strict';
  2. var EventEmitter = require('node:events').EventEmitter;
  3. var Pending = require('./pending');
  4. var debug = require('debug')('mocha:runnable');
  5. var milliseconds = require('ms');
  6. var utils = require('./utils');
  7. const {
  8. createInvalidExceptionError,
  9. createMultipleDoneError,
  10. createTimeoutError
  11. } = require('./errors');
  12. /**
  13. * Save timer references to avoid Sinon interfering (see GH-237).
  14. * @private
  15. */
  16. var Date = global.Date;
  17. var setTimeout = global.setTimeout;
  18. var clearTimeout = global.clearTimeout;
  19. var toString = Object.prototype.toString;
  20. var MAX_TIMEOUT = Math.pow(2, 31) - 1;
  21. module.exports = Runnable;
  22. // "Additional properties" doc comment added for hosted docs (mochajs.org/api)
  23. /**
  24. * Initialize a new `Runnable` with the given `title` and callback `fn`.
  25. * Additional properties, like `getFullTitle()` and `slow()`, can be viewed in the `Runnable` source.
  26. *
  27. * @class
  28. * @extends external:EventEmitter
  29. * @public
  30. * @param {String} title
  31. * @param {Function} fn
  32. */
  33. function Runnable(title, fn) {
  34. this.title = title;
  35. this.fn = fn;
  36. this.body = (fn || '').toString();
  37. this.async = fn && fn.length;
  38. this.sync = !this.async;
  39. this._timeout = 2000;
  40. this._slow = 75;
  41. this._retries = -1;
  42. utils.assignNewMochaID(this);
  43. Object.defineProperty(this, 'id', {
  44. get() {
  45. return utils.getMochaID(this);
  46. }
  47. });
  48. this.reset();
  49. }
  50. /**
  51. * Inherit from `EventEmitter.prototype`.
  52. */
  53. utils.inherits(Runnable, EventEmitter);
  54. /**
  55. * Resets the state initially or for a next run.
  56. */
  57. Runnable.prototype.reset = function () {
  58. this.timedOut = false;
  59. this._currentRetry = 0;
  60. this.pending = false;
  61. delete this.state;
  62. delete this.err;
  63. };
  64. /**
  65. * Get current timeout value in msecs.
  66. *
  67. * @private
  68. * @returns {number} current timeout threshold value
  69. */
  70. /**
  71. * @summary
  72. * Set timeout threshold value (msecs).
  73. *
  74. * @description
  75. * A string argument can use shorthand (e.g., "2s") and will be converted.
  76. * The value will be clamped to range [<code>0</code>, <code>2^<sup>31</sup>-1</code>].
  77. * If clamped value matches either range endpoint, timeouts will be disabled.
  78. *
  79. * @private
  80. * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Maximum_delay_value}
  81. * @param {number|string} ms - Timeout threshold value.
  82. * @returns {Runnable} this
  83. * @chainable
  84. */
  85. Runnable.prototype.timeout = function (ms) {
  86. if (!arguments.length) {
  87. return this._timeout;
  88. }
  89. if (typeof ms === 'string') {
  90. ms = milliseconds(ms);
  91. }
  92. // Clamp to range
  93. var range = [0, MAX_TIMEOUT];
  94. ms = utils.clamp(ms, range);
  95. // see #1652 for reasoning
  96. if (ms === range[0] || ms === range[1]) {
  97. this._timeout = 0;
  98. } else {
  99. this._timeout = ms;
  100. }
  101. debug('timeout %d', this._timeout);
  102. if (this.timer) {
  103. this.resetTimeout();
  104. }
  105. return this;
  106. };
  107. /**
  108. * Set or get slow `ms`.
  109. *
  110. * @private
  111. * @param {number|string} ms
  112. * @return {Runnable|number} ms or Runnable instance.
  113. */
  114. Runnable.prototype.slow = function (ms) {
  115. if (!arguments.length || typeof ms === 'undefined') {
  116. return this._slow;
  117. }
  118. if (typeof ms === 'string') {
  119. ms = milliseconds(ms);
  120. }
  121. debug('slow %d', ms);
  122. this._slow = ms;
  123. return this;
  124. };
  125. /**
  126. * Halt and mark as pending.
  127. *
  128. * @memberof Mocha.Runnable
  129. * @public
  130. */
  131. Runnable.prototype.skip = function () {
  132. this.pending = true;
  133. throw new Pending('sync skip; aborting execution');
  134. };
  135. /**
  136. * Check if this runnable or its parent suite is marked as pending.
  137. *
  138. * @private
  139. */
  140. Runnable.prototype.isPending = function () {
  141. return this.pending || (this.parent && this.parent.isPending());
  142. };
  143. /**
  144. * Return `true` if this Runnable has failed.
  145. * @return {boolean}
  146. * @private
  147. */
  148. Runnable.prototype.isFailed = function () {
  149. return !this.isPending() && this.state === constants.STATE_FAILED;
  150. };
  151. /**
  152. * Return `true` if this Runnable has passed.
  153. * @return {boolean}
  154. * @private
  155. */
  156. Runnable.prototype.isPassed = function () {
  157. return !this.isPending() && this.state === constants.STATE_PASSED;
  158. };
  159. /**
  160. * Set or get number of retries.
  161. *
  162. * @private
  163. */
  164. Runnable.prototype.retries = function (n) {
  165. if (!arguments.length) {
  166. return this._retries;
  167. }
  168. this._retries = n;
  169. };
  170. /**
  171. * Set or get current retry
  172. *
  173. * @private
  174. */
  175. Runnable.prototype.currentRetry = function (n) {
  176. if (!arguments.length) {
  177. return this._currentRetry;
  178. }
  179. this._currentRetry = n;
  180. };
  181. /**
  182. * Return the full title generated by recursively concatenating the parent's
  183. * full title.
  184. *
  185. * @memberof Mocha.Runnable
  186. * @public
  187. * @return {string}
  188. */
  189. Runnable.prototype.fullTitle = function () {
  190. return this.titlePath().join(' ');
  191. };
  192. /**
  193. * Return the title path generated by concatenating the parent's title path with the title.
  194. *
  195. * @memberof Mocha.Runnable
  196. * @public
  197. * @return {string[]}
  198. */
  199. Runnable.prototype.titlePath = function () {
  200. return this.parent.titlePath().concat([this.title]);
  201. };
  202. /**
  203. * Clear the timeout.
  204. *
  205. * @private
  206. */
  207. Runnable.prototype.clearTimeout = function () {
  208. clearTimeout(this.timer);
  209. };
  210. /**
  211. * Reset the timeout.
  212. *
  213. * @private
  214. */
  215. Runnable.prototype.resetTimeout = function () {
  216. var self = this;
  217. var ms = this.timeout() || MAX_TIMEOUT;
  218. this.clearTimeout();
  219. this.timer = setTimeout(function () {
  220. if (self.timeout() === 0) {
  221. return;
  222. }
  223. self.callback(self._timeoutError(ms));
  224. self.timedOut = true;
  225. }, ms);
  226. };
  227. /**
  228. * Set or get a list of whitelisted globals for this test run.
  229. *
  230. * @private
  231. * @param {string[]} globals
  232. */
  233. Runnable.prototype.globals = function (globals) {
  234. if (!arguments.length) {
  235. return this._allowedGlobals;
  236. }
  237. this._allowedGlobals = globals;
  238. };
  239. /**
  240. * Run the test and invoke `fn(err)`.
  241. *
  242. * @param {Function} fn
  243. * @private
  244. */
  245. Runnable.prototype.run = function (fn) {
  246. var self = this;
  247. var start = new Date();
  248. var ctx = this.ctx;
  249. var finished;
  250. var errorWasHandled = false;
  251. if (this.isPending()) return fn();
  252. // Sometimes the ctx exists, but it is not runnable
  253. if (ctx && ctx.runnable) {
  254. ctx.runnable(this);
  255. }
  256. // called multiple times
  257. function multiple(err) {
  258. if (errorWasHandled) {
  259. return;
  260. }
  261. errorWasHandled = true;
  262. self.emit('error', createMultipleDoneError(self, err));
  263. }
  264. // finished
  265. function done(err) {
  266. var ms = self.timeout();
  267. if (self.timedOut) {
  268. return;
  269. }
  270. if (finished) {
  271. return multiple(err);
  272. }
  273. self.clearTimeout();
  274. self.duration = new Date() - start;
  275. finished = true;
  276. if (!err && self.duration > ms && ms > 0) {
  277. err = self._timeoutError(ms);
  278. }
  279. fn(err);
  280. }
  281. // for .resetTimeout() and Runner#uncaught()
  282. this.callback = done;
  283. if (this.fn && typeof this.fn.call !== 'function') {
  284. done(
  285. new TypeError(
  286. 'A runnable must be passed a function as its second argument.'
  287. )
  288. );
  289. return;
  290. }
  291. // explicit async with `done` argument
  292. if (this.async) {
  293. this.resetTimeout();
  294. // allows skip() to be used in an explicit async context
  295. this.skip = function asyncSkip() {
  296. this.pending = true;
  297. done();
  298. // halt execution, the uncaught handler will ignore the failure.
  299. throw new Pending('async skip; aborting execution');
  300. };
  301. try {
  302. callFnAsync(this.fn);
  303. } catch (err) {
  304. // handles async runnables which actually run synchronously
  305. errorWasHandled = true;
  306. if (err instanceof Pending) {
  307. return; // done() is already called in this.skip()
  308. } else if (this.allowUncaught) {
  309. throw err;
  310. }
  311. done(Runnable.toValueOrError(err));
  312. }
  313. return;
  314. }
  315. // sync or promise-returning
  316. try {
  317. callFn(this.fn);
  318. } catch (err) {
  319. errorWasHandled = true;
  320. if (err instanceof Pending) {
  321. return done();
  322. } else if (this.allowUncaught) {
  323. throw err;
  324. }
  325. done(Runnable.toValueOrError(err));
  326. }
  327. function callFn(fn) {
  328. var result = fn.call(ctx);
  329. if (result && typeof result.then === 'function') {
  330. self.resetTimeout();
  331. result.then(
  332. function () {
  333. done();
  334. // Return null so libraries like bluebird do not warn about
  335. // subsequently constructed Promises.
  336. return null;
  337. },
  338. function (reason) {
  339. done(reason || new Error('Promise rejected with no or falsy reason'));
  340. }
  341. );
  342. } else {
  343. if (self.asyncOnly) {
  344. return done(
  345. new Error(
  346. '--async-only option in use without declaring `done()` or returning a promise'
  347. )
  348. );
  349. }
  350. done();
  351. }
  352. }
  353. function callFnAsync(fn) {
  354. var result = fn.call(ctx, function (err) {
  355. if (err instanceof Error || toString.call(err) === '[object Error]') {
  356. return done(err);
  357. }
  358. if (err) {
  359. if (Object.prototype.toString.call(err) === '[object Object]') {
  360. return done(
  361. new Error('done() invoked with non-Error: ' + JSON.stringify(err))
  362. );
  363. }
  364. return done(new Error('done() invoked with non-Error: ' + err));
  365. }
  366. if (result && utils.isPromise(result)) {
  367. return done(
  368. new Error(
  369. 'Resolution method is overspecified. Specify a callback *or* return a Promise; not both.'
  370. )
  371. );
  372. }
  373. done();
  374. });
  375. }
  376. };
  377. /**
  378. * Instantiates a "timeout" error
  379. *
  380. * @param {number} ms - Timeout (in milliseconds)
  381. * @returns {Error} a "timeout" error
  382. * @private
  383. */
  384. Runnable.prototype._timeoutError = function (ms) {
  385. let msg = `Timeout of ${ms}ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.`;
  386. if (this.file) {
  387. msg += ' (' + this.file + ')';
  388. }
  389. return createTimeoutError(msg, ms, this.file);
  390. };
  391. var constants = utils.defineConstants(
  392. /**
  393. * {@link Runnable}-related constants.
  394. * @public
  395. * @memberof Runnable
  396. * @readonly
  397. * @static
  398. * @alias constants
  399. * @enum {string}
  400. */
  401. {
  402. /**
  403. * Value of `state` prop when a `Runnable` has failed
  404. */
  405. STATE_FAILED: 'failed',
  406. /**
  407. * Value of `state` prop when a `Runnable` has passed
  408. */
  409. STATE_PASSED: 'passed',
  410. /**
  411. * Value of `state` prop when a `Runnable` has been skipped by user
  412. */
  413. STATE_PENDING: 'pending'
  414. }
  415. );
  416. /**
  417. * Given `value`, return identity if truthy, otherwise create an "invalid exception" error and return that.
  418. * @param {*} [value] - Value to return, if present
  419. * @returns {*|Error} `value`, otherwise an `Error`
  420. * @private
  421. */
  422. Runnable.toValueOrError = function (value) {
  423. return (
  424. value ||
  425. createInvalidExceptionError(
  426. 'Runnable failed with falsy or undefined exception. Please throw an Error instead.',
  427. value
  428. )
  429. );
  430. };
  431. Runnable.constants = constants;