Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ This project adheres to [Semantic Versioning](https://semver.org/). Version numb
- **MINOR**: New features that are backward-compatible.
- **PATCH**: Bug fixes or minor changes that do not affect backward compatibility.

## [1.13.4]

_released 03-18-2026

### Added
- Extended glob support for robot parser for efficient multiple test result file processing
- Added `--run-assigned-to-id` and `--clear-run-assigned-to-id` options to `add_run` command for clearing and setting test run assignees during run creation and updates

### Fixed
- Cannot add empty runs via add_run command due to empty test case validation.

## [1.13.3]

_released 03-05-2026
Expand Down
63 changes: 58 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ trcli
```
You should get something like this:
```
TestRail CLI v1.13.3
TestRail CLI v1.13.4
Copyright 2025 Gurock Software GmbH - www.gurock.com
Supported and loaded modules:
- parse_junit: JUnit XML Files (& Similar)
Expand All @@ -51,7 +51,7 @@ CLI general reference
--------
```shell
$ trcli --help
TestRail CLI v1.13.3
TestRail CLI v1.13.4
Copyright 2025 Gurock Software GmbH - www.gurock.com
Usage: trcli [OPTIONS] COMMAND [ARGS]...

Expand Down Expand Up @@ -196,7 +196,7 @@ For further detail, please refer to the

### Using Glob Patterns for Multiple Files

TRCLI supports glob patterns to process multiple report files in a single command. This feature is available for **JUnit XML** and **Cucumber JSON** parsers.
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.

#### Important: Shell Quoting Requirement

Expand Down Expand Up @@ -239,6 +239,7 @@ When a glob pattern matches **multiple files**, TRCLI automatically:
3. **Merges test results** into a single combined report
4. **Writes merged file** to current directory:
- JUnit: `Merged-JUnit-report.xml`
- Robot Framework: `Merged-Robot-report.xml`
- Cucumber: `merged_cucumber.json`
5. **Processes the merged file** as a single test run upload

Expand All @@ -263,6 +264,23 @@ trcli parse_junit \
--case-matcher auto
```

**Robot Framework - Multiple output files:**
```bash
# Merge multiple Robot Framework test runs
trcli -y \
-h https://example.testrail.com \
--project "My Project" \
parse_robot \
-f "reports/robot-*.xml" \
--title "Merged Robot Tests"

# Recursive search for all Robot outputs
trcli parse_robot \
-f "test-results/**/output.xml" \
--title "All Robot Results" \
--case-matcher property
```

**Cucumber JSON - Multiple test runs:**
```bash
# Merge multiple Cucumber JSON reports
Expand Down Expand Up @@ -1623,7 +1641,7 @@ Options:
### Reference
```shell
$ trcli add_run --help
TestRail CLI v1.13.3
TestRail CLI v1.13.4
Copyright 2025 Gurock Software GmbH - www.gurock.com
Usage: trcli add_run [OPTIONS]

Expand All @@ -1640,6 +1658,8 @@ Options:
--run-end-date The expected or scheduled end date of this test run in MM/DD/YYYY format
--run-assigned-to-id The ID of the user the test run should be assigned
to. [x>=1]
--clear-run-assigned-to-id Clear the assignee of the test run (only valid
when updating with --run-id).
--run-include-all Use this option to include all test cases in this test run.
--auto-close-run Use this option to automatically close the created run.
--run-case-ids Comma separated list of test case IDs to include in
Expand Down Expand Up @@ -1712,6 +1732,39 @@ trcli -y -h https://example.testrail.io/ --project "My Project" \
- **Action Requirements**: `update` and `delete` actions require an existing run (--run-id must be provided)
- **Validation**: Invalid reference formats are rejected with clear error messages

### Managing Assignees in Test Runs

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.

#### Assigning Runs to Users

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:

```bash
# Create a new run and assign to user ID 5
trcli -y -h https://example.testrail.io/ --project "My Project" \
add_run --title "My Test Run" --run-assigned-to-id 5

# Update an existing run and change the assignee
trcli -y -h https://example.testrail.io/ --project "My Project" \
add_run --run-id 123 --title "My Test Run" --run-assigned-to-id 10
```

#### Clearing Assignees from Test Runs

To remove the assignee from an existing test run, use the `--clear-run-assigned-to-id` flag:

```bash
# Clear the assignee from an existing run
trcli -y -h https://example.testrail.io/ --project "My Project" \
add_run --run-id 123 --title "My Test Run" --clear-run-assigned-to-id
```

#### Assignee Management Rules

- **Update Mode Only**: The `--clear-run-assigned-to-id` flag can only be used when updating an existing run (requires `--run-id`)
- **Mutually Exclusive**: You cannot use both `--run-assigned-to-id` and `--clear-run-assigned-to-id` in the same command

#### Examples

**Complete Workflow Example:**
Expand Down Expand Up @@ -1747,7 +1800,7 @@ providing you with a solid base of test cases, which you can further expand on T
### Reference
```shell
$ trcli parse_openapi --help
TestRail CLI v1.13.3
TestRail CLI v1.13.4
Copyright 2025 Gurock Software GmbH - www.gurock.com
Usage: trcli parse_openapi [OPTIONS]

Expand Down
109 changes: 71 additions & 38 deletions tests/test_cmd_add_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,102 +42,135 @@ def test_write_run_to_file_with_refs_and_description(self, mock_open_file):
environment.run_assigned_to_id = assigned_to_id
environment.run_case_ids = case_ids
environment.run_include_all = True
expected_string = (f"run_assigned_to_id: {assigned_to_id}\nrun_case_ids: '{case_ids}'\n"
f"run_description: {description}\nrun_id: {run_id}\n"
f"run_include_all: true\nrun_refs: {refs}\ntitle: {title}\n")
expected_string = (
f"run_assigned_to_id: {assigned_to_id}\nrun_case_ids: '{case_ids}'\n"
f"run_description: {description}\nrun_id: {run_id}\n"
f"run_include_all: true\nrun_refs: {refs}\ntitle: {title}\n"
)
cmd_add_run.write_run_to_file(environment, run_id)
mock_open_file.assert_called_with(file, "a")
mock_open_file.return_value.__enter__().write.assert_called_once_with(expected_string)

def test_cli_validation_refs_too_long(self):
"""Test that references validation fails when exceeding 250 characters"""
from trcli.cli import Environment

environment = Environment()
environment.run_refs = "A" * 251 # 251 characters, exceeds limit

assert len(environment.run_refs) > 250

runner = CliRunner()
long_refs = "A" * 251

result = runner.invoke(cmd_add_run.cli, [
'--title', 'Test Run',
'--run-refs', long_refs
], catch_exceptions=False)


result = runner.invoke(
cmd_add_run.cli, ["--title", "Test Run", "--run-refs", long_refs], catch_exceptions=False
)

# Should exit with error code 1 due to missing required parameters or validation
assert result.exit_code == 1

def test_cli_validation_refs_exactly_250_chars(self):
"""Test that references validation passes with exactly 250 characters"""
from trcli.cli import Environment

runner = CliRunner()
refs_250 = "A" * 250 # Exactly 250 characters, should pass validation

result = runner.invoke(cmd_add_run.cli, [
'--title', 'Test Run',
'--run-refs', refs_250
], catch_exceptions=False)


result = runner.invoke(cmd_add_run.cli, ["--title", "Test Run", "--run-refs", refs_250], catch_exceptions=False)

# Should not fail due to refs validation (will fail due to missing required parameters)
# But the important thing is that it doesn't fail with the character limit error
assert "References field cannot exceed 250 characters" not in result.output

def test_validation_logic_refs_action_without_run_id(self):
"""Test validation logic for refs action without run_id"""
from trcli.cli import Environment

# Update action validation
environment = Environment()
environment.run_refs_action = "update"
environment.run_id = None
environment.run_refs = "JIRA-123"

# This should be invalid
assert environment.run_refs_action == "update"
assert environment.run_id is None
# Delete action validation

# Delete action validation
environment.run_refs_action = "delete"
assert environment.run_refs_action == "delete"
assert environment.run_id is None

def test_refs_action_parameter_parsing(self):
"""Test that refs action parameter is parsed correctly"""
runner = CliRunner()

# Test that the CLI accepts new param without crashing! :) - acuanico
result = runner.invoke(cmd_add_run.cli, ['--help'])
result = runner.invoke(cmd_add_run.cli, ["--help"])
assert result.exit_code == 0
assert "--run-refs-action" in result.output
assert "Action to perform on references" in result.output

def test_clear_assigned_to_id_parameter_exists(self):
"""Test that --clear-run-assigned-to-id parameter is available"""
runner = CliRunner()

result = runner.invoke(cmd_add_run.cli, ["--help"])
assert result.exit_code == 0
assert "--clear-run-assigned-to-id" in result.output
assert "Clear the assignee" in result.output

@mock.patch("trcli.cli.Environment.check_for_required_parameters")
def test_clear_assigned_to_id_requires_run_id(self, mock_check):
"""Test that --clear-run-assigned-to-id requires --run-id"""
runner = CliRunner()

result = runner.invoke(cmd_add_run.cli, ["--title", "Test Run", "--clear-run-assigned-to-id"])

assert result.exit_code == 1
assert (
"--clear-run-assigned-to-id can only be used when updating" in result.output
or "--run-id required" in result.output
)

@mock.patch("trcli.cli.Environment.check_for_required_parameters")
def test_clear_assigned_to_id_mutually_exclusive_with_assigned_to_id(self, mock_check):
"""Test that --clear-run-assigned-to-id and --run-assigned-to-id are mutually exclusive"""
runner = CliRunner()

result = runner.invoke(
cmd_add_run.cli,
["--title", "Test Run", "--run-id", "123", "--run-assigned-to-id", "42", "--clear-run-assigned-to-id"],
)

assert result.exit_code == 1
assert "cannot be used together" in result.output


class TestApiRequestHandlerReferences:
"""Test class for reference management functionality"""

def test_manage_references_add(self):
"""Test adding references to existing ones"""
from trcli.api.api_request_handler import ApiRequestHandler
from trcli.cli import Environment
from trcli.api.api_client import APIClient
from trcli.data_classes.dataclass_testrail import TestRailSuite

environment = Environment()
api_client = APIClient("https://test.testrail.com")
suite = TestRailSuite(name="Test Suite")
handler = ApiRequestHandler(environment, api_client, suite)

# Adding new references
result = handler._manage_references("JIRA-100,JIRA-200", "JIRA-300,JIRA-400", "add")
assert result == "JIRA-100,JIRA-200,JIRA-300,JIRA-400"

# Adding duplicate references (should not duplicate)
result = handler._manage_references("JIRA-100,JIRA-200", "JIRA-200,JIRA-300", "add")
assert result == "JIRA-100,JIRA-200,JIRA-300"

# Adding to empty existing references
result = handler._manage_references("", "JIRA-100,JIRA-200", "add")
assert result == "JIRA-100,JIRA-200"
Expand All @@ -148,16 +181,16 @@ def test_manage_references_update(self):
from trcli.cli import Environment
from trcli.api.api_client import APIClient
from trcli.data_classes.dataclass_testrail import TestRailSuite

environment = Environment()
api_client = APIClient("https://test.testrail.com")
suite = TestRailSuite(name="Test Suite")
handler = ApiRequestHandler(environment, api_client, suite)

# Test replacing all references
result = handler._manage_references("JIRA-100,JIRA-200", "JIRA-300,JIRA-400", "update")
assert result == "JIRA-300,JIRA-400"

# Test replacing with empty references
result = handler._manage_references("JIRA-100,JIRA-200", "", "update")
assert result == ""
Expand All @@ -168,24 +201,24 @@ def test_manage_references_delete(self):
from trcli.cli import Environment
from trcli.api.api_client import APIClient
from trcli.data_classes.dataclass_testrail import TestRailSuite

environment = Environment()
api_client = APIClient("https://test.testrail.com")
suite = TestRailSuite(name="Test Suite")
handler = ApiRequestHandler(environment, api_client, suite)

# Deleting specific references
result = handler._manage_references("JIRA-100,JIRA-200,JIRA-300", "JIRA-200", "delete")
assert result == "JIRA-100,JIRA-300"

# Deleting multiple specific references
result = handler._manage_references("JIRA-100,JIRA-200,JIRA-300,JIRA-400", "JIRA-200,JIRA-400", "delete")
assert result == "JIRA-100,JIRA-300"

# Deleting all references (empty new_refs)
result = handler._manage_references("JIRA-100,JIRA-200", "", "delete")
assert result == ""

# Deleting non-existent references
result = handler._manage_references("JIRA-100,JIRA-200", "JIRA-999", "delete")
assert result == "JIRA-100,JIRA-200"
31 changes: 31 additions & 0 deletions tests/test_glob_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,37 @@ def test_glob_junit_multiple_results_scenario_2(self):
if merged_file.exists():
merged_file.unlink()

@pytest.mark.parse_robot
def test_glob_robot_duplicate_automation_ids(self):
"""Test Robot Framework glob pattern with duplicate automation_ids."""
env = Environment()
env.case_matcher = MatchersParser.AUTO
env.file = Path(__file__).parent / "test_data/XML/testglob_robot/*.xml"

# Check if test files exist
if not list(Path(__file__).parent.glob("test_data/XML/testglob_robot/*.xml")):
pytest.skip("Robot test data not available")

parser = RobotParser(env)
parsed_suites = parser.parse_file()
suite = parsed_suites[0]

# Similar verification as JUnit tests
data_provider = ApiDataProvider(suite)
cases_to_add = data_provider.add_cases()

# Verify deduplication occurred if there were duplicates
total_cases = sum(len(section.testcases) for section in suite.testsections)
automation_ids = [c.custom_automation_id for c in cases_to_add if c.custom_automation_id]

# Cases to add should have unique automation_ids
assert len(automation_ids) == len(set(automation_ids)), "Cases to add should have unique automation_ids"

# Clean up merged file
merged_file = Path.cwd() / "Merged-Robot-report.xml"
if merged_file.exists():
merged_file.unlink()

@pytest.mark.parse_cucumber
def test_cucumber_glob_filepath_not_pattern(self):
"""Test Scenario 3: Cucumber glob pattern uses correct filepath (not pattern string).
Expand Down
Loading
Loading