Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
117 changes: 49 additions & 68 deletions src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1779,9 +1779,9 @@ unsafe fn retroactively_report_discovered_tests(
agent: *mut bun_jsc::debugger::TestReporterHandle,
next_test_id: i32,
) -> i32 {
use crate::test_runner::bun_test::{DescribeScope, Phase, TestScheduleEntry};
use crate::test_runner::bun_test::{Phase, TestScheduleEntry};
use crate::test_runner::jest::Jest;
use bun_jsc::debugger::{TestReporterHandle, TestType};
use bun_jsc::debugger::TestType;

let Some(runner) = Jest::runner() else {
return next_test_id;
Expand All @@ -1808,77 +1808,58 @@ unsafe fn retroactively_report_discovered_tests(

let mut max_id: i32 = next_test_id;

// Recursively report all discovered tests starting from root scope.
retroactively_report_scope(
agent,
&mut active_file.collection.root_scope,
-1,
&mut max_id,
&source_url,
);

return max_id;

fn retroactively_report_scope(
agent: *mut TestReporterHandle,
scope: &mut DescribeScope,
parent_id: i32,
max_id: &mut i32,
source_url: &bun_core::String,
) {
for entry in scope.entries.iter_mut() {
match entry {
TestScheduleEntry::Describe(describe) => {
if describe.base.test_id_for_debugger == 0 {
*max_id += 1;
let test_id = *max_id;
// Assign the ID so start/end events will fire during
// execution.
describe.base.test_id_for_debugger = test_id;
let name = bun_core::String::from_bytes(
describe.base.name.as_deref().unwrap_or(b"(unnamed)"),
);
// SAFETY: `agent` is a live C++ handle (fn contract).
unsafe { &mut *agent }.report_test_found_with_location(
test_id,
&name,
TestType::Describe,
parent_id,
source_url,
describe.base.line_no as i32,
);
// Recursively report children with this describe as
// parent.
retroactively_report_scope(agent, describe, test_id, max_id, source_url);
} else {
// Already has ID, just recurse with existing ID as
// parent.
let existing = describe.base.test_id_for_debugger;
retroactively_report_scope(agent, describe, existing, max_id, source_url);
}
// Explicit stack, not recursion: describe() nesting depth is unbounded user input.
let mut open: Vec<(core::slice::IterMut<'_, TestScheduleEntry>, i32)> =
vec![(active_file.collection.root_scope.entries.iter_mut(), -1)];
while let Some((children, parent_id)) = open.last_mut() {
let parent_id = *parent_id;
let Some(entry) = children.next() else {
open.pop();
continue;
};
match entry {
TestScheduleEntry::Describe(describe) => {
if describe.base.test_id_for_debugger == 0 {
max_id += 1;
// Assign the ID so start/end events will fire during execution.
describe.base.test_id_for_debugger = max_id;
let name = bun_core::String::from_bytes(
describe.base.name.as_deref().unwrap_or(b"(unnamed)"),
);
// SAFETY: `agent` is a live C++ handle (fn contract).
unsafe { &mut *agent }.report_test_found_with_location(
max_id,
&name,
TestType::Describe,
parent_id,
&source_url,
describe.base.line_no as i32,
);
}
TestScheduleEntry::TestCallback(test_entry) => {
if test_entry.base.test_id_for_debugger == 0 {
*max_id += 1;
let test_id = *max_id;
test_entry.base.test_id_for_debugger = test_id;
let name = bun_core::String::from_bytes(
test_entry.base.name.as_deref().unwrap_or(b"(unnamed)"),
);
// SAFETY: `agent` is a live C++ handle (fn contract).
unsafe { &mut *agent }.report_test_found_with_location(
test_id,
&name,
TestType::Test,
parent_id,
source_url,
test_entry.base.line_no as i32,
);
}
let describe_id = describe.base.test_id_for_debugger;
open.push((describe.entries.iter_mut(), describe_id));
}
TestScheduleEntry::TestCallback(test_entry) => {
if test_entry.base.test_id_for_debugger == 0 {
max_id += 1;
test_entry.base.test_id_for_debugger = max_id;
let name = bun_core::String::from_bytes(
test_entry.base.name.as_deref().unwrap_or(b"(unnamed)"),
);
// SAFETY: `agent` is a live C++ handle (fn contract).
unsafe { &mut *agent }.report_test_found_with_location(
max_id,
&name,
TestType::Test,
parent_id,
&source_url,
test_entry.base.line_no as i32,
);
}
}
}
}
max_id
}

// ════════════════════════════════════════════════════════════════════════════
Expand Down
80 changes: 56 additions & 24 deletions src/runtime/test_runner/Order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,6 @@ impl Order {
}
// `deinit` only freed `groups` / `sequences` — handled by Drop on Vec; no impl Drop needed.

pub(crate) fn generate_order_sub(&mut self, current: &mut TestScheduleEntry) -> JsResult<()> {
match current {
TestScheduleEntry::Describe(describe) => self.generate_order_describe(describe)?,
TestScheduleEntry::TestCallback(test_callback) => {
self.generate_order_test(NonNull::from(&mut **test_callback))?
}
}
Ok(())
}

pub(crate) fn generate_all_order(&mut self, entries: &[Box<ExecutionEntry>]) -> JsResult<AllOrderResult> {
let start = self.groups.len();
for entry_box in entries.iter() {
Expand Down Expand Up @@ -78,9 +68,42 @@ impl Order {
Ok(AllOrderResult { start, end })
}

pub(crate) fn generate_order_describe(&mut self, current: &mut DescribeScope) -> JsResult<()> {
/// Explicit stack, not recursion: `describe()` nesting depth is unbounded user input.
pub(crate) fn generate_order_describe(&mut self, root: &mut DescribeScope) -> JsResult<()> {
let mut open: Vec<OpenDescribe<'_>> = Vec::new();
if let Some(frame) = self.enter_describe(root)? {
open.push(frame);
}
while let Some(frame) = open.last_mut() {
let scope_only = frame.only;
let Some(entry) = frame.children.next() else {
let frame = open.pop().expect("non-empty");
self.exit_describe(&frame)?;
continue;
};
if scope_only == Only::Contains && entry.base().only == Only::No {
continue;
}
match entry {
TestScheduleEntry::Describe(describe) => {
if let Some(child) = self.enter_describe(describe)? {
open.push(child);
}
}
TestScheduleEntry::TestCallback(test_callback) => {
self.generate_order_test(NonNull::from(&mut **test_callback))?
}
}
}
Ok(())
}

fn enter_describe<'a>(
&mut self,
current: &'a mut DescribeScope,
) -> JsResult<Option<OpenDescribe<'a>>> {
if current.failed {
return Ok(()); // do not schedule any tests in a failed describe scope
return Ok(None); // do not schedule any tests in a failed describe scope
}
let use_hooks = self.cfg.always_use_hooks || current.base.has_callback;

Expand All @@ -96,22 +119,22 @@ impl Order {
shuffle_with_index(random, &mut current.entries);
}

// gather children
// reshaped for borrowck — iterate by index since generate_order_sub borrows &mut self.
let scope_only = current.base.only;
for i in 0..current.entries.len() {
if scope_only == Only::Contains && current.entries[i].base().only == Only::No {
continue;
}
self.generate_order_sub(&mut current.entries[i])?;
}
Ok(Some(OpenDescribe {
children: current.entries.iter_mut(),
only: current.base.only,
use_hooks,
beforeall_order,
after_all: &current.after_all,
}))
}

fn exit_describe(&mut self, frame: &OpenDescribe<'_>) -> JsResult<()> {
// update skip_to values for beforeAll to skip to the first afterAll
beforeall_order.set_failure_skip_to(self);
frame.beforeall_order.set_failure_skip_to(self);

// gather afterAll
let afterall_order: AllOrderResult = if use_hooks {
self.generate_all_order(&current.after_all)?
let afterall_order: AllOrderResult = if frame.use_hooks {
self.generate_all_order(frame.after_all)?
} else {
AllOrderResult::EMPTY
};
Expand Down Expand Up @@ -242,6 +265,15 @@ impl Order {
}
}

/// A describe scope whose children `generate_order_describe` is still scheduling.
struct OpenDescribe<'a> {
children: core::slice::IterMut<'a, TestScheduleEntry>,
only: Only,
use_hooks: bool,
beforeall_order: AllOrderResult,
after_all: &'a [Box<ExecutionEntry>],
}

pub(crate) struct AllOrderResult {
pub(crate) start: usize,
pub(crate) end: usize,
Expand Down
14 changes: 13 additions & 1 deletion src/runtime/test_runner/bun_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1743,6 +1743,19 @@ pub(crate) struct DescribeScope {
pub(crate) failed: bool,
}

impl Drop for DescribeScope {
/// Iterative: the default drop recurses once per nested `describe()`, an unbounded depth.
fn drop(&mut self) {
let mut pending: Vec<TestScheduleEntry> = core::mem::take(&mut self.entries);
while let Some(entry) = pending.pop() {
if let TestScheduleEntry::Describe(mut scope) = entry {
pending.append(&mut scope.entries);
// `scope.entries` is empty now, so dropping `scope` here does not recurse.
}
}
}
}

impl DescribeScope {
pub(crate) fn create(base: BaseScope) -> Box<DescribeScope> {
Box::new(DescribeScope {
Expand All @@ -1755,7 +1768,6 @@ impl DescribeScope {
failed: false,
})
}
// destroy → Drop on Box<DescribeScope>; all fields own their contents.

fn mark_contains_only(&mut self) {
let mut target: Option<*mut DescribeScope> = Some(std::ptr::from_mut(self));
Expand Down
Loading
Loading