@exodus/test 1.0.0-rc.102 → 1.0.0-rc.104

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) => {
@@ -272,6 +274,7 @@ const { options, patterns } = parseOptions()
272
274
 
273
275
  const engineName = `${options.engine} engine` // used for warnings to user
274
276
  const engineFlagError = (flag) => `${engineName} does not support --${flag}`
277
+ if (options.engine === 'd8:bundle') options.engine = 'v8:bundle' // compat
275
278
  const engineOptions = ENGINES.get(options.engine)
276
279
  assert(engineOptions, `Unknown engine: ${options.engine}`)
277
280
  Object.assign(options, engineOptions)
@@ -331,7 +334,6 @@ if (options.pure) {
331
334
 
332
335
  args.push('--expose-internals') // this is unoptimal and hopefully temporary, see rationale in src/dark.cjs
333
336
  } else if (options.engine === 'deno:test') {
334
- args.push('test', '--allow-all')
335
337
  assert(!options.jest, 'deno:test engine does not support --jest yet')
336
338
  } else if (options.engine === 'bun:test') {
337
339
  args.push('test')
@@ -340,6 +342,8 @@ if (options.pure) {
340
342
  throw new Error('Unreachable')
341
343
  }
342
344
 
345
+ if (!options.bundle && ['node', 'electron'].includes(options.platform)) args.push('--expose-gc') // for benchmarks
346
+
343
347
  const ignore = ['**/node_modules']
344
348
  let filter
345
349
  if (process.env.EXODUS_TEST_IGNORE) {
@@ -421,13 +425,16 @@ if (options.jest) {
421
425
  }
422
426
  }
423
427
 
428
+ const cpus = availableParallelism()
429
+ // increase from default cpus - 1 on default GH CI runners, but not on browsers which already use other processes
430
+ if (!options.concurrency && isCI && !isBrowserLike && cpus === 2) options.concurrency = cpus
424
431
  if (options.concurrency) {
425
432
  const raw = options.concurrency
426
433
  let concurrency = raw
427
434
  if (typeof raw === 'string') {
428
435
  if (/^\d{1,15}%$/u.test(raw)) {
429
436
  const perc = Number(raw.slice(0, -1))
430
- concurrency = Math.max(1, Math.round((perc * availableParallelism()) / 100))
437
+ concurrency = Math.max(1, Math.round((perc * cpus) / 100))
431
438
  } else {
432
439
  assert(/^\d{1,15}$/u.test(raw), `Wrong concurrency: ${raw}`)
433
440
  concurrency = Number(raw)
@@ -591,7 +598,7 @@ if (options.binary === 'electron') {
591
598
  }
592
599
  }
593
600
 
594
- if (options.barebone || options.binary === 'electron') {
601
+ if (options.barebone || ['electron', 'workerd'].includes(options.binary)) {
595
602
  options.binary = findBinary(options.binary)
596
603
  options.binaryCanBeAbsolute = true
597
604
  }
@@ -648,7 +655,7 @@ async function launch(binary, args, opts = {}, buffering = false) {
648
655
  }
649
656
 
650
657
  const barebones = [...barebonesOk, ...barebonesUnhandled]
651
- assertBinary(binary, ['node', 'bun', 'deno', 'electron', ...barebones, 'v8']) // v8 is an alias to d8
658
+ assertBinary(binary, ['node', 'bun', 'deno', 'electron', 'workerd', ...barebones])
652
659
  if (binary === c8 && process.platform === 'win32') {
653
660
  ;[binary, args] = ['node', [binary, ...args]]
654
661
  }
@@ -690,13 +697,12 @@ if (options.pure) {
690
697
 
691
698
  const failedBare = 'EXODUS_TEST_FAILED_EXIT_CODE_1'
692
699
  const cleanOut = (out) => out.replaceAll(`\n${failedBare}\n`, '\n').replaceAll(failedBare, '')
693
- const { binaryArgs = [] } = options
694
700
  // Timeout is fallback if timeout in script hangs, 50x as it can be adjusted per-script inside them
695
701
  // Do we want to extract timeouts from script code instead? Also, hermes might be slower, so makes sense to increase
696
702
  const timeout = (options.testTimeout || jestConfig?.testTimeout || 5000) * 50
697
703
  const start = process.hrtime.bigint()
698
704
  try {
699
- const fullArgs = [...binaryArgs, ...args, file]
705
+ const fullArgs = [...(options.binaryArgs ?? []), ...args, file]
700
706
  const { code = 0, stdout, stderr } = await launch(options.binary, fullArgs, { timeout }, true)
701
707
  const ms = Number(process.hrtime.bigint() - start) / 1e6
702
708
  if (stdout.includes(failedBare)) return { ok: false, output: [cleanOut(stdout), stderr], ms }
@@ -722,7 +728,7 @@ if (options.pure) {
722
728
  }
723
729
 
724
730
  const { Queue } = await import('@chalker/queue')
725
- const queue = new Queue(options.concurrency || availableParallelism() - 1)
731
+ const queue = new Queue(options.concurrency || cpus - 1)
726
732
  const runConcurrent = async (file) => {
727
733
  await queue.claim()
728
734
  try {
@@ -758,7 +764,6 @@ if (options.pure) {
758
764
  assert(['node:test', 'electron-as-node:test', 'deno:test', 'bun:test'].includes(options.engine))
759
765
  setEnv('EXODUS_TEST_CONTEXT', 'node:test') // The context is always node:test in this branch
760
766
  assert(files.length > 0) // otherwise we can run recursively
761
- assert(!options.binaryArgs)
762
767
  if (options.concurrency) args.push('--test-concurrency', options.concurrency)
763
768
  if (['--inspect', '--inspect-brk', '--inspect-wait'].includes(options.devtools)) {
764
769
  args.push(options.devtools)
@@ -770,7 +775,7 @@ if (options.pure) {
770
775
  )
771
776
  }
772
777
 
773
- const { code } = await launch(options.binary, [...args, ...files])
778
+ const { code } = await launch(options.binary, [...(options.binaryArgs ?? []), ...args, ...files])
774
779
  process.exitCode = code
775
780
  }
776
781
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/test",
3
- "version": "1.0.0-rc.102",
3
+ "version": "1.0.0-rc.104",
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
+ }