From 912ca2d7b15ece96ffb92abf085e0036d7598762 Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Fri, 13 Feb 2026 17:35:48 +0800 Subject: [PATCH 1/2] TRCLI-135: Added support for multiple case ids for name and property matchers --- trcli/data_classes/data_parsers.py | 129 ++++++++++++++++++--- trcli/readers/junit_xml.py | 173 +++++++++++++++++++++++------ 2 files changed, 250 insertions(+), 52 deletions(-) diff --git a/trcli/data_classes/data_parsers.py b/trcli/data_classes/data_parsers.py index 510ac51..f76cc7b 100644 --- a/trcli/data_classes/data_parsers.py +++ b/trcli/data_classes/data_parsers.py @@ -9,8 +9,10 @@ class MatchersParser: PROPERTY = "property" @staticmethod - def parse_name_with_id(case_name: str) -> Tuple[int, str]: + def parse_name_with_id(case_name: str) -> Tuple[Union[int, List[int], None], str]: """Parses case names expecting an ID following one of the following patterns: + + Single ID patterns: - "C123 my test case" - "my test case C123" - "C123_my_test_case" @@ -21,32 +23,122 @@ def parse_name_with_id(case_name: str) -> Tuple[int, str]: - "module 1 [C123] my test case" - "my_test_case_C123()" (JUnit 5 support) + Multiple ID patterns: + - "[C123, C456, C789] my test case" + - "my test case [C123, C456, C789]" + - "C123_C456_C789_my_test_case" (underscore-separated) + :param case_name: Name of the test case - :return: Tuple with test case ID and test case name without the ID + :return: Tuple with test case ID(s) (int for single, List[int] for multiple) and test case name without the ID(s) """ + # First, try to parse brackets for single or multiple IDs + results = re.findall(r"\[(.*?)\]", case_name) + for result in results: + # Check if it contains comma-separated IDs + if "," in result: + # Multiple IDs in brackets: [C123, C456, C789] + case_ids = MatchersParser._parse_multiple_case_ids_from_string(result) + if case_ids: + id_tag = f"[{result}]" + tag_idx = case_name.find(id_tag) + cleaned_name = f"{case_name[0:tag_idx].strip()} {case_name[tag_idx + len(id_tag):].strip()}".strip() + # Return list for multiple IDs, int for single ID (backwards compatibility) + return case_ids if len(case_ids) > 1 else case_ids[0], cleaned_name + elif result.lower().startswith("c"): + # Single ID in brackets: [C123] + case_id = result[1:] + if case_id.isnumeric(): + id_tag = f"[{result}]" + tag_idx = case_name.find(id_tag) + cleaned_name = f"{case_name[0:tag_idx].strip()} {case_name[tag_idx + len(id_tag):].strip()}".strip() + return int(case_id), cleaned_name + + # Try underscore-separated multiple IDs: C123_C456_C789_test_name + underscore_case_ids = MatchersParser._parse_multiple_underscore_ids(case_name) + if underscore_case_ids: + return underscore_case_ids + + # Fall back to original space/underscore single ID parsing for char in [" ", "_"]: parts = case_name.split(char) parts_copy = parts.copy() for idx, part in enumerate(parts): if part.lower().startswith("c") and len(part) > 1: id_part = part[1:] - id_part_clean = re.sub(r'\(.*\)$', '', id_part) + id_part_clean = re.sub(r"\(.*\)$", "", id_part) if id_part_clean.isnumeric(): parts_copy.pop(idx) return int(id_part_clean), char.join(parts_copy) - results = re.findall(r"\[(.*?)\]", case_name) - for result in results: - if result.lower().startswith("c"): - case_id = result[1:] - if case_id.isnumeric(): - id_tag = f"[{result}]" - tag_idx = case_name.find(id_tag) - case_name = f"{case_name[0:tag_idx].strip()} {case_name[tag_idx + len(id_tag):].strip()}".strip() - return int(case_id), case_name - return None, case_name + @staticmethod + def _parse_multiple_case_ids_from_string(ids_string: str) -> List[int]: + """ + Parse comma-separated case IDs from a string. + + Examples: + - "C123, C456, C789" -> [123, 456, 789] + - "123, 456, 789" -> [123, 456, 789] + - " C123 , C456 " -> [123, 456] + + :param ids_string: String containing comma-separated case IDs + :return: List of integer case IDs + """ + case_ids = [] + parts = [part.strip() for part in ids_string.split(",")] + + for part in parts: + if not part: + continue + + # Remove 'C' or 'c' prefix if present + cleaned = part.lower().replace("c", "", 1).strip() + + # Check if it's a valid numeric ID + if cleaned.isdigit(): + case_id = int(cleaned) + # Deduplicate + if case_id not in case_ids: + case_ids.append(case_id) + + return case_ids + + @staticmethod + def _parse_multiple_underscore_ids(case_name: str) -> Union[Tuple[List[int], str], Tuple[int, str], None]: + """ + Parse multiple underscore-separated case IDs from test name. + + Examples: + - "C123_C456_C789_test_name" -> ([123, 456, 789], "test_name") + - "C100_C200_my_test" -> ([100, 200], "my_test") + + :param case_name: Test case name + :return: Tuple with case IDs and cleaned name, or None if no multiple IDs found + """ + parts = case_name.split("_") + case_ids = [] + non_id_parts = [] + + for part in parts: + if part.lower().startswith("c") and len(part) > 1: + id_part = part[1:] + # Remove parentheses (JUnit 5 support) + id_part_clean = re.sub(r"\(.*\)$", "", id_part) + if id_part_clean.isdigit(): + case_id = int(id_part_clean) + if case_id not in case_ids: + case_ids.append(case_id) + continue + non_id_parts.append(part) + + # Only return if we found at least 2 case IDs + if len(case_ids) >= 2: + cleaned_name = "_".join(non_id_parts) + return case_ids, cleaned_name + + return None + class FieldsParser: @@ -72,6 +164,7 @@ def resolve_fields(fields: Union[List[str], Dict]) -> Tuple[Dict, str]: except Exception as ex: return fields_dictionary, f"Error parsing fields: {ex}" + class TestRailCaseFieldsOptimizer: MAX_TESTCASE_TITLE_LENGTH = 250 @@ -82,11 +175,11 @@ def extract_last_words(input_string, max_characters=MAX_TESTCASE_TITLE_LENGTH): return None # Define delimiters for splitting words - delimiters = [' ', '\t', ';', ':', '>', '/', '.'] + delimiters = [" ", "\t", ";", ":", ">", "/", "."] # Replace multiple consecutive delimiters with a single space - regex_pattern = '|'.join(map(re.escape, delimiters)) - cleaned_string = re.sub(f'[{regex_pattern}]+', ' ', input_string.strip()) + regex_pattern = "|".join(map(re.escape, delimiters)) + cleaned_string = re.sub(f"[{regex_pattern}]+", " ", input_string.strip()) # Split the cleaned string into words words = cleaned_string.split() @@ -102,10 +195,10 @@ def extract_last_words(input_string, max_characters=MAX_TESTCASE_TITLE_LENGTH): break # Reverse the extracted words to maintain the original order - result = ' '.join(reversed(extracted_words)) + result = " ".join(reversed(extracted_words)) # as fallback, return the last characters if the result is empty if result.strip() == "": result = input_string[-max_characters:] - return result \ No newline at end of file + return result diff --git a/trcli/readers/junit_xml.py b/trcli/readers/junit_xml.py index c8756d8..6014be5 100644 --- a/trcli/readers/junit_xml.py +++ b/trcli/readers/junit_xml.py @@ -107,11 +107,70 @@ def _extract_case_id_and_name(self, case) -> tuple: for case_props in case.iterchildren(Properties): for prop in case_props.iterchildren(Property): if prop.name == "test_id": - case_id = int(prop.value.lower().replace("c", "")) + case_id = self._parse_multiple_case_ids(prop.value) return case_id, case_name return case_id, case_name + @staticmethod + def _parse_multiple_case_ids(test_id_value: str) -> Union[int, List[int], None]: + """ + Parse single or multiple case IDs from a test_id property value. + + Supports comma-separated case IDs for mapping multiple TestRail cases to one JUnit test. + + Examples: + - "C123" -> 123 (int) + - "C123, C456, C789" -> [123, 456, 789] (list) + - "123, 456, 789" -> [123, 456, 789] (list) + - " C123 , C456 " -> [123, 456] (list) + - "C123, C123" -> 123 (int, deduplicated) + + :param test_id_value: Value of the test_id property + :return: Single case ID (int), multiple case IDs (List[int]), or None if invalid + """ + if not test_id_value or not isinstance(test_id_value, str): + return None + + test_id_value = test_id_value.strip() + if not test_id_value: + return None + + # Check if comma-separated (multiple IDs) + if "," in test_id_value: + case_ids = [] + parts = [part.strip() for part in test_id_value.split(",")] + + for part in parts: + if not part: + continue + + # Remove 'C' or 'c' prefix if present + cleaned = part.lower().replace("c", "", 1).strip() + + # Check if it's a valid numeric ID + if cleaned.isdigit(): + case_id = int(cleaned) + # Deduplicate + if case_id not in case_ids: + case_ids.append(case_id) + + # Return None if no valid IDs found + if not case_ids: + return None + # Return int for single ID (backwards compatibility after deduplication) + elif len(case_ids) == 1: + return case_ids[0] + # Return list for multiple IDs + else: + return case_ids + else: + # Single case ID (original behavior) + cleaned = test_id_value.lower().replace("c", "", 1).strip() + if cleaned.isdigit(): + return int(cleaned) + return None + def _get_status_id_for_case_result(self, case: JUnitTestCase) -> Union[int, None]: if case.is_passed: status = "passed" @@ -202,47 +261,93 @@ def _parse_test_cases(self, section) -> List[TestRailCase]: result_fields_dict, case_fields_dict = self._resolve_case_fields(result_fields, case_fields) status_id = self._get_status_id_for_case_result(case) comment = self._get_comment_for_case_result(case) - result = TestRailResult( - case_id=case_id, - elapsed=case.time, - attachments=attachments, - result_fields=result_fields_dict, - custom_step_results=result_steps, - status_id=status_id, - comment=comment, - ) - - for comment in reversed(comments): - result.prepend_comment(comment) - if sauce_session: - result.prepend_comment(f"SauceLabs session: {sauce_session}") - automation_id = case_fields_dict.pop(OLD_SYSTEM_NAME_AUTOMATION_ID, None) or case._elem.get( + # Prepare data that will be shared across all case IDs (if multiple) + base_automation_id = case_fields_dict.pop(OLD_SYSTEM_NAME_AUTOMATION_ID, None) or case._elem.get( OLD_SYSTEM_NAME_AUTOMATION_ID, automation_id ) + base_title = TestRailCaseFieldsOptimizer.extract_last_words( + case_name, TestRailCaseFieldsOptimizer.MAX_TESTCASE_TITLE_LENGTH + ) - # Create TestRailCase kwargs - case_kwargs = { - "title": TestRailCaseFieldsOptimizer.extract_last_words( - case_name, TestRailCaseFieldsOptimizer.MAX_TESTCASE_TITLE_LENGTH - ), - "case_id": case_id, - "result": result, - "custom_automation_id": automation_id, - "case_fields": case_fields_dict, - } + # Check if case_id is a list (multiple IDs) or single value + if isinstance(case_id, list): + # Multiple case IDs: create a TestRailCase for each ID with same result data + for individual_case_id in case_id: + # Create a new result object for each case (avoid sharing references) + result = TestRailResult( + case_id=individual_case_id, + elapsed=case.time, + attachments=attachments.copy() if attachments else [], + result_fields=result_fields_dict.copy(), + custom_step_results=result_steps.copy() if result_steps else [], + status_id=status_id, + comment=comment, + ) + + # Apply comment prepending + for comment_text in reversed(comments): + result.prepend_comment(comment_text) + if sauce_session: + result.prepend_comment(f"SauceLabs session: {sauce_session}") + + # Create TestRailCase kwargs + case_kwargs = { + "title": base_title, + "case_id": individual_case_id, + "result": result, + "custom_automation_id": base_automation_id, + "case_fields": case_fields_dict.copy(), + } + + # Only set refs field if case_refs has actual content + if case_refs and case_refs.strip(): + case_kwargs["refs"] = case_refs + + test_case = TestRailCase(**case_kwargs) + + # Store JUnit references as a temporary attribute for case updates (not serialized) + if case_refs and case_refs.strip(): + test_case._junit_case_refs = case_refs + + test_cases.append(test_case) + else: + # Single case ID: existing behavior (backwards compatibility) + result = TestRailResult( + case_id=case_id, + elapsed=case.time, + attachments=attachments, + result_fields=result_fields_dict, + custom_step_results=result_steps, + status_id=status_id, + comment=comment, + ) + + for comment_text in reversed(comments): + result.prepend_comment(comment_text) + if sauce_session: + result.prepend_comment(f"SauceLabs session: {sauce_session}") + + # Create TestRailCase kwargs + case_kwargs = { + "title": base_title, + "case_id": case_id, + "result": result, + "custom_automation_id": base_automation_id, + "case_fields": case_fields_dict, + } - # Only set refs field if case_refs has actual content - if case_refs and case_refs.strip(): - case_kwargs["refs"] = case_refs + # Only set refs field if case_refs has actual content + if case_refs and case_refs.strip(): + case_kwargs["refs"] = case_refs - test_case = TestRailCase(**case_kwargs) + test_case = TestRailCase(**case_kwargs) - # Store JUnit references as a temporary attribute for case updates (not serialized) - if case_refs and case_refs.strip(): - test_case._junit_case_refs = case_refs + # Store JUnit references as a temporary attribute for case updates (not serialized) + if case_refs and case_refs.strip(): + test_case._junit_case_refs = case_refs - test_cases.append(test_case) + test_cases.append(test_case) return test_cases From e8b12181808b9488da6fd24cb7223e8b98b47358 Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Fri, 13 Feb 2026 17:37:38 +0800 Subject: [PATCH 2/2] TRCLI-135: Added unit tests, test data and updated readme file for multiple case id support --- README.md | 23 ++ .../XML/multiple_case_ids_in_name.xml | 37 +++ .../XML/multiple_case_ids_in_property.xml | 30 ++ tests/test_multiple_case_ids.py | 277 ++++++++++++++++++ 4 files changed, 367 insertions(+) create mode 100644 tests/test_data/XML/multiple_case_ids_in_name.xml create mode 100644 tests/test_data/XML/multiple_case_ids_in_property.xml create mode 100644 tests/test_multiple_case_ids.py diff --git a/README.md b/README.md index b28bd8d..e2ecc92 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,17 @@ From an implementation perspective, you can do this in one of two ways: ``` + **Multiple Case IDs (name-based):** + You can map a single test to multiple TestRail case IDs using comma-separated values in brackets: + ```xml + + ``` + Or using underscore-separated format: + ```xml + + ``` + When a test with multiple case IDs executes, all mapped case IDs receive the same result status. + 2. Map by setting the case ID in a test case property, using case-matcher `property`: ```xml @@ -267,6 +278,18 @@ From an implementation perspective, you can do this in one of two ways: ``` + + **Multiple Case IDs (property-based):** + You can map a single test to multiple TestRail case IDs using comma-separated values: + ```xml + + + + + + ``` + When a test with multiple case IDs executes, all mapped case IDs receive the same result status. + > **Important usage notes:** > - We recommend using the `-n` option to skip creating new test cases due to the potential risk of duplication diff --git a/tests/test_data/XML/multiple_case_ids_in_name.xml b/tests/test_data/XML/multiple_case_ids_in_name.xml new file mode 100644 index 0000000..8e0e771 --- /dev/null +++ b/tests/test_data/XML/multiple_case_ids_in_name.xml @@ -0,0 +1,37 @@ + + + + + + + Combined test for login scenarios: valid credentials, invalid password, locked account + + + + + Combined test for logout scenarios: normal logout, session timeout + + + + + + Expected: API endpoint should return expected response for all cases + Actual: API endpoint returned expected response for C1050394 and C1050395, but failed for C1050396 + + + + + + + Expected: Registration failed with error "Invalid credentials" + Actual: Registration succeeded without error + + + + + + Single test case using underscore format + + + + diff --git a/tests/test_data/XML/multiple_case_ids_in_property.xml b/tests/test_data/XML/multiple_case_ids_in_property.xml new file mode 100644 index 0000000..804a317 --- /dev/null +++ b/tests/test_data/XML/multiple_case_ids_in_property.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Expected: Login failed + Actual: Login succeeded + + + + diff --git a/tests/test_multiple_case_ids.py b/tests/test_multiple_case_ids.py new file mode 100644 index 0000000..433ce7e --- /dev/null +++ b/tests/test_multiple_case_ids.py @@ -0,0 +1,277 @@ +""" +Unit tests for multiple case ID feature (GitHub #343) + +Tests the ability to map a single JUnit test to multiple TestRail case IDs +using comma-separated values in the test_id property. +""" + +import pytest +from trcli.readers.junit_xml import JunitParser + + +class TestParseMultipleCaseIds: + """Test cases for JunitParser._parse_multiple_case_ids static method""" + + @pytest.mark.parametrize( + "input_value, expected_output", + [ + # Single case ID (backwards compatibility) + ("C123", 123), + ("c123", 123), + ("123", 123), + (" C123 ", 123), + (" 123 ", 123), + # Multiple case IDs + ("C123, C456, C789", [123, 456, 789]), + ("C123,C456,C789", [123, 456, 789]), + ("123, 456, 789", [123, 456, 789]), + ("123,456,789", [123, 456, 789]), + # Mixed case + ("c123, C456, c789", [123, 456, 789]), + # Whitespace variations + ("C123 , C456 , C789", [123, 456, 789]), + (" C123 , C456 , C789 ", [123, 456, 789]), + ("C123 , C456 , C789", [123, 456, 789]), + # Deduplication + ("C123, C123", 123), # Returns single int when deduplicated to one + ("C123, C456, C123", [123, 456]), + ("C100, C200, C100, C300, C200", [100, 200, 300]), + # Invalid inputs (should be ignored) + ("C123, invalid, C456", [123, 456]), + ("C123, , C456", [123, 456]), # Empty part + ("C123, C, C456", [123, 456]), # C without number + ("C123, abc, C456", [123, 456]), + ("invalid", None), + ("", None), + (" ", None), + (",,,", None), + # Edge cases + ("C1", 1), + ("C999999", 999999), + ("C1, C2, C3", [1, 2, 3]), + ("1,2,3,4,5", [1, 2, 3, 4, 5]), + ], + ) + def test_parse_multiple_case_ids(self, input_value, expected_output): + """Test parsing of single and multiple case IDs""" + result = JunitParser._parse_multiple_case_ids(input_value) + assert result == expected_output, f"Failed for input: '{input_value}'" + + def test_parse_multiple_case_ids_none_input(self): + """Test handling of None input""" + result = JunitParser._parse_multiple_case_ids(None) + assert result is None + + def test_parse_multiple_case_ids_very_long_list(self): + """Test handling of very long lists (100+ case IDs)""" + # Create a list of 150 case IDs + case_ids = [f"C{i}" for i in range(1, 151)] + input_value = ", ".join(case_ids) + + result = JunitParser._parse_multiple_case_ids(input_value) + + assert isinstance(result, list) + assert len(result) == 150 + assert result[0] == 1 + assert result[-1] == 150 + + def test_parse_multiple_case_ids_preserves_order(self): + """Test that order is preserved when parsing multiple IDs""" + result = JunitParser._parse_multiple_case_ids("C789, C123, C456") + assert result == [789, 123, 456], "Order should be preserved" + + def test_parse_multiple_case_ids_mixed_valid_invalid(self): + """Test handling of mixed valid and invalid case IDs""" + # Should extract only valid IDs and ignore invalid ones + result = JunitParser._parse_multiple_case_ids("C123, invalid, C456, abc, C789, xyz") + assert result == [123, 456, 789] + + def test_parse_multiple_case_ids_special_characters(self): + """Test that special characters are handled correctly""" + # These should not be parsed as valid case IDs + assert JunitParser._parse_multiple_case_ids("C123!, C456#") is None + assert JunitParser._parse_multiple_case_ids("C-123, C+456") is None + + def test_backwards_compatibility_single_id(self): + """Ensure single case ID returns integer (not list) for backwards compatibility""" + # Single IDs should return int, not list + assert JunitParser._parse_multiple_case_ids("C123") == 123 + assert not isinstance(JunitParser._parse_multiple_case_ids("C123"), list) + + # Single ID after deduplication should also return int + assert JunitParser._parse_multiple_case_ids("C123, C123, C123") == 123 + assert not isinstance(JunitParser._parse_multiple_case_ids("C123, C123, C123"), list) + + +class TestMultipleCaseIdsIntegration: + """Integration tests for multiple case ID feature with JUnit XML parsing""" + + @pytest.fixture + def mock_environment(self, mocker, tmp_path): + """Create a mock environment for testing""" + # Create a dummy XML file + xml_file = tmp_path / "test.xml" + xml_file.write_text( + '' + ) + + env = mocker.Mock() + env.case_matcher = "property" + env.special_parser = None + env.params_from_config = {} + env.file = str(xml_file) + return env + + def test_extract_single_case_id_property(self, mock_environment, mocker): + """Test extraction of single case ID from property (backwards compatibility)""" + parser = JunitParser(mock_environment) + + # Mock a testcase with single test_id property + mock_case = mocker.Mock() + mock_case.name = "test_example" + + mock_prop = mocker.Mock() + mock_prop.name = "test_id" + mock_prop.value = "C123" + + mock_props = mocker.Mock() + mock_props.iterchildren.return_value = [mock_prop] + + mock_case.iterchildren.return_value = [mock_props] + + case_id, case_name = parser._extract_case_id_and_name(mock_case) + + assert case_id == 123 + assert case_name == "test_example" + + def test_extract_multiple_case_ids_property(self, mock_environment, mocker): + """Test extraction of multiple case IDs from property""" + parser = JunitParser(mock_environment) + + # Mock a testcase with multiple test_ids + mock_case = mocker.Mock() + mock_case.name = "test_combined_scenario" + + mock_prop = mocker.Mock() + mock_prop.name = "test_id" + mock_prop.value = "C123, C456, C789" + + mock_props = mocker.Mock() + mock_props.iterchildren.return_value = [mock_prop] + + mock_case.iterchildren.return_value = [mock_props] + + case_id, case_name = parser._extract_case_id_and_name(mock_case) + + assert case_id == [123, 456, 789] + assert case_name == "test_combined_scenario" + + def test_multiple_case_ids_ignored_for_name_matcher(self, mock_environment, mocker): + """Test that multiple case IDs in property are ignored when using name matcher""" + mock_environment.case_matcher = "name" + parser = JunitParser(mock_environment) + + # When using name matcher, we parse from the name, not the property + mock_case = mocker.Mock() + mock_case.name = "test_C100_example" + mock_case.iterchildren.return_value = [] + + # Mock the MatchersParser.parse_name_with_id + with mocker.patch( + "trcli.readers.junit_xml.MatchersParser.parse_name_with_id", return_value=(100, "test_example") + ): + case_id, case_name = parser._extract_case_id_and_name(mock_case) + + # With name matcher, it should extract from name (not property) + assert case_id == 100 + assert case_name == "test_example" + + +class TestMultipleCaseIdsEndToEnd: + """End-to-end tests for multiple case ID feature with real JUnit XML""" + + @pytest.fixture + def mock_environment(self, mocker): + """Create a mock environment for end-to-end testing""" + env = mocker.Mock() + env.case_matcher = "property" + env.special_parser = None + env.params_from_config = {} + env.file = "tests/test_data/XML/multiple_case_ids_in_property.xml" + env.suite_name = None + return env + + def test_parse_junit_xml_with_multiple_case_ids(self, mock_environment): + """Test end-to-end parsing of JUnit XML with multiple case IDs""" + parser = JunitParser(mock_environment) + suites = parser.parse_file() + + assert suites is not None + assert len(suites) > 0 + + # Get all test cases across all suites and sections + all_test_cases = [] + for suite in suites: + for section in suite.testsections: + all_test_cases.extend(section.testcases) + + # We should have 8 test cases total: + # - Test 1: 1 case (C1050381) + # - Test 2: 3 cases (C1050382, C1050383, C1050384) + # - Test 3: 4 cases (C1050385, C1050386, C1050387, C1050388) + assert len(all_test_cases) == 8 + + # Find test cases by case_id + case_ids = [tc.case_id for tc in all_test_cases] + assert 1050381 in case_ids # Single case ID + + # Multiple case IDs from test 2 + assert 1050382 in case_ids + assert 1050383 in case_ids + assert 1050384 in case_ids + + # Multiple case IDs from test 3 + assert 1050385 in case_ids + assert 1050386 in case_ids + assert 1050387 in case_ids + assert 1050388 in case_ids + + # Verify that test cases with same source test have same title + combined_test_cases = [tc for tc in all_test_cases if tc.case_id in [1050382, 1050383, 1050384]] + assert len(combined_test_cases) == 3 + assert combined_test_cases[0].title == combined_test_cases[1].title == combined_test_cases[2].title + + # Verify all combined test cases have the same result status + assert combined_test_cases[0].result.status_id == combined_test_cases[1].result.status_id + assert combined_test_cases[1].result.status_id == combined_test_cases[2].result.status_id + + # Verify comment is preserved across all cases + if combined_test_cases[0].result.comment: + assert "Combined test covering multiple scenarios" in combined_test_cases[0].result.comment + assert combined_test_cases[0].result.comment == combined_test_cases[1].result.comment + + def test_multiple_case_ids_all_get_same_result(self, mock_environment): + """Verify that all case IDs from one test get the same result data""" + parser = JunitParser(mock_environment) + suites = parser.parse_file() + + # Get test cases for C1050382, C1050383, C1050384 (from the same JUnit test) + all_test_cases = [] + for suite in suites: + for section in suite.testsections: + all_test_cases.extend(section.testcases) + + combined_cases = [tc for tc in all_test_cases if tc.case_id in [1050382, 1050383, 1050384]] + assert len(combined_cases) == 3 + + # All should have same status + statuses = [tc.result.status_id for tc in combined_cases] + assert len(set(statuses)) == 1, "All cases should have the same status" + + # All should have same elapsed time + elapsed_times = [tc.result.elapsed for tc in combined_cases] + assert len(set(elapsed_times)) == 1, "All cases should have the same elapsed time" + + # All should have same automation_id + automation_ids = [tc.custom_automation_id for tc in combined_cases] + assert len(set(automation_ids)) == 1, "All cases should have the same automation_id"