@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 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.87",
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
- fetchRecord,
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
- expect = require('expect').expect
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} fail`)(x)[name](...args)
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
  }
@@ -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 = Number(Math.floor(time / minSteps).toPrecision(1))
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
+ }