trcli 1.14.2__tar.gz → 1.14.3__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.14.2 → trcli-1.14.3}/PKG-INFO +1 -1
- {trcli-1.14.2 → trcli-1.14.3}/README.md +75 -4
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_api_request_handler.py +347 -5
- trcli-1.14.3/trcli/__init__.py +1 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/api/api_request_handler.py +76 -3
- trcli-1.14.3/trcli/api/result_handler.py +393 -0
- trcli-1.14.3/trcli/commands/cmd_results.py +335 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli.egg-info/PKG-INFO +1 -1
- {trcli-1.14.2 → trcli-1.14.3}/trcli.egg-info/SOURCES.txt +1 -0
- trcli-1.14.2/trcli/__init__.py +0 -1
- trcli-1.14.2/trcli/api/result_handler.py +0 -190
- {trcli-1.14.2 → trcli-1.14.3}/LICENSE.md +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/setup.cfg +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/setup.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_ai_evaluation_auto_creation.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_api_client.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_api_client_proxy.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_api_data_provider.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_api_request_handler_case_fields_update.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_api_request_handler_case_matcher.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_api_request_handler_labels.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_api_request_handler_references.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_cli.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_cmd_add_run.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_cmd_export_gherkin.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_cmd_import_gherkin.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_cmd_labels.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_cmd_parse_cucumber.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_cmd_references.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_cmd_update.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_cucumber_bdd_matching.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_cucumber_parser.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_dataclass_creation.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_glob_deduplication.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_glob_integration.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_junit_bdd_parser.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_junit_parse_reference.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_junit_parser.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_junit_quality_rating.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_load_data_from_config.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_matchers_parser.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_multiple_case_ids.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_project_based_client.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_quality_rating_parser.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_response_verify.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_result_fields_quality_rating.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_results_uploader.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_robot_parser.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_update_existing_cases_case_fields.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/tests/test_version_checker.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/api/__init__.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/api/api_cache.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/api/api_client.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/api/api_response_verify.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/api/api_utils.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/api/bdd_handler.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/api/case_handler.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/api/case_matcher.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/api/label_manager.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/api/multisuite_uploader.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/api/project_based_client.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/api/reference_manager.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/api/results_uploader.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/api/run_handler.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/api/section_handler.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/api/suite_handler.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/backports.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/cli.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/__init__.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_add_run.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_export_gherkin.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_import_gherkin.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_labels.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_parse_cucumber.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_parse_junit.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_parse_openapi.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_parse_robot.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_references.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_update.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/results_parser_helpers.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/constants.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/data_classes/__init__.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/data_classes/data_parsers.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/data_classes/dataclass_testrail.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/data_classes/quality_rating_parser.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/data_classes/validation_exception.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/data_providers/api_data_provider.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/logging/__init__.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/logging/config.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/logging/file_handler.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/logging/structured_logger.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/readers/__init__.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/readers/cucumber_json.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/readers/file_parser.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/readers/junit_xml.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/readers/openapi_yml.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/readers/robot_xml.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/settings.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli/version_checker.py +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli.egg-info/dependency_links.txt +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli.egg-info/entry_points.txt +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/trcli.egg-info/requires.txt +0 -0
- {trcli-1.14.2 → trcli-1.14.3}/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.14.
|
|
36
|
+
TestRail CLI v1.14.3
|
|
37
37
|
Copyright 2025 Gurock Software GmbH - www.gurock.com
|
|
38
38
|
Supported and loaded modules:
|
|
39
39
|
- parse_junit: JUnit XML Files (& Similar)
|
|
@@ -44,6 +44,7 @@ Supported and loaded modules:
|
|
|
44
44
|
- parse_openapi: OpenAPI YML Files
|
|
45
45
|
- add_run: Create a new test run
|
|
46
46
|
- labels: Manage labels (add, update, delete, list)
|
|
47
|
+
- results: Manage test results (list, update)
|
|
47
48
|
- references: Manage references (cases and runs)
|
|
48
49
|
```
|
|
49
50
|
|
|
@@ -51,7 +52,7 @@ CLI general reference
|
|
|
51
52
|
--------
|
|
52
53
|
```shell
|
|
53
54
|
$ trcli --help
|
|
54
|
-
TestRail CLI v1.14.
|
|
55
|
+
TestRail CLI v1.14.3
|
|
55
56
|
Copyright 2025 Gurock Software GmbH - www.gurock.com
|
|
56
57
|
Usage: trcli [OPTIONS] COMMAND [ARGS]...
|
|
57
58
|
|
|
@@ -92,6 +93,7 @@ Commands:
|
|
|
92
93
|
export_gherkin Export BDD test case from TestRail as .feature file
|
|
93
94
|
import_gherkin Upload Gherkin .feature file to TestRail
|
|
94
95
|
labels Manage labels in TestRail
|
|
96
|
+
results Manage test results in TestRail
|
|
95
97
|
parse_cucumber Parse Cucumber JSON results and upload to TestRail
|
|
96
98
|
parse_junit Parse JUnit report and upload results to TestRail
|
|
97
99
|
parse_openapi Parse OpenAPI spec and create cases in TestRail
|
|
@@ -1357,6 +1359,75 @@ tests are run across parallel, independent test nodes, all nodes should report t
|
|
|
1357
1359
|
First, use the `add_run` command to create a new run; then, pass the run title and id to each of the test nodes, which
|
|
1358
1360
|
will be used to upload all results into the same test run.
|
|
1359
1361
|
|
|
1362
|
+
#### Managing Test Results
|
|
1363
|
+
|
|
1364
|
+
The `results` command provides comprehensive test result management capabilities with two subcommands: `list` and `update`.
|
|
1365
|
+
|
|
1366
|
+
##### Listing Test Results
|
|
1367
|
+
|
|
1368
|
+
Retrieve test results from TestRail with flexible filtering options:
|
|
1369
|
+
|
|
1370
|
+
```bash
|
|
1371
|
+
# List results for a specific test
|
|
1372
|
+
trcli -c myconfig.yml results list --test-id 1001
|
|
1373
|
+
|
|
1374
|
+
# List all results for a run
|
|
1375
|
+
trcli -c myconfig.ymlresults list --run-id 100
|
|
1376
|
+
|
|
1377
|
+
# List results for a specific case within a run
|
|
1378
|
+
trcli -c myconfig.ymlresults list --case-id 200 --run-id 100
|
|
1379
|
+
|
|
1380
|
+
# Use pagination
|
|
1381
|
+
trcli -c myconfig.yml results list --test-id 1001 --offset 10 --limit 50
|
|
1382
|
+
|
|
1383
|
+
# Output as JSON for processing
|
|
1384
|
+
trcli results list --test-id 1001 --json-output
|
|
1385
|
+
|
|
1386
|
+
# Show all fields including custom fields in detail
|
|
1387
|
+
trcli results list --run-id 100 --show-all-fields
|
|
1388
|
+
```
|
|
1389
|
+
|
|
1390
|
+
**Filtering options:**
|
|
1391
|
+
- `--test-id`: Get results for a specific test (mutually exclusive with other filters)
|
|
1392
|
+
- `--run-id`: Get results for all tests in a run (can be used alone or with --case-id)
|
|
1393
|
+
- `--case-id`: Get results for a specific case (requires --run-id)
|
|
1394
|
+
- `--offset`: Pagination offset (default: 0)
|
|
1395
|
+
- `--limit`: Pagination limit (default: 250)
|
|
1396
|
+
- `--json-output`: Output raw JSON response from API
|
|
1397
|
+
- `--show-all-fields`: Show all fields including custom fields in detail
|
|
1398
|
+
|
|
1399
|
+
##### Updating Test Results
|
|
1400
|
+
|
|
1401
|
+
Update existing test results after they have been created. Useful for re-run scenarios, adding comments, linking defects, or correcting result data:
|
|
1402
|
+
|
|
1403
|
+
```bash
|
|
1404
|
+
# Update test result status and add comment
|
|
1405
|
+
trcli -c myconfig.yml results update --result-id 12345 --status-id 5 --comment "Test failed due to timeout"
|
|
1406
|
+
|
|
1407
|
+
# Update multiple fields at once
|
|
1408
|
+
trcli -c myconfig.yml results update --result-id 12345 \
|
|
1409
|
+
--status-id 1 \
|
|
1410
|
+
--comment "Passed after retry" \
|
|
1411
|
+
--elapsed "45s" \
|
|
1412
|
+
--defects "BUG-456" \
|
|
1413
|
+
--version "2.1.0"
|
|
1414
|
+
|
|
1415
|
+
# Update custom fields (JSON format)
|
|
1416
|
+
trcli -c myconfig.yml results update --result-id 12345 \
|
|
1417
|
+
--custom-fields '{"custom_test_environment": "Production", "custom_browser": "Chrome"}'
|
|
1418
|
+
|
|
1419
|
+
# Assign result to a user
|
|
1420
|
+
trcli -c myconfig.yml results update --result-id 12345 --assignedto-id 7 --comment "Needs investigation"
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
**Status IDs:** 1=Passed, 2=Blocked, 3=Untested, 4=Retest, 5=Failed
|
|
1424
|
+
|
|
1425
|
+
For complete documentation:
|
|
1426
|
+
```bash
|
|
1427
|
+
trcli results list --help
|
|
1428
|
+
trcli results update --help
|
|
1429
|
+
```
|
|
1430
|
+
|
|
1360
1431
|
#### Labels Management
|
|
1361
1432
|
|
|
1362
1433
|
The TestRail CLI provides comprehensive label management capabilities using the `labels` command. Labels help categorize and organize your test management assets efficiently, making it easier to filter and manage test cases, runs, and projects.
|
|
@@ -2109,7 +2180,7 @@ Options:
|
|
|
2109
2180
|
### Reference
|
|
2110
2181
|
```shell
|
|
2111
2182
|
$ trcli add_run --help
|
|
2112
|
-
TestRail CLI v1.14.
|
|
2183
|
+
TestRail CLI v1.14.3
|
|
2113
2184
|
Copyright 2025 Gurock Software GmbH - www.gurock.com
|
|
2114
2185
|
Usage: trcli add_run [OPTIONS]
|
|
2115
2186
|
|
|
@@ -2319,7 +2390,7 @@ providing you with a solid base of test cases, which you can further expand on T
|
|
|
2319
2390
|
### Reference
|
|
2320
2391
|
```shell
|
|
2321
2392
|
$ trcli parse_openapi --help
|
|
2322
|
-
TestRail CLI v1.14.
|
|
2393
|
+
TestRail CLI v1.14.3
|
|
2323
2394
|
Copyright 2025 Gurock Software GmbH - www.gurock.com
|
|
2324
2395
|
Usage: trcli parse_openapi [OPTIONS]
|
|
2325
2396
|
|
|
@@ -1380,7 +1380,8 @@ class TestApiRequestHandler:
|
|
|
1380
1380
|
request_id_to_result_id = {id(report_results[0]): 2001}
|
|
1381
1381
|
|
|
1382
1382
|
# Call upload_attachments
|
|
1383
|
-
|
|
1383
|
+
total_attachments = sum(len(r["attachments"]) for r in report_results)
|
|
1384
|
+
api_request_handler.upload_attachments(report_results, request_id_to_result_id, total_attachments)
|
|
1384
1385
|
|
|
1385
1386
|
# Verify the request was made (case-insensitive comparison)
|
|
1386
1387
|
assert requests_mock.last_request.url.lower() == create_url("add_attachment_to_result/2001").lower()
|
|
@@ -1400,7 +1401,8 @@ class TestApiRequestHandler:
|
|
|
1400
1401
|
request_id_to_result_id = {id(report_results[0]): 2001}
|
|
1401
1402
|
|
|
1402
1403
|
# Call upload_attachments
|
|
1403
|
-
|
|
1404
|
+
total_attachments = sum(len(r["attachments"]) for r in report_results)
|
|
1405
|
+
api_request_handler.upload_attachments(report_results, request_id_to_result_id, total_attachments)
|
|
1404
1406
|
|
|
1405
1407
|
# Verify the request was made (case-insensitive comparison)
|
|
1406
1408
|
assert requests_mock.last_request.url.lower() == create_url("add_attachment_to_result/2001").lower()
|
|
@@ -1413,7 +1415,8 @@ class TestApiRequestHandler:
|
|
|
1413
1415
|
request_id_to_result_id = {id(report_results[0]): 2001}
|
|
1414
1416
|
|
|
1415
1417
|
# Call upload_attachments - should not raise exception
|
|
1416
|
-
|
|
1418
|
+
total_attachments = sum(len(r["attachments"]) for r in report_results)
|
|
1419
|
+
api_request_handler.upload_attachments(report_results, request_id_to_result_id, total_attachments)
|
|
1417
1420
|
|
|
1418
1421
|
@pytest.mark.api_handler
|
|
1419
1422
|
def test_upload_attachments_empty_run_scenario(
|
|
@@ -1446,7 +1449,8 @@ class TestApiRequestHandler:
|
|
|
1446
1449
|
request_id_to_result_id = {id(report_results[0]): 5001, id(report_results[1]): 5002}
|
|
1447
1450
|
|
|
1448
1451
|
# Call upload_attachments
|
|
1449
|
-
|
|
1452
|
+
total_attachments = sum(len(r["attachments"]) for r in report_results)
|
|
1453
|
+
api_request_handler.upload_attachments(report_results, request_id_to_result_id, total_attachments)
|
|
1450
1454
|
|
|
1451
1455
|
# Verify both attachments were uploaded correctly
|
|
1452
1456
|
history = requests_mock.request_history
|
|
@@ -1487,7 +1491,8 @@ class TestApiRequestHandler:
|
|
|
1487
1491
|
}
|
|
1488
1492
|
|
|
1489
1493
|
# Call upload_attachments
|
|
1490
|
-
|
|
1494
|
+
total_attachments = sum(len(r["attachments"]) for r in report_results)
|
|
1495
|
+
api_request_handler.upload_attachments(report_results, request_id_to_result_id, total_attachments)
|
|
1491
1496
|
|
|
1492
1497
|
# Verify both attachments were uploaded correctly
|
|
1493
1498
|
history = requests_mock.request_history
|
|
@@ -1563,3 +1568,340 @@ class TestApiRequestHandler:
|
|
|
1563
1568
|
assert stats["miss_count"] == 1
|
|
1564
1569
|
assert stats["hit_count"] == 1
|
|
1565
1570
|
assert stats["hit_rate"] == 50.0 # 1 hit out of 2 total requests
|
|
1571
|
+
|
|
1572
|
+
def test_edit_result_success(self, api_request_handler: ApiRequestHandler, requests_mock):
|
|
1573
|
+
"""Test successfully editing a result with all fields"""
|
|
1574
|
+
result_id = 12345
|
|
1575
|
+
mocked_response = {
|
|
1576
|
+
"id": result_id,
|
|
1577
|
+
"status_id": 5,
|
|
1578
|
+
"comment": "Test failed due to timeout",
|
|
1579
|
+
"version": "2.0.1",
|
|
1580
|
+
"elapsed": "30s",
|
|
1581
|
+
"defects": "BUG-123,BUG-456",
|
|
1582
|
+
"assignedto_id": 7,
|
|
1583
|
+
"custom_field1": "custom_value",
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
requests_mock.post(create_url(f"edit_result/{result_id}"), json=mocked_response)
|
|
1587
|
+
|
|
1588
|
+
success, error = api_request_handler.edit_result(
|
|
1589
|
+
result_id=result_id,
|
|
1590
|
+
status_id=5,
|
|
1591
|
+
comment="Test failed due to timeout",
|
|
1592
|
+
version="2.0.1",
|
|
1593
|
+
elapsed="30s",
|
|
1594
|
+
defects="BUG-123,BUG-456",
|
|
1595
|
+
assignedto_id=7,
|
|
1596
|
+
custom_fields={"custom_field1": "custom_value"},
|
|
1597
|
+
)
|
|
1598
|
+
|
|
1599
|
+
assert success is True
|
|
1600
|
+
assert error is None
|
|
1601
|
+
assert requests_mock.call_count == 1
|
|
1602
|
+
|
|
1603
|
+
# Verify request body
|
|
1604
|
+
request_body = requests_mock.last_request.json()
|
|
1605
|
+
assert request_body["status_id"] == 5
|
|
1606
|
+
assert request_body["comment"] == "Test failed due to timeout"
|
|
1607
|
+
assert request_body["version"] == "2.0.1"
|
|
1608
|
+
assert request_body["elapsed"] == "30s"
|
|
1609
|
+
assert request_body["defects"] == "BUG-123,BUG-456"
|
|
1610
|
+
assert request_body["assignedto_id"] == 7
|
|
1611
|
+
assert request_body["custom_field1"] == "custom_value"
|
|
1612
|
+
|
|
1613
|
+
def test_edit_result_partial_fields(self, api_request_handler: ApiRequestHandler, requests_mock):
|
|
1614
|
+
"""Test editing a result with only some fields"""
|
|
1615
|
+
result_id = 12345
|
|
1616
|
+
mocked_response = {
|
|
1617
|
+
"id": result_id,
|
|
1618
|
+
"status_id": 1,
|
|
1619
|
+
"comment": "Test passed after retry",
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
requests_mock.post(create_url(f"edit_result/{result_id}"), json=mocked_response)
|
|
1623
|
+
|
|
1624
|
+
success, error = api_request_handler.edit_result(
|
|
1625
|
+
result_id=result_id,
|
|
1626
|
+
status_id=1,
|
|
1627
|
+
comment="Test passed after retry",
|
|
1628
|
+
)
|
|
1629
|
+
|
|
1630
|
+
assert success is True
|
|
1631
|
+
assert error is None
|
|
1632
|
+
|
|
1633
|
+
# Verify only provided fields are in request
|
|
1634
|
+
request_body = requests_mock.last_request.json()
|
|
1635
|
+
assert request_body["status_id"] == 1
|
|
1636
|
+
assert request_body["comment"] == "Test passed after retry"
|
|
1637
|
+
assert "version" not in request_body
|
|
1638
|
+
assert "elapsed" not in request_body
|
|
1639
|
+
assert "defects" not in request_body
|
|
1640
|
+
|
|
1641
|
+
def test_edit_result_api_error(self, api_request_handler: ApiRequestHandler, requests_mock):
|
|
1642
|
+
"""Test edit_result when API returns an error"""
|
|
1643
|
+
result_id = 12345
|
|
1644
|
+
error_message = "Field :status_id is not a valid status."
|
|
1645
|
+
|
|
1646
|
+
requests_mock.post(
|
|
1647
|
+
create_url(f"edit_result/{result_id}"),
|
|
1648
|
+
json={"error": error_message},
|
|
1649
|
+
status_code=400,
|
|
1650
|
+
)
|
|
1651
|
+
|
|
1652
|
+
success, error = api_request_handler.edit_result(
|
|
1653
|
+
result_id=result_id,
|
|
1654
|
+
status_id=999, # Invalid status ID
|
|
1655
|
+
)
|
|
1656
|
+
|
|
1657
|
+
assert success is False
|
|
1658
|
+
assert error_message in error
|
|
1659
|
+
|
|
1660
|
+
def test_edit_result_no_fields_provided(self, api_request_handler: ApiRequestHandler):
|
|
1661
|
+
"""Test edit_result when no fields are provided"""
|
|
1662
|
+
result_id = 12345
|
|
1663
|
+
|
|
1664
|
+
success, error = api_request_handler.edit_result(result_id=result_id)
|
|
1665
|
+
|
|
1666
|
+
assert success is False
|
|
1667
|
+
assert error == "No fields provided to update"
|
|
1668
|
+
|
|
1669
|
+
def test_edit_result_custom_fields_only(self, api_request_handler: ApiRequestHandler, requests_mock):
|
|
1670
|
+
"""Test editing a result with only custom fields"""
|
|
1671
|
+
result_id = 12345
|
|
1672
|
+
mocked_response = {
|
|
1673
|
+
"id": result_id,
|
|
1674
|
+
"custom_automation_type": "Automated",
|
|
1675
|
+
"custom_test_environment": "Production",
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
requests_mock.post(create_url(f"edit_result/{result_id}"), json=mocked_response)
|
|
1679
|
+
|
|
1680
|
+
success, error = api_request_handler.edit_result(
|
|
1681
|
+
result_id=result_id,
|
|
1682
|
+
custom_fields={
|
|
1683
|
+
"custom_automation_type": "Automated",
|
|
1684
|
+
"custom_test_environment": "Production",
|
|
1685
|
+
},
|
|
1686
|
+
)
|
|
1687
|
+
|
|
1688
|
+
assert success is True
|
|
1689
|
+
assert error is None
|
|
1690
|
+
|
|
1691
|
+
# Verify custom fields are in request
|
|
1692
|
+
request_body = requests_mock.last_request.json()
|
|
1693
|
+
assert request_body["custom_automation_type"] == "Automated"
|
|
1694
|
+
assert request_body["custom_test_environment"] == "Production"
|
|
1695
|
+
|
|
1696
|
+
def test_get_results_success(self, api_request_handler: ApiRequestHandler, requests_mock):
|
|
1697
|
+
"""Test successfully retrieving results for a test"""
|
|
1698
|
+
test_id = 1001
|
|
1699
|
+
mocked_response = {
|
|
1700
|
+
"offset": 0,
|
|
1701
|
+
"limit": 250,
|
|
1702
|
+
"size": 2,
|
|
1703
|
+
"_links": {"next": None, "prev": None},
|
|
1704
|
+
"results": [
|
|
1705
|
+
{
|
|
1706
|
+
"id": 1,
|
|
1707
|
+
"test_id": test_id,
|
|
1708
|
+
"status_id": 1,
|
|
1709
|
+
"created_on": 1234567890,
|
|
1710
|
+
"created_by": 1,
|
|
1711
|
+
"comment": "Test passed",
|
|
1712
|
+
},
|
|
1713
|
+
{
|
|
1714
|
+
"id": 2,
|
|
1715
|
+
"test_id": test_id,
|
|
1716
|
+
"status_id": 5,
|
|
1717
|
+
"created_on": 1234567900,
|
|
1718
|
+
"created_by": 2,
|
|
1719
|
+
"comment": "Test failed",
|
|
1720
|
+
},
|
|
1721
|
+
],
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
requests_mock.get(create_url(f"get_results/{test_id}&offset=0&limit=250"), json=mocked_response)
|
|
1725
|
+
|
|
1726
|
+
results, error = api_request_handler.get_results(test_id, offset=0, limit=250)
|
|
1727
|
+
|
|
1728
|
+
assert error is None
|
|
1729
|
+
assert len(results) == 2
|
|
1730
|
+
assert results[0]["id"] == 1
|
|
1731
|
+
assert results[0]["status_id"] == 1
|
|
1732
|
+
assert results[1]["id"] == 2
|
|
1733
|
+
assert results[1]["status_id"] == 5
|
|
1734
|
+
|
|
1735
|
+
def test_get_results_with_pagination(self, api_request_handler: ApiRequestHandler, requests_mock):
|
|
1736
|
+
"""Test retrieving results with custom pagination"""
|
|
1737
|
+
test_id = 1001
|
|
1738
|
+
mocked_response = {
|
|
1739
|
+
"offset": 10,
|
|
1740
|
+
"limit": 5,
|
|
1741
|
+
"size": 1,
|
|
1742
|
+
"_links": {"next": None, "prev": None},
|
|
1743
|
+
"results": [
|
|
1744
|
+
{
|
|
1745
|
+
"id": 11,
|
|
1746
|
+
"test_id": test_id,
|
|
1747
|
+
"status_id": 1,
|
|
1748
|
+
"created_on": 1234567890,
|
|
1749
|
+
"created_by": 1,
|
|
1750
|
+
},
|
|
1751
|
+
],
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
requests_mock.get(create_url(f"get_results/{test_id}&offset=10&limit=5"), json=mocked_response)
|
|
1755
|
+
|
|
1756
|
+
results, error = api_request_handler.get_results(test_id, offset=10, limit=5)
|
|
1757
|
+
|
|
1758
|
+
assert error is None
|
|
1759
|
+
assert len(results) == 1
|
|
1760
|
+
assert results[0]["id"] == 11
|
|
1761
|
+
|
|
1762
|
+
def test_get_results_api_error(self, api_request_handler: ApiRequestHandler, requests_mock):
|
|
1763
|
+
"""Test get_results when API returns an error"""
|
|
1764
|
+
test_id = 1001
|
|
1765
|
+
error_message = "Field :test_id is not a valid test."
|
|
1766
|
+
|
|
1767
|
+
requests_mock.get(
|
|
1768
|
+
create_url(f"get_results/{test_id}&offset=0&limit=250"),
|
|
1769
|
+
json={"error": error_message},
|
|
1770
|
+
status_code=400,
|
|
1771
|
+
)
|
|
1772
|
+
|
|
1773
|
+
results, error = api_request_handler.get_results(test_id)
|
|
1774
|
+
|
|
1775
|
+
assert len(results) == 0
|
|
1776
|
+
assert error_message in error
|
|
1777
|
+
|
|
1778
|
+
def test_get_results_for_case_success(self, api_request_handler: ApiRequestHandler, requests_mock):
|
|
1779
|
+
"""Test successfully retrieving results for a case in a run"""
|
|
1780
|
+
run_id = 100
|
|
1781
|
+
case_id = 200
|
|
1782
|
+
mocked_response = {
|
|
1783
|
+
"offset": 0,
|
|
1784
|
+
"limit": 250,
|
|
1785
|
+
"size": 1,
|
|
1786
|
+
"_links": {"next": None, "prev": None},
|
|
1787
|
+
"results": [
|
|
1788
|
+
{
|
|
1789
|
+
"id": 1,
|
|
1790
|
+
"test_id": 5001,
|
|
1791
|
+
"status_id": 1,
|
|
1792
|
+
"created_on": 1234567890,
|
|
1793
|
+
"created_by": 1,
|
|
1794
|
+
"comment": "Test passed",
|
|
1795
|
+
},
|
|
1796
|
+
],
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
requests_mock.get(
|
|
1800
|
+
create_url(f"get_results_for_case/{run_id}/{case_id}&offset=0&limit=250"), json=mocked_response
|
|
1801
|
+
)
|
|
1802
|
+
|
|
1803
|
+
results, error = api_request_handler.get_results_for_case(run_id, case_id, offset=0, limit=250)
|
|
1804
|
+
|
|
1805
|
+
assert error is None
|
|
1806
|
+
assert len(results) == 1
|
|
1807
|
+
assert results[0]["id"] == 1
|
|
1808
|
+
assert results[0]["test_id"] == 5001
|
|
1809
|
+
|
|
1810
|
+
def test_get_results_for_case_api_error(self, api_request_handler: ApiRequestHandler, requests_mock):
|
|
1811
|
+
"""Test get_results_for_case when API returns an error"""
|
|
1812
|
+
run_id = 100
|
|
1813
|
+
case_id = 200
|
|
1814
|
+
error_message = "Field :case_id is not a valid case."
|
|
1815
|
+
|
|
1816
|
+
requests_mock.get(
|
|
1817
|
+
create_url(f"get_results_for_case/{run_id}/{case_id}&offset=0&limit=250"),
|
|
1818
|
+
json={"error": error_message},
|
|
1819
|
+
status_code=400,
|
|
1820
|
+
)
|
|
1821
|
+
|
|
1822
|
+
results, error = api_request_handler.get_results_for_case(run_id, case_id)
|
|
1823
|
+
|
|
1824
|
+
assert len(results) == 0
|
|
1825
|
+
assert error_message in error
|
|
1826
|
+
|
|
1827
|
+
def test_get_results_for_run_success(self, api_request_handler: ApiRequestHandler, requests_mock):
|
|
1828
|
+
"""Test successfully retrieving results for all tests in a run"""
|
|
1829
|
+
run_id = 100
|
|
1830
|
+
mocked_response = {
|
|
1831
|
+
"offset": 0,
|
|
1832
|
+
"limit": 250,
|
|
1833
|
+
"size": 2,
|
|
1834
|
+
"_links": {"next": None, "prev": None},
|
|
1835
|
+
"results": [
|
|
1836
|
+
{
|
|
1837
|
+
"id": 1,
|
|
1838
|
+
"test_id": 5001,
|
|
1839
|
+
"status_id": 1,
|
|
1840
|
+
"created_on": 1234567890,
|
|
1841
|
+
"created_by": 1,
|
|
1842
|
+
"comment": "Test passed",
|
|
1843
|
+
},
|
|
1844
|
+
{
|
|
1845
|
+
"id": 2,
|
|
1846
|
+
"test_id": 5002,
|
|
1847
|
+
"status_id": 5,
|
|
1848
|
+
"created_on": 1234567900,
|
|
1849
|
+
"created_by": 2,
|
|
1850
|
+
"comment": "Test failed",
|
|
1851
|
+
},
|
|
1852
|
+
],
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
requests_mock.get(create_url(f"get_results_for_run/{run_id}&offset=0&limit=250"), json=mocked_response)
|
|
1856
|
+
|
|
1857
|
+
results, error = api_request_handler.get_results_for_run(run_id, offset=0, limit=250)
|
|
1858
|
+
|
|
1859
|
+
assert error is None
|
|
1860
|
+
assert len(results) == 2
|
|
1861
|
+
assert results[0]["id"] == 1
|
|
1862
|
+
assert results[0]["test_id"] == 5001
|
|
1863
|
+
assert results[1]["id"] == 2
|
|
1864
|
+
assert results[1]["test_id"] == 5002
|
|
1865
|
+
|
|
1866
|
+
def test_get_results_for_run_with_pagination(self, api_request_handler: ApiRequestHandler, requests_mock):
|
|
1867
|
+
"""Test retrieving run results with custom pagination"""
|
|
1868
|
+
run_id = 100
|
|
1869
|
+
mocked_response = {
|
|
1870
|
+
"offset": 10,
|
|
1871
|
+
"limit": 5,
|
|
1872
|
+
"size": 1,
|
|
1873
|
+
"_links": {"next": None, "prev": None},
|
|
1874
|
+
"results": [
|
|
1875
|
+
{
|
|
1876
|
+
"id": 11,
|
|
1877
|
+
"test_id": 5011,
|
|
1878
|
+
"status_id": 1,
|
|
1879
|
+
"created_on": 1234567890,
|
|
1880
|
+
"created_by": 1,
|
|
1881
|
+
},
|
|
1882
|
+
],
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
requests_mock.get(create_url(f"get_results_for_run/{run_id}&offset=10&limit=5"), json=mocked_response)
|
|
1886
|
+
|
|
1887
|
+
results, error = api_request_handler.get_results_for_run(run_id, offset=10, limit=5)
|
|
1888
|
+
|
|
1889
|
+
assert error is None
|
|
1890
|
+
assert len(results) == 1
|
|
1891
|
+
assert results[0]["id"] == 11
|
|
1892
|
+
|
|
1893
|
+
def test_get_results_for_run_api_error(self, api_request_handler: ApiRequestHandler, requests_mock):
|
|
1894
|
+
"""Test get_results_for_run when API returns an error"""
|
|
1895
|
+
run_id = 100
|
|
1896
|
+
error_message = "Field :run_id is not a valid run."
|
|
1897
|
+
|
|
1898
|
+
requests_mock.get(
|
|
1899
|
+
create_url(f"get_results_for_run/{run_id}&offset=0&limit=250"),
|
|
1900
|
+
json={"error": error_message},
|
|
1901
|
+
status_code=400,
|
|
1902
|
+
)
|
|
1903
|
+
|
|
1904
|
+
results, error = api_request_handler.get_results_for_run(run_id)
|
|
1905
|
+
|
|
1906
|
+
assert len(results) == 0
|
|
1907
|
+
assert error_message in error
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.14.3"
|
|
@@ -286,13 +286,80 @@ class ApiRequestHandler:
|
|
|
286
286
|
) -> Tuple[bool, str, List[str], List[str], List[str]]:
|
|
287
287
|
return self.case_handler.update_existing_case_references(case_id, junit_refs, case_fields, strategy)
|
|
288
288
|
|
|
289
|
-
def upload_attachments(
|
|
290
|
-
|
|
289
|
+
def upload_attachments(
|
|
290
|
+
self, report_results: List[Dict], request_id_to_result_id: Dict[int, int], total_attachments: int
|
|
291
|
+
):
|
|
292
|
+
return self.result_handler.upload_attachments(report_results, request_id_to_result_id, total_attachments)
|
|
291
293
|
|
|
292
294
|
def add_results(self, run_id: int) -> Tuple[List, str, int]:
|
|
293
295
|
return self.result_handler.add_results(run_id)
|
|
294
296
|
|
|
297
|
+
def get_results(self, test_id: int, offset: int = 0, limit: int = 250) -> Tuple[List[Dict], str]:
|
|
298
|
+
"""
|
|
299
|
+
Get test results for a specific test.
|
|
300
|
+
|
|
301
|
+
:param test_id: TestRail test ID
|
|
302
|
+
:param offset: Pagination offset (default: 0)
|
|
303
|
+
:param limit: Pagination limit (default: 250)
|
|
304
|
+
:returns: Tuple of (results_list, error_message)
|
|
305
|
+
"""
|
|
306
|
+
return self.result_handler.get_results(test_id, offset, limit)
|
|
307
|
+
|
|
308
|
+
def get_results_for_run(self, run_id: int, offset: int = 0, limit: int = 250) -> Tuple[List[Dict], str]:
|
|
309
|
+
"""
|
|
310
|
+
Get test results for all tests in a run.
|
|
311
|
+
|
|
312
|
+
:param run_id: TestRail run ID
|
|
313
|
+
:param offset: Pagination offset (default: 0)
|
|
314
|
+
:param limit: Pagination limit (default: 250)
|
|
315
|
+
:returns: Tuple of (results_list, error_message)
|
|
316
|
+
"""
|
|
317
|
+
return self.result_handler.get_results_for_run(run_id, offset, limit)
|
|
318
|
+
|
|
319
|
+
def get_results_for_case(
|
|
320
|
+
self, run_id: int, case_id: int, offset: int = 0, limit: int = 250
|
|
321
|
+
) -> Tuple[List[Dict], str]:
|
|
322
|
+
"""
|
|
323
|
+
Get test results for a specific case in a run.
|
|
324
|
+
|
|
325
|
+
:param run_id: TestRail run ID
|
|
326
|
+
:param case_id: TestRail case ID
|
|
327
|
+
:param offset: Pagination offset (default: 0)
|
|
328
|
+
:param limit: Pagination limit (default: 250)
|
|
329
|
+
:returns: Tuple of (results_list, error_message)
|
|
330
|
+
"""
|
|
331
|
+
return self.result_handler.get_results_for_case(run_id, case_id, offset, limit)
|
|
332
|
+
|
|
333
|
+
def edit_result(
|
|
334
|
+
self,
|
|
335
|
+
result_id: int,
|
|
336
|
+
status_id: int = None,
|
|
337
|
+
comment: str = None,
|
|
338
|
+
version: str = None,
|
|
339
|
+
elapsed: str = None,
|
|
340
|
+
defects: str = None,
|
|
341
|
+
assignedto_id: int = None,
|
|
342
|
+
custom_fields: Dict = None,
|
|
343
|
+
) -> Tuple[bool, str]:
|
|
344
|
+
"""
|
|
345
|
+
Edit an existing test result.
|
|
346
|
+
|
|
347
|
+
:param result_id: TestRail result ID to edit
|
|
348
|
+
:param status_id: Test status ID (1=Passed, 2=Blocked, 3=Untested, 4=Retest, 5=Failed)
|
|
349
|
+
:param comment: Comment/notes for the result
|
|
350
|
+
:param version: Version or build tested against
|
|
351
|
+
:param elapsed: Time elapsed (e.g., "1m 5s" or "65s")
|
|
352
|
+
:param defects: Comma-separated list of defect IDs
|
|
353
|
+
:param assignedto_id: User ID to assign the test to
|
|
354
|
+
:param custom_fields: Dictionary of custom field values
|
|
355
|
+
:returns: Tuple of (success, error_message)
|
|
356
|
+
"""
|
|
357
|
+
return self.result_handler.edit_result(
|
|
358
|
+
result_id, status_id, comment, version, elapsed, defects, assignedto_id, custom_fields
|
|
359
|
+
)
|
|
360
|
+
|
|
295
361
|
def handle_futures(self, futures, action_string, progress_bar) -> Tuple[list, str]:
|
|
362
|
+
responses_by_request = {} if action_string == "add_results" else None
|
|
296
363
|
responses = []
|
|
297
364
|
error_message = ""
|
|
298
365
|
try:
|
|
@@ -300,10 +367,11 @@ class ApiRequestHandler:
|
|
|
300
367
|
arguments = futures[future]
|
|
301
368
|
response = future.result()
|
|
302
369
|
if not response.error_message:
|
|
303
|
-
responses.append(response)
|
|
304
370
|
if action_string == "add_results":
|
|
371
|
+
responses_by_request[id(arguments)] = response
|
|
305
372
|
progress_bar.update(len(arguments["results"]))
|
|
306
373
|
else:
|
|
374
|
+
responses.append(response)
|
|
307
375
|
if action_string == "add_case":
|
|
308
376
|
arguments = arguments.to_dict()
|
|
309
377
|
arguments.pop("case_id")
|
|
@@ -323,6 +391,11 @@ class ApiRequestHandler:
|
|
|
323
391
|
except KeyboardInterrupt:
|
|
324
392
|
self.__cancel_running_futures(futures, action_string)
|
|
325
393
|
raise KeyboardInterrupt
|
|
394
|
+
|
|
395
|
+
if action_string == "add_results" and responses_by_request:
|
|
396
|
+
request_bodies = list(futures.values())
|
|
397
|
+
responses = [responses_by_request[id(req)] for req in request_bodies if id(req) in responses_by_request]
|
|
398
|
+
|
|
326
399
|
return responses, error_message
|
|
327
400
|
|
|
328
401
|
def close_run(self, run_id: int) -> Tuple[dict, str]:
|