@lumjs/tests 1.1.1 → 1.5.0

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/test.js CHANGED
@@ -1,15 +1,57 @@
1
+ /**
2
+ * Module defining the Test class.
3
+ * @module @lumjs/tests/test
4
+ */
5
+
1
6
  const core = require('@lumjs/core');
2
- const {F,S,N,isType,isObj,isInstance,def} = core.types;
7
+ const types = core.types;
8
+ const {F,S,N,isObj,isArray,needs,def} = types;
3
9
 
4
10
  // We use a separate class to represent test logs.
5
11
  const Log = require('./log');
6
12
 
13
+ // A list of Test methods that return Log objects.
14
+ const TEST_METHODS =
15
+ [
16
+ 'ok', 'call', 'fail', 'pass', 'dies', 'diesWith', 'lives', 'cmp',
17
+ 'is', 'isnt', 'isa', 'nota', 'isJSON', 'isntJSON', 'skip',
18
+ ];
19
+
20
+ // A list of other methods to export that are not standard tests.
21
+ const META_METHODS =
22
+ [
23
+ 'plan', 'diag', 'run', 'tap', 'output', 'done',
24
+ ];
25
+
26
+ // The function that powers `Test.call()` and friends.
27
+ function $call (testfunc, args)
28
+ {
29
+ const ret = {};
30
+ try
31
+ {
32
+ ret.val = testfunc(...args);
33
+ }
34
+ catch (e)
35
+ {
36
+ ret.err = e;
37
+ }
38
+ return ret;
39
+ }
40
+
7
41
  /**
8
42
  * A simple testing library with TAP support.
9
43
  *
10
44
  * Based on Lum.php's Test library.
11
45
  * Which itself was based on Perl 5's Test::More, and
12
46
  * Raku's Test libraries.
47
+ *
48
+ * @property {number} planned - Number of tests planned, `0` if unplanned.
49
+ * @property {number} failed - Number of tests that failed.
50
+ * @property {number} skipped - Number of tests that were skipped.
51
+ * @property {number} ran - Number of tests ran (*calculated*).
52
+ * @property {string} id - Unique test id used by `Harness` libary.
53
+ * @property {boolean} isTop - Test module was loaded from the command line.
54
+ * @property {?object} harness - The top-level `Harness` if one was found.
13
55
  */
14
56
  class Test
