UNPKG

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