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

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
@@ -15,7 +15,7 @@ It can run your existing tests on [all runtimes and also browsers](#engines), wi
15
15
  Chrome, Firefox, WebKit, Brave, Microsoft Edge,
16
16
  [QuickJS](https://github.com/quickjs-ng/quickjs), [XS](https://github.com/Moddable-OpenSource/moddable-xst),
17
17
  [GraalJS](https://github.com/oracle/graaljs), [Escargot](https://github.com/Samsung/escargot),
18
- and even [engine262](https://github.com/engine262/engine262).
18
+ [Boa](https://github.com/boa-dev/boa), and even [engine262](https://github.com/engine262/engine262).
19
19
  - Testsuite-agnostic — can run any file as long as it sets exit code based on test results
20
20
  - Built-in [Jest](https://jestjs.io) compatibility (with `--jest`), including `jest.*` global
21
21
  - Up to ~10x faster depending on the original setup
@@ -45,8 +45,10 @@ Use `--engine` (or `EXODUS_TEST_ENGINE=`) to specify one of:
45
45
  - `node:pure` — implementation in pure JS, runs on Node.js
46
46
  - `node:bundle` — same as `node:pure`, but bundles everything into a single file before launching
47
47
  - Other runtimes:
48
- - `bun:pure` / `bun:bundle` Bun, expects `bun` to be available
48
+ - `deno:pure` Deno (requires Deno v2.4.0 or later, expects `deno` to be available)
49
49
  - `deno:bundle` — Deno (v1 or v2, whichever `deno` is)
50
+ - `deno:test` — incomplete, lacks `--jest` support due to missing `afterEach` / `beforeEach` in Deno
51
+ - `bun:pure` / `bun:bundle` — Bun, expects `bun` to be available
50
52
  - `electron-as-node:test` / `electron-as-node:pure` / `electron-as-node:bundle`\
51
53
  Same as `node:*`, but uses `electron` binary.\
52
54
  The usecase is mostly to test on BoringSSL instead of OpenSSL.
@@ -73,6 +75,7 @@ Use `--engine` (or `EXODUS_TEST_ENGINE=`) to specify one of:
73
75
  - `xs:bundle` — [XS](https://github.com/Moddable-OpenSource/moddable-xst)
74
76
  - `graaljs:bundle` — [GraalJS](https://github.com/oracle/graaljs)
75
77
  - `escargot:bundle` — [Escargot](https://github.com/Samsung/escargot)
78
+ - `boa:bundle` — [Boa](https://github.com/boa-dev/boa)
76
79
  - `engine262:bundle` - [engine262](https://github.com/engine262/engine262), the per-spec implementation of ECMA-262
77
80
  (install with [esvu](https://npmjs.com/package/esvu))
78
81
 
@@ -57,6 +57,7 @@ function findBinaryOnce(name) {
57
57
  case 'quickjs':
58
58
  case 'graaljs':
59
59
  case 'escargot':
60
+ case 'boa': // not present in jsvu, esvu lacks aarch64-apple-darwin platform (as it's only in nightly)
60
61
  case 'ladybird-js': // naming by esvu
61
62
  case 'engine262':
62
63
  return findFile([jsvu, esvu])
package/bin/index.js CHANGED
@@ -19,29 +19,34 @@ import { glob as globImplementation } from '../src/glob.cjs'
19
19
  const DEFAULT_PATTERNS = [`**/?(*.)+(spec|test).?([cm])[jt]s?(x)`] // do not trust magic dirs by default
20
20
  const bundleOpts = { pure: true, bundle: true, esbuild: true, ts: 'auto' }
21
21
  const bareboneOpts = { ...bundleOpts, barebone: true }
22
- const hermesAv = ['-Og', '-Xmicrotask-queue']
22
+ const hermesA = ['-Og', '-Xmicrotask-queue']
23
+ const denoA = ['run', '--allow-all'] // also will set DENO_COMPAT=1 env flag below
23
24
  const ENGINES = new Map(
24
25
  Object.entries({
25
- 'node:test': { binary: 'node', pure: false, hasImportLoader: true, ts: 'flag', haveIsOk: true },
26
- 'node:pure': { binary: 'node', pure: true, hasImportLoader: true, ts: 'flag', haveIsOk: true },
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 },
27
28
  'node:bundle': { binary: 'node', ...bundleOpts },
28
- 'bun:pure': { binary: 'bun', pure: true, hasImportLoader: false, ts: 'auto' },
29
+ 'bun:test': { binary: 'bun', ts: 'auto' },
30
+ 'bun:pure': { binary: 'bun', pure: true, ts: 'auto' },
29
31
  'bun:bundle': { binary: 'bun', ...bundleOpts },
30
- 'electron-as-node:test': { binary: 'electron', pure: false, hasImportLoader: true, ts: 'flag' },
31
- 'electron-as-node:pure': { binary: 'electron', pure: true, hasImportLoader: true, ts: 'flag' },
32
+ 'electron-as-node:test': { binary: 'electron', pure: false, loader: '--import', ts: 'flag' },
33
+ 'electron-as-node:pure': { binary: 'electron', pure: true, loader: '--import', ts: 'flag' },
32
34
  'electron-as-node:bundle': { binary: 'electron', ...bundleOpts },
33
35
  'electron:bundle': { binary: 'electron', electron: true, ...bundleOpts },
36
+ 'deno:test': { binary: 'deno', pure: false, loader: '--preload', ts: 'auto' },
37
+ 'deno:pure': { binary: 'deno', binaryArgs: denoA, pure: true, loader: '--preload', ts: 'auto' },
34
38
  'deno:bundle': { binary: 'deno', binaryArgs: ['run'], target: 'deno1', ...bundleOpts },
35
39
  // Barebone engines
36
40
  'd8:bundle': { binary: 'd8', ...bareboneOpts },
37
41
  'jsc:bundle': { binary: 'jsc', target: 'safari13', ...bareboneOpts },
38
- 'hermes:bundle': { binary: 'hermes', binaryArgs: hermesAv, target: 'es2018', ...bareboneOpts },
42
+ 'hermes:bundle': { binary: 'hermes', binaryArgs: hermesA, target: 'es2018', ...bareboneOpts },
39
43
  'spidermonkey:bundle': { binary: 'spidermonkey', ...bareboneOpts },
40
44
  'engine262:bundle': { binary: 'engine262', ...bareboneOpts },
41
45
  'quickjs:bundle': { binary: 'quickjs', binaryArgs: ['--std'], ...bareboneOpts },
42
46
  'xs:bundle': { binary: 'xs', ...bareboneOpts },
43
47
  'graaljs:bundle': { binary: 'graaljs', ...bareboneOpts },
44
48
  'escargot:bundle': { binary: 'escargot', ...bareboneOpts },
49
+ 'boa:bundle': { binary: 'boa', binaryArgs: ['-m'], ...bareboneOpts },
45
50
  // Browser engines
46
51
  'chrome:puppeteer': { binary: 'chrome', browsers: 'puppeteer', ...bundleOpts },
47
52
  'firefox:puppeteer': { binary: 'firefox', browsers: 'puppeteer', ...bundleOpts },
@@ -55,7 +60,7 @@ const ENGINES = new Map(
55
60
  })
56
61
  )
57
62
  const barebonesOk = ['d8', 'spidermonkey', 'quickjs', 'xs', 'hermes']
58
- const barebonesUnhandled = ['jsc', 'escargot', 'graaljs', 'engine262']
63
+ const barebonesUnhandled = ['jsc', 'escargot', 'boa', 'graaljs', 'engine262']
59
64
 
60
65
  const getEnvFlag = (name) => {
61
66
  if (!Object.hasOwn(process.env, name)) return
@@ -92,6 +97,7 @@ function parseOptions() {
92
97
  require: [],
93
98
  testNamePattern: [],
94
99
  testTimeout: undefined,
100
+ reporter: undefined,
95
101
  }
96
102
 
97
103
  const args = [...process.argv]
@@ -234,6 +240,10 @@ function parseOptions() {
234
240
  case '--testTimeout':
235
241
  options.testTimeout = Number(args.shift())
236
242
  break
243
+ case '--reporter':
244
+ case '--test-reporter':
245
+ options.reporter = String(args.shift())
246
+ break
237
247
  default:
238
248
  throw new Error(`Unknown option: ${option}`)
239
249
  }
@@ -274,6 +284,7 @@ setEnv('EXODUS_TEST_DEVTOOLS', options.devtools ? '1' : '')
274
284
  setEnv('EXODUS_TEST_IS_BROWSER', isBrowserLike ? '1' : '')
275
285
  setEnv('EXODUS_TEST_IS_BAREBONE', options.barebone ? '1' : '')
276
286
  setEnv('EXODUS_TEST_ENVIRONMENT', options.bundle ? 'bundle' : '') // perhaps switch to _IS_BUNDLED?
287
+ if (['deno:pure', 'deno:test'].includes(options.engine)) setEnv('DENO_COMPAT', '1') // https://deno.com/blog/v2.4#deno_compat1
277
288
 
278
289
  assert(!options.devtools || isBrowserLike || !options.pure, engineFlagError('devtools'))
279
290
  assert(!options.throttle || options.browsers, engineFlagError('throttle-cpu'))
@@ -296,8 +307,9 @@ if (options.pure) {
296
307
  assert(!options.forceExit, `Can not use --force-exit with ${engineName} yet`) // TODO
297
308
  assert(!options.watch, `Can not use --watch with with ${engineName}`)
298
309
  assert(options.testNamePattern.length === 0, '--test-name-pattern requires node:test engine now')
310
+ // eslint-disable-next-line unicorn/prefer-switch
299
311
  } else if (options.engine === 'node:test' || options.engine === 'electron-as-node:test') {
300
- const reporter = import.meta.resolve('./reporter.js')
312
+ const reporter = options.reporter ?? import.meta.resolve('./reporter.js')
301
313
  args.push('--test', '--no-warnings=ExperimentalWarning', '--test-reporter', reporter)
302
314
 
303
315
  if (have.haveSnapshots && engineOptions.haveIsOk) args.push('--experimental-test-snapshots')
@@ -318,6 +330,12 @@ if (options.pure) {
318
330
  for (const pattern of options.testNamePattern) args.push('--test-name-pattern', pattern)
319
331
 
320
332
  args.push('--expose-internals') // this is unoptimal and hopefully temporary, see rationale in src/dark.cjs
333
+ } else if (options.engine === 'deno:test') {
334
+ args.push('test', '--allow-all')
335
+ assert(!options.jest, 'deno:test engine does not support --jest yet')
336
+ } else if (options.engine === 'bun:test') {
337
+ args.push('test')
338
+ throw new Error('bun:test is unavailable because Bun test runner has many bugs and does not work')
321
339
  } else {
322
340
  throw new Error('Unreachable')
323
341
  }
@@ -332,8 +350,12 @@ if (process.env.EXODUS_TEST_IGNORE) {
332
350
 
333
351
  // This might be used in presets, so has to be loaded before jest
334
352
  if (options.flow && !options.bundle) args.push('--import', import.meta.resolve('../loader/flow.js'))
335
- if (!options.bundle && !['node:test', 'electron-as-node:test'].includes(options.engine)) {
336
- args.push('--import', import.meta.resolve('../loader/node-test.js'))
353
+ if (['node:test', 'electron-as-node:test', 'deno:test'].includes(options.engine)) {
354
+ // Do not need node:test override
355
+ } else if (options.engine === 'deno:pure') {
356
+ args.push('--import-map', import.meta.resolve('../loader/deno-import-map.json'))
357
+ } else if (!options.bundle) {
358
+ args.push(options.loader ?? '-r', import.meta.resolve('../loader/node-test.js'))
337
359
  }
338
360
 
339
361
  // The comment below is disabled, we don't auto-mock @jest/globals anymore, and having our loader first is faster
@@ -347,7 +369,7 @@ if (options.jest) {
347
369
  if (options.bundle) {
348
370
  setEnv('EXODUS_TEST_JEST_CONFIG', JSON.stringify(jestConfig))
349
371
  } else {
350
- args.push(options.hasImportLoader ? '--import' : '-r', import.meta.resolve('../loader/jest.js'))
372
+ args.push(options.loader ?? '-r', import.meta.resolve('../loader/jest.js'))
351
373
  }
352
374
 
353
375
  if (config.testFailureExitCode !== undefined) {
@@ -418,7 +440,7 @@ if (options.concurrency) {
418
440
 
419
441
  if (options.esbuild && !options.bundle) {
420
442
  setEnv('EXODUS_TEST_ESBUILD', options.esbuild)
421
- if (options.hasImportLoader) {
443
+ if (options.loader === '--import') {
422
444
  const optional = options.esbuild === '*' ? '' : '.optional'
423
445
  args.push('--import', import.meta.resolve(`../loader/esbuild${optional}.js`))
424
446
  } else if (options.flagEngine === false) {
@@ -440,7 +462,7 @@ if (options.typescript) {
440
462
  assert(!options.babel, 'Options --typescript and --babel are mutually exclusive')
441
463
 
442
464
  if (options.ts === 'flag') {
443
- assert(options.hasImportLoader)
465
+ assert(options.loader === '--import')
444
466
  // TODO: switch to native --experimental-strip-types where available
445
467
  args.push('--import', import.meta.resolve('../loader/typescript.js'))
446
468
  } else if (options.ts !== 'auto') {
@@ -660,7 +682,7 @@ if (options.pure) {
660
682
  const missUnhandled = barebonesUnhandled.includes(options.platform) || isBrowserLike
661
683
  if (missUnhandled) warnHuman(`Warning: ${engineName} does not have unhandled rejections tracking`)
662
684
 
663
- const runOne = async (inputFile, attempt = 0) => {
685
+ const runOne = async (inputFile) => {
664
686
  const bundled = buildFile ? await buildFile(inputFile) : undefined
665
687
  if (buildFile) assert(bundled.file)
666
688
  const file = buildFile ? bundled.file : inputFile
@@ -681,12 +703,6 @@ if (options.pure) {
681
703
  const ok = code === 0 && !/^(✖ FAIL|‼ FATAL) /mu.test(stdout)
682
704
  return { ok, output: [stdout, stderr], ms }
683
705
  } catch (err) {
684
- const retryOnXS = new Set(['SIGSEGV', 'SIGBUS'])
685
- if (options.engine === 'xs:bundle' && retryOnXS.has(err.signal) && attempt < 4) {
686
- // xs sometimes randomly crashes with SIGSEGV on CI. Allow 5 attempts (allow #0 - #3 to fail)
687
- return runOne(inputFile, attempt + 1)
688
- }
689
-
690
706
  const ms = Number(process.hrtime.bigint() - start) / 1e6
691
707
  const { code, stderr = '', signal, killed } = err
692
708
  const stdout = cleanOut(err.stdout || '')
@@ -738,8 +754,8 @@ if (options.pure) {
738
754
  console.timeEnd(timeLabel)
739
755
  } else {
740
756
  assert(!buildFile)
741
- assertBinary(options.binary, ['node', 'electron'])
742
- assert(['node:test', 'electron-as-node:test'].includes(options.engine))
757
+ assertBinary(options.binary, ['node', 'electron', 'deno', 'bun'])
758
+ assert(['node:test', 'electron-as-node:test', 'deno:test', 'bun:test'].includes(options.engine))
743
759
  setEnv('EXODUS_TEST_CONTEXT', 'node:test') // The context is always node:test in this branch
744
760
  assert(files.length > 0) // otherwise we can run recursively
745
761
  assert(!options.binaryArgs)
@@ -0,0 +1,9 @@
1
+ {
2
+ "imports": {
3
+ "node:test": "../src/node.js",
4
+ "@jest/globals": "../src/jest.js",
5
+ "tape": "../src/tape.js",
6
+ "tape-promise/tape": "../src/tape.js",
7
+ "micro-should": "../src/jest.js"
8
+ }
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/test",
3
- "version": "1.0.0-rc.100",
3
+ "version": "1.0.0-rc.102",
4
4
  "author": "Exodus Movement, Inc.",
5
5
  "description": "A test suite runner",
6
6
  "homepage": "https://github.com/ExodusMovement/test",
@@ -46,6 +46,7 @@
46
46
  "bin/inband.js",
47
47
  "bin/reporter.js",
48
48
  "loader/babel.cjs",
49
+ "loader/deno-import-map.json",
49
50
  "loader/esbuild.js",
50
51
  "loader/esbuild.optional.js",
51
52
  "loader/flow.js",
@@ -96,9 +97,12 @@
96
97
  "test:simple": "node ./bin/index.js 'tests/*.test.js'",
97
98
  "test:pure": "EXODUS_TEST_ENGINE=node:pure npm run test --",
98
99
  "test:bundle": "EXODUS_TEST_ENGINE=node:bundle npm run test:_bundle --",
100
+ "test:bun:test": "EXODUS_TEST_ENGINE=bun:test npm run test --",
99
101
  "test:bun:pure": "EXODUS_TEST_ENGINE=bun:pure npm run test --",
100
102
  "test:bun:bundle": "EXODUS_TEST_ENGINE=bun:bundle npm run test:_bundle",
101
- "test:deno": "EXODUS_TEST_ENGINE=deno:bundle npm run test:_bundle --",
103
+ "test:deno:test": "EXODUS_TEST_ENGINE=deno:test node ./bin/index.js tests/tape.test.js tests/simple.test.js tests/env.test.js 'tests/engines/**.test.js' tests/node/simple.test.js tests/node/order.test.js",
104
+ "test:deno:pure": "EXODUS_TEST_IGNORE='**/jest-repo/examples/timer/**' EXODUS_TEST_ENGINE=deno:pure npm run test --",
105
+ "test:deno:bundle": "EXODUS_TEST_ENGINE=deno:bundle npm run test:_bundle --",
102
106
  "test:electron:node": "EXODUS_TEST_ENGINE=electron-as-node:test npm run test",
103
107
  "test:electron:node:pure": "EXODUS_TEST_ENGINE=electron-as-node:pure npm run test --",
104
108
  "test:electron:node:bundle": "EXODUS_TEST_ENGINE=electron-as-node:bundle npm run test:_bundle",
@@ -119,10 +123,12 @@
119
123
  "test:xs": "EXODUS_TEST_ENGINE=xs:bundle npm run test:_bundle --",
120
124
  "test:graaljs": "EXODUS_TEST_ENGINE=graaljs:bundle npm run test:_bundle --",
121
125
  "test:escargot": "EXODUS_TEST_ENGINE=escargot:bundle npm run test:_bundle --",
126
+ "test:boa": "EXODUS_TEST_ENGINE=boa:bundle npm run test:_bundle --",
122
127
  "test:fetch": "node ./bin/index.js --jest --drop-network --engine node:pure 'tests/replay/*.test.js'",
123
128
  "test:jsdom": "EXODUS_TEST_JEST_CONFIG='{\"testMatch\":[\"**/*.jsdom-test.js\"],\"testEnvironment\":\"jsdom\", \"rootDir\": \".\"}' ./bin/index.js --jest",
124
129
  "coverage": "node ./bin/index.js --jest --esbuild --coverage",
125
130
  "playwright": "node ./bin/index.js --playwright",
131
+ "esvu": "esvu",
126
132
  "jsvu": "jsvu",
127
133
  "jest": "NODE_OPTIONS=--experimental-vm-modules jest tests/jest/ tests/jest-when/",
128
134
  "lint": "prettier --list-different . && eslint .",
@@ -131,7 +137,7 @@
131
137
  "optionalDependencies": {
132
138
  "@chalker/queue": "^1.0.1",
133
139
  "@exodus/replay": "^1.0.0-rc.9",
134
- "@exodus/test-bundler": "1.0.0-rc.5",
140
+ "@exodus/test-bundler": "1.0.0-rc.6",
135
141
  "c8": "^9.1.0",
136
142
  "expect": "^29.7.0",
137
143
  "fast-glob": "^3.2.11",
@@ -146,9 +152,11 @@
146
152
  "@exodus/prettier": "^1.0.0",
147
153
  "@jest/globals": "^29.7.0",
148
154
  "@types/jest-when": "^3.5.2",
155
+ "@types/node": "^24.0.11",
149
156
  "@typescript-eslint/eslint-plugin": "^7.15.0",
150
157
  "electron": "^35.2.2",
151
158
  "eslint": "^8.44.0",
159
+ "esvu": "^1.2.16",
152
160
  "jest": "^29.7.0",
153
161
  "jest-matcher-utils": "^29.7.0",
154
162
  "jest-serializer-ansi-escapes": "^3.0.0",
@@ -29,7 +29,7 @@ const setSnapshotResolver = (fn) => {
29
29
  snapshot?.setResolveSnapshotPath(resolveSnapshot)
30
30
  }
31
31
 
32
- const mockModule = mock.module
32
+ const mockModule = mock?.module
33
33
  ? (t, o) => mock.module(t.includes('\\') ? pathToFileURL(t) : t, o) // resolve windows-looking paths
34
34
  : undefined
35
35
 
@@ -37,9 +37,9 @@ function parseArgs(args) {
37
37
  }
38
38
 
39
39
  class Context {
40
- test = test // todo: bind to context
41
- describe = describe // todo: bind to context
42
- plan = plan
40
+ test = (...args) => test(...args) // TODO: bind to context
41
+ describe = (...args) => describe(...args) // TODO: bind to context
42
+ plan = (count) => plan(count) // TODO: bind to context
43
43
  children = []
44
44
  #fullName
45
45
  #assert
package/src/jest.mock.js CHANGED
@@ -384,7 +384,12 @@ function jestmock(name, mocker, { override = false, actual, builtin, loc } = {})
384
384
  overridenBuiltins.add(resolved)
385
385
  overrideModule(resolved, true) // Override builtin modules
386
386
  if (syncBuiltinESMExports) {
387
- syncBuiltinESMExports()
387
+ try {
388
+ syncBuiltinESMExports()
389
+ } catch (err) {
390
+ if (!globalThis.Deno) throw err // Deno throws on syncBuiltinESMExports, ignore for now
391
+ }
392
+
388
393
  isOverridenBuiltinSynchedWithESM = true
389
394
  }
390
395
  }
package/src/tape.js CHANGED
@@ -1,4 +1,4 @@
1
- import { assert, assertLoose, test as nodeTest } from './engine.js'
1
+ import { assert, assertLoose, test as nodeTest, after } from './engine.js'
2
2
  import { createCallerLocationHook } from './dark.cjs'
3
3
  import './version.js'
4
4
 
@@ -154,6 +154,7 @@ function tapeWrap(test) {
154
154
 
155
155
  if (test.skip) tap.skip = tapeWrap(test.skip)
156
156
  if (test.only) tap.only = tapeWrap(test.only)
157
+ tap.onFinish = (fn) => after(fn)
157
158
  return tap
158
159
  }
159
160