@@ -191,14 +191,21 @@ fn test_crash_tracking_bin_runtime_callback_frame() {
191191/// Tests that when `collect_all_threads` is enabled, the crash report contains
192192/// entries in `error.threads` for background threads beyond the crashing thread.
193193///
194- /// The behavior enables `collect_all_threads`, spawns two named
195- /// sleeping worker threads in `post()`, and then crashes the main thread.
194+ /// The behavior (test_017_multi_thread_collection.rs) enables `collect_all_threads`,
195+ /// spawns two named sleeping worker threads in `post()`, and then crashes the main thread.
196+ ///
197+ /// Thread collection now happens in the receiver process using libunwind remote unwinding
198+ /// via ptrace (_UPT_create / unw_init_remote / unw_step_remote). The parent process stays
199+ /// alive until the receiver completes, guaranteeing threads are valid ptrace targets.
200+ ///
196201/// We verify:
197- /// - `error.threads` is non-empty
198- /// - Each thread entry is well-formed: `crashed`, `name`, `stack` present.
202+ /// - `error.threads` is non-empty.
203+ /// - Each thread entry is well-formed: `crashed=false `, `name`, and `stack` present.
199204/// - None of the additional threads are marked as crashed (the crashing thread is in
200205/// `error.stack`, not `error.threads`).
201- /// - The worker thread names are recognizable (ct_worker_0, ct_worker_1).
206+ /// - Both worker threads are present by name (ct_worker_0, ct_worker_1).
207+ /// - Each worker has a multi-frame stack trace including their entry function, confirming that
208+ /// libunwind remote unwinding produced a full call chain rather than a single syscall frame.
202209#[ test]
203210#[ cfg( target_os = "linux" ) ]
204211#[ cfg_attr( miri, ignore) ]
@@ -213,11 +220,11 @@ fn test_crash_tracking_multi_thread_collection() {
213220
214221 let validator: ValidatorFn = Box :: new ( |payload, _fixtures| {
215222 let error = & payload[ "error" ] ;
216- assert ! (
217- false ,
218- "{}" ,
219- serde_json:: to_string_pretty( error) . unwrap_or_default( )
220- ) ;
223+ // assert!(
224+ // false,
225+ // "{}",
226+ // serde_json::to_string_pretty(error).unwrap_or_default()
227+ // );
221228 let threads = error[ "threads" ]
222229 . as_array ( )
223230 . expect ( "error.threads should be a JSON array" ) ;
@@ -254,8 +261,7 @@ fn test_crash_tracking_multi_thread_collection() {
254261 ) ;
255262 }
256263
257- // Both named workers must be present; the behavior (test_017_multi_thread_collection.rs)
258- //spawns exactly two
264+ // Both named workers must be present; the behavior spawns exactly two
259265 for expected in [ "ct_worker_0" , "ct_worker_1" ] {
260266 assert ! (
261267 thread_names. contains( & expected) ,
@@ -264,9 +270,13 @@ fn test_crash_tracking_multi_thread_collection() {
264270 ) ;
265271 }
266272
267- // Each worker must have captured at least one stack frame
268- // (they were sleeping in a syscall, so SIGUSR2 interrupted them and
269- // unw_init_local2 should produce at minimum the syscall entry frame).
273+ // Each worker must have a multi-frame stack trace.
274+ //
275+ // The workers sleep in thread::sleep -> wait_for_work_N -> worker_entry_N.
276+ // With libunwind remote unwinding, we expect the full call chain rather than
277+ // a single syscall frame. We verify:
278+ // - More than one frame was captured.
279+ // - At least one frame contains the worker's entry function by name.
270280 for expected in [ "ct_worker_0" , "ct_worker_1" ] {
271281 let worker = threads
272282 . iter ( )
@@ -278,8 +288,26 @@ fn test_crash_tracking_multi_thread_collection() {
278288 . unwrap_or_else ( || panic ! ( "{expected} stack.frames should be an array" ) ) ;
279289
280290 assert ! (
281- !frames. is_empty( ) ,
282- "{expected} should have at least one stack frame; worker: {worker:?}"
291+ frames. len( ) > 1 ,
292+ "{expected} should have a multi-frame stack trace from remote libunwind; \
293+ got {} frame(s): {frames:?}",
294+ frames. len( )
295+ ) ;
296+
297+ let entry_fn = if expected == "ct_worker_0" {
298+ "worker_entry_0"
299+ } else {
300+ "worker_entry_1"
301+ } ;
302+ let has_entry_frame = frames. iter ( ) . any ( |f| {
303+ f[ "function" ]
304+ . as_str ( )
305+ . map ( |name| name. contains ( entry_fn) )
306+ . unwrap_or ( false )
307+ } ) ;
308+ assert ! (
309+ has_entry_frame,
310+ "{expected} stack should contain a frame for '{entry_fn}' but got: {frames:?}"
283311 ) ;
284312 }
285313
0 commit comments