@@ -423,6 +423,8 @@ pub async fn tests(
423423 String :: new ( )
424424 } ;
425425
426+ let mut batch_junit_report = ReportBuilder :: new ( ) . build ( ) ;
427+
426428 ' batch: for ( workspace, args_groups) in & batch_groups {
427429 let ws_path = repo_root. join ( workspace) ;
428430 for ( additional_args, packages) in args_groups {
@@ -468,18 +470,29 @@ pub async fn tests(
468470 (
469471 "batch_doc" ,
470472 format!( "cargo doc --no-deps {pkg_flags} {jobs_flag}" ) ,
471- HashMap :: from( [ (
472- "RUSTDOCFLAGS" . to_string( ) ,
473- "-D warnings" . to_string( ) ,
474- ) ] ) ,
473+ HashMap :: from( [ ( "RUSTDOCFLAGS" . to_string( ) , "-D warnings" . to_string( ) ) ] ) ,
475474 ) ,
476475 ] ;
477476
477+ let mut ts = TestSuiteBuilder :: new ( & format ! ( "Batch {workspace}" ) )
478+ . set_timestamp ( OffsetDateTime :: now_utc ( ) )
479+ . build ( ) ;
480+
478481 for ( id, command, envs) in & steps {
479- tracing:: info!(
480- "Running {id} for {} packages in workspace {workspace}" ,
481- packages. len( )
482+ let step_span = tracing:: info_span!(
483+ "batch_step" ,
484+ otel. name = format!( "batch_step: {workspace}::{id}" ) ,
485+ otel. status_code = tracing:: field:: Empty ,
486+ step = %id,
487+ workspace = %workspace,
482488 ) ;
489+ let _step_entered = step_span. enter ( ) ;
490+
491+ let tc_prefix = format ! ( "{workspace:30.30} batch │ {command:50.50}" , ) ;
492+
493+ tracing:: info!( "│ {} │ ► START ({} packages)" , tc_prefix, packages. len( ) ) ;
494+ let start_time = OffsetDateTime :: now_utc ( ) ;
495+
483496 let output = Script :: new ( command, true )
484497 . name ( format ! ( "{workspace}::{id}" ) )
485498 . current_dir ( & ws_path)
@@ -488,15 +501,84 @@ pub async fn tests(
488501 . log_stderr ( tracing:: Level :: DEBUG )
489502 . execute ( )
490503 . await ;
504+
505+ let end_time = OffsetDateTime :: now_utc ( ) ;
506+ let duration = end_time - start_time;
507+
508+ let ( status, tc) = if output. success {
509+ tracing:: info!(
510+ "│ {} │ 🟢 PASS in {}" ,
511+ & tc_prefix,
512+ duration. human( Truncate :: Second )
513+ ) ;
514+ step_span. record ( "otel.status_code" , "OK" ) ;
515+ ( "PASS" , TestCase :: success ( & tc_prefix, duration) )
516+ } else {
517+ tracing:: info!(
518+ "│ {} │ 🟥 FAIL in {}" ,
519+ & tc_prefix,
520+ duration. human( Truncate :: Second )
521+ ) ;
522+ if !output. stderr . is_empty ( ) {
523+ tracing:: warn!(
524+ "│ {} │ stderr:\n {}" ,
525+ & tc_prefix,
526+ output. stderr. trim_end( )
527+ ) ;
528+ }
529+ if !output. stdout . is_empty ( ) {
530+ tracing:: warn!(
531+ "│ {} │ stdout:\n {}" ,
532+ & tc_prefix,
533+ output. stdout. trim_end( )
534+ ) ;
535+ }
536+ step_span. record ( "otel.status_code" , "ERROR" ) ;
537+ (
538+ "FAIL" ,
539+ TestCase :: failure ( & tc_prefix, duration, command, "required" ) ,
540+ )
541+ } ;
542+
543+ // Record per-step metrics for each package in the batch.
544+ for ( pkg_name, pkg_version) in packages {
545+ metrics. test_duration_h . record (
546+ duration. as_seconds_f64 ( ) ,
547+ & [
548+ KeyValue :: new ( "workspace_name" , workspace. clone ( ) ) ,
549+ KeyValue :: new ( "package_name" , pkg_name. clone ( ) ) ,
550+ KeyValue :: new ( "package_version" , pkg_version. clone ( ) ) ,
551+ KeyValue :: new ( "test_command" , command. clone ( ) ) ,
552+ KeyValue :: new ( "status" , status) ,
553+ ] ,
554+ ) ;
555+ metrics. test_counter . add (
556+ 1 ,
557+ & [
558+ KeyValue :: new ( "workspace_name" , workspace. clone ( ) ) ,
559+ KeyValue :: new ( "package_name" , pkg_name. clone ( ) ) ,
560+ KeyValue :: new ( "package_version" , pkg_version. clone ( ) ) ,
561+ KeyValue :: new ( "test_command" , command. clone ( ) ) ,
562+ KeyValue :: new ( "status" , status) ,
563+ ] ,
564+ ) ;
565+ }
566+
567+ ts. add_testcase ( tc) ;
568+
491569 if !output. success {
492- tracing:: error!( "{id} failed for workspace {workspace}" ) ;
493570 global_failed = true ;
571+ batch_junit_report. add_testsuite ( ts) ;
572+ global_junit_report. add_testsuites ( batch_junit_report. testsuites ( ) . clone ( ) ) ;
494573 break ' batch;
495574 }
496575 }
576+
577+ batch_junit_report. add_testsuite ( ts) ;
497578 batched. extend ( packages. iter ( ) . map ( |( name, _) | name. clone ( ) ) ) ;
498579 }
499580 }
581+ global_junit_report. add_testsuites ( batch_junit_report. testsuites ( ) . clone ( ) ) ;
500582 Arc :: new ( batched)
501583 } ;
502584
@@ -602,6 +684,7 @@ struct LockCheckResult {
602684 stderr : String ,
603685}
604686
687+ #[ allow( clippy:: too_many_arguments) ]
605688async fn do_test_on_package (
606689 common_options : Arc < PackageRelatedOptions > ,
607690 repo_root : PathBuf ,
@@ -993,7 +1076,7 @@ async fn run_package_tests(
9931076 . cloned ( )
9941077 . map ( |mut t| {
9951078 // Skip steps already handled by the workspace batch phase.
996- if batched_packages. contains ( & * package_name)
1079+ if batched_packages. contains ( package_name)
9971080 && matches ! (
9981081 t. id. as_str( ) ,
9991082 "cargo_fmt" | "cargo_check" | "cargo_clippy" | "cargo_doc"
0 commit comments