runnable.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. 'use strict';
  2. var EventEmitter = require('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. /**
  8. * Save timer references to avoid Sinon interfering (see GH-237).
  9. */
  10. /* eslint-disable no-unused-vars, no-native-reassign */
  11. var Date = global.Date;
  12. var setTimeout = global.setTimeout;
  13. var setInterval = global.setInterval;
  14. var clearTimeout = global.clearTimeout;
  15. var clearInterval = global.clearInterval;
  16. /* eslint-enable no-unused-vars, no-native-reassign */
  17. var toString = Object.prototype.toString;
  18. module.exports = Runnable;
  19. /**
  20. * Initialize a new `Runnable` with the given `title` and callback `fn`. Derived from [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
  21. *
  22. * @class
  23. * @extends EventEmitter
  24. * @param {String} title
  25. * @param {Function} fn
  26. */
  27. function Runnable(title, fn) {
  28. this.title = title;
  29. this.fn = fn;
  30. this.body = (fn || '').toString();
  31. this.async = fn && fn.length;
  32. this.sync = !this.async;
  33. this._timeout = 2000;
  34. this._slow = 75;
  35. this._enableTimeouts = true;
  36. this.timedOut = false;
  37. this._retries = -1;
  38. this._currentRetry = 0;
  39. this.pending = false;
  40. }
  41. /**
  42. * Inherit from `EventEmitter.prototype`.
  43. */
  44. utils.inherits(Runnable, EventEmitter);
  45. /**
  46. * Set & get timeout `ms`.
  47. *
  48. * @api private
  49. * @param {number|string} ms
  50. * @return {Runnable|number} ms or Runnable instance.
  51. */
  52. Runnable.prototype.timeout = function(ms) {
  53. if (!arguments.length) {
  54. return this._timeout;
  55. }
  56. // see #1652 for reasoning
  57. if (ms === 0 || ms > Math.pow(2, 31)) {
  58. this._enableTimeouts = false;
  59. }
  60. if (typeof ms === 'string') {
  61. ms = milliseconds(ms);
  62. }
  63. debug('timeout %d', ms);
  64. this._timeout = ms;
  65. if (this.timer) {
  66. this.resetTimeout();
  67. }
  68. return this;
  69. };
  70. /**
  71. * Set or get slow `ms`.
  72. *
  73. * @api private
  74. * @param {number|string} ms
  75. * @return {Runnable|number} ms or Runnable instance.
  76. */
  77. Runnable.prototype.slow = function(ms) {
  78. if (!arguments.length || typeof ms === 'undefined') {
  79. return this._slow;
  80. }
  81. if (typeof ms === 'string') {
  82. ms = milliseconds(ms);
  83. }
  84. debug('slow %d', ms);
  85. this._slow = ms;
  86. return this;
  87. };
  88. /**
  89. * Set and get whether timeout is `enabled`.
  90. *
  91. * @api private
  92. * @param {boolean} enabled
  93. * @return {Runnable|boolean} enabled or Runnable instance.
  94. */
  95. Runnable.prototype.enableTimeouts = function(enabled) {
  96. if (!arguments.length) {
  97. return this._enableTimeouts;
  98. }
  99. debug('enableTimeouts %s', enabled);
  100. this._enableTimeouts = enabled;
  101. return this;
  102. };
  103. /**
  104. * Halt and mark as pending.
  105. *
  106. * @memberof Mocha.Runnable
  107. * @public
  108. * @api public
  109. */
  110. Runnable.prototype.skip = function() {
  111. throw new Pending('sync skip');
  112. };
  113. /**
  114. * Check if this runnable or its parent suite is marked as pending.
  115. *
  116. * @api private
  117. */
  118. Runnable.prototype.isPending = function() {
  119. return this.pending || (this.parent && this.parent.isPending());
  120. };
  121. /**
  122. * Return `true` if this Runnable has failed.
  123. * @return {boolean}
  124. * @private
  125. */
  126. Runnable.prototype.isFailed = function() {
  127. return !this.isPending() && this.state === 'failed';
  128. };
  129. /**
  130. * Return `true` if this Runnable has passed.
  131. * @return {boolean}
  132. * @private
  133. */
  134. Runnable.prototype.isPassed = function() {
  135. return !this.isPending() && this.state === 'passed';
  136. };
  137. /**
  138. * Set or get number of retries.
  139. *
  140. * @api private
  141. */
  142. Runnable.prototype.retries = function(n) {
  143. if (!arguments.length) {
  144. return this._retries;
  145. }
  146. this._retries = n;
  147. };
  148. /**
  149. * Set or get current retry
  150. *
  151. * @api private
  152. */
  153. Runnable.prototype.currentRetry = function(n) {
  154. if (!arguments.length) {
  155. return this._currentRetry;
  156. }
  157. this._currentRetry = n;
  158. };
  159. /**
  160. * Return the full title generated by recursively concatenating the parent's
  161. * full title.
  162. *
  163. * @memberof Mocha.Runnable
  164. * @public
  165. * @api public
  166. * @return {string}
  167. */
  168. Runnable.prototype.fullTitle = function() {
  169. return this.titlePath().join(' ');
  170. };
  171. /**
  172. * Return the title path generated by concatenating the parent's title path with the title.
  173. *
  174. * @memberof Mocha.Runnable
  175. * @public
  176. * @api public
  177. * @return {string}
  178. */
  179. Runnable.prototype.titlePath = function() {
  180. return this.parent.titlePath().concat([this.title]);
  181. };
  182. /**
  183. * Clear the timeout.
  184. *
  185. * @api private
  186. */
  187. Runnable.prototype.clearTimeout = function() {
  188. clearTimeout(this.timer);
  189. };
  190. /**
  191. * Inspect the runnable void of private properties.
  192. *
  193. * @api private
  194. * @return {string}
  195. */
  196. Runnable.prototype.inspect = function() {
  197. return JSON.stringify(
  198. this,
  199. function(key, val) {
  200. if (key[0] === '_') {
  201. return;
  202. }
  203. if (key === 'parent') {
  204. return '#<Suite>';
  205. }
  206. if (key === 'ctx') {
  207. return '#<Context>';
  208. }
  209. return val;
  210. },
  211. 2
  212. );
  213. };
  214. /**
  215. * Reset the timeout.
  216. *
  217. * @api private
  218. */
  219. Runnable.prototype.resetTimeout = function() {
  220. var self = this;
  221. var ms = this.timeout() || 1e9;
  222. if (!this._enableTimeouts) {
  223. return;
  224. }
  225. this.clearTimeout();
  226. this.timer = setTimeout(function() {
  227. if (!self._enableTimeouts) {
  228. return;
  229. }
  230. self.callback(self._timeoutError(ms));
  231. self.timedOut = true;
  232. }, ms);
  233. };
  234. /**
  235. * Set or get a list of whitelisted globals for this test run.
  236. *
  237. * @api private
  238. * @param {string[]} globals
  239. */
  240. Runnable.prototype.globals = function(globals) {
  241. if (!arguments.length) {
  242. return this._allowedGlobals;
  243. }
  244. this._allowedGlobals = globals;
  245. };
  246. /**
  247. * Run the test and invoke `fn(err)`.
  248. *
  249. * @param {Function} fn
  250. * @api private
  251. */
  252. Runnable.prototype.run = function(fn) {
  253. var self = this;
  254. var start = new Date();
  255. var ctx = this.ctx;
  256. var finished;
  257. var emitted;
  258. // Sometimes the ctx exists, but it is not runnable
  259. if (ctx && ctx.runnable) {
  260. ctx.runnable(this);
  261. }
  262. // called multiple times
  263. function multiple(err) {
  264. if (emitted) {
  265. return;
  266. }
  267. emitted = true;
  268. var msg = 'done() called multiple times';
  269. if (err && err.message) {
  270. err.message += " (and Mocha's " + msg + ')';
  271. self.emit('error', err);
  272. } else {
  273. self.emit('error', new Error(msg));
  274. }
  275. }
  276. // finished
  277. function done(err) {
  278. var ms = self.timeout();
  279. if (self.timedOut) {
  280. return;
  281. }
  282. if (finished) {
  283. return multiple(err);
  284. }
  285. self.clearTimeout();
  286. self.duration = new Date() - start;
  287. finished = true;
  288. if (!err && self.duration > ms && self._enableTimeouts) {
  289. err = self._timeoutError(ms);
  290. }
  291. fn(err);
  292. }
  293. // for .resetTimeout()
  294. this.callback = done;
  295. // explicit async with `done` argument
  296. if (this.async) {
  297. this.resetTimeout();
  298. // allows skip() to be used in an explicit async context
  299. this.skip = function asyncSkip() {
  300. done(new Pending('async skip call'));
  301. // halt execution. the Runnable will be marked pending
  302. // by the previous call, and the uncaught handler will ignore
  303. // the failure.
  304. throw new Pending('async skip; aborting execution');
  305. };
  306. if (this.allowUncaught) {
  307. return callFnAsync(this.fn);
  308. }
  309. try {
  310. callFnAsync(this.fn);
  311. } catch (err) {
  312. emitted = true;
  313. done(utils.getError(err));
  314. }
  315. return;
  316. }
  317. if (this.allowUncaught) {
  318. if (this.isPending()) {
  319. done();
  320. } else {
  321. callFn(this.fn);
  322. }
  323. return;
  324. }
  325. // sync or promise-returning
  326. try {
  327. if (this.isPending()) {
  328. done();
  329. } else {
  330. callFn(this.fn);
  331. }
  332. } catch (err) {
  333. emitted = true;
  334. done(utils.getError(err));
  335. }
  336. function callFn(fn) {
  337. var result = fn.call(ctx);
  338. if (result && typeof result.then === 'function') {
  339. self.resetTimeout();
  340. result.then(
  341. function() {
  342. done();
  343. // Return null so libraries like bluebird do not warn about
  344. // subsequently constructed Promises.
  345. return null;
  346. },
  347. function(reason) {
  348. done(reason || new Error('Promise rejected with no or falsy reason'));
  349. }
  350. );
  351. } else {
  352. if (self.asyncOnly) {
  353. return done(
  354. new Error(
  355. '--async-only option in use without declaring `done()` or returning a promise'
  356. )
  357. );
  358. }
  359. done();
  360. }
  361. }
  362. function callFnAsync(fn) {
  363. var result = fn.call(ctx, function(err) {
  364. if (err instanceof Error || toString.call(err) === '[object Error]') {
  365. return done(err);
  366. }
  367. if (err) {
  368. if (Object.prototype.toString.call(err) === '[object Object]') {
  369. return done(
  370. new Error('done() invoked with non-Error: ' + JSON.stringify(err))
  371. );
  372. }
  373. return done(new Error('done() invoked with non-Error: ' + err));
  374. }
  375. if (result && utils.isPromise(result)) {
  376. return done(
  377. new Error(
  378. 'Resolution method is overspecified. Specify a callback *or* return a Promise; not both.'
  379. )
  380. );
  381. }
  382. done();
  383. });
  384. }
  385. };
  386. /**
  387. * Instantiates a "timeout" error
  388. *
  389. * @param {number} ms - Timeout (in milliseconds)
  390. * @returns {Error} a "timeout" error
  391. * @private
  392. */
  393. Runnable.prototype._timeoutError = function(ms) {
  394. var msg =
  395. 'Timeout of ' +
  396. ms +
  397. 'ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.';
  398. if (this.file) {
  399. msg += ' (' + this.file + ')';
  400. }
  401. return new Error(msg);
  402. };