@lumjs/tests 1.9.0 → 2.1.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/CHANGELOG.md CHANGED
@@ -11,6 +11,61 @@ and a reference to a property of a module will be in `@tests.propName` format.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [2.1.0] - 2025-11-26
15
+ ### Changed
16
+ - Bumped core version to newest release with changes to _stringify_.
17
+ - Updated `test/stats` to support new stringify API changes.
18
+ - Updated `test/log` to use stringify from the test instance.
19
+ - Changed how `harness/node` tests to see if a file should be used as a test.
20
+ - addDir() method now composes the Harness options with opts argument.
21
+ - A new `matchFiles` option can specify a custom function that will
22
+ be passed the file information object and the return value will be
23
+ used in a boolean context to determine if the file is a test or not.
24
+ - If specified, `matchFiles` will be used _instead of_ the `ext` option.
25
+ - The `ext` option now checks for `/`, `(` or `[` characters,
26
+ which would indicate it is a RegExp pattern of some sort.
27
+ - `\.[cm]?js$` would match `.js`, `.cjs`, and `.mjs` files.
28
+ - `/\.js$/i` would match `.js`, `.JS`, `.Js`, and `.jS` files.
29
+ - If no special characters are found, the value is assumed to be
30
+ a single, case-sensitive file extension.
31
+ - The default is still `.js` if the `ext` option was not specified.
32
+
33
+ ## [2.0.0] - 2024-08-14
34
+ ### Added
35
+ - The `Stats` class (and `Test` which extends it) now has async support.
36
+ - A new `.waiting` integer property indicates if async calls are in use.
37
+ - New `opts.async` nested options added to constructor options.
38
+ - The `async()` method takes an async function that returns a Promise,
39
+ increments the `.waiting` property, and then decrements it once the
40
+ returned Promise is either resolved or rejected.
41
+ - A `wait()` method is used by the `done()` method, and the Harness
42
+ to wait for all async calls to finish.
43
+ - A new `registerGlobal` option for the `Harness` constructor that if `true`
44
+ will make it assign the instance to a special global variable.
45
+ ### Changed
46
+ - The `Harness` constructor now requires a `Plugin` as a mandatory
47
+ parameter. No more auto-detection or other crufty magic.
48
+ - The `bin/lumtest.js` script was updated with the API changes.
49
+ - The `Plugin` constructor no longer takes the Harness instance as
50
+ a paramter. The `harness` property will be assigned directly by
51
+ the harness instance instead.
52
+ - `Plugin` class no longer extends the deprecated `core.AbstractClass`.
53
+ - `Harness` has support for the new async functionality.
54
+ - The `run()` method is now explicitly `async`, as are its sub-methods.
55
+ - Moved Harness Plugins into a `harness/plugin` sub-folder.
56
+ - `Stats` can find the harness via the `registerGlobal` feature.
57
+ It still supports the CommonJS `require.main` method, but no
58
+ longer depends on it.
59
+ - The `functional` module now supports using a different name
60
+ for the exported function than the underlying method. Useful
61
+ as the `async()` method is a reserved word and cannot be used
62
+ as a variable/function name (so it's renamed to `callAsync()`).
63
+ - A bunch of extra modules are exported directly now.
64
+ - Renamed some constructors for easier debugging.
65
+ ### Removed
66
+ - The `harness/browser` plugin that I never implemented.
67
+ That's better done in a separate package.
68
+
14
69
  ## [1.9.0] - 2024-07-27
15
70
  ### Changed
16
71
  - Renamed `Harness` constructor name to `LumTestsHarness` to be more unique.
@@ -149,7 +204,8 @@ and a reference to a property of a module will be in `@tests.propName` format.
149
204
  - Ported from Lum.js v4 library set.
150
205
  - Added a few more features from the PHP version.
151
206
 
152
- [Unreleased]: https://github.com/supernovus/lum.tests.js/compare/v1.9.0...HEAD
207
+ [Unreleased]: https://github.com/supernovus/lum.tests.js/compare/v2.0.0...HEAD
208
+ [2.0.0]: https://github.com/supernovus/lum.tests.js/compare/v1.9.0...v2.0.0
153
209
  [1.9.0]: https://github.com/supernovus/lum.tests.js/compare/v1.8.0...v1.9.0
154
210
  [1.8.0]: https://github.com/supernovus/lum.tests.js/compare/v1.7.1...v1.8.0
155
211
  [1.7.1]: https://github.com/supernovus/lum.tests.js/compare/v1.7.0...v1.7.1
package/README.md CHANGED
@@ -6,15 +6,6 @@ This is nowhere near as advanced as many of the other options out there,
6
6
  but does what I need it to do, and works with the TAP testing protocol that
7
7
  the Perl and Raku programming languages popularized.
8
8
 
9
- ## Exports
10
-
11
- | Name | Description |
12
- | -------------------- | ---------------------------------------------------- |
13
- | `Test` | The core class for testing. |
14
- | `Harness` | A class for running a bunch of tests together. |
15
- | `functional()` | A function for using functional-style testing. |
16
- | `new()` | A function for getting a new `Test` instance. |
17
-
18
9
  ## Official URLs
19
10
 
20
11
  This library can be found in two places:
package/TODO.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # TODO
2
2
 
3
- - Update to properly support `async` tests; the `harness` will need some work.
3
+ - Add explicit support for running ES Module based tests.
4
+ - An alternative to passing `module` object to test instances.
4
5
  - Write tests for:
5
6
  - `call()`
6
7
  - `diesWith()`
7
8
  - `callIs()`
8
9
  - `matches()`
9
10
  - `run()`
11
+ - `async()` (currently used in `@lumjs/encode:v2.0.0` package).
10
12
  - Test the `harness/parser` and associated `grammar/tap` libraries.
11
13
  - Test the *external* test mode in `Harness`.
12
14
 
package/bin/lumtest.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  const process = require('node:process');
3
3
  const Harness = require('../lib/harness');
4
+ const NodePlugin = require('../lib/harness/plugin/node');
4
5
  const core = require('@lumjs/core');
5
6
  const {S} = core.types;
6
7
 
@@ -59,7 +60,8 @@ for (const arg of args)
59
60
  }
