Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/runtime/test_runner/timers/FakeTimers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,10 +316,41 @@
Ok(frame.this())
}

/// RAII guard: set `vm.suppress_microtask_drain = true` for its lifetime,
/// restore the prior value on drop.
///
/// Jest's sync fake-timer APIs (`advanceTimersByTime`, `runAllTimers`, ...)
/// fire timer callbacks without flushing microtasks between or after them;
/// microtasks queued inside a callback run at the test's next real `await`.
/// The `*Async` variants are what opt into per-timer flushing. Without this
/// guard each fired timer's `event_loop().exit()` sees depth 1 (the test body
/// isn't itself inside an enter/exit pair) and drains eagerly.
struct SuppressMicrotaskDrain {
vm: &'static bun_jsc::virtual_machine::VirtualMachine,
prev: bool,
}

impl SuppressMicrotaskDrain {
#[inline]
fn new(global: &JSGlobalObject) -> Self {
let vm = global.bun_vm();
let prev = vm.suppress_microtask_drain.replace(true);
Self { vm, prev }
}
}

impl Drop for SuppressMicrotaskDrain {
#[inline]
fn drop(&mut self) {
self.vm.suppress_microtask_drain.set(self.prev);
}
}

Check warning on line 347 in src/runtime/test_runner/timers/FakeTimers.rs

View check run for this annotation

Claude / Claude Code Review

Duplicate SuppressMicrotaskDrain RAII guard

nit: this `SuppressMicrotaskDrain` guard is the third copy of the same pattern — an identically-named RAII guard already exists in `src/event_loop/SpawnSyncEventLoop.rs:65-86`, and the same replace/set is open-coded inline at `src/jsc/event_loop.rs:672-678`. Consider defining the guard once as a public helper on `VirtualMachine` in `bun_jsc` (the type that owns `suppress_microtask_drain`) and reusing it here and at the inline `event_loop.rs` site.
Comment thread
robobun marked this conversation as resolved.
Resolved

#[bun_jsc::host_fn]
fn advance_timers_to_next_timer(global: &JSGlobalObject, frame: &CallFrame) -> JsResult<JSValue> {
error_unless_fake_timers(global)?;

let _suppress = SuppressMicrotaskDrain::new(global);
let _ = FakeTimers::execute_next(global);

Ok(frame.this())
Expand Down Expand Up @@ -354,6 +385,7 @@
let effective_advance = if arg_number == 0.0 { 1.0 } else { arg_number };
let target = current.add_ms_float(effective_advance);

let _suppress = SuppressMicrotaskDrain::new(global);
FakeTimers::execute_until(global, target);
CURRENT_TIME.set(global, &target, None);

Expand All @@ -364,6 +396,7 @@
fn run_only_pending_timers(global: &JSGlobalObject, frame: &CallFrame) -> JsResult<JSValue> {
error_unless_fake_timers(global)?;

let _suppress = SuppressMicrotaskDrain::new(global);
FakeTimers::execute_only_pending_timers(global);

Ok(frame.this())
Expand All @@ -373,6 +406,7 @@
fn run_all_timers(global: &JSGlobalObject, frame: &CallFrame) -> JsResult<JSValue> {
error_unless_fake_timers(global)?;

let _suppress = SuppressMicrotaskDrain::new(global);
FakeTimers::execute_all_timers(global);

Ok(frame.this())
Expand Down
90 changes: 90 additions & 0 deletions test/js/bun/test/fake-timers/fake-timers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,96 @@ describe("advanceTimersToNextTimer", () => {
vi.useRealTimers();
});
});
describe("sync APIs do not drain the microtask queue (jest parity)", () => {
// Jest's sync fake-timer APIs fire callbacks without flushing microtasks; a
// microtask scheduled inside a callback runs at the test's next real `await`,
// not before advanceTimersByTime() returns. The *Async variants opt into
// per-timer flushing.
test("advanceTimersByTime", async () => {
vi.useFakeTimers();
const log: string[] = [];
setTimeout(() => {
log.push("T1");
Promise.resolve().then(() => log.push("P1"));
}, 10);
vi.advanceTimersByTime(10);
log.push("after-advance");
await Promise.resolve();
log.push("after-await");
expect(log).toEqual(["T1", "after-advance", "P1", "after-await"]);
});

test("advanceTimersByTime: two timers, microtasks batch after", async () => {
vi.useFakeTimers();
const log: string[] = [];
setTimeout(() => {
log.push("T1");
Promise.resolve().then(() => log.push("P1"));
}, 5);
setTimeout(() => {
log.push("T2");
Promise.resolve().then(() => log.push("P2"));
}, 10);
vi.advanceTimersByTime(10);
log.push("after-advance");
await Promise.resolve();
expect(log).toEqual(["T1", "T2", "after-advance", "P1", "P2"]);
});

test("advanceTimersToNextTimer", async () => {
vi.useFakeTimers();
const log: string[] = [];
setTimeout(() => {
log.push("T1");
Promise.resolve().then(() => log.push("P1"));
}, 10);
vi.advanceTimersToNextTimer();
log.push("after-advance");
await Promise.resolve();
expect(log).toEqual(["T1", "after-advance", "P1"]);
});

test("runAllTimers", async () => {
vi.useFakeTimers();
const log: string[] = [];
setTimeout(() => {
log.push("T1");
Promise.resolve().then(() => log.push("P1"));
}, 5);
setTimeout(() => {
log.push("T2");
Promise.resolve().then(() => log.push("P2"));
}, 10);
vi.runAllTimers();
log.push("after");
await Promise.resolve();
expect(log).toEqual(["T1", "T2", "after", "P1", "P2"]);
});

test("runOnlyPendingTimers", async () => {
vi.useFakeTimers();
const log: string[] = [];
setTimeout(() => {
log.push("T1");
Promise.resolve().then(() => log.push("P1"));
}, 5);
vi.runOnlyPendingTimers();
log.push("after");
await Promise.resolve();
expect(log).toEqual(["T1", "after", "P1"]);
});

test("microtask drain resumes after advanceTimersByTime returns", async () => {
vi.useFakeTimers();
setTimeout(() => {}, 10);
vi.advanceTimersByTime(10);
let ran = false;
queueMicrotask(() => (ran = true));
await Promise.resolve();
expect(ran).toBe(true);
});
});

describe("advanceTimersByTime", () => {
test("setInterval", () => {
vi.useFakeTimers();
Expand Down
Loading