trcli
Advanced tools
| """ | ||
| 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( | ||
| '<testsuites><testsuite name="test"><testcase name="test" classname="Test"/></testsuite></testsuites>' | ||
| ) | ||
| 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" |
+1
-1
| Metadata-Version: 2.4 | ||
| Name: trcli | ||
| Version: 1.13.0 | ||
| Version: 1.13.1 | ||
| License-File: LICENSE.md | ||
@@ -5,0 +5,0 @@ Requires-Dist: click<8.2.2,>=8.1.0 |
| Metadata-Version: 2.4 | ||
| Name: trcli | ||
| Version: 1.13.0 | ||
| Version: 1.13.1 | ||
| License-File: LICENSE.md | ||
@@ -5,0 +5,0 @@ Requires-Dist: click<8.2.2,>=8.1.0 |
@@ -27,2 +27,3 @@ LICENSE.md | ||
| tests/test_matchers_parser.py | ||
| tests/test_multiple_case_ids.py | ||
| tests/test_project_based_client.py | ||
@@ -29,0 +30,0 @@ tests/test_response_verify.py |
@@ -1,1 +0,1 @@ | ||
| __version__ = "1.13.0" | ||
| __version__ = "1.13.1" |
@@ -90,4 +90,17 @@ """ | ||
| ) | ||
| # Validate that we have test cases to include in the run | ||
| # Empty runs are not allowed unless include_all is True | ||
| if not include_all and (not add_run_data.get("case_ids") or len(add_run_data["case_ids"]) == 0): | ||
| error_msg = ( | ||
| "Cannot create test run: No test cases were matched.\n" | ||
| " - For parse_junit: Ensure tests have automation_id/test ids that matches existing cases in TestRail\n" | ||
| " - For parse_cucumber: Ensure features have names or @C test id tag matching the existing BDD cases" | ||
| ) | ||
| return None, error_msg | ||
| if not plan_id: | ||
| response = self.client.send_post(f"add_run/{project_id}", add_run_data) | ||
| if response.error_message: | ||
| return None, response.error_message | ||
| run_id = response.response_text.get("id") | ||
@@ -106,2 +119,4 @@ else: | ||
| response = self.client.send_post(f"add_plan_entry/{plan_id}", entry_data) | ||
| if response.error_message: | ||
| return None, response.error_message | ||
| run_id = response.response_text["runs"][0]["id"] | ||
@@ -108,0 +123,0 @@ return run_id, response.error_message |
@@ -12,4 +12,6 @@ import re, ast | ||
| @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" | ||
@@ -25,5 +27,38 @@ - "my test case C123" | ||
| 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 [" ", "_"]: | ||
@@ -35,3 +70,3 @@ parts = case_name.split(char) | ||
| 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(): | ||
@@ -41,15 +76,72 @@ parts_copy.pop(idx) | ||
| 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: | ||
@@ -79,2 +171,3 @@ | ||
| class TestRailCaseFieldsOptimizer: | ||
@@ -90,7 +183,7 @@ | ||
| # 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()) | ||
@@ -111,3 +204,3 @@ # Split the cleaned string into words | ||
| # Reverse the extracted words to maintain the original order | ||
| result = ' '.join(reversed(extracted_words)) | ||
| result = " ".join(reversed(extracted_words)) | ||
@@ -118,2 +211,2 @@ # as fallback, return the last characters if the result is empty | ||
| return result | ||
| return result |
+139
-34
@@ -110,3 +110,3 @@ import glob | ||
| 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 | ||
@@ -116,2 +116,61 @@ | ||
| @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]: | ||
@@ -207,44 +266,90 @@ if case.is_passed: | ||
| 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, | ||
| ) | ||
| # Only set refs field if case_refs has actual content | ||
| if case_refs and case_refs.strip(): | ||
| case_kwargs["refs"] = case_refs | ||
| # 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}") | ||
| test_case = TestRailCase(**case_kwargs) | ||
| # 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(), | ||
| } | ||
| # 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 | ||
| # Only set refs field if case_refs has actual content | ||
| if case_refs and case_refs.strip(): | ||
| case_kwargs["refs"] = case_refs | ||
| test_cases.append(test_case) | ||
| 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 | ||
| 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) | ||
| return test_cases | ||
@@ -251,0 +356,0 @@ |
Sorry, the diff of this file is too big to display
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
1004706
2.16%87
1.16%18578
2.2%