From af9e3be968c925bc40dd4870ca7cb98612752aa5 Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Mon, 6 Apr 2026 15:26:35 +0800 Subject: [PATCH 1/7] Updated files for v1.14.1 --- CHANGELOG.MD | 6 ++++++ README.md | 8 ++++---- trcli/__init__.py | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index ec8077f..8931c5a 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -6,6 +6,12 @@ This project adheres to [Semantic Versioning](https://semver.org/). Version numb - **MINOR**: New features that are backward-compatible. - **PATCH**: Bug fixes or minor changes that do not affect backward compatibility. +## [1.14.1] + +_released 04-14-2026 + +### Fixed + - ## [1.14.0] diff --git a/README.md b/README.md index aa78cdc..7646225 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ trcli ``` You should get something like this: ``` -TestRail CLI v1.14.0 +TestRail CLI v1.14.1 Copyright 2025 Gurock Software GmbH - www.gurock.com Supported and loaded modules: - parse_junit: JUnit XML Files (& Similar) @@ -51,7 +51,7 @@ CLI general reference -------- ```shell $ trcli --help -TestRail CLI v1.14.0 +TestRail CLI v1.14.1 Copyright 2025 Gurock Software GmbH - www.gurock.com Usage: trcli [OPTIONS] COMMAND [ARGS]... @@ -1675,7 +1675,7 @@ Options: ### Reference ```shell $ trcli add_run --help -TestRail CLI v1.14.0 +TestRail CLI v1.14.1 Copyright 2025 Gurock Software GmbH - www.gurock.com Usage: trcli add_run [OPTIONS] @@ -1885,7 +1885,7 @@ providing you with a solid base of test cases, which you can further expand on T ### Reference ```shell $ trcli parse_openapi --help -TestRail CLI v1.14.0 +TestRail CLI v1.14.1 Copyright 2025 Gurock Software GmbH - www.gurock.com Usage: trcli parse_openapi [OPTIONS] diff --git a/trcli/__init__.py b/trcli/__init__.py index b9f68ed..4454c8d 100644 --- a/trcli/__init__.py +++ b/trcli/__init__.py @@ -1 +1 @@ -__version__ = "1.14.0" +__version__ = "1.14.1" From 9d7e90a51b6111ed13cfc6b960e57d11ae9e8e8b Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Mon, 6 Apr 2026 15:51:59 +0800 Subject: [PATCH 2/7] TRCLI-246: Fixed mapping to use object identity instead of case id which causes attachments to be uploaded to last test result --- trcli/api/api_request_handler.py | 4 ++-- trcli/api/result_handler.py | 23 ++++++++++++----------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/trcli/api/api_request_handler.py b/trcli/api/api_request_handler.py index cb29118..cf44c01 100644 --- a/trcli/api/api_request_handler.py +++ b/trcli/api/api_request_handler.py @@ -286,8 +286,8 @@ def update_existing_case_references( ) -> Tuple[bool, str, List[str], List[str], List[str]]: return self.case_handler.update_existing_case_references(case_id, junit_refs, case_fields, strategy) - def upload_attachments(self, report_results: List[Dict], case_id_to_result_id: Dict[int, int]): - return self.result_handler.upload_attachments(report_results, case_id_to_result_id) + def upload_attachments(self, report_results: List[Dict], request_id_to_result_id: Dict[int, int]): + return self.result_handler.upload_attachments(report_results, request_id_to_result_id) def add_results(self, run_id: int) -> Tuple[List, str, int]: return self.result_handler.add_results(run_id) diff --git a/trcli/api/result_handler.py b/trcli/api/result_handler.py index 105171e..56a39e3 100644 --- a/trcli/api/result_handler.py +++ b/trcli/api/result_handler.py @@ -44,17 +44,18 @@ def __init__( self.__get_all_tests_in_run = get_all_tests_in_run_callback self.handle_futures = handle_futures_callback - def upload_attachments(self, report_results: List[Dict], case_id_to_result_id: Dict[int, int]): + def upload_attachments(self, report_results: List[Dict], request_id_to_result_id: Dict[int, int]): """ Upload attachments to test results. :param report_results: List of test results with attachments from report - :param case_id_to_result_id: Mapping from case_id to result_id + :param request_id_to_result_id: Mapping from request object id to result_id """ failed_uploads = [] for report_result in report_results: case_id = report_result["case_id"] - result_id = case_id_to_result_id.get(case_id) + # Use object identity to find the correct result_id for THIS specific result + result_id = request_id_to_result_id.get(id(report_result)) if result_id is None: self.environment.elog(f"Unable to find result_id for case {case_id}, skipping attachments.") @@ -133,18 +134,18 @@ def add_results(self, run_id: int) -> Tuple[List, str, int]: # Iterate through futures to get all responses from done tasks (not cancelled) responses = ResultHandler.retrieve_results_after_cancelling(futures) responses = [response.response_text for response in responses] - results = [result for results_list in responses for result in results_list] - # Build case_id to result_id mapping based on order correspondence + # Build request to result_id mapping based on order correspondence # TestRail API preserves order, so we can match requests to responses - case_id_to_result_id = {} + # Use id() to uniquely identify each request result object (handles duplicate case_ids) + request_id_to_result_id = {} for request_body, response_results in zip(add_results_data_chunks, responses): - # Match request case_ids to response result_ids by order + # Match request result objects to response result_ids by order for request_result, response_result in zip(request_body["results"], response_results): - case_id = request_result.get("case_id") result_id = response_result.get("id") - if case_id and result_id: - case_id_to_result_id[case_id] = result_id + if result_id: + # Use object identity to uniquely map each request to its API response + request_id_to_result_id[id(request_result)] = result_id report_results_w_attachments = [] for results_data_chunk in add_results_data_chunks: @@ -158,7 +159,7 @@ def add_results(self, run_id: int) -> Tuple[List, str, int]: self.environment.log( f"Uploading {attachments_count} attachments " f"for {len(report_results_w_attachments)} test results." ) - self.upload_attachments(report_results_w_attachments, case_id_to_result_id) + self.upload_attachments(report_results_w_attachments, request_id_to_result_id) else: self.environment.log(f"No attachments found to upload.") From 4d7fbe3447e0b67c80e83c78716ff525ab32bd39 Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Mon, 6 Apr 2026 15:53:30 +0800 Subject: [PATCH 3/7] TRCLI-246: Updated unit test and added edge case for uploading attachments to correct results when case_id appears multiple times --- tests/test_api_request_handler.py | 63 +++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/tests/test_api_request_handler.py b/tests/test_api_request_handler.py index ad5912a..0a67867 100644 --- a/tests/test_api_request_handler.py +++ b/tests/test_api_request_handler.py @@ -1377,10 +1377,10 @@ def test_upload_attachments_413_error(self, api_request_handler: ApiRequestHandl # Prepare test data report_results = [{"case_id": 100, "attachments": [str(test_file)]}] - case_id_to_result_id = {100: 2001} + request_id_to_result_id = {id(report_results[0]): 2001} # Call upload_attachments - api_request_handler.upload_attachments(report_results, case_id_to_result_id) + api_request_handler.upload_attachments(report_results, request_id_to_result_id) # Verify the request was made (case-insensitive comparison) assert requests_mock.last_request.url.lower() == create_url("add_attachment_to_result/2001").lower() @@ -1397,10 +1397,10 @@ def test_upload_attachments_success(self, api_request_handler: ApiRequestHandler # Prepare test data report_results = [{"case_id": 100, "attachments": [str(test_file)]}] - case_id_to_result_id = {100: 2001} + request_id_to_result_id = {id(report_results[0]): 2001} # Call upload_attachments - api_request_handler.upload_attachments(report_results, case_id_to_result_id) + api_request_handler.upload_attachments(report_results, request_id_to_result_id) # Verify the request was made (case-insensitive comparison) assert requests_mock.last_request.url.lower() == create_url("add_attachment_to_result/2001").lower() @@ -1410,10 +1410,10 @@ def test_upload_attachments_file_not_found(self, api_request_handler: ApiRequest """Test that missing attachment files are properly reported.""" # Prepare test data with non-existent file report_results = [{"case_id": 100, "attachments": ["/path/to/nonexistent/file.jpg"]}] - case_id_to_result_id = {100: 2001} + request_id_to_result_id = {id(report_results[0]): 2001} # Call upload_attachments - should not raise exception - api_request_handler.upload_attachments(report_results, case_id_to_result_id) + api_request_handler.upload_attachments(report_results, request_id_to_result_id) @pytest.mark.api_handler def test_upload_attachments_empty_run_scenario( @@ -1423,8 +1423,8 @@ def test_upload_attachments_empty_run_scenario( This test covers the bug fix for issue where TRCLI failed to upload attachments when using --run-id with an empty run (created via API with include_all: false - and no case_ids). The fix uses case_id to result_id mapping instead of fetching - tests from the run. + and no case_ids). The fix uses request object identity to result_id mapping instead + of case_id mapping, which correctly handles duplicate case_ids. """ # Create test attachment files attachment1 = tmp_path / "screenshot1.png" @@ -1442,11 +1442,11 @@ def test_upload_attachments_empty_run_scenario( {"case_id": 101, "attachments": [str(attachment2)]}, ] - # Case ID to result ID mapping (this is built from add_results_for_cases response) - case_id_to_result_id = {100: 5001, 101: 5002} + # Request object ID to result ID mapping (this is built from add_results_for_cases response) + request_id_to_result_id = {id(report_results[0]): 5001, id(report_results[1]): 5002} # Call upload_attachments - api_request_handler.upload_attachments(report_results, case_id_to_result_id) + api_request_handler.upload_attachments(report_results, request_id_to_result_id) # Verify both attachments were uploaded correctly history = requests_mock.request_history @@ -1458,6 +1458,47 @@ def test_upload_attachments_empty_run_scenario( assert any("add_attachment_to_result/5001" in url for url in urls), "Should upload to result 5001" assert any("add_attachment_to_result/5002" in url for url in urls), "Should upload to result 5002" + @pytest.mark.api_handler + def test_upload_attachments_duplicate_case_ids_different_results( + self, api_request_handler: ApiRequestHandler, requests_mock, tmp_path + ): + """Test that attachments are uploaded to correct results when same case_id appears multiple times.""" + # Create test attachment files + attachment1 = tmp_path / "report1_screenshot.png" + attachment1.write_text("screenshot from report 1") + attachment2 = tmp_path / "report2_screenshot.png" + attachment2.write_text("screenshot from report 2") + + # Mock successful attachment uploads + requests_mock.post(create_url("add_attachment_to_result/1001"), status_code=200, json={"attachment_id": 9001}) + requests_mock.post(create_url("add_attachment_to_result/1002"), status_code=200, json={"attachment_id": 9002}) + + # Prepare test data - SAME case_id (123) but different result objects with different attachments + # This simulates what happens when glob pattern processes multiple reports with the same test + result1 = {"case_id": 123, "attachments": [str(attachment1)]} + result2 = {"case_id": 123, "attachments": [str(attachment2)]} + report_results = [result1, result2] + + # Request object ID to result ID mapping + # Key insight: We map by object identity, NOT by case_id + request_id_to_result_id = { + id(result1): 1001, # First occurrence of case 123 + id(result2): 1002, # Second occurrence of case 123 + } + + # Call upload_attachments + api_request_handler.upload_attachments(report_results, request_id_to_result_id) + + # Verify both attachments were uploaded correctly + history = requests_mock.request_history + upload_requests = [req for req in history if "add_attachment_to_result" in req.url] + assert len(upload_requests) == 2, "Should have uploaded 2 attachments" + + # Verify attachments went to DIFFERENT result IDs (not both to 1002) + urls = [req.url.lower() for req in upload_requests] + assert any("add_attachment_to_result/1001" in url for url in urls), "Should upload attachment1 to result 1001" + assert any("add_attachment_to_result/1002" in url for url in urls), "Should upload attachment2 to result 1002" + @pytest.mark.api_handler def test_caching_reduces_api_calls(self, api_request_handler: ApiRequestHandler, requests_mock): """Test that caching reduces the number of API calls for repeated requests""" From 845c310b855f34dea3234fb206f259e941456907 Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Wed, 8 Apr 2026 17:12:08 +0800 Subject: [PATCH 4/7] TRCLI-250: Added is_legacy flag to ensure TestRail auto converts markdown to html --- trcli/api/bdd_handler.py | 3 +++ trcli/api/case_handler.py | 8 +++++++- trcli/api/label_manager.py | 5 ++++- trcli/api/reference_manager.py | 9 ++++++--- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/trcli/api/bdd_handler.py b/trcli/api/bdd_handler.py index 6a3c49b..c268065 100644 --- a/trcli/api/bdd_handler.py +++ b/trcli/api/bdd_handler.py @@ -207,6 +207,9 @@ def add_case_bdd( if ref_tags: body["refs"] = ", ".join(ref_tags) + # Add is_legacy flag for TestRail v9.8.1+ to convert Markdown content to HTML + body["is_legacy"] = True + response = self.client.send_post(f"add_case/{section_id}", body) if response.status_code == 200: diff --git a/trcli/api/case_handler.py b/trcli/api/case_handler.py index d8902a4..75c0761 100644 --- a/trcli/api/case_handler.py +++ b/trcli/api/case_handler.py @@ -109,6 +109,8 @@ def _add_case_and_update_data(self, case: TestRailCase) -> APIClientResult: case_body[UPDATED_SYSTEM_NAME_AUTOMATION_ID] = case_body.pop(OLD_SYSTEM_NAME_AUTOMATION_ID) if self.environment.case_matcher != MatchersParser.AUTO and OLD_SYSTEM_NAME_AUTOMATION_ID in case_body: case_body.pop(OLD_SYSTEM_NAME_AUTOMATION_ID) + # Add is_legacy flag for TestRail v9.8.1+ to convert Markdown content to HTML + case_body["is_legacy"] = True response = self.client.send_post(f"add_case/{case_body.pop('section_id')}", case_body) if response.status_code == 200: case.case_id = response.response_text["id"] @@ -210,6 +212,9 @@ def update_existing_case_references( if not update_data: return True, None, added_refs, skipped_refs, updated_fields + # Add is_legacy flag for TestRail v9.8.1+ to convert Markdown content to HTML + update_data["is_legacy"] = True + # Update the case update_response = self.client.send_post(f"update_case/{case_id}", update_data) @@ -245,7 +250,8 @@ def update_case_automation_id(self, case_id: int, automation_id: str) -> Tuple[b """ self.environment.vlog(f"Setting automation_id '{automation_id}' on case {case_id}") - update_data = {"custom_automation_id": automation_id} + # Add is_legacy flag for TestRail v9.8.1+ to convert Markdown content to HTML + update_data = {"custom_automation_id": automation_id, "is_legacy": True} update_response = self.client.send_post(f"update_case/{case_id}", update_data) if update_response.status_code == 200: diff --git a/trcli/api/label_manager.py b/trcli/api/label_manager.py index e6f444b..c8f69d7 100644 --- a/trcli/api/label_manager.py +++ b/trcli/api/label_manager.py @@ -221,7 +221,8 @@ def add_labels_to_cases( if len(cases_to_update) == 1: # Single case: use update_case/{case_id} case_info = cases_to_update[0] - case_update_data = {"labels": case_info["labels"]} + # Add is_legacy flag for TestRail v9.8.1+ to convert Markdown content to HTML + case_update_data = {"labels": case_info["labels"], "is_legacy": True} update_response = self.client.send_post(f"update_case/{case_info['case_id']}", payload=case_update_data) @@ -266,9 +267,11 @@ def add_labels_to_cases( ) else: # Batch update using update_cases/{suite_id} + # Add is_legacy flag for TestRail v9.8.1+ to convert Markdown content to HTML batch_update_data = { "case_ids": [case_info["case_id"] for case_info in cases_to_update], "labels": cases_to_update[0]["labels"], # Assuming same labels for all cases + "is_legacy": True, } batch_response = self.client.send_post(f"update_cases/{case_suite_id}", payload=batch_update_data) diff --git a/trcli/api/reference_manager.py b/trcli/api/reference_manager.py index 19c4e26..480be36 100644 --- a/trcli/api/reference_manager.py +++ b/trcli/api/reference_manager.py @@ -65,7 +65,8 @@ def add_case_references(self, case_id: int, references: List[str]) -> Tuple[bool return False, error_msg # Update the test case with new references - update_response = self.client.send_post(f"update_case/{case_id}", {"refs": new_refs_string}) + # Add is_legacy flag for TestRail v9.8.1+ to convert Markdown content to HTML + update_response = self.client.send_post(f"update_case/{case_id}", {"refs": new_refs_string, "is_legacy": True}) if update_response.status_code == 200: return True, "" @@ -89,7 +90,8 @@ def update_case_references(self, case_id: int, references: List[str]) -> Tuple[b return False, error_msg # Update the test case with new references - update_response = self.client.send_post(f"update_case/{case_id}", {"refs": new_refs_string}) + # Add is_legacy flag for TestRail v9.8.1+ to convert Markdown content to HTML + update_response = self.client.send_post(f"update_case/{case_id}", {"refs": new_refs_string, "is_legacy": True}) if update_response.status_code == 200: return True, "" @@ -127,7 +129,8 @@ def delete_case_references(self, case_id: int, specific_references: Optional[Lis new_refs_string = merge_references(existing_refs, join_references(specific_references), strategy="delete") # Update the test case - update_response = self.client.send_post(f"update_case/{case_id}", {"refs": new_refs_string}) + # Add is_legacy flag for TestRail v9.8.1+ to convert Markdown content to HTML + update_response = self.client.send_post(f"update_case/{case_id}", {"refs": new_refs_string, "is_legacy": True}) if update_response.status_code == 200: return True, "" From 54532006875999aba685ff51b0c2298ab500bb18 Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Wed, 8 Apr 2026 17:13:16 +0800 Subject: [PATCH 5/7] TRCLI-250: Updated unit tests to include is_legacy flag --- ..._api_request_handler_case_fields_update.py | 4 +- tests/test_api_request_handler_labels.py | 6 +- tests/test_api_request_handler_references.py | 351 +++++++----------- 3 files changed, 130 insertions(+), 231 deletions(-) diff --git a/tests/test_api_request_handler_case_fields_update.py b/tests/test_api_request_handler_case_fields_update.py index 9033fd3..f233ef5 100644 --- a/tests/test_api_request_handler_case_fields_update.py +++ b/tests/test_api_request_handler_case_fields_update.py @@ -125,11 +125,11 @@ def test_update_case_with_only_refs_no_fields(self, handler): assert skipped_refs == [] assert updated_fields == [] - # Verify the API call included only refs + # Verify the API call included only refs and is_legacy handler.client.send_post.assert_called_once() call_args = handler.client.send_post.call_args update_data = call_args[0][1] - assert update_data == {"refs": "REQ-1"} + assert update_data == {"refs": "REQ-1", "is_legacy": True} def test_update_case_filters_internal_fields(self, handler): """Test that internal fields are filtered out from updates""" diff --git a/tests/test_api_request_handler_labels.py b/tests/test_api_request_handler_labels.py index 9a731c6..09178ca 100644 --- a/tests/test_api_request_handler_labels.py +++ b/tests/test_api_request_handler_labels.py @@ -341,7 +341,9 @@ def test_add_labels_to_cases_success(self): mock_add_label.assert_called_once_with(1, "test-label") assert mock_send_get.call_count == 2 # Should call update_cases/{suite_id} once for batch update - mock_send_post.assert_called_once_with("update_cases/1", payload={"case_ids": [1, 2], "labels": [5]}) + mock_send_post.assert_called_once_with( + "update_cases/1", payload={"case_ids": [1, 2], "labels": [5], "is_legacy": True} + ) def test_add_labels_to_cases_single_case(self): """Test adding labels to a single test case using update_case endpoint""" @@ -392,7 +394,7 @@ def test_add_labels_to_cases_single_case(self): mock_add_label.assert_called_once_with(1, "test-label") assert mock_send_get.call_count == 1 # Should call update_case/{case_id} once for single case - mock_send_post.assert_called_once_with("update_case/1", payload={"labels": [5]}) + mock_send_post.assert_called_once_with("update_case/1", payload={"labels": [5], "is_legacy": True}) def test_add_labels_to_cases_existing_label(self): """Test adding labels when label already exists""" diff --git a/tests/test_api_request_handler_references.py b/tests/test_api_request_handler_references.py index 17dc4a8..82c0235 100644 --- a/tests/test_api_request_handler_references.py +++ b/tests/test_api_request_handler_references.py @@ -17,145 +17,104 @@ def references_handler(self): environment.host = "https://test.testrail.com" environment.username = "test@example.com" environment.password = "password" - + mock_client = MagicMock() suite = TestRailSuite(name="Test Suite") - - handler = ApiRequestHandler( - environment=environment, - api_client=mock_client, - suites_data=suite, - verify=False - ) + + handler = ApiRequestHandler(environment=environment, api_client=mock_client, suites_data=suite, verify=False) return handler def test_add_case_references_success(self, references_handler): """Test successful addition of references to a test case""" # Mock get_case response mock_get_case_response = APIClientResult( - status_code=200, - response_text={ - "id": 1, - "title": "Test Case 1", - "refs": "REQ-1, REQ-2" - }, - error_message=None + status_code=200, response_text={"id": 1, "title": "Test Case 1", "refs": "REQ-1, REQ-2"}, error_message=None ) - + # Mock update_case response mock_update_response = APIClientResult( - status_code=200, - response_text={"id": 1, "refs": "REQ-1, REQ-2, REQ-3, REQ-4"}, - error_message=None + status_code=200, response_text={"id": 1, "refs": "REQ-1, REQ-2, REQ-3, REQ-4"}, error_message=None ) - - with patch.object(references_handler.client, 'send_get', return_value=mock_get_case_response), \ - patch.object(references_handler.client, 'send_post', return_value=mock_update_response): - - success, error = references_handler.add_case_references( - case_id=1, - references=["REQ-3", "REQ-4"] - ) - + + with patch.object(references_handler.client, "send_get", return_value=mock_get_case_response), patch.object( + references_handler.client, "send_post", return_value=mock_update_response + ): + + success, error = references_handler.add_case_references(case_id=1, references=["REQ-3", "REQ-4"]) + assert success is True assert error == "" - + # Verify the API calls references_handler.client.send_get.assert_called_once_with("get_case/1") references_handler.client.send_post.assert_called_once_with( - "update_case/1", - {'refs': 'REQ-1,REQ-2,REQ-3,REQ-4'} + "update_case/1", {"refs": "REQ-1,REQ-2,REQ-3,REQ-4", "is_legacy": True} ) def test_add_case_references_no_existing_refs(self, references_handler): """Test adding references to a test case with no existing references""" # Mock get_case response with no refs mock_get_case_response = APIClientResult( - status_code=200, - response_text={ - "id": 1, - "title": "Test Case 1", - "refs": "" - }, - error_message=None + status_code=200, response_text={"id": 1, "title": "Test Case 1", "refs": ""}, error_message=None ) - + # Mock update_case response mock_update_response = APIClientResult( - status_code=200, - response_text={"id": 1, "refs": "REQ-1, REQ-2"}, - error_message=None + status_code=200, response_text={"id": 1, "refs": "REQ-1, REQ-2"}, error_message=None ) - - with patch.object(references_handler.client, 'send_get', return_value=mock_get_case_response), \ - patch.object(references_handler.client, 'send_post', return_value=mock_update_response): - - success, error = references_handler.add_case_references( - case_id=1, - references=["REQ-1", "REQ-2"] - ) - + + with patch.object(references_handler.client, "send_get", return_value=mock_get_case_response), patch.object( + references_handler.client, "send_post", return_value=mock_update_response + ): + + success, error = references_handler.add_case_references(case_id=1, references=["REQ-1", "REQ-2"]) + assert success is True assert error == "" - + # Verify the update call references_handler.client.send_post.assert_called_once_with( - "update_case/1", - {'refs': 'REQ-1,REQ-2'} + "update_case/1", {"refs": "REQ-1,REQ-2", "is_legacy": True} ) def test_add_case_references_avoid_duplicates(self, references_handler): """Test that duplicate references are not added""" # Mock get_case response with existing refs mock_get_case_response = APIClientResult( - status_code=200, - response_text={ - "id": 1, - "title": "Test Case 1", - "refs": "REQ-1, REQ-2" - }, - error_message=None + status_code=200, response_text={"id": 1, "title": "Test Case 1", "refs": "REQ-1, REQ-2"}, error_message=None ) - + # Mock update_case response mock_update_response = APIClientResult( - status_code=200, - response_text={"id": 1, "refs": "REQ-1, REQ-2, REQ-3"}, - error_message=None + status_code=200, response_text={"id": 1, "refs": "REQ-1, REQ-2, REQ-3"}, error_message=None ) - - with patch.object(references_handler.client, 'send_get', return_value=mock_get_case_response), \ - patch.object(references_handler.client, 'send_post', return_value=mock_update_response): - + + with patch.object(references_handler.client, "send_get", return_value=mock_get_case_response), patch.object( + references_handler.client, "send_post", return_value=mock_update_response + ): + success, error = references_handler.add_case_references( - case_id=1, - references=["REQ-1", "REQ-3"] # REQ-1 already exists + case_id=1, references=["REQ-1", "REQ-3"] # REQ-1 already exists ) - + assert success is True assert error == "" - + # Verify only REQ-3 was added (no duplicate REQ-1) references_handler.client.send_post.assert_called_once_with( - "update_case/1", - {'refs': 'REQ-1,REQ-2,REQ-3'} + "update_case/1", {"refs": "REQ-1,REQ-2,REQ-3", "is_legacy": True} ) def test_add_case_references_case_not_found(self, references_handler): """Test handling of non-existent test case""" mock_get_case_response = APIClientResult( - status_code=404, - response_text=None, - error_message="Test case not found" + status_code=404, response_text=None, error_message="Test case not found" ) - - with patch.object(references_handler.client, 'send_get', return_value=mock_get_case_response): - - success, error = references_handler.add_case_references( - case_id=999, - references=["REQ-1"] - ) - + + with patch.object(references_handler.client, "send_get", return_value=mock_get_case_response): + + success, error = references_handler.add_case_references(case_id=999, references=["REQ-1"]) + assert success is False assert error == "Failed to retrieve test case 999: Test case not found" @@ -164,24 +123,17 @@ def test_add_case_references_character_limit_exceeded(self, references_handler): # Mock get_case response with existing refs mock_get_case_response = APIClientResult( status_code=200, - response_text={ - "id": 1, - "title": "Test Case 1", - "refs": "REQ-1" * 500 # Long existing refs - }, - error_message=None + response_text={"id": 1, "title": "Test Case 1", "refs": "REQ-1" * 500}, # Long existing refs + error_message=None, ) - - with patch.object(references_handler.client, 'send_get', return_value=mock_get_case_response): - + + with patch.object(references_handler.client, "send_get", return_value=mock_get_case_response): + # Try to add more refs that would exceed 2000 chars (using unique refs to account for deduplication) long_refs = [f"REQ-{i}-" + "X" * 500 for i in range(5)] - - success, error = references_handler.add_case_references( - case_id=1, - references=long_refs - ) - + + success, error = references_handler.add_case_references(case_id=1, references=long_refs) + assert success is False assert "exceeds 2000 character limit" in error @@ -189,74 +141,56 @@ def test_add_case_references_deduplication(self, references_handler): """Test that duplicate references in input are deduplicated""" # Mock get_case response with existing refs mock_get_case_response = APIClientResult( - status_code=200, - response_text={ - "id": 1, - "title": "Test Case 1", - "refs": "REQ-1" - }, - error_message=None + status_code=200, response_text={"id": 1, "title": "Test Case 1", "refs": "REQ-1"}, error_message=None ) - + # Mock update_case response mock_update_response = APIClientResult( - status_code=200, - response_text={"id": 1, "refs": "REQ-1,REQ-2,REQ-3"}, - error_message=None + status_code=200, response_text={"id": 1, "refs": "REQ-1,REQ-2,REQ-3"}, error_message=None ) - - with patch.object(references_handler.client, 'send_get', return_value=mock_get_case_response), \ - patch.object(references_handler.client, 'send_post', return_value=mock_update_response): - + + with patch.object(references_handler.client, "send_get", return_value=mock_get_case_response), patch.object( + references_handler.client, "send_post", return_value=mock_update_response + ): + success, error = references_handler.add_case_references( - case_id=1, - references=["REQ-2", "REQ-2", "REQ-3", "REQ-2"] # Duplicates should be removed + case_id=1, references=["REQ-2", "REQ-2", "REQ-3", "REQ-2"] # Duplicates should be removed ) - + assert success is True assert error == "" - + # Verify the API call has deduplicated references references_handler.client.send_post.assert_called_once_with( - "update_case/1", - {'refs': 'REQ-1,REQ-2,REQ-3'} # Duplicates removed, order preserved + "update_case/1", {"refs": "REQ-1,REQ-2,REQ-3", "is_legacy": True} # Duplicates removed, order preserved ) def test_update_case_references_success(self, references_handler): """Test successful update of references on a test case""" # Mock update_case response mock_update_response = APIClientResult( - status_code=200, - response_text={"id": 1, "refs": "REQ-3, REQ-4"}, - error_message=None + status_code=200, response_text={"id": 1, "refs": "REQ-3, REQ-4"}, error_message=None ) - - with patch.object(references_handler.client, 'send_post', return_value=mock_update_response): - - success, error = references_handler.update_case_references( - case_id=1, - references=["REQ-3", "REQ-4"] - ) - + + with patch.object(references_handler.client, "send_post", return_value=mock_update_response): + + success, error = references_handler.update_case_references(case_id=1, references=["REQ-3", "REQ-4"]) + assert success is True assert error == "" - + # Verify the API call references_handler.client.send_post.assert_called_once_with( - "update_case/1", - {'refs': 'REQ-3,REQ-4'} + "update_case/1", {"refs": "REQ-3,REQ-4", "is_legacy": True} ) def test_update_case_references_character_limit_exceeded(self, references_handler): """Test character limit validation for update""" # Try to update with refs that exceed 2000 chars (using unique refs to account for deduplication) long_refs = [f"REQ-{i}-" + "X" * 500 for i in range(5)] - - success, error = references_handler.update_case_references( - case_id=1, - references=long_refs - ) - + + success, error = references_handler.update_case_references(case_id=1, references=long_refs) + assert success is False assert "exceeds 2000 character limit" in error @@ -264,69 +198,54 @@ def test_update_case_references_deduplication(self, references_handler): """Test that duplicate references in input are deduplicated""" # Mock update_case response mock_update_response = APIClientResult( - status_code=200, - response_text={"id": 1, "refs": "REQ-1,REQ-2"}, - error_message=None + status_code=200, response_text={"id": 1, "refs": "REQ-1,REQ-2"}, error_message=None ) - - with patch.object(references_handler.client, 'send_post', return_value=mock_update_response): - + + with patch.object(references_handler.client, "send_post", return_value=mock_update_response): + success, error = references_handler.update_case_references( - case_id=1, - references=["REQ-1", "REQ-1", "REQ-2", "REQ-1"] # Duplicates should be removed + case_id=1, references=["REQ-1", "REQ-1", "REQ-2", "REQ-1"] # Duplicates should be removed ) - + assert success is True assert error == "" - + # Verify the API call has deduplicated references references_handler.client.send_post.assert_called_once_with( - "update_case/1", - {'refs': 'REQ-1,REQ-2'} # Duplicates removed, order preserved + "update_case/1", {"refs": "REQ-1,REQ-2", "is_legacy": True} # Duplicates removed, order preserved ) def test_update_case_references_api_failure(self, references_handler): """Test API failure during update""" # Mock update_case response with failure mock_update_response = APIClientResult( - status_code=400, - response_text=None, - error_message="Invalid test case ID" + status_code=400, response_text=None, error_message="Invalid test case ID" ) - - with patch.object(references_handler.client, 'send_post', return_value=mock_update_response): - - success, error = references_handler.update_case_references( - case_id=1, - references=["REQ-1"] - ) - + + with patch.object(references_handler.client, "send_post", return_value=mock_update_response): + + success, error = references_handler.update_case_references(case_id=1, references=["REQ-1"]) + assert success is False assert error == "Invalid test case ID" def test_delete_case_references_all_success(self, references_handler): """Test successful deletion of all references""" # Mock update_case response - mock_update_response = APIClientResult( - status_code=200, - response_text={"id": 1, "refs": ""}, - error_message=None - ) - - with patch.object(references_handler.client, 'send_post', return_value=mock_update_response): - + mock_update_response = APIClientResult(status_code=200, response_text={"id": 1, "refs": ""}, error_message=None) + + with patch.object(references_handler.client, "send_post", return_value=mock_update_response): + success, error = references_handler.delete_case_references( - case_id=1, - specific_references=None # Delete all + case_id=1, specific_references=None # Delete all ) - + assert success is True assert error == "" - + # Verify the API call references_handler.client.send_post.assert_called_once_with( - "update_case/1", - {'refs': ''} + "update_case/1", {"refs": "", "is_legacy": True} ) def test_delete_case_references_specific_success(self, references_handler): @@ -334,80 +253,58 @@ def test_delete_case_references_specific_success(self, references_handler): # Mock get_case response mock_get_case_response = APIClientResult( status_code=200, - response_text={ - "id": 1, - "title": "Test Case 1", - "refs": "REQ-1, REQ-2, REQ-3, REQ-4" - }, - error_message=None + response_text={"id": 1, "title": "Test Case 1", "refs": "REQ-1, REQ-2, REQ-3, REQ-4"}, + error_message=None, ) - + # Mock update_case response mock_update_response = APIClientResult( - status_code=200, - response_text={"id": 1, "refs": "REQ-1, REQ-4"}, - error_message=None + status_code=200, response_text={"id": 1, "refs": "REQ-1, REQ-4"}, error_message=None ) - - with patch.object(references_handler.client, 'send_get', return_value=mock_get_case_response), \ - patch.object(references_handler.client, 'send_post', return_value=mock_update_response): - + + with patch.object(references_handler.client, "send_get", return_value=mock_get_case_response), patch.object( + references_handler.client, "send_post", return_value=mock_update_response + ): + success, error = references_handler.delete_case_references( - case_id=1, - specific_references=["REQ-2", "REQ-3"] + case_id=1, specific_references=["REQ-2", "REQ-3"] ) - + assert success is True assert error == "" - + # Verify the API calls references_handler.client.send_get.assert_called_once_with("get_case/1") references_handler.client.send_post.assert_called_once_with( - "update_case/1", - {'refs': 'REQ-1,REQ-4'} + "update_case/1", {"refs": "REQ-1,REQ-4", "is_legacy": True} ) def test_delete_case_references_no_existing_refs(self, references_handler): """Test deletion when no references exist""" # Mock get_case response with no refs mock_get_case_response = APIClientResult( - status_code=200, - response_text={ - "id": 1, - "title": "Test Case 1", - "refs": "" - }, - error_message=None + status_code=200, response_text={"id": 1, "title": "Test Case 1", "refs": ""}, error_message=None ) - - with patch.object(references_handler.client, 'send_get', return_value=mock_get_case_response): - - success, error = references_handler.delete_case_references( - case_id=1, - specific_references=["REQ-1"] - ) - + + with patch.object(references_handler.client, "send_get", return_value=mock_get_case_response): + + success, error = references_handler.delete_case_references(case_id=1, specific_references=["REQ-1"]) + assert success is True assert error == "" - + # Verify no update call was made since there were no refs to delete references_handler.client.send_post.assert_not_called() def test_delete_case_references_case_not_found(self, references_handler): """Test handling of non-existent test case during deletion""" mock_get_case_response = APIClientResult( - status_code=404, - response_text=None, - error_message="Test case not found" + status_code=404, response_text=None, error_message="Test case not found" ) - - with patch.object(references_handler.client, 'send_get', return_value=mock_get_case_response): - - success, error = references_handler.delete_case_references( - case_id=999, - specific_references=["REQ-1"] - ) - + + with patch.object(references_handler.client, "send_get", return_value=mock_get_case_response): + + success, error = references_handler.delete_case_references(case_id=999, specific_references=["REQ-1"]) + assert success is False assert error == "Failed to retrieve test case 999: Test case not found" - From 4bcce3b86e76f209d7840355fa731b7a1071a4de Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Wed, 15 Apr 2026 14:47:59 +0800 Subject: [PATCH 6/7] TRCLI-250: Updated junit parser to properly update cases, also added changes to output messages when updating multiple case fields, updated unit tests --- tests/test_junit_parse_reference.py | 155 +++++++++++++++++++--------- trcli/api/results_uploader.py | 45 ++++---- trcli/commands/cmd_parse_junit.py | 16 ++- 3 files changed, 143 insertions(+), 73 deletions(-) diff --git a/tests/test_junit_parse_reference.py b/tests/test_junit_parse_reference.py index 1ab13db..765be1f 100644 --- a/tests/test_junit_parse_reference.py +++ b/tests/test_junit_parse_reference.py @@ -2,6 +2,7 @@ Unit tests for new features coverage - focused on critical missing areas. Tests for --test-run-ref, case updates, and reference management functionality. """ + import pytest from unittest.mock import Mock, patch import json @@ -17,11 +18,11 @@ def test_validate_test_run_ref_valid_input(self): # Valid single reference result = _validate_test_run_ref("REF-123") assert result is None - + # Valid multiple references result = _validate_test_run_ref("REF-123,REF-456,REF-789") assert result is None - + # Valid with spaces result = _validate_test_run_ref("REF-123, REF-456 , REF-789") assert result is None @@ -31,15 +32,15 @@ def test_validate_test_run_ref_invalid_input(self): # Empty string result = _validate_test_run_ref("") assert "cannot be empty" in result - + # Whitespace only result = _validate_test_run_ref(" ") assert "cannot be empty" in result - + # Only commas result = _validate_test_run_ref(",,,") assert "malformed input" in result - + # Too long (over 250 chars) long_refs = ",".join([f"REF-{i:03d}" for i in range(50)]) # Creates ~300 chars result = _validate_test_run_ref(long_refs) @@ -49,44 +50,104 @@ def test_handle_case_update_reporting_console(self): """Test _handle_case_update_reporting console output""" env = Mock() env.json_output = False - + case_update_results = { "updated_cases": [ {"case_id": 123, "case_title": "Test Case 1", "added_refs": ["REF-1"], "skipped_refs": []} ], "skipped_cases": [ - {"case_id": 456, "case_title": "Test Case 2", "reason": "All references already present", "skipped_refs": ["REF-2"]} + { + "case_id": 456, + "case_title": "Test Case 2", + "reason": "All references already present", + "skipped_refs": ["REF-2"], + } ], - "failed_cases": [ - {"case_id": 789, "case_title": "Test Case 3", "error": "API error"} - ] + "failed_cases": [{"case_id": 789, "case_title": "Test Case 3", "error": "API error"}], } - + _handle_case_update_reporting(env, case_update_results) - + # Verify console output was logged assert env.log.called call_args = [call[0][0] for call in env.log.call_args_list] output = " ".join(call_args) - assert "Case Reference Updates Summary:" in output + assert "Case Updates Summary:" in output assert "Updated cases: 1" in output assert "Skipped cases: 1" in output assert "Failed cases: 1" in output - @patch('builtins.print') + def test_handle_case_update_reporting_with_fields(self): + """Test _handle_case_update_reporting console output with field updates""" + env = Mock() + env.json_output = False + + case_update_results = { + "updated_cases": [ + { + "case_id": 123, + "case_title": "Test Case 1", + "added_refs": [], + "skipped_refs": [], + "updated_fields": ["custom_preconds", "custom_steps", "custom_expected"], + } + ], + "skipped_cases": [], + "failed_cases": [], + } + + _handle_case_update_reporting(env, case_update_results) + + # Verify console output includes field updates + assert env.log.called + call_args = [call[0][0] for call in env.log.call_args_list] + output = " ".join(call_args) + assert "Case Updates Summary:" in output + assert "Updated cases: 1" in output + assert "updated 3 field(s): custom_preconds, custom_steps, custom_expected" in output + + def test_handle_case_update_reporting_with_refs_and_fields(self): + """Test _handle_case_update_reporting with both references and fields""" + env = Mock() + env.json_output = False + + case_update_results = { + "updated_cases": [ + { + "case_id": 456, + "case_title": "Test Case 2", + "added_refs": ["REF-1", "REF-2"], + "skipped_refs": ["REF-3"], + "updated_fields": ["custom_automation_type"], + } + ], + "skipped_cases": [], + "failed_cases": [], + } + + _handle_case_update_reporting(env, case_update_results) + + # Verify both refs and fields are shown + assert env.log.called + call_args = [call[0][0] for call in env.log.call_args_list] + output = " ".join(call_args) + assert "added 2 refs, skipped 1 duplicates" in output + assert "updated 1 field(s): custom_automation_type" in output + + @patch("builtins.print") def test_handle_case_update_reporting_json(self, mock_print): """Test _handle_case_update_reporting JSON output""" env = Mock() env.json_output = True - + case_update_results = { "updated_cases": [{"case_id": 123, "added_refs": ["REF-1"]}], "skipped_cases": [], - "failed_cases": [] + "failed_cases": [], } - + _handle_case_update_reporting(env, case_update_results) - + # Verify JSON output assert mock_print.called json_output = mock_print.call_args[0][0] @@ -98,7 +159,7 @@ def test_handle_case_update_reporting_json(self, mock_print): def test_handle_case_update_reporting_none_input(self): """Test _handle_case_update_reporting with None input""" env = Mock() - + # Should return early without error result = _handle_case_update_reporting(env, None) assert result is None @@ -111,7 +172,7 @@ def test_reference_deduplication_logic(self): """Test the deduplication logic used in reference management""" # Test input with duplicates references = ["REF-1", "REF-1", "REF-2", "REF-2", "REF-1", "REF-3"] - + # Apply deduplication logic (same as in api_request_handler.py) seen = set() unique_refs = [] @@ -119,7 +180,7 @@ def test_reference_deduplication_logic(self): if ref not in seen: seen.add(ref) unique_refs.append(ref) - + # Should preserve order and remove duplicates assert unique_refs == ["REF-1", "REF-2", "REF-3"] assert len(unique_refs) == 3 @@ -133,10 +194,10 @@ def test_reference_string_parsing(self): ("REF-1,,REF-2", ["REF-1", "REF-2"]), (" REF-1 , REF-2 ", ["REF-1", "REF-2"]), ] - + for input_str, expected in test_cases: # Apply parsing logic (same as in api_request_handler.py) - refs_list = [ref.strip() for ref in input_str.split(',') if ref.strip()] + refs_list = [ref.strip() for ref in input_str.split(",") if ref.strip()] assert refs_list == expected def test_character_limit_validation(self): @@ -144,10 +205,10 @@ def test_character_limit_validation(self): # Test 250 character limit (for run references) short_refs = ",".join([f"REF-{i:02d}" for i in range(30)]) # ~150 chars assert len(short_refs) < 250 - + long_refs = ",".join([f"REF-{i:03d}" for i in range(50)]) # ~300 chars assert len(long_refs) > 250 - + # Test 2000 character limit (for case references) very_long_refs = ",".join([f"VERY-LONG-REFERENCE-NAME-{i:03d}" for i in range(100)]) assert len(very_long_refs) > 2000 @@ -165,7 +226,7 @@ def test_testrail_case_field_parsing(self): ("refs:REF-1,REF-2,REF-3", "REF-1,REF-2,REF-3"), ("refs: REF-1 , REF-2 ", " REF-1 , REF-2 "), # Spaces preserved ] - + for testrail_field, expected_refs in test_cases: # Apply parsing logic (same as in junit_xml.py) if testrail_field.startswith("refs:"): @@ -183,7 +244,7 @@ def test_case_refs_validation(self): ("refs:REF-1", True), ("refs: REF-1 ", True), ] - + for case_refs, should_be_valid in test_cases: # Apply validation logic (same as in junit_xml.py) if case_refs.startswith("refs:"): @@ -201,29 +262,29 @@ def test_case_categorization_logic(self): existing_case = {"case_id": 123, "has_junit_refs": True} newly_created_case = {"case_id": 456, "has_junit_refs": True} case_without_refs = {"case_id": 789, "has_junit_refs": False} - + # Mock newly created case IDs newly_created_case_ids = {456} - + # Test categorization logic cases_to_update = [] cases_to_skip = [] - + for case in [existing_case, newly_created_case, case_without_refs]: case_id = case["case_id"] has_refs = case["has_junit_refs"] - + if case_id in newly_created_case_ids: cases_to_skip.append({"case_id": case_id, "reason": "Newly created case"}) elif not has_refs: cases_to_skip.append({"case_id": case_id, "reason": "No JUnit refs"}) else: cases_to_update.append(case) - + # Verify categorization assert len(cases_to_update) == 1 assert cases_to_update[0]["case_id"] == 123 - + assert len(cases_to_skip) == 2 assert any(c["case_id"] == 456 and "Newly created" in c["reason"] for c in cases_to_skip) assert any(c["case_id"] == 789 and "No JUnit refs" in c["reason"] for c in cases_to_skip) @@ -234,40 +295,34 @@ def test_update_result_categorization(self): api_responses = [ (True, "Success", ["REF-1"], []), # Successful update (True, "Success", [], ["REF-2"]), # All refs already present - (False, "API Error", [], []), # Failed update + (False, "API Error", [], []), # Failed update ] - + updated_cases = [] skipped_cases = [] failed_cases = [] - + for i, (success, message, added_refs, skipped_refs) in enumerate(api_responses): case_id = 100 + i - + if not success: failed_cases.append({"case_id": case_id, "error": message}) elif not added_refs: # No refs were added (all were duplicates) - skipped_cases.append({ - "case_id": case_id, - "reason": "All references already present", - "skipped_refs": skipped_refs - }) + skipped_cases.append( + {"case_id": case_id, "reason": "All references already present", "skipped_refs": skipped_refs} + ) else: - updated_cases.append({ - "case_id": case_id, - "added_refs": added_refs, - "skipped_refs": skipped_refs - }) - + updated_cases.append({"case_id": case_id, "added_refs": added_refs, "skipped_refs": skipped_refs}) + # Verify categorization assert len(updated_cases) == 1 assert updated_cases[0]["case_id"] == 100 assert updated_cases[0]["added_refs"] == ["REF-1"] - + assert len(skipped_cases) == 1 assert skipped_cases[0]["case_id"] == 101 assert "All references already present" in skipped_cases[0]["reason"] - + assert len(failed_cases) == 1 assert failed_cases[0]["case_id"] == 102 assert failed_cases[0]["error"] == "API Error" diff --git a/trcli/api/results_uploader.py b/trcli/api/results_uploader.py index 2a08169..fdb4b57 100644 --- a/trcli/api/results_uploader.py +++ b/trcli/api/results_uploader.py @@ -267,39 +267,42 @@ def update_existing_cases_with_junit_refs(self, added_test_cases: List[Dict] = N # Process all test cases in all sections for section in self.api_request_handler.suites_data_from_provider.testsections: for test_case in section.testcases: - # Only process cases that have a case_id (existing cases) and JUnit refs + # Only process cases that have a case_id (existing cases) and (JUnit refs OR case fields) # AND exclude newly created cases + has_refs = hasattr(test_case, "_junit_case_refs") and test_case._junit_case_refs + has_case_fields = test_case.case_fields and len(test_case.case_fields) > 0 + if ( test_case.case_id - and hasattr(test_case, "_junit_case_refs") - and test_case._junit_case_refs + and (has_refs or has_case_fields) and int(test_case.case_id) not in newly_created_case_ids ): try: - success, error_msg, added_refs, skipped_refs = ( + # Get refs if they exist, otherwise pass empty string + junit_refs = test_case._junit_case_refs if has_refs else "" + + success, error_msg, added_refs, skipped_refs, updated_fields = ( self.api_request_handler.update_existing_case_references( - test_case.case_id, test_case._junit_case_refs, strategy + test_case.case_id, junit_refs, test_case.case_fields, strategy ) ) if success: - if added_refs: - # Only count as "updated" if references were actually added - update_results["updated_cases"].append( - { - "case_id": test_case.case_id, - "case_title": test_case.title, - "added_refs": added_refs, - "skipped_refs": skipped_refs, - } - ) + if added_refs or updated_fields: + # Count as "updated" if references or fields were actually added/updated + update_info = { + "case_id": test_case.case_id, + "case_title": test_case.title, + } + if added_refs: + update_info["added_refs"] = added_refs + update_info["skipped_refs"] = skipped_refs + if updated_fields: + update_info["updated_fields"] = updated_fields + update_results["updated_cases"].append(update_info) else: - # If no refs were added (all were duplicates or no valid refs), count as skipped - reason = ( - "All references already present" - if skipped_refs - else "No valid references to process" - ) + # If nothing was added/updated (all were duplicates or no valid data), count as skipped + reason = "All references already present" if skipped_refs else "No changes to apply" update_results["skipped_cases"].append( { "case_id": test_case.case_id, diff --git a/trcli/commands/cmd_parse_junit.py b/trcli/commands/cmd_parse_junit.py index 093b91a..9bb61af 100644 --- a/trcli/commands/cmd_parse_junit.py +++ b/trcli/commands/cmd_parse_junit.py @@ -219,7 +219,7 @@ def _handle_case_update_reporting(environment: Environment, case_update_results: failed_cases = case_update_results.get("failed_cases", []) if updated_cases or skipped_cases or failed_cases: - environment.log("Case Reference Updates Summary:") + environment.log("Case Updates Summary:") environment.log(f" Updated cases: {len(updated_cases)}") environment.log(f" Skipped cases: {len(skipped_cases)}") environment.log(f" Failed cases: {len(failed_cases)}") @@ -230,7 +230,19 @@ def _handle_case_update_reporting(environment: Environment, case_update_results: case_id = case_info["case_id"] added = case_info.get("added_refs", []) skipped = case_info.get("skipped_refs", []) - environment.log(f" C{case_id}: added {len(added)} refs, skipped {len(skipped)} duplicates") + updated_fields = case_info.get("updated_fields", []) + + # Build details message + details = [] + if added or skipped: + details.append(f"added {len(added)} refs, skipped {len(skipped)} duplicates") + if updated_fields: + details.append(f"updated {len(updated_fields)} field(s): {', '.join(updated_fields)}") + + if details: + environment.log(f" C{case_id}: {'; '.join(details)}") + else: + environment.log(f" C{case_id}: no changes") if skipped_cases: environment.log(" Skipped case details:") From a46231184831aadc1b06119f34b4faef2e33ea3d Mon Sep 17 00:00:00 2001 From: acuanico-tr-galt Date: Thu, 16 Apr 2026 14:59:22 +0800 Subject: [PATCH 7/7] Updated changelog for v1.14.1 release --- CHANGELOG.MD | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 8931c5a..7b9b921 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -8,10 +8,13 @@ This project adheres to [Semantic Versioning](https://semver.org/). Version numb ## [1.14.1] -_released 04-14-2026 +_released 04-16-2026 + +### Added + - Keep Markdown Syntax when sending markdown data to Testrail with new WYSIWYG editor - **Requires TestRail 10.3.0 or higher** ### Fixed - - + - Fixed an issue where attachments are all uploaded to the last result when multiple results are added to case while parsing several reports via glob support ## [1.14.0]