diff --git a/.github/workflows/gc-native-roots.yml b/.github/workflows/gc-native-roots.yml index d6f7993cd8..5532fc6705 100644 --- a/.github/workflows/gc-native-roots.yml +++ b/.github/workflows/gc-native-roots.yml @@ -673,7 +673,9 @@ jobs: export PERRY_RUNTIME_DIR="$PWD/target/perry-dev" export PERRY_NO_AUTO_OPTIMIZE=1 unset PERRY_LLVM_OPT PERRY_LLVM_CLANG - export LLVM_SYS_221_PREFIX="$(brew --prefix llvm)" + # Keep LLVM_SYS_221_PREFIX from setup-llvm22. The stock-toolchain + # assertion removes the external opt/clang overrides; the in-process + # backend itself still has to link the LLVM 22 ABI used by llvm-sys. export RUSTFLAGS="-C force-frame-pointers=yes -C force-unwind-tables=yes" cargo build --profile perry-dev -p perry -p perry-runtime-static \ -p perry-stdlib-static --features perry-codegen/llvm-inprocess diff --git a/changelog.d/9916-test-reporter-nested.md b/changelog.d/9916-test-reporter-nested.md new file mode 100644 index 0000000000..caa77bbd56 --- /dev/null +++ b/changelog.d/9916-test-reporter-nested.md @@ -0,0 +1,2 @@ +Preserve nested suite structure in `node:test` spec reports and indent nested +spec and TAP output consistently with Node. diff --git a/changelog.d/9920-gc-native-roots-platforms.md b/changelog.d/9920-gc-native-roots-platforms.md new file mode 100644 index 0000000000..6a9b6a2a58 --- /dev/null +++ b/changelog.d/9920-gc-native-roots-platforms.md @@ -0,0 +1,5 @@ +### Fixed + +- Restore the scheduled native-roots gate on ARM Linux and macOS by handling + libyaml's target-specific `c_char` signedness and retaining its configured + LLVM 22 prefix for the in-process backend. diff --git a/crates/perry-runtime/src/bun_compat/cli_utils.rs b/crates/perry-runtime/src/bun_compat/cli_utils.rs index 3e75b4a25d..88a4f44d0c 100644 --- a/crates/perry-runtime/src/bun_compat/cli_utils.rs +++ b/crates/perry-runtime/src/bun_compat/cli_utils.rs @@ -947,7 +947,7 @@ fn yaml_parse(input: f64) -> f64 { let problem = if parser.problem.is_null() { "invalid YAML".to_string() } else { - std::ffi::CStr::from_ptr(parser.problem) + std::ffi::CStr::from_ptr(parser.problem.cast::()) .to_string_lossy() .into_owned() }; diff --git a/crates/perry-runtime/src/node_submodules/test_reporters.rs b/crates/perry-runtime/src/node_submodules/test_reporters.rs index 3cab2b3b85..1d647dbd73 100644 --- a/crates/perry-runtime/src/node_submodules/test_reporters.rs +++ b/crates/perry-runtime/src/node_submodules/test_reporters.rs @@ -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; @@ -130,6 +153,18 @@ fn spec_directive_suffix(data: f64) -> String { .unwrap_or_default() } +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) + .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(); @@ -140,8 +175,9 @@ fn format_reporter_events(kind: i32, events: &[f64]) -> String { } else if kind == REPORTER_JUNIT { out.push_str("\n\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'); @@ -152,45 +188,62 @@ 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) -> 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| { + "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!("{}\u{25b6} {name}\n", event_indent(parent, 2))); + } + } + if let Some(name) = object_string(data, b"name") { let marker = if reporter_directive(data).is_some_and(|(label, _)| label == "SKIP") { - "﹣" + "\u{fe63}" } else { - "✔" + "\u{2714}" }; - format!("{marker} {name}{}\n", spec_directive_suffix(data)) - }) - .unwrap_or_default(), + output.push_str(&format!( + "{}{marker} {name}{}\n", + event_indent(data, 2), + spec_directive_suffix(data) + )); + } + 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", - directive_suffix(data) + "{indent}ok undefined - {name}{}\n{indent} ---\n{indent} type: '{detail_type}'\n{indent} ...\n", + directive_suffix(data) ) } "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(), }, diff --git a/test-parity/node-suite/test/reporters/nested.ts b/test-parity/node-suite/test/reporters/nested.ts index e5b74ed6bb..0eeb868297 100644 --- a/test-parity/node-suite/test/reporters/nested.ts +++ b/test-parity/node-suite/test/reporters/nested.ts @@ -14,9 +14,21 @@ const events = [ }, ]; -async function collect(name: string, reporter: any) { +async function collect(name: string, reporter: any): Promise { let output = ""; - for await (const chunk of reporter(Readable.from(events))) output += String(chunk); + const result = reporter(Readable.from(events)); + if (typeof result.write === "function") { + const transform = reporter(); + transform.on("data", (chunk: unknown) => { + output += String(chunk); + }); + await new Promise((resolve) => { + transform.on("end", resolve); + Readable.from(events).pipe(transform); + }); + } else { + for await (const chunk of result) output += String(chunk); + } console.log(`${name}:`, JSON.stringify(output)); }