trcli 1.13.3__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 (98) hide show
  1. {trcli-1.13.3 → trcli-1.14.0}/PKG-INFO +1 -1
  2. {trcli-1.13.3 → trcli-1.14.0}/README.md +160 -22
  3. {trcli-1.13.3 → trcli-1.14.0}/tests/test_api_request_handler.py +293 -38
  4. trcli-1.14.0/tests/test_cmd_add_run.py +363 -0
  5. {trcli-1.13.3 → trcli-1.14.0}/tests/test_glob_integration.py +31 -0
  6. trcli-1.14.0/tests/test_robot_parser.py +186 -0
  7. trcli-1.14.0/trcli/__init__.py +1 -0
  8. {trcli-1.13.3 → trcli-1.14.0}/trcli/api/api_request_handler.py +19 -3
  9. trcli-1.14.0/trcli/api/multisuite_uploader.py +405 -0
  10. {trcli-1.13.3 → trcli-1.14.0}/trcli/api/project_based_client.py +63 -3
  11. {trcli-1.13.3 → trcli-1.14.0}/trcli/api/result_handler.py +63 -52
  12. {trcli-1.13.3 → trcli-1.14.0}/trcli/api/run_handler.py +128 -25
  13. trcli-1.14.0/trcli/commands/cmd_add_run.py +255 -0
  14. {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_parse_junit.py +33 -10
  15. {trcli-1.13.3 → trcli-1.14.0}/trcli/constants.py +6 -0
  16. {trcli-1.13.3 → trcli-1.14.0}/trcli/readers/junit_xml.py +8 -0
  17. {trcli-1.13.3 → trcli-1.14.0}/trcli/readers/robot_xml.py +59 -3
  18. {trcli-1.13.3 → trcli-1.14.0}/trcli.egg-info/PKG-INFO +1 -1
  19. {trcli-1.13.3 → trcli-1.14.0}/trcli.egg-info/SOURCES.txt +1 -0
  20. trcli-1.13.3/tests/test_cmd_add_run.py +0 -191
  21. trcli-1.13.3/tests/test_robot_parser.py +0 -78
  22. trcli-1.13.3/trcli/__init__.py +0 -1
  23. trcli-1.13.3/trcli/commands/cmd_add_run.py +0 -152
  24. {trcli-1.13.3 → trcli-1.14.0}/LICENSE.md +0 -0
  25. {trcli-1.13.3 → trcli-1.14.0}/setup.cfg +0 -0
  26. {trcli-1.13.3 → trcli-1.14.0}/setup.py +0 -0
  27. {trcli-1.13.3 → trcli-1.14.0}/tests/test_api_client.py +0 -0
  28. {trcli-1.13.3 → trcli-1.14.0}/tests/test_api_client_proxy.py +0 -0
  29. {trcli-1.13.3 → trcli-1.14.0}/tests/test_api_data_provider.py +0 -0
  30. {trcli-1.13.3 → trcli-1.14.0}/tests/test_api_request_handler_case_fields_update.py +0 -0
  31. {trcli-1.13.3 → trcli-1.14.0}/tests/test_api_request_handler_case_matcher.py +0 -0
  32. {trcli-1.13.3 → trcli-1.14.0}/tests/test_api_request_handler_labels.py +0 -0
  33. {trcli-1.13.3 → trcli-1.14.0}/tests/test_api_request_handler_references.py +0 -0
  34. {trcli-1.13.3 → trcli-1.14.0}/tests/test_cli.py +0 -0
  35. {trcli-1.13.3 → trcli-1.14.0}/tests/test_cmd_export_gherkin.py +0 -0
  36. {trcli-1.13.3 → trcli-1.14.0}/tests/test_cmd_import_gherkin.py +0 -0
  37. {trcli-1.13.3 → trcli-1.14.0}/tests/test_cmd_labels.py +0 -0
  38. {trcli-1.13.3 → trcli-1.14.0}/tests/test_cmd_parse_cucumber.py +0 -0
  39. {trcli-1.13.3 → trcli-1.14.0}/tests/test_cmd_references.py +0 -0
  40. {trcli-1.13.3 → trcli-1.14.0}/tests/test_cmd_update.py +0 -0
  41. {trcli-1.13.3 → trcli-1.14.0}/tests/test_cucumber_bdd_matching.py +0 -0
  42. {trcli-1.13.3 → trcli-1.14.0}/tests/test_cucumber_parser.py +0 -0
  43. {trcli-1.13.3 → trcli-1.14.0}/tests/test_dataclass_creation.py +0 -0
  44. {trcli-1.13.3 → trcli-1.14.0}/tests/test_glob_deduplication.py +0 -0
  45. {trcli-1.13.3 → trcli-1.14.0}/tests/test_junit_bdd_parser.py +0 -0
  46. {trcli-1.13.3 → trcli-1.14.0}/tests/test_junit_parse_reference.py +0 -0
  47. {trcli-1.13.3 → trcli-1.14.0}/tests/test_junit_parser.py +0 -0
  48. {trcli-1.13.3 → trcli-1.14.0}/tests/test_load_data_from_config.py +0 -0
  49. {trcli-1.13.3 → trcli-1.14.0}/tests/test_matchers_parser.py +0 -0
  50. {trcli-1.13.3 → trcli-1.14.0}/tests/test_multiple_case_ids.py +0 -0
  51. {trcli-1.13.3 → trcli-1.14.0}/tests/test_project_based_client.py +0 -0
  52. {trcli-1.13.3 → trcli-1.14.0}/tests/test_response_verify.py +0 -0
  53. {trcli-1.13.3 → trcli-1.14.0}/tests/test_results_uploader.py +0 -0
  54. {trcli-1.13.3 → trcli-1.14.0}/tests/test_version_checker.py +0 -0
  55. {trcli-1.13.3 → trcli-1.14.0}/trcli/api/__init__.py +0 -0
  56. {trcli-1.13.3 → trcli-1.14.0}/trcli/api/api_cache.py +0 -0
  57. {trcli-1.13.3 → trcli-1.14.0}/trcli/api/api_client.py +0 -0
  58. {trcli-1.13.3 → trcli-1.14.0}/trcli/api/api_response_verify.py +0 -0
  59. {trcli-1.13.3 → trcli-1.14.0}/trcli/api/api_utils.py +0 -0
  60. {trcli-1.13.3 → trcli-1.14.0}/trcli/api/bdd_handler.py +0 -0
  61. {trcli-1.13.3 → trcli-1.14.0}/trcli/api/case_handler.py +0 -0
  62. {trcli-1.13.3 → trcli-1.14.0}/trcli/api/case_matcher.py +0 -0
  63. {trcli-1.13.3 → trcli-1.14.0}/trcli/api/label_manager.py +0 -0
  64. {trcli-1.13.3 → trcli-1.14.0}/trcli/api/reference_manager.py +0 -0
  65. {trcli-1.13.3 → trcli-1.14.0}/trcli/api/results_uploader.py +0 -0
  66. {trcli-1.13.3 → trcli-1.14.0}/trcli/api/section_handler.py +0 -0
  67. {trcli-1.13.3 → trcli-1.14.0}/trcli/api/suite_handler.py +0 -0
  68. {trcli-1.13.3 → trcli-1.14.0}/trcli/backports.py +0 -0
  69. {trcli-1.13.3 → trcli-1.14.0}/trcli/cli.py +0 -0
  70. {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/__init__.py +0 -0
  71. {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_export_gherkin.py +0 -0
  72. {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_import_gherkin.py +0 -0
  73. {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_labels.py +0 -0
  74. {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_parse_cucumber.py +0 -0
  75. {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_parse_openapi.py +0 -0
  76. {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_parse_robot.py +0 -0
  77. {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_references.py +0 -0
  78. {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_update.py +0 -0
  79. {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/results_parser_helpers.py +0 -0
  80. {trcli-1.13.3 → trcli-1.14.0}/trcli/data_classes/__init__.py +0 -0
  81. {trcli-1.13.3 → trcli-1.14.0}/trcli/data_classes/data_parsers.py +0 -0
  82. {trcli-1.13.3 → trcli-1.14.0}/trcli/data_classes/dataclass_testrail.py +0 -0
  83. {trcli-1.13.3 → trcli-1.14.0}/trcli/data_classes/validation_exception.py +0 -0
  84. {trcli-1.13.3 → trcli-1.14.0}/trcli/data_providers/api_data_provider.py +0 -0
  85. {trcli-1.13.3 → trcli-1.14.0}/trcli/logging/__init__.py +0 -0
  86. {trcli-1.13.3 → trcli-1.14.0}/trcli/logging/config.py +0 -0
  87. {trcli-1.13.3 → trcli-1.14.0}/trcli/logging/file_handler.py +0 -0
  88. {trcli-1.13.3 → trcli-1.14.0}/trcli/logging/structured_logger.py +0 -0
  89. {trcli-1.13.3 → trcli-1.14.0}/trcli/readers/__init__.py +0 -0
  90. {trcli-1.13.3 → trcli-1.14.0}/trcli/readers/cucumber_json.py +0 -0
  91. {trcli-1.13.3 → trcli-1.14.0}/trcli/readers/file_parser.py +0 -0
  92. {trcli-1.13.3 → trcli-1.14.0}/trcli/readers/openapi_yml.py +0 -0
  93. {trcli-1.13.3 → trcli-1.14.0}/trcli/settings.py +0 -0
  94. {trcli-1.13.3 → trcli-1.14.0}/trcli/version_checker.py +0 -0
  95. {trcli-1.13.3 → trcli-1.14.0}/trcli.egg-info/dependency_links.txt +0 -0
  96. {trcli-1.13.3 → trcli-1.14.0}/trcli.egg-info/entry_points.txt +0 -0
  97. {trcli-1.13.3 → trcli-1.14.0}/trcli.egg-info/requires.txt +0 -0
  98. {trcli-1.13.3 → 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.3
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.3
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.3
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
 
@@ -196,7 +196,7 @@ For further detail, please refer to the
196
196
 
197
197
  ### Using Glob Patterns for Multiple Files
198
198
 
199
- TRCLI supports glob patterns to process multiple report files in a single command. This feature is available for **JUnit XML** and **Cucumber JSON** parsers.
199
+ TRCLI supports glob patterns to process multiple report files in a single command. This feature is available for **JUnit XML**, **Robot Framework**, and **Cucumber JSON** parsers.
200
200
 
201
201
  #### Important: Shell Quoting Requirement
202
202
 
@@ -239,6 +239,7 @@ When a glob pattern matches **multiple files**, TRCLI automatically:
239
239
  3. **Merges test results** into a single combined report
240
240
  4. **Writes merged file** to current directory:
241
241
  - JUnit: `Merged-JUnit-report.xml`
242
+ - Robot Framework: `Merged-Robot-report.xml`
242
243
  - Cucumber: `merged_cucumber.json`
243
244
  5. **Processes the merged file** as a single test run upload
244
245
 
@@ -263,6 +264,23 @@ trcli parse_junit \
263
264
  --case-matcher auto
264
265
  ```
265
266
 
267
+ **Robot Framework - Multiple output files:**
268
+ ```bash
269
+ # Merge multiple Robot Framework test runs
270
+ trcli -y \
271
+ -h https://example.testrail.com \
272
+ --project "My Project" \
273
+ parse_robot \
274
+ -f "reports/robot-*.xml" \
275
+ --title "Merged Robot Tests"
276
+
277
+ # Recursive search for all Robot outputs
278
+ trcli parse_robot \
279
+ -f "test-results/**/output.xml" \
280
+ --title "All Robot Results" \
281
+ --case-matcher property
282
+ ```
283
+
266
284
  **Cucumber JSON - Multiple test runs:**
267
285
  ```bash
268
286
  # Merge multiple Cucumber JSON reports
@@ -864,6 +882,40 @@ the `--special-parser saucectl` command line option.
864
882
  Please refer to the [SauceLabs and saucectl reports](https://support.gurock.com/hc/en-us/articles/12719558686484)
865
883
  documentation for further information.
866
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
+
867
919
  #### Creating new test runs
868
920
 
869
921
  When a test run MUST created before using one of the parse commands, use the `add_run` command. For example, if
@@ -1623,7 +1675,7 @@ Options:
1623
1675
  ### Reference
1624
1676
  ```shell
1625
1677
  $ trcli add_run --help
1626
- TestRail CLI v1.13.3
1678
+ TestRail CLI v1.14.0
1627
1679
  Copyright 2025 Gurock Software GmbH - www.gurock.com
1628
1680
  Usage: trcli add_run [OPTIONS]
1629
1681
 
@@ -1640,6 +1692,18 @@ Options:
1640
1692
  --run-end-date The expected or scheduled end date of this test run in MM/DD/YYYY format
1641
1693
  --run-assigned-to-id The ID of the user the test run should be assigned
1642
1694
  to. [x>=1]
1695
+ --clear-run-assigned-to-id Clear the assignee of the test run (only valid
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).
1643
1707
  --run-include-all Use this option to include all test cases in this test run.
1644
1708
  --auto-close-run Use this option to automatically close the created run.
1645
1709
  --run-case-ids Comma separated list of test case IDs to include in
@@ -1712,29 +1776,103 @@ trcli -y -h https://example.testrail.io/ --project "My Project" \
1712
1776
  - **Action Requirements**: `update` and `delete` actions require an existing run (--run-id must be provided)
1713
1777
  - **Validation**: Invalid reference formats are rejected with clear error messages
1714
1778
 
1715
- #### Examples
1779
+ ### Managing Assignees in Test Runs
1780
+
1781
+ The `add_run` command supports comprehensive assignee management for test runs. You can assign runs to users when creating or updating them, and clear assignees when needed.
1782
+
1783
+ #### Assigning Runs to Users
1784
+
1785
+ When creating a new test run or updating an existing one, you can assign it to a user using the `--run-assigned-to-id` option:
1716
1786
 
1717
- **Complete Workflow Example:**
1718
1787
  ```bash
1719
- # 1. Create run with initial references
1720
- trcli -y -h https://example.testrail.io/ <--username and --password or --key> --project "My Project" \
1721
- add_run --title "Sprint 1 Tests" --run-refs "JIRA-100,JIRA-200" -f "run_config.yml"
1788
+ # Create a new run and assign to user ID 5
1789
+ trcli -y -h https://example.testrail.io/ --project "My Project" \
1790
+ add_run --title "My Test Run" --run-assigned-to-id 5
1722
1791
 
1723
- # 2. Add more references (from the config file)
1724
- trcli -y -h https://example.testrail.io/ <--username and --password or --key> --project "My Project" \
1725
- -c run_config.yml add_run --run-refs "JIRA-300,REQ-001" --run-refs-action "add"
1792
+ # Update an existing run and change the assignee
1793
+ trcli -y -h https://example.testrail.io/ --project "My Project" \
1794
+ add_run --run-id 123 --title "My Test Run" --run-assigned-to-id 10
1795
+ ```
1796
+
1797
+ #### Clearing Assignees from Test Runs
1798
+
1799
+ To remove the assignee from an existing test run, use the `--clear-run-assigned-to-id` flag:
1800
+
1801
+ ```bash
1802
+ # Clear the assignee from an existing run
1803
+ trcli -y -h https://example.testrail.io/ --project "My Project" \
1804
+ add_run --run-id 123 --title "My Test Run" --clear-run-assigned-to-id
1805
+ ```
1726
1806
 
1727
- # 3. Replace all references with new ones
1728
- trcli -y -h https://example.testrail.io/ <--username and --password or --key> --project "My Project" \
1729
- -c run_config.yml add_run --run-refs "FINAL-100,FINAL-200" --run-refs-action "update"
1807
+ #### Assignee Management Rules
1730
1808
 
1731
- # 4. Remove specific references
1732
- trcli -y -h https://example.testrail.io/ <--username and --password or --key> --project "My Project" \
1733
- -c run_config.yml add_run --run-refs "FINAL-100" --run-refs-action "delete"
1809
+ - **Update Mode Only**: The `--clear-run-assigned-to-id` flag can only be used when updating an existing run (requires `--run-id`)
1810
+ - **Mutually Exclusive**: You cannot use both `--run-assigned-to-id` and `--clear-run-assigned-to-id` in the same command
1734
1811
 
1735
- # 5. Clear all references
1736
- trcli -y -h https://example.testrail.io/ <--username and --password or --key> --project "My Project" \
1737
- -c run_config.yml add_run --run-refs-action "delete"
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
1827
+
1828
+ ```bash
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
+ ```
1833
+
1834
+ #### Clear Milestone Example
1835
+
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
+ ```
1841
+
1842
+ #### Clear Dates Example
1843
+
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
1738
1876
  ```
1739
1877
 
1740
1878
  Generating test cases from OpenAPI specs
@@ -1747,7 +1885,7 @@ providing you with a solid base of test cases, which you can further expand on T
1747
1885
  ### Reference
1748
1886
  ```shell
1749
1887
  $ trcli parse_openapi --help
1750
- TestRail CLI v1.13.3
1888
+ TestRail CLI v1.14.0
1751
1889
  Copyright 2025 Gurock Software GmbH - www.gurock.com
1752
1890
  Usage: trcli parse_openapi [OPTIONS]
1753
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):