diff --git a/mergify_cli/ci/junit_processing/junit.py b/mergify_cli/ci/junit_processing/junit.py index 06aae3bc..75a50b56 100644 --- a/mergify_cli/ci/junit_processing/junit.py +++ b/mergify_cli/ci/junit_processing/junit.py @@ -194,17 +194,19 @@ async def junit_to_spans( for testcase in testsuite.findall("testcase"): classname = testcase.get("classname") + test_case_name = testcase.get("name", "unnamed test") if classname is not None: - test_name = classname + "." + testcase.get("name", "unnamed test") + test_name = classname + "." + test_case_name else: - test_name = testcase.get("name", "unnamed test") + test_name = test_case_name start_time = now - int(float(testcase.get("time", 0)) * 10e9) min_start_time = min(min_start_time, start_time) attributes: dict[str, str | bool] = { "test.scope": "case", - "test.case.name": test_name, - "code.function.name": test_name, + "test.suite.name": classname if classname is not None else "", + "test.case.name": test_case_name, + "code.function.name": test_case_name, "cicd.test.quarantined": False, } diff --git a/mergify_cli/ci/junit_processing/quarantine.py b/mergify_cli/ci/junit_processing/quarantine.py index 768e33a7..0328e497 100644 --- a/mergify_cli/ci/junit_processing/quarantine.py +++ b/mergify_cli/ci/junit_processing/quarantine.py @@ -21,6 +21,16 @@ class QuarantineFailedError(Exception): ) +def generate_test_report(test_class: str, test_name: str) -> None: + click.echo(" · Test:") + click.echo( + f" Class: {test_class}", + ) + click.echo( + f" Name: {test_name}", + ) + + async def check_and_update_failing_spans( api_url: str, token: str, @@ -98,12 +108,30 @@ async def check_and_update_failing_spans( if quarantined_tests_spans: click.echo(" - 🔒 Quarantined:") for qt_span in quarantined_tests_spans: - click.echo(f" · {qt_span.name}") + if qt_span.attributes is not None: + generate_test_report( + test_class=typing.cast( + "str", + qt_span.attributes["test.suite.name"], + ), + test_name=typing.cast("str", qt_span.attributes["test.case.name"]), + ) + else: + click.echo(f" · {qt_span.name}") if non_quarantined_tests_spans: click.echo(" - ❌ Unquarantined:") for nqt_span in non_quarantined_tests_spans: - click.echo(f" · {nqt_span.name}") + if nqt_span.attributes is not None: + generate_test_report( + test_class=typing.cast( + "str", + nqt_span.attributes["test.suite.name"], + ), + test_name=typing.cast("str", nqt_span.attributes["test.case.name"]), + ) + else: + click.echo(f" · {nqt_span.name}") return failing_tests_not_quarantined_count diff --git a/mergify_cli/tests/ci/junit_processing/test_check_failing_spans.py b/mergify_cli/tests/ci/junit_processing/test_check_failing_spans.py index 3a738341..22d3206c 100644 --- a/mergify_cli/tests/ci/junit_processing/test_check_failing_spans.py +++ b/mergify_cli/tests/ci/junit_processing/test_check_failing_spans.py @@ -57,7 +57,11 @@ async def test_no_failing_tests_quarantined( ReadableSpan( name="test_me.py::test_mee", status=Status(status_code=StatusCode.ERROR, description=""), - attributes={"test.scope": "case"}, + attributes={ + "test.scope": "case", + "test.suite.name": "TestMe", + "test.case.name": "test_me", + }, ), ] @@ -90,17 +94,29 @@ async def test_some_failing_tests_quarantined( ReadableSpan( name="test_me.py::test_me1", status=Status(status_code=StatusCode.ERROR, description=""), - attributes={"test.scope": "case"}, + attributes={ + "test.scope": "case", + "test.suite.name": "TestMe1", + "test.case.name": "test_me1", + }, ), ReadableSpan( name="test_me.py::test_me2", status=Status(status_code=StatusCode.ERROR, description=""), - attributes={"test.scope": "case"}, + attributes={ + "test.scope": "case", + "test.suite.name": "TestMe2", + "test.case.name": "test_me2", + }, ), ReadableSpan( name="test_me.py::test_me3", status=Status(status_code=StatusCode.OK, description=""), - attributes={"test.scope": "case"}, + attributes={ + "test.scope": "case", + "test.suite.name": "TestMe3", + "test.case.name": "test_me3", + }, ), ReadableSpan( name="test_me.py::test_me4", @@ -152,17 +168,29 @@ async def test_all_failing_tests_quarantined( ReadableSpan( name="test_me.py::test_me1", status=Status(status_code=StatusCode.ERROR, description=""), - attributes={"test.scope": "case"}, + attributes={ + "test.scope": "case", + "test.suite.name": "TestMe1", + "test.case.name": "test_me1", + }, ), ReadableSpan( name="test_me.py::test_me2", status=Status(status_code=StatusCode.ERROR, description=""), - attributes={"test.scope": "case"}, + attributes={ + "test.scope": "case", + "test.suite.name": "TestMe2", + "test.case.name": "test_me2", + }, ), ReadableSpan( name="test_me.py::test_me3", status=Status(status_code=StatusCode.ERROR, description=""), - attributes={"test.scope": "case"}, + attributes={ + "test.scope": "case", + "test.suite.name": "TestMe3", + "test.case.name": "test_me3", + }, ), ] diff --git a/mergify_cli/tests/ci/test_junit.py b/mergify_cli/tests/ci/test_junit.py index d3dd5e2d..2f831d10 100644 --- a/mergify_cli/tests/ci/test_junit.py +++ b/mergify_cli/tests/ci/test_junit.py @@ -131,8 +131,9 @@ async def test_parse( }, { "attributes": { - "test.case.name": "Tests.Registration.testCase1", - "code.function.name": "Tests.Registration.testCase1", + "test.suite.name": "Tests.Registration", + "test.case.name": "testCase1", + "code.function.name": "testCase1", "test.case.result.status": "passed", "test.scope": "case", "test.framework": "unittest", @@ -161,8 +162,9 @@ async def test_parse( }, { "attributes": { - "test.case.name": "Tests.Registration.testCase2", - "code.function.name": "Tests.Registration.testCase2", + "test.suite.name": "Tests.Registration", + "test.case.name": "testCase2", + "code.function.name": "testCase2", "test.case.result.status": "skipped", "test.scope": "case", "test.framework": "unittest", @@ -194,8 +196,9 @@ async def test_parse( "exception.message": "invalid literal for int() with base 10: 'foobar'", "exception.stacktrace": "bip, bip, bip, error!", "exception.type": "ValueError", - "test.case.name": "Tests.Registration.testCase3", - "code.function.name": "Tests.Registration.testCase3", + "test.suite.name": "Tests.Registration", + "test.case.name": "testCase3", + "code.function.name": "testCase3", "test.case.result.status": "failed", "test.scope": "case", "test.framework": "unittest", @@ -251,8 +254,9 @@ async def test_parse( }, { "attributes": { - "test.case.name": "Tests.Authentication.testCase7", - "code.function.name": "Tests.Authentication.testCase7", + "test.suite.name": "Tests.Authentication", + "test.case.name": "testCase7", + "code.function.name": "testCase7", "test.case.result.status": "passed", "test.scope": "case", "test.framework": "unittest", @@ -281,8 +285,9 @@ async def test_parse( }, { "attributes": { - "test.case.name": "Tests.Authentication.testCase8", - "code.function.name": "Tests.Authentication.testCase8", + "test.suite.name": "Tests.Authentication", + "test.case.name": "testCase8", + "code.function.name": "testCase8", "test.case.result.status": "passed", "test.scope": "case", "test.framework": "unittest", @@ -314,8 +319,9 @@ async def test_parse( "exception.message": "Assertion error message", "exception.stacktrace": "Such a mess, the failure is unrecoverable", "exception.type": "AssertionError", - "test.case.name": "Tests.Authentication.testCase9", - "code.function.name": "Tests.Authentication.testCase9", + "test.suite.name": "Tests.Authentication", + "test.case.name": "testCase9", + "code.function.name": "testCase9", "test.case.result.status": "failed", "test.scope": "case", "test.framework": "unittest", @@ -348,8 +354,9 @@ async def test_parse( "exception.stacktrace": "Everything is broken, meh!\n" "With a second line!", "exception.type": "ZeroDivisionError", - "test.case.name": "Tests.Permission.testCase10", - "code.function.name": "Tests.Permission.testCase10", + "test.suite.name": "Tests.Permission", + "test.case.name": "testCase10", + "code.function.name": "testCase10", "test.case.result.status": "failed", "test.scope": "case", "test.framework": "unittest", @@ -405,8 +412,9 @@ async def test_parse( }, { "attributes": { - "test.case.name": "Tests.Authentication.Login.testCase4", - "code.function.name": "Tests.Authentication.Login.testCase4", + "test.suite.name": "Tests.Authentication.Login", + "test.case.name": "testCase4", + "code.function.name": "testCase4", "test.case.result.status": "passed", "test.scope": "case", "test.framework": "unittest", @@ -438,8 +446,9 @@ async def test_parse( "exception.message": "invalid syntax", "exception.stacktrace": "bad syntax, bad!", "exception.type": "SyntaxError", - "test.case.name": "Tests.Authentication.Login.testCase5", - "code.function.name": "Tests.Authentication.Login.testCase5", + "test.suite.name": "Tests.Authentication.Login", + "test.case.name": "testCase5", + "code.function.name": "testCase5", "test.case.result.status": "failed", "test.scope": "case", "test.framework": "unittest", @@ -468,8 +477,9 @@ async def test_parse( }, { "attributes": { - "test.case.name": "Tests.Authentication.Login.testCase6", - "code.function.name": "Tests.Authentication.Login.testCase6", + "test.suite.name": "Tests.Authentication.Login", + "test.case.name": "testCase6", + "code.function.name": "testCase6", "test.case.result.status": "passed", "test.scope": "case", "test.framework": "unittest",