@lumjs/tests 1.6.0 → 1.7.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.
@@ -0,0 +1,18 @@
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;
@@ -0,0 +1,44 @@
1
+
2
+ /**
3
+ * An error for when a test set had failures
4
+ *
5
+ * @prop {number} failed - The number of tests that failed
6
+ *
7
+ * @exports module:@lumjs/tests/harness~TestFailure
8
+ * @extends Error
9
+ */
10
+ class TestFailure extends Error
11
+ {
12
+ constructor(failed)
13
+ {
14
+ super(`${failed} tests failed`);
15
+ this.name = 'TestFailure';
16
+ this.failed = failed;
17
+ }
18
+ }
19
+
20
+ /**
21
+ * An error for when the test plan was broken
22
+ *
23
+ * @prop {number} planned - The number of tests planned
24
+ * @prop {number} ran - The number of tests actually ran
25
+ *
26
+ * @exports module:@lumjs/tests/harness~PlanFailure
27
+ * @extends Error
28
+ */
29
+ class PlanFailure extends Error
30
+ {
31
+ constructor(planned, ran)
32
+ {
33
+ super(`${planned} tests planned; ${ran} tests ran`);
34
+ this.name = 'PlanFailure';
35
+ this.planned = planned;
36
+ this.ran = ran;
37
+ }
38
+ }
39
+
40
+ // Export all of our custom errors.
41
+ module.exports =
42
+ {
43
+ TestFailure, PlanFailure,
44
+ }
@@ -0,0 +1,260 @@
1
+ const core = require('@lumjs/core');
2
+ const {def,lazy,context:ctx} = core;
3
+ const {B,F} = core.types;
4
+ const QueuedTest = require('./queuedtest');
5
+ const Plugin = require('./plugin');
6
+
7
+ /**
8
+ * A class that acts as a *test harness* for running other tests.
9
+ *
10
+ * Loosely based off of my PHP `Lum\Test\Harness` class,
11
+ * but with a much more modular design.
12
+ *
13
+ * @prop {object} opts - Options passed to constructor
14
+ *
15
+ * @prop {object} testSuite - A `Stats` (or `Test`) instance
16
+ *
17
+ * This meta-test will keep track of the success or failure of
18
+ * all the other tests. So its `plan` will be equal to the number of
19
+ * tests we're running, and so forth.
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.
25
+ *
26
+ * @prop {Array} queue - An array of `QueuedTest` instances
27
+ * representing tests that we want to run.
28
+ *
29
+ * @prop {Array} stats - An array of `Stats` or `Error` instances
30
+ * representing tests that have been ran.
31
+ *
32
+ * @prop {object} parser - A lazy-loaded `Parser` instance
33
+ *
34
+ * @exports module:@lumjs/tests/harness
35
+ */
36
+ class Harness
37
+ {
38
+ /**
39
+ * Build a new Harness
40
+ *
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.
45
+ *
46
+ * @param {object} [opts.plugin] A `Plugin` instance.
47
+ *
48
+ * This is probably not needed. We'll automatically determine a default
49
+ * plugin to use based on the JS environment.
50
+ *
51
+ * @param {boolean} [opts.plannedFailure=true] Is a broken plan a failure?
52
+ *
53
+ * If this is `true` and a test set has a non-zero plan, then the
54
+ * number of tests ran must match the plan or it will be a failure.
55
+ *
56
+ * If this is `false`, then see `opts.plannedWarning` for how we'll
57
+ * report broken plans.
58
+ *
59
+ * @param {boolean} [opts.plannedWarning=true] Show a broken plan warning?
60
+ *
61
+ * If this is `true` and `opts.plannedFailure` is `false` then when
62
+ * a test set has a broken plan we'll send a warning to the JS console.
63
+ *
64
+ * If this is `false` then no warning will be shown.
65
+ *
66
+ */
67
+ constructor(opts={})
68
+ {
69
+ // Save a reference to the options.
70
+ this.opts = opts;
71
+
72
+ if (opts.testSuite instanceof Stats)
73
+ { // Set the test suite instance.
74
+ this.testSuite = opts.testSuite;
75
+ }
76
+ else
77
+ { // Build a new test suite instance.
78
+ this.testSuite = new Stats(opts.testSuite);
79
+ }
80
+
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
+ //console.debug("# ~ plugin", this.plugin, this);
99
+
100
+ this.queue = []; // Tests to be ran.
101
+ this.stats = []; // Results from tests that have been ran
102
+
103
+ lazy(this, 'parser', () => require('./parser').new(this));
104
+
105
+ this.plannedFailure = opts.plannedFailure ?? true;
106
+ this.plannedWarning = opts.plannedWarning ?? true;
107
+
108
+ } // constructor()
109
+
110
+ /**
111
+ * Add `Test` or `Stats` instances that have been run
112
+ * to our list of test stats.
113
+ *
114
+ * @protected
115
+ *
116
+ * @param {object} info - The `QueuedTest` instance
117
+ * @param {?object} result - The returned result
118
+ *
119
+ * Will usually be a `Test` or `Stats` instance, but may
120
+ * be an `Error` if an exception was thrown trying to run the test.
121
+ *
122
+ * @returns {object} `this`
123
+ */
124
+ addStats(info, stats)
125
+ {
126
+ if (info instanceof QueuedTest
127
+ && (stats instanceof Stats
128
+ || stats instanceof Error))
129
+ {
130
+ this.stats[info.name] = {info, stats};
131
+ }
132
+ else
133
+ {
134
+ throw new TypeError("Invalid object type");
135
+ }
136
+ return this;
137
+ }
138
+
139
+ /**
140
+ * Add a test to our testing queue.
141
+ *
142
+ * @param {string} filename - A filename for the test.
143
+ * @param {object} [opts] Options for the `QueuedTest` instance.
144
+ * @returns {object} `this`
145
+ */
146
+ addTest(filename, opts={})
147
+ {
148
+ const queued = new QueuedTest(this, filename, opts);
149
+ this.queue.push(queued);
150
+ return this;
151
+ }
152
+
153
+ /**
154
+ * Add an entire folder of tests.
155
+ *
156
+ * This method may not be implemented by all
157
+ * Harness Plugins. Your mileage may vary.
158
+ *
159
+ * @param {string} dir - The directory name/path
160
+ * @param {object} [opts] Options
161
+ * @returns {object} `this`
162
+ */
163
+ addDir(dir, opts={})
164
+ {
165
+ if (typeof this.plugin.addDir === F)
166
+ {
167
+ this.plugin.addDir(dir, opts);
168
+ }
169
+ else
170
+ {
171
+ console.warn("The plugin does not support addDir()");
172
+ }
173
+ return this;
174
+ }
175
+
176
+ /**
177
+ * Run all the queued tests
178
+ *
179
+ * @param {boolean} [plan=true] Set a test plan for the entire suite
180
+ * @returns {object} `this`
181
+ */
182
+ run(plan=true)
183
+ {
184
+ if (plan)
185
+ {
186
+ this.testSuite.plan(this.queue.length);
187
+ }
188
+
189
+ for (const queued of this.queue)
190
+ {
191
+ this.runTest(queued);
192
+ }
193
+
194
+ this.testSuite.done();
195
+
196
+ return this;
197
+ }
198
+
199
+ runTest(queued)
200
+ {
201
+ const name = queued.filename;
202
+ let test;
203
+
204
+ try
205
+ {
206
+ test = this.plugin.run(queued);
207
+ }
208
+ catch (err)
209
+ {
210
+ test = err;
211
+ }
212
+
213
+ this.addStats(queued, test);
214
+
215
+ if (typeof test === Error)
216
+ { // Something went wrong.
217
+ this.testSuite.fail(name, test);
218
+ }
219
+ else
220
+ { // 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
+ }
230
+ else
231
+ { // This should never happen...
232
+ throw new Error("Invalid result from plugin.ok() !!");
233
+ }
234
+ }
235
+
236
+ return this;
237
+ }
238
+
239
+ tap()
240
+ {
241
+ return this.testSuite.tap();
242
+ }
243
+
244
+ get TAP()
245
+ {
246
+ return this.tap();
247
+ }
248
+
249
+ } // Harness class
250
+
251
+ def(Harness, 'Plugin', Plugin);
252
+ def(Harness, 'QueuedTest', QueuedTest);
253
+ def(Harness, 'Errors', require('./errors'));
254
+
255
+ // Export it.
256
+ module.exports = Harness;
257
+
258
+ // Some classes required at end for recursive sanity reasons.
259
+
260
+ const Stats = require('../test/stats');
@@ -0,0 +1,71 @@
1
+ // Node.js harness plugin
2
+ const core = require('@lumjs/core');
3
+ const {N} = core.types;
4
+ const Plugin = require('./plugin');
5
+ const cp = require('node:child_process');
6
+ const getCwd = require('node:process').cwd;
7
+ const fs = require('node:fs');
8
+ const path = require('node:path');
9
+
10
+ /**
11
+ * Node.js Harness plugin
12
+ *
13
+ * @exports module:@lumjs/tests/harness~NodePlugin
14
+ * @extends module:@lumjs/tetss/harness~Plugin
15
+ */
16
+ class NodePlugin extends Plugin
17
+ {
18
+ runInternal(queued)
19
+ {
20
+ let name = path.join(getCwd(), queued.filename);
21
+ return require(name);
22
+ }
23
+
24
+ runExternal(queued)
25
+ {
26
+ const name = queued.filename;
27
+ const args = queued.options.args ?? [];
28
+ const proc = cp.spawnSync(name, args, {encoding: 'utf8'});
29
+ return this.harness.parser.parse(proc.stdout);
30
+ }
31
+
32
+ run(queued)
33
+ {
34
+ if (queued.options.external)
35
+ {
36
+ return this.runExternal(queued);
37
+ }
38
+ else
39
+ {
40
+ return this.runInternal(queued);
41
+ }
42
+ }
43
+
44
+ addDir(dir, opts={}, recurse)
45
+ {
46
+ if (typeof recurse !== N && typeof opts.recurse === N)
47
+ {
48
+ recurse = opts.recurse;
49
+ }
50
+
51
+ const ext = opts.ext ?? '.js';
52
+ const testOpts = opts.test ?? opts;
53
+ const files = fs.readdirSync(dir, {encoding: 'utf8', withFileTypes: true});
54
+
55
+ for (const file of files)
56
+ {
57
+ if (file.name === '.' || file.name === '..') continue;
58
+ if (typeof recurse === N && recurse > 0 && file.isDirectory())
59
+ { // Recurse to a nested directory.
60
+ this.addDir(path.join(dir, file.name), opts, recurse-1);
61
+ }
62
+ else if (file.isFile() && file.name.endsWith(ext))
63
+ { // It would seem to be a valid test.
64
+ this.harness.addTest(path.join(dir, file.name), testOpts);
65
+ }
66
+ }
67
+ }
68
+
69
+ } // Node plugin class
70
+
71
+ module.exports = NodePlugin;
@@ -0,0 +1,153 @@
1
+ const {S,N,needType,isArray,isObj} = require('@lumjs/core/types');
2
+ const Stats = require('../test/stats');
3
+ const Tap = require('../grammar/tap');
4
+
5
+ const T = 'test',
6
+ I = 'info',
7
+ M = 'meta',
8
+ A = 'item',
9
+ C = 'comment',
10
+ O = 'other';
11
+
12
+ /**
13
+ * A simplistic TAP parser.
14
+ *
15
+ * Currently only handles *very basic* TAP v12 output.
16
+ * This is loosely based on the TAP parser in my PHP
17
+ * `Lum\Test\Harness` library, but is split off into
18
+ * its own class, and how it handles the parsed data
19
+ * is quite different. I think I may eventually update
20
+ * the PHP version to be more like this one.
21
+ *
22
+ * @exports module:@lumjs/tests/harness/parser
23
+ */
24
+ class Parser
25
+ {
26
+ /**
27
+ * Build a TAP Parser.
28
+ * @param {object} harness - The parent Harness
29
+ */
30
+ constructor(harness)
31
+ {
32
+ this.harness = harness;
33
+ }
34
+
35
+ /**
36
+ * Parse a multiline TAP string
37
+ *
38
+ * @param {string} tapSource - The full TAP string to parse.
39
+ *
40
+ * @returns {object} A simple `Test`-like object with the parsed results.
41
+ */
42
+ parse(tapSource)
43
+ {
44
+ needType(S, tapSource);
45
+
46
+ // Our base Test for populating values into.
47
+ const test = new Stats();
48
+
49
+ // An AST that we'll use to populate the stats.
50
+ const tap = Tap.parse(tapSource);
51
+
52
+ // Test the first line for a plan.
53
+ if (typeof tap.plan === N)
54
+ {
55
+ test.plan(tap.plan);
56
+ }
57
+
58
+ if (tap.items.length === 0) return test; // No lines, cannot continue.
59
+
60
+ // A structure to store our parsing state.
61
+ const cur =
62
+ {
63
+ log: null,
64
+ pos: -1,
65
+ cmd: null,
66
+ }
67
+
68
+ const unhandled = line => console.log("unhandled log line", {line, cur});
69
+
70
+ for (const line of tap.lines)
71
+ {
72
+ if (line.type === T)
73
+ { // A regular test line; will increment the counter
74
+ if (line.skip)
75
+ {
76
+ cur.log = test.skip(line.comment, line.desc);
77
+ }
78
+ else if (line.todo)
79
+ {
80
+ cur.log = test.todo(line.comment, line.desc);
81
+ }
82
+ else
83
+ {
84
+ cur.log = test.ok(line.ok, line.desc, line.comment);
85
+ }
86
+ cur.pos++;
87
+ }
88
+ else if (line.type === I && cur.log)
89
+ { // Log detail fields
90
+ cur.cmd = line.info;
91
+ cur.log.details[line.info] = line.comment;
92
+ }
93
+ else if (line.type === M)
94
+ { // We skip meta info.
95
+ cur.cmd = null;
96
+ cur.log = null;
97
+ continue;
98
+ }
99
+ else if (line.type === A && cur.log)
100
+ { // An array item
101
+ cur.cmd = null;
102
+ if (cur.log.details.info === undefined)
103
+ {
104
+ cur.log.details.info = [];
105
+ }
106
+ cur.log.details.info.push(line.text);
107
+ }
108
+ else if (line.type === C)
109
+ { // A regular comment
110
+ test.diag(line.text);
111
+ cur.cmd = null;
112
+ cur.log = null;
113
+ cur.pos++;
114
+ }
115
+ else if (line.type === O)
116
+ { // A line that doesn't match anything else.
117
+ if (cur.log && cur.cmd)
118
+ { // There's a comment command statement in use.
119
+ const ld = cur.log.details;
120
+ ld[cur.cmd] += "\n" + line.text;
121
+ }
122
+ else if (cur.log && isArray(cur.log.details.info))
123
+ { // The current log has info, let's add to it.
124
+ const ldi = cur.log.details.info;
125
+ const last = ldi.length - 1;
126
+ ldi[last] += "\n" + line.text;
127
+ }
128
+ else if (cur.pos > -1 && typeof test.log[cur.pos] === S)
129
+ { // The current entry is a string, let's add to it.
130
+ test.log[cur.pos] += "\n" + line.text;
131
+ }
132
+ else
133
+ { // Don't know what to do with this.
134
+ unhandled(line);
135
+ }
136
+ }
137
+ else
138
+ { // Can't do anything, so warn about it and move on.
139
+ unhandled(line);
140
+ }
141
+ }
142
+
143
+ return test;
144
+ }
145
+
146
+ static new(opts)
147
+ {
148
+ return new Parser(opts);
149
+ }
150
+ }
151
+
152
+ // Export the class as the parser.
153
+ module.exports = Parser;
@@ -0,0 +1,77 @@
1
+ const core = require('@lumjs/core');
2
+ const {AbstractClass} = core;
3
+ const {TestFailure,PlanFailure} = require('./errors');
4
+
5
+ /**
6
+ * An abstract base class for Harness plugins
7
+ *
8
+ * @exports module:@lumjs/tests/harness~Plugin
9
+ */
10
+ class Plugin extends AbstractClass
11
+ {
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
+ /**
24
+ * (ABSTRACT METHOD) Run a test
25
+ *
26
+ * This method must be implemented by every plugin.
27
+ *
28
+ * @name module:@lumjs/tests/harness~Plugin#run
29
+ * @function
30
+ * @param {object} queued - A queued test object
31
+ */
32
+
33
+ /**
34
+ * See if a test had failures
35
+ *
36
+ * @param {object} test - The `Test` or `Stats` object to check
37
+ * @returns {true|Error}
38
+ *
39
+ * - Will return `true` if no failures reported.
40
+ * - Will return a `TestFailure` if the test had failures reported.
41
+ * - May return a `PlanFailure` if the test plan was broken.
42
+ * This only applies if `this.harness.plannedFailure` is `true`.
43
+ *
44
+ */
45
+ ok(test)
46
+ {
47
+ if (test.failed !== 0)
48
+ { // The test had failed units.
49
+ return new TestFailure(test.failed);
50
+ }
51
+
52
+ const planned = test.planned,
53
+ ran = test.ran;
54
+
55
+ const planOk = (planned === 0 || planned === ran);
56
+
57
+ if (this.harness.plannedFailure && !planOk)
58
+ { // The number of tests ran was not the number of tests planned.
59
+ return new PlanFailure(planned, ran);
60
+ }
61
+ else if (this.harness.plannedWarning && !planOk)
62
+ { // Report the plan failure to the console log.
63
+ console.warn(new PlanFailure(planned, ran));
64
+ }
65
+
66
+ // If we reached here, everything looks good.
67
+ return true;
68
+ }
69
+
70
+ static new(harness)
71
+ {
72
+ return new this(harness);
73
+ }
74
+
75
+ }
76
+
77
+ module.exports = Plugin;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A class representing a queued test.
3
+ * @exports module:@lumjs/tests/harness~QueuedTest
4
+ */
5
+ class QueuedTest
6
+ {
7
+ constructor(harness, filename, opts)
8
+ {
9
+ this.harness = harness;
10
+ this.filename = filename;
11
+ this.options = opts;
12
+ }
13
+ }
14
+ module.exports = QueuedTest;
package/lib/index.js CHANGED
@@ -2,28 +2,34 @@
2
2
  * Several test related classes.
