Skip to content
Merged
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
77 changes: 43 additions & 34 deletions src/runtime/api/bun/js_bun_spawn_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@
args: JSValue,
secondary_args_value: Option<JSValue>,
) -> JsResult<JSValue> {
spawn_maybe_sync::<false>(global_this, args, secondary_args_value)
spawn_maybe_sync::<false>(global_this, args, secondary_args_value, &mut None)
}

/// Bun.spawnSync() calls this.
Expand All @@ -298,13 +298,39 @@
args: JSValue,
secondary_args_value: Option<JSValue>,
) -> JsResult<JSValue> {
spawn_maybe_sync::<true>(global_this, args, secondary_args_value)
let mut bun_test_deadline: Option<Timespec> = None;
let result = spawn_maybe_sync::<true>(
global_this,
args,
secondary_args_value,
&mut bun_test_deadline,
);
// A bun:test deadline that passed while the isolated loop was blocking is reported only now: the loop is torn down and the child reaped, so the runner's callback re-enters nothing that is mid-flight. With an exception pending (spawn failure, termination) the file timer is left armed and reports it from the main loop instead.
if let Some(deadline) = bun_test_deadline
&& result.is_ok()
&& !global_this.has_exception()
&& let Some(runner) = crate::test_runner::jest::Jest::runner()
&& let Some(active_file) = runner.bun_test_root.active_file.clone()
{
let vm = global_this.bun_vm().as_mut();
runner.remove_active_timeout(vm);
crate::test_runner::bun_test::BunTest::bun_test_timeout_callback(
&active_file,
&deadline,
vm,
);
if global_this.has_exception() {
return Ok(JSValue::ZERO);
}
}
result
}

fn spawn_maybe_sync<const IS_SYNC: bool>(
global_this: &JSGlobalObject,
args_: JSValue,
secondary_args_value: Option<JSValue>,
bun_test_deadline: &mut Option<Timespec>,
) -> JsResult<JSValue> {
if IS_SYNC {
// We skip this on Windows due to test failures.
Expand Down Expand Up @@ -1977,39 +2003,22 @@
// Support bun:test timeouts AND spawnSync() timeout.
// There is a scenario where inside of spawnSync() a totally
// different test fails, and that SHOULD be okay.
if has_bun_test_timeout {
if bun_test_timeout.order(&now) == core::cmp::Ordering::Less {
bun_test_fired = true;
let mut active_file_strong = crate::test_runner::jest::Jest::runner()
.unwrap()
.bun_test_root
.active_file
// TODO: add a .cloneNonOptional()?
.clone();

let taken_active_file = active_file_strong.take().unwrap();

// SAFETY: jsc_vm_ptr is the live thread VM.
crate::test_runner::jest::Jest::runner()
.unwrap()
.remove_active_timeout(unsafe { &mut *jsc_vm_ptr });

// This might internally call `kill(2)` on this
// spawnSync process. Even if we do that, we still
// need to reap the process. So we may go through
// the event loop again, but it should wake up
// ~instantly so we can drain the events.
crate::test_runner::bun_test::BunTest::bun_test_timeout_callback(
&taken_active_file,
&absolute_timespec,
// SAFETY: jsc_vm_ptr is the live thread VM.
unsafe { &*jsc_vm_ptr },
);
// The direct child may already be reaped (and
// gone from the auto-killer), so kill it here too.
let _ = subprocess.try_kill(subprocess.kill_signal);
// active_file_strong / taken_active_file drop here (was `defer .deinit()`).
// Kill the dangling processes now so this loop can drain, but leave the runner's timeout callback to `spawn_sync`: it re-enters the test runner and must not run while this isolated loop is still active.
if has_bun_test_timeout
&& bun_test_timeout.order(&now) == core::cmp::Ordering::Less
{
bun_test_fired = true;
*bun_test_deadline = Some(absolute_timespec);
if let Some(active_file) = crate::test_runner::jest::Jest::runner()
.unwrap()
.bun_test_root
.active_file
.clone()
{
// `Err` means the exception is pending on `global_this`; the check after this loop propagates it.
let _ = active_file.get().execution.handle_timeout(global_this);

Check warning on line 2019 in src/runtime/api/bun/js_bun_spawn_bindings.rs

View check run for this annotation

Claude / Claude Code Review

handle_timeout runs twice on the deferred spawnSync bun:test timeout path

`Execution::handle_timeout` is not kill-only — after `auto_killer.kill()` it unconditionally does `add_result(RefDataValue::Start)` (Execution.rs:332) — so calling it here *and* again inside the deferred `bun_test_timeout_callback` (bun_test.rs:889) enqueues `Start` twice and writes to `result_queue` from inside the isolated loop, which is exactly the runner state the comment above says must be left to `spawn_sync`. The extra `Start` looks idempotent in `step_group` and the second `auto_killer.k
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
}
let _ = subprocess.try_kill(subprocess.kill_signal);
}
}
}
Expand Down
Loading