@exodus/test 1.0.0-rc.37 → 1.0.0-rc.38
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/bin/index.js +12 -2
- package/package.json +2 -2
- package/src/bundle-apis/globals.cjs +3 -2
- package/src/engine.js +2 -2
- package/src/engine.node.cjs +2 -1
- package/src/engine.pure.cjs +52 -23
- package/src/jest.config.js +5 -1
- package/src/jest.timers.js +14 -2
package/bin/index.js
CHANGED
|
@@ -21,6 +21,7 @@ const EXTS = `.?([cm])[jt]s?(x)` // we differ from jest, allowing [cm] before ev
|
|
|
21
21
|
const DEFAULT_PATTERNS = [`**/__tests__/**/*${EXTS}`, `**/?(*.)+(spec|test)${EXTS}`]
|
|
22
22
|
|
|
23
23
|
const bundleOptions = { pure: true, bundle: true, esbuild: true, ts: 'auto' }
|
|
24
|
+
const hermesAv = ['-Og', '-Xmicrotask-queue']
|
|
24
25
|
const ENGINES = new Map(
|
|
25
26
|
Object.entries({
|
|
26
27
|
'node:test': { binary: 'node', pure: false, hasImportLoader: true, ts: 'flag' },
|
|
@@ -30,7 +31,7 @@ const ENGINES = new Map(
|
|
|
30
31
|
'bun:bundle': { binary: 'bun', ...bundleOptions },
|
|
31
32
|
'deno:bundle': { binary: 'deno', binaryArgs: ['run'], target: 'deno1', ...bundleOptions },
|
|
32
33
|
'jsc:bundle': { binary: 'jsc', ...bundleOptions, target: 'safari13' },
|
|
33
|
-
'hermes:bundle': { binary: 'hermes', binaryArgs:
|
|
34
|
+
'hermes:bundle': { binary: 'hermes', binaryArgs: hermesAv, target: 'es2018', ...bundleOptions },
|
|
34
35
|
})
|
|
35
36
|
)
|
|
36
37
|
|
|
@@ -51,6 +52,7 @@ function parseOptions() {
|
|
|
51
52
|
dropNetwork: ![undefined, '', '0'].includes(process.env.EXODUS_TEST_DROP_NETWORK),
|
|
52
53
|
ideaCompat: false,
|
|
53
54
|
engine: process.env.EXODUS_TEST_ENGINE ?? 'node:test',
|
|
55
|
+
require: [],
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
const args = [...process.argv]
|
|
@@ -63,7 +65,7 @@ function parseOptions() {
|
|
|
63
65
|
const jsname = args.shift()
|
|
64
66
|
assert(basename(jsname) === 'exodus-test' || jsname === fileURLToPath(import.meta.url))
|
|
65
67
|
|
|
66
|
-
while (args[0]?.startsWith('
|
|
68
|
+
while (args[0]?.startsWith('-')) {
|
|
67
69
|
const option = args.shift()
|
|
68
70
|
if (options.ideaCompat) {
|
|
69
71
|
// Ignore some options IntelliJ IDEA is passing
|
|
@@ -96,6 +98,9 @@ function parseOptions() {
|
|
|
96
98
|
case '--babel':
|
|
97
99
|
options.babel = true
|
|
98
100
|
break
|
|
101
|
+
case '--require':
|
|
102
|
+
options.require.push(args.shift())
|
|
103
|
+
break
|
|
99
104
|
case '--coverage-engine':
|
|
100
105
|
options.coverageEngine = args.shift()
|
|
101
106
|
break
|
|
@@ -284,6 +289,11 @@ if (options.typescript) {
|
|
|
284
289
|
}
|
|
285
290
|
}
|
|
286
291
|
|
|
292
|
+
for (const r of options.require) {
|
|
293
|
+
assert(!options.bundle, 'Can not use -r with *:bundle engines')
|
|
294
|
+
args.push('-r', r)
|
|
295
|
+
}
|
|
296
|
+
|
|
287
297
|
if (patterns.length === 0) patterns.push(...DEFAULT_PATTERNS) // defaults
|
|
288
298
|
const globbed = await glob(patterns, { ignore })
|
|
289
299
|
const allfiles = filter ? globbed.filter(filter) : globbed
|
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.38",
|
|
4
4
|
"author": "Exodus Movement, Inc.",
|
|
5
5
|
"description": "A test suite runner",
|
|
6
6
|
"homepage": "https://github.com/ExodusMovement/test",
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"@babel/plugin-transform-private-methods": "^7.0.0",
|
|
105
105
|
"@babel/register": "^7.0.0",
|
|
106
106
|
"@chalker/queue": "^1.0.0",
|
|
107
|
-
"@exodus/replay": "1.0.0-rc.
|
|
107
|
+
"@exodus/replay": "1.0.0-rc.3",
|
|
108
108
|
"@ungap/url-search-params": "^0.2.2",
|
|
109
109
|
"amaro": "^0.0.5",
|
|
110
110
|
"assert": "^2.1.0",
|
|
@@ -76,18 +76,19 @@ if (typeof process === 'undefined') {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
if (process.env.EXODUS_TEST_PLATFORM === 'hermes') {
|
|
79
|
+
const print = console.log.bind(console) // we don not want overrides
|
|
79
80
|
let headerLogged = false
|
|
80
81
|
globalThis.HermesInternal?.enablePromiseRejectionTracker({
|
|
81
82
|
allRejections: true,
|
|
82
83
|
onUnhandled: (i, err) => {
|
|
83
84
|
globalThis.EXODUS_TEST_PROCESS.exitCode = 1
|
|
84
85
|
if (!headerLogged) {
|
|
85
|
-
|
|
86
|
+
print(`‼ FATAL Tests generated asynchronous activity after they ended.
|
|
86
87
|
This activity created errors and would have caused tests to fail, but instead triggered unhandledRejection events`)
|
|
87
88
|
headerLogged = true
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
print(`Uncaught error #${i}: ${err}`)
|
|
91
92
|
globalThis.EXODUS_TEST_PROCESS._maybeProcessExitCode()
|
|
92
93
|
},
|
|
93
94
|
})
|
package/src/engine.js
CHANGED
|
@@ -12,8 +12,8 @@ export { mock, describe, test, beforeEach, afterEach, before, after }
|
|
|
12
12
|
const { builtinModules, syncBuiltinESMExports } = engine
|
|
13
13
|
export { builtinModules, syncBuiltinESMExports }
|
|
14
14
|
|
|
15
|
-
const { utilFormat, isPromise, nodeVersion } = engine
|
|
16
|
-
export { utilFormat, isPromise, nodeVersion }
|
|
15
|
+
const { utilFormat, isPromise, nodeVersion, awaitForMicrotaskQueue } = engine
|
|
16
|
+
export { utilFormat, isPromise, nodeVersion, awaitForMicrotaskQueue }
|
|
17
17
|
|
|
18
18
|
const { requireIsRelative, relativeRequire, isTopLevelESM } = engine
|
|
19
19
|
export { requireIsRelative, relativeRequire, isTopLevelESM }
|
package/src/engine.node.cjs
CHANGED
|
@@ -10,6 +10,7 @@ const { mock, describe, test, beforeEach, afterEach, before, after } = nodeTest
|
|
|
10
10
|
|
|
11
11
|
const isPromise = types.isPromise
|
|
12
12
|
const nodeVersion = process.versions.node
|
|
13
|
+
const awaitForMicrotaskQueue = () => new Promise((resolve) => process.nextTick(resolve))
|
|
13
14
|
|
|
14
15
|
const files = process.argv.slice(1)
|
|
15
16
|
const baseFile = files.length === 1 && existsSync(files[0]) ? normalize(files[0]) : undefined
|
|
@@ -33,7 +34,7 @@ module.exports = {
|
|
|
33
34
|
...{ assert, assertLoose },
|
|
34
35
|
...{ mock, describe, test, beforeEach, afterEach, before, after },
|
|
35
36
|
...{ builtinModules, syncBuiltinESMExports },
|
|
36
|
-
...{ utilFormat, isPromise, nodeVersion },
|
|
37
|
+
...{ utilFormat, isPromise, nodeVersion, awaitForMicrotaskQueue },
|
|
37
38
|
...{ requireIsRelative, relativeRequire, isTopLevelESM },
|
|
38
39
|
...{ readSnapshot, setSnapshotSerializers, setSnapshotResolver },
|
|
39
40
|
}
|
package/src/engine.pure.cjs
CHANGED
|
@@ -4,6 +4,7 @@ const assertLoose = require('node:assert')
|
|
|
4
4
|
const { setTimeout, setInterval, setImmediate, Date } = globalThis
|
|
5
5
|
const { clearTimeout, clearInterval, clearImmediate } = globalThis
|
|
6
6
|
|
|
7
|
+
const print = console.log.bind(console) // we don not want overrides
|
|
7
8
|
Error.stackTraceLimit = 100
|
|
8
9
|
|
|
9
10
|
let context
|
|
@@ -72,12 +73,12 @@ async function runContext(context) {
|
|
|
72
73
|
// eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
|
|
73
74
|
context.running = true
|
|
74
75
|
assert(children.length === 0 || !fn)
|
|
75
|
-
if (options.skip) return
|
|
76
|
+
if (options.skip) return print('⏭ SKIP', context.fullName)
|
|
76
77
|
if (context.fn) {
|
|
77
78
|
if (runOnly) {
|
|
78
|
-
if (!context.only) return
|
|
79
|
+
if (!context.only) return print('⏭ SKIP', context.fullName)
|
|
79
80
|
} else if (options.only) {
|
|
80
|
-
|
|
81
|
+
print(`⚠ WARN test.only requires the --only command-line option`)
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
let error
|
|
@@ -104,23 +105,23 @@ async function runContext(context) {
|
|
|
104
105
|
stack.reverse()
|
|
105
106
|
for (const c of stack) for (const hook of c.hooks.afterEach) await runFunction(hook, context)
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
print(error === undefined ? '✔ PASS' : '✖ FAIL', context.fullName)
|
|
108
109
|
if (error) {
|
|
109
|
-
|
|
110
|
+
print(' ', error)
|
|
110
111
|
abstractProcess.exitCode = 1
|
|
111
112
|
}
|
|
112
113
|
} else {
|
|
113
114
|
if (options.only && !runOnly) {
|
|
114
|
-
|
|
115
|
+
print(`⚠ WARN describe.only requires the --only command-line option`)
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
// if (context !== context.root)
|
|
118
|
+
// if (context !== context.root) print(`▶ ${context.fullName}`)
|
|
118
119
|
// TODO: try/catch for hooks?
|
|
119
120
|
// TODO: flatten recursion before running?
|
|
120
121
|
for (const hook of hooks.before) await runFunction(hook, context)
|
|
121
122
|
for (const child of children) await runContext(child)
|
|
122
123
|
for (const hook of hooks.after) await runFunction(hook, context)
|
|
123
|
-
// if (context !== context.root)
|
|
124
|
+
// if (context !== context.root) print(`▶ ${context.fullName}`)
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
|
|
@@ -130,7 +131,7 @@ async function run() {
|
|
|
130
131
|
assert(context === context.root)
|
|
131
132
|
await runContext(context).catch((error) => {
|
|
132
133
|
// Should not throw under regular circumstances
|
|
133
|
-
|
|
134
|
+
print('‼ FATAL', error)
|
|
134
135
|
abstractProcess.exitCode = 1
|
|
135
136
|
})
|
|
136
137
|
abstractProcess._maybeProcessExitCode?.()
|
|
@@ -146,8 +147,8 @@ async function describe(...args) {
|
|
|
146
147
|
// we don't need to be async if fn is sync
|
|
147
148
|
if (isPromise(res)) await res
|
|
148
149
|
} catch (error) {
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
print('✖ FAIL', context.fullName)
|
|
151
|
+
print(' describe() body threw an error:', error)
|
|
151
152
|
abstractProcess.exitCode = 1
|
|
152
153
|
}
|
|
153
154
|
}
|
|
@@ -242,19 +243,31 @@ class MockTimers {
|
|
|
242
243
|
|
|
243
244
|
tick(milliseconds = 1) {
|
|
244
245
|
this.#elapsed += milliseconds
|
|
245
|
-
while (
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
246
|
+
while (this.#microtick() !== null);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async tickAsync(milliseconds = 1) {
|
|
250
|
+
let shouldAwait = true
|
|
251
|
+
for (let i = 0; i < milliseconds; i++) {
|
|
252
|
+
if (shouldAwait) await awaitForMicrotaskQueue()
|
|
253
|
+
this.#elapsed += 1
|
|
254
|
+
shouldAwait = this.#microtick() !== null
|
|
255
|
+
if (shouldAwait) while (this.#microtick() !== null);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
255
258
|
|
|
256
|
-
|
|
259
|
+
#microtick() {
|
|
260
|
+
const next =
|
|
261
|
+
this.#queue.find((x) => x.type === 'immediate') ||
|
|
262
|
+
this.#queue.find((x) => x.at <= this.#elapsed)
|
|
263
|
+
if (!next) return null
|
|
264
|
+
if (next.type === 'interval') {
|
|
265
|
+
next.at += next.interval
|
|
266
|
+
} else {
|
|
267
|
+
this.#queue = this.#queue.filter((x) => x !== next)
|
|
257
268
|
}
|
|
269
|
+
|
|
270
|
+
next.fn(...next.args)
|
|
258
271
|
}
|
|
259
272
|
|
|
260
273
|
runAll() {
|
|
@@ -385,6 +398,22 @@ const after = (fn) => context.hooks.after.push(fn)
|
|
|
385
398
|
|
|
386
399
|
const isPromise = (x) => Boolean(x && x.then && x.catch && x.finally)
|
|
387
400
|
const nodeVersion = '9999.99.99'
|
|
401
|
+
const awaitForMicrotaskQueue = async () => {
|
|
402
|
+
if (globalThis?.process?.nextTick) {
|
|
403
|
+
// We are in microtasks, awaiting for "next" tick will get us out of here
|
|
404
|
+
return new Promise((resolve) => globalThis.process.nextTick(resolve))
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// If that is not available, we can wait for the actual next cycle
|
|
408
|
+
// For Hermes, we use -Xmicrotask-queue for this to act not like just a Promise.resolve().then(
|
|
409
|
+
// TODO: recheck if setImmediate is not faked with setTimeout if we enable a polyfill for it for JSC?
|
|
410
|
+
if (setImmediate) return new Promise((resolve) => setImmediate(resolve))
|
|
411
|
+
|
|
412
|
+
// Do not rely on setTimeout here! it will tick actual time and is terribly slow (i.e. timers no longer fake)
|
|
413
|
+
// 100_000 should be enough to flush everything that's going on in the microtask queue
|
|
414
|
+
// Only JSC hits this currently
|
|
415
|
+
for (let i = 0; i < 100_000; i++) await Promise.resolve()
|
|
416
|
+
}
|
|
388
417
|
|
|
389
418
|
let builtinModules = []
|
|
390
419
|
let requireIsRelative = false
|
|
@@ -433,7 +462,7 @@ module.exports = {
|
|
|
433
462
|
...{ assert, assertLoose },
|
|
434
463
|
...{ mock, describe, test, beforeEach, afterEach, before, after },
|
|
435
464
|
...{ builtinModules, syncBuiltinESMExports },
|
|
436
|
-
...{ utilFormat, isPromise, nodeVersion },
|
|
465
|
+
...{ utilFormat, isPromise, nodeVersion, awaitForMicrotaskQueue },
|
|
437
466
|
...{ requireIsRelative, relativeRequire, isTopLevelESM },
|
|
438
467
|
...{ readSnapshot, setSnapshotSerializers, setSnapshotResolver },
|
|
439
468
|
}
|
package/src/jest.config.js
CHANGED
|
@@ -26,6 +26,10 @@ const normalizeJestConfig = (config) => ({
|
|
|
26
26
|
},
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
+
function checkModuleNameMapper(mapper) {
|
|
30
|
+
return !mapper || JSON.stringify(mapper) === '{"^(\\\\.{1,2}/.*)\\\\.js$":"$1"}' // extension stripping is fine
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
function verifyJestConfig(c) {
|
|
30
34
|
assert(!configUsed, 'Can not apply new config as the current one was already used')
|
|
31
35
|
|
|
@@ -37,7 +41,7 @@ function verifyJestConfig(c) {
|
|
|
37
41
|
assert.deepEqual(environmentOptions, {}, 'Jest config.testEnvironmentOptions is not supported')
|
|
38
42
|
|
|
39
43
|
assert(!c.automock, 'Automocking all modules is not currently supported (config.automock)')
|
|
40
|
-
assert(
|
|
44
|
+
assert(checkModuleNameMapper(c.moduleNameMapper), 'Jest config.moduleNameMapper is not supported')
|
|
41
45
|
if (c.moduleDirectories) {
|
|
42
46
|
const valid = ['node_modules']
|
|
43
47
|
assert.deepEqual(c.moduleDirectories, valid, 'Jest config.moduleDirectories is not supported')
|
package/src/jest.timers.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mock, assert } from './engine.js'
|
|
1
|
+
import { mock, assert, awaitForMicrotaskQueue } from './engine.js'
|
|
2
2
|
import { jestConfig } from './jest.config.js'
|
|
3
3
|
import { haveValidTimers, haveNoTimerInfiniteLoopBug } from './version.js'
|
|
4
4
|
|
|
@@ -65,7 +65,19 @@ export function advanceTimersByTime(time) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
export async function advanceTimersByTimeAsync(time) {
|
|
68
|
-
|
|
68
|
+
assertHaveTimers()
|
|
69
|
+
warnOldTimers()
|
|
70
|
+
|
|
71
|
+
if (mock.timers.tickAsync) {
|
|
72
|
+
await mock.timers.tickAsync(time)
|
|
73
|
+
} else {
|
|
74
|
+
for (let i = 0; i < time; i++) {
|
|
75
|
+
await awaitForMicrotaskQueue()
|
|
76
|
+
mock.timers.tick(1)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return this
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
export function setSystemTime(time) {
|