-
-
Notifications
You must be signed in to change notification settings - Fork 161
fix(runtime): format nested node:test reports #9916
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| Preserve nested suite structure in `node:test` spec reports and indent nested | ||
| spec and TAP output consistently with Node. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,8 +36,12 @@ pub(crate) extern "C" fn thunk_reporter_lcov(_closure: *const ClosureHeader, sou | |
| } | ||
|
|
||
| fn reporter_transform(kind: i32) -> f64 { | ||
| let transform = make_closure(reporter_transform_chunk as *const u8, 3, 1); | ||
| let captures = if kind == REPORTER_SPEC { 2 } else { 1 }; | ||
| let transform = make_closure(reporter_transform_chunk as *const u8, 3, captures); | ||
| js_closure_set_capture_f64(transform, 0, kind as f64); | ||
| if kind == REPORTER_SPEC { | ||
| js_closure_set_capture_f64(transform, 1, undefined_value()); | ||
| } | ||
| let opts = js_object_alloc(0, 1); | ||
| set_field(opts, "transform", boxed_ptr(transform)); | ||
| crate::node_stream::js_node_stream_transform_new(boxed_ptr(opts)) | ||
|
|
@@ -50,7 +54,26 @@ extern "C" fn reporter_transform_chunk( | |
| callback: f64, | ||
| ) -> f64 { | ||
| let kind = js_closure_get_capture_f64(closure, 0) as i32; | ||
| let output = format_reporter_event(kind, chunk); | ||
| let scope = crate::gc::RuntimeHandleScope::new(); | ||
| let closure_handle = scope.root_raw_mut_ptr(closure as *mut ClosureHeader); | ||
| let chunk_handle = scope.root_nanbox_f64(chunk); | ||
| let stack_handle = (kind == REPORTER_SPEC) | ||
| .then(|| scope.root_nanbox_f64(js_closure_get_capture_f64(closure, 1))); | ||
| let mut starts = stack_handle | ||
| .as_ref() | ||
| .and_then(|handle| array_values(handle.get_nanbox_f64())) | ||
| .unwrap_or_default(); | ||
| let output = format_reporter_event(kind, chunk_handle.get_nanbox_f64(), &mut starts); | ||
|
|
||
| if kind == REPORTER_SPEC { | ||
| let mut stack = crate::array::js_array_alloc(starts.len() as u32); | ||
| for start in starts { | ||
| stack = crate::array::js_array_push_f64(stack, start); | ||
| } | ||
| closure_handle.with_mut_ptr(|closure: *mut ClosureHeader| { | ||
| js_closure_set_capture_f64(closure, 1, boxed_ptr(stack)); | ||
| }); | ||
| } | ||
| if !output.is_empty() { | ||
| let this = crate::object::js_implicit_this_get(); | ||
| let handle = (this.to_bits() & POINTER_MASK) as i64; | ||
|
|
@@ -88,6 +111,18 @@ fn event_data(event: f64) -> f64 { | |
| object_property(event, b"data").unwrap_or(undefined_value()) | ||
| } | ||
|
|
||
| fn event_nesting(data: f64) -> usize { | ||
| object_property(data, b"nesting") | ||
| .map(|value| JSValue::from_bits(value.to_bits()).to_number()) | ||
| .filter(|value| value.is_finite() && *value > 0.0) | ||
| .map(|value| value as usize) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🤖 get_repo_knowledge executed:
Length of output: 24907 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- diff ---'
git diff -- crates/perry-runtime/src/node_submodules/test_reporters.rs
printf '%s\n' '--- file excerpt ---'
sed -n '1,180p' crates/perry-runtime/src/node_submodules/test_reporters.rs
printf '%s\n' '--- related symbols ---'
rg -n -C 4 'event_nesting|nesting|test_reporters|boxed_ptr|RuntimeHandleScope' crates/perry-runtime/src crates/perry-stdlib/srcRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- relevant line ranges ---'
nl -ba crates/perry-runtime/src/node_submodules/test_reporters.rs | sed -n '45,135p'
printf '%s\n' '--- numeric conversion and repeat contracts in repository ---'
rg -n -C 3 'as usize|saturating_mul|"\s*"\.repeat|repeat\(' crates/perry-runtime crates/perry-stdlibRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: #!/bin/bash
set -eu
nl -ba crates/perry-runtime/src/node_submodules/test_reporters.rs | sed -n '45,135p'
printf '%s\n' '--- related definitions and callers ---'
rg -n -C 5 'event_nesting|nesting|boxed_ptr|RuntimeHandleScope' crates/perry-runtime/srcRepository: PerryTS/perry Length of output: 50372 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- reviewed file ---'
nl -ba crates/perry-runtime/src/node_submodules/test_reporters.rs | sed -n '1,145p'
printf '%s\n' '--- focused symbol matches ---'
rg -n -C 5 --glob '*.rs' 'event_nesting|nesting' crates/perry-runtime/src/node_submodules crates/perry-runtime/src/node_submodules/test_reporters.rs
printf '%s\n' '--- focused diff ---'
git diff --unified=20 -- crates/perry-runtime/src/node_submodules/test_reporters.rsRepository: PerryTS/perry Length of output: 4008 🌐 Web query:
💡 Result: In Rust, the behavior of casting a floating-point number (f64) to an integer type (like usize) using the 'as' keyword is defined as follows: values exceeding the target type's range saturate to the type's minimum or maximum value, while NaN is mapped to 0 [1][2]. This saturation behavior was formalized in Rust 1.45 [1]. Regarding str::repeat, this method will panic if the required capacity for the resulting string exceeds the capacity limits of the system [3]. Historically, a buffer overflow vulnerability existed in str::repeat (CVE-2018-1000810) in versions 1.26.0 through 1.29.0 because it failed to properly check for integer overflow when calculating the required capacity [4][5]. This was fixed in Rust 1.29.1 and subsequent versions by introducing a deterministic panic when an overflow occurs [4][5][6]. The function is documented to panic if the resulting capacity would overflow [3]. Because 'as' casts perform saturation rather than providing error handling or overflow detection, they are generally discouraged for fallible conversions where precision or range is critical [7][1]. For safe conversions, you should manually validate the range of the float (e.g., check for NaN and bounds) before performing the cast, or use other methods such as TryFrom/TryInto if available for the specific types [1]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
sed -n '126,220p' crates/perry-runtime/src/node_submodules/test_reporters.rs
printf '%s\n' '--- event construction/callers ---'
rg -n -C 4 'format_reporter_events|event_indent|reporter event|nesting' crates/perry-runtime/src/node_submodules/test_reporters.rsRepository: PerryTS/perry Length of output: 7033 <|diff|>Cap A finite 🤖 Prompt for AI Agents |
||
| .unwrap_or(0) | ||
| } | ||
|
|
||
| fn event_indent(data: f64, width: usize) -> String { | ||
| " ".repeat(event_nesting(data).saturating_mul(width)) | ||
| } | ||
|
|
||
| fn format_reporter_events(kind: i32, events: &[f64]) -> String { | ||
| if kind == REPORTER_LCOV { | ||
| return String::new(); | ||
|
|
@@ -98,8 +133,9 @@ fn format_reporter_events(kind: i32, events: &[f64]) -> String { | |
| } else if kind == REPORTER_JUNIT { | ||
| out.push_str("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<testsuites>\n"); | ||
| } | ||
| let mut starts = Vec::new(); | ||
| for &event in events { | ||
| out.push_str(&format_reporter_event(kind, event)); | ||
| out.push_str(&format_reporter_event(kind, event, &mut starts)); | ||
| } | ||
| if kind == REPORTER_DOT && !out.is_empty() && !out.ends_with('\n') { | ||
| out.push('\n'); | ||
|
|
@@ -110,34 +146,51 @@ fn format_reporter_events(kind: i32, events: &[f64]) -> String { | |
| out | ||
| } | ||
|
|
||
| fn format_reporter_event(kind: i32, event: f64) -> String { | ||
| fn format_reporter_event(kind: i32, event: f64, starts: &mut Vec<f64>) -> String { | ||
| let Some(typ) = event_type(event) else { | ||
| return String::new(); | ||
| }; | ||
| let data = event_data(event); | ||
| match kind { | ||
| REPORTER_SPEC => match typ.as_str() { | ||
| "test:pass" => object_string(data, b"name") | ||
| .map(|name| format!("✔ {name}\n")) | ||
| .unwrap_or_default(), | ||
| "test:start" => { | ||
| starts.push(data); | ||
| String::new() | ||
| } | ||
| "test:pass" => { | ||
| starts.pop(); | ||
| let mut output = String::new(); | ||
| for parent in starts.drain(..) { | ||
| if let Some(name) = object_string(parent, b"name") { | ||
| output.push_str(&format!("{}▶ {name}\n", event_indent(parent, 2))); | ||
| } | ||
| } | ||
| if let Some(name) = object_string(data, b"name") { | ||
| output.push_str(&format!("{}✔ {name}\n", event_indent(data, 2))); | ||
| } | ||
| output | ||
| } | ||
| "test:diagnostic" => object_string(data, b"message") | ||
| .map(|message| format!("ℹ {message}\n")) | ||
| .map(|message| format!("{}ℹ {message}\n", event_indent(data, 2))) | ||
| .unwrap_or_default(), | ||
| _ => String::new(), | ||
| }, | ||
| REPORTER_TAP => match typ.as_str() { | ||
| "test:start" => object_string(data, b"name") | ||
| .map(|name| format!("# Subtest: {name}\n")) | ||
| .map(|name| format!("{}# Subtest: {name}\n", event_indent(data, 4))) | ||
| .unwrap_or_default(), | ||
| "test:pass" => { | ||
| let name = object_string(data, b"name").unwrap_or_default(); | ||
| let indent = event_indent(data, 4); | ||
| let detail_type = object_property(data, b"details") | ||
| .and_then(|details| object_string(details, b"type")) | ||
| .unwrap_or_else(|| "test".to_string()); | ||
| format!("ok undefined - {name}\n ---\n type: '{detail_type}'\n ...\n") | ||
| format!( | ||
| "{indent}ok undefined - {name}\n{indent} ---\n{indent} type: '{detail_type}'\n{indent} ...\n" | ||
| ) | ||
| } | ||
| "test:diagnostic" => object_string(data, b"message") | ||
| .map(|message| format!("# {message}\n")) | ||
| .map(|message| format!("{}# {message}\n", event_indent(data, 4))) | ||
| .unwrap_or_default(), | ||
| _ => String::new(), | ||
| }, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Reload
closurethroughclosure_handlebefore reading capture 1.Line 61 reads the original raw
closurepointer after the scope rootsclosureandchunk. A moving GC can invalidate that raw pointer. Read the capture insideclosure_handle.with_ptr(...)before rooting the capture value. Otherwise, GC stress can read an invalid closure address and crash the reporter.Based on learnings: Rust stack locals are not GC roots, and rooted values must be reloaded after an allocating operation.
🤖 Prompt for AI Agents
Source: Learnings