@exodus/test 1.0.0-rc.101 → 1.0.0-rc.103
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/find-binary.js +2 -0
- package/bin/index.js +27 -18
- package/package.json +7 -5
- package/src/benchmark.js +54 -0
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ Use `--engine` (or `EXODUS_TEST_ENGINE=`) to specify one of:
|
|
|
67
67
|
- `brave:puppeteer` — Brave
|
|
68
68
|
- `msedge:puppeteer` — Microsoft Edge
|
|
69
69
|
- Barebone engines (system-provided or installed with `npx jsvu` / `npx esvu`):
|
|
70
|
-
- `
|
|
70
|
+
- `v8:bundle` — [v8 CLI](https://v8.dev/docs/d8) (Chrome/Blink/Node.js JavaScript engine)
|
|
71
71
|
- `jsc:bundle` — [JavaScriptCore](https://docs.webkit.org/Deep%20Dive/JSC/JavaScriptCore.html) (Safari/WebKit JavaScript engine)
|
|
72
72
|
- `hermes:bundle` — [Hermes](https://hermesengine.dev) (React Native JavaScript engine)
|
|
73
73
|
- `spidermonkey:bundle` — [SpiderMonkey](https://spidermonkey.dev/) (Firefox/Gecko JavaScript engine)
|
package/bin/find-binary.js
CHANGED
|
@@ -65,6 +65,8 @@ function findBinaryOnce(name) {
|
|
|
65
65
|
return findFile([jsvu, esvu], false)
|
|
66
66
|
case 'electron':
|
|
67
67
|
return require('electron')
|
|
68
|
+
case 'workerd':
|
|
69
|
+
return require.resolve('workerd/bin/workerd')
|
|
68
70
|
case 'c8':
|
|
69
71
|
return require.resolve('c8/bin/c8.js')
|
|
70
72
|
case 'chrome':
|
package/bin/index.js
CHANGED
|
@@ -21,23 +21,25 @@ const bundleOpts = { pure: true, bundle: true, esbuild: true, ts: 'auto' }
|
|
|
21
21
|
const bareboneOpts = { ...bundleOpts, barebone: true }
|
|
22
22
|
const hermesA = ['-Og', '-Xmicrotask-queue']
|
|
23
23
|
const denoA = ['run', '--allow-all'] // also will set DENO_COMPAT=1 env flag below
|
|
24
|
+
const denoT = ['test', '--allow-all']
|
|
25
|
+
const nodeTS = process.features.typescript ? 'auto' : 'flag'
|
|
24
26
|
const ENGINES = new Map(
|
|
25
27
|
Object.entries({
|
|
26
|
-
'node:test': { binary: 'node',
|
|
27
|
-
'node:pure': { binary: 'node', pure: true, loader: '--import', ts:
|
|
28
|
-
'node:bundle': { binary: 'node', ...bundleOpts },
|
|
28
|
+
'node:test': { binary: 'node', loader: '--import', ts: nodeTS, haveIsOk: true },
|
|
29
|
+
'node:pure': { binary: 'node', pure: true, loader: '--import', ts: nodeTS, haveIsOk: true },
|
|
30
|
+
'node:bundle': { binary: 'node', binaryArgs: ['--expose-gc'], ...bundleOpts },
|
|
29
31
|
'bun:test': { binary: 'bun', ts: 'auto' },
|
|
30
32
|
'bun:pure': { binary: 'bun', pure: true, ts: 'auto' },
|
|
31
33
|
'bun:bundle': { binary: 'bun', ...bundleOpts },
|
|
32
|
-
'electron-as-node:test': { binary: 'electron',
|
|
34
|
+
'electron-as-node:test': { binary: 'electron', loader: '--import', ts: 'flag' },
|
|
33
35
|
'electron-as-node:pure': { binary: 'electron', pure: true, loader: '--import', ts: 'flag' },
|
|
34
|
-
'electron-as-node:bundle': { binary: 'electron', ...bundleOpts },
|
|
36
|
+
'electron-as-node:bundle': { binary: 'electron', binaryArgs: ['--expose-gc'], ...bundleOpts },
|
|
35
37
|
'electron:bundle': { binary: 'electron', electron: true, ...bundleOpts },
|
|
36
|
-
'deno:test': { binary: 'deno',
|
|
38
|
+
'deno:test': { binary: 'deno', binaryArgs: denoT, loader: '--preload', ts: 'auto' },
|
|
37
39
|
'deno:pure': { binary: 'deno', binaryArgs: denoA, pure: true, loader: '--preload', ts: 'auto' },
|
|
38
40
|
'deno:bundle': { binary: 'deno', binaryArgs: ['run'], target: 'deno1', ...bundleOpts },
|
|
39
41
|
// Barebone engines
|
|
40
|
-
'
|
|
42
|
+
'v8:bundle': { binary: 'd8', binaryArgs: ['--expose-gc'], ...bareboneOpts },
|
|
41
43
|
'jsc:bundle': { binary: 'jsc', target: 'safari13', ...bareboneOpts },
|
|
42
44
|
'hermes:bundle': { binary: 'hermes', binaryArgs: hermesA, target: 'es2018', ...bareboneOpts },
|
|
43
45
|
'spidermonkey:bundle': { binary: 'spidermonkey', ...bareboneOpts },
|
|
@@ -59,7 +61,7 @@ const ENGINES = new Map(
|
|
|
59
61
|
'msedge:playwright': { binary: 'msedge', browsers: 'playwright', ...bundleOpts },
|
|
60
62
|
})
|
|
61
63
|
)
|
|
62
|
-
const barebonesOk = ['d8', 'spidermonkey', 'quickjs', 'xs', 'hermes']
|
|
64
|
+
const barebonesOk = ['v8', 'd8', 'spidermonkey', 'quickjs', 'xs', 'hermes']
|
|
63
65
|
const barebonesUnhandled = ['jsc', 'escargot', 'boa', 'graaljs', 'engine262']
|
|
64
66
|
|
|
65
67
|
const getEnvFlag = (name) => {
|
|
@@ -97,6 +99,7 @@ function parseOptions() {
|
|
|
97
99
|
require: [],
|
|
98
100
|
testNamePattern: [],
|
|
99
101
|
testTimeout: undefined,
|
|
102
|
+
reporter: undefined,
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
const args = [...process.argv]
|
|
@@ -239,6 +242,10 @@ function parseOptions() {
|
|
|
239
242
|
case '--testTimeout':
|
|
240
243
|
options.testTimeout = Number(args.shift())
|
|
241
244
|
break
|
|
245
|
+
case '--reporter':
|
|
246
|
+
case '--test-reporter':
|
|
247
|
+
options.reporter = String(args.shift())
|
|
248
|
+
break
|
|
242
249
|
default:
|
|
243
250
|
throw new Error(`Unknown option: ${option}`)
|
|
244
251
|
}
|
|
@@ -267,6 +274,7 @@ const { options, patterns } = parseOptions()
|
|
|
267
274
|
|
|
268
275
|
const engineName = `${options.engine} engine` // used for warnings to user
|
|
269
276
|
const engineFlagError = (flag) => `${engineName} does not support --${flag}`
|
|
277
|
+
if (options.engine === 'd8:bundle') options.engine = 'v8:bundle' // compat
|
|
270
278
|
const engineOptions = ENGINES.get(options.engine)
|
|
271
279
|
assert(engineOptions, `Unknown engine: ${options.engine}`)
|
|
272
280
|
Object.assign(options, engineOptions)
|
|
@@ -304,7 +312,7 @@ if (options.pure) {
|
|
|
304
312
|
assert(options.testNamePattern.length === 0, '--test-name-pattern requires node:test engine now')
|
|
305
313
|
// eslint-disable-next-line unicorn/prefer-switch
|
|
306
314
|
} else if (options.engine === 'node:test' || options.engine === 'electron-as-node:test') {
|
|
307
|
-
const reporter = import.meta.resolve('./reporter.js')
|
|
315
|
+
const reporter = options.reporter ?? import.meta.resolve('./reporter.js')
|
|
308
316
|
args.push('--test', '--no-warnings=ExperimentalWarning', '--test-reporter', reporter)
|
|
309
317
|
|
|
310
318
|
if (have.haveSnapshots && engineOptions.haveIsOk) args.push('--experimental-test-snapshots')
|
|
@@ -326,7 +334,6 @@ if (options.pure) {
|
|
|
326
334
|
|
|
327
335
|
args.push('--expose-internals') // this is unoptimal and hopefully temporary, see rationale in src/dark.cjs
|
|
328
336
|
} else if (options.engine === 'deno:test') {
|
|
329
|
-
args.push('test', '--allow-all')
|
|
330
337
|
assert(!options.jest, 'deno:test engine does not support --jest yet')
|
|
331
338
|
} else if (options.engine === 'bun:test') {
|
|
332
339
|
args.push('test')
|
|
@@ -335,6 +342,8 @@ if (options.pure) {
|
|
|
335
342
|
throw new Error('Unreachable')
|
|
336
343
|
}
|
|
337
344
|
|
|
345
|
+
if (!options.bundle && ['node', 'electron'].includes(options.platform)) args.push('--expose-gc') // for benchmarks
|
|
346
|
+
|
|
338
347
|
const ignore = ['**/node_modules']
|
|
339
348
|
let filter
|
|
340
349
|
if (process.env.EXODUS_TEST_IGNORE) {
|
|
@@ -416,13 +425,15 @@ if (options.jest) {
|
|
|
416
425
|
}
|
|
417
426
|
}
|
|
418
427
|
|
|
428
|
+
const cpus = availableParallelism()
|
|
429
|
+
if (!options.concurrency && isCI && cpus === 2) options.concurrency = cpus // increase from default cpus - 1 on default GH CI runners
|
|
419
430
|
if (options.concurrency) {
|
|
420
431
|
const raw = options.concurrency
|
|
421
432
|
let concurrency = raw
|
|
422
433
|
if (typeof raw === 'string') {
|
|
423
434
|
if (/^\d{1,15}%$/u.test(raw)) {
|
|
424
435
|
const perc = Number(raw.slice(0, -1))
|
|
425
|
-
concurrency = Math.max(1, Math.round((perc *
|
|
436
|
+
concurrency = Math.max(1, Math.round((perc * cpus) / 100))
|
|
426
437
|
} else {
|
|
427
438
|
assert(/^\d{1,15}$/u.test(raw), `Wrong concurrency: ${raw}`)
|
|
428
439
|
concurrency = Number(raw)
|
|
@@ -586,7 +597,7 @@ if (options.binary === 'electron') {
|
|
|
586
597
|
}
|
|
587
598
|
}
|
|
588
599
|
|
|
589
|
-
if (options.barebone || options.binary
|
|
600
|
+
if (options.barebone || ['electron', 'workerd'].includes(options.binary)) {
|
|
590
601
|
options.binary = findBinary(options.binary)
|
|
591
602
|
options.binaryCanBeAbsolute = true
|
|
592
603
|
}
|
|
@@ -643,7 +654,7 @@ async function launch(binary, args, opts = {}, buffering = false) {
|
|
|
643
654
|
}
|
|
644
655
|
|
|
645
656
|
const barebones = [...barebonesOk, ...barebonesUnhandled]
|
|
646
|
-
assertBinary(binary, ['node', 'bun', 'deno', 'electron', ...barebones
|
|
657
|
+
assertBinary(binary, ['node', 'bun', 'deno', 'electron', 'workerd', ...barebones])
|
|
647
658
|
if (binary === c8 && process.platform === 'win32') {
|
|
648
659
|
;[binary, args] = ['node', [binary, ...args]]
|
|
649
660
|
}
|
|
@@ -685,13 +696,12 @@ if (options.pure) {
|
|
|
685
696
|
|
|
686
697
|
const failedBare = 'EXODUS_TEST_FAILED_EXIT_CODE_1'
|
|
687
698
|
const cleanOut = (out) => out.replaceAll(`\n${failedBare}\n`, '\n').replaceAll(failedBare, '')
|
|
688
|
-
const { binaryArgs = [] } = options
|
|
689
699
|
// Timeout is fallback if timeout in script hangs, 50x as it can be adjusted per-script inside them
|
|
690
700
|
// Do we want to extract timeouts from script code instead? Also, hermes might be slower, so makes sense to increase
|
|
691
701
|
const timeout = (options.testTimeout || jestConfig?.testTimeout || 5000) * 50
|
|
692
702
|
const start = process.hrtime.bigint()
|
|
693
703
|
try {
|
|
694
|
-
const fullArgs = [...binaryArgs, ...args, file]
|
|
704
|
+
const fullArgs = [...(options.binaryArgs ?? []), ...args, file]
|
|
695
705
|
const { code = 0, stdout, stderr } = await launch(options.binary, fullArgs, { timeout }, true)
|
|
696
706
|
const ms = Number(process.hrtime.bigint() - start) / 1e6
|
|
697
707
|
if (stdout.includes(failedBare)) return { ok: false, output: [cleanOut(stdout), stderr], ms }
|
|
@@ -717,7 +727,7 @@ if (options.pure) {
|
|
|
717
727
|
}
|
|
718
728
|
|
|
719
729
|
const { Queue } = await import('@chalker/queue')
|
|
720
|
-
const queue = new Queue(options.concurrency ||
|
|
730
|
+
const queue = new Queue(options.concurrency || cpus - 1)
|
|
721
731
|
const runConcurrent = async (file) => {
|
|
722
732
|
await queue.claim()
|
|
723
733
|
try {
|
|
@@ -753,7 +763,6 @@ if (options.pure) {
|
|
|
753
763
|
assert(['node:test', 'electron-as-node:test', 'deno:test', 'bun:test'].includes(options.engine))
|
|
754
764
|
setEnv('EXODUS_TEST_CONTEXT', 'node:test') // The context is always node:test in this branch
|
|
755
765
|
assert(files.length > 0) // otherwise we can run recursively
|
|
756
|
-
assert(!options.binaryArgs)
|
|
757
766
|
if (options.concurrency) args.push('--test-concurrency', options.concurrency)
|
|
758
767
|
if (['--inspect', '--inspect-brk', '--inspect-wait'].includes(options.devtools)) {
|
|
759
768
|
args.push(options.devtools)
|
|
@@ -765,7 +774,7 @@ if (options.pure) {
|
|
|
765
774
|
)
|
|
766
775
|
}
|
|
767
776
|
|
|
768
|
-
const { code } = await launch(options.binary, [...args, ...files])
|
|
777
|
+
const { code } = await launch(options.binary, [...(options.binaryArgs ?? []), ...args, ...files])
|
|
769
778
|
process.exitCode = code
|
|
770
779
|
}
|
|
771
780
|
|
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.103",
|
|
4
4
|
"author": "Exodus Movement, Inc.",
|
|
5
5
|
"description": "A test suite runner",
|
|
6
6
|
"homepage": "https://github.com/ExodusMovement/test",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"exports": {
|
|
29
29
|
"./node-test-reporter": "./bin/reporter.js",
|
|
30
30
|
"./loader/jest": "./loader/jest.js",
|
|
31
|
+
"./benchmark": "./src/benchmark.js",
|
|
31
32
|
"./expect": "./src/expect.cjs",
|
|
32
33
|
"./jest": "./src/jest.js",
|
|
33
34
|
"./node": "./src/node.js",
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
"loader/node-test.js",
|
|
55
56
|
"loader/typescript.js",
|
|
56
57
|
"loader/typescript.loader.js",
|
|
58
|
+
"src/benchmark.js",
|
|
57
59
|
"src/dark.cjs",
|
|
58
60
|
"src/engine.js",
|
|
59
61
|
"src/engine.node.cjs",
|
|
@@ -112,9 +114,8 @@
|
|
|
112
114
|
"test:chromium:playwright": "EXODUS_TEST_ENGINE=chromium:playwright npm run test:_bundle --",
|
|
113
115
|
"test:firefox:playwright": "EXODUS_TEST_ENGINE=firefox:playwright npm run test:_bundle --",
|
|
114
116
|
"test:webkit:playwright": "EXODUS_TEST_ENGINE=webkit:playwright npm run test:_bundle --",
|
|
115
|
-
"test:v8": "npm run test:d8 --",
|
|
116
117
|
"test:javascriptcore": "npm run test:jsc --",
|
|
117
|
-
"test:
|
|
118
|
+
"test:v8": "EXODUS_TEST_ENGINE=v8:bundle npm run test:_bundle --",
|
|
118
119
|
"test:jsc": "EXODUS_TEST_ENGINE=jsc:bundle npm run test:_bundle --",
|
|
119
120
|
"test:hermes": "EXODUS_TEST_ENGINE=hermes:bundle npm run test:_bundle --",
|
|
120
121
|
"test:spidermonkey": "EXODUS_TEST_ENGINE=spidermonkey:bundle npm run test:_bundle --",
|
|
@@ -154,7 +155,7 @@
|
|
|
154
155
|
"@types/jest-when": "^3.5.2",
|
|
155
156
|
"@types/node": "^24.0.11",
|
|
156
157
|
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
|
157
|
-
"electron": "^
|
|
158
|
+
"electron": "^37.3.1",
|
|
158
159
|
"eslint": "^8.44.0",
|
|
159
160
|
"esvu": "^1.2.16",
|
|
160
161
|
"jest": "^29.7.0",
|
|
@@ -163,7 +164,8 @@
|
|
|
163
164
|
"jest-when": "^3.6.0",
|
|
164
165
|
"jsdom": "^26.1.0",
|
|
165
166
|
"jsvu": "^3.0.0",
|
|
166
|
-
"prettier": "^3.0.3"
|
|
167
|
+
"prettier": "^3.0.3",
|
|
168
|
+
"workerd": "^1.20250826.0"
|
|
167
169
|
},
|
|
168
170
|
"peerDependencies": {
|
|
169
171
|
"@babel/register": "^7.0.0",
|
package/src/benchmark.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const fRps = (rps) => (rps > 10 ? Math.round(rps).toLocaleString() : rps.toPrecision(2))
|
|
2
|
+
const fTime = (ns) => {
|
|
3
|
+
const us = ns / 10n ** 3n
|
|
4
|
+
if (us < 2n) return `${ns}ns`
|
|
5
|
+
const ms = us / 10n ** 3n
|
|
6
|
+
if (ms < 2n) return `${us}μs`
|
|
7
|
+
const s = ms / 10n ** 3n
|
|
8
|
+
if (s < 10n) return `${ms}ms`
|
|
9
|
+
const min = s / 60n
|
|
10
|
+
return min < 5n ? `${s}s` : `${min}min`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const getTime = (() => {
|
|
14
|
+
if (globalThis.process) return () => process.hrtime.bigint()
|
|
15
|
+
if (globalThis.performance) return () => BigInt(Math.round(performance.now() * 1e6))
|
|
16
|
+
return () => BigInt(Math.round(Date.now() * 1e6))
|
|
17
|
+
})()
|
|
18
|
+
|
|
19
|
+
let gcWarned = false
|
|
20
|
+
export async function benchmark(name, options, fn) {
|
|
21
|
+
if (typeof options === 'function') [fn, options] = [options, undefined]
|
|
22
|
+
if (options?.skip) return
|
|
23
|
+
const { args, timeout = 1000 } = options ?? {}
|
|
24
|
+
|
|
25
|
+
if (globalThis.gc) {
|
|
26
|
+
for (let i = 0; i < 4; i++) globalThis.gc()
|
|
27
|
+
} else if (!gcWarned) {
|
|
28
|
+
gcWarned = true
|
|
29
|
+
console.log('Warning: no gc() available\n')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let min, max
|
|
33
|
+
let total = 0n
|
|
34
|
+
let count = 0
|
|
35
|
+
while (true) {
|
|
36
|
+
const arg = args ? args[count % args.length] : count
|
|
37
|
+
count++
|
|
38
|
+
const start = getTime()
|
|
39
|
+
const val = fn(arg)
|
|
40
|
+
if (val instanceof Promise) await val
|
|
41
|
+
const stop = getTime()
|
|
42
|
+
const diff = stop - start
|
|
43
|
+
total += diff
|
|
44
|
+
if (min === undefined || min > diff) min = diff
|
|
45
|
+
if (max === undefined || max < diff) max = diff
|
|
46
|
+
if (total >= BigInt(timeout)) break
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const mean = total / BigInt(count)
|
|
50
|
+
const rps = 1e9 / Number(mean)
|
|
51
|
+
console.log(`${name} x ${fRps(rps)} ops/sec @ ${fTime(mean)}/op (${fTime(min)}..${fTime(max)})`)
|
|
52
|
+
|
|
53
|
+
if (globalThis.gc) for (let i = 0; i < 4; i++) globalThis.gc()
|
|
54
|
+
}
|