60
61
  }
61
62
 
62
- const harness = new Harness(hOpts); // Create our Harness instance.
63
- module.exports = harness; // Export it as the 'main' module.
64
- harness.addDir(aDir, aOpts); // Add all the files in the test dir.
65
- harness.run(rPlan); // Run all the tests.
63
+ // Create our Harness instance.
64
+ const harness = new Harness(NodePlugin, hOpts);
65
+ module.exports = harness; // Export it as the 'main' module.
66
+ harness.addDir(aDir, aOpts); // Add all the files in the test dir.
67
+ harness.run(rPlan); // Run all the tests.
@@ -1,5 +1,5 @@
1
1
  const core = require('@lumjs/core');
2
- const {def,lazy,context:ctx} = core;
2
+ const {def,lazy} = core;
3
3
  const {B,F} = core.types;
4
4
  const QueuedTest = require('./queuedtest');
5
5
  const Plugin = require('./plugin');
@@ -12,16 +12,13 @@ const Plugin = require('./plugin');
12
12
  *
13
13
  * @prop {object} opts - Options passed to constructor
14
14
  *
15
- * @prop {object} testSuite - A `Stats` (or `Test`) instance
15
+ * @prop {module:@lumjs/tests/test/stats} testSuite
16
16
  *
17
17
  * This meta-test will keep track of the success or failure of
18
18
  * all the other tests. So its `plan` will be equal to the number of
19
19
  * tests we're running, and so forth.
20
20
  *
21
- * @prop {object} plugin - A platform-specific `Plugin`
22
- *
23
- * This may be provided as a parameter to the constructor,
24
- * or auto-selected based on our Javascript environment.
21
+ * @prop {module:@lumjs/tests/harness/plugin} plugin - JS platform plugin
25
22
  *
26
23
  * @prop {Array} queue - An array of `QueuedTest` instances
27
24
  * representing tests that we want to run.
