@@ -188,6 +188,97 @@ fn test_crash_tracking_bin_runtime_callback_frame() {
188188 run_crash_test_with_artifacts ( & config, & artifacts_map, & artifacts, validator) . unwrap ( ) ;
189189}
190190
191+ /// Tests that when `collect_all_threads` is enabled, the crash report contains
192+ /// entries in `error.threads` for background threads beyond the crashing thread.
193+ ///
194+ /// The behavior enables `collect_all_threads`, spawns two named
195+ /// sleeping worker threads in `post()`, and then crashes the main thread.
196+ /// We verify:
197+ /// - `error.threads` is non-empty
198+ /// - Each thread entry is well-formed: `crashed`, `name`, `stack` present.
199+ /// - None of the additional threads are marked as crashed (the crashing thread is in
200+ /// `error.stack`, not `error.threads`).
201+ /// - The worker thread names are recognizable (ct_worker_0, ct_worker_1).
202+ #[ test]
203+ #[ cfg( target_os = "linux" ) ]
204+ #[ cfg_attr( miri, ignore) ]
205+ fn test_crash_tracking_multi_thread_collection ( ) {
206+ let config = CrashTestConfig :: new (
207+ BuildProfile :: Release ,
208+ TestMode :: MultiThreadCollection ,
209+ CrashType :: NullDeref ,
210+ ) ;
211+ let artifacts = StandardArtifacts :: new ( config. profile ) ;
212+ let artifacts_map = fetch_built_artifacts ( & artifacts. as_slice ( ) ) . unwrap ( ) ;
213+
214+ let validator: ValidatorFn = Box :: new ( |payload, _fixtures| {
215+ let error = & payload[ "error" ] ;
216+ let threads = error[ "threads" ]
217+ . as_array ( )
218+ . expect ( "error.threads should be a JSON array" ) ;
219+
220+ assert ! (
221+ !threads. is_empty( ) ,
222+ "error.threads should be non-empty when collect_all_threads is enabled; \
223+ got payload: {}",
224+ serde_json:: to_string_pretty( payload) . unwrap_or_default( )
225+ ) ;
226+
227+ // We spawned 2 workers; there may be additional runtime threads too.
228+ // Require at least 1 non-crashing thread captured.
229+ let non_crashing: Vec < _ > = threads
230+ . iter ( )
231+ . filter ( |t| !t[ "crashed" ] . as_bool ( ) . unwrap_or ( true ) )
232+ . collect ( ) ;
233+ assert ! (
234+ !non_crashing. is_empty( ) ,
235+ "Expected at least one non-crashing thread in error.threads"
236+ ) ;
237+
238+ // Every thread entry must have the required fields.
239+ for thread in threads {
240+ assert ! (
241+ thread[ "name" ] . is_string( ) ,
242+ "thread entry missing 'name': {thread:?}"
243+ ) ;
244+ assert ! (
245+ thread[ "crashed" ] . is_boolean( ) ,
246+ "thread entry missing 'crashed': {thread:?}"
247+ ) ;
248+ assert ! (
249+ thread[ "stack" ] . is_object( ) ,
250+ "thread entry missing 'stack': {thread:?}"
251+ ) ;
252+ // crashed must be false for entries in error.threads
253+ assert ! (
254+ !thread[ "crashed" ] . as_bool( ) . unwrap_or( true ) ,
255+ "threads in error.threads must have crashed=false: {thread:?}"
256+ ) ;
257+ }
258+
259+ // At least one of the two named workers should appear.
260+ let worker_found = threads. iter ( ) . any ( |t| {
261+ t[ "name" ]
262+ . as_str ( )
263+ . map ( |n| n. starts_with ( "ct_worker" ) )
264+ . unwrap_or ( false )
265+ } ) ;
266+ assert ! (
267+ worker_found,
268+ "Expected to find at least one ct_worker_N thread in error.threads; \
269+ thread names: {:?}",
270+ threads
271+ . iter( )
272+ . map( |t| t[ "name" ] . as_str( ) . unwrap_or( "<none>" ) )
273+ . collect:: <Vec <_>>( )
274+ ) ;
275+
276+ Ok ( ( ) )
277+ } ) ;
278+
279+ run_crash_test_with_artifacts ( & config, & artifacts_map, & artifacts, validator) . unwrap ( ) ;
280+ }
281+
191282#[ test]
192283#[ cfg( target_os = "linux" ) ]
193284#[ cfg_attr( miri, ignore) ]
0 commit comments