@exodus/test 1.0.0-rc.32 → 1.0.0-rc.34
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 +1 -1
- package/bin/babel-worker.cjs +50 -0
- package/bin/bundle.js +285 -0
- package/bin/index.js +96 -257
- package/package.json +10 -3
- package/src/bundle-apis/crypto.cjs +1 -1
- package/src/bundle-apis/fs.cjs +5 -1
- package/src/bundle-apis/globals.cjs +37 -1
- package/src/bundle-apis/node-buffer.cjs +3 -0
- package/src/bundle-apis/util-format.cjs +1 -1
- package/src/engine.pure.cjs +86 -56
- package/src/jest.config.js +3 -3
- package/src/jest.environment.js +4 -0
- package/src/jest.js +1 -1
- package/src/jest.snapshot.js +15 -1
- package/src/tape.cjs +15 -0
- package/src/tape.js +4 -4
package/README.md
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const { Worker, MessageChannel, isMainThread, parentPort } = require('node:worker_threads')
|
|
2
|
+
const { once } = require('node:events')
|
|
3
|
+
const { availableParallelism } = require('node:os')
|
|
4
|
+
|
|
5
|
+
if (isMainThread) {
|
|
6
|
+
const maxWorkers = availableParallelism() >= 4 ? 2 : 1
|
|
7
|
+
const workers = []
|
|
8
|
+
|
|
9
|
+
const getWorker = () => {
|
|
10
|
+
const idle = workers.find((info) => info.busy === 0)
|
|
11
|
+
if (idle) return idle
|
|
12
|
+
|
|
13
|
+
if (workers.length < maxWorkers) {
|
|
14
|
+
const worker = new Worker(__filename)
|
|
15
|
+
worker.unref()
|
|
16
|
+
// unhandled top-level errors will crash automatically, which is desired behavior, no need to listen to error
|
|
17
|
+
workers.unshift({ worker, busy: 0 })
|
|
18
|
+
} else if (workers.length > 1) {
|
|
19
|
+
workers.sort((a, b) => a.busy - b.busy)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return workers[0]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const transformAsync = async (code, options) => {
|
|
26
|
+
const info = getWorker()
|
|
27
|
+
info.busy++
|
|
28
|
+
const channel = new MessageChannel()
|
|
29
|
+
info.worker.postMessage({ port: channel.port1, code, options }, [channel.port1])
|
|
30
|
+
const [{ result, error }] = await once(channel.port2, 'message')
|
|
31
|
+
info.busy--
|
|
32
|
+
if (error) throw error
|
|
33
|
+
return result
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { transformAsync }
|
|
37
|
+
} else {
|
|
38
|
+
const babel = require('@babel/core')
|
|
39
|
+
parentPort.on('message', ({ port, code: input, options }) => {
|
|
40
|
+
try {
|
|
41
|
+
const { code, sourcetype, map } = babel.transformSync(input, options) // async here is useless and slower
|
|
42
|
+
// additional properties are deleted as we don't want to transfer e.g. Plugin instances
|
|
43
|
+
port.postMessage({ result: { code, sourcetype, map } })
|
|
44
|
+
} catch (error) {
|
|
45
|
+
port.postMessage({ error })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
port.close()
|
|
49
|
+
})
|
|
50
|
+
}
|
package/bin/bundle.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
5
|
+
import { basename, dirname, resolve, join } from 'node:path'
|
|
6
|
+
import { createRequire } from 'node:module'
|
|
7
|
+
import { randomUUID as uuid, randomBytes } from 'node:crypto'
|
|
8
|
+
import * as esbuild from 'esbuild'
|
|
9
|
+
import glob from 'fast-glob'
|
|
10
|
+
|
|
11
|
+
const require = createRequire(import.meta.url)
|
|
12
|
+
const resolveRequire = (query) => require.resolve(query)
|
|
13
|
+
const resolveImport = import.meta.resolve && ((query) => fileURLToPath(import.meta.resolve(query)))
|
|
14
|
+
|
|
15
|
+
const readSnapshots = async (files) => {
|
|
16
|
+
const snapshots = []
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
for (const resolver of [
|
|
19
|
+
(dir, name) => [dir, `${name}.snapshot`], // node:test
|
|
20
|
+
(dir, name) => [dir, '__snapshots__', `${name}.snap`], // jest
|
|
21
|
+
]) {
|
|
22
|
+
const snapshotFile = join(...resolver(dirname(file), basename(file)))
|
|
23
|
+
try {
|
|
24
|
+
snapshots.push([snapshotFile, await readFile(snapshotFile, 'utf8')])
|
|
25
|
+
} catch (e) {
|
|
26
|
+
if (e.code !== 'ENOENT') throw e
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return snapshots
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// These packages throw on import
|
|
35
|
+
const blockedDeps = ['@pollyjs/adapter-node-http', '@pollyjs/node-server']
|
|
36
|
+
const loadPipeline = [
|
|
37
|
+
function (source, filepath) {
|
|
38
|
+
return source
|
|
39
|
+
.replace(/\bimport\.meta\.url\b/g, JSON.stringify(pathToFileURL(filepath)))
|
|
40
|
+
.replace(/\b(__dirname|import\.meta\.dirname)\b/g, JSON.stringify(dirname(filepath)))
|
|
41
|
+
.replace(/\b(__filename|import\.meta\.filename)\b/g, JSON.stringify(filepath))
|
|
42
|
+
},
|
|
43
|
+
function (source, filepath) {
|
|
44
|
+
// Just a convenience wrapper to show pretty errors instead of generic bundle-apis/empty/module-throw.cjs
|
|
45
|
+
for (const pkg of blockedDeps) {
|
|
46
|
+
const str = `require(${JSON.stringify(pkg)})`
|
|
47
|
+
assert(!str.includes("'"))
|
|
48
|
+
const err = `module unsupported in bundled form: ${pkg}\n loaded from ${filepath}`
|
|
49
|
+
const rep = `((() => { throw new Error(${JSON.stringify(err)}) })())`
|
|
50
|
+
for (const sub of [str, str.replaceAll('"', "'")]) source = source.replace(sub, rep)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return source
|
|
54
|
+
},
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
const options = {}
|
|
58
|
+
|
|
59
|
+
export const init = async ({ platform, jest, target, jestConfig, outdir }) => {
|
|
60
|
+
Object.assign(options, { platform, jest, target, jestConfig, outdir })
|
|
61
|
+
if (options.platform === 'hermes') {
|
|
62
|
+
const babel = await import('./babel-worker.cjs')
|
|
63
|
+
loadPipeline.push(async (source) => {
|
|
64
|
+
const result = await babel.transformAsync(source, {
|
|
65
|
+
compact: false,
|
|
66
|
+
babelrc: false,
|
|
67
|
+
configFile: false,
|
|
68
|
+
plugins: [
|
|
69
|
+
'@babel/plugin-syntax-typescript',
|
|
70
|
+
'@babel/plugin-transform-block-scoping',
|
|
71
|
+
'@babel/plugin-transform-class-properties',
|
|
72
|
+
'@babel/plugin-transform-classes',
|
|
73
|
+
'@babel/plugin-transform-private-methods',
|
|
74
|
+
],
|
|
75
|
+
})
|
|
76
|
+
return result.code
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const hermesSupported = {
|
|
82
|
+
arrow: false,
|
|
83
|
+
class: false, // we get a safeguard check this way that it's not used
|
|
84
|
+
'async-generator': false,
|
|
85
|
+
'const-and-let': false, // have to explicitly set for esbuild to not emit that in helpers, also to get a safeguard check
|
|
86
|
+
'for-await': false,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const getPackageFiles = async (dir) => {
|
|
90
|
+
// Returns an empty list on errors
|
|
91
|
+
let patterns
|
|
92
|
+
try {
|
|
93
|
+
patterns = JSON.parse(await readFile(resolve(dir, 'package.json'), 'utf8')).files
|
|
94
|
+
} catch {}
|
|
95
|
+
|
|
96
|
+
if (!patterns) {
|
|
97
|
+
const parent = dirname(dir)
|
|
98
|
+
if (parent !== dir) return getPackageFiles(parent)
|
|
99
|
+
return []
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Hack for now, TODO: fix this
|
|
103
|
+
const expanded = patterns.flatMap((x) => (x.includes('.') ? [x] : [x, `${x}/**/*`]))
|
|
104
|
+
return glob(expanded, { ignore: ['**/node_modules'], cwd: dir, absolute: true })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const loadCache = new Map()
|
|
108
|
+
const loadSourceFile = async (filepath) => {
|
|
109
|
+
if (!loadCache.has(filepath)) {
|
|
110
|
+
const load = async () => {
|
|
111
|
+
let contents = await readFile(filepath, 'utf8')
|
|
112
|
+
for (const transform of loadPipeline) contents = await transform(contents, filepath)
|
|
113
|
+
return contents
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
loadCache.set(filepath, load())
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return loadCache.get(filepath)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const build = async (...files) => {
|
|
123
|
+
const input = []
|
|
124
|
+
const importSource = async (file) => input.push(await loadSourceFile(resolveRequire(file)))
|
|
125
|
+
const importFile = (...args) => input.push(`await import(${JSON.stringify(resolve(...args))});`)
|
|
126
|
+
const stringify = (x) => ([undefined, null].includes(x) ? `${x}` : JSON.stringify(x))
|
|
127
|
+
|
|
128
|
+
if (!['node'].includes(options.platform)) {
|
|
129
|
+
if (['jsc', 'hermes'].includes(options.platform)) {
|
|
130
|
+
const entropy = randomBytes(5 * 1024).toString('base64')
|
|
131
|
+
input.push(`globalThis.EXODUS_TEST_CRYPTO_ENTROPY = ${stringify(entropy)};`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await importSource('../src/bundle-apis/globals.cjs')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (options.jest) {
|
|
138
|
+
const { jestConfig } = options
|
|
139
|
+
assert(jestConfig.rootDir)
|
|
140
|
+
const preload = [...(jestConfig.setupFiles || []), ...(jestConfig.setupFilesAfterEnv || [])]
|
|
141
|
+
if (jestConfig.testEnvironment && jestConfig.testEnvironment !== 'node') {
|
|
142
|
+
const { specialEnvironments } = await import('../src/jest.environment.js')
|
|
143
|
+
assert(Object.hasOwn(specialEnvironments, jestConfig.testEnvironment))
|
|
144
|
+
preload.push(...(specialEnvironments[jestConfig.testEnvironment].dependencies || []))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const local = createRequire(resolve(jestConfig.rootDir, 'package.json'))
|
|
148
|
+
const w = (f) => `[${stringify(f)}, () => require(${stringify(local.resolve(f))})]`
|
|
149
|
+
input.push(`globalThis.EXODUS_TEST_PRELOADED = [${preload.map((f) => w(f)).join(', ')}]`)
|
|
150
|
+
await importSource('./jest.js')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const file of files) importFile(file)
|
|
154
|
+
|
|
155
|
+
const filename = files.length === 1 ? `${files[0]}-${uuid().slice(0, 8)}` : `bundle-${uuid()}`
|
|
156
|
+
const outfile = `${join(options.outdir, filename)}.js`
|
|
157
|
+
const EXODUS_TEST_SNAPSHOTS = await readSnapshots(files)
|
|
158
|
+
const buildWrap = async (opts) => esbuild.build(opts).catch((err) => err)
|
|
159
|
+
let main = input.join(';\n')
|
|
160
|
+
if (['jsc', 'hermes'].includes(options.platform)) {
|
|
161
|
+
const exit = `EXODUS_TEST_PROCESS.exitCode = 1; EXODUS_TEST_PROCESS._maybeProcessExitCode();`
|
|
162
|
+
main = `try {\n${main}\n} catch (err) { print(err); ${exit} }`
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const fsfiles = await getPackageFiles(filename ? dirname(resolve(filename)) : process.cwd())
|
|
166
|
+
|
|
167
|
+
const hasBuffer = ['node', 'bun'].includes(options.platform)
|
|
168
|
+
const api = (f) => resolveRequire(join('../src/bundle-apis', f))
|
|
169
|
+
const res = await buildWrap({
|
|
170
|
+
logLevel: 'silent',
|
|
171
|
+
stdin: {
|
|
172
|
+
contents: `(async function () {\n${main}\n})()`,
|
|
173
|
+
resolveDir: dirname(fileURLToPath(import.meta.url)),
|
|
174
|
+
},
|
|
175
|
+
bundle: true,
|
|
176
|
+
outdir: options.outdir,
|
|
177
|
+
entryNames: filename,
|
|
178
|
+
platform: 'neutral',
|
|
179
|
+
mainFields: ['browser', 'module', 'main'],
|
|
180
|
+
define: {
|
|
181
|
+
'process.env.FORCE_COLOR': stringify('0'),
|
|
182
|
+
'process.env.NO_COLOR': stringify('1'),
|
|
183
|
+
'process.env.NODE_ENV': stringify(process.env.NODE_ENV),
|
|
184
|
+
'process.env.EXODUS_TEST_CONTEXT': stringify('pure'),
|
|
185
|
+
'process.env.EXODUS_TEST_ENVIRONMENT': stringify('bundle'),
|
|
186
|
+
'process.env.EXODUS_TEST_PLATFORM': stringify(process.env.EXODUS_TEST_PLATFORM),
|
|
187
|
+
'process.env.EXODUS_TEST_JEST_CONFIG': stringify(JSON.stringify(options.jestConfig)),
|
|
188
|
+
'process.env.EXODUS_TEST_EXECARGV': stringify(process.env.EXODUS_TEST_EXECARGV),
|
|
189
|
+
'process.env.NODE_DEBUG': stringify(),
|
|
190
|
+
'process.env.DEBUG': stringify(),
|
|
191
|
+
'process.env.READABLE_STREAM': stringify(),
|
|
192
|
+
'process.env.CI': stringify(process.env.CI),
|
|
193
|
+
'process.env.CI_ENABLE_VERBOSE_LOGS': stringify(process.env.CI_ENABLE_VERBOSE_LOGS),
|
|
194
|
+
'process.browser': stringify(true),
|
|
195
|
+
'process.emitWarning': 'undefined',
|
|
196
|
+
'process.stderr': 'undefined',
|
|
197
|
+
'process.stdout': 'undefined',
|
|
198
|
+
'process.type': 'undefined',
|
|
199
|
+
'process.version': stringify('v22.5.1'), // shouldn't depend on currently used Node.js version
|
|
200
|
+
'process.versions.node': stringify('22.5.1'), // see line above
|
|
201
|
+
EXODUS_TEST_FILES: stringify(files.map((f) => [dirname(f), basename(f)])),
|
|
202
|
+
EXODUS_TEST_SNAPSHOTS: stringify(EXODUS_TEST_SNAPSHOTS),
|
|
203
|
+
EXODUS_TEST_FSFILES: stringify(fsfiles), // TODO: can we safely use relative paths?
|
|
204
|
+
},
|
|
205
|
+
alias: {
|
|
206
|
+
// Jest and tape
|
|
207
|
+
'@jest/globals': resolveImport('../src/jest.js'),
|
|
208
|
+
tape: resolveImport('../src/tape.cjs'),
|
|
209
|
+
'tape-promise/tape': resolveImport('../src/tape.cjs'),
|
|
210
|
+
// Node browserify
|
|
211
|
+
'node:assert': dirname(dirname(resolveRequire('assert/'))),
|
|
212
|
+
'node:assert/strict': api('assert-strict.cjs'),
|
|
213
|
+
'node:fs': api('fs.cjs'),
|
|
214
|
+
'node:fs/promises': api('fs-promises.cjs'),
|
|
215
|
+
fs: api('fs.cjs'),
|
|
216
|
+
'fs/promises': api('fs-promises.cjs'),
|
|
217
|
+
assert: dirname(dirname(resolveRequire('assert/'))),
|
|
218
|
+
buffer: hasBuffer ? api('node-buffer.cjs') : dirname(resolveRequire('buffer/')),
|
|
219
|
+
child_process: api('child_process.cjs'),
|
|
220
|
+
constants: resolveRequire('constants-browserify'),
|
|
221
|
+
crypto: api('crypto.cjs'),
|
|
222
|
+
events: dirname(resolveRequire('events/')),
|
|
223
|
+
http: api('http.cjs'),
|
|
224
|
+
https: api('https.cjs'),
|
|
225
|
+
os: resolveRequire('os-browserify'),
|
|
226
|
+
path: resolveRequire('path-browserify'),
|
|
227
|
+
querystring: resolveRequire('querystring-es3'),
|
|
228
|
+
stream: resolveRequire('stream-browserify'),
|
|
229
|
+
timers: resolveRequire('timers-browserify'),
|
|
230
|
+
url: dirname(resolveRequire('url/')),
|
|
231
|
+
util: dirname(resolveRequire('util/')),
|
|
232
|
+
zlib: resolveRequire('browserify-zlib'),
|
|
233
|
+
// expect-related deps
|
|
234
|
+
'ansi-styles': api('ansi-styles.cjs'),
|
|
235
|
+
'jest-util': api('jest-util.js'),
|
|
236
|
+
'jest-message-util': api('jest-message-util.js'),
|
|
237
|
+
// unwanted deps
|
|
238
|
+
bindings: api('empty/function-throw.cjs'),
|
|
239
|
+
'node-gyp-build': api('empty/function-throw.cjs'),
|
|
240
|
+
ws: api('ws.cjs'),
|
|
241
|
+
// unsupported deps
|
|
242
|
+
...Object.fromEntries(blockedDeps.map((n) => [n, api('empty/module-throw.cjs')])),
|
|
243
|
+
},
|
|
244
|
+
sourcemap: ['hermes', 'jsc'].includes(options.platform) ? 'inline' : 'linked', // FIXME?
|
|
245
|
+
sourcesContent: false,
|
|
246
|
+
keepNames: true,
|
|
247
|
+
format: 'iife',
|
|
248
|
+
target: options.target || `node${process.versions.node}`,
|
|
249
|
+
supported: {
|
|
250
|
+
bigint: true,
|
|
251
|
+
...(options.platform === 'hermes' ? hermesSupported : {}),
|
|
252
|
+
},
|
|
253
|
+
plugins: [
|
|
254
|
+
{
|
|
255
|
+
name: 'exodus-test.bundle',
|
|
256
|
+
setup({ onLoad }) {
|
|
257
|
+
onLoad({ filter: /\.[cm]?[jt]s$/, namespace: 'file' }, async (args) => {
|
|
258
|
+
let filepath = args.path
|
|
259
|
+
// Resolve .native versions
|
|
260
|
+
// TODO: move flag to engine options
|
|
261
|
+
// TODO: maybe follow package.json for this
|
|
262
|
+
if (['jsc', 'hermes'].includes(options.platform)) {
|
|
263
|
+
const maybeNative = filepath.replace(/(\.[cm]?[jt]s)$/u, '.native$1')
|
|
264
|
+
if (existsSync(maybeNative)) filepath = maybeNative
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const loader = /\.[cm]?ts$/.test(filepath) ? 'ts' : 'js'
|
|
268
|
+
return { contents: await loadSourceFile(filepath), loader }
|
|
269
|
+
})
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
})
|
|
274
|
+
assert.equal(res instanceof Error, res.errors.length > 0)
|
|
275
|
+
|
|
276
|
+
// if (res.errors.length === 0) require('fs').copyFileSync(outfile, 'tempout.cjs') // DEBUG
|
|
277
|
+
|
|
278
|
+
// We treat warnings as errors, so just merge all them
|
|
279
|
+
const errors = []
|
|
280
|
+
const formatOpts = { color: process.stdout.hasColors?.(), terminalWidth: process.stdout.columns }
|
|
281
|
+
const formatMessages = (list, kind) => esbuild.formatMessages(list, { kind, ...formatOpts })
|
|
282
|
+
if (res.warnings.length > 0) errors.push(...(await formatMessages(res.warnings, 'warning')))
|
|
283
|
+
if (res.errors.length > 0) errors.push(...(await formatMessages(res.errors, 'error')))
|
|
284
|
+
return { file: outfile, errors }
|
|
285
|
+
}
|
package/bin/index.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { spawn } from 'node:child_process'
|
|
3
|
+
import { spawn, execFile as execFileCallback } from 'node:child_process'
|
|
4
|
+
import { promisify, inspect } from 'node:util'
|
|
4
5
|
import { once } from 'node:events'
|
|
5
|
-
import { fileURLToPath
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
6
7
|
import { basename, dirname, resolve, join } from 'node:path'
|
|
7
8
|
import { createRequire } from 'node:module'
|
|
8
|
-
import { randomUUID
|
|
9
|
-
import { existsSync } from 'node:fs'
|
|
9
|
+
import { randomUUID } from 'node:crypto'
|
|
10
|
+
import { existsSync, rmSync } from 'node:fs'
|
|
11
|
+
import { unlink } from 'node:fs/promises'
|
|
12
|
+
import { tmpdir, availableParallelism } from 'node:os'
|
|
10
13
|
import assert from 'node:assert/strict'
|
|
14
|
+
import { Queue } from '@chalker/queue'
|
|
11
15
|
import glob from 'fast-glob'
|
|
12
16
|
import { haveModuleMocks, haveSnapshots, haveForceExit } from '../src/version.js'
|
|
13
17
|
|
|
@@ -25,7 +29,7 @@ const ENGINES = new Map(
|
|
|
25
29
|
'bun:pure': { binary: 'bun', pure: true, hasImportLoader: false, ts: 'auto' },
|
|
26
30
|
'bun:bundle': { binary: 'bun', ...bundleOptions },
|
|
27
31
|
'deno:bundle': { binary: 'deno', binaryArgs: ['run'], target: 'deno1', ...bundleOptions },
|
|
28
|
-
'jsc:bundle': { binary: 'jsc', ...bundleOptions, target: '
|
|
32
|
+
'jsc:bundle': { binary: 'jsc', ...bundleOptions, target: 'safari13' },
|
|
29
33
|
'hermes:bundle': { binary: 'hermes', binaryArgs: ['-Og'], target: 'es2018', ...bundleOptions },
|
|
30
34
|
})
|
|
31
35
|
)
|
|
@@ -147,6 +151,7 @@ const { options, patterns } = parseOptions()
|
|
|
147
151
|
const engineOptions = ENGINES.get(options.engine)
|
|
148
152
|
assert(engineOptions, `Unknown engine: ${options.engine}`)
|
|
149
153
|
Object.assign(options, engineOptions)
|
|
154
|
+
options.platform = options.binary // binary can be overriden by c8
|
|
150
155
|
|
|
151
156
|
const require = createRequire(import.meta.url)
|
|
152
157
|
const resolveRequire = (query) => require.resolve(query)
|
|
@@ -348,258 +353,27 @@ if (options.coverage) {
|
|
|
348
353
|
args.unshift(options.binary)
|
|
349
354
|
options.binary = c8
|
|
350
355
|
// perhaps use text-summary ?
|
|
351
|
-
args.unshift('-r', 'text', '-r', 'html')
|
|
356
|
+
args.unshift('-r', 'text', '-r', 'html', '-r', 'lcov', '-r', 'json-summary')
|
|
352
357
|
} else {
|
|
353
358
|
throw new Error(`Unknown coverage engine: ${JSON.stringify(options.coverageEngine)}`)
|
|
354
359
|
}
|
|
355
360
|
}
|
|
356
361
|
|
|
357
362
|
process.env.EXODUS_TEST_EXECARGV = JSON.stringify(args)
|
|
358
|
-
|
|
363
|
+
let buildFile
|
|
359
364
|
|
|
360
365
|
if (options.bundle) {
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
const { rmSync } = await import('node:fs')
|
|
364
|
-
const os = await import('node:os')
|
|
365
|
-
const outdir = join(os.tmpdir(), `exodus-test-${randomUUID().slice(0, 8)}`)
|
|
366
|
-
process.on('beforeExit', async () => rmSync(outdir, { recursive: true, force: true }))
|
|
366
|
+
const outdir = join(tmpdir(), `exodus-test-${randomUUID().slice(0, 8)}`)
|
|
367
|
+
process.on('exit', () => rmSync(outdir, { recursive: true, force: true }))
|
|
367
368
|
assert.deepEqual(args, [])
|
|
368
|
-
if (options.binary === 'node') args.unshift('--enable-source-maps') // FIXME
|
|
369
|
-
|
|
370
|
-
const readSnapshots = async (ifiles) => {
|
|
371
|
-
const snapshots = []
|
|
372
|
-
for (const file of ifiles) {
|
|
373
|
-
for (const resolver of [
|
|
374
|
-
(dir, name) => [dir, `${name}.snapshot`], // node:test
|
|
375
|
-
(dir, name) => [dir, '__snapshots__', `${name}.snap`], // jest
|
|
376
|
-
]) {
|
|
377
|
-
const snapshotFile = join(...resolver(dirname(file), basename(file)))
|
|
378
|
-
try {
|
|
379
|
-
snapshots.push([snapshotFile, await readFile(snapshotFile, 'utf8')])
|
|
380
|
-
} catch (e) {
|
|
381
|
-
if (e.code !== 'ENOENT') throw e
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
return snapshots
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// These packages throw on import
|
|
390
|
-
const blockedDeps = ['@pollyjs/adapter-node-http', '@pollyjs/node-server']
|
|
391
|
-
const loadPipeline = [
|
|
392
|
-
function (source, args) {
|
|
393
|
-
return source
|
|
394
|
-
.replace(/\bimport\.meta\.url\b/g, JSON.stringify(pathToFileURL(args.path)))
|
|
395
|
-
.replace(/\b(__dirname|import\.meta\.dirname)\b/g, JSON.stringify(dirname(args.path)))
|
|
396
|
-
.replace(/\b(__filename|import\.meta\.filename)\b/g, JSON.stringify(args.path))
|
|
397
|
-
},
|
|
398
|
-
function (source, args) {
|
|
399
|
-
// Just a convenience wrapper to show pretty errors instead of generic bundle-apis/empty/module-throw.cjs
|
|
400
|
-
for (const pkg of blockedDeps) {
|
|
401
|
-
const str = `require(${JSON.stringify(pkg)})`
|
|
402
|
-
assert(!str.includes("'"))
|
|
403
|
-
const err = `module unsupported in bundled form: ${pkg}\n loaded from ${args.path}`
|
|
404
|
-
const rep = `((() => { throw new Error(${JSON.stringify(err)}) })())`
|
|
405
|
-
for (const sub of [str, str.replaceAll('"', "'")]) source = source.replace(sub, rep)
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
return source
|
|
409
|
-
},
|
|
410
|
-
]
|
|
411
|
-
|
|
412
|
-
const writePipeline = []
|
|
413
|
-
if (options.binary === 'hermes') {
|
|
414
|
-
const babel = await import('@babel/core')
|
|
415
|
-
writePipeline.push((source) => {
|
|
416
|
-
const result = babel.transformSync(source, {
|
|
417
|
-
compact: false,
|
|
418
|
-
plugins: [
|
|
419
|
-
'@babel/plugin-transform-arrow-functions',
|
|
420
|
-
'@babel/plugin-transform-async-generator-functions',
|
|
421
|
-
'@babel/plugin-transform-class-properties',
|
|
422
|
-
'@babel/plugin-transform-classes',
|
|
423
|
-
'@babel/plugin-transform-block-scoping',
|
|
424
|
-
],
|
|
425
|
-
})
|
|
426
|
-
return result.code
|
|
427
|
-
})
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const getPackageFiles = async () => {
|
|
431
|
-
// Returns an empty list on errors
|
|
432
|
-
let patterns
|
|
433
|
-
try {
|
|
434
|
-
patterns = JSON.parse(await readFile('package.json', 'utf8')).files
|
|
435
|
-
} catch {}
|
|
436
|
-
|
|
437
|
-
if (!patterns) return []
|
|
438
|
-
// Hack for now, TODO: fix this
|
|
439
|
-
const expanded = patterns.flatMap((x) => (x.includes('.') ? [x] : [x, `${x}/**/*`]))
|
|
440
|
-
return glob(expanded, { ignore: ['**/node_modules'] })
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const buildOne = async (...ifiles) => {
|
|
444
|
-
const input = []
|
|
445
|
-
const importSource = async (file) => input.push(await readFile(resolveRequire(file), 'utf8'))
|
|
446
|
-
const importFile = (...args) => input.push(`await import(${JSON.stringify(resolve(...args))});`)
|
|
447
|
-
const stringify = (x) => ([undefined, null].includes(x) ? `${x}` : JSON.stringify(x))
|
|
448
|
-
|
|
449
|
-
if (!['node', c8].includes(options.binary)) {
|
|
450
|
-
if (['jsc', 'hermes'].includes(options.binary)) {
|
|
451
|
-
const entropy = randomBytes(5 * 1024).toString('base64')
|
|
452
|
-
input.push(`globalThis.EXODUS_TEST_CRYPTO_ENTROPY = ${stringify(entropy)};`)
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
await importSource('../src/bundle-apis/globals.cjs')
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
if (options.jest) {
|
|
459
|
-
assert(jestConfig.rootDir)
|
|
460
|
-
const preload = [...(jestConfig.setupFiles || []), ...(jestConfig.setupFilesAfterEnv || [])]
|
|
461
|
-
if (jestConfig.testEnvironment && jestConfig.testEnvironment !== 'node') {
|
|
462
|
-
const { specialEnvironments } = await import('../src/jest.environment.js')
|
|
463
|
-
assert(Object.hasOwn(specialEnvironments, jestConfig.testEnvironment))
|
|
464
|
-
preload.push(...(specialEnvironments[jestConfig.testEnvironment].dependencies || []))
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const local = createRequire(resolve(jestConfig.rootDir, 'package.json'))
|
|
468
|
-
const w = (f) => `[${stringify(f)}, () => require(${stringify(local.resolve(f))})]`
|
|
469
|
-
input.push(`globalThis.EXODUS_TEST_PRELOADED = [${preload.map((f) => w(f)).join(', ')}]`)
|
|
470
|
-
await importSource('./jest.js')
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
for (const file of ifiles) importFile(file)
|
|
474
|
-
|
|
475
|
-
const filename =
|
|
476
|
-
ifiles.length === 1 ? `${ifiles[0]}-${randomUUID().slice(0, 8)}` : `bundle-${randomUUID()}`
|
|
477
|
-
const outfile = `${join(outdir, filename)}.js`
|
|
478
|
-
const EXODUS_TEST_SNAPSHOTS = await readSnapshots(ifiles)
|
|
479
|
-
const build = async (opts) => esbuild.build(opts).catch((err) => ({ errors: [err] }))
|
|
480
|
-
let main = input.join('\n')
|
|
481
|
-
if (['jsc', 'hermes'].includes(options.binary)) {
|
|
482
|
-
const exit = `EXODUS_TEST_PROCESS.exitCode = 1; EXODUS_TEST_PROCESS._maybeProcessExitCode();`
|
|
483
|
-
main = `try {\n${main}\n} catch (err) { print(err); ${exit} }`
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const fsfiles = await getPackageFiles()
|
|
487
|
-
|
|
488
|
-
const res = await build({
|
|
489
|
-
stdin: {
|
|
490
|
-
contents: `(async function () {\n${main}\n})()`,
|
|
491
|
-
resolveDir: bindir,
|
|
492
|
-
},
|
|
493
|
-
bundle: true,
|
|
494
|
-
outdir,
|
|
495
|
-
entryNames: filename,
|
|
496
|
-
platform: 'neutral',
|
|
497
|
-
mainFields: ['browser', 'module', 'main'],
|
|
498
|
-
define: {
|
|
499
|
-
'process.env.FORCE_COLOR': stringify('0'),
|
|
500
|
-
'process.env.NO_COLOR': stringify('1'),
|
|
501
|
-
'process.env.NODE_ENV': stringify(process.env.NODE_ENV),
|
|
502
|
-
'process.env.EXODUS_TEST_CONTEXT': stringify('pure'),
|
|
503
|
-
'process.env.EXODUS_TEST_ENVIRONMENT': stringify('bundle'),
|
|
504
|
-
'process.env.EXODUS_TEST_PLATFORM': stringify(process.env.EXODUS_TEST_PLATFORM),
|
|
505
|
-
'process.env.EXODUS_TEST_JEST_CONFIG': stringify(JSON.stringify(jestConfig)),
|
|
506
|
-
'process.env.EXODUS_TEST_EXECARGV': stringify(process.env.EXODUS_TEST_EXECARGV),
|
|
507
|
-
'process.env.NODE_DEBUG': stringify(),
|
|
508
|
-
'process.env.DEBUG': stringify(),
|
|
509
|
-
'process.env.READABLE_STREAM': stringify(),
|
|
510
|
-
'process.env.CI': stringify(process.env.CI),
|
|
511
|
-
'process.env.CI_ENABLE_VERBOSE_LOGS': stringify(process.env.CI_ENABLE_VERBOSE_LOGS),
|
|
512
|
-
'process.browser': stringify(true),
|
|
513
|
-
'process.emitWarning': 'undefined',
|
|
514
|
-
'process.stderr': 'undefined',
|
|
515
|
-
'process.stdout': 'undefined',
|
|
516
|
-
'process.type': 'undefined',
|
|
517
|
-
'process.version': stringify('v22.5.1'), // shouldn't depend on currently used Node.js version
|
|
518
|
-
'process.versions.node': stringify('22.5.1'), // see line above
|
|
519
|
-
EXODUS_TEST_FILES: stringify(ifiles.map((f) => [dirname(f), basename(f)])),
|
|
520
|
-
EXODUS_TEST_SNAPSHOTS: stringify(EXODUS_TEST_SNAPSHOTS),
|
|
521
|
-
EXODUS_TEST_FSFILES: stringify(fsfiles.map((file) => resolve(file))), // TODO: can we safely use relative paths?
|
|
522
|
-
},
|
|
523
|
-
alias: {
|
|
524
|
-
// Node browserify
|
|
525
|
-
'node:assert': resolveRequire('assert'),
|
|
526
|
-
'node:assert/strict': resolveRequire('../src/bundle-apis/assert-strict.cjs'),
|
|
527
|
-
'node:fs': resolveRequire('../src/bundle-apis/fs.cjs'),
|
|
528
|
-
'node:fs/promises': resolveRequire('../src/bundle-apis/fs-promises.cjs'),
|
|
529
|
-
fs: resolveRequire('../src/bundle-apis/fs.cjs'),
|
|
530
|
-
'fs/promises': resolveRequire('../src/bundle-apis/fs-promises.cjs'),
|
|
531
|
-
assert: resolveRequire('assert'),
|
|
532
|
-
buffer: resolveRequire('buffer'),
|
|
533
|
-
child_process: resolveRequire('../src/bundle-apis/child_process.cjs'),
|
|
534
|
-
constants: resolveRequire('constants-browserify'),
|
|
535
|
-
crypto: resolveRequire('../src/bundle-apis/crypto.cjs'),
|
|
536
|
-
events: resolveRequire('events'),
|
|
537
|
-
http: resolveRequire('../src/bundle-apis/http.cjs'),
|
|
538
|
-
https: resolveRequire('../src/bundle-apis/https.cjs'),
|
|
539
|
-
os: resolveRequire('os-browserify'),
|
|
540
|
-
path: resolveRequire('path-browserify'),
|
|
541
|
-
querystring: resolveRequire('querystring-es3'),
|
|
542
|
-
stream: resolveRequire('stream-browserify'),
|
|
543
|
-
url: resolveRequire('url'),
|
|
544
|
-
util: resolveRequire('util'),
|
|
545
|
-
// expect-related deps
|
|
546
|
-
'ansi-styles': resolveRequire('../src/bundle-apis/ansi-styles.cjs'),
|
|
547
|
-
'jest-util': resolveRequire('../src/bundle-apis/jest-util.js'),
|
|
548
|
-
'jest-message-util': resolveRequire('../src/bundle-apis/jest-message-util.js'),
|
|
549
|
-
// unwanted deps
|
|
550
|
-
bindings: resolveRequire('../src/bundle-apis/empty/function-throw.cjs'),
|
|
551
|
-
'node-gyp-build': resolveRequire('../src/bundle-apis/empty/function-throw.cjs'),
|
|
552
|
-
ws: resolveRequire('../src/bundle-apis/ws.cjs'),
|
|
553
|
-
// unsupported deps
|
|
554
|
-
...Object.fromEntries(
|
|
555
|
-
blockedDeps.map((n) => [n, resolveRequire('../src/bundle-apis/empty/module-throw.cjs')])
|
|
556
|
-
),
|
|
557
|
-
},
|
|
558
|
-
sourcemap: writePipeline.length > 0 ? 'inline' : 'linked',
|
|
559
|
-
sourcesContent: false,
|
|
560
|
-
keepNames: true,
|
|
561
|
-
format: 'iife',
|
|
562
|
-
target: options.target || `node${process.versions.node}`,
|
|
563
|
-
supported: {
|
|
564
|
-
bigint: true,
|
|
565
|
-
},
|
|
566
|
-
plugins: [
|
|
567
|
-
{
|
|
568
|
-
name: 'exodus-test.bundle',
|
|
569
|
-
setup({ onLoad }) {
|
|
570
|
-
onLoad({ filter: /\.m?js$/, namespace: 'file' }, async (args) => {
|
|
571
|
-
let filepath = args.path
|
|
572
|
-
// Resolve .native versions
|
|
573
|
-
// TODO: move flag to engine options
|
|
574
|
-
// TODO: maybe follow package.json for this
|
|
575
|
-
if (['jsc', 'hermes'].includes(options.binary)) {
|
|
576
|
-
const maybeNative = filepath.replace(/(\.[cm]?js)$/u, '.native$1')
|
|
577
|
-
if (existsSync(maybeNative)) filepath = maybeNative
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
let contents = await readFile(filepath, 'utf8')
|
|
581
|
-
for (const transform of loadPipeline) contents = await transform(contents, args)
|
|
582
|
-
return { contents }
|
|
583
|
-
})
|
|
584
|
-
},
|
|
585
|
-
},
|
|
586
|
-
],
|
|
587
|
-
})
|
|
588
369
|
|
|
589
|
-
|
|
590
|
-
let contents = await readFile(outfile, 'utf8')
|
|
591
|
-
for (const transform of writePipeline) contents = await transform(contents)
|
|
592
|
-
await writeFile(outfile, contents)
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// require('fs').copyFileSync(outfile, 'tempout.cjs') // DEBUG
|
|
596
|
-
return { file: outfile, errors: res.errors, warnings: res.warnings }
|
|
597
|
-
}
|
|
370
|
+
if (options.binary === 'node') args.unshift('--enable-source-maps') // FIXME
|
|
598
371
|
|
|
599
|
-
|
|
372
|
+
const bundle = await import('./bundle.js')
|
|
373
|
+
await bundle.init({ ...options, outdir, jestConfig })
|
|
374
|
+
buildFile = (file) => bundle.build(file)
|
|
600
375
|
}
|
|
601
376
|
|
|
602
|
-
assert.equal(inputs.length, files.length)
|
|
603
377
|
assert(options.binary && ['node', 'bun', 'deno', 'jsc', 'hermes', c8].includes(options.binary))
|
|
604
378
|
|
|
605
379
|
if (options.pure) {
|
|
@@ -630,30 +404,95 @@ if (options.pure) {
|
|
|
630
404
|
|
|
631
405
|
process.env.EXODUS_TEST_CONTEXT = 'pure'
|
|
632
406
|
console.warn(`\n${options.engine} engine is experimental and may not work an expected\n`)
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
407
|
+
|
|
408
|
+
const execFile = promisify(execFileCallback)
|
|
409
|
+
|
|
410
|
+
const runOne = async (inputFile) => {
|
|
411
|
+
const bundled = buildFile ? await buildFile(inputFile) : undefined
|
|
412
|
+
if (buildFile) assert(bundled.file)
|
|
413
|
+
const file = buildFile ? bundled.file : inputFile
|
|
414
|
+
if (bundled?.errors.length > 0) return { ok: false, output: bundled.errors }
|
|
639
415
|
|
|
640
416
|
const { binaryArgs = [] } = options
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
const
|
|
644
|
-
|
|
417
|
+
// 5 MiB just in case, timeout is fallback if timeout in script hangs, 50x as it can be adjusted per-script inside them
|
|
418
|
+
// Do we want to extract timeouts from script code instead? Also, hermes might be slower, so makes sense to increase
|
|
419
|
+
const execOpts = { maxBuffer: 5 * 1024 * 1024, timeout: (jestConfig?.testTimeout || 5000) * 50 }
|
|
420
|
+
try {
|
|
421
|
+
const fullArgs = [...binaryArgs, ...args, file]
|
|
422
|
+
const { code = 0, stdout, stderr } = await execFile(options.binary, fullArgs, execOpts)
|
|
423
|
+
return { ok: code === 0, output: [stdout, stderr] }
|
|
424
|
+
} catch (err) {
|
|
425
|
+
const { code, stdout = '', stderr = '', signal, killed } = err
|
|
426
|
+
if (code === null) {
|
|
427
|
+
assert(signal)
|
|
428
|
+
const message = ` ${signal}${killed ? ' (killed)' : ''}`
|
|
429
|
+
const comment = killed && signal === 'SIGTERM' ? ' Most likely due to timeout reached' : ''
|
|
430
|
+
return { ok: false, output: [stdout, stderr, message, comment] }
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
assert(Number.isInteger(code) && code > 0)
|
|
434
|
+
return { ok: false, output: [stdout, stderr] }
|
|
435
|
+
} finally {
|
|
436
|
+
if (bundled) await unlink(bundled.file)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const queue = new Queue(availableParallelism() - 1)
|
|
441
|
+
const runConcurrent = async (file) => {
|
|
442
|
+
await queue.claim()
|
|
443
|
+
try {
|
|
444
|
+
// need to await here
|
|
445
|
+
return await runOne(file)
|
|
446
|
+
} finally {
|
|
447
|
+
queue.release()
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const haveColors = process.stdout.hasColors?.()
|
|
452
|
+
const colors = new Map(Object.entries(inspect.colors))
|
|
453
|
+
const color = (text, color) => {
|
|
454
|
+
if (!haveColors || text === '') return text
|
|
455
|
+
if (!colors.has(color)) throw new Error(`Unknown color: ${color}`)
|
|
456
|
+
const [start, end] = colors.get(color)
|
|
457
|
+
return `\x1B[${start}m${text}\x1B[${end}m`
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const format = (chunk) => {
|
|
461
|
+
if (!haveColors) return chunk
|
|
462
|
+
return chunk
|
|
463
|
+
.replaceAll(/^✔ PASS /gmu, color('✔ PASS ', 'green'))
|
|
464
|
+
.replaceAll(/^⏭ SKIP /gmu, color('⏭ SKIP ', 'dim'))
|
|
465
|
+
.replaceAll(/^✖ FAIL /gmu, color('✖ FAIL ', 'red'))
|
|
466
|
+
.replaceAll(/^‼ FATAL /gmu, `${color('‼', 'red')} ${color(' FATAL ', 'bgRed')} `)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const failures = []
|
|
470
|
+
const tasks = files.map((file) => ({ file, task: runConcurrent(file) }))
|
|
471
|
+
const timeString = color('Total time', 'dim')
|
|
472
|
+
console.time(timeString)
|
|
473
|
+
for (const { file, task } of tasks) {
|
|
474
|
+
console.log(color(`# ${file}`, 'bold'))
|
|
475
|
+
const { ok, output } = await task
|
|
476
|
+
for (const chunk of output.map((x) => x.trimEnd()).filter(Boolean)) console.log(format(chunk))
|
|
477
|
+
if (!ok) failures.push(file)
|
|
645
478
|
}
|
|
646
479
|
|
|
647
480
|
if (failures.length > 0) {
|
|
648
481
|
process.exitCode = 1
|
|
649
482
|
const [total, passed, failed] = [files.length, files.length - failures.length, failures.length]
|
|
650
|
-
|
|
651
|
-
|
|
483
|
+
const failLine = color(`${failed} / ${total}`, 'red')
|
|
484
|
+
const passLine = color(`${passed} / ${total}`, 'green')
|
|
485
|
+
const suffix = passed > 0 ? color(` (passed: ${passLine})`, 'dim') : ''
|
|
486
|
+
console.log(`${color('Test suites failed:', 'bold')} ${failLine}${suffix}`)
|
|
487
|
+
console.log(color('Failed test suites:', 'red'))
|
|
652
488
|
for (const file of failures) console.log(` ${file}`) // joining with \n can get truncated, too big
|
|
653
489
|
} else {
|
|
654
|
-
console.log(`All ${files.length} test suites passed
|
|
490
|
+
console.log(color(`All ${files.length} test suites passed`, 'green'))
|
|
655
491
|
}
|
|
492
|
+
|
|
493
|
+
console.timeEnd(timeString)
|
|
656
494
|
} else {
|
|
495
|
+
assert(!buildFile)
|
|
657
496
|
assert(['node', c8].includes(options.binary), `Unexpected native engine: ${options.binary}`)
|
|
658
497
|
assert(['node:test'].includes(options.engine))
|
|
659
498
|
process.env.EXODUS_TEST_CONTEXT = options.engine
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/test",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.34",
|
|
4
4
|
"author": "Exodus Movement, Inc.",
|
|
5
5
|
"description": "A test suite runner",
|
|
6
6
|
"homepage": "https://github.com/ExodusMovement/test",
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
"prettier": "@exodus/prettier",
|
|
37
37
|
"files": [
|
|
38
38
|
"bin/babel.cjs",
|
|
39
|
+
"bin/babel-worker.cjs",
|
|
40
|
+
"bin/bundle.js",
|
|
39
41
|
"bin/jest.js",
|
|
40
42
|
"bin/typescript.js",
|
|
41
43
|
"bin/typescript.loader.js",
|
|
@@ -52,6 +54,7 @@
|
|
|
52
54
|
"src/bundle-apis/globals.cjs",
|
|
53
55
|
"src/bundle-apis/jest-message-util.js",
|
|
54
56
|
"src/bundle-apis/jest-util.js",
|
|
57
|
+
"src/bundle-apis/node-buffer.cjs",
|
|
55
58
|
"src/bundle-apis/util-format.cjs",
|
|
56
59
|
"src/bundle-apis/ws.cjs",
|
|
57
60
|
"src/dark.cjs",
|
|
@@ -69,6 +72,7 @@
|
|
|
69
72
|
"src/jest.timers.js",
|
|
70
73
|
"src/node.js",
|
|
71
74
|
"src/tape.js",
|
|
75
|
+
"src/tape.cjs",
|
|
72
76
|
"src/version.js",
|
|
73
77
|
"!__tests__",
|
|
74
78
|
"CHANGELOG.md"
|
|
@@ -88,15 +92,17 @@
|
|
|
88
92
|
},
|
|
89
93
|
"dependencies": {
|
|
90
94
|
"@babel/core": "^7.0.0",
|
|
91
|
-
"@babel/plugin-
|
|
92
|
-
"@babel/plugin-transform-async-generator-functions": "^7.0.0",
|
|
95
|
+
"@babel/plugin-syntax-typescript": "^7.0.0",
|
|
93
96
|
"@babel/plugin-transform-block-scoping": "^7.0.0",
|
|
94
97
|
"@babel/plugin-transform-class-properties": "^7.0.0",
|
|
95
98
|
"@babel/plugin-transform-classes": "^7.0.0",
|
|
99
|
+
"@babel/plugin-transform-private-methods": "^7.0.0",
|
|
96
100
|
"@babel/register": "^7.0.0",
|
|
101
|
+
"@chalker/queue": "^1.0.0",
|
|
97
102
|
"@ungap/url-search-params": "^0.2.2",
|
|
98
103
|
"amaro": "^0.0.4",
|
|
99
104
|
"assert": "^2.1.0",
|
|
105
|
+
"browserify-zlib": "^0.2.0",
|
|
100
106
|
"buffer": "^6.0.3",
|
|
101
107
|
"c8": "^9.1.0",
|
|
102
108
|
"constants-browserify": "^1.0.0",
|
|
@@ -113,6 +119,7 @@
|
|
|
113
119
|
"querystring-es3": "^0.2.1",
|
|
114
120
|
"stream-browserify": "^3.0.0",
|
|
115
121
|
"text-encoding": "^0.7.0",
|
|
122
|
+
"timers-browserify": "^2.0.12",
|
|
116
123
|
"tsx": "^4.16.2",
|
|
117
124
|
"url": "^0.11.0",
|
|
118
125
|
"util": "^0.12.5"
|
|
@@ -2,4 +2,4 @@ const cb = require('crypto-browserify')
|
|
|
2
2
|
const webcrypto = globalThis.crypto
|
|
3
3
|
const randomUUID = () => webcrypto.randomUUID()
|
|
4
4
|
const getRandomValues = (array) => webcrypto.getRandomValues(array)
|
|
5
|
-
module.exports = { ...cb, webcrypto, subtle: webcrypto
|
|
5
|
+
module.exports = { ...cb, webcrypto, subtle: webcrypto?.subtle, randomUUID, getRandomValues }
|
package/src/bundle-apis/fs.cjs
CHANGED
|
@@ -81,4 +81,8 @@ const existsSync = (file) => {
|
|
|
81
81
|
err('existsSync', file)
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
const readFileSync = (file /*, options */) => {
|
|
85
|
+
err('readFileSync', file)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { ...stubs, existsSync, readFileSync, promises, constants, F_OK, R_OK, W_OK, X_OK }
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// We expect bundler to optimize out EXODUS_TEST_PLATFORM blocks
|
|
2
|
+
/* eslint-disable sonarjs/no-collapsible-if, unicorn/no-lonely-if */
|
|
3
|
+
|
|
1
4
|
if (!globalThis.global) globalThis.global = globalThis
|
|
2
5
|
if (!globalThis.Buffer) globalThis.Buffer = require('buffer').Buffer
|
|
3
6
|
if (!globalThis.console) {
|
|
@@ -24,6 +27,19 @@ if (!Array.prototype.at) {
|
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
if (process.env.EXODUS_TEST_PLATFORM === 'hermes') {
|
|
31
|
+
// Fixed after 0.12, not present in 0.12
|
|
32
|
+
// Refs: https://github.com/facebook/hermes/commit/e8fa81328dd630e39975e6d16ac3e6f47f4cba06
|
|
33
|
+
if (!Promise.allSettled) {
|
|
34
|
+
const wrap = (element) =>
|
|
35
|
+
Promise.resolve(element).then(
|
|
36
|
+
(value) => ({ status: 'fulfilled', value }),
|
|
37
|
+
(reason) => ({ status: 'rejected', reason })
|
|
38
|
+
)
|
|
39
|
+
Promise.allSettled = (iterable) => Promise.all([...iterable].map((element) => wrap(element)))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
if (globalThis.describe) delete globalThis.describe
|
|
28
44
|
|
|
29
45
|
if (typeof process === 'undefined') {
|
|
@@ -48,7 +64,9 @@ if (typeof process === 'undefined') {
|
|
|
48
64
|
if (globalThis.Deno) return // has native exitCode support
|
|
49
65
|
if (process._exitCode !== 0) {
|
|
50
66
|
setTimeout(() => {
|
|
51
|
-
|
|
67
|
+
const err = new Error('Test failed')
|
|
68
|
+
err.stack = ''
|
|
69
|
+
throw err
|
|
52
70
|
}, 0)
|
|
53
71
|
}
|
|
54
72
|
},
|
|
@@ -57,6 +75,24 @@ if (typeof process === 'undefined') {
|
|
|
57
75
|
globalThis.EXODUS_TEST_PROCESS = process
|
|
58
76
|
}
|
|
59
77
|
|
|
78
|
+
if (process.env.EXODUS_TEST_PLATFORM === 'hermes') {
|
|
79
|
+
let headerLogged = false
|
|
80
|
+
globalThis.HermesInternal?.enablePromiseRejectionTracker({
|
|
81
|
+
allRejections: true,
|
|
82
|
+
onUnhandled: (i, err) => {
|
|
83
|
+
globalThis.EXODUS_TEST_PROCESS.exitCode = 1
|
|
84
|
+
if (!headerLogged) {
|
|
85
|
+
console.log(`‼ FATAL Tests generated asynchronous activity after they ended.
|
|
86
|
+
This activity created errors and would have caused tests to fail, but instead triggered unhandledRejection events`)
|
|
87
|
+
headerLogged = true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(`Uncaught error #${i}: ${err}`)
|
|
91
|
+
globalThis.EXODUS_TEST_PROCESS._maybeProcessExitCode()
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
60
96
|
if (
|
|
61
97
|
process.env.EXODUS_TEST_PLATFORM === 'hermes' ||
|
|
62
98
|
(process.env.EXODUS_TEST_PLATFORM === 'jsc' && !globalThis.clearTimeout)
|
package/src/engine.pure.cjs
CHANGED
|
@@ -69,21 +69,21 @@ async function runContext(context) {
|
|
|
69
69
|
|
|
70
70
|
// TODO: try/catch for hooks?
|
|
71
71
|
for (const c of stack) for (const hook of c.hooks.beforeEach) await runFunction(hook, context)
|
|
72
|
+
const guard = { id: null, failed: false }
|
|
73
|
+
guard.promise = new Promise((resolve) => {
|
|
74
|
+
guard.id = setTimeout(() => {
|
|
75
|
+
guard.failed = true
|
|
76
|
+
resolve()
|
|
77
|
+
}, options.timeout || 5000)
|
|
78
|
+
})
|
|
72
79
|
try {
|
|
73
|
-
const guard = { id: null, failed: false }
|
|
74
|
-
guard.promise = new Promise((resolve) => {
|
|
75
|
-
guard.id = setTimeout(() => {
|
|
76
|
-
guard.failed = true
|
|
77
|
-
resolve()
|
|
78
|
-
}, options.timeout || 5000)
|
|
79
|
-
})
|
|
80
80
|
await Promise.race([guard.promise, runFunction(fn, context)])
|
|
81
|
-
clearTimeout(guard.id)
|
|
82
81
|
if (guard.failed) throw new Error('timeout reached')
|
|
83
82
|
} catch (e) {
|
|
84
83
|
error = e ?? 'Unknown error'
|
|
85
84
|
}
|
|
86
85
|
|
|
86
|
+
clearTimeout(guard.id)
|
|
87
87
|
stack.reverse()
|
|
88
88
|
for (const c of stack) for (const hook of c.hooks.afterEach) await runFunction(hook, context)
|
|
89
89
|
|
|
@@ -109,17 +109,29 @@ async function run() {
|
|
|
109
109
|
assert(context === context.root)
|
|
110
110
|
await runContext(context).catch((error) => {
|
|
111
111
|
// Should not throw under regular circumstances
|
|
112
|
-
console.log('
|
|
112
|
+
console.log('‼ FATAL', error)
|
|
113
113
|
abstractProcess.exitCode = 1
|
|
114
114
|
})
|
|
115
115
|
abstractProcess._maybeProcessExitCode?.()
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
function describe(...args) {
|
|
118
|
+
async function describe(...args) {
|
|
119
119
|
const { name, options, fn } = parseArgs(args)
|
|
120
120
|
enterContext(name, options)
|
|
121
121
|
context.options = options
|
|
122
|
-
|
|
122
|
+
// todo: callback support?
|
|
123
|
+
if (!options.skip) {
|
|
124
|
+
try {
|
|
125
|
+
const res = fn(context)
|
|
126
|
+
// we don't need to be async if fn is sync
|
|
127
|
+
if (isPromise(res)) await res
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.log('✖ FAIL', context.fullName)
|
|
130
|
+
console.log(' describe() body threw an error:', error)
|
|
131
|
+
abstractProcess.exitCode = 1
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
123
135
|
exitContext()
|
|
124
136
|
}
|
|
125
137
|
|
|
@@ -216,7 +228,7 @@ class MockTimers {
|
|
|
216
228
|
}
|
|
217
229
|
|
|
218
230
|
runAll() {
|
|
219
|
-
this.tick(Math.max(0, ...this.#queue.map((x) => x.at)))
|
|
231
|
+
this.tick(Math.max(0, ...this.#queue.map((x) => x.at - this.#elapsed)))
|
|
220
232
|
}
|
|
221
233
|
|
|
222
234
|
setTime(milliseconds) {
|
|
@@ -224,15 +236,21 @@ class MockTimers {
|
|
|
224
236
|
}
|
|
225
237
|
|
|
226
238
|
#setTimeout(fn, delay, ...args) {
|
|
227
|
-
|
|
239
|
+
const id = { type: 'timeout', fn, at: delay + this.#elapsed, args }
|
|
240
|
+
this.#queue.push(id)
|
|
241
|
+
return id
|
|
228
242
|
}
|
|
229
243
|
|
|
230
244
|
#setInterval(fn, delay, ...args) {
|
|
231
|
-
|
|
245
|
+
const id = { type: 'interval', fn, at: delay + this.#elapsed, interval: delay, args }
|
|
246
|
+
this.#queue.push(id)
|
|
247
|
+
return id
|
|
232
248
|
}
|
|
233
249
|
|
|
234
250
|
#setImmediate(fn, ...args) {
|
|
235
|
-
|
|
251
|
+
const id = { type: 'immediate', fn, args }
|
|
252
|
+
this.#queue.push(id)
|
|
253
|
+
return id
|
|
236
254
|
}
|
|
237
255
|
|
|
238
256
|
#clearTimeout(id) {
|
|
@@ -248,50 +266,12 @@ class MockTimers {
|
|
|
248
266
|
}
|
|
249
267
|
}
|
|
250
268
|
|
|
251
|
-
class MockedFunction extends Function {
|
|
252
|
-
get mock() {
|
|
253
|
-
return this._mock
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
269
|
const mock = {
|
|
258
270
|
module: undefined,
|
|
259
271
|
timers: new MockTimers(),
|
|
260
272
|
fn: (original = () => {}, implementation = original) => {
|
|
261
273
|
let impl = implementation
|
|
262
|
-
const
|
|
263
|
-
const call = {
|
|
264
|
-
arguments: args,
|
|
265
|
-
// eslint-disable-next-line unicorn/error-message
|
|
266
|
-
stack: new Error(), // todo: recheck location
|
|
267
|
-
target: undefined,
|
|
268
|
-
this: this,
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
mocked.mock.calls.push(call)
|
|
272
|
-
|
|
273
|
-
try {
|
|
274
|
-
// todo: what's if it a promise
|
|
275
|
-
if (this instanceof mocked) {
|
|
276
|
-
impl.apply(this, args)
|
|
277
|
-
call.result = call.target = this
|
|
278
|
-
} else {
|
|
279
|
-
call.result = impl.apply(this, args)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
call.error = undefined
|
|
283
|
-
} catch (err) {
|
|
284
|
-
call.result = undefined
|
|
285
|
-
call.error = err
|
|
286
|
-
throw err
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return call.result
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
Object.setPrototypeOf(mocked, MockedFunction.prototype)
|
|
293
|
-
|
|
294
|
-
mocked._mock = {
|
|
274
|
+
const _mock = {
|
|
295
275
|
calls: [],
|
|
296
276
|
get callCount() {
|
|
297
277
|
return this.calls.length
|
|
@@ -307,14 +287,64 @@ const mock = {
|
|
|
307
287
|
}
|
|
308
288
|
},
|
|
309
289
|
resetCalls: () => {
|
|
310
|
-
|
|
290
|
+
_mock.calls.length = 0
|
|
311
291
|
},
|
|
312
292
|
restore: () => {
|
|
313
293
|
impl = original
|
|
314
294
|
},
|
|
315
295
|
}
|
|
316
296
|
|
|
317
|
-
return
|
|
297
|
+
return new Proxy(function () {}, {
|
|
298
|
+
__proto__: null,
|
|
299
|
+
apply(fn, _this, args) {
|
|
300
|
+
// eslint-disable-next-line unicorn/error-message
|
|
301
|
+
const call = { arguments: args, stack: new Error(), target: undefined, this: _this } // todo: recheck .stack location
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
call.result = impl.apply(_this, args)
|
|
305
|
+
call.error = undefined
|
|
306
|
+
} catch (err) {
|
|
307
|
+
call.result = undefined
|
|
308
|
+
call.error = err
|
|
309
|
+
throw err
|
|
310
|
+
} finally {
|
|
311
|
+
_mock.calls.push(call)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return call.result
|
|
315
|
+
},
|
|
316
|
+
construct(target, args) {
|
|
317
|
+
// eslint-disable-next-line unicorn/error-message
|
|
318
|
+
const call = { arguments: args, stack: new Error(), target } // todo: recheck .stack location
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
call.result = call.this = new impl(...args) // eslint-disable-line new-cap
|
|
322
|
+
call.error = undefined
|
|
323
|
+
} catch (err) {
|
|
324
|
+
call.result = undefined
|
|
325
|
+
call.error = err
|
|
326
|
+
throw err
|
|
327
|
+
} finally {
|
|
328
|
+
_mock.calls.push(call)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return call.result
|
|
332
|
+
},
|
|
333
|
+
get: (fn, key) => {
|
|
334
|
+
if (key === 'mock') return _mock
|
|
335
|
+
const target = key !== 'prototype' && key in fn ? fn : impl
|
|
336
|
+
return target[key]
|
|
337
|
+
},
|
|
338
|
+
set: (fn, key, value) => {
|
|
339
|
+
const target = key !== 'prototype' && key in fn ? fn : impl
|
|
340
|
+
target[key] = value
|
|
341
|
+
return true
|
|
342
|
+
},
|
|
343
|
+
getOwnPropertyDescriptor(fn, key) {
|
|
344
|
+
const target = key !== 'prototype' && key in fn ? fn : impl
|
|
345
|
+
return Object.getOwnPropertyDescriptor(target, key)
|
|
346
|
+
},
|
|
347
|
+
})
|
|
318
348
|
},
|
|
319
349
|
}
|
|
320
350
|
|
package/src/jest.config.js
CHANGED
|
@@ -64,11 +64,11 @@ export const jestConfig = () => {
|
|
|
64
64
|
|
|
65
65
|
export async function loadJestConfig(...args) {
|
|
66
66
|
let rawConfig
|
|
67
|
-
if (process.env.EXODUS_TEST_JEST_CONFIG) {
|
|
68
|
-
rawConfig = JSON.parse(process.env.EXODUS_TEST_JEST_CONFIG)
|
|
69
|
-
} else {
|
|
67
|
+
if (process.env.EXODUS_TEST_JEST_CONFIG === undefined) {
|
|
70
68
|
const { readJestConfig } = await import('./jest.config.fs.js')
|
|
71
69
|
rawConfig = await readJestConfig(...args)
|
|
70
|
+
} else {
|
|
71
|
+
rawConfig = JSON.parse(process.env.EXODUS_TEST_JEST_CONFIG)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
config = normalizeJestConfig(rawConfig)
|
package/src/jest.environment.js
CHANGED
|
@@ -32,12 +32,16 @@ export const specialEnvironments = {
|
|
|
32
32
|
dependencies: ['@pollyjs/core', 'setup-polly-jest', 'setup-polly-jest/lib/common'],
|
|
33
33
|
setup: async (require, engine) => {
|
|
34
34
|
const { getTestNamePath } = await import('./dark.cjs')
|
|
35
|
+
// polly has bad defer impl in case if it finds MessageChannel but not process.* (e.g. on deno), forever blocking
|
|
36
|
+
const { MessageChannel } = globalThis
|
|
37
|
+
if (MessageChannel) globalThis.MessageChannel = undefined
|
|
35
38
|
const { Polly } = require('@pollyjs/core')
|
|
36
39
|
const pollyJest = require('setup-polly-jest')
|
|
37
40
|
const {
|
|
38
41
|
JestPollyGlobals,
|
|
39
42
|
createPollyContextAccessor,
|
|
40
43
|
} = require('setup-polly-jest/lib/common')
|
|
44
|
+
if (MessageChannel) globalThis.MessageChannel = MessageChannel
|
|
41
45
|
const pollyGlobals = new JestPollyGlobals(globalThis)
|
|
42
46
|
pollyGlobals.isJestPollyEnvironment = true
|
|
43
47
|
pollyJest.setupPolly = (options) => {
|
package/src/jest.js
CHANGED
|
@@ -94,7 +94,7 @@ const describe = (...args) => {
|
|
|
94
94
|
inDescribe.push(fn)
|
|
95
95
|
const optionsConcurrent = args?.at(-1)?.concurrency > 1
|
|
96
96
|
if (optionsConcurrent) inConcurrent.push(fn)
|
|
97
|
-
const result = node.describe(...args,
|
|
97
|
+
const result = node.describe(...args, () => {
|
|
98
98
|
const res = fn()
|
|
99
99
|
|
|
100
100
|
// We do only block-level concurrency, not file-level
|
package/src/jest.snapshot.js
CHANGED
|
@@ -96,6 +96,20 @@ const snapInline = (obj, inline) => {
|
|
|
96
96
|
getAssert().strictEqual(serialize(obj).trim(), inline.trim())
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
const deepMerge = (obj, matcher) => {
|
|
100
|
+
if (!obj || !matcher) return matcher
|
|
101
|
+
const proto = Object.getPrototypeOf(obj)
|
|
102
|
+
if (![Object.prototype, null].includes(proto)) return matcher
|
|
103
|
+
const protoM = Object.getPrototypeOf(matcher)
|
|
104
|
+
if (![Object.prototype, null].includes(protoM)) return matcher
|
|
105
|
+
// all matcher keys should be already in obj, as verified by toMatchObject prior to this
|
|
106
|
+
const map = new Map(Object.entries(matcher))
|
|
107
|
+
const merge = (key, value) => [key, map.has(key) ? deepMerge(value, map.get(key)) : value]
|
|
108
|
+
const res = Object.fromEntries(Object.entries(obj).map(([key, value]) => merge(key, value)))
|
|
109
|
+
Object.setPrototypeOf(res, proto)
|
|
110
|
+
return res
|
|
111
|
+
}
|
|
112
|
+
|
|
99
113
|
const snapOnDisk = (orig, matcher) => {
|
|
100
114
|
if (matcher) {
|
|
101
115
|
expect(orig).toMatchObject(matcher)
|
|
@@ -105,7 +119,7 @@ const snapOnDisk = (orig, matcher) => {
|
|
|
105
119
|
state.numPassingAsserts--
|
|
106
120
|
}
|
|
107
121
|
|
|
108
|
-
const obj = matcher ?
|
|
122
|
+
const obj = matcher ? deepMerge(orig, matcher) : orig
|
|
109
123
|
const escape = (str) => str.replaceAll(/([\\`])/gu, '\\$1')
|
|
110
124
|
|
|
111
125
|
maybeSetupJestSnapshots()
|
package/src/tape.cjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
let lib
|
|
2
|
+
|
|
3
|
+
const loadLib = async () => {
|
|
4
|
+
if (!lib) lib = await import('./tape.js')
|
|
5
|
+
return lib
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* eslint-disable unicorn/no-await-expression-member */
|
|
9
|
+
const test = async (...args) => (await loadLib()).test(...args)
|
|
10
|
+
test.skip = async (...args) => (await loadLib()).test.skip(...args)
|
|
11
|
+
test.only = async (...args) => (await loadLib()).test.only(...args)
|
|
12
|
+
test.test = test
|
|
13
|
+
/* eslint-enable unicorn/no-await-expression-member */
|
|
14
|
+
|
|
15
|
+
module.exports = test
|
package/src/tape.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assert, assertLoose, test } from './engine.js'
|
|
1
|
+
import { assert, assertLoose, test as nodeTest } from './engine.js'
|
|
2
2
|
import { createCallerLocationHook } from './dark.cjs'
|
|
3
3
|
import './version.js'
|
|
4
4
|
|
|
@@ -63,7 +63,7 @@ const aliases = {
|
|
|
63
63
|
doesNotThrow: ['doesNotThrow'],
|
|
64
64
|
fail: ['fail'],
|
|
65
65
|
rejects: ['rejects'],
|
|
66
|
-
doesNotReject: ['resolves'],
|
|
66
|
+
doesNotReject: ['doesNotReject', 'resolves'],
|
|
67
67
|
|
|
68
68
|
// specially handled ones as do not exist in t.assert / assert
|
|
69
69
|
notOk: ['notOk', 'false', 'notok'],
|
|
@@ -156,5 +156,5 @@ function tapeWrap(test) {
|
|
|
156
156
|
return tap
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
export const
|
|
160
|
-
export default
|
|
159
|
+
export const test = tapeWrap(nodeTest)
|
|
160
|
+
export default test
|