@@ -38,15 +35,12 @@ class LumTestsHarness
38
35
  /**
39
36
  * Build a new Harness
40
37
  *
41
- * @param {object} [opts] Options
42
- * @param {object} [opts.testSuite] A `Test` or `Stats` instance
43
- *
44
- * This is probably not needed. We'll generate a new `Stats` instance.
38
+ * @param {module:@lumjs/tests/harness/plugin} plugin
39
+ * The plugin for the current JS environment.
45
40
  *
46
- * @param {object} [opts.plugin] A `Plugin` instance.
41
+ * You can pass an instance `object`, or a class constructor `function`.
47
42
  *
48
- * This is probably not needed. We'll automatically determine a default
49
- * plugin to use based on the JS environment.
43
+ * @param {object} [opts] Advanced Options
50
44
  *
51
45
  * @param {boolean} [opts.plannedFailure=true] Is a broken plan a failure?
52
46
  *
@@ -63,9 +57,35 @@ class LumTestsHarness
63
57
  *
64
58
  * If this is `false` then no warning will be shown.
65
59
  *
60
+ * @param {boolean} [opts.registerGlobal=false] Add a global variable?
61
+ *
62
+ * If this is `true`, a global variable called `__lum_tests_harness__`
63
+ * will be registered as a pointer to this instance.
64
+ *
65
+ * @param {module:@lumjs/tests/test/stats} [opts.testSuite] Meta-test
66
+ *
67
+ * This option is generally for internal (mostly debugging) use only.
68
+ * Don't use it, just let the constructor build a new `Stats` instance.
69
+ *
66
70
  */
67
- constructor(opts={})
71
+ constructor(plugin, opts={})
68
72
  {
73
+ if (typeof plugin === F && Plugin.isPrototypeOf(plugin))
74
+ { // Class constructor was passed, build an instance.
75
+ plugin = new plugin();
76
+ }
77
+
78
+ if (plugin instanceof Plugin)
79
+ { // An explicit plugin was specified.
80
+ this.plugin = plugin;
81
+ plugin.harness = this;
82
+ }
83
+ else
84
+ { // That's not valid...
85
+ console.error({plugin, opts, harness: this});
86
+ throw new TypeError('Invalid Plugin specified');
87
+ }
88
+
69
89
  // Save a reference to the options.
70
90
  this.opts = opts;
71
91
 
@@ -78,23 +98,6 @@ class LumTestsHarness
78
98
  this.testSuite = new Stats(opts.testSuite);
79
99
  }
80
100
 
81
- if (opts.plugin instanceof Plugin)
82
- {
83
- this.plugin = opts.plugin;
84
- }
85
- else if (ctx.isBrowser)
86
- {
87
- this.plugin = require('./browser').new(this);
88
- }
89
- else if (ctx.isNode)
90
- {
91
- this.plugin = require('./node').new(this);
92
- }
93
- else
94
- {
95
- throw new Error('Could not determine plugin');
96
- }
97
-
98
101
  //console.debug("# ~ plugin", this.plugin, this);
99
102
 
100
103
  this.queue = []; // Tests to be ran.
@@ -105,6 +108,11 @@ class LumTestsHarness
105
108
  this.plannedFailure = opts.plannedFailure ?? true;
106
109
  this.plannedWarning = opts.plannedWarning ?? true;
107
110
 
111
+ if (opts.registerGlobal)
112
+ { // Add a global variable.
113
+ globalThis[Stats.HARNESS_GLOBAL] = this;
114
+ }
115
+
108
116
  } // constructor()
109
117
 
110
118
  /**
@@ -177,9 +185,9 @@ class LumTestsHarness
177
185
  * Run all the queued tests
178
186
  *
179
187
  * @param {boolean} [plan=true] Set a test plan for the entire suite
180
- * @returns {object} `this`
188
+ * @returns {Promise<object>} Resolves to `this`
181
189
  */
