trcli 1.13.3__tar.gz → 1.13.4__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.13.4}/PKG-INFO +1 -1
- {trcli-1.13.3 → trcli-1.13.4}/README.md +58 -5
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_cmd_add_run.py +71 -38
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_glob_integration.py +31 -0
- trcli-1.13.4/tests/test_robot_parser.py +186 -0
- trcli-1.13.4/trcli/__init__.py +1 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/api/api_request_handler.py +4 -1
- {trcli-1.13.3 → trcli-1.13.4}/trcli/api/project_based_client.py +10 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/api/run_handler.py +17 -4
- {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_add_run.py +58 -46
- {trcli-1.13.3 → trcli-1.13.4}/trcli/readers/robot_xml.py +59 -3
- {trcli-1.13.3 → trcli-1.13.4}/trcli.egg-info/PKG-INFO +1 -1
- 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-1.13.4}/LICENSE.md +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/setup.cfg +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/setup.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_api_client.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_api_client_proxy.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_api_data_provider.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_api_request_handler.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_api_request_handler_case_fields_update.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_api_request_handler_case_matcher.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_api_request_handler_labels.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_api_request_handler_references.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_cli.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_cmd_export_gherkin.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_cmd_import_gherkin.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_cmd_labels.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_cmd_parse_cucumber.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_cmd_references.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_cmd_update.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_cucumber_bdd_matching.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_cucumber_parser.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_dataclass_creation.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_glob_deduplication.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_junit_bdd_parser.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_junit_parse_reference.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_junit_parser.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_load_data_from_config.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_matchers_parser.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_multiple_case_ids.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_project_based_client.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_response_verify.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_results_uploader.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/tests/test_version_checker.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/api/__init__.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/api/api_cache.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/api/api_client.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/api/api_response_verify.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/api/api_utils.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/api/bdd_handler.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/api/case_handler.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/api/case_matcher.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/api/label_manager.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/api/reference_manager.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/api/result_handler.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/api/results_uploader.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/api/section_handler.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/api/suite_handler.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/backports.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/cli.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/__init__.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_export_gherkin.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_import_gherkin.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_labels.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_parse_cucumber.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_parse_junit.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_parse_openapi.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_parse_robot.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_references.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_update.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/results_parser_helpers.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/constants.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/data_classes/__init__.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/data_classes/data_parsers.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/data_classes/dataclass_testrail.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/data_classes/validation_exception.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/data_providers/api_data_provider.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/logging/__init__.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/logging/config.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/logging/file_handler.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/logging/structured_logger.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/readers/__init__.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/readers/cucumber_json.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/readers/file_parser.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/readers/junit_xml.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/readers/openapi_yml.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/settings.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli/version_checker.py +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli.egg-info/SOURCES.txt +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli.egg-info/dependency_links.txt +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli.egg-info/entry_points.txt +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/trcli.egg-info/requires.txt +0 -0
- {trcli-1.13.3 → trcli-1.13.4}/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.13.
|
|
36
|
+
TestRail CLI v1.13.4
|
|
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.
|
|
54
|
+
TestRail CLI v1.13.4
|
|
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
|
|
@@ -1623,7 +1641,7 @@ Options:
|
|
|
1623
1641
|
### Reference
|
|
1624
1642
|
```shell
|
|
1625
1643
|
$ trcli add_run --help
|
|
1626
|
-
TestRail CLI v1.13.
|
|
1644
|
+
TestRail CLI v1.13.4
|
|
1627
1645
|
Copyright 2025 Gurock Software GmbH - www.gurock.com
|
|
1628
1646
|
Usage: trcli add_run [OPTIONS]
|
|
1629
1647
|
|
|
@@ -1640,6 +1658,8 @@ Options:
|
|
|
1640
1658
|
--run-end-date The expected or scheduled end date of this test run in MM/DD/YYYY format
|
|
1641
1659
|
--run-assigned-to-id The ID of the user the test run should be assigned
|
|
1642
1660
|
to. [x>=1]
|
|
1661
|
+
--clear-run-assigned-to-id Clear the assignee of the test run (only valid
|
|
1662
|
+
when updating with --run-id).
|
|
1643
1663
|
--run-include-all Use this option to include all test cases in this test run.
|
|
1644
1664
|
--auto-close-run Use this option to automatically close the created run.
|
|
1645
1665
|
--run-case-ids Comma separated list of test case IDs to include in
|
|
@@ -1712,6 +1732,39 @@ trcli -y -h https://example.testrail.io/ --project "My Project" \
|
|
|
1712
1732
|
- **Action Requirements**: `update` and `delete` actions require an existing run (--run-id must be provided)
|
|
1713
1733
|
- **Validation**: Invalid reference formats are rejected with clear error messages
|
|
1714
1734
|
|
|
1735
|
+
### Managing Assignees in Test Runs
|
|
1736
|
+
|
|
1737
|
+
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.
|
|
1738
|
+
|
|
1739
|
+
#### Assigning Runs to Users
|
|
1740
|
+
|
|
1741
|
+
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:
|
|
1742
|
+
|
|
1743
|
+
```bash
|
|
1744
|
+
# Create a new run and assign to user ID 5
|
|
1745
|
+
trcli -y -h https://example.testrail.io/ --project "My Project" \
|
|
1746
|
+
add_run --title "My Test Run" --run-assigned-to-id 5
|
|
1747
|
+
|
|
1748
|
+
# Update an existing run and change the assignee
|
|
1749
|
+
trcli -y -h https://example.testrail.io/ --project "My Project" \
|
|
1750
|
+
add_run --run-id 123 --title "My Test Run" --run-assigned-to-id 10
|
|
1751
|
+
```
|
|
1752
|
+
|
|
1753
|
+
#### Clearing Assignees from Test Runs
|
|
1754
|
+
|
|
1755
|
+
To remove the assignee from an existing test run, use the `--clear-run-assigned-to-id` flag:
|
|
1756
|
+
|
|
1757
|
+
```bash
|
|
1758
|
+
# Clear the assignee from an existing run
|
|
1759
|
+
trcli -y -h https://example.testrail.io/ --project "My Project" \
|
|
1760
|
+
add_run --run-id 123 --title "My Test Run" --clear-run-assigned-to-id
|
|
1761
|
+
```
|
|
1762
|
+
|
|
1763
|
+
#### Assignee Management Rules
|
|
1764
|
+
|
|
1765
|
+
- **Update Mode Only**: The `--clear-run-assigned-to-id` flag can only be used when updating an existing run (requires `--run-id`)
|
|
1766
|
+
- **Mutually Exclusive**: You cannot use both `--run-assigned-to-id` and `--clear-run-assigned-to-id` in the same command
|
|
1767
|
+
|
|
1715
1768
|
#### Examples
|
|
1716
1769
|
|
|
1717
1770
|
**Complete Workflow Example:**
|
|
@@ -1747,7 +1800,7 @@ providing you with a solid base of test cases, which you can further expand on T
|
|
|
1747
1800
|
### Reference
|
|
1748
1801
|
```shell
|
|
1749
1802
|
$ trcli parse_openapi --help
|
|
1750
|
-
TestRail CLI v1.13.
|
|
1803
|
+
TestRail CLI v1.13.4
|
|
1751
1804
|
Copyright 2025 Gurock Software GmbH - www.gurock.com
|
|
1752
1805
|
Usage: trcli parse_openapi [OPTIONS]
|
|
1753
1806
|
|
|
@@ -42,9 +42,11 @@ class TestCmdAddRun:
|
|
|
42
42
|
environment.run_assigned_to_id = assigned_to_id
|
|
43
43
|
environment.run_case_ids = case_ids
|
|
44
44
|
environment.run_include_all = True
|
|
45
|
-
expected_string = (
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
expected_string = (
|
|
46
|
+
f"run_assigned_to_id: {assigned_to_id}\nrun_case_ids: '{case_ids}'\n"
|
|
47
|
+
f"run_description: {description}\nrun_id: {run_id}\n"
|
|
48
|
+
f"run_include_all: true\nrun_refs: {refs}\ntitle: {title}\n"
|
|
49
|
+
)
|
|
48
50
|
cmd_add_run.write_run_to_file(environment, run_id)
|
|
49
51
|
mock_open_file.assert_called_with(file, "a")
|
|
50
52
|
mock_open_file.return_value.__enter__().write.assert_called_once_with(expected_string)
|
|
@@ -52,35 +54,31 @@ class TestCmdAddRun:
|
|
|
52
54
|
def test_cli_validation_refs_too_long(self):
|
|
53
55
|
"""Test that references validation fails when exceeding 250 characters"""
|
|
54
56
|
from trcli.cli import Environment
|
|
55
|
-
|
|
57
|
+
|
|
56
58
|
environment = Environment()
|
|
57
59
|
environment.run_refs = "A" * 251 # 251 characters, exceeds limit
|
|
58
|
-
|
|
60
|
+
|
|
59
61
|
assert len(environment.run_refs) > 250
|
|
60
|
-
|
|
62
|
+
|
|
61
63
|
runner = CliRunner()
|
|
62
64
|
long_refs = "A" * 251
|
|
63
|
-
|
|
64
|
-
result = runner.invoke(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
|
|
66
|
+
result = runner.invoke(
|
|
67
|
+
cmd_add_run.cli, ["--title", "Test Run", "--run-refs", long_refs], catch_exceptions=False
|
|
68
|
+
)
|
|
69
|
+
|
|
69
70
|
# Should exit with error code 1 due to missing required parameters or validation
|
|
70
71
|
assert result.exit_code == 1
|
|
71
72
|
|
|
72
73
|
def test_cli_validation_refs_exactly_250_chars(self):
|
|
73
74
|
"""Test that references validation passes with exactly 250 characters"""
|
|
74
75
|
from trcli.cli import Environment
|
|
75
|
-
|
|
76
|
+
|
|
76
77
|
runner = CliRunner()
|
|
77
78
|
refs_250 = "A" * 250 # Exactly 250 characters, should pass validation
|
|
78
|
-
|
|
79
|
-
result = runner.invoke(cmd_add_run.cli, [
|
|
80
|
-
|
|
81
|
-
'--run-refs', refs_250
|
|
82
|
-
], catch_exceptions=False)
|
|
83
|
-
|
|
79
|
+
|
|
80
|
+
result = runner.invoke(cmd_add_run.cli, ["--title", "Test Run", "--run-refs", refs_250], catch_exceptions=False)
|
|
81
|
+
|
|
84
82
|
# Should not fail due to refs validation (will fail due to missing required parameters)
|
|
85
83
|
# But the important thing is that it doesn't fail with the character limit error
|
|
86
84
|
assert "References field cannot exceed 250 characters" not in result.output
|
|
@@ -88,18 +86,18 @@ class TestCmdAddRun:
|
|
|
88
86
|
def test_validation_logic_refs_action_without_run_id(self):
|
|
89
87
|
"""Test validation logic for refs action without run_id"""
|
|
90
88
|
from trcli.cli import Environment
|
|
91
|
-
|
|
89
|
+
|
|
92
90
|
# Update action validation
|
|
93
91
|
environment = Environment()
|
|
94
92
|
environment.run_refs_action = "update"
|
|
95
93
|
environment.run_id = None
|
|
96
94
|
environment.run_refs = "JIRA-123"
|
|
97
|
-
|
|
95
|
+
|
|
98
96
|
# This should be invalid
|
|
99
97
|
assert environment.run_refs_action == "update"
|
|
100
98
|
assert environment.run_id is None
|
|
101
|
-
|
|
102
|
-
# Delete action validation
|
|
99
|
+
|
|
100
|
+
# Delete action validation
|
|
103
101
|
environment.run_refs_action = "delete"
|
|
104
102
|
assert environment.run_refs_action == "delete"
|
|
105
103
|
assert environment.run_id is None
|
|
@@ -107,37 +105,72 @@ class TestCmdAddRun:
|
|
|
107
105
|
def test_refs_action_parameter_parsing(self):
|
|
108
106
|
"""Test that refs action parameter is parsed correctly"""
|
|
109
107
|
runner = CliRunner()
|
|
110
|
-
|
|
108
|
+
|
|
111
109
|
# Test that the CLI accepts new param without crashing! :) - acuanico
|
|
112
|
-
result = runner.invoke(cmd_add_run.cli, [
|
|
110
|
+
result = runner.invoke(cmd_add_run.cli, ["--help"])
|
|
113
111
|
assert result.exit_code == 0
|
|
114
112
|
assert "--run-refs-action" in result.output
|
|
115
113
|
assert "Action to perform on references" in result.output
|
|
116
114
|
|
|
115
|
+
def test_clear_assigned_to_id_parameter_exists(self):
|
|
116
|
+
"""Test that --clear-run-assigned-to-id parameter is available"""
|
|
117
|
+
runner = CliRunner()
|
|
118
|
+
|
|
119
|
+
result = runner.invoke(cmd_add_run.cli, ["--help"])
|
|
120
|
+
assert result.exit_code == 0
|
|
121
|
+
assert "--clear-run-assigned-to-id" in result.output
|
|
122
|
+
assert "Clear the assignee" in result.output
|
|
123
|
+
|
|
124
|
+
@mock.patch("trcli.cli.Environment.check_for_required_parameters")
|
|
125
|
+
def test_clear_assigned_to_id_requires_run_id(self, mock_check):
|
|
126
|
+
"""Test that --clear-run-assigned-to-id requires --run-id"""
|
|
127
|
+
runner = CliRunner()
|
|
128
|
+
|
|
129
|
+
result = runner.invoke(cmd_add_run.cli, ["--title", "Test Run", "--clear-run-assigned-to-id"])
|
|
130
|
+
|
|
131
|
+
assert result.exit_code == 1
|
|
132
|
+
assert (
|
|
133
|
+
"--clear-run-assigned-to-id can only be used when updating" in result.output
|
|
134
|
+
or "--run-id required" in result.output
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
@mock.patch("trcli.cli.Environment.check_for_required_parameters")
|
|
138
|
+
def test_clear_assigned_to_id_mutually_exclusive_with_assigned_to_id(self, mock_check):
|
|
139
|
+
"""Test that --clear-run-assigned-to-id and --run-assigned-to-id are mutually exclusive"""
|
|
140
|
+
runner = CliRunner()
|
|
141
|
+
|
|
142
|
+
result = runner.invoke(
|
|
143
|
+
cmd_add_run.cli,
|
|
144
|
+
["--title", "Test Run", "--run-id", "123", "--run-assigned-to-id", "42", "--clear-run-assigned-to-id"],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
assert result.exit_code == 1
|
|
148
|
+
assert "cannot be used together" in result.output
|
|
149
|
+
|
|
117
150
|
|
|
118
151
|
class TestApiRequestHandlerReferences:
|
|
119
152
|
"""Test class for reference management functionality"""
|
|
120
|
-
|
|
153
|
+
|
|
121
154
|
def test_manage_references_add(self):
|
|
122
155
|
"""Test adding references to existing ones"""
|
|
123
156
|
from trcli.api.api_request_handler import ApiRequestHandler
|
|
124
157
|
from trcli.cli import Environment
|
|
125
158
|
from trcli.api.api_client import APIClient
|
|
126
159
|
from trcli.data_classes.dataclass_testrail import TestRailSuite
|
|
127
|
-
|
|
160
|
+
|
|
128
161
|
environment = Environment()
|
|
129
162
|
api_client = APIClient("https://test.testrail.com")
|
|
130
163
|
suite = TestRailSuite(name="Test Suite")
|
|
131
164
|
handler = ApiRequestHandler(environment, api_client, suite)
|
|
132
|
-
|
|
165
|
+
|
|
133
166
|
# Adding new references
|
|
134
167
|
result = handler._manage_references("JIRA-100,JIRA-200", "JIRA-300,JIRA-400", "add")
|
|
135
168
|
assert result == "JIRA-100,JIRA-200,JIRA-300,JIRA-400"
|
|
136
|
-
|
|
169
|
+
|
|
137
170
|
# Adding duplicate references (should not duplicate)
|
|
138
171
|
result = handler._manage_references("JIRA-100,JIRA-200", "JIRA-200,JIRA-300", "add")
|
|
139
172
|
assert result == "JIRA-100,JIRA-200,JIRA-300"
|
|
140
|
-
|
|
173
|
+
|
|
141
174
|
# Adding to empty existing references
|
|
142
175
|
result = handler._manage_references("", "JIRA-100,JIRA-200", "add")
|
|
143
176
|
assert result == "JIRA-100,JIRA-200"
|
|
@@ -148,16 +181,16 @@ class TestApiRequestHandlerReferences:
|
|
|
148
181
|
from trcli.cli import Environment
|
|
149
182
|
from trcli.api.api_client import APIClient
|
|
150
183
|
from trcli.data_classes.dataclass_testrail import TestRailSuite
|
|
151
|
-
|
|
184
|
+
|
|
152
185
|
environment = Environment()
|
|
153
186
|
api_client = APIClient("https://test.testrail.com")
|
|
154
187
|
suite = TestRailSuite(name="Test Suite")
|
|
155
188
|
handler = ApiRequestHandler(environment, api_client, suite)
|
|
156
|
-
|
|
189
|
+
|
|
157
190
|
# Test replacing all references
|
|
158
191
|
result = handler._manage_references("JIRA-100,JIRA-200", "JIRA-300,JIRA-400", "update")
|
|
159
192
|
assert result == "JIRA-300,JIRA-400"
|
|
160
|
-
|
|
193
|
+
|
|
161
194
|
# Test replacing with empty references
|
|
162
195
|
result = handler._manage_references("JIRA-100,JIRA-200", "", "update")
|
|
163
196
|
assert result == ""
|
|
@@ -168,24 +201,24 @@ class TestApiRequestHandlerReferences:
|
|
|
168
201
|
from trcli.cli import Environment
|
|
169
202
|
from trcli.api.api_client import APIClient
|
|
170
203
|
from trcli.data_classes.dataclass_testrail import TestRailSuite
|
|
171
|
-
|
|
204
|
+
|
|
172
205
|
environment = Environment()
|
|
173
206
|
api_client = APIClient("https://test.testrail.com")
|
|
174
207
|
suite = TestRailSuite(name="Test Suite")
|
|
175
208
|
handler = ApiRequestHandler(environment, api_client, suite)
|
|
176
|
-
|
|
209
|
+
|
|
177
210
|
# Deleting specific references
|
|
178
211
|
result = handler._manage_references("JIRA-100,JIRA-200,JIRA-300", "JIRA-200", "delete")
|
|
179
212
|
assert result == "JIRA-100,JIRA-300"
|
|
180
|
-
|
|
213
|
+
|
|
181
214
|
# Deleting multiple specific references
|
|
182
215
|
result = handler._manage_references("JIRA-100,JIRA-200,JIRA-300,JIRA-400", "JIRA-200,JIRA-400", "delete")
|
|
183
216
|
assert result == "JIRA-100,JIRA-300"
|
|
184
|
-
|
|
217
|
+
|
|
185
218
|
# Deleting all references (empty new_refs)
|
|
186
219
|
result = handler._manage_references("JIRA-100,JIRA-200", "", "delete")
|
|
187
220
|
assert result == ""
|
|
188
|
-
|
|
221
|
+
|
|
189
222
|
# Deleting non-existent references
|
|
190
223
|
result = handler._manage_references("JIRA-100,JIRA-200", "JIRA-999", "delete")
|
|
191
224
|
assert result == "JIRA-100,JIRA-200"
|
|
@@ -146,6 +146,37 @@ class TestGlobIntegration:
|
|
|
146
146
|
if merged_file.exists():
|
|
147
147
|
merged_file.unlink()
|
|
148
148
|
|
|
149
|
+
@pytest.mark.parse_robot
|
|
150
|
+
def test_glob_robot_duplicate_automation_ids(self):
|
|
151
|
+
"""Test Robot Framework glob pattern with duplicate automation_ids."""
|
|
152
|
+
env = Environment()
|
|
153
|
+
env.case_matcher = MatchersParser.AUTO
|
|
154
|
+
env.file = Path(__file__).parent / "test_data/XML/testglob_robot/*.xml"
|
|
155
|
+
|
|
156
|
+
# Check if test files exist
|
|
157
|
+
if not list(Path(__file__).parent.glob("test_data/XML/testglob_robot/*.xml")):
|
|
158
|
+
pytest.skip("Robot test data not available")
|
|
159
|
+
|
|
160
|
+
parser = RobotParser(env)
|
|
161
|
+
parsed_suites = parser.parse_file()
|
|
162
|
+
suite = parsed_suites[0]
|
|
163
|
+
|
|
164
|
+
# Similar verification as JUnit tests
|
|
165
|
+
data_provider = ApiDataProvider(suite)
|
|
166
|
+
cases_to_add = data_provider.add_cases()
|
|
167
|
+
|
|
168
|
+
# Verify deduplication occurred if there were duplicates
|
|
169
|
+
total_cases = sum(len(section.testcases) for section in suite.testsections)
|
|
170
|
+
automation_ids = [c.custom_automation_id for c in cases_to_add if c.custom_automation_id]
|
|
171
|
+
|
|
172
|
+
# Cases to add should have unique automation_ids
|
|
173
|
+
assert len(automation_ids) == len(set(automation_ids)), "Cases to add should have unique automation_ids"
|
|
174
|
+
|
|
175
|
+
# Clean up merged file
|
|
176
|
+
merged_file = Path.cwd() / "Merged-Robot-report.xml"
|
|
177
|
+
if merged_file.exists():
|
|
178
|
+
merged_file.unlink()
|
|
179
|
+
|
|
149
180
|
@pytest.mark.parse_cucumber
|
|
150
181
|
def test_cucumber_glob_filepath_not_pattern(self):
|
|
151
182
|
"""Test Scenario 3: Cucumber glob pattern uses correct filepath (not pattern string).
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import asdict
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from deepdiff import DeepDiff
|
|
8
|
+
|
|
9
|
+
from trcli.cli import Environment
|
|
10
|
+
from trcli.data_classes.data_parsers import MatchersParser
|
|
11
|
+
from trcli.data_classes.dataclass_testrail import TestRailSuite
|
|
12
|
+
from trcli.readers.robot_xml import RobotParser
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestRobotParser:
|
|
16
|
+
|
|
17
|
+
@pytest.mark.parse_robot
|
|
18
|
+
@pytest.mark.parametrize(
|
|
19
|
+
"matcher, input_xml_path, expected_path",
|
|
20
|
+
[
|
|
21
|
+
# RF 5.0 format
|
|
22
|
+
(
|
|
23
|
+
MatchersParser.AUTO,
|
|
24
|
+
Path(__file__).parent / "test_data/XML/robotframework_simple_RF50.xml",
|
|
25
|
+
Path(__file__).parent / "test_data/json/robotframework_simple_RF50.json",
|
|
26
|
+
),
|
|
27
|
+
(
|
|
28
|
+
MatchersParser.NAME,
|
|
29
|
+
Path(__file__).parent / "test_data/XML/robotframework_id_in_name_RF50.xml",
|
|
30
|
+
Path(__file__).parent / "test_data/json/robotframework_id_in_name_RF50.json",
|
|
31
|
+
),
|
|
32
|
+
# RF 7.0 format
|
|
33
|
+
(
|
|
34
|
+
MatchersParser.AUTO,
|
|
35
|
+
Path(__file__).parent / "test_data/XML/robotframework_simple_RF70.xml",
|
|
36
|
+
Path(__file__).parent / "test_data/json/robotframework_simple_RF70.json",
|
|
37
|
+
),
|
|
38
|
+
(
|
|
39
|
+
MatchersParser.NAME,
|
|
40
|
+
Path(__file__).parent / "test_data/XML/robotframework_id_in_name_RF70.xml",
|
|
41
|
+
Path(__file__).parent / "test_data/json/robotframework_id_in_name_RF70.json",
|
|
42
|
+
),
|
|
43
|
+
],
|
|
44
|
+
ids=["Case Matcher Auto", "Case Matcher Name", "Case Matcher Auto", "Case Matcher Name"],
|
|
45
|
+
)
|
|
46
|
+
@pytest.mark.parse_robot
|
|
47
|
+
def test_robot_xml_parser_id_matcher_name(
|
|
48
|
+
self, matcher: str, input_xml_path: Union[str, Path], expected_path: str, freezer
|
|
49
|
+
):
|
|
50
|
+
freezer.move_to("2020-05-20 01:00:00")
|
|
51
|
+
env = Environment()
|
|
52
|
+
env.case_matcher = matcher
|
|
53
|
+
env.file = input_xml_path
|
|
54
|
+
file_reader = RobotParser(env)
|
|
55
|
+
read_junit = self.__clear_unparsable_junit_elements(file_reader.parse_file()[0])
|
|
56
|
+
parsing_result_json = asdict(read_junit)
|
|
57
|
+
file_json = open(expected_path)
|
|
58
|
+
expected_json = json.load(file_json)
|
|
59
|
+
assert (
|
|
60
|
+
DeepDiff(parsing_result_json, expected_json) == {}
|
|
61
|
+
), f"Result of parsing XML is different than expected \n{DeepDiff(parsing_result_json, expected_json)}"
|
|
62
|
+
|
|
63
|
+
def __clear_unparsable_junit_elements(self, test_rail_suite: TestRailSuite) -> TestRailSuite:
|
|
64
|
+
"""helper method to delete temporary junit_case_refs attribute,
|
|
65
|
+
which asdict() method of dataclass can't handle"""
|
|
66
|
+
for section in test_rail_suite.testsections:
|
|
67
|
+
for case in section.testcases:
|
|
68
|
+
# Remove temporary junit_case_refs attribute if it exists
|
|
69
|
+
if hasattr(case, "_junit_case_refs"):
|
|
70
|
+
delattr(case, "_junit_case_refs")
|
|
71
|
+
return test_rail_suite
|
|
72
|
+
|
|
73
|
+
@pytest.mark.parse_robot
|
|
74
|
+
def test_robot_xml_parser_file_not_found(self):
|
|
75
|
+
with pytest.raises(FileNotFoundError):
|
|
76
|
+
env = Environment()
|
|
77
|
+
env.file = Path(__file__).parent / "not_found.xml"
|
|
78
|
+
RobotParser(env)
|
|
79
|
+
|
|
80
|
+
@pytest.mark.parse_robot
|
|
81
|
+
def test_robot_xml_parser_glob_pattern_single_file(self):
|
|
82
|
+
"""Test glob pattern that matches single file"""
|
|
83
|
+
env = Environment()
|
|
84
|
+
env.case_matcher = MatchersParser.AUTO
|
|
85
|
+
# Use glob pattern that matches only one file
|
|
86
|
+
env.file = Path(__file__).parent / "test_data/XML/robotframework_simple_RF50.xml"
|
|
87
|
+
|
|
88
|
+
# This should work just like a regular file path
|
|
89
|
+
file_reader = RobotParser(env)
|
|
90
|
+
result = file_reader.parse_file()
|
|
91
|
+
|
|
92
|
+
assert len(result) == 1
|
|
93
|
+
assert isinstance(result[0], TestRailSuite)
|
|
94
|
+
# Verify it has test sections and cases
|
|
95
|
+
assert len(result[0].testsections) > 0
|
|
96
|
+
|
|
97
|
+
@pytest.mark.parse_robot
|
|
98
|
+
def test_robot_xml_parser_glob_pattern_multiple_files(self):
|
|
99
|
+
"""Test glob pattern that matches multiple files and merges them"""
|
|
100
|
+
env = Environment()
|
|
101
|
+
env.case_matcher = MatchersParser.AUTO
|
|
102
|
+
# Use glob pattern that matches multiple Robot XML files
|
|
103
|
+
env.file = Path(__file__).parent / "test_data/XML/testglob_robot/*.xml"
|
|
104
|
+
|
|
105
|
+
file_reader = RobotParser(env)
|
|
106
|
+
result = file_reader.parse_file()
|
|
107
|
+
|
|
108
|
+
# Should return a merged result
|
|
109
|
+
assert len(result) == 1
|
|
110
|
+
assert isinstance(result[0], TestRailSuite)
|
|
111
|
+
|
|
112
|
+
# Verify merged file was created
|
|
113
|
+
merged_file = Path.cwd() / "Merged-Robot-report.xml"
|
|
114
|
+
assert merged_file.exists(), "Merged Robot report should be created"
|
|
115
|
+
|
|
116
|
+
# Verify the merged result contains test cases from both files
|
|
117
|
+
total_cases = sum(len(section.testcases) for section in result[0].testsections)
|
|
118
|
+
assert total_cases > 0, "Merged result should contain test cases"
|
|
119
|
+
|
|
120
|
+
# Clean up merged file
|
|
121
|
+
if merged_file.exists():
|
|
122
|
+
merged_file.unlink()
|
|
123
|
+
|
|
124
|
+
@pytest.mark.parse_robot
|
|
125
|
+
def test_robot_xml_parser_glob_pattern_no_matches(self):
|
|
126
|
+
"""Test glob pattern that matches no files"""
|
|
127
|
+
with pytest.raises(FileNotFoundError):
|
|
128
|
+
env = Environment()
|
|
129
|
+
env.case_matcher = MatchersParser.AUTO
|
|
130
|
+
# Use glob pattern that matches no files
|
|
131
|
+
env.file = Path(__file__).parent / "test_data/XML/nonexistent_*.xml"
|
|
132
|
+
RobotParser(env)
|
|
133
|
+
|
|
134
|
+
@pytest.mark.parse_robot
|
|
135
|
+
def test_robot_check_file_glob_returns_path(self):
|
|
136
|
+
"""Test that check_file method returns valid Path for glob pattern"""
|
|
137
|
+
# Test single file match
|
|
138
|
+
single_file_glob = Path(__file__).parent / "test_data/XML/robotframework_simple_RF50.xml"
|
|
139
|
+
result = RobotParser.check_file(single_file_glob)
|
|
140
|
+
assert isinstance(result, Path)
|
|
141
|
+
assert result.exists()
|
|
142
|
+
|
|
143
|
+
# Test multiple file match (returns merged file path)
|
|
144
|
+
multi_file_glob = Path(__file__).parent / "test_data/XML/testglob_robot/*.xml"
|
|
145
|
+
result = RobotParser.check_file(multi_file_glob)
|
|
146
|
+
assert isinstance(result, Path)
|
|
147
|
+
assert result.name == "Merged-Robot-report.xml"
|
|
148
|
+
assert result.exists()
|
|
149
|
+
|
|
150
|
+
# Clean up
|
|
151
|
+
if result.exists() and result.name == "Merged-Robot-report.xml":
|
|
152
|
+
result.unlink()
|
|
153
|
+
|
|
154
|
+
@pytest.mark.parse_robot
|
|
155
|
+
def test_robot_xml_parser_glob_merges_duplicate_sections(self):
|
|
156
|
+
"""Test that glob pattern merging handles duplicate section names correctly.
|
|
157
|
+
|
|
158
|
+
When multiple Robot XML files have the same suite structure, sections with
|
|
159
|
+
the same name should be merged into one section with all test cases combined.
|
|
160
|
+
This prevents the "Section duplicates detected" error.
|
|
161
|
+
"""
|
|
162
|
+
env = Environment()
|
|
163
|
+
env.case_matcher = MatchersParser.AUTO
|
|
164
|
+
env.file = Path(__file__).parent / "test_data/XML/testglob_robot/*.xml"
|
|
165
|
+
|
|
166
|
+
file_reader = RobotParser(env)
|
|
167
|
+
result = file_reader.parse_file()
|
|
168
|
+
|
|
169
|
+
assert len(result) == 1
|
|
170
|
+
suite = result[0]
|
|
171
|
+
|
|
172
|
+
# Verify no duplicate section names
|
|
173
|
+
section_names = [section.name for section in suite.testsections]
|
|
174
|
+
unique_section_names = set(section_names)
|
|
175
|
+
|
|
176
|
+
assert len(section_names) == len(unique_section_names), f"Duplicate section names detected: {section_names}"
|
|
177
|
+
|
|
178
|
+
# Verify sections have combined test cases from both files
|
|
179
|
+
# Both robot-1.xml and robot-2.xml have same structure, so sections should have tests from both
|
|
180
|
+
total_cases = sum(len(section.testcases) for section in suite.testsections)
|
|
181
|
+
assert total_cases > 4, "Sections should contain test cases from both merged files"
|
|
182
|
+
|
|
183
|
+
# Clean up merged file
|
|
184
|
+
merged_file = Path.cwd() / "Merged-Robot-report.xml"
|
|
185
|
+
if merged_file.exists():
|
|
186
|
+
merged_file.unlink()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.13.4"
|
|
@@ -256,8 +256,11 @@ class ApiRequestHandler:
|
|
|
256
256
|
milestone_id: int = None,
|
|
257
257
|
refs: str = None,
|
|
258
258
|
refs_action: str = "add",
|
|
259
|
+
assigned_to_id: Union[int, None] = ...,
|
|
259
260
|
) -> Tuple[dict, str]:
|
|
260
|
-
return self.run_handler.update_run(
|
|
261
|
+
return self.run_handler.update_run(
|
|
262
|
+
run_id, run_name, start_date, end_date, milestone_id, refs, refs_action, assigned_to_id
|
|
263
|
+
)
|
|
261
264
|
|
|
262
265
|
def _manage_references(self, existing_refs: str, new_refs: str, action: str) -> str:
|
|
263
266
|
return self.run_handler._manage_references(existing_refs, new_refs, action)
|
|
@@ -213,6 +213,15 @@ class ProjectBasedClient:
|
|
|
213
213
|
else:
|
|
214
214
|
self.environment.log(f"Updating test run. ", new_line=False)
|
|
215
215
|
run_id = self.environment.run_id
|
|
216
|
+
|
|
217
|
+
# Determine assigned_to_id value based on flags
|
|
218
|
+
if hasattr(self.environment, "clear_run_assigned_to_id") and self.environment.clear_run_assigned_to_id:
|
|
219
|
+
assigned_to_id = None # Clear the assignee
|
|
220
|
+
elif self.environment.run_assigned_to_id:
|
|
221
|
+
assigned_to_id = self.environment.run_assigned_to_id # Set new assignee
|
|
222
|
+
else:
|
|
223
|
+
assigned_to_id = ... # Don't change (sentinel value)
|
|
224
|
+
|
|
216
225
|
run, error_message = self.api_request_handler.update_run(
|
|
217
226
|
run_id,
|
|
218
227
|
self.run_name,
|
|
@@ -221,6 +230,7 @@ class ProjectBasedClient:
|
|
|
221
230
|
milestone_id=self.environment.milestone_id,
|
|
222
231
|
refs=self.environment.run_refs,
|
|
223
232
|
refs_action=getattr(self.environment, "run_refs_action", "add"),
|
|
233
|
+
assigned_to_id=assigned_to_id,
|
|
224
234
|
)
|
|
225
235
|
if self.environment.auto_close_run:
|
|
226
236
|
self.environment.log("Closing run. ", new_line=False)
|
|
@@ -8,7 +8,7 @@ It manages all test run operations including:
|
|
|
8
8
|
- Closing and deleting runs
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
from beartype.typing import List, Tuple, Dict
|
|
11
|
+
from beartype.typing import List, Tuple, Dict, Union
|
|
12
12
|
|
|
13
13
|
from trcli.api.api_client import APIClient
|
|
14
14
|
from trcli.api.api_utils import (
|
|
@@ -89,8 +89,14 @@ class RunHandler:
|
|
|
89
89
|
)
|
|
90
90
|
|
|
91
91
|
# Validate that we have test cases to include in the run
|
|
92
|
-
# Empty runs are not allowed unless include_all is True
|
|
93
|
-
|
|
92
|
+
# Empty runs are not allowed for parse commands unless include_all is True
|
|
93
|
+
# However, add_run command explicitly allows empty runs for later result uploads
|
|
94
|
+
is_add_run_command = self.environment.cmd == "add_run"
|
|
95
|
+
if (
|
|
96
|
+
not is_add_run_command
|
|
97
|
+
and not include_all
|
|
98
|
+
and (not add_run_data.get("case_ids") or len(add_run_data["case_ids"]) == 0)
|
|
99
|
+
):
|
|
94
100
|
error_msg = (
|
|
95
101
|
"Cannot create test run: No test cases were matched.\n"
|
|
96
102
|
" - For parse_junit: Ensure tests have automation_id/test ids that matches existing cases in TestRail\n"
|
|
@@ -129,6 +135,7 @@ class RunHandler:
|
|
|
129
135
|
milestone_id: int = None,
|
|
130
136
|
refs: str = None,
|
|
131
137
|
refs_action: str = "add",
|
|
138
|
+
assigned_to_id: Union[int, None] = ...,
|
|
132
139
|
) -> Tuple[dict, str]:
|
|
133
140
|
"""
|
|
134
141
|
Updates an existing run
|
|
@@ -136,10 +143,11 @@ class RunHandler:
|
|
|
136
143
|
:param run_id: run id
|
|
137
144
|
:param run_name: run name
|
|
138
145
|
:param start_date: start date
|
|
139
|
-
:param end_date: end date
|
|
146
|
+
:param end_date: end_date: end date
|
|
140
147
|
:param milestone_id: milestone id
|
|
141
148
|
:param refs: references to manage
|
|
142
149
|
:param refs_action: action to perform ('add', 'update', 'delete')
|
|
150
|
+
:param assigned_to_id: user ID to assign (int), None to clear, or ... to leave unchanged
|
|
143
151
|
:returns: Tuple with run and error string.
|
|
144
152
|
"""
|
|
145
153
|
run_response = self.client.send_get(f"get_run/{run_id}")
|
|
@@ -161,6 +169,11 @@ class RunHandler:
|
|
|
161
169
|
else:
|
|
162
170
|
add_run_data["refs"] = existing_refs # Keep existing refs if none provided
|
|
163
171
|
|
|
172
|
+
# Handle assigned_to_id - only add to payload if explicitly provided
|
|
173
|
+
if assigned_to_id is not ...:
|
|
174
|
+
add_run_data["assignedto_id"] = assigned_to_id # Can be None (clears) or int (sets)
|
|
175
|
+
# else: Don't include assignedto_id in payload (no change to existing assignee)
|
|
176
|
+
|
|
164
177
|
existing_include_all = run_response.response_text.get("include_all", False)
|
|
165
178
|
add_run_data["include_all"] = existing_include_all
|
|
166
179
|
|