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"
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