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.
- {trcli-1.13.3 → trcli-1.14.0}/PKG-INFO +1 -1
- {trcli-1.13.3 → trcli-1.14.0}/README.md +160 -22
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_api_request_handler.py +293 -38
- trcli-1.14.0/tests/test_cmd_add_run.py +363 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_glob_integration.py +31 -0
- trcli-1.14.0/tests/test_robot_parser.py +186 -0
- trcli-1.14.0/trcli/__init__.py +1 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/api/api_request_handler.py +19 -3
- trcli-1.14.0/trcli/api/multisuite_uploader.py +405 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/api/project_based_client.py +63 -3
- {trcli-1.13.3 → trcli-1.14.0}/trcli/api/result_handler.py +63 -52
- {trcli-1.13.3 → trcli-1.14.0}/trcli/api/run_handler.py +128 -25
- trcli-1.14.0/trcli/commands/cmd_add_run.py +255 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_parse_junit.py +33 -10
- {trcli-1.13.3 → trcli-1.14.0}/trcli/constants.py +6 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/readers/junit_xml.py +8 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/readers/robot_xml.py +59 -3
- {trcli-1.13.3 → trcli-1.14.0}/trcli.egg-info/PKG-INFO +1 -1
- {trcli-1.13.3 → trcli-1.14.0}/trcli.egg-info/SOURCES.txt +1 -0
- trcli-1.13.3/tests/test_cmd_add_run.py +0 -191
- trcli-1.13.3/tests/test_robot_parser.py +0 -78
- trcli-1.13.3/trcli/__init__.py +0 -1
- trcli-1.13.3/trcli/commands/cmd_add_run.py +0 -152
- {trcli-1.13.3 → trcli-1.14.0}/LICENSE.md +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/setup.cfg +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/setup.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_api_client.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_api_client_proxy.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_api_data_provider.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_api_request_handler_case_fields_update.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_api_request_handler_case_matcher.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_api_request_handler_labels.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_api_request_handler_references.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_cli.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_cmd_export_gherkin.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_cmd_import_gherkin.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_cmd_labels.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_cmd_parse_cucumber.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_cmd_references.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_cmd_update.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_cucumber_bdd_matching.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_cucumber_parser.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_dataclass_creation.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_glob_deduplication.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_junit_bdd_parser.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_junit_parse_reference.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_junit_parser.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_load_data_from_config.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_matchers_parser.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_multiple_case_ids.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_project_based_client.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_response_verify.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_results_uploader.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/tests/test_version_checker.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/api/__init__.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/api/api_cache.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/api/api_client.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/api/api_response_verify.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/api/api_utils.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/api/bdd_handler.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/api/case_handler.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/api/case_matcher.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/api/label_manager.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/api/reference_manager.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/api/results_uploader.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/api/section_handler.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/api/suite_handler.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/backports.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/cli.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/__init__.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_export_gherkin.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_import_gherkin.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_labels.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_parse_cucumber.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_parse_openapi.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_parse_robot.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_references.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/cmd_update.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/commands/results_parser_helpers.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/data_classes/__init__.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/data_classes/data_parsers.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/data_classes/dataclass_testrail.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/data_classes/validation_exception.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/data_providers/api_data_provider.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/logging/__init__.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/logging/config.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/logging/file_handler.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/logging/structured_logger.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/readers/__init__.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/readers/cucumber_json.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/readers/file_parser.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/readers/openapi_yml.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/settings.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli/version_checker.py +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli.egg-info/dependency_links.txt +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli.egg-info/entry_points.txt +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli.egg-info/requires.txt +0 -0
- {trcli-1.13.3 → trcli-1.14.0}/trcli.egg-info/top_level.txt +0 -0
|
@@ -33,7 +33,7 @@ trcli
|
|
|
33
33
|
```
|
|
34
34
|
You should get something like this:
|
|
35
35
|
```
|
|
36
|
-
TestRail CLI v1.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
#
|
|
1720
|
-
trcli -y -h https://example.testrail.io/
|
|
1721
|
-
add_run --title "
|
|
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
|
-
#
|
|
1724
|
-
trcli -y -h https://example.testrail.io/
|
|
1725
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1732
|
-
|
|
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
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
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.
|
|
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
|
|
1118
|
-
"""Test that
|
|
1119
|
-
run_id =
|
|
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
|
-
|
|
1122
|
-
mocked_tests_response = {
|
|
1265
|
+
get_tests_response = {
|
|
1123
1266
|
"offset": 0,
|
|
1124
1267
|
"limit": 250,
|
|
1125
|
-
"size":
|
|
1268
|
+
"size": 2,
|
|
1126
1269
|
"_links": {"next": None, "prev": None},
|
|
1127
|
-
"tests": [{"id":
|
|
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
|
-
|
|
1380
|
+
case_id_to_result_id = {100: 2001}
|
|
1145
1381
|
|
|
1146
1382
|
# Call upload_attachments
|
|
1147
|
-
api_request_handler.upload_attachments(report_results,
|
|
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
|
-
|
|
1400
|
+
case_id_to_result_id = {100: 2001}
|
|
1177
1401
|
|
|
1178
1402
|
# Call upload_attachments
|
|
1179
|
-
api_request_handler.upload_attachments(report_results,
|
|
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
|
-
|
|
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,
|
|
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):
|