15
57
  {
@@ -29,6 +71,9 @@ class Test
29
71
  * Also, if this is passed, and `opts.id` was not specified, and id
30
72
  * will be auto-generated based on the filename of the module.
31
73
  *
74
+ * @param {number} [opts.stringify=1] The depth `stringify()` should recurse
75
+ * objects and Arrays before switching to plain JSON stringification.
76
+ *
32
77
  */
33
78
  constructor (opts={})
34
79
  {
@@ -56,20 +101,46 @@ class Test
56
101
  this.id = null;
57
102
  }
58
103
 
104
+ this.stringifyDepth = opts.stringify ?? 1;
105
+
59
106
  this.failed = 0;
60
107
  this.skipped = 0;
61
108
  this.planned = 0;
62
109
  this.log = [];
63
110
 
111
+ // These three will be updated below if possible.
112
+ this.isTop = false;
113
+ this.harness = null;
114
+
64
115
  if (typeof opts.plan === N)
65
116
  {
66
117
  this.plan(opts.plan);
67
118
  }
68
119
 
69
120
  if (hasModule)
70
- { // Finally, if a module was passed, its going to export this test.
121
+ { // If a module was passed, its going to export this test.
71
122
  opts.module.exports = this;
123
+ // We'll also use the module to determine if we're Harnessed or not.
124
+ if (require.main === opts.module)
125
+ { // Was called directly.
126
+ this.isTop = true;
127
+ }
128
+ }
129
+
130
+ if (!this.isTop)
131
+ { // Try to find a Harness instance.
132
+ if (isObj(require.main) && require.main.exports instanceof Harness)
133
+ { // We found the Harness instance.
134
+ this.harness = require.main.exports;
135
+ }
72
136
  }
137
+
138
+ }
139
+
140
+ // A wrapper around types.stringify()
141
+ stringify(what)
142
+ {
143
+ return types.stringify(what, this.stringifyDepth);
73
144
  }
74
145
 
75
146
  /**
@@ -106,15 +177,14 @@ class Test
106
177
  * If the value evaluates as `true` (aka a truthy value), the test passes.
107
178
  * If it evaluates as `false` (a falsey value), the test fails.
108
179
  *
109
- * @param {string} desc - A short description of the test.
110
- *
180
+ * @param {string} [desc] A short description of the test.
111
181
  * @param {(string|Error)} [directive] Further information for the log.
112
- *
182
+ * @param {object} [details] Extra details to add to the log.
113
183
  * @returns {Log} The test log with the results.
114
184
  */
115
- ok (test, desc, directive)
185
+ ok (test, desc, directive, details)
116
186
  {
117
- const log = new Log();
187
+ const log = new Log(this);
118
188
 
119
189
  if (test)
120
190
  {
@@ -135,11 +205,34 @@ class Test
135
205
  log.directive = directive;
136
206
  }
137
207
 
208
+ if (isObj(details))
209
+ {
210
+ log.details = details;
211
+ }
212
+
138
213
  this.log.push(log);
139
214
 
140
215
  return log;
141
216
  }
142
217
 
218
+ /**
219
+ * Run a function and pass its return value to `ok()`.
220
+ *
221
+ * @param {function} testfunc - The function to run.
222
+ * If the function returns, the return value will be passed to `ok()`.
223
+ * If it throws an Error, the test will be marked as failed, and the
224
+ * Error will be passed as the `directive` to the `ok()` method.
225
+ *
226
+ * @param {string} [desc] A description for `ok()`.
227
+ * @param {...any} [args] Arguments to pass to the test function.
228
+ * @returns {Log}
229
+ */
230
+ call (testfunc, desc, ...args)
231
+ {
232
+ const ret = $call(testfunc, args);
233
+ return this.ok(ret.val, desc, ret.err);
234
+ }
235
+
143
236
  /**
144
237
  * Mark a test as failed.
145
238
  *
@@ -174,20 +267,91 @@ class Test
174
267
  * call. If no error is caught the test will be considered to have failed.
175
268
  *
176
269
  * @param {function} testfunc
177
- * @param {string} desc
270
+ * @param {string} [desc]
271
+ * @param {...any} [args]
178
272
  * @returns {Log}
179
273
  */
180
- dies (testfunc, desc)
274
+ dies (testfunc, desc, ...args)
181
275
  {
182
- let ok = false;
183
- let err;
184
- try { testfunc(); }
185
- catch (e)
276
+ const ret = $call(testfunc, args);
277
+ return this.ok(('err' in ret), desc, ret.err);
278
+ }
279
+
280
+ /**
281
+ * See if a function throws an Error, and if the Error passes a second test.
282
+ *
283
+ * All the notes that apply to `dies()` apply here as well.
284
+ *
285
+ * @param {function} testfunc - Same as `dies()` and `call()`
286
+ * We don't care about the return value of this function.
287
+ * We only care that it *should* throw an Error.
288
+ *
289
+ * @param {(function|string)} testerr - A test to validate the Error.
290
+ *
291
+ * If this is a `function` it will be passed the thrown `Error` as the first
292
+ * parameter, and an `Array` of `args` as the second parameter. The return
293
+ * value from this will be passed to `ok()`.
294
+ *
295
+ * If this is a `string` then the test will pass only if the either the
296
+ * `Error.name` or `Error.message` is exactly equal to this value.
297
+ *
298
+ * @param {string} [desc]
299
+ * @param {...any} [args]
300
+ * @returns {Log}
301
+ */
302
+ diesWith (testfunc, testerr, desc, ...args)
303
+ {
304
+ let ok = false, details = {}, err = null;
305
+
306
+ const r1 = $call(testfunc, args);
307
+
308
+ if ('err' in r1)
186
309
  {
187
- ok = true;
188
- err = e;
189
- }
190
- return this.ok(ok, desc, err);
310
+ err = r1.err;
311
+
312
+ if (typeof testerr === F)
313
+ { // A secondary function to test the error with.
314
+ const r2 = $call(testerr, [err, args]);
315
+ if ('err' in r2)
316
+ { // Second function threw an error, add it as diagnostic info.
317
+ details.info = r2.err;
318
+ }
319
+ else
320
+ { // No error, use the function output as the test value.
321
+ ok = r2.val;
322
+ }
323
+ }
324
+ else if (typeof testerr === S)
325
+ { // A simple name/message test.
326
+ if (err.name === testerr || err.message === testerr)
327
+ { // Either the name or the message matched the string.
328
+ ok = true;
329
+ }
330
+ }
331
+
332
+ } // if r1.err
333
+
334
+ return this.ok(ok, desc, err, details);
335
+ }
336
+
337
+ /**
338
+ * See if a function runs without throwing an Error.
339
+ *
340
+ * The function will be called in a `try { } catch (err) { }` block.
341
+ *
342
+ * If an error is caught, the test will be considered to have failed,
343
+ * and the `Error` object will be used as the `directive` in the `ok()`
344
+ * call. If no error is caught the test will be considered to have passed.
345
+ *
346
+ * @param {function} testfunc
347
+ * @param {string} [desc]
348
+ * @param {...any} [args]
349
+ * @returns {Log}
350
+ */
351
+ lives (testfunc, desc, ...args)
352
+ {
353
+ const ret = $call(testfunc, args);
354
+ return this.ok(!('err' in ret), desc, ret.err);
191
355
  }
