@@ -555,6 +555,243 @@ async def test_traceparent_injection(
555555 assert span .context .trace_id == 0x80E1AFED08E019FC1110464CFA66635C
556556
557557
558+ @mock .patch .object (detector , "get_ci_provider" , return_value = "github_actions" )
559+ @mock .patch .object (detector , "get_pipeline_name" , return_value = "PIPELINE" )
560+ @mock .patch .object (detector , "get_job_name" , return_value = "JOB" )
561+ @mock .patch .object (
562+ detector ,
563+ "get_cicd_pipeline_runner_name" ,
564+ return_value = "self-hosted" ,
565+ )
566+ @mock .patch .object (detector , "get_cicd_pipeline_run_id" , return_value = 123 )
567+ @mock .patch .object (detector , "get_cicd_pipeline_run_attempt" , return_value = 1 )
568+ @mock .patch .object (
569+ detector ,
570+ "get_head_sha" ,
571+ return_value = "3af96aa24f1d32fcfbb7067793cacc6dc0c6b199" ,
572+ )
573+ @mock .patch .object (
574+ detector ,
575+ "get_head_ref_name" ,
576+ return_value = "refs/heads/main" ,
577+ )
578+ async def test_parse_single_suite (
579+ _get_ci_provider : mock .Mock ,
580+ _get_pipeline_name : mock .Mock ,
581+ _get_job_name : mock .Mock ,
582+ _get_cicd_pipeline_runner_name : mock .Mock ,
583+ _get_cicd_pipeline_run_id : mock .Mock ,
584+ _get_cicd_pipeline_run_attempt : mock .Mock ,
585+ _get_head_sha : mock .Mock ,
586+ _get_head_ref_name : mock .Mock ,
587+ monkeypatch : pytest .MonkeyPatch ,
588+ ) -> None :
589+ monkeypatch .setenv ("MERGIFY_TEST_JOB_NAME" , "foobar" )
590+ filename = (
591+ pathlib .Path (__file__ ).parent / "fixtures" / "junit_example_single_suite.xml"
592+ )
593+ run_id = (32312 ).to_bytes (8 , "big" ).hex ()
594+ spans = await junit .junit_to_spans (
595+ run_id ,
596+ filename .read_bytes (),
597+ "python" ,
598+ "unittest" ,
599+ )
600+ assert spans [0 ].parent is None
601+
602+ dictified_spans = [json .loads (span .to_json ()) for span in spans ]
603+ trace_id = "0x" + opentelemetry .trace .span .format_trace_id (
604+ spans [1 ].context .trace_id ,
605+ )
606+ resource_attributes = {
607+ "test.run.id" : run_id ,
608+ "cicd.pipeline.name" : "PIPELINE" ,
609+ "cicd.pipeline.task.name" : "JOB" ,
610+ "cicd.pipeline.run.id" : 123 ,
611+ "cicd.pipeline.run.attempt" : 1 ,
612+ "cicd.pipeline.runner.name" : "self-hosted" ,
613+ "cicd.provider.name" : "github_actions" ,
614+ "vcs.ref.head.revision" : "3af96aa24f1d32fcfbb7067793cacc6dc0c6b199" ,
615+ "vcs.ref.head.name" : "refs/heads/main" ,
616+ "service.name" : "unknown_service" ,
617+ "telemetry.sdk.language" : "python" ,
618+ "telemetry.sdk.name" : "opentelemetry" ,
619+ "telemetry.sdk.version" : anys .ANY_STR ,
620+ "mergify.test.job.name" : "foobar" ,
621+ }
622+ assert dictified_spans == [
623+ {
624+ "attributes" : {
625+ "test.framework" : "unittest" ,
626+ "test.language" : "python" ,
627+ "test.scope" : "session" ,
628+ },
629+ "context" : {
630+ "span_id" : anys .ANY_STR ,
631+ "trace_id" : trace_id ,
632+ "trace_state" : "[]" ,
633+ },
634+ "end_time" : anys .ANY_DATETIME_STR ,
635+ "events" : [],
636+ "kind" : "SpanKind.INTERNAL" ,
637+ "links" : [],
638+ "name" : "test session" ,
639+ "parent_id" : None ,
640+ "resource" : {
641+ "attributes" : resource_attributes ,
642+ "schema_url" : "" ,
643+ },
644+ "start_time" : anys .ANY_DATETIME_STR ,
645+ "status" : {
646+ "status_code" : "UNSET" ,
647+ },
648+ },
649+ {
650+ "attributes" : {
651+ "test.case.name" : "Tests.Registration" ,
652+ "test.scope" : "suite" ,
653+ "test.framework" : "unittest" ,
654+ "test.language" : "python" ,
655+ },
656+ "context" : {
657+ "span_id" : anys .ANY_STR ,
658+ "trace_id" : trace_id ,
659+ "trace_state" : "[]" ,
660+ },
661+ "end_time" : anys .ANY_DATETIME_STR ,
662+ "events" : [],
663+ "kind" : "SpanKind.INTERNAL" ,
664+ "links" : [],
665+ "name" : "Tests.Registration" ,
666+ "parent_id" : anys .ANY_STR ,
667+ "resource" : {
668+ "attributes" : resource_attributes ,
669+ "schema_url" : "" ,
670+ },
671+ "start_time" : anys .ANY_DATETIME_STR ,
672+ "status" : {
673+ "status_code" : "UNSET" ,
674+ },
675+ },
676+ {
677+ "attributes" : {
678+ "test.case.name" : "Tests.Registration.testCase1" ,
679+ "code.function.name" : "Tests.Registration.testCase1" ,
680+ "test.case.result.status" : "passed" ,
681+ "test.scope" : "case" ,
682+ "test.framework" : "unittest" ,
683+ "test.language" : "python" ,
684+ "cicd.test.quarantined" : False ,
685+ },
686+ "context" : {
687+ "span_id" : anys .ANY_STR ,
688+ "trace_id" : trace_id ,
689+ "trace_state" : "[]" ,
690+ },
691+ "end_time" : anys .ANY_DATETIME_STR ,
692+ "events" : [],
693+ "kind" : "SpanKind.INTERNAL" ,
694+ "links" : [],
695+ "name" : "Tests.Registration.testCase1" ,
696+ "parent_id" : anys .ANY_STR ,
697+ "resource" : {
698+ "attributes" : resource_attributes ,
699+ "schema_url" : "" ,
700+ },
701+ "start_time" : anys .ANY_DATETIME_STR ,
702+ "status" : {
703+ "status_code" : "OK" ,
704+ },
705+ },
706+ {
707+ "attributes" : {
708+ "test.case.name" : "Tests.Registration.testCase2" ,
709+ "code.function.name" : "Tests.Registration.testCase2" ,
710+ "test.case.result.status" : "skipped" ,
711+ "test.scope" : "case" ,
712+ "test.framework" : "unittest" ,
713+ "test.language" : "python" ,
714+ "cicd.test.quarantined" : False ,
715+ },
716+ "context" : {
717+ "span_id" : anys .ANY_STR ,
718+ "trace_id" : trace_id ,
719+ "trace_state" : "[]" ,
720+ },
721+ "end_time" : anys .ANY_DATETIME_STR ,
722+ "events" : [],
723+ "kind" : "SpanKind.INTERNAL" ,
724+ "links" : [],
725+ "name" : "Tests.Registration.testCase2" ,
726+ "parent_id" : anys .ANY_STR ,
727+ "resource" : {
728+ "attributes" : resource_attributes ,
729+ "schema_url" : "" ,
730+ },
731+ "start_time" : anys .ANY_DATETIME_STR ,
732+ "status" : {
733+ "status_code" : "OK" ,
734+ },
735+ },
736+ {
737+ "attributes" : {
738+ "exception.message" : "invalid literal for int() with base 10: 'foobar'" ,
739+ "exception.stacktrace" : "bip, bip, bip, error!" ,
740+ "exception.type" : "ValueError" ,
741+ "test.case.name" : "Tests.Registration.testCase3" ,
742+ "code.function.name" : "Tests.Registration.testCase3" ,
743+ "test.case.result.status" : "failed" ,
744+ "test.scope" : "case" ,
745+ "test.framework" : "unittest" ,
746+ "test.language" : "python" ,
747+ "cicd.test.quarantined" : False ,
748+ },
749+ "context" : {
750+ "span_id" : anys .ANY_STR ,
751+ "trace_id" : trace_id ,
752+ "trace_state" : "[]" ,
753+ },
754+ "end_time" : anys .ANY_DATETIME_STR ,
755+ "events" : [],
756+ "kind" : "SpanKind.INTERNAL" ,
757+ "links" : [],
758+ "name" : "Tests.Registration.testCase3" ,
759+ "parent_id" : anys .ANY_STR ,
760+ "resource" : {
761+ "attributes" : resource_attributes ,
762+ "schema_url" : "" ,
763+ },
764+ "start_time" : anys .ANY_DATETIME_STR ,
765+ "status" : {
766+ "status_code" : "ERROR" ,
767+ },
768+ },
769+ ]
770+
771+
772+ async def test_parse_single_suite_with_nested_suites () -> None :
773+ filename = (
774+ pathlib .Path (__file__ ).parent
775+ / "fixtures"
776+ / "junit_example_nested_single_suite.xml"
777+ )
778+ run_id = (32312 ).to_bytes (8 , "big" ).hex ()
779+ spans = await junit .junit_to_spans (run_id , filename .read_bytes ())
780+
781+ suite_names = [
782+ span .attributes ["test.case.name" ]
783+ for span in spans
784+ if span .attributes is not None and span .attributes .get ("test.scope" ) == "suite"
785+ ]
786+ case_names = [
787+ span .attributes ["test.case.name" ]
788+ for span in spans
789+ if span .attributes is not None and span .attributes .get ("test.scope" ) == "case"
790+ ]
791+ assert suite_names == ["Outer" , "Inner" ]
792+ assert case_names == ["Outer.directCase" , "Inner.nestedCase" ]
793+
794+
558795# ── Exit code pinning tests ──
559796
560797
0 commit comments