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.
Files changed (103) hide show
  1. {trcli-1.14.2 → trcli-1.14.3}/PKG-INFO +1 -1
  2. {trcli-1.14.2 → trcli-1.14.3}/README.md +75 -4
  3. {trcli-1.14.2 → trcli-1.14.3}/tests/test_api_request_handler.py +347 -5
  4. trcli-1.14.3/trcli/__init__.py +1 -0
  5. {trcli-1.14.2 → trcli-1.14.3}/trcli/api/api_request_handler.py +76 -3
  6. trcli-1.14.3/trcli/api/result_handler.py +393 -0
  7. trcli-1.14.3/trcli/commands/cmd_results.py +335 -0
  8. {trcli-1.14.2 → trcli-1.14.3}/trcli.egg-info/PKG-INFO +1 -1
  9. {trcli-1.14.2 → trcli-1.14.3}/trcli.egg-info/SOURCES.txt +1 -0
  10. trcli-1.14.2/trcli/__init__.py +0 -1
  11. trcli-1.14.2/trcli/api/result_handler.py +0 -190
  12. {trcli-1.14.2 → trcli-1.14.3}/LICENSE.md +0 -0
  13. {trcli-1.14.2 → trcli-1.14.3}/setup.cfg +0 -0
  14. {trcli-1.14.2 → trcli-1.14.3}/setup.py +0 -0
  15. {trcli-1.14.2 → trcli-1.14.3}/tests/test_ai_evaluation_auto_creation.py +0 -0
  16. {trcli-1.14.2 → trcli-1.14.3}/tests/test_api_client.py +0 -0
  17. {trcli-1.14.2 → trcli-1.14.3}/tests/test_api_client_proxy.py +0 -0
  18. {trcli-1.14.2 → trcli-1.14.3}/tests/test_api_data_provider.py +0 -0
  19. {trcli-1.14.2 → trcli-1.14.3}/tests/test_api_request_handler_case_fields_update.py +0 -0
  20. {trcli-1.14.2 → trcli-1.14.3}/tests/test_api_request_handler_case_matcher.py +0 -0
  21. {trcli-1.14.2 → trcli-1.14.3}/tests/test_api_request_handler_labels.py +0 -0
  22. {trcli-1.14.2 → trcli-1.14.3}/tests/test_api_request_handler_references.py +0 -0
  23. {trcli-1.14.2 → trcli-1.14.3}/tests/test_cli.py +0 -0
  24. {trcli-1.14.2 → trcli-1.14.3}/tests/test_cmd_add_run.py +0 -0
  25. {trcli-1.14.2 → trcli-1.14.3}/tests/test_cmd_export_gherkin.py +0 -0
  26. {trcli-1.14.2 → trcli-1.14.3}/tests/test_cmd_import_gherkin.py +0 -0
  27. {trcli-1.14.2 → trcli-1.14.3}/tests/test_cmd_labels.py +0 -0
  28. {trcli-1.14.2 → trcli-1.14.3}/tests/test_cmd_parse_cucumber.py +0 -0
  29. {trcli-1.14.2 → trcli-1.14.3}/tests/test_cmd_references.py +0 -0
  30. {trcli-1.14.2 → trcli-1.14.3}/tests/test_cmd_update.py +0 -0
  31. {trcli-1.14.2 → trcli-1.14.3}/tests/test_cucumber_bdd_matching.py +0 -0
  32. {trcli-1.14.2 → trcli-1.14.3}/tests/test_cucumber_parser.py +0 -0
  33. {trcli-1.14.2 → trcli-1.14.3}/tests/test_dataclass_creation.py +0 -0
  34. {trcli-1.14.2 → trcli-1.14.3}/tests/test_glob_deduplication.py +0 -0
  35. {trcli-1.14.2 → trcli-1.14.3}/tests/test_glob_integration.py +0 -0
  36. {trcli-1.14.2 → trcli-1.14.3}/tests/test_junit_bdd_parser.py +0 -0
  37. {trcli-1.14.2 → trcli-1.14.3}/tests/test_junit_parse_reference.py +0 -0
  38. {trcli-1.14.2 → trcli-1.14.3}/tests/test_junit_parser.py +0 -0
  39. {trcli-1.14.2 → trcli-1.14.3}/tests/test_junit_quality_rating.py +0 -0
  40. {trcli-1.14.2 → trcli-1.14.3}/tests/test_load_data_from_config.py +0 -0
  41. {trcli-1.14.2 → trcli-1.14.3}/tests/test_matchers_parser.py +0 -0
  42. {trcli-1.14.2 → trcli-1.14.3}/tests/test_multiple_case_ids.py +0 -0
  43. {trcli-1.14.2 → trcli-1.14.3}/tests/test_project_based_client.py +0 -0
  44. {trcli-1.14.2 → trcli-1.14.3}/tests/test_quality_rating_parser.py +0 -0
  45. {trcli-1.14.2 → trcli-1.14.3}/tests/test_response_verify.py +0 -0
  46. {trcli-1.14.2 → trcli-1.14.3}/tests/test_result_fields_quality_rating.py +0 -0
  47. {trcli-1.14.2 → trcli-1.14.3}/tests/test_results_uploader.py +0 -0
  48. {trcli-1.14.2 → trcli-1.14.3}/tests/test_robot_parser.py +0 -0
  49. {trcli-1.14.2 → trcli-1.14.3}/tests/test_update_existing_cases_case_fields.py +0 -0
  50. {trcli-1.14.2 → trcli-1.14.3}/tests/test_version_checker.py +0 -0
  51. {trcli-1.14.2 → trcli-1.14.3}/trcli/api/__init__.py +0 -0
  52. {trcli-1.14.2 → trcli-1.14.3}/trcli/api/api_cache.py +0 -0
  53. {trcli-1.14.2 → trcli-1.14.3}/trcli/api/api_client.py +0 -0
  54. {trcli-1.14.2 → trcli-1.14.3}/trcli/api/api_response_verify.py +0 -0
  55. {trcli-1.14.2 → trcli-1.14.3}/trcli/api/api_utils.py +0 -0
  56. {trcli-1.14.2 → trcli-1.14.3}/trcli/api/bdd_handler.py +0 -0
  57. {trcli-1.14.2 → trcli-1.14.3}/trcli/api/case_handler.py +0 -0
  58. {trcli-1.14.2 → trcli-1.14.3}/trcli/api/case_matcher.py +0 -0
  59. {trcli-1.14.2 → trcli-1.14.3}/trcli/api/label_manager.py +0 -0
  60. {trcli-1.14.2 → trcli-1.14.3}/trcli/api/multisuite_uploader.py +0 -0
  61. {trcli-1.14.2 → trcli-1.14.3}/trcli/api/project_based_client.py +0 -0
  62. {trcli-1.14.2 → trcli-1.14.3}/trcli/api/reference_manager.py +0 -0
  63. {trcli-1.14.2 → trcli-1.14.3}/trcli/api/results_uploader.py +0 -0
  64. {trcli-1.14.2 → trcli-1.14.3}/trcli/api/run_handler.py +0 -0
  65. {trcli-1.14.2 → trcli-1.14.3}/trcli/api/section_handler.py +0 -0
  66. {trcli-1.14.2 → trcli-1.14.3}/trcli/api/suite_handler.py +0 -0
  67. {trcli-1.14.2 → trcli-1.14.3}/trcli/backports.py +0 -0
  68. {trcli-1.14.2 → trcli-1.14.3}/trcli/cli.py +0 -0
  69. {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/__init__.py +0 -0
  70. {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_add_run.py +0 -0
  71. {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_export_gherkin.py +0 -0
  72. {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_import_gherkin.py +0 -0
  73. {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_labels.py +0 -0
  74. {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_parse_cucumber.py +0 -0
  75. {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_parse_junit.py +0 -0
  76. {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_parse_openapi.py +0 -0
  77. {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_parse_robot.py +0 -0
  78. {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_references.py +0 -0
  79. {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/cmd_update.py +0 -0
  80. {trcli-1.14.2 → trcli-1.14.3}/trcli/commands/results_parser_helpers.py +0 -0
  81. {trcli-1.14.2 → trcli-1.14.3}/trcli/constants.py +0 -0
  82. {trcli-1.14.2 → trcli-1.14.3}/trcli/data_classes/__init__.py +0 -0
  83. {trcli-1.14.2 → trcli-1.14.3}/trcli/data_classes/data_parsers.py +0 -0
  84. {trcli-1.14.2 → trcli-1.14.3}/trcli/data_classes/dataclass_testrail.py +0 -0
  85. {trcli-1.14.2 → trcli-1.14.3}/trcli/data_classes/quality_rating_parser.py +0 -0
  86. {trcli-1.14.2 → trcli-1.14.3}/trcli/data_classes/validation_exception.py +0 -0
  87. {trcli-1.14.2 → trcli-1.14.3}/trcli/data_providers/api_data_provider.py +0 -0
  88. {trcli-1.14.2 → trcli-1.14.3}/trcli/logging/__init__.py +0 -0
  89. {trcli-1.14.2 → trcli-1.14.3}/trcli/logging/config.py +0 -0
  90. {trcli-1.14.2 → trcli-1.14.3}/trcli/logging/file_handler.py +0 -0
  91. {trcli-1.14.2 → trcli-1.14.3}/trcli/logging/structured_logger.py +0 -0
  92. {trcli-1.14.2 → trcli-1.14.3}/trcli/readers/__init__.py +0 -0
  93. {trcli-1.14.2 → trcli-1.14.3}/trcli/readers/cucumber_json.py +0 -0
  94. {trcli-1.14.2 → trcli-1.14.3}/trcli/readers/file_parser.py +0 -0
  95. {trcli-1.14.2 → trcli-1.14.3}/trcli/readers/junit_xml.py +0 -0
  96. {trcli-1.14.2 → trcli-1.14.3}/trcli/readers/openapi_yml.py +0 -0
  97. {trcli-1.14.2 → trcli-1.14.3}/trcli/readers/robot_xml.py +0 -0
  98. {trcli-1.14.2 → trcli-1.14.3}/trcli/settings.py +0 -0
  99. {trcli-1.14.2 → trcli-1.14.3}/trcli/version_checker.py +0 -0
  100. {trcli-1.14.2 → trcli-1.14.3}/trcli.egg-info/dependency_links.txt +0 -0
  101. {trcli-1.14.2 → trcli-1.14.3}/trcli.egg-info/entry_points.txt +0 -0
  102. {trcli-1.14.2 → trcli-1.14.3}/trcli.egg-info/requires.txt +0 -0
  103. {trcli-1.14.2 → trcli-1.14.3}/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.14.2
3
+ Version: 1.14.3
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.14.2
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.2
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.2
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.2
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
- api_request_handler.upload_attachments(report_results, request_id_to_result_id)
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
- api_request_handler.upload_attachments(report_results, request_id_to_result_id)
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
- api_request_handler.upload_attachments(report_results, request_id_to_result_id)
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
- api_request_handler.upload_attachments(report_results, request_id_to_result_id)
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
- api_request_handler.upload_attachments(report_results, request_id_to_result_id)
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(self, report_results: List[Dict], request_id_to_result_id: Dict[int, int]):
290
- return self.result_handler.upload_attachments(report_results, request_id_to_result_id)
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]: