trcli 1.13.4__tar.gz → 1.14.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. {trcli-1.13.4 → trcli-1.14.0}/PKG-INFO +1 -1
  2. {trcli-1.13.4 → trcli-1.14.0}/README.md +106 -21
  3. {trcli-1.13.4 → trcli-1.14.0}/tests/test_api_request_handler.py +293 -38
  4. {trcli-1.13.4 → trcli-1.14.0}/tests/test_cmd_add_run.py +139 -0
  5. trcli-1.14.0/trcli/__init__.py +1 -0
  6. {trcli-1.13.4 → trcli-1.14.0}/trcli/api/api_request_handler.py +16 -3
  7. trcli-1.14.0/trcli/api/multisuite_uploader.py +405 -0
  8. {trcli-1.13.4 → trcli-1.14.0}/trcli/api/project_based_client.py +53 -3
  9. {trcli-1.13.4 → trcli-1.14.0}/trcli/api/result_handler.py +63 -52
  10. {trcli-1.13.4 → trcli-1.14.0}/trcli/api/run_handler.py +113 -23
  11. {trcli-1.13.4 → trcli-1.14.0}/trcli/commands/cmd_add_run.py +91 -0
  12. {trcli-1.13.4 → trcli-1.14.0}/trcli/commands/cmd_parse_junit.py +33 -10
  13. {trcli-1.13.4 → trcli-1.14.0}/trcli/constants.py +6 -0
  14. {trcli-1.13.4 → trcli-1.14.0}/trcli/readers/junit_xml.py +8 -0
  15. {trcli-1.13.4 → trcli-1.14.0}/trcli.egg-info/PKG-INFO +1 -1
  16. {trcli-1.13.4 → trcli-1.14.0}/trcli.egg-info/SOURCES.txt +1 -0
  17. trcli-1.13.4/trcli/__init__.py +0 -1
  18. {trcli-1.13.4 → trcli-1.14.0}/LICENSE.md +0 -0
  19. {trcli-1.13.4 → trcli-1.14.0}/setup.cfg +0 -0
  20. {trcli-1.13.4 → trcli-1.14.0}/setup.py +0 -0
  21. {trcli-1.13.4 → trcli-1.14.0}/tests/test_api_client.py +0 -0
  22. {trcli-1.13.4 → trcli-1.14.0}/tests/test_api_client_proxy.py +0 -0
  23. {trcli-1.13.4 → trcli-1.14.0}/tests/test_api_data_provider.py +0 -0
  24. {trcli-1.13.4 → trcli-1.14.0}/tests/test_api_request_handler_case_fields_update.py +0 -0
  25. {trcli-1.13.4 → trcli-1.14.0}/tests/test_api_request_handler_case_matcher.py +0 -0
  26. {trcli-1.13.4 → trcli-1.14.0}/tests/test_api_request_handler_labels.py +0 -0
  27. {trcli-1.13.4 → trcli-1.14.0}/tests/test_api_request_handler_references.py +0 -0
  28. {trcli-1.13.4 → trcli-1.14.0}/tests/test_cli.py +0 -0
  29. {trcli-1.13.4 → trcli-1.14.0}/tests/test_cmd_export_gherkin.py +0 -0
  30. {trcli-1.13.4 → trcli-1.14.0}/tests/test_cmd_import_gherkin.py +0 -0
  31. {trcli-1.13.4 → trcli-1.14.0}/tests/test_cmd_labels.py +0 -0
  32. {trcli-1.13.4 → trcli-1.14.0}/tests/test_cmd_parse_cucumber.py +0 -0
  33. {trcli-1.13.4 → trcli-1.14.0}/tests/test_cmd_references.py +0 -0
  34. {trcli-1.13.4 → trcli-1.14.0}/tests/test_cmd_update.py +0 -0
  35. {trcli-1.13.4 → trcli-1.14.0}/tests/test_cucumber_bdd_matching.py +0 -0
  36. {trcli-1.13.4 → trcli-1.14.0}/tests/test_cucumber_parser.py +0 -0
  37. {trcli-1.13.4 → trcli-1.14.0}/tests/test_dataclass_creation.py +0 -0
  38. {trcli-1.13.4 → trcli-1.14.0}/tests/test_glob_deduplication.py +0 -0
  39. {trcli-1.13.4 → trcli-1.14.0}/tests/test_glob_integration.py +0 -0
  40. {trcli-1.13.4 → trcli-1.14.0}/tests/test_junit_bdd_parser.py +0 -0
  41. {trcli-1.13.4 → trcli-1.14.0}/tests/test_junit_parse_reference.py +0 -0
  42. {trcli-1.13.4 → trcli-1.14.0}/tests/test_junit_parser.py +0 -0
  43. {trcli-1.13.4 → trcli-1.14.0}/tests/test_load_data_from_config.py +0 -0
  44. {trcli-1.13.4 → trcli-1.14.0}/tests/test_matchers_parser.py +0 -0
  45. {trcli-1.13.4 → trcli-1.14.0}/tests/test_multiple_case_ids.py +0 -0
  46. {trcli-1.13.4 → trcli-1.14.0}/tests/test_project_based_client.py +0 -0
  47. {trcli-1.13.4 → trcli-1.14.0}/tests/test_response_verify.py +0 -0
  48. {trcli-1.13.4 → trcli-1.14.0}/tests/test_results_uploader.py +0 -0
  49. {trcli-1.13.4 → trcli-1.14.0}/tests/test_robot_parser.py +0 -0
  50. {trcli-1.13.4 → trcli-1.14.0}/tests/test_version_checker.py +0 -0
  51. {trcli-1.13.4 → trcli-1.14.0}/trcli/api/__init__.py +0 -0
  52. {trcli-1.13.4 → trcli-1.14.0}/trcli/api/api_cache.py +0 -0
  53. {trcli-1.13.4 → trcli-1.14.0}/trcli/api/api_client.py +0 -0
  54. {trcli-1.13.4 → trcli-1.14.0}/trcli/api/api_response_verify.py +0 -0
  55. {trcli-1.13.4 → trcli-1.14.0}/trcli/api/api_utils.py +0 -0
  56. {trcli-1.13.4 → trcli-1.14.0}/trcli/api/bdd_handler.py +0 -0
  57. {trcli-1.13.4 → trcli-1.14.0}/trcli/api/case_handler.py +0 -0
  58. {trcli-1.13.4 → trcli-1.14.0}/trcli/api/case_matcher.py +0 -0
  59. {trcli-1.13.4 → trcli-1.14.0}/trcli/api/label_manager.py +0 -0
  60. {trcli-1.13.4 → trcli-1.14.0}/trcli/api/reference_manager.py +0 -0
  61. {trcli-1.13.4 → trcli-1.14.0}/trcli/api/results_uploader.py +0 -0
  62. {trcli-1.13.4 → trcli-1.14.0}/trcli/api/section_handler.py +0 -0
  63. {trcli-1.13.4 → trcli-1.14.0}/trcli/api/suite_handler.py +0 -0
  64. {trcli-1.13.4 → trcli-1.14.0}/trcli/backports.py +0 -0
  65. {trcli-1.13.4 → trcli-1.14.0}/trcli/cli.py +0 -0
  66. {trcli-1.13.4 → trcli-1.14.0}/trcli/commands/__init__.py +0 -0
  67. {trcli-1.13.4 → trcli-1.14.0}/trcli/commands/cmd_export_gherkin.py +0 -0
  68. {trcli-1.13.4 → trcli-1.14.0}/trcli/commands/cmd_import_gherkin.py +0 -0
  69. {trcli-1.13.4 → trcli-1.14.0}/trcli/commands/cmd_labels.py +0 -0
  70. {trcli-1.13.4 → trcli-1.14.0}/trcli/commands/cmd_parse_cucumber.py +0 -0
  71. {trcli-1.13.4 → trcli-1.14.0}/trcli/commands/cmd_parse_openapi.py +0 -0
  72. {trcli-1.13.4 → trcli-1.14.0}/trcli/commands/cmd_parse_robot.py +0 -0
  73. {trcli-1.13.4 → trcli-1.14.0}/trcli/commands/cmd_references.py +0 -0
  74. {trcli-1.13.4 → trcli-1.14.0}/trcli/commands/cmd_update.py +0 -0
  75. {trcli-1.13.4 → trcli-1.14.0}/trcli/commands/results_parser_helpers.py +0 -0
  76. {trcli-1.13.4 → trcli-1.14.0}/trcli/data_classes/__init__.py +0 -0
  77. {trcli-1.13.4 → trcli-1.14.0}/trcli/data_classes/data_parsers.py +0 -0
  78. {trcli-1.13.4 → trcli-1.14.0}/trcli/data_classes/dataclass_testrail.py +0 -0
  79. {trcli-1.13.4 → trcli-1.14.0}/trcli/data_classes/validation_exception.py +0 -0
  80. {trcli-1.13.4 → trcli-1.14.0}/trcli/data_providers/api_data_provider.py +0 -0
  81. {trcli-1.13.4 → trcli-1.14.0}/trcli/logging/__init__.py +0 -0
  82. {trcli-1.13.4 → trcli-1.14.0}/trcli/logging/config.py +0 -0
  83. {trcli-1.13.4 → trcli-1.14.0}/trcli/logging/file_handler.py +0 -0
  84. {trcli-1.13.4 → trcli-1.14.0}/trcli/logging/structured_logger.py +0 -0
  85. {trcli-1.13.4 → trcli-1.14.0}/trcli/readers/__init__.py +0 -0
  86. {trcli-1.13.4 → trcli-1.14.0}/trcli/readers/cucumber_json.py +0 -0
  87. {trcli-1.13.4 → trcli-1.14.0}/trcli/readers/file_parser.py +0 -0
  88. {trcli-1.13.4 → trcli-1.14.0}/trcli/readers/openapi_yml.py +0 -0
  89. {trcli-1.13.4 → trcli-1.14.0}/trcli/readers/robot_xml.py +0 -0
  90. {trcli-1.13.4 → trcli-1.14.0}/trcli/settings.py +0 -0
  91. {trcli-1.13.4 → trcli-1.14.0}/trcli/version_checker.py +0 -0
  92. {trcli-1.13.4 → trcli-1.14.0}/trcli.egg-info/dependency_links.txt +0 -0
  93. {trcli-1.13.4 → trcli-1.14.0}/trcli.egg-info/entry_points.txt +0 -0
  94. {trcli-1.13.4 → trcli-1.14.0}/trcli.egg-info/requires.txt +0 -0
  95. {trcli-1.13.4 → trcli-1.14.0}/trcli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trcli
3
- Version: 1.13.4
3
+ Version: 1.14.0
4
4
  License-File: LICENSE.md
5
5
  Requires-Dist: click<8.2.2,>=8.1.0
6
6
  Requires-Dist: pyyaml<7.0.0,>=6.0.0
@@ -33,7 +33,7 @@ trcli
33
33
  ```
34
34
  You should get something like this:
35
35
  ```
36
- TestRail CLI v1.13.4
36
+ TestRail CLI v1.14.0
37
37
  Copyright 2025 Gurock Software GmbH - www.gurock.com
38
38
  Supported and loaded modules:
39
39
  - parse_junit: JUnit XML Files (& Similar)
@@ -51,7 +51,7 @@ CLI general reference
51
51
  --------
52
52
  ```shell
53
53
  $ trcli --help
54
- TestRail CLI v1.13.4
54
+ TestRail CLI v1.14.0
55
55
  Copyright 2025 Gurock Software GmbH - www.gurock.com
56
56
  Usage: trcli [OPTIONS] COMMAND [ARGS]...
57
57
 
@@ -882,6 +882,40 @@ the `--special-parser saucectl` command line option.
882
882
  Please refer to the [SauceLabs and saucectl reports](https://support.gurock.com/hc/en-us/articles/12719558686484)
883
883
  documentation for further information.
884
884
 
885
+ #### Cross-suite test plans with multisuite parser
886
+
887
+ If your test automation spans multiple TestRail suites, the multisuite parser allows you to create a single test plan with one run per suite from a single JUnit XML report. The CLI automatically detects which suite each test case belongs to, groups them accordingly, and creates or updates a test plan with the appropriate structure.
888
+
889
+ **Requirements:**
890
+ - All test cases must have case IDs (C123 format in test names or `test_id` properties)
891
+ - Must use `--case-matcher name` or `--case-matcher property` (not `auto`)
892
+ - All cases must belong to the same project (cross-project cases are skipped with warnings)
893
+
894
+ **Basic usage (create new plan):**
895
+ ```bash
896
+ trcli parse_junit \
897
+ --special-parser multisuite \
898
+ --title "Cross-Suite Test Plan" \
899
+ --file results.xml \
900
+ --case-matcher property
901
+ ```
902
+
903
+ **Add to existing plan:**
904
+ ```bash
905
+ trcli parse_junit \
906
+ --special-parser multisuite \
907
+ --plan-id 1234 \
908
+ --file results.xml \
909
+ --case-matcher property
910
+ ```
911
+
912
+ The parser automatically:
913
+ - Fetches suite information for each case ID concurrently (fast performance)
914
+ - Groups cases by their suite
915
+ - Creates a test plan with one run per suite
916
+ - Uploads results to the correct run within the plan
917
+ - Includes suite names and test counts in the plan description
918
+
885
919
  #### Creating new test runs
886
920
 
887
921
  When a test run MUST created before using one of the parse commands, use the `add_run` command. For example, if
@@ -1641,7 +1675,7 @@ Options:
1641
1675
  ### Reference
1642
1676
  ```shell
1643
1677
  $ trcli add_run --help
1644
- TestRail CLI v1.13.4
1678
+ TestRail CLI v1.14.0
1645
1679
  Copyright 2025 Gurock Software GmbH - www.gurock.com
1646
1680
  Usage: trcli add_run [OPTIONS]
1647
1681
 
@@ -1660,6 +1694,16 @@ Options:
1660
1694
  to. [x>=1]
1661
1695
  --clear-run-assigned-to-id Clear the assignee of the test run (only valid
1662
1696
  when updating with --run-id).
1697
+ --clear-run-description Clear the description of the test run (only valid
1698
+ when updating with --run-id).
1699
+ --clear-milestone-id Clear the milestone association of the test run (only
1700
+ valid when updating with --run-id).
1701
+ --clear-run-start-date Clear the start date of the test run (only valid
1702
+ when updating with --run-id).
1703
+ --clear-run-end-date Clear the end date of the test run (only valid when
1704
+ updating with --run-id).
1705
+ --clear-run-case-ids Clear all case IDs from the test run (only valid when
1706
+ updating with --run-id).
1663
1707
  --run-include-all Use this option to include all test cases in this test run.
1664
1708
  --auto-close-run Use this option to automatically close the created run.
1665
1709
  --run-case-ids Comma separated list of test case IDs to include in
