bun:test: honor the steps argument of advanceTimersToNextTimer

Closed
RO
9d ago
+65-2

Problem

  • jest.advanceTimersToNextTimer(3) fires one timer. Jest fires three. The function ignored its argument, and bun-types declared it without one, so a suite that migrates from Jest under-advances with no error.
  • One call also fired only the earliest timer even when several were due at the same time. Jest's implementation is clock.next() followed by clock.tick(0), so the whole batch at that time fires as one step.
  • The cause is advance_timers_to_next_timer in src/runtime/test_runner/timers/FakeTimers.rs, which called execute_next once.

Fix

  • steps defaults to 1 and counts down the way Jest's loop does (for (let i = steps; i > 0; i--)): NaN, 0 and a negative value fire nothing, the loop stops when the queue is empty.
  • After each fired timer, execute_until(now) fires every other timer due at the same fake time. A setTimeout(fn, 0) scheduled by a callback is due 1 ms later and stays for the next step, as in Jest.
  • packages/bun-types/test.d.ts adds the optional steps parameter.
  • Verified: test/js/bun/test/fake-timers/fake-timers.test.ts (three new tests: steps, same-time batch, non-positive steps). Stock bun fails all three. The rest of the file passes.

Background

  • jest.useFakeTimers() replaces the event loop timers with a heap ordered by due time and a fake clock. execute_next pops the earliest timer, moves the clock to its due time and runs it. execute_until(t) pops and runs every timer due at or before t.
  • Jest's advanceTimersToNextTimer(steps = 1) comes from @jest/fake-timers (modern implementation over @sinonjs/fake-timers).

[human-review] gate passed · iteration 0 · 3 files touched

fails on main (without fix)
ASAN without fix: 3 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/bun/test/fake-timers/fake-timers.test.ts
bun test v1.4.3 (f42e98025)

test/js/bun/test/fake-timers/fake-timers.test.ts:
(pass) fake timers [25.72ms]
(pass) advanceTimersToNextTimer > one setTimeout [7.69ms]
(pass) advanceTimersToNextTimer > setInterval [6.63ms]
(pass) advanceTimersToNextTimer > sorted timeouts [11.66ms]
(pass) advanceTimersToNextTimer > alternating intervals [8.69ms]
121 |     setTimeout(() => order.add("a"), 10);
122 |     setTimeout(() => order.add("b"), 20);
123 |     setTimeout(() => order.add("c"), 30);
124 |     setTimeout(() => order.add("d"), 40);
125 |     vi.advanceTimersToNextTimer(3);
126 |     expect(order.takeOrderMessages()).toEqual(["a", "b", "c"]);
                                            ^
error: expect(received).toEqual(expected)

  [
    "a",
-   "b",
-   "c",
  ]

- Expected  - 2
+ Received  + 0

      at <anonymous> (/workspace/bun/test/js/bun/test/fake-timers/fake-timers.test.ts:126:39)
(fail) advanceTimersToNextTimer > steps argument fires that many timers [8.93ms]
140 |       order.ad
... (truncated)

release without fix: 3 FAILED
bun test v1.4.3-canary.1 (2601c52f9)

test/js/bun/test/fake-timers/fake-timers.test.ts:
(pass) fake timers [16.14ms]
(pass) advanceTimersToNextTimer > one setTimeout [0.17ms]
(pass) advanceTimersToNextTimer > setInterval [0.12ms]
(pass) advanceTimersToNextTimer > sorted timeouts [0.11ms]
(pass) advanceTimersToNextTimer > alternating intervals [0.08ms]
121 |     setTimeout(() => order.add("a"), 10);
122 |     setTimeout(() => order.add("b"), 20);
123 |     setTimeout(() => order.add("c"), 30);
124 |     setTimeout(() => order.add("d"), 40);
125 |     vi.advanceTimersToNextTimer(3);
126 |     expect(order.takeOrderMessages()).toEqual(["a", "b", "c"]);
                                            ^
error: expect(received).toEqual(expected)

  [
    "a",
-   "b",
-   "c",
  ]

- Expected  - 2
+ Received  + 0

      at <anonymous> (/workspace/bun/test/js/bun/test/fake-timers/fake-timers.test.ts:126:39)
(fail) advanceTimersToNextTimer > steps argument fires that many timers [0.35ms]
140 |       order.add("c");
141 |       setTimeout(() => order.add("c0"), 0);
142 |     }, 10);
143 |     setTimeout(() => order.add("d"), 20);
144 |     vi.advanceTimersToNextTimer();
145 |   
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/bun/test/fake-timers/fake-timers.test.ts
bun test v1.4.3 (f42e98025)

test/js/bun/test/fake-timers/fake-timers.test.ts:
(pass) fake timers [27.18ms]
(pass) advanceTimersToNextTimer > one setTimeout [9.21ms]
(pass) advanceTimersToNextTimer > setInterval [7.45ms]
(pass) advanceTimersToNextTimer > sorted timeouts [13.83ms]
(pass) advanceTimersToNextTimer > alternating intervals [14.70ms]
(pass) advanceTimersToNextTimer > steps argument fires that many timers [17.74ms]
(pass) advanceTimersToNextTimer > one step fires every timer due at the same time [17.35ms]
(pass) advanceTimersToNextTimer > steps that count down to nothing fire no timer [4.98ms]
(pass) advanceTimersByTime > setInterval [6.05ms]
(pass) advanceTimersByTime > advanceTimersByTime(NaN) throws and does not move the clock [6.64ms]
(pass) advanceTimersByTime > advanceTimersByTime(-1) throws and does not move the clock [1.37ms]
(pass) advanceTimersByTime > advanceTimersByTime(Infinity) throws and does not move the clock [1.02ms]
(pass) advanceTimersByTime > advanceTimersBy
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 1832ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/9] gen generated_host_exports.rs
generated_host_exports.rs: 122 exports (host=5, lazy=10, generic=107, rust=0); 242 extern-C blocks audited
[1/9] cargo bun_runtime → libbun_runtime.a
   Compiling bun_core v0.0.0 (/workspace/bun/src/bun_core)
   Compiling bun_errno v0.0.0 (/workspace/bun/src/errno)
   Compiling bun_ptr v0.0.0 (/workspace/bun/src/ptr)
   Compiling bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
   Compiling bun_safety v0.0.0 (/workspace/bun/src/safety)
   Compiling bun_base64 v0.0.0 (/workspace/bun/src/base64)
   Compiling bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
   Compiling bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
   Compiling bun_zstd v0.0.0 (/workspace/bun/src/zstd)
   Compiling bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
   Compiling bun_brotli v0.0.0 (/workspace/bun/src/brotli)
   Compiling[0
... (truncated)
diff hotspot
packages/bun-types/test.d.ts                     |  2 +-
 src/runtime/test_runner/timers/FakeTimers.rs     | 21 ++++++++++-
 test/js/bun/test/fake-timers/fake-timers.test.ts | 44 ++++++++++++++++++++++++
 3 files changed, 65 insertions(+), 2 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                              reads  edits  tests
packages/bun-types/test.d.ts                          0      0      6
src/runtime/test_runner/timers/FakeTimers.rs          0      0      6
test/js/bun/test/fake-timers/fake-timers.test.ts      0      0      5