diff --git a/launchable/utils/java.py b/launchable/utils/java.py index df091a2e5..b4a2ffee1 100644 --- a/launchable/utils/java.py +++ b/launchable/utils/java.py @@ -45,6 +45,12 @@ def junit5_nested_class_path_builder( Note: Launchable allows $ in test paths. But we decided to remove it in this case because $ in the class name is not a common case. + Separately, when a @Nested class carries an explicit @DisplayName, Surefire (>= ~3.5.x) + drops the outer class name from the `classname` attribute entirely and reports just the + @DisplayName value (e.g. classname="addFollowList", with no dot and no $). In that case we + fall back to the enclosing attribute, which is always the outer + class's fully-qualified name. + Args: default_path_builder: The original path builder function to wrap @@ -53,6 +59,17 @@ def junit5_nested_class_path_builder( """ def path_builder(case: TestCase, suite: TestSuite, report_file: str) -> TestPath: test_path = default_path_builder(case, suite, report_file) - return [{**item, "name": item["name"].split("$")[0]} if item["type"] == "class" else item for item in test_path] + + def fix_class(item): + if item["type"] != "class": + return item + name = item["name"] + if "." not in name: + suite_name = suite._elem.attrib.get("name") + if suite_name and "." in suite_name: + name = suite_name + return {**item, "name": name.split("$")[0]} + + return [fix_class(item) for item in test_path] return path_builder diff --git a/tests/data/maven/record_test_nested_displayname_result.json b/tests/data/maven/record_test_nested_displayname_result.json new file mode 100644 index 000000000..73239f61d --- /dev/null +++ b/tests/data/maven/record_test_nested_displayname_result.json @@ -0,0 +1,27 @@ +{ + "events": [ + { + "type": "case", + "testPath": [ + { + "type": "class", + "name": "com.launchableinc.rocket_car_maven.NestedDisplayNameTest" + }, + { + "type": "testcase", + "name": "methodIsCalled" + } + ], + "duration": 0.001, + "status": 1, + "stdout": "", + "stderr": "", + "data": null + } + ], + "testRunner": "maven", + "group": "", + "noBuild": false, + "flavors": [], + "testSuite": "" +} diff --git a/tests/data/maven/reports-nested-displayname/TEST-nested-displayname.xml b/tests/data/maven/reports-nested-displayname/TEST-nested-displayname.xml new file mode 100644 index 000000000..5a718d572 --- /dev/null +++ b/tests/data/maven/reports-nested-displayname/TEST-nested-displayname.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_runners/test_maven.py b/tests/test_runners/test_maven.py index c3421d9c1..b151ccac0 100644 --- a/tests/test_runners/test_maven.py +++ b/tests/test_runners/test_maven.py @@ -4,9 +4,13 @@ from unittest import mock import responses # type: ignore +from junitparser import JUnitXml # type: ignore +from launchable.commands.record.case_event import CaseEvent from launchable.test_runners import maven +from launchable.testpath import FilePathNormalizer from launchable.utils.http_client import get_base_url +from launchable.utils.java import junit5_nested_class_path_builder from tests.cli_test_case import CliTestCase @@ -165,48 +169,54 @@ def test_record_test_maven(self): self.assert_success(result) self.assert_record_tests_payload("record_test_result.json") + def _build_path_builder(self): + """Builds the same wrapped path_builder that maven.py's record_tests wires up.""" + default_path_builder = CaseEvent.default_path_builder(FilePathNormalizer()) + return junit5_nested_class_path_builder(default_path_builder) + + def _class_path_for_first_case(self, report_path): + path_builder = self._build_path_builder() + xml = JUnitXml.fromfile(str(self.test_files_dir.joinpath(report_path))) + suite = next(iter(xml)) + case = next(iter(suite)) + test_path = path_builder(case, suite, str(report_path)) + return next(item["name"] for item in test_path if item["type"] == "class") + + def test_junit5_nested_class_path_builder_strips_dollar_suffix(self): + """@Nested class WITHOUT @DisplayName: Surefire keeps "Outer$Inner" as classname.""" + class_name = self._class_path_for_first_case('reports/TEST-nested.xml') + self.assertEqual(class_name, "com.launchableinc.rocket_car_maven.NestedTest") + self.assertNotIn("$", class_name) + + def test_junit5_nested_class_path_builder_falls_back_to_suite_name(self): + """@Nested class WITH @DisplayName: Surefire (>= ~3.5.x) drops the outer class from + `classname` entirely (e.g. classname="addFollowList"). We must fall back to the + enclosing attribute, which is always the outer class FQCN.""" + class_name = self._class_path_for_first_case('reports-nested-displayname/TEST-nested-displayname.xml') + self.assertEqual(class_name, "com.launchableinc.rocket_car_maven.NestedDisplayNameTest") + self.assertNotIn("$", class_name) + self.assertNotEqual(class_name, "addFollowList") + @responses.activate @mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token}) def test_record_test_maven_with_nested_class(self): """Verify that class names containing $ (inner class marker) are processed correctly during test recording""" - # Test the path_builder function directly by extracting it from the maven module - from unittest import TestCase as UnitTestCase - from unittest import TestSuite as UnitTestSuite - - # Extract the implementation from maven.py directly - # This gets the implementation without going through the CLI/Click command - def create_custom_path_builder(default_path_builder): - def path_builder(case, suite, report_file): - test_path = default_path_builder(case, suite, report_file) - return [{**item, "name": item["name"].split("$")[0]} if item["type"] == "class" else item for item in test_path] - return path_builder - - # Mock the default path builder that would return a class with $ in it - def default_path_builder(case, suite, report_file): - return [{"type": "class", "name": "com.launchableinc.rocket_car_maven.NestedTest$InnerClass"}] - - # Create our custom path builder function - custom_path_builder = create_custom_path_builder(default_path_builder) - - # Test it directly with dummy inputs - test_case = UnitTestCase() - test_suite = UnitTestSuite() - report_file = "TEST-nested.xml" - - # Call the path_builder - result_path = custom_path_builder(test_case, test_suite, report_file) - - # Verify the result - it should remove everything after $ - self.assertEqual(result_path[0]["name"], "com.launchableinc.rocket_car_maven.NestedTest") - self.assertNotIn("$", result_path[0]["name"]) - - # Now run the actual CLI command to ensure integration works result = self.cli('record', 'tests', '--session', self.session, 'maven', - str(self.test_files_dir) + "/maven/reports/TEST-1.xml", - str(self.test_files_dir) + "/maven/reports/TEST-2.xml", - str(self.test_files_dir) + "/maven/reports/TEST-nested.xml") + str(self.test_files_dir.joinpath('reports/TEST-1.xml')), + str(self.test_files_dir.joinpath('reports/TEST-2.xml')), + str(self.test_files_dir.joinpath('reports/TEST-nested.xml'))) + self.assert_success(result) + + @responses.activate + @mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token}) + def test_record_test_maven_with_nested_displayname_class(self): + """Verify the bare-@DisplayName-as-classname bug (no dot, no $) is corrected end-to-end.""" + result = self.cli('record', 'tests', '--session', self.session, + 'maven', + str(self.test_files_dir.joinpath('reports-nested-displayname/TEST-nested-displayname.xml'))) self.assert_success(result) + self.assert_record_tests_payload("record_test_nested_displayname_result.json") @responses.activate @mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token})