@exodus/test 1.0.0-rc.10 → 1.0.0-rc.101
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/LICENSE +21 -0
- package/README.md +171 -50
- package/bin/browsers.js +137 -0
- package/bin/color.js +14 -0
- package/bin/electron.js +71 -0
- package/bin/electron.preload.cjs +2 -0
- package/bin/find-binary.js +102 -0
- package/bin/inband.js +14 -0
- package/bin/index.js +638 -92
- package/bin/reporter.js +194 -0
- package/expect.cjs +1 -0
- package/jest.js +1 -0
- package/{bin → loader}/babel.cjs +0 -2
- package/loader/deno-import-map.json +9 -0
- package/loader/esbuild.js +1 -0
- package/loader/esbuild.optional.js +6 -0
- package/loader/flow.js +27 -0
- package/loader/jest.js +2 -0
- package/loader/node-test.js +14 -0
- package/loader/typescript.js +3 -0
- package/loader/typescript.loader.js +36 -0
- package/node.js +1 -0
- package/package.json +130 -17
- package/src/dark.cjs +129 -27
- package/src/engine.js +22 -0
- package/src/engine.node.cjs +46 -0
- package/src/engine.pure.cjs +592 -0
- package/src/engine.pure.snapshot.cjs +37 -0
- package/src/engine.select.cjs +5 -0
- package/src/exodus.js +51 -0
- package/src/expect.cjs +180 -0
- package/src/glob.cjs +13 -0
- package/src/jest.config.fs.js +55 -0
- package/src/jest.config.js +233 -0
- package/src/jest.environment.js +34 -0
- package/src/jest.fn.js +23 -21
- package/src/jest.js +252 -52
- package/src/jest.mock.js +385 -67
- package/src/jest.setup.js +7 -0
- package/src/jest.snapshot.js +104 -40
- package/src/jest.timers.js +101 -20
- package/src/node.js +10 -0
- package/src/pretty-format.cjs +25 -0
- package/src/replay.js +106 -0
- package/src/tape.cjs +15 -0
- package/src/tape.js +11 -8
- package/src/timers-track.js +89 -0
- package/src/version.js +21 -0
- package/tape.js +1 -0
- package/bin/jest.js +0 -13
package/bin/index.js
CHANGED
|
@@ -1,49 +1,144 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { spawn } from 'node:child_process'
|
|
3
|
+
import { spawn, execFile as execFileCallback } from 'node:child_process'
|
|
4
|
+
import { promisify } from 'node:util'
|
|
5
|
+
import { once } from 'node:events'
|
|
4
6
|
import { fileURLToPath } from 'node:url'
|
|
5
|
-
import { basename,
|
|
6
|
-
import {
|
|
7
|
+
import { basename, join } from 'node:path'
|
|
8
|
+
import { randomUUID } from 'node:crypto'
|
|
9
|
+
import { existsSync, rmSync, realpathSync } from 'node:fs'
|
|
10
|
+
import { unlink } from 'node:fs/promises'
|
|
11
|
+
import { tmpdir, availableParallelism, homedir } from 'node:os'
|
|
7
12
|
import assert from 'node:assert/strict'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
// The following make sense only when we run the code in the same Node.js version, i.e. engineOptions.haveIsOk
|
|
14
|
+
import * as have from '../src/version.js'
|
|
15
|
+
import { findBinary } from './find-binary.js'
|
|
16
|
+
import * as browsers from './browsers.js'
|
|
17
|
+
import { glob as globImplementation } from '../src/glob.cjs'
|
|
18
|
+
|
|
19
|
+
const DEFAULT_PATTERNS = [`**/?(*.)+(spec|test).?([cm])[jt]s?(x)`] // do not trust magic dirs by default
|
|
20
|
+
const bundleOpts = { pure: true, bundle: true, esbuild: true, ts: 'auto' }
|
|
21
|
+
const bareboneOpts = { ...bundleOpts, barebone: true }
|
|
22
|
+
const hermesA = ['-Og', '-Xmicrotask-queue']
|
|
23
|
+
const denoA = ['run', '--allow-all'] // also will set DENO_COMPAT=1 env flag below
|
|
24
|
+
const ENGINES = new Map(
|
|
25
|
+
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 },
|
|
29
|
+
'bun:test': { binary: 'bun', ts: 'auto' },
|
|
30
|
+
'bun:pure': { binary: 'bun', pure: true, ts: 'auto' },
|
|
31
|
+
'bun:bundle': { binary: 'bun', ...bundleOpts },
|
|
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' },
|
|
34
|
+
'electron-as-node:bundle': { binary: 'electron', ...bundleOpts },
|
|
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' },
|
|
38
|
+
'deno:bundle': { binary: 'deno', binaryArgs: ['run'], target: 'deno1', ...bundleOpts },
|
|
39
|
+
// Barebone engines
|
|
40
|
+
'd8:bundle': { binary: 'd8', ...bareboneOpts },
|
|
41
|
+
'jsc:bundle': { binary: 'jsc', target: 'safari13', ...bareboneOpts },
|
|
42
|
+
'hermes:bundle': { binary: 'hermes', binaryArgs: hermesA, target: 'es2018', ...bareboneOpts },
|
|
43
|
+
'spidermonkey:bundle': { binary: 'spidermonkey', ...bareboneOpts },
|
|
44
|
+
'engine262:bundle': { binary: 'engine262', ...bareboneOpts },
|
|
45
|
+
'quickjs:bundle': { binary: 'quickjs', binaryArgs: ['--std'], ...bareboneOpts },
|
|
46
|
+
'xs:bundle': { binary: 'xs', ...bareboneOpts },
|
|
47
|
+
'graaljs:bundle': { binary: 'graaljs', ...bareboneOpts },
|
|
48
|
+
'escargot:bundle': { binary: 'escargot', ...bareboneOpts },
|
|
49
|
+
'boa:bundle': { binary: 'boa', binaryArgs: ['-m'], ...bareboneOpts },
|
|
50
|
+
// Browser engines
|
|
51
|
+
'chrome:puppeteer': { binary: 'chrome', browsers: 'puppeteer', ...bundleOpts },
|
|
52
|
+
'firefox:puppeteer': { binary: 'firefox', browsers: 'puppeteer', ...bundleOpts },
|
|
53
|
+
'brave:puppeteer': { binary: 'brave', browsers: 'puppeteer', ...bundleOpts },
|
|
54
|
+
'msedge:puppeteer': { binary: 'msedge', browsers: 'puppeteer', ...bundleOpts },
|
|
55
|
+
'chromium:playwright': { binary: 'chromium', browsers: 'playwright', ...bundleOpts },
|
|
56
|
+
'firefox:playwright': { binary: 'firefox', browsers: 'playwright', ...bundleOpts },
|
|
57
|
+
'webkit:playwright': { binary: 'webkit', browsers: 'playwright', ...bundleOpts },
|
|
58
|
+
'chrome:playwright': { binary: 'chrome', browsers: 'playwright', ...bundleOpts },
|
|
59
|
+
'msedge:playwright': { binary: 'msedge', browsers: 'playwright', ...bundleOpts },
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
const barebonesOk = ['d8', 'spidermonkey', 'quickjs', 'xs', 'hermes']
|
|
63
|
+
const barebonesUnhandled = ['jsc', 'escargot', 'boa', 'graaljs', 'engine262']
|
|
64
|
+
|
|
65
|
+
const getEnvFlag = (name) => {
|
|
66
|
+
if (!Object.hasOwn(process.env, name)) return
|
|
67
|
+
if ([undefined, '', '0', '1'].includes(process.env[name])) return process.env[name] === '1'
|
|
68
|
+
throw new Error(`Unexpected ${name} env value, expected '', '0', or '1'`)
|
|
69
|
+
}
|
|
18
70
|
|
|
19
|
-
|
|
71
|
+
function getNumber(arg) {
|
|
72
|
+
assert.equal(`${arg}`, `${Number(arg)}`)
|
|
73
|
+
return Number(arg)
|
|
20
74
|
}
|
|
21
75
|
|
|
22
76
|
function parseOptions() {
|
|
23
77
|
const options = {
|
|
78
|
+
concurrency: undefined, // undefined means unset (can read from config), 0 means auto
|
|
24
79
|
jest: false,
|
|
25
80
|
typescript: false,
|
|
81
|
+
flow: false,
|
|
26
82
|
esbuild: false,
|
|
27
83
|
babel: false,
|
|
28
|
-
coverage:
|
|
29
|
-
coverageEngine: 'c8', // c8 or node
|
|
84
|
+
coverage: getEnvFlag('EXODUS_TEST_COVERAGE'),
|
|
85
|
+
coverageEngine: process.platform === 'win32' ? 'node' : 'c8', // c8 or node. TODO: can we use c8 on win?
|
|
30
86
|
watch: false,
|
|
87
|
+
only: false,
|
|
31
88
|
passWithNoTests: false,
|
|
32
89
|
writeSnapshots: false,
|
|
90
|
+
devtools: false,
|
|
91
|
+
debug: { files: false },
|
|
92
|
+
dropNetwork: getEnvFlag('EXODUS_TEST_DROP_NETWORK'),
|
|
93
|
+
ideaCompat: false,
|
|
94
|
+
engine: process.env.EXODUS_TEST_ENGINE ?? 'node:test',
|
|
95
|
+
flagEngine: false, // Option combination error reporting differs when engine is passed by flag or env
|
|
96
|
+
entropySize: 5 * 1024,
|
|
97
|
+
require: [],
|
|
98
|
+
testNamePattern: [],
|
|
99
|
+
testTimeout: undefined,
|
|
33
100
|
}
|
|
34
101
|
|
|
35
102
|
const args = [...process.argv]
|
|
36
103
|
|
|
37
104
|
// First argument should be node
|
|
38
|
-
assert.
|
|
39
|
-
assert.
|
|
105
|
+
assert(['node', 'node.exe'].includes(basename(args.shift())))
|
|
106
|
+
assert(['node', 'node.exe'].includes(basename(process.argv0)))
|
|
40
107
|
|
|
41
108
|
// Second argument should be this script
|
|
42
109
|
const jsname = args.shift()
|
|
43
|
-
|
|
110
|
+
const pathsEqual = (a, b) => a === b || (existsSync(a) && realpathSync(a) === b) // resolve symlinks
|
|
111
|
+
assert(basename(jsname) === 'exodus-test' || pathsEqual(jsname, fileURLToPath(import.meta.url)))
|
|
44
112
|
|
|
45
|
-
|
|
113
|
+
if (args[0] === '--playwright') {
|
|
114
|
+
const res = browsers.runPlaywrightCommand(args.slice(1))
|
|
115
|
+
process.exitCode = res.status ?? 1
|
|
116
|
+
process.exit(0)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
class OptionValue extends String {}
|
|
120
|
+
|
|
121
|
+
while (args[0]?.startsWith('-')) {
|
|
46
122
|
const option = args.shift()
|
|
123
|
+
if (option.includes('=')) {
|
|
124
|
+
const [optionName, ...rest] = option.split('=')
|
|
125
|
+
args.unshift(optionName, new OptionValue(rest.join('=')))
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (options.ideaCompat) {
|
|
130
|
+
// Ignore some options IntelliJ IDEA is passing
|
|
131
|
+
switch (option) {
|
|
132
|
+
case '--reporters':
|
|
133
|
+
args.shift()
|
|
134
|
+
continue
|
|
135
|
+
case '--verbose':
|
|
136
|
+
case '--runTestsByPath':
|
|
137
|
+
case '--runInBand':
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
47
142
|
switch (option) {
|
|
48
143
|
case '--global': // compat, will be removed in release
|
|
49
144
|
case '--jest':
|
|
@@ -52,21 +147,34 @@ function parseOptions() {
|
|
|
52
147
|
case '--typescript':
|
|
53
148
|
options.typescript = true
|
|
54
149
|
break
|
|
150
|
+
case '--flow':
|
|
151
|
+
options.flow = true
|
|
152
|
+
break
|
|
55
153
|
case '--esbuild':
|
|
56
|
-
options.esbuild =
|
|
154
|
+
options.esbuild = args[0] instanceof OptionValue ? String(args.shift()) : '*'
|
|
57
155
|
break
|
|
58
156
|
case '--babel':
|
|
59
157
|
options.babel = true
|
|
60
158
|
break
|
|
159
|
+
case '--require':
|
|
160
|
+
options.require.push(String(args.shift()))
|
|
161
|
+
break
|
|
61
162
|
case '--coverage-engine':
|
|
62
|
-
options.coverageEngine = args.shift()
|
|
163
|
+
options.coverageEngine = String(args.shift())
|
|
63
164
|
break
|
|
64
165
|
case '--coverage':
|
|
65
166
|
options.coverage = true
|
|
66
167
|
break
|
|
168
|
+
case '--no-coverage':
|
|
169
|
+
options.coverage = false
|
|
170
|
+
break
|
|
67
171
|
case '--watch':
|
|
68
172
|
options.watch = true
|
|
69
173
|
break
|
|
174
|
+
case '--test-only':
|
|
175
|
+
case '--only':
|
|
176
|
+
options.only = true
|
|
177
|
+
break
|
|
70
178
|
case '--passWithNoTests':
|
|
71
179
|
options.passWithNoTests = true
|
|
72
180
|
break
|
|
@@ -78,117 +186,313 @@ function parseOptions() {
|
|
|
78
186
|
case '--forceExit':
|
|
79
187
|
options.forceExit = true
|
|
80
188
|
break
|
|
189
|
+
case '--engine':
|
|
190
|
+
options.engine = String(args.shift())
|
|
191
|
+
options.flagEngine = true
|
|
192
|
+
break
|
|
193
|
+
case '--devtools':
|
|
194
|
+
case '--inspect-brk':
|
|
195
|
+
options.devtools = '--inspect-brk'
|
|
196
|
+
break
|
|
197
|
+
case '--inspect-wait':
|
|
198
|
+
if (options.devtools !== '--inspect-brk') options.devtools = '--inspect-wait'
|
|
199
|
+
break
|
|
200
|
+
case '--inspect':
|
|
201
|
+
if (!options.devtools) options.devtools = '--inspect'
|
|
202
|
+
break
|
|
203
|
+
case '--debug-files':
|
|
204
|
+
options.debug.files = true
|
|
205
|
+
break
|
|
206
|
+
case '--colors':
|
|
207
|
+
process.env.FORCE_COLOR = '1'
|
|
208
|
+
break
|
|
209
|
+
case '--no-colors':
|
|
210
|
+
process.env.FORCE_COLOR = '0'
|
|
211
|
+
process.env.NO_COLOR = '1'
|
|
212
|
+
process.env.NODE_DISABLE_COLORS = '1'
|
|
213
|
+
break
|
|
214
|
+
case '--drop-network':
|
|
215
|
+
options.dropNetwork = true
|
|
216
|
+
break
|
|
217
|
+
case '--idea-compat':
|
|
218
|
+
options.ideaCompat = true
|
|
219
|
+
break
|
|
220
|
+
case '--throttle-cpu':
|
|
221
|
+
options.throttle = getNumber(args.shift())
|
|
222
|
+
assert(Number.isInteger(options.throttle) && options.throttle > 0) // throttle x times, 1 is no throttle, 2 is 2x slowdown
|
|
223
|
+
break
|
|
224
|
+
case '--debug-timers':
|
|
225
|
+
setEnv('EXODUS_TEST_TIMERS_TRACK', '1')
|
|
226
|
+
break
|
|
227
|
+
case '--concurrency':
|
|
228
|
+
options.concurrency = getNumber(args.shift())
|
|
229
|
+
assert(Number.isInteger(options.concurrency) && options.concurrency >= 0)
|
|
230
|
+
break
|
|
231
|
+
case '--bundle-entropy-size':
|
|
232
|
+
options.entropySize = Number(args.shift())
|
|
233
|
+
break
|
|
234
|
+
case '-t':
|
|
235
|
+
case '--test-name-pattern':
|
|
236
|
+
case '--testNamePattern':
|
|
237
|
+
options.testNamePattern.push(String(args.shift()))
|
|
238
|
+
break
|
|
239
|
+
case '--testTimeout':
|
|
240
|
+
options.testTimeout = Number(args.shift())
|
|
241
|
+
break
|
|
81
242
|
default:
|
|
82
243
|
throw new Error(`Unknown option: ${option}`)
|
|
83
244
|
}
|
|
84
245
|
}
|
|
85
246
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
'Options should come before patterns'
|
|
89
|
-
)
|
|
247
|
+
const argsArePlainStrings = args.every((arg) => typeof arg === 'string' && !arg.startsWith('--'))
|
|
248
|
+
assert(argsArePlainStrings, 'Options should come before patterns')
|
|
90
249
|
|
|
91
250
|
const patterns = [...args]
|
|
92
|
-
if (patterns.length === 0) patterns.push(...DEFAULT_PATTERNS) // defaults
|
|
93
251
|
|
|
94
252
|
return { options, patterns }
|
|
95
253
|
}
|
|
96
254
|
|
|
97
|
-
const
|
|
255
|
+
const isTTY = process.stdout.isTTY
|
|
256
|
+
const isCI = process.env.CI
|
|
257
|
+
const warnHuman = isTTY && !isCI ? (...args) => console.warn(...args) : () => {}
|
|
258
|
+
if (isCI) process.env.FORCE_COLOR = '1' // should support colored output even though not a TTY, overridable with --no-color
|
|
259
|
+
|
|
260
|
+
const setEnv = (name, value) => {
|
|
261
|
+
const env = process.env[name]
|
|
262
|
+
if (env && env !== value) throw new Error(`env conflict: ${name}="${env}", effective: "${value}"`)
|
|
263
|
+
process.env[name] = value === undefined ? '' : value
|
|
264
|
+
}
|
|
265
|
+
|
|
98
266
|
const { options, patterns } = parseOptions()
|
|
99
267
|
|
|
100
|
-
|
|
268
|
+
const engineName = `${options.engine} engine` // used for warnings to user
|
|
269
|
+
const engineFlagError = (flag) => `${engineName} does not support --${flag}`
|
|
270
|
+
const engineOptions = ENGINES.get(options.engine)
|
|
271
|
+
assert(engineOptions, `Unknown engine: ${options.engine}`)
|
|
272
|
+
Object.assign(options, engineOptions)
|
|
273
|
+
options.platform = options.binary // binary can be overriden by c8 or electron
|
|
274
|
+
const isBrowserLike = options.browsers || options.electron
|
|
275
|
+
setEnv('EXODUS_TEST_ENGINE', options.engine) // e.g. 'hermes:bundle', 'node:bundle', 'node:test', 'node:pure'
|
|
276
|
+
setEnv('EXODUS_TEST_PLATFORM', options.binary) // e.g. 'hermes', 'node'
|
|
277
|
+
setEnv('EXODUS_TEST_TIMEOUT', options.testTimeout)
|
|
278
|
+
setEnv('EXODUS_TEST_DEVTOOLS', options.devtools ? '1' : '')
|
|
279
|
+
setEnv('EXODUS_TEST_IS_BROWSER', isBrowserLike ? '1' : '')
|
|
280
|
+
setEnv('EXODUS_TEST_IS_BAREBONE', options.barebone ? '1' : '')
|
|
281
|
+
setEnv('EXODUS_TEST_ENVIRONMENT', options.bundle ? 'bundle' : '') // perhaps switch to _IS_BUNDLED?
|
|
282
|
+
if (['deno:pure', 'deno:test'].includes(options.engine)) setEnv('DENO_COMPAT', '1') // https://deno.com/blog/v2.4#deno_compat1
|
|
283
|
+
|
|
284
|
+
assert(!options.devtools || isBrowserLike || !options.pure, engineFlagError('devtools'))
|
|
285
|
+
assert(!options.throttle || options.browsers, engineFlagError('throttle-cpu'))
|
|
286
|
+
|
|
287
|
+
const args = []
|
|
288
|
+
|
|
289
|
+
if (have.haveModuleMocks && engineOptions.haveIsOk) {
|
|
290
|
+
args.push('--experimental-test-module-mocks')
|
|
291
|
+
}
|
|
101
292
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
293
|
+
if (options.pure) {
|
|
294
|
+
if (options.bundle) {
|
|
295
|
+
assert(!options.coverage, `Can not use --coverage with ${engineName}`)
|
|
296
|
+
assert(!options.babel, `Can not use --babel with ${engineName}`) // TODO?
|
|
297
|
+
}
|
|
105
298
|
|
|
106
|
-
const
|
|
107
|
-
|
|
299
|
+
const requiresNodeCoverage = options.coverage && options.coverageEngine === 'node'
|
|
300
|
+
assert(!requiresNodeCoverage, '"--coverage-engine node" requires "--engine node:test" (default)')
|
|
301
|
+
assert(!options.writeSnapshots, `Can not use write snapshots with ${engineName}`)
|
|
302
|
+
assert(!options.forceExit, `Can not use --force-exit with ${engineName} yet`) // TODO
|
|
303
|
+
assert(!options.watch, `Can not use --watch with with ${engineName}`)
|
|
304
|
+
assert(options.testNamePattern.length === 0, '--test-name-pattern requires node:test engine now')
|
|
305
|
+
// eslint-disable-next-line unicorn/prefer-switch
|
|
306
|
+
} else if (options.engine === 'node:test' || options.engine === 'electron-as-node:test') {
|
|
307
|
+
const reporter = import.meta.resolve('./reporter.js')
|
|
308
|
+
args.push('--test', '--no-warnings=ExperimentalWarning', '--test-reporter', reporter)
|
|
309
|
+
|
|
310
|
+
if (have.haveSnapshots && engineOptions.haveIsOk) args.push('--experimental-test-snapshots')
|
|
311
|
+
|
|
312
|
+
if (options.writeSnapshots) {
|
|
313
|
+
assert(have.haveSnapshots && engineOptions.haveIsOk, 'For snapshots, use Node.js >=22.3.0')
|
|
314
|
+
args.push('--test-update-snapshots')
|
|
315
|
+
}
|
|
108
316
|
|
|
109
|
-
|
|
317
|
+
if (options.forceExit) {
|
|
318
|
+
assert(have.haveForceExit && engineOptions.haveIsOk, 'For forceExit, use Node.js >= 20.14.0')
|
|
319
|
+
args.push('--test-force-exit')
|
|
320
|
+
}
|
|
110
321
|
|
|
111
|
-
|
|
112
|
-
if (
|
|
322
|
+
if (options.watch) args.push('--watch')
|
|
323
|
+
if (options.only) args.push('--test-only')
|
|
113
324
|
|
|
114
|
-
const
|
|
115
|
-
if (haveSnapshots) args.push('--experimental-test-snapshots')
|
|
325
|
+
for (const pattern of options.testNamePattern) args.push('--test-name-pattern', pattern)
|
|
116
326
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
args.push('--
|
|
327
|
+
args.push('--expose-internals') // this is unoptimal and hopefully temporary, see rationale in src/dark.cjs
|
|
328
|
+
} else if (options.engine === 'deno:test') {
|
|
329
|
+
args.push('test', '--allow-all')
|
|
330
|
+
assert(!options.jest, 'deno:test engine does not support --jest yet')
|
|
331
|
+
} else if (options.engine === 'bun:test') {
|
|
332
|
+
args.push('test')
|
|
333
|
+
throw new Error('bun:test is unavailable because Bun test runner has many bugs and does not work')
|
|
334
|
+
} else {
|
|
335
|
+
throw new Error('Unreachable')
|
|
120
336
|
}
|
|
121
337
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
338
|
+
const ignore = ['**/node_modules']
|
|
339
|
+
let filter
|
|
340
|
+
if (process.env.EXODUS_TEST_IGNORE) {
|
|
341
|
+
// fast-glob treats negative ignore patterns exactly the same as positive, let's not cause a confusion
|
|
342
|
+
assert(!process.env.EXODUS_TEST_IGNORE.startsWith('!'), 'Ignore pattern should not be negative')
|
|
343
|
+
ignore.push(process.env.EXODUS_TEST_IGNORE)
|
|
125
344
|
}
|
|
126
345
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
346
|
+
// This might be used in presets, so has to be loaded before jest
|
|
347
|
+
if (options.flow && !options.bundle) args.push('--import', import.meta.resolve('../loader/flow.js'))
|
|
348
|
+
if (['node:test', 'electron-as-node:test', 'deno:test'].includes(options.engine)) {
|
|
349
|
+
// Do not need node:test override
|
|
350
|
+
} else if (options.engine === 'deno:pure') {
|
|
351
|
+
args.push('--import-map', import.meta.resolve('../loader/deno-import-map.json'))
|
|
352
|
+
} else if (!options.bundle) {
|
|
353
|
+
args.push(options.loader ?? '-r', import.meta.resolve('../loader/node-test.js'))
|
|
130
354
|
}
|
|
131
355
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
356
|
+
// The comment below is disabled, we don't auto-mock @jest/globals anymore, and having our loader first is faster
|
|
357
|
+
// [Disabled] Our loader should be last, as enabling module mocks confuses other loaders
|
|
358
|
+
let jestConfig = null
|
|
359
|
+
let globalTeardown
|
|
360
|
+
if (options.jest) {
|
|
361
|
+
const { loadJestConfig } = await import('../src/jest.config.js')
|
|
362
|
+
const config = await loadJestConfig(process.cwd())
|
|
363
|
+
jestConfig = config
|
|
364
|
+
if (options.bundle) {
|
|
365
|
+
setEnv('EXODUS_TEST_JEST_CONFIG', JSON.stringify(jestConfig))
|
|
142
366
|
} else {
|
|
143
|
-
|
|
367
|
+
args.push(options.loader ?? '-r', import.meta.resolve('../loader/jest.js'))
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (config.testFailureExitCode !== undefined) {
|
|
371
|
+
if (Number(config.testFailureExitCode) === 0) {
|
|
372
|
+
console.warn('Jest is configured to succeed with exit code 0 on test failures!')
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
process.on('exit', (code) => {
|
|
376
|
+
if (code !== 0) process.exitCode = config.testFailureExitCode
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (patterns.length > 0) {
|
|
381
|
+
// skip, we already have patterns via argv
|
|
382
|
+
} else if (config.testRegex) {
|
|
383
|
+
assert(typeof config.testRegex === 'string', `config.testRegex should be a string`)
|
|
384
|
+
assert(!config.testMatch, 'config.testRegex can not be used together with config.testMatch')
|
|
385
|
+
patterns.push('**/*')
|
|
386
|
+
} else if (config.testMatch) {
|
|
387
|
+
patterns.push(...(Array.isArray(config.testMatch) ? config.testMatch : [config.testMatch]))
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (config.passWithNoTests) options.passWithNoTests = true
|
|
391
|
+
const testRegex = config.testRegex ? new RegExp(config.testRegex, 'u') : null
|
|
392
|
+
const ignoreRegexes = config.testPathIgnorePatterns.map((x) => new RegExp(x, 'u'))
|
|
393
|
+
if (testRegex || ignoreRegexes.length > 0) {
|
|
394
|
+
filter = (x) => {
|
|
395
|
+
const resolved = `<rootDir>/${x}` // don't actually include cwd, that should be irrelevant
|
|
396
|
+
if (testRegex && !testRegex.test(resolved)) return false
|
|
397
|
+
return !ignoreRegexes.some((r) => r.test(resolved))
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (config.collectCoverage && options.coverage === undefined) options.coverage = true
|
|
402
|
+
if (config.maxWorkers && options.concurrency === undefined) {
|
|
403
|
+
options.concurrency = config.maxWorkers
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const key of ['globalSetup', 'globalTeardown']) {
|
|
407
|
+
if (!config[key]) continue
|
|
408
|
+
const { default: method } = await import(config[key])
|
|
409
|
+
assert(method, `config.${key} does not export a default method`)
|
|
410
|
+
assert(method.length === 0, `Arguments for config.${key} are not supported yet`)
|
|
411
|
+
if (key === 'globalTeardown') {
|
|
412
|
+
globalTeardown = method
|
|
413
|
+
} else {
|
|
414
|
+
await method() // globalSetup
|
|
415
|
+
}
|
|
144
416
|
}
|
|
145
417
|
}
|
|
146
418
|
|
|
147
|
-
if (options.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
419
|
+
if (options.concurrency) {
|
|
420
|
+
const raw = options.concurrency
|
|
421
|
+
let concurrency = raw
|
|
422
|
+
if (typeof raw === 'string') {
|
|
423
|
+
if (/^\d{1,15}%$/u.test(raw)) {
|
|
424
|
+
const perc = Number(raw.slice(0, -1))
|
|
425
|
+
concurrency = Math.max(1, Math.round((perc * availableParallelism()) / 100))
|
|
426
|
+
} else {
|
|
427
|
+
assert(/^\d{1,15}$/u.test(raw), `Wrong concurrency: ${raw}`)
|
|
428
|
+
concurrency = Number(raw)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
assert(Number.isSafeInteger(concurrency) && concurrency >= 1, `Wrong concurrency: ${raw}`)
|
|
433
|
+
options.concurrency = concurrency
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (options.esbuild && !options.bundle) {
|
|
437
|
+
setEnv('EXODUS_TEST_ESBUILD', options.esbuild)
|
|
438
|
+
if (options.loader === '--import') {
|
|
439
|
+
const optional = options.esbuild === '*' ? '' : '.optional'
|
|
440
|
+
args.push('--import', import.meta.resolve(`../loader/esbuild${optional}.js`))
|
|
441
|
+
} else if (options.flagEngine === false) {
|
|
442
|
+
// Engine is set via env, --esbuild set via flag. Allow but warn
|
|
443
|
+
console.warn(`Warning: ${engineName} does not support --esbuild option`)
|
|
151
444
|
} else {
|
|
152
|
-
|
|
445
|
+
console.error(`Error: ${engineName} does not support --esbuild option`)
|
|
446
|
+
process.exit(1)
|
|
153
447
|
}
|
|
154
448
|
}
|
|
155
449
|
|
|
156
450
|
if (options.babel) {
|
|
157
|
-
assert(!options.
|
|
158
|
-
args.push('-r',
|
|
451
|
+
assert(!options.esbuild, 'Options --babel and --esbuild are mutually exclusive')
|
|
452
|
+
args.push('-r', import.meta.resolve('../loader/babel.cjs'))
|
|
159
453
|
}
|
|
160
454
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
455
|
+
if (options.typescript) {
|
|
456
|
+
assert(!options.esbuild, 'Options --typescript and --esbuild are mutually exclusive')
|
|
457
|
+
assert(!options.babel, 'Options --typescript and --babel are mutually exclusive')
|
|
458
|
+
|
|
459
|
+
if (options.ts === 'flag') {
|
|
460
|
+
assert(options.loader === '--import')
|
|
461
|
+
// TODO: switch to native --experimental-strip-types where available
|
|
462
|
+
args.push('--import', import.meta.resolve('../loader/typescript.js'))
|
|
463
|
+
} else if (options.ts !== 'auto') {
|
|
464
|
+
throw new Error(`Processing --typescript is not possible with ${engineName}`)
|
|
167
465
|
}
|
|
168
466
|
}
|
|
169
467
|
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
assert(!process.env.EXODUS_TEST_IGNORE.startsWith('!'), 'Ignore pattern should not be negative')
|
|
174
|
-
ignore.push(process.env.EXODUS_TEST_IGNORE)
|
|
468
|
+
for (const r of options.require) {
|
|
469
|
+
assert(!options.bundle, 'Can not use -r with *:bundle engines')
|
|
470
|
+
args.push('-r', r)
|
|
175
471
|
}
|
|
176
472
|
|
|
177
|
-
|
|
473
|
+
async function glob(patterns, { ignore, cwd }) {
|
|
474
|
+
const patternsY = patterns.filter((x) => !x.startsWith('!'))
|
|
475
|
+
const patternsN = patterns.filter((x) => x.startsWith('!')).map((x) => x.slice(1))
|
|
476
|
+
return globImplementation(patternsY, { exclude: [...ignore, ...patternsN], cwd })
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (patterns.length === 0) patterns.push(...DEFAULT_PATTERNS) // defaults
|
|
480
|
+
const globbed = await glob(patterns, { ignore })
|
|
481
|
+
const allfiles = filter ? globbed.filter(filter) : globbed
|
|
178
482
|
|
|
179
483
|
if (allfiles.length === 0) {
|
|
180
484
|
if (options.passWithNoTests) {
|
|
181
|
-
console.warn('No
|
|
485
|
+
console.warn('No test files found, but passing due to --passWithNoTests')
|
|
182
486
|
process.exit(0)
|
|
183
487
|
}
|
|
184
488
|
|
|
185
|
-
console.error('No
|
|
489
|
+
console.error('No test files found!')
|
|
186
490
|
process.exit(1)
|
|
187
491
|
}
|
|
188
492
|
|
|
189
493
|
let subfiles // must be a strict subset of allfiles
|
|
190
494
|
if (process.env.EXODUS_TEST_SELECT) {
|
|
191
|
-
|
|
495
|
+
subfiles = await glob(process.env.EXODUS_TEST_SELECT, { ignore })
|
|
192
496
|
|
|
193
497
|
const allSet = new Set(allfiles)
|
|
194
498
|
const stray = subfiles.filter((file) => !allSet.has(file))
|
|
@@ -198,29 +502,271 @@ if (process.env.EXODUS_TEST_SELECT) {
|
|
|
198
502
|
}
|
|
199
503
|
|
|
200
504
|
if (subfiles.length === 0) {
|
|
201
|
-
console.error('No
|
|
505
|
+
console.error('No test files selected due to EXODUS_TEST_SELECT, passing')
|
|
202
506
|
process.exit(0)
|
|
203
507
|
}
|
|
204
508
|
}
|
|
205
509
|
|
|
206
510
|
const files = subfiles ?? allfiles
|
|
207
511
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
512
|
+
files.sort((a, b) => {
|
|
513
|
+
const [al, bl] = [a.split('/'), b.split('/')]
|
|
514
|
+
while (al[0] === bl[0]) {
|
|
515
|
+
al.shift()
|
|
516
|
+
bl.shift()
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// First process each file in dir, then subdirs
|
|
520
|
+
if (al.length < 2) return -1
|
|
521
|
+
if (bl.length < 2) return 1
|
|
522
|
+
// Prefer example/ over example-something/
|
|
523
|
+
const [an, bn] = [al, bl].map((list) => list.join(String.fromCodePoint(0)))
|
|
524
|
+
if (an < bn) return -1
|
|
525
|
+
if (an > bn) return 1
|
|
526
|
+
throw new Error('Unreachable')
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
if (options.debug.files) {
|
|
530
|
+
for (const f of files) console.log(f) // joining with \n can get truncated, too big
|
|
531
|
+
process.exit(1) // do not succeed!
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const tsTests = files.filter((file) => /\.[mc]?tsx?$/u.test(file))
|
|
535
|
+
const tsSupport = options.ts === 'auto' || options.esbuild || options.typescript || options.babel
|
|
536
|
+
if (tsTests.length > 0 && !tsSupport) {
|
|
537
|
+
console.error(`Some tests require --typescript or --esbuild flag:\n ${tsTests.join('\n ')}`)
|
|
211
538
|
process.exit(1)
|
|
212
|
-
} else if (!allfiles.some((file) => file
|
|
539
|
+
} else if (!allfiles.some((file) => /\.[cm]?ts$/.test(file)) && options.typescript) {
|
|
213
540
|
console.warn(`Flag --typescript has been used, but there were no TypeScript tests found!`)
|
|
214
541
|
}
|
|
215
542
|
|
|
216
|
-
|
|
217
|
-
|
|
543
|
+
if (!options.bundle) {
|
|
544
|
+
// uses top-level await, :bundle doesn't have that
|
|
545
|
+
const inband = new Set(files.filter((f) => basename(f).includes('.inband.')))
|
|
546
|
+
if (inband.size > 0) {
|
|
547
|
+
process.env.EXODUS_TEST_INBAND = JSON.stringify([...inband])
|
|
548
|
+
const remaning = files.filter((f) => !inband.has(f))
|
|
549
|
+
files.length = 0
|
|
550
|
+
files.push(fileURLToPath(import.meta.resolve('./inband.js')), ...remaning)
|
|
551
|
+
}
|
|
552
|
+
}
|
|
218
553
|
|
|
219
554
|
if (!Object.hasOwn(process.env, 'NODE_ENV')) process.env.NODE_ENV = 'test'
|
|
555
|
+
setEnv('EXODUS_TEST_ONLY', options.only ? '1' : '')
|
|
556
|
+
|
|
557
|
+
let c8
|
|
558
|
+
if (options.coverage) {
|
|
559
|
+
assert.equal(options.binary, 'node', 'Coverage is only supported with Node.js')
|
|
560
|
+
if (options.coverageEngine === 'node') {
|
|
561
|
+
args.push('--experimental-test-coverage')
|
|
562
|
+
if (have.haveCoverExclude && engineOptions.haveIsOk) {
|
|
563
|
+
args.push(
|
|
564
|
+
`--test-coverage-exclude=**/@exodus/test/src/**`,
|
|
565
|
+
`--test-coverage-exclude=${DEFAULT_PATTERNS[0]}`
|
|
566
|
+
)
|
|
567
|
+
}
|
|
568
|
+
} else if (options.coverageEngine === 'c8') {
|
|
569
|
+
c8 = findBinary('c8')
|
|
570
|
+
assert.equal(c8, fileURLToPath(import.meta.resolve('c8/bin/c8.js')))
|
|
571
|
+
args.unshift(options.binary)
|
|
572
|
+
options.binary = c8
|
|
573
|
+
// perhaps use text-summary ?
|
|
574
|
+
args.unshift('-r', 'text', '-r', 'html', '-r', 'lcov', '-r', 'json-summary')
|
|
575
|
+
} else {
|
|
576
|
+
throw new Error(`Unknown coverage engine: ${JSON.stringify(options.coverageEngine)}`)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (options.binary === 'electron') {
|
|
581
|
+
if (isBrowserLike) {
|
|
582
|
+
assert(!options.binaryArgs)
|
|
583
|
+
options.binaryArgs = [fileURLToPath(import.meta.resolve('./electron.js'))]
|
|
584
|
+
} else {
|
|
585
|
+
setEnv('ELECTRON_RUN_AS_NODE', '1')
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (options.barebone || options.binary === 'electron') {
|
|
590
|
+
options.binary = findBinary(options.binary)
|
|
591
|
+
options.binaryCanBeAbsolute = true
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const makeTitle = () => {
|
|
595
|
+
let title = options.browsers === 'puppeteer' ? findBinary(options.binary) : options.binary
|
|
596
|
+
if (options.browsers === 'playwright') return `${title} (Playwright-managed)`
|
|
597
|
+
if (basename(title) === title) return title
|
|
598
|
+
const dir = { '~': `${process.cwd()}/`, '.': `${homedir()}/` }
|
|
599
|
+
if (title.startsWith(dir['~']) && dir['~'].length > 1) title = `./${title.slice(dir['~'].length)}`
|
|
600
|
+
if (title.startsWith(dir['.']) && dir['.'].length > 1) title = `~/${title.slice(dir['.'].length)}`
|
|
601
|
+
return /\s/u.test(title) ? JSON.stringify(title) : title
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const { color } = await import('./color.js') // can't load before env flags are set
|
|
605
|
+
console.info(color(`Engine: ${options.engine}, running on ${makeTitle()}`, 'green'))
|
|
606
|
+
|
|
607
|
+
const assertBinary = (binary, allowed) => {
|
|
608
|
+
if (allowed.includes(binary)) return
|
|
609
|
+
if (existsSync(binary)) {
|
|
610
|
+
const name = basename(binary.toLowerCase()).replace(/\.exe$/u, '')
|
|
611
|
+
if ((c8 && binary === c8) || (options.binaryCanBeAbsolute && allowed.includes(name))) return
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
throw new Error(`Unexpected binary: ${binary}`)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
setEnv('EXODUS_TEST_EXECARGV', JSON.stringify(args))
|
|
618
|
+
let buildFile
|
|
619
|
+
|
|
620
|
+
if (options.bundle) {
|
|
621
|
+
const outdir = join(tmpdir(), `exodus-test-${randomUUID().slice(0, 8)}`)
|
|
622
|
+
process.on('exit', () => rmSync(outdir, { recursive: true, force: true }))
|
|
623
|
+
assert.deepEqual(args, [])
|
|
624
|
+
|
|
625
|
+
if (options.binary === 'node') args.unshift('--enable-source-maps') // FIXME
|
|
626
|
+
|
|
627
|
+
const bundle = await import('@exodus/test-bundler/bundle')
|
|
628
|
+
bundle.setResolver((file) => fileURLToPath(import.meta.resolve(`../src/${file}`)))
|
|
629
|
+
await bundle.init({ ...options, outdir, jestConfig })
|
|
630
|
+
buildFile = (file) => bundle.build(file)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (options.dropNetwork) warnHuman('--drop-network is a test helper, not a security mechanism')
|
|
634
|
+
|
|
635
|
+
const execFile = promisify(execFileCallback)
|
|
636
|
+
|
|
637
|
+
async function launch(binary, args, opts = {}, buffering = false) {
|
|
638
|
+
if (options.browsers) {
|
|
639
|
+
assert(buffering, 'Unexpected non-buffered browser run')
|
|
640
|
+
const { timeout } = opts
|
|
641
|
+
const { browsers: runner, devtools, dropNetwork, throttle } = options
|
|
642
|
+
return browsers.run(runner, args, { binary, devtools, dropNetwork, timeout, throttle })
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const barebones = [...barebonesOk, ...barebonesUnhandled]
|
|
646
|
+
assertBinary(binary, ['node', 'bun', 'deno', 'electron', ...barebones, 'v8']) // v8 is an alias to d8
|
|
647
|
+
if (binary === c8 && process.platform === 'win32') {
|
|
648
|
+
;[binary, args] = ['node', [binary, ...args]]
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (options.dropNetwork) {
|
|
652
|
+
switch (process.platform) {
|
|
653
|
+
case 'darwin':
|
|
654
|
+
;[binary, args] = ['sandbox-exec', ['-n', 'no-network', binary, ...args]]
|
|
655
|
+
break
|
|
656
|
+
case 'linux':
|
|
657
|
+
;[binary, args] = ['unshare', ['-n', '-r', binary, ...args]]
|
|
658
|
+
break
|
|
659
|
+
default:
|
|
660
|
+
assert.fail(`--drop-network is not implemented on platform: ${process.platform}`)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (buffering) return execFile(binary, args, { maxBuffer: 5 * 1024 * 1024, ...opts }) // 5 MiB just in case
|
|
665
|
+
const child = spawn(binary, args, { stdio: 'inherit', ...opts })
|
|
666
|
+
const [code] = await once(child, 'close')
|
|
667
|
+
return { code }
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (options.pure) {
|
|
671
|
+
if (!process.env.FORCE_COLOR && process.stdout.hasColors?.() && process.stderr.hasColors?.()) {
|
|
672
|
+
setEnv('FORCE_COLOR', '1') // Default to color output for subprocesses if our stream supports it
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
setEnv('EXODUS_TEST_CONTEXT', 'pure')
|
|
676
|
+
warnHuman(`${engineName} is experimental and may not work an expected`)
|
|
677
|
+
const missUnhandled = barebonesUnhandled.includes(options.platform) || isBrowserLike
|
|
678
|
+
if (missUnhandled) warnHuman(`Warning: ${engineName} does not have unhandled rejections tracking`)
|
|
679
|
+
|
|
680
|
+
const runOne = async (inputFile) => {
|
|
681
|
+
const bundled = buildFile ? await buildFile(inputFile) : undefined
|
|
682
|
+
if (buildFile) assert(bundled.file)
|
|
683
|
+
const file = buildFile ? bundled.file : inputFile
|
|
684
|
+
if (bundled?.errors.length > 0) return { ok: false, output: bundled.errors }
|
|
685
|
+
|
|
686
|
+
const failedBare = 'EXODUS_TEST_FAILED_EXIT_CODE_1'
|
|
687
|
+
const cleanOut = (out) => out.replaceAll(`\n${failedBare}\n`, '\n').replaceAll(failedBare, '')
|
|
688
|
+
const { binaryArgs = [] } = options
|
|
689
|
+
// Timeout is fallback if timeout in script hangs, 50x as it can be adjusted per-script inside them
|
|
690
|
+
// Do we want to extract timeouts from script code instead? Also, hermes might be slower, so makes sense to increase
|
|
691
|
+
const timeout = (options.testTimeout || jestConfig?.testTimeout || 5000) * 50
|
|
692
|
+
const start = process.hrtime.bigint()
|
|
693
|
+
try {
|
|
694
|
+
const fullArgs = [...binaryArgs, ...args, file]
|
|
695
|
+
const { code = 0, stdout, stderr } = await launch(options.binary, fullArgs, { timeout }, true)
|
|
696
|
+
const ms = Number(process.hrtime.bigint() - start) / 1e6
|
|
697
|
+
if (stdout.includes(failedBare)) return { ok: false, output: [cleanOut(stdout), stderr], ms }
|
|
698
|
+
const ok = code === 0 && !/^(✖ FAIL|‼ FATAL) /mu.test(stdout)
|
|
699
|
+
return { ok, output: [stdout, stderr], ms }
|
|
700
|
+
} catch (err) {
|
|
701
|
+
const ms = Number(process.hrtime.bigint() - start) / 1e6
|
|
702
|
+
const { code, stderr = '', signal, killed } = err
|
|
703
|
+
const stdout = cleanOut(err.stdout || '')
|
|
704
|
+
if (code === null) {
|
|
705
|
+
assert(signal)
|
|
706
|
+
const message = ` ${signal}${killed ? ' (killed)' : ''}`
|
|
707
|
+
const comment = killed && signal === 'SIGTERM' ? ' Most likely due to timeout reached' : ''
|
|
708
|
+
return { ok: false, output: [stdout, stderr, message, comment], ms }
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (Number.isInteger(code) && code > 0) return { ok: false, output: [stdout, stderr], ms } // Expected, test error
|
|
712
|
+
|
|
713
|
+
throw err // Internal test runner error, e.g. launch() failed
|
|
714
|
+
} finally {
|
|
715
|
+
if (bundled) await unlink(bundled.file)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
220
718
|
|
|
221
|
-
|
|
222
|
-
const
|
|
719
|
+
const { Queue } = await import('@chalker/queue')
|
|
720
|
+
const queue = new Queue(options.concurrency || availableParallelism() - 1)
|
|
721
|
+
const runConcurrent = async (file) => {
|
|
722
|
+
await queue.claim()
|
|
723
|
+
try {
|
|
724
|
+
// need to await here
|
|
725
|
+
return await runOne(file)
|
|
726
|
+
} finally {
|
|
727
|
+
queue.release()
|
|
728
|
+
}
|
|
729
|
+
}
|
|
223
730
|
|
|
224
|
-
|
|
731
|
+
const { format, head, middle, tail, timeLabel, summary } = await import('./reporter.js')
|
|
732
|
+
|
|
733
|
+
const failures = []
|
|
734
|
+
const tasks = files.map((file) => ({ file, task: runConcurrent(file) }))
|
|
735
|
+
console.time(timeLabel)
|
|
736
|
+
for (const { file, task } of tasks) {
|
|
737
|
+
head(file)
|
|
738
|
+
const { ok, output, ms } = await task
|
|
739
|
+
middle(file, ok, ms)
|
|
740
|
+
for (const chunk of output.filter((x) => x.trim())) console.log(format(chunk).trimEnd())
|
|
741
|
+
tail(file)
|
|
742
|
+
if (!ok) failures.push(file)
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (failures.length > 0) process.exitCode = 1
|
|
746
|
+
summary(files, failures)
|
|
747
|
+
|
|
748
|
+
if (options.browsers) await browsers.close()
|
|
749
|
+
console.timeEnd(timeLabel)
|
|
750
|
+
} else {
|
|
751
|
+
assert(!buildFile)
|
|
752
|
+
assertBinary(options.binary, ['node', 'electron', 'deno', 'bun'])
|
|
753
|
+
assert(['node:test', 'electron-as-node:test', 'deno:test', 'bun:test'].includes(options.engine))
|
|
754
|
+
setEnv('EXODUS_TEST_CONTEXT', 'node:test') // The context is always node:test in this branch
|
|
755
|
+
assert(files.length > 0) // otherwise we can run recursively
|
|
756
|
+
assert(!options.binaryArgs)
|
|
757
|
+
if (options.concurrency) args.push('--test-concurrency', options.concurrency)
|
|
758
|
+
if (['--inspect', '--inspect-brk', '--inspect-wait'].includes(options.devtools)) {
|
|
759
|
+
args.push(options.devtools)
|
|
760
|
+
if (have.haveNetworkInspection) args.push('--experimental-network-inspection')
|
|
761
|
+
console.warn(
|
|
762
|
+
['--inspect-brk', '--inspect-wait'].includes(options.devtools)
|
|
763
|
+
? 'Open chrome://inspect/ to connect devtools, waiting'
|
|
764
|
+
: 'Open chrome://inspect/ to connect devtools\nUse --inspect-brk to wait for inspector'
|
|
765
|
+
)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const { code } = await launch(options.binary, [...args, ...files])
|
|
225
769
|
process.exitCode = code
|
|
226
|
-
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (globalTeardown) await globalTeardown()
|