@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 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
- - `d8:bundle` — [v8 CLI](https://v8.dev/docs/d8) (Chrome/Blink/Node.js JavaScript engine)
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)
@@ -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', pure: false, loader: '--import', ts: 'flag', haveIsOk: true },
27
- 'node:pure': { binary: 'node', pure: true, loader: '--import', ts: 'flag', haveIsOk: true },
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', pure: false, loader: '--import', ts: 'flag' },
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', pure: false, loader: '--preload', ts: 'auto' },
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
- 'd8:bundle': { binary: 'd8', ...bareboneOpts },
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 * availableParallelism()) / 100))
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 === 'electron') {
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, 'v8']) // v8 is an alias to d8
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 || availableParallelism() - 1)
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.101",
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:d8": "EXODUS_TEST_ENGINE=d8:bundle npm run test:_bundle --",
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": "^35.2.2",
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",
@@ -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
+ }