182
- run(plan=true)
190
+ async run(plan=true)
183
191
  {
184
192
  if (plan)
185
193
  {
@@ -188,15 +196,15 @@ class LumTestsHarness
188
196
 
189
197
  for (const queued of this.queue)
190
198
  {
191
- this.runTest(queued);
199
+ await this.runTest(queued);
192
200
  }
193
201
 
194
202
  this.testSuite.done();
195
203
 
196
- return this;
204
+ return this
197
205
  }
198
206
 
199
- runTest(queued)
207
+ async runTest(queued)
200
208
  {
201
209
  const name = queued.filename;
202
210
  let test;
@@ -218,22 +226,42 @@ class LumTestsHarness
218
226
  }
219
227
  else
220
228
  { // Evaluate if the test is successful or not.
221
- const ok = this.plugin.ok(test);
222
- if (typeof ok === B)
223
- { // A simple boolean response
224
- this.testSuite.ok(ok, name);
225
- }
226
- else if (ok instanceof Error)
227
- { // An error was returned
228
- this.testSuite.fail(name, ok);
229
+ if (test.waiting)
230
+ { // async tests running
231
+ try
232
+ {
233
+ await test.wait();
234
+ this.testTest(test, name);
235
+ }
236
+ catch (err)
237
+ {
238
+ this.testSuite.fail(name, err);
239
+ }
229
240
  }
230
- else
231
- { // This should never happen...
232
- throw new Error("Invalid result from plugin.ok() !!");
241
+ else
242
+ {
243
+ this.testTest(test, name);
233
244
  }
234
245
  }
235
246
 
236
- return this;
247
+ return test;
248
+ }
249
+
250
+ testTest(test, name)
251
+ {
252
+ const ok = this.plugin.ok(test);
253
+ if (typeof ok === B)
254
+ { // A simple boolean response
255
+ this.testSuite.ok(ok, name);
256
+ }
257
+ else if (ok instanceof Error)
258
+ { // An error was returned
259
+ this.testSuite.fail(name, ok);
260
+ }
261
+ else
262
+ { // This should never happen...
263
+ throw new Error("Invalid result from plugin.ok() !!");
264
+ }
237
265
  }
238
266
 
239
267
  tap()
@@ -1,4 +1,4 @@
1
- const {S,N,needType,isArray,isObj} = require('@lumjs/core/types');
1
+ const {S,N,needType,isArray} = require('@lumjs/core/types');
2
2
  const Stats = require('../test/stats');
3
3
  const Tap = require('../grammar/tap');
4
4
 
@@ -21,7 +21,7 @@ const T = 'test',
21
21
  *
22
22
  * @exports module:@lumjs/tests/harness/parser
23
23
  */
24
- class Parser
24
+ class LumTestParser
25
25
  {
26
26
  /**
27
27
  * Build a TAP Parser.
@@ -145,9 +145,9 @@ class Parser
145
145
 
146
146
  static new(opts)
147
147
  {
148
- return new Parser(opts);
148
+ return new LumTestParser(opts);
149
149
  }
150
150
  }
151
151
 
152
152
  // Export the class as the parser.
153
- module.exports = Parser;
153
+ module.exports = LumTestParser;
@@ -1,34 +1,28 @@
1
1
  const core = require('@lumjs/core');
2
- const {AbstractClass} = core;
3
- const {TestFailure,PlanFailure} = require('./errors');
2
+ const {AbstractError} = core;
3
+ const {TestFailure,PlanFailure} = require('../errors');
4
4
 
5
5
  /**
6
6
  * An abstract base class for Harness plugins
7
7
  *
8
- * @exports module:@lumjs/tests/harness~Plugin
8
+ * @prop {module:@lumjs/tests/harness} harness
9
+ * The harness instance will assign itself to this property.
10
+ *
11
+ * @exports module:@lumjs/tests/harness/plugin
9
12
  */
10
- class Plugin extends AbstractClass
13
+ class Plugin
11
14
  {
12
- /**
13
- * (Internal class constructor)
14
- * @param {module:@lumjs/tests/harness} harness - Parent `Harness` instance
15
- */
16
- constructor(harness)
17
- {
18
- super();
19
- this.harness = harness;
20
- this.$needs('run');
21
- }
22
-
23
15
  /**
24
16
  * (ABSTRACT METHOD) Run a test
25
17
  *
26
18
  * This method must be implemented by every plugin.
27
19
  *
28
- * @name module:@lumjs/tests/harness~Plugin#run
29
- * @function
30
20
  * @param {object} queued - A queued test object
31
21
  */
22
+ run(queued)
23
+ {
24
+ throw new AbstractError("run() method not implemented");
25
+ }
32
26
 
33
27
  /**
34
28
  * See if a test had failures
@@ -67,11 +61,6 @@ class Plugin extends AbstractClass
67
61
  return true;
68
62
  }
69
63
 
70
- static new(harness)
71
- {
72
- return new this(harness);
73
- }
74
-
75
64
  }
76
65
 
77
66
  module.exports = Plugin;
@@ -1,17 +1,19 @@
1
1
  // Node.js harness plugin
2
2
  const core = require('@lumjs/core');
3
- const {N} = core.types;
4
- const Plugin = require('./plugin');
3
+ const {N,F,S} = core.types;
4
+ const Plugin = require('./index');
5
5
  const cp = require('node:child_process');
6
6
  const getCwd = require('node:process').cwd;
7
7
  const fs = require('node:fs');
8
8
  const path = require('node:path');
9
+ const RE = /[\/\(\[]+/;
10
+ const WS = /\s+/g;
9
11
 
10
12
  /**
11
13
  * Node.js Harness plugin
12
14
  *
13
- * @exports module:@lumjs/tests/harness~NodePlugin
14
- * @extends module:@lumjs/tetss/harness~Plugin
15
+ * @exports module:@lumjs/tests/harness/plugin/node
16
+ * @extends module:@lumjs/tetss/harness/plugin
15
17
  */
16
18
  class NodePlugin extends Plugin
17
19
  {
@@ -41,17 +43,43 @@ class NodePlugin extends Plugin
41
43
  }
42
44
  }
43
45
 
44
- addDir(dir, opts={}, recurse)
46
+ addDir(dir, opts, recurse)
45
47
  {
48
+ opts = Object.assign({}, this.harness.options, opts);
49
+
46
50
  if (typeof recurse !== N && typeof opts.recurse === N)
47
51
  {
48
52
  recurse = opts.recurse;
49
53
  }
50
54
 
51
- const ext = opts.ext ?? '.js';
52
55
  const testOpts = opts.test ?? opts;
53
56
  const files = fs.readdirSync(dir, {encoding: 'utf8', withFileTypes: true});
54
57
 
58
+ let isTest = opts.matchFiles;
59
+ if (typeof isTest !== F)
60
+ { // No custom test, let's make one.
61
+ let ext = opts.ext ?? '.js';
62
+
63
+ if (typeof ext === S)
64
+ {
65
+ ext = ext.trim();
66
+ if (ext.match(RE))
67
+ { // A RegExp pattern string of some sort.
68
+ ext = ext.split('/').map(v => v.replaceAll(WS, '')).filter(v => v !== '');
69
+ ext = new RegExp(...ext);
70
+ }
71
+ else
72
+ { // A simple file extension.
73
+ isTest = (file) => file.name.endsWith(ext);
74
+ }
75
+ }
76
+
77
+ if (ext instanceof RegExp)
78
+ { // RegExp filename test.
79
+ isTest = (file) => file.name.match(ext);
80
+ }
81
+ }
82
+
55
83
  for (const file of files)
56
84
  {
57
85
  if (file.name === '.' || file.name === '..') continue;
@@ -59,7 +87,7 @@ class NodePlugin extends Plugin
59
87
  { // Recurse to a nested directory.
60
88
  this.addDir(path.join(dir, file.name), opts, recurse-1);
61
89
  }
62
- else if (file.isFile() && file.name.endsWith(ext))
90
+ else if (file.isFile() && isTest(file))
63
91
  { // It would seem to be a valid test.
64
92
  this.harness.addTest(path.join(dir, file.name), testOpts);
65
93
  }
@@ -56,11 +56,22 @@ function functional(opts={}, testClass=Test)
56
56
  const proxyMethods = testClass.$METHODS.all;
57
57
  const test = new testClass(opts);
58
58
  const functions = { test };
59
- for (const meth of proxyMethods)
59
+ for (const mdef of proxyMethods)
60
60
  {
61
- functions[meth] = function ()
61
+ let sname, tname;
62
+ if (Array.isArray(mdef))
63
+ { // separate source and target names
64
+ sname = mdef[0];
65
+ tname = mdef[1];
66
+ }
67
+ else
68
+ { // same names for both
69
+ sname = tname = mdef;
70
+ }
71
+
72
+ functions[tname] = function ()
62
73
  {
63
- return test[meth](...arguments);
74
+ return test[sname](...arguments);
64
75
  }
65
76
  }
66
77
  return functions;
package/lib/test/index.js CHANGED
@@ -9,12 +9,13 @@ const TEST_METHODS =
9
9
  [
10
10
  'ok', 'call', 'callIs', 'fail', 'pass', 'dies', 'diesWith', 'lives', 'cmp',
11
11
  'is', 'isnt', 'isa', 'nota', 'isJSON', 'isntJSON', 'matches', 'skip',
12
+ ['async', 'callAsync'],
12
13
  ];
13
14
 
14
15
  // A list of other methods to export that are not standard tests.
15
16
  const META_METHODS =
16
17
  [
17
- 'plan', 'diag', 'run', 'tap', 'output', 'done',
18
+ 'plan', 'diag', 'run', 'tap', 'output', 'done', 'wait',
18
19
  ];
19
20
 
20
21
  // The function that powers `Test.call()` and friends.
@@ -40,10 +41,10 @@ function $call (testfunc, args)
40
41
  * Raku's Test libraries.
41
42
  *
42
43
  * @exports module:@lumjs/tests/test
43
- * @extends module:@lumjs/tests/test~Base
44
+ * @extends module:@lumjs/tests/test/stats
44
45
  *
45
46
  */
46
- class Test extends Stats
47
+ class LumTest extends Stats
47
48
  {
48
49
  /**
49
50
  * Build a new Test instance.
@@ -621,16 +622,16 @@ class Test extends Stats
621
622
  } // class Test
622
623
 
623
624
  // May want this for sub-classes.
624
- def(Test, 'Stats', Stats);
625
+ def(LumTest, 'Stats', Stats);
625
626
 
626
627
  // Should never need this, but...
627
- def(Test, 'Log', Log);
628
+ def(LumTest, 'Log', Log);
628
629
 
629
630
  // Probably don't need this either, but...
630
- def(Test, '$call', $call);
631
+ def(LumTest, '$call', $call);
631
632
 
632
633
  // Methods we're exporting for the 'functional' API.
633
- def(Test, '$METHODS',
634
+ def(LumTest, '$METHODS',
634
635
  {
635
636
  test: TEST_METHODS,
636
637
  meta: META_METHODS,
@@ -685,4 +686,4 @@ def(Test, '$METHODS',
685
686
  }); // Test.$METHODS
686
687
 
687
688
  // Export the class
688
- module.exports = Test;
689
+ module.exports = LumTest;
package/lib/test/log.js CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  const types = require('@lumjs/core').types;
3
- const {S,O,isArray,stringify,def} = types;
3
+ const {S,O,isArray,def} = types;
4
4
 
5
5
  /**
6
6
  * A log representing the results of a test.
@@ -22,7 +22,7 @@ const {S,O,isArray,stringify,def} = types;
22
22
  * @property {object} [details.info] An optional array of extra information.
23
23
  *
24
24
  */
25
- class Log
25
+ class LumTestLog
26
26
  {
27
27
  /**
28
28
  * (Internal Constructor)
@@ -37,7 +37,6 @@ class Log
37
37
  this.desc = null;
38
38
  this.directive = null;
39
39
  this.details = {};
40
- this.stringifyDepth = test.stringifyDepth;
41
40
  def(this, '$test$', test);
42
41
  }
43
42
 
@@ -52,7 +51,7 @@ class Log
52
51
  */
53
52
  tap (num)
54
53
  {
55
- const SD = this.stringifyDepth;
54
+ const test = this.$test$;
56
55
 
57
56
  var out;
58
57
  if (this.ok)
@@ -81,13 +80,13 @@ class Log
81
80
  const stringy = this.details.stringify;
82
81
 
83
82
  let got = this.details.got;
84
- if (stringy) got = stringify(got, SD);
83
+ if (stringy) got = test.stringify(got);
85
84
  out += `# got: ${got}\n`;
86
85
 
87
86
  if ('wanted' in this.details)
88
87
  {
89
88
  let want = this.details.wanted;
90
- if (stringy) want = stringify(want, SD);
89
+ if (stringy) want = test.stringify(want);
91
90
  out += `# expected: ${want}\n`;
92
91
  }
93
92
 
@@ -106,7 +105,9 @@ class Log
106
105
 
107
106
  for (const i in info)
108
107
  {
109
- const line = (typeof info[i] === S) ? info[i] : stringify(info[i], SD);
108
+ const line = (typeof info[i] === S)
109
+ ? info[i]
110
+ : test.stringify(info[i]);
110
111
  out += `# - ${line}\n`;
111
112
  }
112
113
  }
@@ -117,4 +118,4 @@ class Log
117
118
  } // class Log
118
119
 
119
120
  // Export the class itself.
120
- module.exports = Log;
121
+ module.exports = LumTestLog;
package/lib/test/stats.js CHANGED
@@ -1,8 +1,21 @@
1
1
  const core = require('@lumjs/core');
2
2
  const mods = require('@lumjs/core/modules');
3
3
  const {types} = core;
4
- const {S,N,isObj,def} = types;
4
+ const {S,N,F,isObj,def} = types;
5
5
  const Log = require('./log');
6
+ const HARNESS_GLOBAL = '__lum_tests_harness__';
7
+
8
+ const DEFAULT_ASYNC_OPTS =
9
+ {
10
+ timeout: 30000,
11
+ interval: 5,
12
+ }
13
+
14
+ const DEFAULT_STRINGIFY_OPTS =
15
+ {
16
+ maxDepth: 1,
17
+ jsonObjects: true,
18
+ }
6
19
 
7
20
  /**
8
21
  * Minimalistic base class for the `Test` library.
@@ -11,7 +24,7 @@ const Log = require('./log');
11
24
  * It's extremely minimal and does not provide the core testing methods.
12
25
  * See the [Test]{@link module:@lumjs/tests/test} class for that.
13
26
  *
14
- * @alias module:@lumjs/tests/test~Stats
27
+ * @alias module:@lumjs/tests/test/stats
15
28
  *
16
29
  * @property {number} planned - Number of tests planned, `0` if unplanned.
17
30
  * @property {number} failed - Number of tests that failed.
@@ -21,7 +34,7 @@ const Log = require('./log');
21
34
  * @property {boolean} isTop - Test module was loaded from the command line.
22
35
  * @property {?object} harness - The top-level `Harness` if one was found.
23
36
  */
24
- class Stats
37
+ class LumTestStats
25
38
  {
26
39
  /**
27
40
  * Build a new instance.
@@ -42,6 +55,10 @@ class Stats
42
55
  * @param {number} [opts.stringify=1] The depth `stringify()` should recurse
43
56
  * objects and Arrays before switching to plain JSON stringification.
44
57
  *
58
+ * @param {object} [opts.async] Options for async tests
59
+ * @param {number} [opts.async.timeout=30000] Max time to wait (in ms)
60
+ * @param {number} [opts.async.interval=5] Status check every (in ms)
61
+ *
45
62
  */
46
63
  constructor (opts={})
47
64
  {
@@ -69,11 +86,16 @@ class Stats
69
86
  this.id = null;
70
87
  }
71
88
 
72
- this.stringifyDepth = opts.stringify ?? 1;
73
-
89
+ this.stringifyOpts = Object.assign({},
90
+ DEFAULT_STRINGIFY_OPTS,
91
+ opts.stringify);
92
+
74
93
  this.failed = 0;
75
94
  this.skipped = 0;
76
95
  this.planned = 0;
96
+ this.waiting = 0;
97
+
98
+ this.asyncOpts = Object.assign({}, DEFAULT_ASYNC_OPTS, opts.async);
77
99
 
78
100
  // The test logs for each unit.
79
101
  this.log = [];
@@ -106,10 +128,12 @@ class Stats
106
128
  { // We found the Harness instance.
107
129
  this.harness = require.main.exports;
108
130
  }
131
+ else if (isObj(globalThis[HARNESS_GLOBAL]))
132
+ {
133
+ this.harness = globalThis[HARNESS_GLOBAL];
134
+ }
109
135
  }
110
-
111
- // Finally, mark the test stats as not generated yet.
112
- def(this, '$done', false);
136
+
113
137
  }
114
138
 
115
139
  // Internal method.
@@ -118,10 +142,9 @@ class Stats
118
142
  return new Log(this);
119
143
  }
120
144
 
121
- // A wrapper around types.stringify()
122
145
  stringify(what)
123
146
  {
124
- return types.stringify(what, this.stringifyDepth);
147
+ return types.stringify(what, this.stringifyOpts);
125
148
  }
126
149
 
127
150
  /**
@@ -277,6 +300,83 @@ class Stats
277
300
  this.log.push(msg);
278
301
  }
279
302
 
303
+ /**
304
+ * Run async test code
305
+ *
306
+ * If a test needs to wait for the results of an async operation,
307
+ * then this is the way to do it. This method does not actually
308
+ * call any testing methods, that needs to be done inside the `call`.
309
+ *
310
+ * This increments `this.waiting`, which will be decremented when
311
+ * the `Promise` returned by the `call` is resolved or rejected.
312
+ *
313
+ * @param {(function|Promise)} call - Async code to wait for
314
+ *
315
+ * The `function` must be `async` or return a `Promise`.
316
+ * The function will have this Stats instance applied as `this`.
317
+ *
318
+ * @param {...any} args - Arguments for `call` function.
319
+ *
320
+ * @returns {Promise}
321
+ */
322
+ async async(call, ...args)
323
+ {
324
+ let testPromise;
325
+ this.waiting++;
326
+
327
+ if (typeof call === F)
328
+ { // It MUST be an async function, OR return a Promise.
329
+ testPromise = call.apply(this, args);
330
+ }
331
+ else
332
+ { // Assume the promise was passed directly.
333
+ testPromise = call;
334
+ }
335
+
336
+ if (testPromise instanceof Promise)
337
+ {
338
+ testPromise.finally(() => this.waiting--);
339
+ }
340
+ else
341
+ {
342
+ throw new TypeError("async() call must return a Promise");
343
+ }
344
+
345
+ return testPromise;
346
+ }
347
+
348
+ /**
349
+ * Called if `this.waiting` is greater than `0`
350
+ *
351
+ * @returns {Promise<object>}
352
+ *
353
+ * Will resolve to `this` when `this.waiting` becomes `0`.
354
+ * Will be rejected if the maximum wait timeout is reached.
355
+ */
356
+ wait()
357
+ {
358
+ const ao = this.asyncOpts;
359
+ return new Promise((resolve, reject) =>
360
+ {
361
+ const timeout = setTimeout(() =>
362
+ {
363
+ clearInterval(test);
364
+ clearTimeout(timeout);
365
+ reject(new Error("Timed out waiting for async encoding"));
366
+ }, ao.timeout);
367
+
368
+ const test = setInterval(() =>
369
+ {
370
+ if (this.waiting === 0)
371
+ {
372
+ clearInterval(test);
373
+ clearTimeout(timeout);
374
+ resolve(this);
375
+ }
376
+ }, ao.interval);
377
+ });
378
+ }
379
+
280
380
  /**
281
381
  * Return TAP formatted output for all the tests.
282
382
  *
@@ -360,24 +460,33 @@ class Stats
360
460
  }
361
461
 
362
462
  /**
363
- * We're done testing.
364
- *
365
- * This will mark the test-set as finished, so attempting to run further
366
- * tests after will result in a `RangeError` being thrown.
463
+ * Run this when you're done testing.
367
464
  *
368
- * If no `Harness` is in use, this will also run `this.output()`.
465
+ * It doesn't do much at all if a `Harness` is in use.
466
+ *
467
+ * If no `Harness` is in use, this will call `this.output()`.
468
+ * If there are any async calls we're waiting on this will wait
469
+ * until they are complete before calling `this.output()`.
369
470
  */
370
471
  done ()
371
472
  {
372
- if (this.$done)
473
+ if (this.harness)
474
+ { // The harness will handle the rest.
475
+ return this;
476
+ }
477
+ else if (this.waiting)
478
+ {
479
+ this.wait().finally(() => this.done());
480
+ }
481
+ else
373
482
  {
374
- throw new RangeError('Test set is already done');
483
+ this.output();
375
484
  }
376
- def(this, '$done', true);
377
- return (this.harness ? this : this.output());
378
485
  }
379
486
 
380
- } // class Test
487
+ } // class Stats
488
+
489
+ def(LumTestStats, 'HARNESS_GLOBAL', HARNESS_GLOBAL);
381
490
 
382
491
  // Export the class
383
- module.exports = Stats;
492
+ module.exports = LumTestStats;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumjs/tests",
3
- "version": "1.9.0",
3
+ "version": "2.1.0",
4
4
  "main": "lib/index.js",
5
5
  "bin":
6
6
  {
@@ -11,8 +11,11 @@
11
11
  ".": "./lib/index.js",
12
12
  "./test": "./lib/test/index.js",
13
13
  "./test/functional": "./lib/test/functional.js",
14
+ "./test/stats": "./lib/test/stats.js",
14
15
  "./harness": "./lib/harness/index.js",
15
16
  "./harness/parser": "./lib/harness/parser.js",
17
+ "./harness/plugin": "./lib/harness/plugin/index.js",
18
+ "./harness/plugin/node": "./lib/harness/plugin/node.js",
16
19
  "./package.json": "./package.json",
17
20
  "./data/*": "./test/data/*.js"
18
21
  },
@@ -24,7 +27,7 @@
24
27
  },
25
28
  "dependencies":
26
29
  {
27
- "@lumjs/core": "^1.8.0"
30
+ "@lumjs/core": "^1.38.4"
28
31
  },
29
32
  "scripts": {
30
33
  "test": "./bin/lumtest.js",
@@ -1,18 +0,0 @@
1
- // Browser harness plugin
2
- const Plugin = require('./plugin');
3
-
4
- /**
5
- * Browser Harness plugin
6
- *
7
- * @exports module:@lumjs/tests/harness~BrowserPlugin
8
- * @extends module:@lumjs/tetss/harness~Plugin
9
- */
10
- class BrowserPlugin extends Plugin
11
- {
12
- run(queued)
13
- {
14
- throw new Error("Browser tests not supported yet");
15
- }
16
- } // Node plugin class
17
-
18
- module.exports = BrowserPlugin;