mocha 7.1.1 → 8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/runner.js CHANGED
@@ -18,8 +18,8 @@ var HOOK_TYPE_BEFORE_ALL = Suite.constants.HOOK_TYPE_BEFORE_ALL;
18
18
  var EVENT_ROOT_SUITE_RUN = Suite.constants.EVENT_ROOT_SUITE_RUN;
19
19
  var STATE_FAILED = Runnable.constants.STATE_FAILED;
20
20
  var STATE_PASSED = Runnable.constants.STATE_PASSED;
21
+ var STATE_PENDING = Runnable.constants.STATE_PENDING;
21
22
  var dQuote = utils.dQuote;
22
- var ngettext = utils.ngettext;
23
23
  var sQuote = utils.sQuote;
24
24
  var stackFilter = utils.stackTraceFilter();
25
25
  var stringify = utils.stringify;
@@ -27,6 +27,7 @@ var type = utils.type;
27
27
  var errors = require('./errors');
28
28
  var createInvalidExceptionError = errors.createInvalidExceptionError;
29
29
  var createUnsupportedError = errors.createUnsupportedError;
30
+ var createFatalError = errors.createFatalError;
30
31
 
31
32
  /**
32
33
  * Non-enumerable globals.
@@ -109,7 +110,19 @@ var constants = utils.defineConstants(
109
110
  /**
110
111
  * Emitted when {@link Test} execution has failed, but will retry
111
112
  */
112
- EVENT_TEST_RETRY: 'retry'
113
+ EVENT_TEST_RETRY: 'retry',
114
+ /**
115
+ * Initial state of Runner
116
+ */
117
+ STATE_IDLE: 'idle',
118
+ /**
119
+ * State set to this value when the Runner has started running
120
+ */
121
+ STATE_RUNNING: 'running',
122
+ /**
123
+ * State set to this value when the Runner has stopped
124
+ */
125
+ STATE_STOPPED: 'stopped'
113
126
  }
114
127
  );
115
128
 
@@ -121,21 +134,32 @@ module.exports = Runner;
121
134
  * @extends external:EventEmitter
122
135
  * @public
123
136
  * @class
124
- * @param {Suite} suite Root suite
125
- * @param {boolean} [delay] Whether or not to delay execution of root suite
126
- * until ready.
137
+ * @param {Suite} suite - Root suite
138
+ * @param {Object|boolean} [opts] - Options. If `boolean`, whether or not to delay execution of root suite until ready (for backwards compatibility).
139
+ * @param {boolean} [opts.delay] - Whether to delay execution of root suite until ready.
140
+ * @param {boolean} [opts.cleanReferencesAfterRun] - Whether to clean references to test fns and hooks when a suite is done.
127
141
  */