@@ -1765,29 +1809,70 @@ trcli -y -h https://example.testrail.io/ --project "My Project" \
1765
1809
  - **Update Mode Only**: The `--clear-run-assigned-to-id` flag can only be used when updating an existing run (requires `--run-id`)
1766
1810
  - **Mutually Exclusive**: You cannot use both `--run-assigned-to-id` and `--clear-run-assigned-to-id` in the same command
1767
1811
 
1768
- #### Examples
1812
+ ### Clearing Run Attributes
1813
+
1814
+ The `add_run` command provides `--clear-*` flags to remove (set to null) various run attributes during updates. All clear flags require `--run-id` and are mutually exclusive with their corresponding set parameters.
1815
+
1816
+ #### Available Clear Flags
1817
+
1818
+ | Flag | Clears | API Effect | Mutually Exclusive With |
1819
+ |------|--------|------------|------------------------|
1820
+ | `--clear-run-description` | Description text | Sets `description: null` | `--run-description` |
1821
+ | `--clear-milestone-id` | Milestone association | Sets `milestone_id: null` | `--milestone-id` |
1822
+ | `--clear-run-start-date` | Start date | Sets `start_on: null` | `--run-start-date` |
1823
+ | `--clear-run-end-date` | End date | Sets `due_on: null` | `--run-end-date` |
1824
+ | `--clear-run-case-ids` | All case selections | Sets `include_all: false, case_ids: []` | `--run-case-ids`, `--run-include-all` |
1825
+
1826
+ #### Clear Description Example
1769
1827
 
1770
- **Complete Workflow Example:**
1771
1828
  ```bash
1772
- # 1. Create run with initial references
1773
- trcli -y -h https://example.testrail.io/ <--username and --password or --key> --project "My Project" \
1774
- add_run --title "Sprint 1 Tests" --run-refs "JIRA-100,JIRA-200" -f "run_config.yml"
1829
+ # Clear the description from a run
1830
+ trcli -y -h https://example.testrail.io/ --project "My Project" \
1831
+ add_run --run-id 123 --title "My Test Run" --clear-run-description
1832
+ ```
1775
1833
 
1776
- # 2. Add more references (from the config file)
1777
- trcli -y -h https://example.testrail.io/ <--username and --password or --key> --project "My Project" \
1778
- -c run_config.yml add_run --run-refs "JIRA-300,REQ-001" --run-refs-action "add"
1834
+ #### Clear Milestone Example
1779
1835
 
1780
- # 3. Replace all references with new ones
1781
- trcli -y -h https://example.testrail.io/ <--username and --password or --key> --project "My Project" \
1782
- -c run_config.yml add_run --run-refs "FINAL-100,FINAL-200" --run-refs-action "update"
1836
+ ```bash
1837
+ # Remove milestone association
1838
+ trcli -y -h https://example.testrail.io/ --project "My Project" \
1839
+ add_run --run-id 123 --title "My Test Run" --clear-milestone-id
1840
+ ```
1783
1841
 
1784
- # 4. Remove specific references
1785
- trcli -y -h https://example.testrail.io/ <--username and --password or --key> --project "My Project" \
1786
- -c run_config.yml add_run --run-refs "FINAL-100" --run-refs-action "delete"
1842
+ #### Clear Dates Example
1787
1843
 
1788
- # 5. Clear all references
1789
- trcli -y -h https://example.testrail.io/ <--username and --password or --key> --project "My Project" \
1790
- -c run_config.yml add_run --run-refs-action "delete"
1844
+ ```bash
1845
+ # Clear start date
1846
+ trcli -y -h https://example.testrail.io/ --project "My Project" \
1847
+ add_run --run-id 123 --title "My Test Run" --clear-run-start-date
1848
+
1849
+ # Clear end date
1850
+ trcli -y -h https://example.testrail.io/ --project "My Project" \
1851
+ add_run --run-id 123 --title "My Test Run" --clear-run-end-date
1852
+
1853
+ # Clear both dates at once
1854
+ trcli -y -h https://example.testrail.io/ --project "My Project" \
1855
+ add_run --run-id 123 --title "My Test Run" --clear-run-start-date --clear-run-end-date
1856
+ ```
1857
+
1858
+ #### Clear Case Selection Example
1859
+
1860
+ ```bash
1861
+ # Clear all case selections (empty run)
1862
+ trcli -y -h https://example.testrail.io/ --project "My Project" \
1863
+ add_run --run-id 123 --title "My Test Run" --clear-run-case-ids
1864
+ ```
1865
+
1866
+ #### Clear Multiple Attributes Example
1867
+
1868
+ ```bash
1869
+ # Clear multiple attributes in one command
1870
+ trcli -y -h https://example.testrail.io/ --project "My Project" \
1871
+ add_run --run-id 123 --title "Clean Run" \
1872
+ --clear-run-description \
1873
+ --clear-milestone-id \
1874
+ --clear-run-start-date \
1875
+ --clear-run-end-date
1791
1876
  ```