192
356
 
193
357
  /**
@@ -198,15 +362,22 @@ class Test
198
362
  * @param {string} comp - A comparitor to test with.
199
363
  *
200
364
  * - `===`, `is` (See also: `is()`)
201
- * - `!==`, `isnt` (See also: `isnt()`)
365
+ * - `!==`, `isnt`, `not` (See also: `isnt()`)
202
366
  * - `==`, `eq`
203
367
  * - `!=`, `ne`
204
368
  * - `>`, `gt`
205
369
  * - `<`, `lt`
206
370
  * - `>=`, `ge`, `gte`
207
371
  * - `<=`, `le`, `lte`
372
+ *
373
+ * A few special comparitors for *binary flag* testing:
374
+ *
375
+ * - `=&` → `((got & want) === want)`
376
+ * - `!&` → `((got & want) !== want)`
377
+ * - `+&` → `((got & want) !== 0)`
378
+ * - `-&` → `((got & want) === 0)`
208
379
  *
209
- * @param {string} desc
380
+ * @param {string} [desc]
210
381
  * @param {boolean} [stringify=true] Stringify values in TAP output?
211
382
  * @returns {Log}
212
383
  */
@@ -221,6 +392,7 @@ class Test
221
392
  break;
222
393
  case 'isnt':
223
394
  case '!==':
395
+ case 'not':
224
396
  test = (got !== want);
225
397
  break;
226
398
  case 'eq':
@@ -249,19 +421,66 @@ class Test
249
421
  case '>=':
250
422
  test = (got >= want);
251
423
  break;
424
+ case '=&':
425
+ test = ((got&want)===want);
426
+ break;
427
+ case '!&':
428
+ test = ((got&want)!==want)
429
+ case '+&':
430
+ test = ((got&want)!==0);
431
+ break;
432
+ case '-&':
433
+ test = ((got&want)===0);
252
434
  default:
253
435
  test = false;
254
436
  }
255
437
 
256
- const log = this.ok(test, desc);
438
+ let details = null;
257
439
  if (!test)
