tape-six 0.9.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/LICENSE +11 -0
- package/README.md +9 -0
- package/bin/tape6-server.js +196 -0
- package/bin/tape6.js +187 -0
- package/index.js +74 -0
- package/package.json +57 -0
- package/src/State.js +93 -0
- package/src/TTYReporter.js +270 -0
- package/src/TapReporter.js +126 -0
- package/src/Tester.js +411 -0
- package/src/deep6/env.js +174 -0
- package/src/deep6/index.js +21 -0
- package/src/deep6/traverse/assemble.js +158 -0
- package/src/deep6/traverse/clone.js +109 -0
- package/src/deep6/traverse/deref.js +104 -0
- package/src/deep6/traverse/preprocess.js +112 -0
- package/src/deep6/traverse/walk.js +262 -0
- package/src/deep6/unifiers/matchCondition.js +16 -0
- package/src/deep6/unifiers/matchInstanceOf.js +16 -0
- package/src/deep6/unifiers/matchString.js +33 -0
- package/src/deep6/unifiers/matchTypeOf.js +16 -0
- package/src/deep6/unifiers/ref.js +21 -0
- package/src/deep6/unify.js +446 -0
- package/src/deep6/utils/replaceVars.js +24 -0
- package/src/node/TestWorker.js +37 -0
- package/src/node/config.js +56 -0
- package/src/node/listing.js +78 -0
- package/src/test.js +187 -0
- package/src/utils/Deferred.js +10 -0
- package/src/utils/EventServer.js +78 -0
- package/src/utils/box.js +101 -0
- package/src/utils/defer.js +33 -0
- package/src/utils/fileSets.js +113 -0
- package/src/utils/formatters.js +30 -0
- package/src/utils/timeout.js +3 -0
- package/src/utils/timer.js +24 -0
- package/src/utils/yamlFormatter.js +150 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Copyright 2005-2022 Eugene Lazutkin
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
4
|
+
|
|
5
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
6
|
+
|
|
7
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
8
|
+
|
|
9
|
+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
10
|
+
|
|
11
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# tape6 [![NPM version][npm-img]][npm-url]
|
|
2
|
+
|
|
3
|
+
[npm-img]: https://img.shields.io/npm/v/tape6.svg
|
|
4
|
+
[npm-url]: https://npmjs.org/package/tape6
|
|
5
|
+
|
|
6
|
+
tape6 is a [TAP](https://en.wikipedia.org/wiki/Test_Anything_Protocol)-based library for unit tests. It is written in modern ES6 and works in [node](https://nodejs.org/), [deno](https://deno.land/) and browsers.
|
|
7
|
+
|
|
8
|
+
The documentation is TBD but you can inspect `tests/` to see how it is used.
|
|
9
|
+
If you are familiar with other TAP-based libraries you'll feel right at home.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import http from 'http';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
import {resolveTests, resolvePatterns} from '../src/node/config.js';
|
|
8
|
+
|
|
9
|
+
const fsp = fs.promises;
|
|
10
|
+
|
|
11
|
+
// simple static server with no dependencies
|
|
12
|
+
|
|
13
|
+
// MIME source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
|
14
|
+
const mimeTable = {
|
|
15
|
+
css: 'text/css',
|
|
16
|
+
csv: 'text/csv',
|
|
17
|
+
eot: 'application/vnd.ms-fontobject',
|
|
18
|
+
gif: 'image/gif',
|
|
19
|
+
html: 'text/html',
|
|
20
|
+
ico: 'image/vnd.microsoft.icon',
|
|
21
|
+
jpg: 'image/jpeg',
|
|
22
|
+
js: 'text/javascript',
|
|
23
|
+
json: 'application/json',
|
|
24
|
+
otf: 'font/otf',
|
|
25
|
+
png: 'image/png',
|
|
26
|
+
svg: 'image/svg+xml',
|
|
27
|
+
ttf: 'font/ttf',
|
|
28
|
+
txt: 'text/plain',
|
|
29
|
+
webp: 'image/webp',
|
|
30
|
+
woff: 'font/woff',
|
|
31
|
+
woff2: 'font/woff2',
|
|
32
|
+
xml: 'application/xml'
|
|
33
|
+
},
|
|
34
|
+
defaultMime = 'application/octet-stream',
|
|
35
|
+
rootFolder = process.env.SERVER_ROOT || process.cwd(),
|
|
36
|
+
traceCalls = process.argv.includes('--trace'),
|
|
37
|
+
isTTY = process.stdout.isTTY,
|
|
38
|
+
hasColors = isTTY && process.stdout.hasColors();
|
|
39
|
+
|
|
40
|
+
let webAppPath = process.env.WEBAPP_PATH;
|
|
41
|
+
if (!webAppPath) {
|
|
42
|
+
const url = import.meta.url;
|
|
43
|
+
if (!/^file:\/\//i.test(url)) throw Error('Cannot identify the location of the web application. Use WEBAPP_PATH.');
|
|
44
|
+
webAppPath = path.relative(rootFolder, path.join(path.dirname(url.substr(7)), '../webApp/'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// common aliases
|
|
48
|
+
const mimeAliases = {mjs: 'js', cjs: 'js', htm: 'html', jpeg: 'jpg'};
|
|
49
|
+
Object.keys(mimeAliases).forEach(name => (mimeTable[name] = mimeTable[mimeAliases[name]]));
|
|
50
|
+
|
|
51
|
+
// colors to use
|
|
52
|
+
const join = (...args) => args.map(value => value || '').join(''),
|
|
53
|
+
paint = hasColors ? (prefix, suffix = '\x1B[39m') => text => join(prefix, text, suffix) : () => text => text,
|
|
54
|
+
grey = paint('\x1B[2;37m', '\x1B[22;39m'),
|
|
55
|
+
red = paint('\x1B[41;97m', '\x1B[49;39m'),
|
|
56
|
+
green = paint('\x1B[32m'),
|
|
57
|
+
yellow = paint('\x1B[93m'),
|
|
58
|
+
blue = paint('\x1B[44;97m', '\x1B[49;39m');
|
|
59
|
+
|
|
60
|
+
// sending helpers
|
|
61
|
+
|
|
62
|
+
const sendFile = (req, res, fileName, ext, justHeaders) => {
|
|
63
|
+
if (!ext) {
|
|
64
|
+
ext = path.extname(fileName).toLowerCase();
|
|
65
|
+
}
|
|
66
|
+
let mime = ext && mimeTable[ext.substr(1)];
|
|
67
|
+
if (!mime || typeof mime != 'string') {
|
|
68
|
+
mime = defaultMime;
|
|
69
|
+
}
|
|
70
|
+
res.writeHead(200, {'Content-Type': mime});
|
|
71
|
+
if (justHeaders) {
|
|
72
|
+
res.end();
|
|
73
|
+
} else {
|
|
74
|
+
fs.createReadStream(fileName).pipe(res);
|
|
75
|
+
}
|
|
76
|
+
traceCalls && console.log(green('200') + ' ' + grey(req.method) + ' ' + grey(req.url));
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const sendJson = (req, res, json, justHeaders) => {
|
|
80
|
+
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
81
|
+
if (justHeaders) {
|
|
82
|
+
res.end();
|
|
83
|
+
} else {
|
|
84
|
+
res.end(JSON.stringify(json));
|
|
85
|
+
}
|
|
86
|
+
traceCalls && console.log(green('200') + ' ' + grey(req.method) + ' ' + grey(req.url));
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const sendRedirect = (req, res, to, code = 301) => {
|
|
90
|
+
res.writeHead(code, {Location: to});
|
|
91
|
+
res.end();
|
|
92
|
+
traceCalls && console.log(blue(code) + ' ' + grey(req.method) + ' ' + grey(req.url));
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const bailOut = (req, res, code = 404) => {
|
|
96
|
+
res.writeHead(code).end();
|
|
97
|
+
traceCalls && console.log(red(code) + ' ' + grey(req.method) + ' ' + grey(req.url));
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// server
|
|
101
|
+
|
|
102
|
+
const server = http.createServer(async (req, res) => {
|
|
103
|
+
const method = req.method.toUpperCase();
|
|
104
|
+
if (method !== 'GET' && method !== 'HEAD') return bailOut(req, res, 405);
|
|
105
|
+
|
|
106
|
+
const url = new URL(req.url, 'http://' + req.headers.host);
|
|
107
|
+
if (url.pathname === '/--tests') {
|
|
108
|
+
// get tests
|
|
109
|
+
return sendJson(req, res, await resolveTests(rootFolder, 'browser'), method === 'HEAD');
|
|
110
|
+
}
|
|
111
|
+
if (url.pathname === '/--patterns') {
|
|
112
|
+
// resolve patterns
|
|
113
|
+
return sendJson(req, res, await resolvePatterns(rootFolder, url.searchParams.getAll('q')), method === 'HEAD');
|
|
114
|
+
}
|
|
115
|
+
if (url.pathname === '/' || url.pathname === '/index' || url.pathname === '/index.html') {
|
|
116
|
+
// redirect to the web app
|
|
117
|
+
url.pathname = webAppPath;
|
|
118
|
+
return sendRedirect(req, res, url.href);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (path.normalize(url.pathname).includes('..')) return bailOut(req, res, 403);
|
|
122
|
+
|
|
123
|
+
const fileName = path.join(rootFolder, url.pathname),
|
|
124
|
+
ext = path.extname(fileName).toLowerCase(),
|
|
125
|
+
stat = await fsp.stat(fileName).catch(() => null);
|
|
126
|
+
if (stat && stat.isFile()) return sendFile(req, res, fileName, ext, method === 'HEAD');
|
|
127
|
+
|
|
128
|
+
if (stat && stat.isDirectory()) {
|
|
129
|
+
if (fileName.length && fileName[fileName.length - 1] == path.sep) {
|
|
130
|
+
const altFile = path.join(fileName, 'index.html'),
|
|
131
|
+
stat = await fsp.stat(altFile).catch(() => null);
|
|
132
|
+
if (stat && stat.isFile()) return sendFile(req, res, altFile, '.html', method === 'HEAD');
|
|
133
|
+
} else {
|
|
134
|
+
url.pathname += path.sep;
|
|
135
|
+
return sendRedirect(req, res, url.href);
|
|
136
|
+
}
|
|
137
|
+
return bailOut(req, res);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!ext && fileName.length && fileName[fileName.length - 1] != path.sep) {
|
|
141
|
+
const altFile = fileName + '.html',
|
|
142
|
+
stat = await fsp.stat(altFile).catch(() => null);
|
|
143
|
+
if (stat && stat.isFile()) return sendFile(req, res, altFile, '.html', method === 'HEAD');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
bailOut(req, res);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
server.on('clientError', (err, socket) => {
|
|
150
|
+
if (err.code === 'ECONNRESET' || !socket.writable) return;
|
|
151
|
+
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// general setup
|
|
155
|
+
|
|
156
|
+
const normalizePort = val => {
|
|
157
|
+
const port = parseInt(val);
|
|
158
|
+
if (isNaN(port)) return val; // named pipe
|
|
159
|
+
if (port >= 0) return port; // port number
|
|
160
|
+
return false;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const portToString = port => (typeof port === 'string' ? 'pipe' : 'port') + ' ' + port;
|
|
164
|
+
|
|
165
|
+
const host = process.env.HOST || 'localhost',
|
|
166
|
+
port = normalizePort(process.env.PORT || '3000');
|
|
167
|
+
|
|
168
|
+
server.listen(port, host);
|
|
169
|
+
|
|
170
|
+
server.on('error', error => {
|
|
171
|
+
if (error.syscall !== 'listen') throw error;
|
|
172
|
+
const bind = portToString(port);
|
|
173
|
+
switch (error.code) {
|
|
174
|
+
case 'EACCES':
|
|
175
|
+
console.log(red('Error: ') + yellow(bind) + red(' requires elevated privileges') + '\n');
|
|
176
|
+
process.exit(1);
|
|
177
|
+
case 'EADDRINUSE':
|
|
178
|
+
console.log(red('Error: ') + yellow(bind) + red(' is already in use') + '\n');
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
throw error;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
server.on('listening', () => {
|
|
185
|
+
//const addr = server.address();
|
|
186
|
+
const bind = portToString(port);
|
|
187
|
+
console.log(
|
|
188
|
+
grey('Listening on ') +
|
|
189
|
+
yellow(host || 'all network interfaces') +
|
|
190
|
+
grey(' at ') +
|
|
191
|
+
yellow(bind) +
|
|
192
|
+
grey(', serving static files from ') +
|
|
193
|
+
yellow(rootFolder) +
|
|
194
|
+
'\n'
|
|
195
|
+
);
|
|
196
|
+
});
|
package/bin/tape6.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import cluster from 'cluster';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
import {resolveTests, resolvePatterns} from '../src/node/config.js';
|
|
8
|
+
|
|
9
|
+
import {getTests, clearTests, getReporter, setReporter, runTests, setConfiguredFlag} from '../src/test.js';
|
|
10
|
+
import State from '../src/State.js';
|
|
11
|
+
import TapReporter from '../src/TapReporter.js';
|
|
12
|
+
import TestWorker from '../src/node/TestWorker.js';
|
|
13
|
+
import {selectTimer} from '../src/utils/timer.js';
|
|
14
|
+
import defer from '../src/utils/defer.js';
|
|
15
|
+
|
|
16
|
+
const options = {},
|
|
17
|
+
rootFolder = process.cwd();
|
|
18
|
+
|
|
19
|
+
let flags = '',
|
|
20
|
+
parallel = '',
|
|
21
|
+
files = [];
|
|
22
|
+
|
|
23
|
+
const masterConfiguration = () => {
|
|
24
|
+
const optionNames = {f: 'failureOnly', t: 'showTime', b: 'showBanner', d: 'showData', o: 'failOnce', n: 'showAssertNumber'};
|
|
25
|
+
|
|
26
|
+
let flagIsSet = false,
|
|
27
|
+
parIsSet = false;
|
|
28
|
+
|
|
29
|
+
for (let i = 2; i < process.argv.length; ++i) {
|
|
30
|
+
const arg = process.argv[i];
|
|
31
|
+
if (arg == '-f' || arg == '--flags') {
|
|
32
|
+
if (++i < process.argv.length) {
|
|
33
|
+
flags = process.argv[i];
|
|
34
|
+
flagIsSet = true;
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
} else if (arg == '-p' || arg == '--par') {
|
|
38
|
+
if (++i < process.argv.length) {
|
|
39
|
+
parallel = process.argv[i];
|
|
40
|
+
parIsSet = true;
|
|
41
|
+
if (!parallel || isNaN(parallel)) {
|
|
42
|
+
parallel = '';
|
|
43
|
+
parIsSet = false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
files.push(arg);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!flagIsSet) {
|
|
52
|
+
flags = process.env.TAPE6_FLAGS || flags;
|
|
53
|
+
}
|
|
54
|
+
for (let i = 0; i < flags.length; ++i) {
|
|
55
|
+
const option = flags[i].toLowerCase(),
|
|
56
|
+
name = optionNames[option];
|
|
57
|
+
if (typeof name == 'string') options[name] = option !== flags[i];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!parIsSet) {
|
|
61
|
+
parallel = process.env.TAPE6_PAR || parallel;
|
|
62
|
+
}
|
|
63
|
+
if (parallel) {
|
|
64
|
+
parallel = Math.max(0, +parallel);
|
|
65
|
+
if (parallel === Infinity) parallel = 0;
|
|
66
|
+
} else {
|
|
67
|
+
parallel = 0;
|
|
68
|
+
}
|
|
69
|
+
if (!parallel) parallel = os.cpus().length;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const masterInitialization = async () => {
|
|
73
|
+
let reporter = getReporter();
|
|
74
|
+
if (!reporter) {
|
|
75
|
+
if (process.stdout.isTTY) {
|
|
76
|
+
if (!process.env.TAPE6_TAP) {
|
|
77
|
+
const TTYReporter = (await import('../src/TTYReporter.js')).default,
|
|
78
|
+
ttyReporter = new TTYReporter(options);
|
|
79
|
+
ttyReporter.testCounter = -2;
|
|
80
|
+
ttyReporter.technicalDepth = 1;
|
|
81
|
+
reporter = ttyReporter.report.bind(ttyReporter);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!reporter) {
|
|
85
|
+
const tapReporter = new TapReporter({useJson: true});
|
|
86
|
+
reporter = tapReporter.report.bind(tapReporter);
|
|
87
|
+
}
|
|
88
|
+
setReporter(reporter);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (files.length) {
|
|
92
|
+
files = await resolvePatterns(rootFolder, files);
|
|
93
|
+
} else {
|
|
94
|
+
files = await resolveTests(rootFolder, 'node');
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const masterProcess = async () => {
|
|
99
|
+
masterConfiguration();
|
|
100
|
+
await masterInitialization();
|
|
101
|
+
await selectTimer();
|
|
102
|
+
|
|
103
|
+
const rootState = new State(null, {callback: getReporter(), failOnce: options.failOnce}),
|
|
104
|
+
worker = new TestWorker(event => rootState.emit(event), parallel, options);
|
|
105
|
+
|
|
106
|
+
rootState.emit({type: 'test', test: 0, time: rootState.timer.now()});
|
|
107
|
+
await new Promise(resolve => {
|
|
108
|
+
worker.done = () => resolve();
|
|
109
|
+
worker.execute(files);
|
|
110
|
+
});
|
|
111
|
+
rootState.emit({type: 'end', test: 0, time: rootState.timer.now(), fail: rootState.failed > 0, data: rootState});
|
|
112
|
+
|
|
113
|
+
process.exit(rootState.failed > 0 ? 1 : 0);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
class BufferedReporter {
|
|
117
|
+
constructor() {
|
|
118
|
+
this.buffer = [];
|
|
119
|
+
this.inFlight = false;
|
|
120
|
+
this.shouldExit = false;
|
|
121
|
+
}
|
|
122
|
+
report(event) {
|
|
123
|
+
if (this.inFlight) {
|
|
124
|
+
this.buffer.push(event);
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
this.buffer.push(event);
|
|
128
|
+
return this.send();
|
|
129
|
+
}
|
|
130
|
+
send() {
|
|
131
|
+
if (!this.buffer.length) {
|
|
132
|
+
this.shouldExit && defer(() => process.exit(0));
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
this.inFlight = true;
|
|
136
|
+
const events = this.buffer;
|
|
137
|
+
this.buffer = [];
|
|
138
|
+
process.send({events});
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const reporter = new BufferedReporter();
|
|
144
|
+
|
|
145
|
+
const workerProcess = async () => {
|
|
146
|
+
setReporter(reporter.report.bind(reporter));
|
|
147
|
+
|
|
148
|
+
await new Promise((resolve, reject) => {
|
|
149
|
+
process.on('message', async ({id, fileName, options, received, done}) => {
|
|
150
|
+
reporter.inFlight = false;
|
|
151
|
+
|
|
152
|
+
if (done) return resolve();
|
|
153
|
+
|
|
154
|
+
if (received) {
|
|
155
|
+
if (reporter.buffer.length) {
|
|
156
|
+
reporter.send();
|
|
157
|
+
} else if (reporter.shouldExit) {
|
|
158
|
+
resolve();
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await import(path.join(rootFolder, fileName));
|
|
165
|
+
} catch (error) {
|
|
166
|
+
reject(error);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
process.send({started: true});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (reporter.inFlight || reporter.buffer.length) {
|
|
174
|
+
reporter.shouldExit = true;
|
|
175
|
+
} else {
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const main = async () => {
|
|
181
|
+
if (cluster.isMaster) {
|
|
182
|
+
await masterProcess();
|
|
183
|
+
} else if (cluster.isWorker) {
|
|
184
|
+
await workerProcess();
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
main().catch(error => console.error('ERROR:', error));
|
package/index.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {test, getTests, clearTests, getReporter, setReporter, runTests, getConfiguredFlag} from './src/test.js';
|
|
2
|
+
import defer from './src/utils/defer.js';
|
|
3
|
+
import State from './src/State.js';
|
|
4
|
+
import TapReporter from './src/TapReporter.js';
|
|
5
|
+
|
|
6
|
+
const optionNames = {f: 'failureOnly', t: 'showTime', b: 'showBanner', d: 'showData', o: 'failOnce', n: 'showAssertNumber'};
|
|
7
|
+
|
|
8
|
+
defer(async () => {
|
|
9
|
+
if (getConfiguredFlag()) return; // bail out => somebody else is running the show
|
|
10
|
+
|
|
11
|
+
const isNode = typeof process == 'object' && typeof process.exit == 'function',
|
|
12
|
+
isBrowser = typeof window == 'object' && !!window.location,
|
|
13
|
+
options = {};
|
|
14
|
+
|
|
15
|
+
let flags = '';
|
|
16
|
+
|
|
17
|
+
if (isBrowser) {
|
|
18
|
+
if (typeof window.__tape6_flags == 'string') {
|
|
19
|
+
flags = window.__tape6_flags;
|
|
20
|
+
} else if (window.location.search) {
|
|
21
|
+
flags = (new URLSearchParams(window.location.search.substr(1))).get('flags') || '';
|
|
22
|
+
}
|
|
23
|
+
} else if (isNode) {
|
|
24
|
+
flags = process.env.TAPE6_FLAGS || '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < flags.length; ++i) {
|
|
28
|
+
const option = flags[i].toLowerCase(),
|
|
29
|
+
name = optionNames[option];
|
|
30
|
+
if (typeof name == 'string') options[name] = option !== flags[i];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let reporter = getReporter();
|
|
34
|
+
if (!reporter) {
|
|
35
|
+
if (isBrowser) {
|
|
36
|
+
const id = window.__tape6_id || (new URLSearchParams(window.location.search.substr(1))).get('id');
|
|
37
|
+
if (typeof window.__tape6_reporter == 'function') {
|
|
38
|
+
reporter = event => window.__tape6_reporter(id, event);
|
|
39
|
+
} else if (window.parent && typeof window.parent.__tape6_reporter == 'function') {
|
|
40
|
+
reporter = event => window.parent.__tape6_reporter(id, event);
|
|
41
|
+
}
|
|
42
|
+
} else if (isNode) {
|
|
43
|
+
if (process.stdout.isTTY && !process.env.TAPE6_TAP) {
|
|
44
|
+
const TTYReporter = (await import('./src/TTYReporter.js')).default,
|
|
45
|
+
ttyReporter = new TTYReporter(options);
|
|
46
|
+
reporter = ttyReporter.report.bind(ttyReporter);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!reporter) {
|
|
50
|
+
const tapReporter = new TapReporter({useJson: true});
|
|
51
|
+
reporter = tapReporter.report.bind(tapReporter);
|
|
52
|
+
}
|
|
53
|
+
setReporter(reporter);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const rootState = new State(null, {callback: reporter, failOnce: options.failOnce});
|
|
57
|
+
|
|
58
|
+
rootState.emit({type: 'test', test: 0, time: rootState.timer.now()});
|
|
59
|
+
for (;;) {
|
|
60
|
+
const tests = getTests();
|
|
61
|
+
if (!tests.length) break;
|
|
62
|
+
clearTests();
|
|
63
|
+
await runTests(rootState, tests);
|
|
64
|
+
}
|
|
65
|
+
rootState.emit({type: 'end', test: 0, time: rootState.timer.now(), fail: rootState.failed > 0, data: rootState});
|
|
66
|
+
|
|
67
|
+
if (isNode) {
|
|
68
|
+
!process.env.TAPE6_WORKER && process.exit(rootState.failed > 0 ? 1 : 0);
|
|
69
|
+
} else if (typeof __reportTape6Results == 'function') {
|
|
70
|
+
__reportTape6Results(rootState.failed > 0 ? 'failure' : 'success');
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export default test;
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tape-six",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "TAP for the modern JavaScript (ES6).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"module": "index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.js"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"tape6": "bin/tape6.js",
|
|
13
|
+
"tape6-server": "bin/tape6-server.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node bin/tape6-server.js --trace",
|
|
17
|
+
"test-chrome": "node tests/puppeteer-chrome.js",
|
|
18
|
+
"copyDeep6": "node scripts/copyFolder.js --src ./vendors/deep6/src --dst ./src/deep6 --clear",
|
|
19
|
+
"build": "npm run copyDeep6",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"github": "http://github.com/uhop/tape6",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/uhop/tape6.git"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"tap",
|
|
29
|
+
"test",
|
|
30
|
+
"harness",
|
|
31
|
+
"assert",
|
|
32
|
+
"browser"
|
|
33
|
+
],
|
|
34
|
+
"author": "Eugene Lazutkin <eugene.lazutkin@gmail.com> (https://www.lazutkin.com/)",
|
|
35
|
+
"license": "BSD-3-Clause",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/uhop/tape6/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/uhop/tape6#readme",
|
|
40
|
+
"files": [
|
|
41
|
+
"index.js",
|
|
42
|
+
"bin",
|
|
43
|
+
"web",
|
|
44
|
+
"src",
|
|
45
|
+
"cjs"
|
|
46
|
+
],
|
|
47
|
+
"tape6": {
|
|
48
|
+
"tests": [
|
|
49
|
+
"/tests/manual/test-skip*.js",
|
|
50
|
+
"/tests/manual/test-todo.js",
|
|
51
|
+
"/tests/manual/test-three*.js"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"puppeteer": "^15.3.2"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/State.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {getTimer} from './utils/timer.js';
|
|
2
|
+
|
|
3
|
+
export class StopTest extends Error {}
|
|
4
|
+
|
|
5
|
+
const serialize = object => {
|
|
6
|
+
if (object instanceof Error) return {type: 'Error', message: object.message, stack: object.stack};
|
|
7
|
+
if (object instanceof RegExp) return {type: 'RegExp', source: object.source, flags: object.flags};
|
|
8
|
+
try {
|
|
9
|
+
return JSON.stringify(object);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
// squelch
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
return {type: 'String', value: String(object)};
|
|
15
|
+
} catch (error) {
|
|
16
|
+
// squelch
|
|
17
|
+
}
|
|
18
|
+
return {problem: 'cannot convert value to JSON or string'};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
class State {
|
|
22
|
+
constructor(parent, {callback, skip, todo, failOnce}) {
|
|
23
|
+
this.parent = parent;
|
|
24
|
+
parent = parent || {};
|
|
25
|
+
this.callback = callback || parent.callback;
|
|
26
|
+
this.skip = skip || parent.skip;
|
|
27
|
+
this.todo = todo || parent.todo;
|
|
28
|
+
this.failOnce = failOnce || parent.failOnce;
|
|
29
|
+
this.offset = parent.asserts || 0;
|
|
30
|
+
this.timer = parent.timer || getTimer();
|
|
31
|
+
this.asserts = this.skipped = this.failed = 0;
|
|
32
|
+
this.startTime = this.time = this.timer.now();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
updateParent() {
|
|
36
|
+
if (!this.parent) return;
|
|
37
|
+
this.parent.asserts += this.asserts;
|
|
38
|
+
this.parent.skipped += this.skipped;
|
|
39
|
+
this.parent.failed += this.failed;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
emit(event) {
|
|
43
|
+
event = {...event, skip: event.skip || this.skip, todo: event.todo || this.todo};
|
|
44
|
+
!event.type && (event.type = 'assert');
|
|
45
|
+
|
|
46
|
+
const isFailed = event.fail && !event.todo && !event.skip;
|
|
47
|
+
|
|
48
|
+
switch (event.type) {
|
|
49
|
+
case 'assert':
|
|
50
|
+
++this.asserts;
|
|
51
|
+
event.skip && ++this.skipped;
|
|
52
|
+
isFailed && ++this.failed;
|
|
53
|
+
event.id = this.asserts + this.offset;
|
|
54
|
+
!event.hasOwnProperty('diffTime') && (event.diffTime = event.time - this.time);
|
|
55
|
+
break;
|
|
56
|
+
case 'end':
|
|
57
|
+
!event.hasOwnProperty('diffTime') && (event.diffTime = event.time - this.startTime);
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (event.type === 'assert' && event.operator === 'error' && event.data && event.data.actual && typeof event.data.actual.stack == 'string') {
|
|
62
|
+
const lines = event.data.actual.stack.split('\n');
|
|
63
|
+
event.at = lines[Math.min(2, lines.length) - 1].trim().replace(/^at\s+/i, '');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!event.at && event.marker && typeof event.marker.stack == 'string') {
|
|
67
|
+
const lines = event.marker.stack.split('\n');
|
|
68
|
+
event.at = lines[Math.min(3, lines.length) - 1].trim().replace(/^at\s+/i, '');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (event.type === 'assert' && event.data) {
|
|
72
|
+
event.data.hasOwnProperty('expected') && (event.expected = serialize(event.data.expected));
|
|
73
|
+
event.data.hasOwnProperty('actual') && (event.actual = serialize(event.data.actual));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.callback(event);
|
|
77
|
+
|
|
78
|
+
switch (event.type) {
|
|
79
|
+
case 'assert':
|
|
80
|
+
if (isFailed && this.failOnce && !this.skip) {
|
|
81
|
+
for (let state = this; state; state = state.parent) state.skip = true;
|
|
82
|
+
throw new StopTest('failOnce is activated');
|
|
83
|
+
}
|
|
84
|
+
this.time = this.timer.now();
|
|
85
|
+
break;
|
|
86
|
+
case 'bail-out':
|
|
87
|
+
for (let state = this; state; state = state.parent) state.skip = true;
|
|
88
|
+
throw new StopTest('bailOut is activated');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default State;
|