@@ -135,6 +135,18 @@ fn test_crash_ping_timing_and_content() {
135
135
test_crash_tracking_bin ( BuildProfile :: Release , "donothing" , "null_deref" ) ;
136
136
}
137
137
138
+ #[ test]
139
+ #[ cfg_attr( miri, ignore) ]
140
+ fn test_crash_tracking_errors_intake_upload ( ) {
141
+ test_crash_tracking_bin_with_errors_intake ( BuildProfile :: Release , "donothing" , "null_deref" ) ;
142
+ }
143
+
144
+ #[ test]
145
+ #[ cfg_attr( miri, ignore) ]
146
+ fn test_crash_tracking_errors_intake_crash_ping ( ) {
147
+ test_crash_tracking_errors_intake_dual_upload ( BuildProfile :: Release , "donothing" , "null_deref" ) ;
148
+ }
149
+
138
150
// This test is disabled for now on x86_64 musl and macos
139
151
// It seems that on aarch64 musl, libc has CFI which allows
140
152
// unwinding passed the signal frame.
@@ -700,6 +712,214 @@ fn setup_crashtracking_crates(
700
712
( crashtracker_bin, crashtracker_receiver)
701
713
}
702
714
715
+ fn test_crash_tracking_bin_with_errors_intake (
716
+ crash_tracking_receiver_profile : BuildProfile ,
717
+ mode : & str ,
718
+ crash_typ : & str ,
719
+ ) {
720
+ let ( crashtracker_bin, crashtracker_receiver) =
721
+ setup_crashtracking_crates ( crash_tracking_receiver_profile) ;
722
+ let fixtures = setup_test_fixtures ( & [ & crashtracker_receiver, & crashtracker_bin] ) ;
723
+
724
+ let mut p = process:: Command :: new ( & fixtures. artifacts [ & crashtracker_bin] )
725
+ . arg ( format ! ( "file://{}" , fixtures. crash_profile_path. display( ) ) )
726
+ . arg ( fixtures. artifacts [ & crashtracker_receiver] . as_os_str ( ) )
727
+ . arg ( & fixtures. output_dir )
728
+ . arg ( mode)
729
+ . arg ( crash_typ)
730
+ . spawn ( )
731
+ . unwrap ( ) ;
732
+
733
+ let exit_status = bin_tests:: timeit!( "exit after signal" , {
734
+ eprintln!( "Waiting for exit" ) ;
735
+ p. wait( ) . unwrap( )
736
+ } ) ;
737
+
738
+ match crash_typ {
739
+ "kill_sigabrt" | "kill_sigill" | "null_deref" | "raise_sigabrt" | "raise_sigill" => {
740
+ assert ! ( !exit_status. success( ) )
741
+ }
742
+ "kill_sigbus" | "kill_sigsegv" | "raise_sigbus" | "raise_sigsegv" => {
743
+ assert ! ( exit_status. success( ) )
744
+ }
745
+ _ => unreachable ! ( "{crash_typ} shouldn't happen" ) ,
746
+ }
747
+
748
+ // Check that errors intake file was created
749
+ let errors_intake_path = fixtures. crash_profile_path . with_extension ( "errors" ) ;
750
+ assert ! (
751
+ errors_intake_path. exists( ) ,
752
+ "Errors intake file should be created at {}" ,
753
+ errors_intake_path. display( )
754
+ ) ;
755
+
756
+ // Read and validate errors intake payload
757
+ let errors_intake_content = fs:: read ( & errors_intake_path)
758
+ . context ( "reading errors intake payload" )
759
+ . unwrap ( ) ;
760
+ let errors_payload = serde_json:: from_slice :: < serde_json:: Value > ( & errors_intake_content)
761
+ . context ( "deserializing errors intake payload to json" )
762
+ . unwrap ( ) ;
763
+
764
+ // Validate errors intake payload structure
765
+ assert_errors_intake_payload ( & errors_payload, crash_typ) ;
766
+
767
+ // Also validate telemetry still works (dual upload)
768
+ let crash_telemetry = fs:: read ( & fixtures. crash_telemetry_path )
769
+ . context ( "reading crashtracker telemetry payload" )
770
+ . unwrap ( ) ;
771
+ assert_telemetry_message ( & crash_telemetry, crash_typ) ;
772
+ }
773
+
774
+ fn test_crash_tracking_errors_intake_dual_upload (
775
+ crash_tracking_receiver_profile : BuildProfile ,
776
+ mode : & str ,
777
+ crash_typ : & str ,
778
+ ) {
779
+ let ( crashtracker_bin, crashtracker_receiver) =
780
+ setup_crashtracking_crates ( crash_tracking_receiver_profile) ;
781
+ let fixtures = setup_test_fixtures ( & [ & crashtracker_receiver, & crashtracker_bin] ) ;
782
+
783
+ let mut p = process:: Command :: new ( & fixtures. artifacts [ & crashtracker_bin] )
784
+ . arg ( format ! ( "file://{}" , fixtures. crash_profile_path. display( ) ) )
785
+ . arg ( fixtures. artifacts [ & crashtracker_receiver] . as_os_str ( ) )
786
+ . arg ( & fixtures. output_dir )
787
+ . arg ( mode)
788
+ . arg ( crash_typ)
789
+ . spawn ( )
790
+ . unwrap ( ) ;
791
+
792
+ let exit_status = bin_tests:: timeit!( "exit after signal" , {
793
+ eprintln!( "Waiting for exit" ) ;
794
+ p. wait( ) . unwrap( )
795
+ } ) ;
796
+
797
+ match crash_typ {
798
+ "kill_sigabrt" | "kill_sigill" | "null_deref" | "raise_sigabrt" | "raise_sigill" => {
799
+ assert ! ( !exit_status. success( ) )
800
+ }
801
+ "kill_sigbus" | "kill_sigsegv" | "raise_sigbus" | "raise_sigsegv" => {
802
+ assert ! ( exit_status. success( ) )
803
+ }
804
+ _ => unreachable ! ( "{crash_typ} shouldn't happen" ) ,
805
+ }
806
+
807
+ // Check that errors intake file was created
808
+ let errors_intake_path = fixtures. crash_profile_path . with_extension ( "errors" ) ;
809
+ assert ! (
810
+ errors_intake_path. exists( ) ,
811
+ "Errors intake file should be created at {}" ,
812
+ errors_intake_path. display( )
813
+ ) ;
814
+
815
+ // Read and validate errors intake payload
816
+ let errors_intake_content = fs:: read ( & errors_intake_path)
817
+ . context ( "reading errors intake payload" )
818
+ . unwrap ( ) ;
819
+
820
+ // The errors intake might contain multiple JSON objects (crash ping + crash report)
821
+ // Try to parse as a single JSON first, if that fails, try line by line
822
+ if let Ok ( single_payload) = serde_json:: from_slice :: < serde_json:: Value > ( & errors_intake_content)
823
+ {
824
+ // Single JSON payload - validate it
825
+ assert_errors_intake_payload ( & single_payload, crash_typ) ;
826
+ } else {
827
+ // Multiple JSON objects - parse line by line
828
+ let content_str = String :: from_utf8 ( errors_intake_content) . unwrap ( ) ;
829
+ let lines: Vec < & str > = content_str. lines ( ) . collect ( ) ;
830
+ assert ! ( !lines. is_empty( ) , "Errors intake file should not be empty" ) ;
831
+
832
+ let mut _found_crash_ping = false ;
833
+ let mut found_crash_report = false ;
834
+
835
+ for line in lines {
836
+ if line. trim ( ) . is_empty ( ) {
837
+ continue ;
838
+ }
839
+
840
+ let payload: serde_json:: Value = serde_json:: from_str ( line)
841
+ . context ( "parsing errors intake payload line" )
842
+ . unwrap ( ) ;
843
+
844
+ assert_errors_intake_payload ( & payload, crash_typ) ;
845
+
846
+ // Check which type this is
847
+ let ddtags = payload[ "ddtags" ] . as_str ( ) . unwrap ( ) ;
848
+ if ddtags. contains ( "is_crash_ping:true" ) {
849
+ _found_crash_ping = true ;
850
+ } else {
851
+ found_crash_report = true ;
852
+ }
853
+ }
854
+
855
+ // In dual upload mode, we expect at least the crash report
856
+ // Crash ping might not always be sent (e.g., file endpoints skip it)
857
+ assert ! (
858
+ found_crash_report,
859
+ "Should have found crash report in errors intake"
860
+ ) ;
861
+ }
862
+
863
+ // Also validate telemetry still works (dual upload)
864
+ let crash_telemetry = fs:: read ( & fixtures. crash_telemetry_path )
865
+ . context ( "reading crashtracker telemetry payload" )
866
+ . unwrap ( ) ;
867
+ assert_telemetry_message ( & crash_telemetry, crash_typ) ;
868
+ }
869
+
870
+ fn assert_errors_intake_payload ( payload : & Value , crash_typ : & str ) {
871
+ // Validate basic structure
872
+ assert_eq ! ( payload[ "ddsource" ] , "crashtracker" ) ;
873
+ assert ! ( payload[ "timestamp" ] . is_number( ) ) ;
874
+ assert ! ( payload[ "ddtags" ] . is_string( ) ) ;
875
+
876
+ let ddtags = payload[ "ddtags" ] . as_str ( ) . unwrap ( ) ;
877
+ assert ! ( ddtags. contains( "service:foo" ) ) ;
878
+ assert ! ( ddtags. contains( "uuid:" ) ) ;
879
+
880
+ let error = & payload[ "error" ] ;
881
+ assert_eq ! ( error[ "source_type" ] , "Crashtracking" ) ;
882
+ assert ! ( error[ "type" ] . is_string( ) ) ; // Note: "error_type" field is serialized as "type"
883
+ assert ! ( error[ "message" ] . is_string( ) ) ;
884
+
885
+ // Check if this is a crash ping or crash report
886
+ if ddtags. contains ( "is_crash_ping:true" ) {
887
+ assert_eq ! ( error[ "is_crash" ] , false ) ;
888
+ assert ! ( error[ "stack" ] . is_null( ) ) ;
889
+ } else {
890
+ assert_eq ! ( error[ "is_crash" ] , true ) ;
891
+ }
892
+
893
+ // Check signal-specific values
894
+ match crash_typ {
895
+ "null_deref" => {
896
+ assert_eq ! ( error[ "type" ] , "SIGSEGV" ) ;
897
+ assert ! ( error[ "message" ]
898
+ . as_str( )
899
+ . unwrap( )
900
+ . contains( "Process terminated" ) ) ;
901
+ assert ! ( error[ "message" ] . as_str( ) . unwrap( ) . contains( "SIGSEGV" ) ) ;
902
+ }
903
+ "kill_sigabrt" | "raise_sigabrt" => {
904
+ assert_eq ! ( error[ "type" ] , "SIGABRT" ) ;
905
+ assert ! ( error[ "message" ] . as_str( ) . unwrap( ) . contains( "SIGABRT" ) ) ;
906
+ }
907
+ "kill_sigill" | "raise_sigill" => {
908
+ assert_eq ! ( error[ "type" ] , "SIGILL" ) ;
909
+ assert ! ( error[ "message" ] . as_str( ) . unwrap( ) . contains( "SIGILL" ) ) ;
910
+ }
911
+ "kill_sigbus" | "raise_sigbus" => {
912
+ assert_eq ! ( error[ "type" ] , "SIGBUS" ) ;
913
+ assert ! ( error[ "message" ] . as_str( ) . unwrap( ) . contains( "SIGBUS" ) ) ;
914
+ }
915
+ "kill_sigsegv" | "raise_sigsegv" => {
916
+ assert_eq ! ( error[ "type" ] , "SIGSEGV" ) ;
917
+ assert ! ( error[ "message" ] . as_str( ) . unwrap( ) . contains( "SIGSEGV" ) ) ;
918
+ }
919
+ _ => panic ! ( "Unexpected crash_typ: {crash_typ}" ) ,
920
+ }
921
+ }
922
+
703
923
fn extend_path < T : AsRef < Path > > ( parent : & Path , path : T ) -> PathBuf {
704
924
let mut parent = parent. to_path_buf ( ) ;
705
925
parent. push ( path) ;
0 commit comments