3
3
  * @module @lumjs/tests
4
4
  */
5
+ const {lazy} = require('@lumjs/core/types');
5
6
 
6
7
  /**
7
- * Test class.
8
- * @alias module:@lumjs/tests.Test
8
+ * The main Test class
9
+ *
9
10
  * @see module:@lumjs/tests/test
10
11
  */
11
- const Test = require('./test');
12
- module.exports.Test = Test;
12
+ exports.Test = require('./test');
13
+
14
+ const lz = {enumerable: true};
13
15
 
14
16
  /**
15
- * Test Harness class.
16
- * @alias module:@lumjs/tests.Harness
17
+ * Test Harness class (lazy loaded)
18
+ *
19
+ * @name module:@lumjs/tests.Harness
20
+ * @class
17
21
  * @see module:@lumjs/tests/harness
18
22
  */
19
- module.exports.Harness = require('./harness');
23
+ lazy(exports, 'Harness', () => require('./harness'), lz);
20
24
 
21
25
  /**
22
- * Functional API registration.
23
- * @alias module:@lumjs/tests.functional
24
- * @see module:@lumjs/tests/functional
26
+ * Functional API registration (lazy loaded)
27
+ *
28
+ * @name module:@lumjs/tests.functional
29
+ * @function
30
+ * @see module:@lumjs/tests/test/functional
25
31
  */
26
- module.exports.functional = require('./functional');
32
+ lazy(exports, 'functional', () => require('./test/functional'), lz);
27
33
 
28
34
  /**
29
35
  * Create a new Test instance.
@@ -31,8 +37,7 @@ module.exports.functional = require('./functional');
31
37
  * @param {object} [opts] Options to pass to the `Test` constructor.
32
38
  * @returns {Test} A new test instance.
33
39
  */
34
- module.exports.new = function(opts={})
40
+ exports.new = function(opts={})
35
41
  {
36
- return new Test(opts);
42
+ return new exports.Test(opts);
37
43
  }
38
-