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.
Files changed (95) hide show
  1. {trcli-1.13.3 → trcli-1.13.4}/PKG-INFO +1 -1
  2. {trcli-1.13.3 → trcli-1.13.4}/README.md +58 -5
  3. {trcli-1.13.3 → trcli-1.13.4}/tests/test_cmd_add_run.py +71 -38
  4. {trcli-1.13.3 → trcli-1.13.4}/tests/test_glob_integration.py +31 -0
  5. trcli-1.13.4/tests/test_robot_parser.py +186 -0
  6. trcli-1.13.4/trcli/__init__.py +1 -0
  7. {trcli-1.13.3 → trcli-1.13.4}/trcli/api/api_request_handler.py +4 -1
  8. {trcli-1.13.3 → trcli-1.13.4}/trcli/api/project_based_client.py +10 -0
  9. {trcli-1.13.3 → trcli-1.13.4}/trcli/api/run_handler.py +17 -4
  10. {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_add_run.py +58 -46
  11. {trcli-1.13.3 → trcli-1.13.4}/trcli/readers/robot_xml.py +59 -3
  12. {trcli-1.13.3 → trcli-1.13.4}/trcli.egg-info/PKG-INFO +1 -1
  13. trcli-1.13.3/tests/test_robot_parser.py +0 -78
  14. trcli-1.13.3/trcli/__init__.py +0 -1
  15. {trcli-1.13.3 → trcli-1.13.4}/LICENSE.md +0 -0
  16. {trcli-1.13.3 → trcli-1.13.4}/setup.cfg +0 -0
  17. {trcli-1.13.3 → trcli-1.13.4}/setup.py +0 -0
  18. {trcli-1.13.3 → trcli-1.13.4}/tests/test_api_client.py +0 -0
  19. {trcli-1.13.3 → trcli-1.13.4}/tests/test_api_client_proxy.py +0 -0
  20. {trcli-1.13.3 → trcli-1.13.4}/tests/test_api_data_provider.py +0 -0
  21. {trcli-1.13.3 → trcli-1.13.4}/tests/test_api_request_handler.py +0 -0
  22. {trcli-1.13.3 → trcli-1.13.4}/tests/test_api_request_handler_case_fields_update.py +0 -0
  23. {trcli-1.13.3 → trcli-1.13.4}/tests/test_api_request_handler_case_matcher.py +0 -0
  24. {trcli-1.13.3 → trcli-1.13.4}/tests/test_api_request_handler_labels.py +0 -0
  25. {trcli-1.13.3 → trcli-1.13.4}/tests/test_api_request_handler_references.py +0 -0
  26. {trcli-1.13.3 → trcli-1.13.4}/tests/test_cli.py +0 -0
  27. {trcli-1.13.3 → trcli-1.13.4}/tests/test_cmd_export_gherkin.py +0 -0
  28. {trcli-1.13.3 → trcli-1.13.4}/tests/test_cmd_import_gherkin.py +0 -0
  29. {trcli-1.13.3 → trcli-1.13.4}/tests/test_cmd_labels.py +0 -0
  30. {trcli-1.13.3 → trcli-1.13.4}/tests/test_cmd_parse_cucumber.py +0 -0
  31. {trcli-1.13.3 → trcli-1.13.4}/tests/test_cmd_references.py +0 -0
  32. {trcli-1.13.3 → trcli-1.13.4}/tests/test_cmd_update.py +0 -0
  33. {trcli-1.13.3 → trcli-1.13.4}/tests/test_cucumber_bdd_matching.py +0 -0
  34. {trcli-1.13.3 → trcli-1.13.4}/tests/test_cucumber_parser.py +0 -0
  35. {trcli-1.13.3 → trcli-1.13.4}/tests/test_dataclass_creation.py +0 -0
  36. {trcli-1.13.3 → trcli-1.13.4}/tests/test_glob_deduplication.py +0 -0
  37. {trcli-1.13.3 → trcli-1.13.4}/tests/test_junit_bdd_parser.py +0 -0
  38. {trcli-1.13.3 → trcli-1.13.4}/tests/test_junit_parse_reference.py +0 -0
  39. {trcli-1.13.3 → trcli-1.13.4}/tests/test_junit_parser.py +0 -0
  40. {trcli-1.13.3 → trcli-1.13.4}/tests/test_load_data_from_config.py +0 -0
  41. {trcli-1.13.3 → trcli-1.13.4}/tests/test_matchers_parser.py +0 -0
  42. {trcli-1.13.3 → trcli-1.13.4}/tests/test_multiple_case_ids.py +0 -0
  43. {trcli-1.13.3 → trcli-1.13.4}/tests/test_project_based_client.py +0 -0
  44. {trcli-1.13.3 → trcli-1.13.4}/tests/test_response_verify.py +0 -0
  45. {trcli-1.13.3 → trcli-1.13.4}/tests/test_results_uploader.py +0 -0
  46. {trcli-1.13.3 → trcli-1.13.4}/tests/test_version_checker.py +0 -0
  47. {trcli-1.13.3 → trcli-1.13.4}/trcli/api/__init__.py +0 -0
  48. {trcli-1.13.3 → trcli-1.13.4}/trcli/api/api_cache.py +0 -0
  49. {trcli-1.13.3 → trcli-1.13.4}/trcli/api/api_client.py +0 -0
  50. {trcli-1.13.3 → trcli-1.13.4}/trcli/api/api_response_verify.py +0 -0
  51. {trcli-1.13.3 → trcli-1.13.4}/trcli/api/api_utils.py +0 -0
  52. {trcli-1.13.3 → trcli-1.13.4}/trcli/api/bdd_handler.py +0 -0
  53. {trcli-1.13.3 → trcli-1.13.4}/trcli/api/case_handler.py +0 -0
  54. {trcli-1.13.3 → trcli-1.13.4}/trcli/api/case_matcher.py +0 -0
  55. {trcli-1.13.3 → trcli-1.13.4}/trcli/api/label_manager.py +0 -0
  56. {trcli-1.13.3 → trcli-1.13.4}/trcli/api/reference_manager.py +0 -0
  57. {trcli-1.13.3 → trcli-1.13.4}/trcli/api/result_handler.py +0 -0
  58. {trcli-1.13.3 → trcli-1.13.4}/trcli/api/results_uploader.py +0 -0
  59. {trcli-1.13.3 → trcli-1.13.4}/trcli/api/section_handler.py +0 -0
  60. {trcli-1.13.3 → trcli-1.13.4}/trcli/api/suite_handler.py +0 -0
  61. {trcli-1.13.3 → trcli-1.13.4}/trcli/backports.py +0 -0
  62. {trcli-1.13.3 → trcli-1.13.4}/trcli/cli.py +0 -0
  63. {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/__init__.py +0 -0
  64. {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_export_gherkin.py +0 -0
  65. {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_import_gherkin.py +0 -0
  66. {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_labels.py +0 -0
  67. {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_parse_cucumber.py +0 -0
  68. {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_parse_junit.py +0 -0
  69. {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_parse_openapi.py +0 -0
  70. {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_parse_robot.py +0 -0
  71. {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_references.py +0 -0
  72. {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/cmd_update.py +0 -0
  73. {trcli-1.13.3 → trcli-1.13.4}/trcli/commands/results_parser_helpers.py +0 -0
  74. {trcli-1.13.3 → trcli-1.13.4}/trcli/constants.py +0 -0
  75. {trcli-1.13.3 → trcli-1.13.4}/trcli/data_classes/__init__.py +0 -0
  76. {trcli-1.13.3 → trcli-1.13.4}/trcli/data_classes/data_parsers.py +0 -0
  77. {trcli-1.13.3 → trcli-1.13.4}/trcli/data_classes/dataclass_testrail.py +0 -0
  78. {trcli-1.13.3 → trcli-1.13.4}/trcli/data_classes/validation_exception.py +0 -0
  79. {trcli-1.13.3 → trcli-1.13.4}/trcli/data_providers/api_data_provider.py +0 -0
  80. {trcli-1.13.3 → trcli-1.13.4}/trcli/logging/__init__.py +0 -0
  81. {trcli-1.13.3 → trcli-1.13.4}/trcli/logging/config.py +0 -0
  82. {trcli-1.13.3 → trcli-1.13.4}/trcli/logging/file_handler.py +0 -0
  83. {trcli-1.13.3 → trcli-1.13.4}/trcli/logging/structured_logger.py +0 -0
  84. {trcli-1.13.3 → trcli-1.13.4}/trcli/readers/__init__.py +0 -0
  85. {trcli-1.13.3 → trcli-1.13.4}/trcli/readers/cucumber_json.py +0 -0
  86. {trcli-1.13.3 → trcli-1.13.4}/trcli/readers/file_parser.py +0 -0
  87. {trcli-1.13.3 → trcli-1.13.4}/trcli/readers/junit_xml.py +0 -0
  88. {trcli-1.13.3 → trcli-1.13.4}/trcli/readers/openapi_yml.py +0 -0
  89. {trcli-1.13.3 → trcli-1.13.4}/trcli/settings.py +0 -0
  90. {trcli-1.13.3 → trcli-1.13.4}/trcli/version_checker.py +0 -0
  91. {trcli-1.13.3 → trcli-1.13.4}/trcli.egg-info/SOURCES.txt +0 -0
  92. {trcli-1.13.3 → trcli-1.13.4}/trcli.egg-info/dependency_links.txt +0 -0
  93. {trcli-1.13.3 → trcli-1.13.4}/trcli.egg-info/entry_points.txt +0 -0
  94. {trcli-1.13.3 → trcli-1.13.4}/trcli.egg-info/requires.txt +0 -0
  95. {trcli-1.13.3 → trcli-1.13.4}/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.13.4
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.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.3
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.3
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.3
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 = (f"run_assigned_to_id: {assigned_to_id}\nrun_case_ids: '{case_ids}'\n"
46
- f"run_description: {description}\nrun_id: {run_id}\n"
47
- f"run_include_all: true\nrun_refs: {refs}\ntitle: {title}\n")
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(cmd_add_run.cli, [
65
- '--title', 'Test Run',
66
- '--run-refs', long_refs
67
- ], catch_exceptions=False)
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
- '--title', 'Test Run',
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, ['--help'])
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(run_id, run_name, start_date, end_date, milestone_id, refs, refs_action)
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
- if not include_all and (not add_run_data.get("case_ids") or len(add_run_data["case_ids"]) == 0):
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