1 | 'use strict';
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | var EventEmitter = require('events').EventEmitter;
|
8 | var JSON = require('json3');
|
9 | var Pending = require('./pending');
|
10 | var debug = require('debug')('mocha:runnable');
|
11 | var milliseconds = require('./ms');
|
12 | var utils = require('./utils');
|
13 | var create = require('lodash.create');
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | var Date = global.Date;
|
21 | var setTimeout = global.setTimeout;
|
22 | var setInterval = global.setInterval;
|
23 | var clearTimeout = global.clearTimeout;
|
24 | var clearInterval = global.clearInterval;
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | var toString = Object.prototype.toString;
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 | module.exports = Runnable;
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 | function 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 |
|
66 |
|
67 | Runnable.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 | */
|
78 | Runnable.prototype.timeout = function (ms) {
|
79 | if (!arguments.length) {
|
80 | return this._timeout;
|
81 | }
|
82 |
|
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 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 | Runnable.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 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 | Runnable.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 |
|
134 |
|
135 |
|
136 |
|
137 | Runnable.prototype.skip = function () {
|
138 | throw new Pending('sync skip');
|
139 | };
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 | Runnable.prototype.isPending = function () {
|
147 | return this.pending || (this.parent && this.parent.isPending());
|
148 | };
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 | Runnable.prototype.retries = function (n) {
|
156 | if (!arguments.length) {
|
157 | return this._retries;
|
158 | }
|
159 | this._retries = n;
|
160 | };
|
161 |
|
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 | Runnable.prototype.currentRetry = function (n) {
|
168 | if (!arguments.length) {
|
169 | return this._currentRetry;
|
170 | }
|
171 | this._currentRetry = n;
|
172 | };
|
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 | Runnable.prototype.fullTitle = function () {
|
182 | return this.parent.fullTitle() + ' ' + this.title;
|
183 | };
|
184 |
|
185 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 | Runnable.prototype.clearTimeout = function () {
|
191 | clearTimeout(this.timer);
|
192 | };
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 | Runnable.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 |
|
217 |
|
218 |
|
219 |
|
220 | Runnable.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 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 | Runnable.prototype.globals = function (globals) {
|
245 | if (!arguments.length) {
|
246 | return this._allowedGlobals;
|
247 | }
|
248 | this._allowedGlobals = globals;
|
249 | };
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
256 |
|
257 | Runnable.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 |
|
265 | if (ctx && ctx.runnable) {
|
266 | ctx.runnable(this);
|
267 | }
|
268 |
|
269 |
|
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 |
|
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 |
|
299 | this.callback = done;
|
300 |
|
301 |
|
302 | if (this.async) {
|
303 | this.resetTimeout();
|
304 |
|
305 |
|
306 | this.skip = function asyncSkip () {
|
307 | done(new Pending('async skip call'));
|
308 |
|
309 |
|
310 |
|
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 |
|
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 |
|
355 |
|
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 | };
|