258
- {
259
- log.details.got = got;
260
- log.details.wanted = want;
261
- log.details.stringify = stringify;
262
- log.details.comparitor = comp;
440
+ { // The test failed, add the deets.
441
+ details =
442
+ {
443
+ got,
444
+ wanted: want,
445
+ stringify,
446
+ comparitor: comp,
447
+ };
263
448
  }
264
- return log;
449
+
450
+ return this.ok(test, desc, null, details);
451
+ }
452
+
453
+ /**
454
+ * See if a string matches a value.
455
+ *
456
+ * @param {string} got
457
+ * @param {RegExp} want
458
+ * @param {string} [desc]
459
+ * @param {boolean} [stringify=true]
460
+ * @returns {Log}
461
+ */
462
+ matches(got, want, desc, stringify=true)
463
+ {
464
+ const no = {error: "matches 'got' value must be a string"};
465
+ needs(got, no, S);
466
+ no.error = "matches 'want' value must be a RegExp";
467
+ needs(want, no, RegExp);
468
+
469
+ const test = want.test(got);
470
+
471
+ let details = null;
472
+ if (!test)
473
+ { // The test failed, add the deets.
474
+ details =
475
+ {
476
+ got,
477
+ wanted: want,
478
+ stringify,
479
+ comparitor: 'matches',
480
+ };
481
+ }
482
+
483
+ return this.ok(test, desc, null, details);
265
484
  }
266
485
 
267
486
  /**
@@ -271,7 +490,7 @@ class Test
271
490
  *
272
491
  * @param {*} got
273
492
  * @param {*} want
274
- * @param {string} desc
493
+ * @param {string} [desc]
275
494
  * @param {boolean} [stringify=true]
276
495
  * @returns {Log}
277
496
  */
@@ -287,8 +506,8 @@ class Test
287
506
  *
288
507
  * @param {*} got - The result value from the test function.
289
508
  * @param {*} want - The value we expected from the test function.
290
- * @param {string} desc
291
- * @param {boolean} [stringify=true] Use JSON details in TAP output?
509
+ * @param {string} [desc]
510
+ * @param {boolean} [stringify=true]
292
511
  * @returns {Log}
293
512
  */
294
513
  isnt (got, want, desc, stringify=true)
@@ -300,57 +519,176 @@ class Test
300
519
  * See if a value is of a certain type.
301
520
  *
302
521
  * @param {*} got
303
- * @param {(string|function)} want - A type name, or a constructor function.
522
+ * @param {Array} wants - Type names or constructor functions.
304
523
  *
305
- * This uses two type checking functions from `@lumsj/core`.
306
- * If this is a string, we use the `isType()` function.
307
- * If this is a constructor function, we use the `isInstance()` function.
524
+ * Uses `@lumjs/core/types.isa()` to perform the test.
308
525
  *
309
- * @param {string} desc
526
+ * @param {string} [desc]
310
527
  * @param {boolean} [stringify=true]
311
528
  * @returns {Log}
312
529
  */