1792
1877
 
1793
1878
  Generating test cases from OpenAPI specs
@@ -1800,7 +1885,7 @@ providing you with a solid base of test cases, which you can further expand on T
1800
1885
  ### Reference
1801
1886
  ```shell
1802
1887
  $ trcli parse_openapi --help
1803
- TestRail CLI v1.13.4
1888
+ TestRail CLI v1.14.0
1804
1889
  Copyright 2025 Gurock Software GmbH - www.gurock.com
1805
1890
  Usage: trcli parse_openapi [OPTIONS]
1806
1891
 
@@ -1114,20 +1114,256 @@ class TestApiRequestHandler:
1114
1114
  assert 50 in payload["case_ids"], "Should include existing case ID"
1115
1115
 
1116
1116
  @pytest.mark.api_handler
1117
- def test_upload_attachments_413_error(self, api_request_handler: ApiRequestHandler, requests_mock, tmp_path):
1118
- """Test that 413 errors (file too large) are properly reported."""
1119
- run_id = 1
1117
+ def test_update_run_clears_description(self, api_request_handler: ApiRequestHandler, requests_mock):
1118
+ """Test that update_run can clear description by setting it to null"""
1119
+ run_id = 300
1120
+ run_name = "Test Run"
1121
+
1122
+ # Mock get_run response with existing description
1123
+ get_run_response = {
1124
+ "id": run_id,
1125
+ "name": "Original Run",
1126
+ "description": "Existing description",
1127
+ "refs": "",
1128
+ "include_all": True,
1129
+ "plan_id": None,
1130
+ "config_ids": [],
1131
+ }
1132
+
1133
+ update_run_response = {"id": run_id, "name": run_name}
1134
+
1135
+ requests_mock.get(create_url(f"get_run/{run_id}"), json=get_run_response)
1136
+ requests_mock.post(create_url(f"update_run/{run_id}"), json=update_run_response)
1137
+
1138
+ # Execute update_run with description=None to clear it
1139
+ run_data, error = api_request_handler.update_run(run_id, run_name, description=None)
1140
+
1141
+ assert error == "", "No error should occur"
1142
+
1143
+ # Verify the payload sent to update_run
1144
+ request_history = requests_mock.request_history
1145
+ update_request = [r for r in request_history if "update_run" in r.url and r.method == "POST"][0]
1146
+ payload = update_request.json()
1147
+
1148
+ assert payload["description"] is None, "description should be null"
1149
+
1150
+ @pytest.mark.api_handler
1151
+ def test_update_run_clears_milestone(self, api_request_handler: ApiRequestHandler, requests_mock):
1152
+ """Test that update_run can clear milestone_id by setting it to null"""
1153
+ run_id = 301
1154
+ run_name = "Test Run"
1155
+
1156
+ get_run_response = {
1157
+ "id": run_id,
1158
+ "name": "Original Run",
1159
+ "description": "",
1160
+ "refs": "",
1161
+ "include_all": True,
1162
+ "plan_id": None,
1163
+ "config_ids": [],
1164
+ }
1165
+
1166
+ update_run_response = {"id": run_id, "name": run_name}
1167
+
1168
+ requests_mock.get(create_url(f"get_run/{run_id}"), json=get_run_response)
1169
+ requests_mock.post(create_url(f"update_run/{run_id}"), json=update_run_response)
1170
+
1171
+ # Execute update_run with milestone_id=None to clear it
1172
+ run_data, error = api_request_handler.update_run(run_id, run_name, milestone_id=None)
1173
+
1174
+ assert error == "", "No error should occur"
1175
+
1176
+ # Verify the payload
1177
+ request_history = requests_mock.request_history
1178
+ update_request = [r for r in request_history if "update_run" in r.url and r.method == "POST"][0]
1179
+ payload = update_request.json()
1180
+
1181
+ assert payload["milestone_id"] is None, "milestone_id should be null"
1182
+
1183
+ @pytest.mark.api_handler
1184
+ def test_update_run_clears_start_date(self, api_request_handler: ApiRequestHandler, requests_mock):
1185
+ """Test that update_run can clear start_date by setting it to null"""
1186
+ run_id = 302
1187
+ run_name = "Test Run"
1188
+
1189
+ get_run_response = {
1190
+ "id": run_id,
1191
+ "name": "Original Run",
1192
+ "description": "",
1193
+ "refs": "",
1194
+ "include_all": True,
1195
+ "plan_id": None,
1196
+ "config_ids": [],
1197
+ }
1198
+
1199
+ update_run_response = {"id": run_id, "name": run_name}
1200
+
1201
+ requests_mock.get(create_url(f"get_run/{run_id}"), json=get_run_response)
1202
+ requests_mock.post(create_url(f"update_run/{run_id}"), json=update_run_response)
1203
+
1204
+ # Execute update_run with start_date=None to clear it
1205
+ run_data, error = api_request_handler.update_run(run_id, run_name, start_date=None)
1206
+
1207
+ assert error == "", "No error should occur"
1208
+
1209
+ # Verify the payload
1210
+ request_history = requests_mock.request_history
1211
+ update_request = [r for r in request_history if "update_run" in r.url and r.method == "POST"][0]
1212
+ payload = update_request.json()
1213
+
1214
+ assert payload["start_on"] is None, "start_on should be null"
1215
+
1216
+ @pytest.mark.api_handler
1217
+ def test_update_run_clears_end_date(self, api_request_handler: ApiRequestHandler, requests_mock):
1218
+ """Test that update_run can clear end_date by setting it to null"""
1219
+ run_id = 303
1220
+ run_name = "Test Run"
1221
+
1222
+ get_run_response = {
1223
+ "id": run_id,
1224
+ "name": "Original Run",
1225
+ "description": "",
1226
+ "refs": "",
1227
+ "include_all": True,
1228
+ "plan_id": None,
1229
+ "config_ids": [],
1230
+ }
1231
+
1232
+ update_run_response = {"id": run_id, "name": run_name}
1233
+
1234
+ requests_mock.get(create_url(f"get_run/{run_id}"), json=get_run_response)
1235
+ requests_mock.post(create_url(f"update_run/{run_id}"), json=update_run_response)
1236
+
1237
+ # Execute update_run with end_date=None to clear it
1238
+ run_data, error = api_request_handler.update_run(run_id, run_name, end_date=None)
1239
+
1240
+ assert error == "", "No error should occur"
1241
+
1242
+ # Verify the payload
1243
+ request_history = requests_mock.request_history
1244
+ update_request = [r for r in request_history if "update_run" in r.url and r.method == "POST"][0]
1245
+ payload = update_request.json()
1246
+
1247
+ assert payload["due_on"] is None, "due_on should be null"
1248
+
1249
+ @pytest.mark.api_handler
1250
+ def test_update_run_clears_case_ids(self, api_request_handler: ApiRequestHandler, requests_mock):
1251
+ """Test that update_run can clear case_ids by setting to empty array"""
1252
+ run_id = 304
1253
+ run_name = "Test Run"
1254
+
1255
+ get_run_response = {
1256
+ "id": run_id,
1257
+ "name": "Original Run",
1258
+ "description": "",
1259
+ "refs": "",
1260
+ "include_all": False,
1261
+ "plan_id": None,
1262
+ "config_ids": [],
1263
+ }
1120
1264
 
1121
- # Mock get_tests endpoint
1122
- mocked_tests_response = {
1265
+ get_tests_response = {
1123
1266
  "offset": 0,
1124
1267
  "limit": 250,
1125
- "size": 1,
1268
+ "size": 2,
1126
1269
  "_links": {"next": None, "prev": None},
1127
- "tests": [{"id": 1001, "case_id": 100}],
1270
+ "tests": [{"id": 1, "case_id": 10, "status_id": 1}, {"id": 2, "case_id": 20, "status_id": 1}],
1271
+ }
1272
+
1273
+ update_run_response = {"id": run_id, "name": run_name}
1274
+
1275
+ requests_mock.get(create_url(f"get_run/{run_id}"), json=get_run_response)
1276
+ requests_mock.get(create_url(f"get_tests/{run_id}"), json=get_tests_response)
1277
+ requests_mock.post(create_url(f"update_run/{run_id}"), json=update_run_response)
1278
+
1279
+ # Execute update_run with case_ids=[] and include_all=False to clear all cases
1280
+ run_data, error = api_request_handler.update_run(run_id, run_name, include_all=False, case_ids=[])
1281
+
1282
+ assert error == "", "No error should occur"
1283
+
1284
+ # Verify the payload
1285
+ request_history = requests_mock.request_history
1286
+ update_request = [r for r in request_history if "update_run" in r.url and r.method == "POST"][0]
1287
+ payload = update_request.json()
1288
+
1289
+ assert payload["include_all"] == False, "include_all should be False"
1290
+ assert payload["case_ids"] == [], "case_ids should be empty array"
1291
+
1292
+ @pytest.mark.api_handler
1293
+ def test_update_run_clears_multiple_attributes(self, api_request_handler: ApiRequestHandler, requests_mock):
1294
+ """Test that update_run can clear multiple attributes at once"""
1295
+ run_id = 305
1296
+ run_name = "Test Run"
1297
+
1298
+ get_run_response = {
1299
+ "id": run_id,
1300
+ "name": "Original Run",
1301
+ "description": "Old description",
1302
+ "refs": "REF-1",
1303
+ "include_all": True,
1304
+ "plan_id": None,
1305
+ "config_ids": [],
1306
+ }
1307
+
1308
+ update_run_response = {"id": run_id, "name": run_name}
1309
+
1310
+ requests_mock.get(create_url(f"get_run/{run_id}"), json=get_run_response)
1311
+ requests_mock.post(create_url(f"update_run/{run_id}"), json=update_run_response)
1312
+
1313
+ # Execute update_run clearing multiple attributes
1314
+ run_data, error = api_request_handler.update_run(
1315
+ run_id, run_name, description=None, milestone_id=None, start_date=None, end_date=None
1316
+ )
1317
+
1318
+ assert error == "", "No error should occur"
1319
+
1320
+ # Verify the payload
1321
+ request_history = requests_mock.request_history
1322
+ update_request = [r for r in request_history if "update_run" in r.url and r.method == "POST"][0]
1323
+ payload = update_request.json()
1324
+
1325
+ assert payload["description"] is None, "description should be null"
1326
+ assert payload["milestone_id"] is None, "milestone_id should be null"
1327
+ assert payload["start_on"] is None, "start_on should be null"
1328
+ assert payload["due_on"] is None, "due_on should be null"
1329
+
1330
+ @pytest.mark.api_handler
1331
+ def test_update_run_preserves_values_with_sentinel(self, api_request_handler: ApiRequestHandler, requests_mock):
1332
+ """Test that update_run preserves values when sentinel (...) is used"""
1333
+ run_id = 306
1334
+ run_name = "Test Run"
1335
+
1336
+ get_run_response = {
1337
+ "id": run_id,
1338
+ "name": "Original Run",
1339
+ "description": "Keep this description",
1340
+ "refs": "REF-1",
1341
+ "include_all": True,
1342
+ "plan_id": None,
1343
+ "config_ids": [],
1128
1344
  }
1129
- requests_mock.get(create_url(f"get_tests/{run_id}"), json=mocked_tests_response)
1130
1345
 
1346
+ update_run_response = {"id": run_id, "name": run_name}
1347
+
1348
+ requests_mock.get(create_url(f"get_run/{run_id}"), json=get_run_response)
1349
+ requests_mock.post(create_url(f"update_run/{run_id}"), json=update_run_response)
1350
+
1351
+ # Execute update_run with sentinel (...) for all clearable attributes
1352
+ run_data, error = api_request_handler.update_run(run_id, run_name)
1353
+
1354
+ assert error == "", "No error should occur"
1355
+
1356
+ # Verify the payload preserves existing description
1357
+ request_history = requests_mock.request_history
1358
+ update_request = [r for r in request_history if "update_run" in r.url and r.method == "POST"][0]
1359
+ payload = update_request.json()
1360
+
1361
+ assert payload["description"] == "Keep this description", "description should be preserved"
1362
+ assert payload["refs"] == "REF-1", "refs should be preserved"
1363
+
1364
+ @pytest.mark.api_handler
1365
+ def test_upload_attachments_413_error(self, api_request_handler: ApiRequestHandler, requests_mock, tmp_path):
1366
+ """Test that 413 errors (file too large) are properly reported."""
1131
1367
  # Create a temporary test file
1132
1368
  test_file = tmp_path / "large_attachment.jpg"
1133
1369
  test_file.write_text("test content")
@@ -1141,10 +1377,10 @@ class TestApiRequestHandler:
1141
1377
 
1142
1378
  # Prepare test data
1143
1379
  report_results = [{"case_id": 100, "attachments": [str(test_file)]}]
1144
- results = [{"id": 2001, "test_id": 1001}]
1380
+ case_id_to_result_id = {100: 2001}
1145
1381
 
1146
1382
  # Call upload_attachments
1147
- api_request_handler.upload_attachments(report_results, results, run_id)
1383
+ api_request_handler.upload_attachments(report_results, case_id_to_result_id)
1148
1384
 
1149
1385
  # Verify the request was made (case-insensitive comparison)
1150
1386
  assert requests_mock.last_request.url.lower() == create_url("add_attachment_to_result/2001").lower()
@@ -1152,18 +1388,6 @@ class TestApiRequestHandler:
1152
1388
  @pytest.mark.api_handler
1153
1389
  def test_upload_attachments_success(self, api_request_handler: ApiRequestHandler, requests_mock, tmp_path):
1154
1390
  """Test that successful attachment uploads work correctly."""
1155
- run_id = 1
1156
-
1157
- # Mock get_tests endpoint
1158
- mocked_tests_response = {
1159
- "offset": 0,
1160
- "limit": 250,
1161
- "size": 1,
1162
- "_links": {"next": None, "prev": None},
1163
- "tests": [{"id": 1001, "case_id": 100}],
1164
- }
1165
- requests_mock.get(create_url(f"get_tests/{run_id}"), json=mocked_tests_response)
1166
-
1167
1391
  # Create a temporary test file
1168
1392
  test_file = tmp_path / "test_attachment.jpg"
1169
1393
  test_file.write_text("test content")
@@ -1173,10 +1397,10 @@ class TestApiRequestHandler:
1173
1397
 
1174
1398
  # Prepare test data
1175
1399
  report_results = [{"case_id": 100, "attachments": [str(test_file)]}]
1176
- results = [{"id": 2001, "test_id": 1001}]
1400
+ case_id_to_result_id = {100: 2001}
1177
1401
 
1178
1402
  # Call upload_attachments
1179
- api_request_handler.upload_attachments(report_results, results, run_id)
1403
+ api_request_handler.upload_attachments(report_results, case_id_to_result_id)
1180
1404
 
1181
1405
  # Verify the request was made (case-insensitive comparison)
1182
1406
  assert requests_mock.last_request.url.lower() == create_url("add_attachment_to_result/2001").lower()
@@ -1184,24 +1408,55 @@ class TestApiRequestHandler:
1184
1408
  @pytest.mark.api_handler
1185
1409
  def test_upload_attachments_file_not_found(self, api_request_handler: ApiRequestHandler, requests_mock):
1186
1410
  """Test that missing attachment files are properly reported."""
1187
- run_id = 1
1188
-
1189
- # Mock get_tests endpoint
1190
- mocked_tests_response = {
1191
- "offset": 0,
1192
- "limit": 250,
1193
- "size": 1,
1194
- "_links": {"next": None, "prev": None},
1195
- "tests": [{"id": 1001, "case_id": 100}],
1196
- }
1197
- requests_mock.get(create_url(f"get_tests/{run_id}"), json=mocked_tests_response)
1198
-
1199
1411
  # Prepare test data with non-existent file
1200
1412
  report_results = [{"case_id": 100, "attachments": ["/path/to/nonexistent/file.jpg"]}]
1201
- results = [{"id": 2001, "test_id": 1001}]
1413
+ case_id_to_result_id = {100: 2001}
1202
1414
 
1203
1415
  # Call upload_attachments - should not raise exception
1204
- api_request_handler.upload_attachments(report_results, results, run_id)
1416
+ api_request_handler.upload_attachments(report_results, case_id_to_result_id)
1417
+
1418
+ @pytest.mark.api_handler
1419
+ def test_upload_attachments_empty_run_scenario(
1420
+ self, api_request_handler: ApiRequestHandler, requests_mock, tmp_path
1421
+ ):
1422
+ """Test that attachments work correctly when results are added to an empty run.
1423
+
1424
+ This test covers the bug fix for issue where TRCLI failed to upload attachments
1425
+ when using --run-id with an empty run (created via API with include_all: false
1426
+ and no case_ids). The fix uses case_id to result_id mapping instead of fetching
1427
+ tests from the run.
1428
+ """
1429
+ # Create test attachment files
1430
+ attachment1 = tmp_path / "screenshot1.png"
1431
+ attachment1.write_text("screenshot content 1")
1432
+ attachment2 = tmp_path / "screenshot2.png"
1433
+ attachment2.write_text("screenshot content 2")
1434
+
1435
+ # Mock successful attachment uploads
1436
+ requests_mock.post(create_url("add_attachment_to_result/5001"), status_code=200, json={"attachment_id": 9001})
1437
+ requests_mock.post(create_url("add_attachment_to_result/5002"), status_code=200, json={"attachment_id": 9002})
1438
+
1439
+ # Prepare test data - two cases with attachments
1440
+ report_results = [
1441
+ {"case_id": 100, "attachments": [str(attachment1)]},
1442
+ {"case_id": 101, "attachments": [str(attachment2)]},
1443
+ ]
1444
+
1445
+ # Case ID to result ID mapping (this is built from add_results_for_cases response)
1446
+ case_id_to_result_id = {100: 5001, 101: 5002}
1447
+
1448
+ # Call upload_attachments
1449
+ api_request_handler.upload_attachments(report_results, case_id_to_result_id)
1450
+
1451
+ # Verify both attachments were uploaded correctly
1452
+ history = requests_mock.request_history
1453
+ upload_requests = [req for req in history if "add_attachment_to_result" in req.url]
1454
+ assert len(upload_requests) == 2, "Should have uploaded 2 attachments"
1455
+
1456
+ # Verify correct result IDs were used
1457
+ urls = [req.url.lower() for req in upload_requests]
1458
+ assert any("add_attachment_to_result/5001" in url for url in urls), "Should upload to result 5001"
1459
+ assert any("add_attachment_to_result/5002" in url for url in urls), "Should upload to result 5002"
1205
1460
 
1206
1461
  @pytest.mark.api_handler
1207
1462
  def test_caching_reduces_api_calls(self, api_request_handler: ApiRequestHandler, requests_mock):