@exodus/test 1.0.0-rc.87 → 1.0.0-rc.89
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/bin/index.js +3 -0
- package/bin/reporter.js +1 -1
- package/package.json +3 -2
- package/src/exodus.js +4 -4
- package/src/expect.cjs +7 -3
- package/src/jest.js +6 -0
- package/src/jest.timers.js +11 -1
- package/src/timers-track.js +89 -0
package/bin/index.js
CHANGED
|
@@ -209,6 +209,9 @@ function parseOptions() {
|
|
|
209
209
|
options.throttle = getNumber(args.shift())
|
|
210
210
|
assert(Number.isInteger(options.throttle) && options.throttle > 0) // throttle x times, 1 is no throttle, 2 is 2x slowdown
|
|
211
211
|
break
|
|
212
|
+
case '--debug-timers':
|
|
213
|
+
setEnv('EXODUS_TEST_TIMERS_TRACK', '1')
|
|
214
|
+
break
|
|
212
215
|
case '--concurrency':
|
|
213
216
|
options.concurrency = getNumber(args.shift())
|
|
214
217
|
assert(Number.isInteger(options.concurrency) && options.concurrency >= 0)
|
package/bin/reporter.js
CHANGED
|
@@ -155,7 +155,7 @@ export default async function nodeTestReporterExodus(source) {
|
|
|
155
155
|
break
|
|
156
156
|
case 'test:fail':
|
|
157
157
|
if (!pskip(path)) print(`${color('✖ FAIL ', 'red')}${pathstr(path)}${formatSuffix(data)}`)
|
|
158
|
-
assert(path.pop() === data.name)
|
|
158
|
+
if (path.length > 0) assert(path.pop() === data.name) // afterAll can generate failures too, with an empty path
|
|
159
159
|
if (!data.todo) failedFiles.add(file)
|
|
160
160
|
if (!notPrintedError(data.details.error)) {
|
|
161
161
|
const { body, loc } = extractError(data, relative(cwd, data.file || data.name)) // might be different from current file if in subimport
|
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.89",
|
|
4
4
|
"author": "Exodus Movement, Inc.",
|
|
5
5
|
"description": "A test suite runner",
|
|
6
6
|
"homepage": "https://github.com/ExodusMovement/test",
|
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
"src/replay.js",
|
|
97
97
|
"src/tape.js",
|
|
98
98
|
"src/tape.cjs",
|
|
99
|
+
"src/timers-track.js",
|
|
99
100
|
"src/version.js",
|
|
100
101
|
"expect.cjs",
|
|
101
102
|
"jest.js",
|
|
@@ -148,7 +149,6 @@
|
|
|
148
149
|
"dependencies": {
|
|
149
150
|
"@chalker/queue": "^1.0.1",
|
|
150
151
|
"@exodus/replay": "^1.0.0-rc.8",
|
|
151
|
-
"expect": "^29.7.0",
|
|
152
152
|
"pretty-format": "^29.7.0"
|
|
153
153
|
},
|
|
154
154
|
"optionalDependencies": {
|
|
@@ -170,6 +170,7 @@
|
|
|
170
170
|
"crypto-browserify": "^3.12.0",
|
|
171
171
|
"esbuild": "~0.25.4",
|
|
172
172
|
"events": "^3.3.0",
|
|
173
|
+
"expect": "^29.7.0",
|
|
173
174
|
"fast-glob": "^3.2.11",
|
|
174
175
|
"flow-remove-types": "^2.242.0",
|
|
175
176
|
"jest-extended": "^4.0.2",
|
package/src/exodus.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mock } from './engine.js'
|
|
2
2
|
import * as node from './engine.js'
|
|
3
3
|
import { fetchReplay, fetchRecord, websocketRecord, websocketReplay } from './replay.js'
|
|
4
|
+
import { timersTrack, timersList, timersDebug, timersAssert } from './timers-track.js'
|
|
4
5
|
import { insideEsbuild } from './dark.cjs'
|
|
5
6
|
import { haveValidTimers } from './version.js'
|
|
6
7
|
|
|
@@ -20,9 +21,8 @@ export const exodus = {
|
|
|
20
21
|
concurrency: node.engine !== 'pure', // pure engine doesn't support concurrency
|
|
21
22
|
},
|
|
22
23
|
mock: {
|
|
23
|
-
|
|
24
|
-
fetchReplay,
|
|
25
|
-
websocketRecord,
|
|
26
|
-
websocketReplay,
|
|
24
|
+
...{ timersTrack, timersList, timersDebug, timersAssert }, // eslint-disable-line unicorn/no-useless-spread
|
|
25
|
+
...{ fetchRecord, fetchReplay }, // eslint-disable-line unicorn/no-useless-spread
|
|
26
|
+
...{ websocketRecord, websocketReplay }, // eslint-disable-line unicorn/no-useless-spread
|
|
27
27
|
},
|
|
28
28
|
}
|
package/src/expect.cjs
CHANGED
|
@@ -11,10 +11,14 @@ function fixupAssertions() {
|
|
|
11
11
|
assertionsDelta = 0
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
15
14
|
function loadExpect(loadReason) {
|
|
16
15
|
if (expect) return expect
|
|
17
|
-
|
|
16
|
+
try {
|
|
17
|
+
expect = require('expect').expect
|
|
18
|
+
} catch {
|
|
19
|
+
throw new Error(`Failed to load 'expect', required for ${loadReason}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
// console.log('expect load reason:', loadReason)
|
|
19
23
|
try {
|
|
20
24
|
expect.extend(require('jest-extended'))
|
|
@@ -82,7 +86,7 @@ function createExpect() {
|
|
|
82
86
|
const matcher = matchers[name] || matchersFalseNegative[name]
|
|
83
87
|
if (matcher) {
|
|
84
88
|
return (...args) => {
|
|
85
|
-
if (!matcher(x, ...args)) return loadExpect(`.${name}
|
|
89
|
+
if (!matcher(x, ...args)) return loadExpect(`.${name} check`)(x)[name](...args)
|
|
86
90
|
assertionsDelta++
|
|
87
91
|
}
|
|
88
92
|
}
|
package/src/jest.js
CHANGED
|
@@ -9,8 +9,12 @@ import { createCallerLocationHook } from './dark.cjs'
|
|
|
9
9
|
import { exodus } from './exodus.js'
|
|
10
10
|
import { expect } from './expect.cjs'
|
|
11
11
|
import { format as prettyFormat } from 'pretty-format'
|
|
12
|
+
import { timersTrack, timersDebug } from './timers-track.js'
|
|
12
13
|
|
|
13
14
|
const { getCallerLocation, installLocationInNextTest } = createCallerLocationHook()
|
|
15
|
+
const { setTimeout } = globalThis
|
|
16
|
+
|
|
17
|
+
if (process.env.EXODUS_TEST_TIMERS_TRACK) timersTrack()
|
|
14
18
|
|
|
15
19
|
let inband = false
|
|
16
20
|
if (process.env.EXODUS_TEST_ENVIRONMENT !== 'bundle') {
|
|
@@ -208,6 +212,7 @@ if (process.env.EXODUS_TEST_PLATFORM !== 'deno' && globalThis.process) {
|
|
|
208
212
|
// give everything additional (configurable) defaultTimeout time to finish, otherwide fail
|
|
209
213
|
const timeout = defaultTimeout
|
|
210
214
|
setTimeout(() => {
|
|
215
|
+
if (process.env.EXODUS_TEST_TIMERS_TRACK) timersDebug()
|
|
211
216
|
console.error(`${prefix} additional ${timeout}ms. Terminating with a failure...`)
|
|
212
217
|
process.exit(1)
|
|
213
218
|
}, timeout).unref()
|
|
@@ -216,6 +221,7 @@ if (process.env.EXODUS_TEST_PLATFORM !== 'deno' && globalThis.process) {
|
|
|
216
221
|
const warnTimeout = 5000
|
|
217
222
|
if (warnTimeout < timeout + 1000) {
|
|
218
223
|
setTimeout(() => {
|
|
224
|
+
if (process.env.EXODUS_TEST_TIMERS_TRACK) timersDebug()
|
|
219
225
|
console.warn(`${prefix} ${warnTimeout}ms. Waiting for ${timeout}ms to pass to finish...`)
|
|
220
226
|
}, warnTimeout).unref()
|
|
221
227
|
}
|
package/src/jest.timers.js
CHANGED
|
@@ -65,14 +65,24 @@ export function runOnlyPendingTimers() {
|
|
|
65
65
|
return this
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// We have to tick in divisors of 1000, or e.g. 6s will mismatch a bit from 1s + 5s
|
|
69
|
+
const divisors1000 = [1000, 500, 250, 200, 125, 100, 50, 40, 25, 20, 10, 8, 5, 4, 2, 1]
|
|
70
|
+
|
|
71
|
+
function divisor1000(x) {
|
|
72
|
+
if (x <= 1) return 1 // fast path
|
|
73
|
+
for (const d of divisors1000) if (x >= d) return d
|
|
74
|
+
return 1 // unreachable
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
// We split this into multiple steps to run timers scheduled during the time we are running
|
|
69
78
|
function splitTime(time, min = 1000) {
|
|
70
79
|
const minSteps = Math.min(min, time) // usually just split e.g. 5 seconds into 1000 * 5ms
|
|
71
|
-
const step =
|
|
80
|
+
const step = divisor1000(Math.floor(time / minSteps))
|
|
72
81
|
const steps = Math.floor(time / step) // up to 2x higher than minSteps
|
|
73
82
|
const last = time - steps * step
|
|
74
83
|
// 1999 -> { step: 1, steps: 1999, last: 0 }
|
|
75
84
|
// 2001 -> { step: 2, steps: 1000, last: 1 }
|
|
85
|
+
// 6000 -> { step: 5, steps: 1200, last: 0 }
|
|
76
86
|
return { step, steps, last }
|
|
77
87
|
}
|
|
78
88
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const getStack = (fn) => {
|
|
2
|
+
const { stackTraceLimit } = Error
|
|
3
|
+
Error.stackTraceLimit = 50
|
|
4
|
+
const err = {}
|
|
5
|
+
Error.captureStackTrace(err, fn)
|
|
6
|
+
const { stack } = err
|
|
7
|
+
Error.stackTraceLimit = stackTraceLimit
|
|
8
|
+
return stack.replace(/^Error\n/u, '')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { setTimeout, setInterval, clearTimeout, clearInterval } = globalThis
|
|
12
|
+
const timersMap = new Map()
|
|
13
|
+
let timersMockEnabled = false
|
|
14
|
+
|
|
15
|
+
export const timersTrack = () => {
|
|
16
|
+
const mock = {
|
|
17
|
+
__proto__: null,
|
|
18
|
+
setTimeout(callback, ms, ...args) {
|
|
19
|
+
const wrapped = function (...brgs) {
|
|
20
|
+
timersMap.delete(value)
|
|
21
|
+
return callback.apply(this, brgs)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const stack = getStack(mock.setTimeout)
|
|
25
|
+
const value = setTimeout(wrapped, ms, ...args)
|
|
26
|
+
timersMap.set(value, { start: Date.now(), ms, stack, callback, args })
|
|
27
|
+
return value
|
|
28
|
+
},
|
|
29
|
+
setInterval(callback, ms, ...args) {
|
|
30
|
+
const stack = getStack(mock.setInterval)
|
|
31
|
+
const value = setInterval(callback, ms, ...args)
|
|
32
|
+
timersMap.set(value, { start: Date.now(), ms, stack, callback, args, repeating: true })
|
|
33
|
+
return value
|
|
34
|
+
},
|
|
35
|
+
clearTimeout(id) {
|
|
36
|
+
timersMap.delete(id)
|
|
37
|
+
return clearTimeout(id)
|
|
38
|
+
},
|
|
39
|
+
clearInterval(id) {
|
|
40
|
+
timersMap.delete(id)
|
|
41
|
+
return clearInterval(id)
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Object.assign(globalThis, mock)
|
|
46
|
+
timersMockEnabled = true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const timersList = () => {
|
|
50
|
+
if (!timersMockEnabled) throw new Error('Use exodus.mock.timersTrack() to enable timer tracking')
|
|
51
|
+
const now = Date.now()
|
|
52
|
+
// we don't provide raw timer values, so this is not misused to clear them
|
|
53
|
+
return [...timersMap.values()].map((entry) =>
|
|
54
|
+
entry.repeating ? entry : { ...entry, remaining: entry.ms + entry.start - now }
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const timersListFormatted = (comment = '') => {
|
|
59
|
+
const entries = timersList()
|
|
60
|
+
const head = `Timers ${comment}[at ${Date.now()}]: ${entries.length}`
|
|
61
|
+
if (entries.length === 0) return head
|
|
62
|
+
const first = (stack) => stack.split('\n')[0].replace(/^\s+at\s+/u, '') // doesn't have to be robust
|
|
63
|
+
const short = entries.map(
|
|
64
|
+
({ ms, repeating, remaining, stack }, i) =>
|
|
65
|
+
` #${i}: ${repeating ? `setInterval each ${ms}` : `setTimeout in ${remaining}`}ms from ${first(stack)}` // eslint-disable-line sonarjs/no-nested-template-literals
|
|
66
|
+
)
|
|
67
|
+
const full = entries.map(
|
|
68
|
+
({ start, ms, stack, callback, args, repeating }, i) =>
|
|
69
|
+
` #${i} [at ${start}]: ${repeating ? 'setInterval' : 'setTimeout'}(${callback}, ${ms}${['', ...args].join(', ')})\n${stack}`
|
|
70
|
+
)
|
|
71
|
+
const sep = (n) => '-'.repeat(n)
|
|
72
|
+
return `${sep(60)}\n${head}\n${short.join('\n')}\n ${sep(59)}\n${full.join('\n')}\n${sep(60)}`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const timersDebug = async (...times) => {
|
|
76
|
+
if (!timersMockEnabled) throw new Error('Use exodus.mock.timersTrack() to enable timer tracking')
|
|
77
|
+
console.log(timersListFormatted())
|
|
78
|
+
for (const time of times) {
|
|
79
|
+
await new Promise((resolve) => setTimeout(resolve, time))
|
|
80
|
+
console.log(timersListFormatted(`after additional ${time}ms `))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const timersAssert = () => {
|
|
85
|
+
if (!timersMockEnabled) throw new Error('Use exodus.mock.timersTrack() to enable timer tracking')
|
|
86
|
+
if (timersMap.size === 0) return
|
|
87
|
+
console.log(timersListFormatted())
|
|
88
|
+
throw new Error('timersAssert() failed: there are unfinished timers')
|
|
89
|
+
}
|