128
- function Runner(suite, delay) {
142
+ function Runner(suite, opts) {
143
+ if (opts === undefined) {
144
+ opts = {};
145
+ }
146
+ if (typeof opts === 'boolean') {
147
+ this._delay = opts;
148
+ opts = {};
149
+ } else {
150
+ this._delay = opts.delay;
151
+ }
129
152
  var self = this;
130
153
  this._globals = [];
131
154
  this._abort = false;
132
- this._delay = delay;
133
155
  this.suite = suite;
134
- this.started = false;
156
+ this._opts = opts;
157
+ this.state = constants.STATE_IDLE;
135
158
  this.total = suite.total();
136
159
  this.failures = 0;
160
+ this._eventListeners = [];
137
161
  this.on(constants.EVENT_TEST_END, function(test) {
138
- if (test.retriedTest() && test.parent) {
162
+ if (test.type === 'test' && test.retriedTest() && test.parent) {
139
163
  var idx =
140
164
  test.parent.tests && test.parent.tests.indexOf(test.retriedTest());
141
165
  if (idx > -1) test.parent.tests[idx] = test;
@@ -148,6 +172,8 @@ function Runner(suite, delay) {
148
172
  this._defaultGrep = /.*/;
149
173
  this.grep(this._defaultGrep);
150
174
  this.globals(this.globalProps());
175
+
176
+ this.uncaught = this._uncaught.bind(this);
151
177
  }
152
178
 
153
179
  /**
@@ -163,6 +189,58 @@ Runner.immediately = global.setImmediate || process.nextTick;
163
189
  */
164
190
  inherits(Runner, EventEmitter);
165
191
 
192
+ /**
193
+ * Replacement for `target.on(eventName, listener)` that does bookkeeping to remove them when this runner instance is disposed.
194
+ * @param {EventEmitter} target - The `EventEmitter`
195
+ * @param {string} eventName - The event name
196
+ * @param {string} fn - Listener function
197
+ * @private
198
+ */
199
+ Runner.prototype._addEventListener = function(target, eventName, listener) {
200
+ target.on(eventName, listener);
201
+ this._eventListeners.push([target, eventName, listener]);
202
+ };
203
+
204
+ /**
205
+ * Replacement for `target.removeListener(eventName, listener)` that also updates the bookkeeping.
206
+ * @param {EventEmitter} target - The `EventEmitter`
207
+ * @param {string} eventName - The event anme
208
+ * @param {function} listener - Listener function
209
+ * @private
210
+ */
211
+ Runner.prototype._removeEventListener = function(target, eventName, listener) {
212
+ var eventListenerIndex = -1;
213
+ for (var i = 0; i < this._eventListeners.length; i++) {
214
+ var eventListenerDescriptor = this._eventListeners[i];
215
+ if (
216
+ eventListenerDescriptor[0] === target &&
217
+ eventListenerDescriptor[1] === eventName &&
218
+ eventListenerDescriptor[2] === listener
219
+ ) {
220
+ eventListenerIndex = i;
221
+ break;
222
+ }
223
+ }
224
+ if (eventListenerIndex !== -1) {
225
+ var removedListener = this._eventListeners.splice(eventListenerIndex, 1)[0];
226
+ removedListener[0].removeListener(removedListener[1], removedListener[2]);
227
+ }
228
+ };
229
+
230
+ /**
231
+ * Removes all event handlers set during a run on this instance.
232
+ * Remark: this does _not_ clean/dispose the tests or suites themselves.
233
+ */
234
+ Runner.prototype.dispose = function() {
235
+ this.removeAllListeners();
236
+ this._eventListeners.forEach(function(eventListenerDescriptor) {
237
+ eventListenerDescriptor[0].removeListener(
238
+ eventListenerDescriptor[1],
239
+ eventListenerDescriptor[2]
240
+ );
241
+ });
242
+ };
243
+
166
244
  /**
167
245
  * Run tests with full titles matching `re`. Updates runner.total
168
246
  * with number of tests matched.
@@ -174,7 +252,7 @@ inherits(Runner, EventEmitter);
174
252
  * @return {Runner} Runner instance.
175
253
  */
176
254
  Runner.prototype.grep = function(re, invert) {
177
- debug('grep %s', re);
255
+ debug('grep(): setting to %s', re);
178
256
  this._grep = re;
179
257
  this._invert = invert;
180
258
  this.total = this.grepTotal(this.suite);
@@ -239,7 +317,7 @@ Runner.prototype.globals = function(arr) {
239
317
  if (!arguments.length) {
240
318
  return this._globals;
241
319
  }
242
- debug('globals %j', arr);
320
+ debug('globals(): setting to %O', arr);
243
321
  this._globals = this._globals.concat(arr);
244
322
  return this;
245
323
  };
@@ -271,12 +349,8 @@ Runner.prototype.checkGlobals = function(test) {
271
349
  this._globals = this._globals.concat(leaks);
272
350
 
273
351
  if (leaks.length) {
274
- var format = ngettext(
275
- leaks.length,
276
- 'global leak detected: %s',
277
- 'global leaks detected: %s'
278
- );
279
- var error = new Error(util.format(format, leaks.map(sQuote).join(', ')));
352
+ var msg = 'global leak(s) detected: %s';
353
+ var error = new Error(util.format(msg, leaks.map(sQuote).join(', ')));
280
354
  this.fail(test, error);
281
355
  }
282
356
  };
@@ -285,15 +359,27 @@ Runner.prototype.checkGlobals = function(test) {
285
359
  * Fail the given `test`.
286
360
  *
287
361
  * @private
288
- * @param {Test} test
362
+ * @param {Runnable} test
289
363
  * @param {Error} err
364
+ * @param {boolean} [force=false] - Whether to fail a pending test.
290
365
  */
291
- Runner.prototype.fail = function(test, err) {
292
- if (test.isPending()) {
366
+ Runner.prototype.fail = function(test, err, force) {
367
+ force = force === true;
368
+ if (test.isPending() && !force) {
293
369
  return;
294
370
  }
371
+ if (this.state === constants.STATE_STOPPED) {
372
+ if (err.code === errors.constants.MULTIPLE_DONE) {
373
+ throw err;
374
+ }
375
+ throw createFatalError(
376
+ 'Test failed after root suite execution completed!',
377
+ err
378
+ );
379
+ }
295
380
 
296
381
  ++this.failures;
382
+ debug('total number of failures: %d', this.failures);
297
383
  test.state = STATE_FAILED;
298
384
 
299
385
  if (!isError(err)) {
@@ -381,12 +467,12 @@ Runner.prototype.hook = function(name, fn) {
381
467
  self.emit(constants.EVENT_HOOK_BEGIN, hook);
382
468
 
383
469
  if (!hook.listeners('error').length) {
384
- hook.on('error', function(err) {
470
+ self._addEventListener(hook, 'error', function(err) {
385
471
  self.failHook(hook, err);
386
472
  });
387
473
  }
388
474
 
389
- hook.run(function(err) {
475
+ hook.run(function cbHookRun(err) {
390
476
  var testError = hook.error();
391
477
  if (testError) {
392
478
  self.fail(self.test, testError);
@@ -412,6 +498,7 @@ Runner.prototype.hook = function(name, fn) {
412
498
  suite.suites.forEach(function(suite) {
413
499
  suite.pending = true;
414
500
  });
501
+ hooks = [];
415
502
  } else {
416
503
  hook.pending = false;
417
504
  var errForbid = createUnsupportedError('`this.skip` forbidden');
@@ -524,18 +611,10 @@ Runner.prototype.runTest = function(fn) {
524
611
  return;
525
612
  }
526
613
 
527
- var suite = this.parents().reverse()[0] || this.suite;
528
- if (this.forbidOnly && suite.hasOnly()) {
529
- fn(new Error('`.only` forbidden'));
530
- return;
531
- }
532
614
  if (this.asyncOnly) {
533
615
  test.asyncOnly = true;
534
616
  }
535
- test.on('error', function(err) {
536
- if (err instanceof Pending) {
537
- return;
538
- }
617
+ this._addEventListener(test, 'error', function(err) {
539
618
  self.fail(test, err);
540
619
  });
541
620
  if (this.allowUncaught) {
@@ -634,10 +713,9 @@ Runner.prototype.runTests = function(suite, fn) {
634
713
  // static skip, no hooks are executed
635
714
  if (test.isPending()) {
636
715
  if (self.forbidPending) {
637
- test.isPending = alwaysFalse;
638
- self.fail(test, new Error('Pending test forbidden'));
639
- delete test.isPending;
716
+ self.fail(test, new Error('Pending test forbidden'), true);
640
717
  } else {
718
+ test.state = STATE_PENDING;
641
719
  self.emit(constants.EVENT_TEST_PENDING, test);
642
720
  }
643
721
  self.emit(constants.EVENT_TEST_END, test);
@@ -650,10 +728,9 @@ Runner.prototype.runTests = function(suite, fn) {
650
728
  // conditional skip within beforeEach
651
729
  if (test.isPending()) {
652
730
  if (self.forbidPending) {
653
- test.isPending = alwaysFalse;
654
- self.fail(test, new Error('Pending test forbidden'));
655
- delete test.isPending;
731
+ self.fail(test, new Error('Pending test forbidden'), true);
656
732
  } else {
733
+ test.state = STATE_PENDING;
657
734
  self.emit(constants.EVENT_TEST_PENDING, test);
658
735
  }
659
736
  self.emit(constants.EVENT_TEST_END, test);
@@ -674,10 +751,9 @@ Runner.prototype.runTests = function(suite, fn) {
674
751
  // conditional skip within it
675
752
  if (test.pending) {
676
753
  if (self.forbidPending) {
677
- test.isPending = alwaysFalse;
678
- self.fail(test, new Error('Pending test forbidden'));
679
- delete test.isPending;
754
+ self.fail(test, new Error('Pending test forbidden'), true);
680
755
  } else {
756
+ test.state = STATE_PENDING;
681
757
  self.emit(constants.EVENT_TEST_PENDING, test);
682
758
  }
683
759
  self.emit(constants.EVENT_TEST_END, test);
@@ -714,10 +790,6 @@ Runner.prototype.runTests = function(suite, fn) {
714
790
  next();
715
791
  };
716
792
 
717
- function alwaysFalse() {
718
- return false;
719
- }
720
-
721
793
  /**
722
794
  * Run the given `suite` and invoke the callback `fn()` when complete.
723
795
  *
@@ -730,9 +802,10 @@ Runner.prototype.runSuite = function(suite, fn) {
730
802
  var self = this;
731
803
  var total = this.grepTotal(suite);
732
804
 
733
- debug('run suite %s', suite.fullTitle());
805
+ debug('runSuite(): running %s', suite.fullTitle());
734
806
 
735
807
  if (!total || (self.failures && suite._bail)) {
808
+ debug('runSuite(): bailing');
736
809
  return fn();
737
810
  }
738
811
 
@@ -798,22 +871,49 @@ Runner.prototype.runSuite = function(suite, fn) {
798
871
  /**
799
872
  * Handle uncaught exceptions within runner.
800
873
  *
801
- * @param {Error} err
874
+ * This function is bound to the instance as `Runner#uncaught` at instantiation
875
+ * time. It's intended to be listening on the `Process.uncaughtException` event.
876
+ * In order to not leak EE listeners, we need to ensure no more than a single
877
+ * `uncaughtException` listener exists per `Runner`. The only way to do
878
+ * this--because this function needs the context (and we don't have lambdas)--is
879
+ * to use `Function.prototype.bind`. We need strict equality to unregister and
880
+ * _only_ unregister the _one_ listener we set from the
881
+ * `Process.uncaughtException` event; would be poor form to just remove
882
+ * everything. See {@link Runner#run} for where the event listener is registered
883
+ * and unregistered.
884
+ * @param {Error} err - Some uncaught error
802
885
  * @private
803
886
  */
804
- Runner.prototype.uncaught = function(err) {
887
+ Runner.prototype._uncaught = function(err) {
888
+ // this is defensive to prevent future developers from mis-calling this function.
889
+ // it's more likely that it'd be called with the incorrect context--say, the global
890
+ // `process` object--than it would to be called with a context that is not a "subclass"
891
+ // of `Runner`.
892
+ if (!(this instanceof Runner)) {
893
+ throw createFatalError(
894
+ 'Runner#uncaught() called with invalid context',
895
+ this
896
+ );
897
+ }
805
898
  if (err instanceof Pending) {
899
+ debug('uncaught(): caught a Pending');
806
900
  return;
807
901
  }
808
902
  // browser does not exit script when throwing in global.onerror()
809
- if (this.allowUncaught && !process.browser) {
903
+ if (this.allowUncaught && !utils.isBrowser()) {
904
+ debug('uncaught(): bubbling exception due to --allow-uncaught');
905
+ throw err;
906
+ }
907
+
908
+ if (this.state === constants.STATE_STOPPED) {
909
+ debug('uncaught(): throwing after run has completed!');
810
910
  throw err;
811
911
  }
812
912
 
813
913
  if (err) {
814
- debug('uncaught exception %O', err);
914
+ debug('uncaught(): got truthy exception %O', err);
815
915
  } else {
816
- debug('uncaught undefined/falsy exception');
916
+ debug('uncaught(): undefined/falsy exception');
817
917
  err = createInvalidExceptionError(
818
918
  'Caught falsy/undefined exception which would otherwise be uncaught. No stack trace found; try a debugger',
819
919
  err
@@ -822,6 +922,7 @@ Runner.prototype.uncaught = function(err) {
822
922
 
823
923
  if (!isError(err)) {
824
924
  err = thrown2Error(err);
925
+ debug('uncaught(): converted "error" %o to Error', err);
825
926
  }
826
927
  err.uncaught = true;
827
928
 
@@ -829,12 +930,15 @@ Runner.prototype.uncaught = function(err) {
829
930
 
830
931
  if (!runnable) {
831
932
  runnable = new Runnable('Uncaught error outside test suite');
933
+ debug('uncaught(): no current Runnable; created a phony one');
832
934
  runnable.parent = this.suite;
833
935
 
834
- if (this.started) {
936
+ if (this.state === constants.STATE_RUNNING) {
937
+ debug('uncaught(): failing gracefully');
835
938
  this.fail(runnable, err);
836
939
  } else {
837
940
  // Can't recover from this failure
941
+ debug('uncaught(): test run has not yet started; unrecoverable');
838
942
  this.emit(constants.EVENT_RUN_BEGIN);
839
943
  this.fail(runnable, err);
840
944
  this.emit(constants.EVENT_RUN_END);
@@ -846,98 +950,94 @@ Runner.prototype.uncaught = function(err) {
846
950
  runnable.clearTimeout();
847
951
 
848
952
  if (runnable.isFailed()) {
953
+ debug('uncaught(): Runnable has already failed');
849
954
  // Ignore error if already failed
850
955
  return;
851
956
  } else if (runnable.isPending()) {
957
+ debug('uncaught(): pending Runnable wound up failing!');
852
958
  // report 'pending test' retrospectively as failed
853
- runnable.isPending = alwaysFalse;
854
- this.fail(runnable, err);
855
- delete runnable.isPending;
959
+ this.fail(runnable, err, true);
856
960
  return;
857
961
  }
858
962
 
859
963
  // we cannot recover gracefully if a Runnable has already passed
860
964
  // then fails asynchronously
861
965
  if (runnable.isPassed()) {
966
+ debug('uncaught(): Runnable has already passed; bailing gracefully');
862
967
  this.fail(runnable, err);
863
968
  this.abort();
864
969
  } else {
865
- debug(runnable);
970
+ debug('uncaught(): forcing Runnable to complete with Error');
866
971
  return runnable.callback(err);
867
972
  }
868
973
  };
869
974
 
870
- /**
871
- * Handle uncaught exceptions after runner's end event.
872
- *
873
- * @param {Error} err
874
- * @private
875
- */
876
- Runner.prototype.uncaughtEnd = function uncaughtEnd(err) {
877
- if (err instanceof Pending) return;
878
- throw err;
879
- };
880
-
881
975
  /**
882
976
  * Run the root suite and invoke `fn(failures)`
883
977
  * on completion.
884
978
  *
885
979
  * @public
886
980
  * @memberof Runner
887
- * @param {Function} fn
981
+ * @param {Function} fn - Callback when finished
982
+ * @param {{files: string[], options: Options}} [opts] - For subclasses
888
983
  * @return {Runner} Runner instance.
889
984
  */
890
- Runner.prototype.run = function(fn) {
985
+ Runner.prototype.run = function(fn, opts) {
891
986
  var self = this;
892
987
  var rootSuite = this.suite;
893
988
 
894
989
  fn = fn || function() {};
895
990
 
896
- function uncaught(err) {
897
- self.uncaught(err);
898
- }
899
-
900
991
  function start() {
992
+ debug('run(): starting');
901
993
  // If there is an `only` filter
902
994
  if (rootSuite.hasOnly()) {
903
995
  rootSuite.filterOnly();
996
+ debug('run(): filtered exclusive Runnables');
904
997
  }
905
- self.started = true;
998
+ self.state = constants.STATE_RUNNING;
906
999
  if (self._delay) {
907
1000
  self.emit(constants.EVENT_DELAY_END);
1001
+ debug('run(): "delay" ended');
908
1002
  }
1003
+ debug('run(): emitting %s', constants.EVENT_RUN_BEGIN);
909
1004
  self.emit(constants.EVENT_RUN_BEGIN);
1005
+ debug('run(): emitted %s', constants.EVENT_RUN_BEGIN);
910
1006
 
911
1007
  self.runSuite(rootSuite, function() {
912
- debug('finished running');
1008
+ debug(
1009
+ 'run(): root suite completed; emitting %s',
1010
+ constants.EVENT_RUN_END
1011
+ );
913
1012
  self.emit(constants.EVENT_RUN_END);
1013
+ debug('run(): emitted %s', constants.EVENT_RUN_END);
914
1014
  });
915
1015
  }
916
1016
 
917
- debug(constants.EVENT_RUN_BEGIN);
918
-
919
1017
  // references cleanup to avoid memory leaks
920
- this.on(constants.EVENT_SUITE_END, function(suite) {
921
- suite.cleanReferences();
922
- });
1018
+ if (this._opts.cleanReferencesAfterRun) {
1019
+ this.on(constants.EVENT_SUITE_END, function(suite) {
1020
+ suite.cleanReferences();
1021
+ });
1022
+ }
923
1023
 
924
1024
  // callback
925
1025
  this.on(constants.EVENT_RUN_END, function() {
1026
+ self.state = constants.STATE_STOPPED;
926
1027
  debug(constants.EVENT_RUN_END);
927
- process.removeListener('uncaughtException', uncaught);
928
- process.on('uncaughtException', self.uncaughtEnd);
1028
+ debug('run(): emitted %s', constants.EVENT_RUN_END);
929
1029
  fn(self.failures);
930
1030
  });
931
1031
 
932
- // uncaught exception
933
- process.removeListener('uncaughtException', self.uncaughtEnd);
934
- process.on('uncaughtException', uncaught);
1032
+ self._removeEventListener(process, 'uncaughtException', self.uncaught);
1033
+ self._addEventListener(process, 'uncaughtException', self.uncaught);
935
1034
 
936
1035
  if (this._delay) {
937
1036
  // for reporters, I guess.
938
1037
  // might be nice to debounce some dots while we wait.
939
1038
  this.emit(constants.EVENT_DELAY_BEGIN, rootSuite);
940
1039
  rootSuite.once(EVENT_ROOT_SUITE_RUN, start);
1040
+ debug('run(): waiting for green light due to --delay');
941
1041
  } else {
942
1042
  Runner.immediately(function() {
943
1043
  start();
@@ -955,7 +1055,7 @@ Runner.prototype.run = function(fn) {
955
1055
  * @return {Runner} Runner instance.
956
1056
  */
957
1057
  Runner.prototype.abort = function() {
958
- debug('aborting');
1058
+ debug('abort(): aborting');
959
1059
  this._abort = true;
960
1060
 
961
1061
  return this;
package/lib/suite.js CHANGED
@@ -61,20 +61,19 @@ function Suite(title, parentContext, isRoot) {
61
61
  this.ctx = new Context();
62
62
  this.suites = [];
63
63
  this.tests = [];
64
+ this.root = isRoot === true;
64
65
  this.pending = false;
66
+ this._retries = -1;
65
67
  this._beforeEach = [];
66
68
  this._beforeAll = [];
67
69
  this._afterEach = [];
68
70
  this._afterAll = [];
69
- this.root = isRoot === true;
70
71
  this._timeout = 2000;
71
- this._enableTimeouts = true;
72
72
  this._slow = 75;
73
73
  this._bail = false;
74
- this._retries = -1;
75
74
  this._onlyTests = [];
76
75
  this._onlySuites = [];
77
- this.delayed = false;
76
+ this.reset();
78
77
 
79
78
  this.on('newListener', function(event) {
80
79
  if (deprecatedEvents[event]) {
@@ -92,6 +91,22 @@ function Suite(title, parentContext, isRoot) {
92
91
  */
93
92
  inherits(Suite, EventEmitter);
94
93
 
94
+ /**
95
+ * Resets the state initially or for a next run.
96
+ */
97
+ Suite.prototype.reset = function() {
98
+ this.delayed = false;
99
+ function doReset(thingToReset) {
100
+ thingToReset.reset();
101
+ }
102
+ this.suites.forEach(doReset);
103
+ this.tests.forEach(doReset);
104
+ this._beforeEach.forEach(doReset);
105
+ this._afterEach.forEach(doReset);
106
+ this._beforeAll.forEach(doReset);
107
+ this._afterAll.forEach(doReset);
108
+ };
109
+
95
110
  /**
96
111
  * Return a clone of this `Suite`.
97
112
  *
@@ -105,7 +120,6 @@ Suite.prototype.clone = function() {
105
120
  suite.root = this.root;
106
121
  suite.timeout(this.timeout());
107
122
  suite.retries(this.retries());
108
- suite.enableTimeouts(this.enableTimeouts());
109
123
  suite.slow(this.slow());
110
124
  suite.bail(this.bail());
111
125
  return suite;
@@ -123,12 +137,15 @@ Suite.prototype.timeout = function(ms) {
123
137
  if (!arguments.length) {
124
138
  return this._timeout;
125
139
  }
126
- if (ms.toString() === '0') {
127
- this._enableTimeouts = false;
128
- }
129
140
  if (typeof ms === 'string') {
130
141
  ms = milliseconds(ms);
131
142
  }
143
+
144
+ // Clamp to range
145
+ var INT_MAX = Math.pow(2, 31) - 1;
146
+ var range = [0, INT_MAX];
147
+ ms = utils.clamp(ms, range);
148
+
132
149
  debug('timeout %d', ms);
133
150
  this._timeout = parseInt(ms, 10);
134
151
  return this;
@@ -150,22 +167,6 @@ Suite.prototype.retries = function(n) {
150
167
  return this;
151
168
  };
152
169
 
153
- /**
154
- * Set or get timeout to `enabled`.
155
- *
156
- * @private
157
- * @param {boolean} enabled
158
- * @return {Suite|boolean} self or enabled
159
- */
160
- Suite.prototype.enableTimeouts = function(enabled) {
161
- if (!arguments.length) {
162
- return this._enableTimeouts;
163
- }
164
- debug('enableTimeouts %s', enabled);
165
- this._enableTimeouts = enabled;
166
- return this;
167
- };
168
-
169
170
  /**
170
171
  * Set or get slow `ms` or short-hand such as "2s".
171
172
  *
@@ -222,7 +223,6 @@ Suite.prototype._createHook = function(title, fn) {
222
223
  hook.parent = this;
223
224
  hook.timeout(this.timeout());
224
225
  hook.retries(this.retries());
225
- hook.enableTimeouts(this.enableTimeouts());
226
226
  hook.slow(this.slow());
227
227
  hook.ctx = this.ctx;
228
228
  hook.file = this.file;
@@ -337,7 +337,6 @@ Suite.prototype.addSuite = function(suite) {
337
337
  suite.root = false;
338
338
  suite.timeout(this.timeout());
339
339
  suite.retries(this.retries());
340
- suite.enableTimeouts(this.enableTimeouts());
341
340
  suite.slow(this.slow());
342
341
  suite.bail(this.bail());
343
342
  this.suites.push(suite);
@@ -356,7 +355,6 @@ Suite.prototype.addTest = function(test) {
356
355
  test.parent = this;
357
356
  test.timeout(this.timeout());
358
357
  test.retries(this.retries());
359
- test.enableTimeouts(this.enableTimeouts());
360
358
  test.slow(this.slow());
361
359
  test.ctx = this.ctx;
362
360
  this.tests.push(test);
@@ -493,6 +491,15 @@ Suite.prototype.appendOnlySuite = function(suite) {
493
491
  this._onlySuites.push(suite);
494
492
  };
495
493
 
494
+ /**
495
+ * Marks a suite to be `only`.
496
+ *
497
+ * @private
498
+ */
499
+ Suite.prototype.markOnly = function() {
500
+ this.parent && this.parent.appendOnlySuite(this);
501
+ };
502
+
496
503
  /**
497
504
  * Adds a test to the list of tests marked `only`.
498
505
  *
@@ -511,6 +518,16 @@ Suite.prototype.getHooks = function getHooks(name) {
511
518
  return this['_' + name];
512
519
  };
513
520
 
521
+ /**
522
+ * cleans all references from this suite and all child suites.
523
+ */
524
+ Suite.prototype.dispose = function() {
525
+ this.suites.forEach(function(suite) {
526
+ suite.dispose();
527
+ });
528
+ this.cleanReferences();
529
+ };
530
+
514
531
  /**
515
532
  * Cleans up the references to all the deferred functions
516
533
  * (before/after/beforeEach/afterEach) and tests of a Suite.
@@ -549,6 +566,22 @@ Suite.prototype.cleanReferences = function cleanReferences() {
549
566
  }
550
567
  };
551
568
 
569
+ /**
570
+ * Returns an object suitable for IPC.
571
+ * Functions are represented by keys beginning with `$$`.
572
+ * @private
573
+ * @returns {Object}
574
+ */
575
+ Suite.prototype.serialize = function serialize() {
576
+ return {
577
+ _bail: this._bail,
578
+ $$fullTitle: this.fullTitle(),
579
+ $$isPending: this.isPending(),
580
+ root: this.root,
581
+ title: this.title
582
+ };
583
+ };
584
+
552
585
  var constants = utils.defineConstants(
553
586
  /**
554
587
  * {@link Suite}-related constants.