tape-six 1.7.0 → 1.7.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/README.md CHANGED
@@ -99,11 +99,29 @@ And:
99
99
 
100
100
  <img width="240" height="329" alt="image" src="https://github.com/user-attachments/assets/f3d8ac65-9e6a-499d-837f-0271146da1de" />
101
101
 
102
+ ## Project structure
103
+
104
+ ```
105
+ tape-six/
106
+ ├── index.js # Main entry point, exports test, hooks, aliases
107
+ ├── index.d.ts # TypeScript definitions for the public API
108
+ ├── package.json # Package config; "tape6" section configures test discovery
109
+ ├── bin/ # CLI utilities: tape6, tape6-server, tape6-node, tape6-bun, tape6-deno, tape6-seq
110
+ ├── src/ # Source code (test engine, reporters, runners, utilities)
111
+ ├── web-app/ # Browser test UI application
112
+ ├── tests/ # Test files
113
+ ├── ts-tests/ # TypeScript test files
114
+ ├── wiki/ # GitHub wiki documentation (submodule)
115
+ └── vendors/ # Git submodules (deep6)
116
+ ```
117
+
102
118
  ## Docs
103
119
 
104
120
  The documentation can be found in the [wiki](https://github.com/uhop/tape-six/wiki).
105
121
  See how it can be used in [tests/](https://github.com/uhop/tape-six/tree/master/tests).
106
122
 
123
+ For AI assistants: see [llms.txt](https://github.com/uhop/tape-six/blob/master/llms.txt) and [llms-full.txt](https://github.com/uhop/tape-six/blob/master/llms-full.txt) for LLM-optimized documentation.
124
+
107
125
  The whole API is based on two objects: `test` and `Tester`.
108
126
 
109
127
  ### `test`
@@ -247,6 +265,8 @@ The following methods are available (all `msg` arguments are optional):
247
265
  - `notStrictEquals()` &mdash; an alias of `notStrictEqual()`.
248
266
  - `doesNotEqual()` &mdash; an alias of `notStrictEqual()`.
249
267
  - `isUnequal()` &mdash; an alias of `notStrictEqual()`.
268
+ - `isNotEqual()` &mdash; an alias of `notStrictEqual()`.
269
+ - `isNot()` &mdash; an alias of `notStrictEqual()`.
250
270
  - `looseEqual(a, b, msg)` &mdash; asserts that `a` and `b` are loosely equal.
251
271
  - Loose equality is defined as `a == b`.
252
272
  - `looseEquals()` &mdash; an alias of `looseEqual()`.
@@ -290,6 +310,7 @@ The following methods are available (all `msg` arguments are optional):
290
310
  - Miscellaneous:
291
311
  - `any` &mdash; returns the `any` object. It can be used in deep equivalency asserts to match any value.
292
312
  See [deep6's any](https://github.com/uhop/deep6/wiki/any) for details.
313
+ - `_` &mdash; an alias of `any`.
293
314
  - `plan(n)` &mdash; sets the number of tests in the test suite. Rarely used.
294
315
  - `comment(msg)` &mdash; sends a comment to the test harness. Rarely used.
295
316
  - `skipTest(...args, msg)` &mdash; skips the current test yet sends a message to the test harness.
@@ -390,6 +411,7 @@ Test output can be controlled by flags. See [Supported flags](https://github.com
390
411
 
391
412
  The most recent releases:
392
413
 
414
+ - 1.7.1 _Added AI support, added timeout to start test runners, fixed some bugs in the sequential test runner._
393
415
  - 1.7.0 _New features: after/before hooks for tests, aliases for `suite()`, `describe()`, `it()`, `tape6-seq` &mdash; an in-process sequential test runner. Improvements: stricter monochrome detection, refactoring, bugfixes, updated dev dependencies and the documentation._
394
416
  - 1.6.0 _New features: support for `AssertionError` and 3rd-party assertion libraries based on it like `node:assert` and `chai`, support for `console.assert()`, support for `signal` to cancel asynchronous operations, tests wait for embedded tests, improved reporting of errors, updated dev dependencies._
395
417
  - 1.5.1 _Better support for stopping parallel tests, better support for "failed to load" errors._
package/bin/tape6-bun.js CHANGED
@@ -2,7 +2,12 @@
2
2
 
3
3
  import {fileURLToPath} from 'node:url';
4
4
 
5
- import {resolveTests, resolvePatterns, getReporterType} from '../src/utils/config.js';
5
+ import {
6
+ resolveTests,
7
+ resolvePatterns,
8
+ getReporterFileName,
9
+ getReporterType
10
+ } from '../src/utils/config.js';
6
11
 
7
12
  import {getReporter, setReporter} from '../src/test.js';
8
13
  import {selectTimer} from '../src/utils/timer.js';
@@ -85,17 +90,11 @@ const config = () => {
85
90
  if (!parallel) parallel = globalThis.navigator?.hardwareConcurrency || 1;
86
91
  };
87
92
 
88
- const reporters = {
89
- jsonl: 'JSONLReporter.js',
90
- tap: 'TapReporter.js',
91
- tty: 'TTYReporter.js'
92
- };
93
-
94
93
  const init = async () => {
95
94
  const currentReporter = getReporter();
96
95
  if (!currentReporter) {
97
96
  const reporterType = getReporterType(),
98
- reporterFile = reporters[reporterType] || reporters.tty,
97
+ reporterFile = getReporterFileName(reporterType),
99
98
  CustomReporter = (await import('../src/reporters/' + reporterFile)).default,
100
99
  hasColors = !(
101
100
  options.monochrome ||
package/bin/tape6-deno.js CHANGED
@@ -2,7 +2,12 @@
2
2
 
3
3
  import {fileURLToPath} from 'node:url';
4
4
 
5
- import {resolveTests, resolvePatterns, getReporterType} from '../src/utils/config.js';
5
+ import {
6
+ resolveTests,
7
+ resolvePatterns,
8
+ getReporterFileName,
9
+ getReporterType
10
+ } from '../src/utils/config.js';
6
11
 
7
12
  import {getReporter, setReporter} from '../src/test.js';
8
13
  import {selectTimer} from '../src/utils/timer.js';
@@ -85,17 +90,11 @@ const config = () => {
85
90
  if (!parallel) parallel = globalThis.navigator?.hardwareConcurrency || 1;
86
91
  };
87
92
 
88
- const reporters = {
89
- jsonl: 'JSONLReporter.js',
90
- tap: 'TapReporter.js',
91
- tty: 'TTYReporter.js'
92
- };
93
-
94
93
  const init = async () => {
95
94
  const currentReporter = getReporter();
96
95
  if (!currentReporter) {
97
96
  const reporterType = getReporterType(),
98
- reporterFile = reporters[reporterType] || reporters.tty,
97
+ reporterFile = getReporterFileName(reporterType),
99
98
  CustomReporter = (await import('../src/reporters/' + reporterFile)).default,
100
99
  hasColors = !(
101
100
  options.monochrome ||
package/bin/tape6-node.js CHANGED
@@ -3,7 +3,12 @@
3
3
  import process from 'node:process';
4
4
  import {fileURLToPath} from 'node:url';
5
5
 
6
- import {resolveTests, resolvePatterns, getReporterType} from '../src/utils/config.js';
6
+ import {
7
+ resolveTests,
8
+ resolvePatterns,
9
+ getReporterFileName,
10
+ getReporterType
11
+ } from '../src/utils/config.js';
7
12
 
8
13
  import {getReporter, setReporter} from '../src/test.js';
9
14
  import {selectTimer} from '../src/utils/timer.js';
@@ -86,17 +91,11 @@ const config = () => {
86
91
  if (!parallel) parallel = globalThis.navigator?.hardwareConcurrency || 1;
87
92
  };
88
93
 
89
- const reporters = {
90
- jsonl: 'JSONLReporter.js',
91
- tap: 'TapReporter.js',
92
- tty: 'TTYReporter.js'
93
- };
94
-
95
94
  const init = async () => {
96
95
  const currentReporter = getReporter();
97
96
  if (!currentReporter) {
98
97
  const reporterType = getReporterType(),
99
- reporterFile = reporters[reporterType] || reporters.tty,
98
+ reporterFile = getReporterFileName(reporterType),
100
99
  CustomReporter = (await import('../src/reporters/' + reporterFile)).default,
101
100
  hasColors = !(
102
101
  options.monochrome ||
@@ -125,6 +124,11 @@ const main = async () => {
125
124
  console.error('UNHANDLED ERROR:', origin, error)
126
125
  );
127
126
 
127
+ if (!files.length) {
128
+ console.log('No files found.');
129
+ process.exit(1);
130
+ }
131
+
128
132
  const reporter = getReporter(),
129
133
  worker = new TestWorker(reporter, parallel, options);
130
134
 
package/bin/tape6-seq.js CHANGED
@@ -3,7 +3,12 @@
3
3
  import process from 'node:process';
4
4
  import {fileURLToPath} from 'node:url';
5
5
 
6
- import {resolveTests, resolvePatterns, getReporterType} from '../src/utils/config.js';
6
+ import {
7
+ resolveTests,
8
+ resolvePatterns,
9
+ getReporterFileName,
10
+ getReporterType
11
+ } from '../src/utils/config.js';
7
12
 
8
13
  import {getReporter, setReporter, setConfiguredFlag, testRunner} from '../src/test.js';
9
14
  import {selectTimer} from '../src/utils/timer.js';
@@ -68,17 +73,11 @@ const config = () => {
68
73
  options.flags = flags;
69
74
  };
70
75
 
71
- const reporters = {
72
- jsonl: 'JSONLReporter.js',
73
- tap: 'TapReporter.js',
74
- tty: 'TTYReporter.js'
75
- };
76
-
77
76
  const init = async () => {
78
77
  const currentReporter = getReporter();
79
78
  if (!currentReporter) {
80
79
  const reporterType = getReporterType(),
81
- reporterFile = reporters[reporterType] || reporters.tty,
80
+ reporterFile = getReporterFileName(reporterType),
82
81
  CustomReporter = (await import('../src/reporters/' + reporterFile)).default,
83
82
  hasColors = !(
84
83
  options.monochrome ||
package/index.d.ts CHANGED
@@ -629,7 +629,7 @@ export declare interface Tester {
629
629
  is(a: unknown, b: unknown, message?: string): void;
630
630
 
631
631
  /**
632
- * Asserts that `actual` is deeply equal to `expected`. Alias of `strictEqual`.
632
+ * Asserts that `actual` is strictly equal to `expected`. Alias of `strictEqual`.
633
633
  * @param actual - The actual value
634
634
  * @param expected - The expected value
635
635
  * @param message - Optional message to display if the assertion fails
package/index.js CHANGED
@@ -110,6 +110,9 @@ const init = async () => {
110
110
  if (Deno.env.get('TAPE6_JSONL')) {
111
111
  const {JSONLReporter} = await import('./src/reporters/JSONLReporter.js');
112
112
  reporter = new JSONLReporter({...options, originalConsole, hasColors});
113
+ } else if (Deno.env.get('TAPE6_MIN')) {
114
+ const {MinReporter} = await import('./src/reporters/MinReporter.js');
115
+ reporter = new MinReporter({...options, originalConsole});
113
116
  } else if (!Deno.env.get('TAPE6_TAP')) {
114
117
  const {TTYReporter} = await import('./src/reporters/TTYReporter.js');
115
118
  reporter = new TTYReporter({...options, originalConsole, hasColors});
@@ -124,6 +127,9 @@ const init = async () => {
124
127
  if (Bun.env.TAPE6_JSONL) {
125
128
  const {JSONLReporter} = await import('./src/reporters/JSONLReporter.js');
126
129
  reporter = new JSONLReporter({...options, originalConsole, hasColors});
130
+ } else if (Bun.env.TAPE6_MIN) {
131
+ const {MinReporter} = await import('./src/reporters/MinReporter.js');
132
+ reporter = new MinReporter({...options, originalConsole});
127
133
  } else if (!Bun.env.TAPE6_TAP) {
128
134
  const {TTYReporter} = await import('./src/reporters/TTYReporter.js');
129
135
  reporter = new TTYReporter({...options, originalConsole, hasColors});
@@ -138,6 +144,9 @@ const init = async () => {
138
144
  if (process.env.TAPE6_JSONL) {
139
145
  const {JSONLReporter} = await import('./src/reporters/JSONLReporter.js');
140
146
  reporter = new JSONLReporter({...options, originalConsole, hasColors});
147
+ } else if (process.env.TAPE6_MIN) {
148
+ const {MinReporter} = await import('./src/reporters/MinReporter.js');
149
+ reporter = new MinReporter({...options, originalConsole});
141
150
  } else if (!process.env.TAPE6_TAP) {
142
151
  const {TTYReporter} = await import('./src/reporters/TTYReporter.js');
143
152
  reporter = new TTYReporter({...options, originalConsole, hasColors});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tape-six",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "The test harness for the modern JavaScript and TypeScript.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "bin": {
15
15
  "tape6": "bin/tape6.js",
16
+ "tape6-node": "bin/tape6-node.js",
16
17
  "tape6-bun": "bin/tape6-bun.js",
17
18
  "tape6-deno": "bin/tape6-deno.js",
18
19
  "tape6-seq": "bin/tape6-seq.js",
@@ -30,8 +31,8 @@
30
31
  "test:bun": "bun run ./bin/tape6-bun.js --flags FO",
31
32
  "test:deno": "deno run -A ./bin/tape6-deno.js --flags FO",
32
33
  "test:seq": "node ./bin/tape6-seq.js --flags FO",
33
- "test:bun:seq": "bun run ./bin/tape6-seq.js --flags FO",
34
- "test:deno:seq": "deno run -A ./bin/tape6-seq.js --flags FO",
34
+ "test:seq:bun": "bun run ./bin/tape6-seq.js --flags FO",
35
+ "test:seq:deno": "deno run -A ./bin/tape6-seq.js --flags FO",
35
36
  "test:puppeteer": "node tests/puppeteer-chrome.js",
36
37
  "test:playwright": "node tests/playwright-chrome.js",
37
38
  "ts-check": "tsc --noEmit",
@@ -49,7 +50,18 @@
49
50
  "test",
50
51
  "harness",
51
52
  "assert",
52
- "browser"
53
+ "browser",
54
+ "unit-test",
55
+ "testing",
56
+ "test-runner",
57
+ "esm",
58
+ "es-modules",
59
+ "typescript",
60
+ "deno",
61
+ "bun",
62
+ "nodejs",
63
+ "tap-protocol",
64
+ "cross-runtime"
53
65
  ],
54
66
  "author": "Eugene Lazutkin <eugene.lazutkin@gmail.com> (https://www.lazutkin.com/)",
55
67
  "funding": "https://github.com/sponsors/uhop",
@@ -58,6 +70,8 @@
58
70
  "url": "https://github.com/uhop/tape-six/issues"
59
71
  },
60
72
  "homepage": "https://github.com/uhop/tape-six#readme",
73
+ "llms": "https://raw.githubusercontent.com/uhop/tape-six/master/llms.txt",
74
+ "llmsFull": "https://raw.githubusercontent.com/uhop/tape-six/master/llms-full.txt",
61
75
  "files": [
62
76
  "index.*",
63
77
  "bin",
@@ -85,10 +99,10 @@
85
99
  },
86
100
  "devDependencies": {
87
101
  "@types/chai": "^5.2.3",
88
- "@types/node": "^25.2.1",
102
+ "@types/node": "^25.3.0",
89
103
  "chai": "^6.2.2",
90
104
  "playwright": "^1.58.2",
91
- "puppeteer": "^24.37.2",
105
+ "puppeteer": "^24.37.5",
92
106
  "typescript": "^5.9.3"
93
107
  }
94
108
  }
@@ -0,0 +1,37 @@
1
+ import Reporter from './Reporter.js';
2
+
3
+ const getEnvVar = name => {
4
+ if (typeof Deno == 'object' && Deno?.version) {
5
+ return Deno.env.get(name);
6
+ } else if (typeof Bun == 'object' && Bun?.version) {
7
+ return Bun.env[name];
8
+ } else if (typeof process == 'object' && process?.versions?.node) {
9
+ return process.env[name];
10
+ }
11
+ return undefined;
12
+ };
13
+
14
+ export class MinReporter extends Reporter {
15
+ constructor({failOnce = false, originalConsole} = {}) {
16
+ super({failOnce});
17
+ this.console = originalConsole || console;
18
+ }
19
+ report(event) {
20
+ event = this.state?.preprocess(event) || event;
21
+ const handler = Reporter.EVENT_MAP[event.type];
22
+ typeof handler == 'string' && this[handler]?.(event);
23
+ this.console.log(
24
+ 'Test:',
25
+ event.test,
26
+ 'Type:',
27
+ event.type,
28
+ 'Name:',
29
+ event.name,
30
+ 'Fail:',
31
+ event.fail
32
+ );
33
+ this.state?.postprocess(event);
34
+ }
35
+ }
36
+
37
+ export default MinReporter;
@@ -58,7 +58,7 @@ export class TTYReporter extends Reporter {
58
58
  this.hasColors =
59
59
  hasColors &&
60
60
  this.output.isTTY &&
61
- (typeof this.output.hasColors == 'function' ? this.output.hasColors(256) : true);
61
+ (typeof this.output.hasColors == 'function' ? this.output.hasColors(16) : true);
62
62
  this.renumberAsserts = renumberAsserts;
63
63
  this.failureOnly = failureOnly;
64
64
  this.showBanner = showBanner;
@@ -221,7 +221,7 @@ export class TTYReporter extends Reporter {
221
221
  paint = method === 'error' || method === 'assert' ? 'stderrPaint' : 'stdoutPaint',
222
222
  prefix = this[paint](consoleDict[method] + ':') + ' ';
223
223
  for (const line of lines) {
224
- this.out(prefix + line);
224
+ this.out(prefix + line, this.failureOnly);
225
225
  }
226
226
  }
227
227
  }
@@ -231,7 +231,7 @@ export class TTYReporter extends Reporter {
231
231
  const lines = event.name.split(/\r?\n/),
232
232
  prefix = this.stdoutPaint('stdout:') + ' ';
233
233
  for (const line of lines) {
234
- this.out(prefix + line);
234
+ this.out(prefix + line, this.failureOnly);
235
235
  }
236
236
  }
237
237
  break;
@@ -240,7 +240,7 @@ export class TTYReporter extends Reporter {
240
240
  const lines = event.name.split(/\r?\n/),
241
241
  prefix = this.stderrPaint('stderr:') + ' ';
242
242
  for (const line of lines) {
243
- this.out(prefix + line);
243
+ this.out(prefix + line, this.failureOnly);
244
244
  }
245
245
  }
246
246
  break;
@@ -1,8 +1,36 @@
1
- const reportToParent = fileName => msg => {
2
- if ((msg.type === 'test' || msg.type === 'end') && !msg.test && !msg.name) {
3
- msg.name = 'FILE: /' + fileName;
4
- }
5
- postMessage(msg);
1
+ const DEFAULT_START_TIMEOUT = 5_000;
2
+
3
+ const getTimeout = () => {
4
+ const timeoutValue = Bun.env.TAPE6_WORKER_START_TIMEOUT;
5
+ if (!timeoutValue) return DEFAULT_START_TIMEOUT;
6
+ let timeout = Number(timeoutValue);
7
+ if (isNaN(timeout) || timeout <= 0 || timeout === Infinity) timeout = DEFAULT_START_TIMEOUT;
8
+ return timeout;
9
+ };
10
+
11
+ const reportToParent = fileName => {
12
+ const timeout = getTimeout();
13
+ let timeoutBeforeStartId = setTimeout(() => {
14
+ postMessage({type: 'test', test: 0, name: 'FILE: /' + fileName});
15
+ postMessage({
16
+ name: `No tests found in ${timeout}ms`,
17
+ test: 0,
18
+ marker: new Error(),
19
+ operator: 'error',
20
+ fail: true
21
+ });
22
+ postMessage({type: 'end', test: 0, name: 'FILE: /' + fileName, fail: true});
23
+ }, timeout);
24
+ return msg => {
25
+ if (timeoutBeforeStartId) {
26
+ clearTimeout(timeoutBeforeStartId);
27
+ timeoutBeforeStartId = null;
28
+ }
29
+ if ((msg.type === 'test' || msg.type === 'end') && !msg.test && !msg.name) {
30
+ msg.name = 'FILE: /' + fileName;
31
+ }
32
+ postMessage(msg);
33
+ };
6
34
  };
7
35
 
8
36
  addEventListener('message', async event => {
@@ -1,8 +1,36 @@
1
- const reportToParent = fileName => msg => {
2
- if ((msg.type === 'test' || msg.type === 'end') && !msg.test && !msg.name) {
3
- msg.name = 'FILE: /' + fileName;
4
- }
5
- postMessage(msg);
1
+ const DEFAULT_START_TIMEOUT = 5_000;
2
+
3
+ const getTimeout = () => {
4
+ const timeoutValue = Deno.env.get('TAPE6_WORKER_START TIMEOUT');
5
+ if (!timeoutValue) return DEFAULT_START_TIMEOUT;
6
+ let timeout = Number(timeoutValue);
7
+ if (isNaN(timeout) || timeout <= 0 || timeout === Infinity) timeout = DEFAULT_START_TIMEOUT;
8
+ return timeout;
9
+ };
10
+
11
+ const reportToParent = fileName => {
12
+ const timeout = getTimeout();
13
+ let timeoutBeforeStartId = setTimeout(() => {
14
+ postMessage({type: 'test', test: 0, name: 'FILE: /' + fileName});
15
+ postMessage({
16
+ name: `No tests found in ${timeout}ms`,
17
+ test: 0,
18
+ marker: new Error(),
19
+ operator: 'error',
20
+ fail: true
21
+ });
22
+ postMessage({type: 'end', test: 0, name: 'FILE: /' + fileName, fail: true});
23
+ }, timeout);
24
+ return msg => {
25
+ if (timeoutBeforeStartId) {
26
+ clearTimeout(timeoutBeforeStartId);
27
+ timeoutBeforeStartId = null;
28
+ }
29
+ if ((msg.type === 'test' || msg.type === 'end') && !msg.test && !msg.name) {
30
+ msg.name = 'FILE: /' + fileName;
31
+ }
32
+ postMessage(msg);
33
+ };
6
34
  };
7
35
 
8
36
  addEventListener('message', async event => {
@@ -1,10 +1,39 @@
1
+ import process from 'node:process';
1
2
  import {parentPort} from 'node:worker_threads';
2
3
 
3
- const reportToParent = fileName => msg => {
4
- if ((msg.type === 'test' || msg.type === 'end') && !msg.test && !msg.name) {
5
- msg.name = 'FILE: /' + fileName;
6
- }
7
- parentPort.postMessage(msg);
4
+ const DEFAULT_START_TIMEOUT = 5_000;
5
+
6
+ const getTimeout = () => {
7
+ const timeoutValue = process.env.TAPE6_WORKER_START_TIMEOUT;
8
+ if (!timeoutValue) return DEFAULT_START_TIMEOUT;
9
+ let timeout = Number(timeoutValue);
10
+ if (isNaN(timeout) || timeout <= 0 || timeout === Infinity) timeout = DEFAULT_START_TIMEOUT;
11
+ return timeout;
12
+ };
13
+
14
+ const reportToParent = fileName => {
15
+ const timeout = getTimeout();
16
+ let timeoutBeforeStartId = setTimeout(() => {
17
+ parentPort.postMessage({type: 'test', test: 0, name: 'FILE: /' + fileName});
18
+ parentPort.postMessage({
19
+ name: `No tests found in ${timeout}ms`,
20
+ test: 0,
21
+ marker: new Error(),
22
+ operator: 'error',
23
+ fail: true
24
+ });
25
+ parentPort.postMessage({type: 'end', test: 0, name: 'FILE: /' + fileName, fail: true});
26
+ }, timeout);
27
+ return msg => {
28
+ if (timeoutBeforeStartId) {
29
+ clearTimeout(timeoutBeforeStartId);
30
+ timeoutBeforeStartId = null;
31
+ }
32
+ if ((msg.type === 'test' || msg.type === 'end') && !msg.test && !msg.name) {
33
+ msg.name = 'FILE: /' + fileName;
34
+ }
35
+ parentPort.postMessage(msg);
36
+ };
8
37
  };
9
38
 
10
39
  parentPort.on('message', async msg => {
@@ -4,6 +4,7 @@ import {pathToFileURL} from 'node:url';
4
4
 
5
5
  import {isStopTest} from '../../State.js';
6
6
  import EventServer from '../../utils/EventServer.js';
7
+ import {getTimeoutValue} from '../../utils/config.js';
7
8
  import {getOriginalConsole, setCurrentReporter} from '../../utils/capture-console.js';
8
9
  import {
9
10
  clearBeforeAll,
@@ -11,7 +12,8 @@ import {
11
12
  clearBeforeEach,
12
13
  clearAfterEach,
13
14
  setReporter,
14
- registerNotifyCallback
15
+ registerNotifyCallback,
16
+ unregisterNotifyCallback
15
17
  } from '../../test.js';
16
18
  import BypassReporter from './BypassReporter.js';
17
19
 
@@ -23,10 +25,16 @@ export default class TestWorker extends EventServer {
23
25
  this.testRunner = options.testRunner;
24
26
  this.counter = 0;
25
27
  this.originalConsole = getOriginalConsole() || globalThis.console;
28
+ this.timeout = getTimeoutValue();
29
+ this.timeoutId = null;
26
30
  }
27
31
  makeTask(fileName) {
28
32
  const id = String(++this.counter),
29
33
  reporter = new BypassReporter(this.reporter, event => {
34
+ if (this.timeoutId) {
35
+ clearTimeout(this.timeoutId);
36
+ this.timeoutId = null;
37
+ }
30
38
  try {
31
39
  this.report(id, event);
32
40
  } catch (error) {
@@ -39,33 +47,22 @@ export default class TestWorker extends EventServer {
39
47
  setReporter(reporter);
40
48
  setCurrentReporter(this.reporter);
41
49
  process.env.TAPE6_TEST_FILE_NAME = fileName;
42
- import(new URL(fileName, baseName))
50
+ const url = new URL(fileName, baseName);
51
+ import(url)
43
52
  .then(async () => {
44
53
  const testRunner = await this.testRunner;
45
54
  registerNotifyCallback(testRunner);
46
- })
47
- .catch(error => {
48
- this.report(id, {
49
- type: 'comment',
50
- name: 'fail to load: ' + (error.message || 'Worker error'),
51
- test: 0
52
- });
53
- try {
54
- this.report(id, {
55
- name: String(error),
56
- test: 0,
57
- marker: new Error(),
58
- operator: 'error',
59
- fail: true,
60
- data: {
61
- actual: error
62
- }
63
- });
64
- } catch (error) {
65
- if (!isStopTest(error)) throw error;
55
+ if (this.timeoutId) {
56
+ clearTimeout(this.timeoutId);
57
+ this.timeoutId = null;
66
58
  }
67
- this.close(id);
68
- });
59
+ this.timeoutId = setTimeout(() => {
60
+ this.timeoutId = null;
61
+ unregisterNotifyCallback(testRunner);
62
+ this.#reportTimeout(id, fileName);
63
+ }, this.timeout);
64
+ })
65
+ .catch(error => this.#reportError(id, error));
69
66
  return id;
70
67
  }
71
68
  destroyTask() {
@@ -76,4 +73,51 @@ export default class TestWorker extends EventServer {
76
73
  clearBeforeEach();
77
74
  clearAfterEach();
78
75
  }
76
+ #reportError(id, error) {
77
+ this.report(id, {
78
+ type: 'comment',
79
+ name: 'fail to load: ' + (error.message || 'Worker error'),
80
+ test: 0
81
+ });
82
+ try {
83
+ this.report(id, {
84
+ name: String(error),
85
+ test: 0,
86
+ marker: new Error(),
87
+ operator: 'error',
88
+ fail: true,
89
+ data: {
90
+ actual: error
91
+ }
92
+ });
93
+ } catch (error) {
94
+ if (!isStopTest(error)) throw error;
95
+ }
96
+ this.close(id);
97
+ }
98
+ #reportTimeout(id, fileName) {
99
+ this.report(id, {
100
+ type: 'test',
101
+ test: 0,
102
+ name: 'FILE: /' + fileName
103
+ });
104
+ try {
105
+ this.report(id, {
106
+ name: `No tests found in ${this.timeout}ms`,
107
+ test: 0,
108
+ marker: new Error(),
109
+ operator: 'error',
110
+ fail: true
111
+ });
112
+ } catch (error) {
113
+ if (!isStopTest(error)) throw error;
114
+ }
115
+ this.report(id, {
116
+ type: 'end',
117
+ test: 0,
118
+ name: 'FILE: /' + fileName,
119
+ fail: true
120
+ });
121
+ this.close(id);
122
+ }
79
123
  }
package/src/test.js CHANGED
@@ -12,7 +12,7 @@ let tests = [],
12
12
  reporter = null,
13
13
  testCounter = 0,
14
14
  isConfigured = false,
15
- notifyCallback = [];
15
+ notifyCallback = new Set();
16
16
 
17
17
  export const getConfiguredFlag = () => isConfigured;
18
18
  export const setConfiguredFlag = value => (isConfigured = !!value);
@@ -26,10 +26,12 @@ export const registerNotifyCallback = callback => {
26
26
  if (tests.length) {
27
27
  defer(callback);
28
28
  } else {
29
- notifyCallback.push(callback);
29
+ notifyCallback.add(callback);
30
30
  }
31
31
  };
32
32
 
33
+ export const unregisterNotifyCallback = callback => notifyCallback.delete(callback);
34
+
33
35
  const testers = [];
34
36
 
35
37
  export const getTesters = () => testers;
@@ -86,9 +88,9 @@ export const test = async (name, options, testFn) => {
86
88
  isTimerSet = true;
87
89
  }
88
90
  const deferred = makeDeferred();
89
- if (tests.push({options, deferred}) === 1 && notifyCallback.length) {
91
+ if (tests.push({options, deferred}) === 1 && notifyCallback.size) {
90
92
  for (const callback of notifyCallback) defer(callback);
91
- notifyCallback = [];
93
+ notifyCallback = new Set();
92
94
  }
93
95
 
94
96
  return deferred.promise;
@@ -2,18 +2,32 @@ import {promises as fsp} from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
4
  import {listing, wildToRe} from './listing.js';
5
- import {union, exclude} from './fileSets.js';
5
+
6
+ const exclude = (files, pattern) => {
7
+ const excluded = new Set();
8
+ for (const file of files) {
9
+ if (pattern.test(file)) excluded.add(file);
10
+ }
11
+ return files.difference(excluded);
12
+ };
6
13
 
7
14
  export const resolvePatterns = async (rootFolder, patterns) => {
8
- let result = [];
15
+ let result = new Set();
9
16
  for (const item of patterns) {
10
17
  if (item.length && item[0] == '!') {
11
18
  result = exclude(result, wildToRe(rootFolder, item.substring(1)));
12
19
  } else {
13
- result = union(result, await listing(rootFolder, item));
20
+ for await (const file of listing(rootFolder, item)) {
21
+ result.add(file);
22
+ }
14
23
  }
15
24
  }
16
- return result.map(fileName => path.relative(rootFolder, fileName));
25
+
26
+ const files = [];
27
+ for (const file of result) {
28
+ files.push(path.relative(rootFolder, file));
29
+ }
30
+ return files;
17
31
  };
18
32
 
19
33
  export const getConfig = async (rootFolder, traceFn) => {
@@ -88,9 +102,33 @@ if (typeof Deno == 'object' && Deno?.version) {
88
102
  runtime.name = 'browser';
89
103
  }
90
104
 
105
+ export const reporterFileNames = {
106
+ jsonl: 'JSONLReporter.js',
107
+ min: 'MinReporter.js',
108
+ tap: 'TapReporter.js',
109
+ tty: 'TTYReporter.js'
110
+ };
111
+
91
112
  export const getReporterType = () => {
92
113
  if (runtime.name === 'browser') return null;
93
114
  if (runtime.getEnvVar('TAPE6_JSONL')) return 'jsonl';
115
+ if (runtime.getEnvVar('TAPE6_MIN')) return 'min';
94
116
  if (runtime.getEnvVar('TAPE6_TAP')) return 'tap';
95
117
  return 'tty';
96
118
  };
119
+
120
+ export const getReporterFileName = type => {
121
+ const reporterType = type || getReporterType();
122
+ return reporterFileNames[reporterType] || reporterFileNames.tty;
123
+ };
124
+
125
+ export const DEFAULT_START_TIMEOUT = 5_000;
126
+
127
+ export const getTimeoutValue = () => {
128
+ if (runtime.name === 'browser') return DEFAULT_START_TIMEOUT;
129
+ const timeoutValue = runtime.getEnvVar('TAPE6_WORKER_START_TIMEOUT');
130
+ if (!timeoutValue) return DEFAULT_START_TIMEOUT;
131
+ let timeout = Number(timeoutValue);
132
+ if (isNaN(timeout) || timeout <= 0 || timeout === Infinity) timeout = DEFAULT_START_TIMEOUT;
133
+ return timeout;
134
+ };
@@ -17,44 +17,36 @@ const mergeWildcards = folders =>
17
17
  []
18
18
  );
19
19
 
20
- const listFiles = async (rootFolder, folders, baseRe, parents) => {
20
+ const listFiles = async function* (rootFolder, folders, baseRe, parents) {
21
21
  const dir = path.join(rootFolder, parents.join(path.sep)),
22
22
  files = await fsp.readdir(dir, {withFileTypes: true});
23
23
 
24
- let result = [];
25
-
26
24
  if (!folders.length) {
27
25
  for (const file of files) {
28
- if (file.isFile() && baseRe.test(file.name)) result.push(path.join(dir, file.name));
26
+ if (file.isFile() && baseRe.test(file.name)) yield path.join(dir, file.name);
29
27
  }
30
- return result;
28
+ return;
31
29
  }
32
30
 
33
31
  const theRest = folders.slice(1);
34
32
 
35
33
  if (folders[0]) {
36
34
  for (const file of files) {
37
- if (file.isDirectory() && folders[0].test(file.name)) {
38
- result = result.concat(
39
- await listFiles(rootFolder, theRest, baseRe, parents.concat(file.name))
40
- );
41
- }
35
+ if (file.isDirectory() && folders[0].test(file.name))
36
+ yield* listFiles(rootFolder, theRest, baseRe, parents.concat(file.name));
42
37
  }
43
- return result;
38
+ return;
44
39
  }
45
40
 
46
- result = result.concat(await listFiles(rootFolder, theRest, baseRe, parents));
41
+ yield* listFiles(rootFolder, theRest, baseRe, parents);
47
42
  for (const file of files) {
48
- if (file.isDirectory()) {
49
- result = result.concat(
50
- await listFiles(rootFolder, folders, baseRe, parents.concat(file.name))
51
- );
52
- }
43
+ if (file.isDirectory())
44
+ yield* listFiles(rootFolder, folders, baseRe, parents.concat(file.name));
53
45
  }
54
- return result;
46
+ return;
55
47
  };
56
48
 
57
- export const listing = async (rootFolder, wildcard) => {
49
+ export const listing = async function* (rootFolder, wildcard) {
58
50
  const parsed = path.parse(path.normalize(wildcard)),
59
51
  baseRe = new RegExp('^' + prepRe(parsed.name, '.*') + prepRe(parsed.ext, '.*', true) + '$'),
60
52
  folders = mergeWildcards(
@@ -63,7 +55,7 @@ export const listing = async (rootFolder, wildcard) => {
63
55
  .filter(part => part)
64
56
  .map(part => (part === '**' ? null : new RegExp('^' + prepRe(part, notSep) + '$')))
65
57
  );
66
- return listFiles(rootFolder, folders, baseRe, []);
58
+ yield* listFiles(rootFolder, folders, baseRe, []);
67
59
  };
68
60
 
69
61
  export const wildToRe = (rootFolder, wildcard) => {
@@ -1,114 +0,0 @@
1
- // file sets are represented as sorted name lists with no duplicated items
2
-
3
- export const normalize = fileSet =>
4
- fileSet.sort().filter((name, i, list) => !i || list[i - 1] !== name);
5
-
6
- export const union = (a, b) => {
7
- if (!a.length) return b.slice(0);
8
- if (!b.length) return a.slice(0);
9
-
10
- let i = 0,
11
- j = 0,
12
- x = a[0],
13
- y = b[0];
14
-
15
- const result = [];
16
- for (;;) {
17
- if (x < y) {
18
- result.push(x);
19
- ++i;
20
- if (i >= a.length) break;
21
- x = a[i];
22
- } else if (x > y) {
23
- result.push(y);
24
- ++j;
25
- if (j >= b.length) break;
26
- y = b[j];
27
- } else {
28
- result.push(x);
29
- ++i;
30
- ++j;
31
- if (i >= a.length || j >= b.length) break;
32
- x = a[i];
33
- y = b[j];
34
- }
35
- }
36
-
37
- if (i < a.length) {
38
- result.push(...a.slice(i));
39
- } else if (j < b.length) {
40
- result.push(...b.slice(j));
41
- }
42
-
43
- return result;
44
- };
45
-
46
- export const intersection = (a, b) => {
47
- if (!a.length || !b.length) return [];
48
-
49
- let i = 0,
50
- j = 0,
51
- x = a[0],
52
- y = b[0];
53
-
54
- const result = [];
55
- for (;;) {
56
- if (x < y) {
57
- ++i;
58
- if (i >= a.length) break;
59
- x = a[i];
60
- } else if (x > y) {
61
- ++j;
62
- if (j >= b.length) break;
63
- y = b[j];
64
- } else {
65
- result.push(x);
66
- ++i;
67
- ++j;
68
- if (i >= a.length || j >= b.length) break;
69
- x = a[i];
70
- y = b[j];
71
- }
72
- }
73
-
74
- return result;
75
- };
76
-
77
- export const difference = (a, b) => {
78
- if (!a.length) return [];
79
- if (!b.length) return a.slice(0);
80
-
81
- let i = 0,
82
- j = 0,
83
- x = a[0],
84
- y = b[0];
85
-
86
- const result = [];
87
- for (;;) {
88
- if (x < y) {
89
- result.push(x);
90
- ++i;
91
- if (i >= a.length) break;
92
- x = a[i];
93
- } else if (x > y) {
94
- ++j;
95
- if (j >= b.length) break;
96
- y = b[j];
97
- } else {
98
- ++i;
99
- ++j;
100
- if (i >= a.length || j >= b.length) break;
101
- x = a[i];
102
- y = b[j];
103
- }
104
- }
105
-
106
- if (i < a.length) {
107
- result.push(...a.slice(i));
108
- }
109
-
110
- return result;
111
- };
112
-
113
- export const filter = (a, re) => a.filter(item => re.test(item));
114
- export const exclude = (a, re) => a.filter(item => !re.test(item));