313
- isa (got, want, desc, stringify=true)
530
+ isa (got, wants, desc, stringify=true, not=false)
314
531
  {
315
- let test;
316
- if (typeof want === S)
317
- { // A string, we're going to use the isType function.
318
- test = isType(want, got);
319
- }
320
- else if (typeof want === F)
321
- { // Assuming it's a constructor.
322
- test = isInstance(got, want);
532
+ if (!isArray(wants))
533
+ {
534
+ wants = [wants];
323
535
  }
324
- else
325
- { // That's not supported.
326
- throw new TypeError("'want' must be a string or a constructor");
536
+ let res = types.isa(got, ...wants);
537
+ if (not)
538
+ { // Inverse the result.
539
+ res = !res;
327
540
  }
328
541
 
329
- const log = this.ok(test, desc);
330
- if (!test)
331
- {
332
- log.details.got = got;
333
- log.details.wanted = want;
334
- log.details.stringify = stringify;
542
+ let details = null;
543
+ if (!res)
544
+ { // The test failed, add the deets.
545
+ details =
546
+ {
547
+ got,
548
+ wanted: wants,
549
+ stringify,
550
+ comparitor: not ? 'nota()' : 'isa()',
551
+ };
335
552
  }
336
- return log;
553
+
554
+ return this.ok(res, desc, null, details);
555
+ }
556
+
557
+ /**
558
+ * See if a value is NOT of a certain type.
559
+ *
560
+ * Just inverses the results of `isa()`.
561
+ *
562
+ * @param {*} got
563
+ * @param {Array} wants - Type names of constructor functions.
564
+ * @param {string} [desc]
565
+ * @param {boolean} [stringify=true]
566
+ * @returns {Log}
567
+ */
568
+ nota (got, wants, desc, stringify=true)
569
+ {
570
+ return this.isa(got, wants, desc, stringify, true);
337
571
  }
338
572
 
339
573
  /**
340
574
  * An `is()` test, but encode both values as JSON first.
341
575
  *
576
+ * Actually uses `core.types.stringify()` so it supports more
577
+ * data types than standard JSON, and can stringify functions,
578
+ * symbols, and several extended object types.
579
+ *
342
580
  * @param {*} got
343
581
  * @param {*} want
344
- * @param {string} desc
345
- * @returns
582
+ * @param {string} [desc]
583
+ * @returns {Log}
346
584
  */
347
585
  isJSON (got, want, desc)
348
586
  {
349
- got = JSON.stringify(got);
350
- want = JSON.stringify(want);
587
+ got = this.stringify(got);
588
+ want = this.stringify(want);
351
589
  return this.is(got, want, desc, false);
352
590
  }
353
591
 
592
+ /**
593
+ * An `isnt()` test, but encode both values as JSON first.
594
+ *
595
+ * Like `isJSON()` this uses `core.types.stringify()`.
596
+ *
597
+ * @param {*} got
598
+ * @param {*} want
599
+ * @param {string} [desc]
600
+ * @returns {Log}
601
+ */
602
+ isntJSON (got, want, desc)
603
+ {
604
+ got = this.stringify(got);
605
+ want = this.stringify(want);
606
+ return this.isnt(got, want, desc, false);
607
+ }
608
+
609
+ /**
610
+ * Run a function and see if it's return value is what we wanted.
611
+ *
612
+ * @param {function} testfunc - The function to run.
613
+ * The return value will be passed to `cmp()` or another appropriate
614
+ * testing method as determined by the options.
615
+ * How this handles error handling is determined by options as well.
616
+ *
617
+ * @param {*} want - The value we want.
618
+ * @param {(object|string)} [opts] Named options for further behaviour.
619
+ * If it is a string it's considered the `opts.desc` option.
620
+ * @param {string} [opts.desc] A description for `ok()`.
621
+ * @param {boolean} [opts.stringify=true]
622
+ * @param {Array} [opts.args] Arguments to pass to the test function.
623
+ * @param {string} [opts.comp="is"] - The comparitor to test with.
624
+ * In addition to all of the comparitors from `cmp()`, there are a few
625
+ * extra comparitors that will pass through to other methods:
626
+ * - `isa` → Use `isa()` to test return value.
627
+ * - `nota` → Use `nota()` to test return value.
628
+ * - `=json`, `isJSON` → Use `isJSON()` to test return value.
629
+ * - `!json`, `isntJSON` → Use `isntJSON()` to test return value.
630
+ * - `matches` → Use `matches()` to test return value.
631
+ *
632
+ * @param {boolean} [opts.thrown=false] How to handle thrown errors.
633
+ *
634
+ * If this is `true`, then anything thrown will be passed as if it was
635
+ * the return value from the function.
636
+ *
637
+ * If this is `false`, then any errors thrown will result in an immediate
638
+ * failure of the test without any further processing, and the error will
639
+ * be passed as the `directive` to the `ok()` method.
640
+ *
641
+ * @returns {Log}
642
+ */
643
+ callIs (testfunc, want, opts={})
644
+ {
645
+ const args = opts.args ?? [];
646
+ const ret = $call(testfunc, args);
647
+ const desc = opts.desc;
648
+
649
+ let got;
650
+
651
+ if (ret.err)
652
+ { // How to handle errors.
653
+ if (opts.thrown)
654
+ { // We're going to test the error.
655
+ got = ret.err;
656
+ }
657
+ else
658
+ { // This is an automatic failure.
659
+ return this.ok(false, desc, ret.err);
660
+ }
661
+ }
662
+ else
663
+ { // No errors, good, testing against the return value.
664
+ got = ret.val;
665
+ }
666
+
667
+ const CFUN =
668
+ {
669
+ 'matches': 'matches',
670
+ 'isa': 'isa',
671
+ 'nota': 'nota',
672
+ 'isJSON': 'isJSON',
673
+ '=json': 'isJSON',
674
+ 'isntJSON': 'isntJSON',
675
+ '!json': 'isntJSON',
676
+ };
677
+
678
+ const comp = opts.comp ?? 'is';
679
+ const stringify = opts.stringify ?? true;
680
+
681
+ if (typeof CFUN[comp] === S)
682
+ { // A function with a custom return value.
683
+ const meth = CFUN[comp];
684
+ return this[meth](got, want, desc, stringify);
685
+ }
686
+ else
687
+ { // We're going to use the cmp() method.
688
+ return this.cmp(got, want, comp, desc, stringify);
689
+ }
690
+ }
691
+
354
692
  /**
355
693
  * Skip a test.
356
694
  *
@@ -360,7 +698,7 @@ class Test
360
698
  */
361
699
  skip (reason, desc)
362
700
  {
363
- var log = this.ok(true, desc);
701
+ const log = this.ok(true, desc);
364
702
  log.skipped = true;
365
703
  if (typeof reason === S)
366
704
  log.skippedReason = reason;
@@ -381,6 +719,78 @@ class Test
381
719
  this.log.push(msg);
382
720
  }
383
721
 
722
+ /**
723
+ * Run an assortment of tests using a map.
724
+ *
725
+ * The *current* test method defaults to `ok`.
726
+ *
727
+ * @param {...any} tests - The tests we're running.
728
+ *
729
+ * If this is a `string`, and is the name of one of the standard testing
730
+ * methods in this class, it will be set as the *current* test method.
731
+ *
732
+ * If this is a `function`, it will be set as the *current* test method.
733
+ * By default function test methods are passed to `call()` with the test
734
+ * parameters. However, if the *previous* test method was `callIs` then
735
+ * the `callIs()` method will be used as long as the custom function is
736
+ * the *current* test method. Likewise to switch back to `call()` simply
737
+ * set the *current* test method to `call` before setting it to a new custom
738
+ * test `function`.
739
+ *
740
+ * If this is an `Array` then it's the parameters for the *current* test
741
+ * method. If a custom `function` is in use, remember that it's the
742
+ * `call()` or `callIs()` methods that will be being called, with their
743
+ * first parameter always being the custom function.
744
+ *
745
+ * Any value other than one of those will throw a `TypeError`.
746
+ *
747
+ * @returns {Log[]} A `Log` item for each test that was ran.
748
+ */
749
+ run (...tests)
750
+ {
751
+ const CF = 'call';
752
+ const CI = 'callIs';
753
+
754
+ const logs = [];
755
+ let funcall = CF;
756
+ let current = 'ok';
757
+
758
+ for (const test of tests)
759
+ {
760
+ const tt = typeof test;
761
+ if (tt === S && TEST_METHODS.includes(test))
762
+ { // Set the current test to a built-in.
763
+ current = test;
764
+ }
765
+ else if (tt === F)
766
+ { // A custom test function for further tests.
767
+ if (current === CI)
768
+ { // Last test was `callIs` using that for the custom function.
769
+ funcall = CI;
770
+ }
771
+ else if (current === CF)
772
+ { // Last test was `call`, using that for the custom function.
773
+ funcall = CF;
774
+ }
775
+ }
776
+ else if (isArray(test))
777
+ { // A set of test parameters.
778
+ let log;
779
+ if (typeof current === F)
780
+ { // A custom test function is in use.
781
+ log = this[funcall](current, ...test);
782
+ }
783
+ else
784
+ { // A standard test is in use.
785
+ log = this[current](...test);
786
+ }
787
+ logs.push(log);
788
+ }
789
+ }
790
+
791
+ return logs;
792
+ }
793
+
384
794
  /**
385
795
  * Return TAP formatted output for all the tests.
386
796
  *
@@ -388,22 +798,21 @@ class Test
388
798
  */
389
799
  tap ()
390
800
  {
391
- var out = '';
801
+ let out = '';
392
802
  if (this.planned > 0)
393
803
  {
394
804
  out += '1..'+this.planned+"\n";
395
805
  }
396
- var t = 1;
397
- for (var i = 0; i < this.log.length; i++)
806
+ let t = 1;
807
+ for (const log of this.log)
398
808
  {
399
- var log = this.log[i];
400
809
  if (log instanceof Log)
401
810
  {
402
811
  out += log.tap(t++);
403
812
  }
404
813
  else
405
814
  { // A comment.
406
- out += '# ' + (typeof log === S ? log : JSON.stringify(log)) + "\n";
815
+ out += '# ' + (typeof log === S ? log : types.stringify(log)) + "\n";
407
816
  }
408
817
  }
409
818
  if (this.skipped)
@@ -417,7 +826,7 @@ class Test
417
826
  out += ' out of '+this.planned;
418
827
  out += "\n";
419
828
  }
420
- var ran = t-1;
829
+ const ran = t-1;
421
830
  if (this.planned > 0 && this.planned != ran)
422
831
  {
423
832
  out += '# Looks like you planned '+this.planned+' but ran '+ran+" tests\n";
@@ -425,8 +834,28 @@ class Test
425
834
  return out;
426
835
  }
427
836
 
837
+ /**
838
+ * A calculated property of the number of tests that were ran.
839
+ * @type {int}
840
+ */
841
+ get ran ()
842
+ {
843
+ let ran = 0;
844
+ for (const log of this.log)
845
+ {
846
+ if (log instanceof Log)
847
+ {
848
+ ran++;
849
+ }
850
+ }
851
+ return ran;
852
+ }
853
+
428
854
  /**
429
855
  * Send the TAP output to the `console`.
856
+ *
857
+ * This is a low-level method and is no longer recommended for use.
858
+ * Instead call the `done()` method, which will *do the right thing*.
430
859
  */
431
860
  output ()
432
861
  {
@@ -434,10 +863,58 @@ class Test
434
863
  return this;
435
864
  }
436
865
 
866
+ /**
867
+ * We're done testing.
868
+ *
869
+ * This will mark the test-set as finished, so attempting to run further
870
+ * tests after will result in a `RangeError` being thrown.
871
+ *
872
+ * If no `Harness` is in use, this will also run `this.output()`.
873
+ */
874
+ done ()
875
+ {
876
+ if (this.$done)
877
+ {
878
+ throw new RangeError('Test set is already done');
879
+ }
880
+ this.$done = true;
881
+
882
+ return (this.harness ? this : this.output());
883
+ }
884
+
437
885
  } // class Test
438
886
 
439
887
  // Should never need this, but...
440
888
  def(Test, 'Log', Log);
441
889
 
890
+ // Probably don't need this either, but...
891
+ def(Test, '$call', $call);
892
+
893
+ // Methods we're exporting for the 'functional' API.
894
+ def(Test, '$METHODS',
895
+ {
896
+ test: TEST_METHODS,
897
+ meta: META_METHODS,
898
+ get all()
899
+ {
900
+ const list = [];
901
+ for (const name in this)
902
+ {
903
+ if (name === 'all') continue;
904
+ const prop = this[name];
905
+ if (isArray(prop))
906
+ {
907
+ list.push(...prop);
908
+ }
909
+ }
910
+ return list;
911
+ },
912
+ });
913
+
442
914
  // Export the class
443
915
  module.exports = Test;
916
+
917
+ // Finally at the bottom after `module.exports` has been set, we will load
918
+ // the Harness class to avoid circular references failing.
919
+ const Harness = require('./harness');
920
+