alita-sdk 0.3.428b2__py3-none-any.whl → 0.3.428.post1__py3-none-any.whl
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.
- alita_sdk/runtime/clients/client.py +31 -12
- alita_sdk/tools/qtest/api_wrapper.py +1168 -117
- {alita_sdk-0.3.428b2.dist-info → alita_sdk-0.3.428.post1.dist-info}/METADATA +1 -1
- {alita_sdk-0.3.428b2.dist-info → alita_sdk-0.3.428.post1.dist-info}/RECORD +7 -7
- {alita_sdk-0.3.428b2.dist-info → alita_sdk-0.3.428.post1.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.428b2.dist-info → alita_sdk-0.3.428.post1.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.428b2.dist-info → alita_sdk-0.3.428.post1.dist-info}/top_level.txt +0 -0
|
@@ -49,6 +49,11 @@ Steps: Array of test steps with Description and Expected Result.
|
|
|
49
49
|
- Single value: "Team": "Epam"
|
|
50
50
|
- Multiple values: "Team": ["Epam", "EJ"]
|
|
51
51
|
|
|
52
|
+
**Clearing/Unsetting fields**: To clear a field value (unassign, set to empty/blank):
|
|
53
|
+
- Use `null` in JSON: "Priority": null
|
|
54
|
+
- Works for multi-select fields, user assignments, etc. (Note: single-select dropdowns have API limitations)
|
|
55
|
+
- Example: {{"QTest Id": "4626964", "Assigned To": null, "Review status": null}}
|
|
56
|
+
|
|
52
57
|
**For Updates**: Include only the fields you want to modify. The system will validate property values against project configuration.
|
|
53
58
|
|
|
54
59
|
### EXAMPLE
|
|
@@ -73,6 +78,73 @@ Steps: Array of test steps with Description and Expected Result.
|
|
|
73
78
|
Json object
|
|
74
79
|
"""
|
|
75
80
|
|
|
81
|
+
# DQL Syntax Documentation - reusable across all DQL-based search tools
|
|
82
|
+
DQL_SYNTAX_DOCS = """
|
|
83
|
+
CRITICAL: USE SINGLE QUOTES ONLY - DQL does not support double quotes!
|
|
84
|
+
- ✓ CORRECT: Description ~ 'Forgot Password'
|
|
85
|
+
- ✗ WRONG: Description ~ "Forgot Password"
|
|
86
|
+
|
|
87
|
+
LIMITATION - CANNOT SEARCH BY LINKED OBJECTS:
|
|
88
|
+
- ✗ Searching by linked requirements, test cases, defects is NOT supported
|
|
89
|
+
- Use dedicated find_*_by_*_id tools for relationship queries
|
|
90
|
+
|
|
91
|
+
SEARCHABLE FIELDS:
|
|
92
|
+
- Direct fields: Id, Name, Description, Status, Type, Priority, etc.
|
|
93
|
+
- Custom fields: Use exact field name from project configuration
|
|
94
|
+
- Date fields: MUST use ISO DateTime format (e.g., '2024-01-01T00:00:00.000Z')
|
|
95
|
+
|
|
96
|
+
ENTITY-SPECIFIC NOTES:
|
|
97
|
+
- test-logs: Only support 'Execution Start Date' and 'Execution End Date' queries
|
|
98
|
+
- builds/test-cycles: Also support 'Created Date' and 'Last Modified Date'
|
|
99
|
+
- defects: Can use 'Affected Release/Build' and 'Fixed Release/Build'
|
|
100
|
+
|
|
101
|
+
SYNTAX RULES:
|
|
102
|
+
1. ALL string values MUST use single quotes (never double quotes)
|
|
103
|
+
2. Field names with spaces MUST be in single quotes: 'Created Date' > '2024-01-01T00:00:00.000Z'
|
|
104
|
+
3. Use ~ for 'contains', !~ for 'not contains': Description ~ 'login'
|
|
105
|
+
4. Use 'is not empty' for non-empty check: Name is 'not empty'
|
|
106
|
+
5. Operators: =, !=, <, >, <=, >=, in, ~, !~
|
|
107
|
+
|
|
108
|
+
EXAMPLES:
|
|
109
|
+
- Id = 'TC-123' or Id = 'RQ-15' or Id = 'DF-100' (depending on entity type)
|
|
110
|
+
- Description ~ 'Forgot Password'
|
|
111
|
+
- Status = 'New' and Priority = 'High'
|
|
112
|
+
- Name ~ 'login'
|
|
113
|
+
- 'Created Date' > '2024-01-01T00:00:00.000Z'
|
|
114
|
+
- 'Execution Start Date' > '2024-01-01T00:00:00.000Z' (for test-logs)
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
# Supported object types for DQL search (based on QTest Search API documentation)
|
|
118
|
+
# Note: Prefixes are configurable per-project but these are standard defaults
|
|
119
|
+
# Modules (MD) are NOT searchable via DQL - use get_modules tool instead
|
|
120
|
+
# Test-logs have NO prefix - they are internal records accessed via test runs
|
|
121
|
+
|
|
122
|
+
# Entity types with ID prefixes (can be looked up by ID like TC-123)
|
|
123
|
+
QTEST_OBJECT_TYPES = {
|
|
124
|
+
# Core test management entities
|
|
125
|
+
'test-cases': {'prefix': 'TC', 'name': 'Test Case', 'description': 'Test case definitions with steps'},
|
|
126
|
+
'test-runs': {'prefix': 'TR', 'name': 'Test Run', 'description': 'Execution instances of test cases'},
|
|
127
|
+
'defects': {'prefix': 'DF', 'name': 'Defect', 'description': 'Bugs/issues found during testing'},
|
|
128
|
+
'requirements': {'prefix': 'RQ', 'name': 'Requirement', 'description': 'Requirements to be tested'},
|
|
129
|
+
|
|
130
|
+
# Test organization entities
|
|
131
|
+
'test-suites': {'prefix': 'TS', 'name': 'Test Suite', 'description': 'Collections of test runs'},
|
|
132
|
+
'test-cycles': {'prefix': 'CL', 'name': 'Test Cycle', 'description': 'Test execution cycles'},
|
|
133
|
+
|
|
134
|
+
# Release management entities
|
|
135
|
+
'releases': {'prefix': 'RL', 'name': 'Release', 'description': 'Software releases'},
|
|
136
|
+
'builds': {'prefix': 'BL', 'name': 'Build', 'description': 'Builds within releases'},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Entity types searchable via DQL but without ID prefixes
|
|
140
|
+
# These can be searched by specific fields only, not by ID
|
|
141
|
+
QTEST_SEARCHABLE_ONLY_TYPES = {
|
|
142
|
+
'test-logs': {
|
|
143
|
+
'name': 'Test Log',
|
|
144
|
+
'description': "Execution logs. Only date queries supported (Execution Start Date, Execution End Date). For specific log details, use test run's 'Latest Test Log' field."
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
|
|
76
148
|
logger = logging.getLogger(__name__)
|
|
77
149
|
|
|
78
150
|
QtestDataQuerySearch = create_model(
|
|
@@ -143,6 +215,40 @@ GetAllTestCasesFieldsForProject = create_model(
|
|
|
143
215
|
default=False)),
|
|
144
216
|
)
|
|
145
217
|
|
|
218
|
+
FindTestCasesByRequirementId = create_model(
|
|
219
|
+
"FindTestCasesByRequirementId",
|
|
220
|
+
requirement_id=(str, Field(description="QTest requirement ID in format RQ-123. This will find all test cases linked to this requirement.")),
|
|
221
|
+
include_details=(Optional[bool], Field(description="If true, returns full test case details. If false (default), returns Id, QTest Id, Name, and Description fields.", default=False)),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
FindRequirementsByTestCaseId = create_model(
|
|
225
|
+
"FindRequirementsByTestCaseId",
|
|
226
|
+
test_case_id=(str, Field(description="Test case ID in format TC-123. This will find all requirements linked to this test case.")),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
FindTestRunsByTestCaseId = create_model(
|
|
230
|
+
"FindTestRunsByTestCaseId",
|
|
231
|
+
test_case_id=(str, Field(description="Test case ID in format TC-123. This will find all test runs associated with this test case.")),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
FindDefectsByTestRunId = create_model(
|
|
235
|
+
"FindDefectsByTestRunId",
|
|
236
|
+
test_run_id=(str, Field(description="Test run ID in format TR-123. This will find all defects associated with this test run.")),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Generic search model for any entity type
|
|
240
|
+
GenericDqlSearch = create_model(
|
|
241
|
+
"GenericDqlSearch",
|
|
242
|
+
object_type=(str, Field(description="Entity type to search: 'test-cases', 'test-runs', 'defects', 'requirements', 'test-suites', 'test-cycles', 'test-logs', 'releases', or 'builds'. Note: test-logs only support date queries; modules are NOT searchable - use get_modules tool.")),
|
|
243
|
+
dql=(str, Field(description="QTest Data Query Language (DQL) query string")),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Generic find by ID model - only for entities with ID prefixes (NOT test-logs)
|
|
247
|
+
FindEntityById = create_model(
|
|
248
|
+
"FindEntityById",
|
|
249
|
+
entity_id=(str, Field(description="Entity ID with prefix: TC-123 (test case), RQ-15 (requirement), DF-100 (defect), TR-39 (test run), TS-5 (test suite), CL-3 (test cycle), RL-1 (release), or BL-2 (build). Note: test-logs and modules do NOT have ID prefixes.")),
|
|
250
|
+
)
|
|
251
|
+
|
|
146
252
|
NoInput = create_model(
|
|
147
253
|
"NoInput"
|
|
148
254
|
)
|
|
@@ -156,6 +262,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
156
262
|
no_of_tests_shown_in_dql_search: int = 10
|
|
157
263
|
_client: Any = PrivateAttr()
|
|
158
264
|
_field_definitions_cache: Optional[dict] = PrivateAttr(default=None)
|
|
265
|
+
_modules_cache: Optional[list] = PrivateAttr(default=None)
|
|
159
266
|
llm: Any
|
|
160
267
|
|
|
161
268
|
@model_validator(mode='before')
|
|
@@ -195,11 +302,11 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
195
302
|
|
|
196
303
|
def __get_field_definitions_cached(self) -> dict:
|
|
197
304
|
"""Get field definitions with session-level caching.
|
|
198
|
-
|
|
305
|
+
|
|
199
306
|
Field definitions are cached for the lifetime of this wrapper instance.
|
|
200
307
|
If project field configuration changes, call refresh_field_definitions_cache()
|
|
201
308
|
to reload the definitions.
|
|
202
|
-
|
|
309
|
+
|
|
203
310
|
Returns:
|
|
204
311
|
dict: Field definitions mapping
|
|
205
312
|
"""
|
|
@@ -209,11 +316,11 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
209
316
|
|
|
210
317
|
def refresh_field_definitions_cache(self) -> dict:
|
|
211
318
|
"""Manually refresh the field definitions cache.
|
|
212
|
-
|
|
319
|
+
|
|
213
320
|
Call this method if project field configuration has been updated
|
|
214
321
|
(new fields added, dropdown values changed, etc.) and you need to
|
|
215
322
|
reload the definitions within the same agent session.
|
|
216
|
-
|
|
323
|
+
|
|
217
324
|
Returns:
|
|
218
325
|
dict: Freshly loaded field definitions
|
|
219
326
|
"""
|
|
@@ -224,15 +331,15 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
224
331
|
base_properties: list = None) -> list:
|
|
225
332
|
"""
|
|
226
333
|
Convert user-friendly property names/values to QTest API PropertyResource format.
|
|
227
|
-
|
|
334
|
+
|
|
228
335
|
Args:
|
|
229
336
|
test_case_data: Dict with property names as keys (e.g., {"Status": "New", "Priority": "High"})
|
|
230
337
|
field_definitions: Output from __get_project_field_definitions()
|
|
231
338
|
base_properties: Existing properties from a test case (for updates, optional)
|
|
232
|
-
|
|
339
|
+
|
|
233
340
|
Returns:
|
|
234
341
|
list[PropertyResource]: Properties ready for API submission
|
|
235
|
-
|
|
342
|
+
|
|
236
343
|
Raises:
|
|
237
344
|
ValueError: If any field names are unknown or values are invalid (shows ALL errors)
|
|
238
345
|
"""
|
|
@@ -248,32 +355,73 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
248
355
|
'field_value': prop['field_value'],
|
|
249
356
|
'field_value_name': prop.get('field_value_name')
|
|
250
357
|
}
|
|
251
|
-
|
|
358
|
+
|
|
252
359
|
# Collect ALL validation errors before raising
|
|
253
360
|
validation_errors = []
|
|
254
|
-
|
|
361
|
+
|
|
255
362
|
# Map incoming properties from test_case_data
|
|
256
363
|
for field_name, field_value in test_case_data.items():
|
|
257
364
|
# Skip non-property fields (these are handled separately)
|
|
258
365
|
if field_name in ['Name', 'Description', 'Precondition', 'Steps', 'Id', QTEST_ID]:
|
|
259
366
|
continue
|
|
260
367
|
|
|
261
|
-
# Skip
|
|
262
|
-
if field_value
|
|
368
|
+
# Skip empty string values (don't update these fields)
|
|
369
|
+
if field_value == '':
|
|
263
370
|
continue
|
|
264
371
|
|
|
372
|
+
# Handle None value - this means "clear/unset this field"
|
|
373
|
+
if field_value is None:
|
|
374
|
+
# Validate field exists before attempting to clear
|
|
375
|
+
if field_name not in field_definitions:
|
|
376
|
+
validation_errors.append(
|
|
377
|
+
f"❌ Unknown field '{field_name}' - not defined in project configuration"
|
|
378
|
+
)
|
|
379
|
+
continue
|
|
380
|
+
|
|
381
|
+
field_def = field_definitions[field_name]
|
|
382
|
+
field_id = field_def['field_id']
|
|
383
|
+
is_multiple = field_def.get('multiple', False)
|
|
384
|
+
has_allowed_values = bool(field_def.get('values')) # True = dropdown, False = text
|
|
385
|
+
|
|
386
|
+
if not has_allowed_values:
|
|
387
|
+
# TEXT FIELD: can clear with empty string
|
|
388
|
+
props_dict[field_name] = {
|
|
389
|
+
'field_id': field_id,
|
|
390
|
+
'field_name': field_name,
|
|
391
|
+
'field_value': '',
|
|
392
|
+
'field_value_name': ''
|
|
393
|
+
}
|
|
394
|
+
elif is_multiple:
|
|
395
|
+
# MULTI-SELECT: can clear using empty array "[]"
|
|
396
|
+
props_dict[field_name] = {
|
|
397
|
+
'field_id': field_id,
|
|
398
|
+
'field_name': field_name,
|
|
399
|
+
'field_value': "[]",
|
|
400
|
+
'field_value_name': None
|
|
401
|
+
}
|
|
402
|
+
else:
|
|
403
|
+
# SINGLE-SELECT: QTest API limitation - cannot clear to empty
|
|
404
|
+
# Note: Users CAN clear these fields from UI, but API doesn't expose this capability
|
|
405
|
+
validation_errors.append(
|
|
406
|
+
f"⚠️ Cannot clear single-select field '{field_name}' - this is a QTest API limitation "
|
|
407
|
+
f"(clearing is possible from UI but not exposed via API). "
|
|
408
|
+
f"Please select an alternative value instead. "
|
|
409
|
+
f"Available values: {', '.join(field_def.get('values', {}).keys()) or 'none'}"
|
|
410
|
+
)
|
|
411
|
+
continue
|
|
412
|
+
|
|
265
413
|
# Validate field exists in project - STRICT validation
|
|
266
414
|
if field_name not in field_definitions:
|
|
267
415
|
validation_errors.append(
|
|
268
416
|
f"❌ Unknown field '{field_name}' - not defined in project configuration"
|
|
269
417
|
)
|
|
270
418
|
continue # Skip to next field, keep collecting errors
|
|
271
|
-
|
|
419
|
+
|
|
272
420
|
field_def = field_definitions[field_name]
|
|
273
421
|
field_id = field_def['field_id']
|
|
274
422
|
data_type = field_def.get('data_type')
|
|
275
423
|
is_multiple = field_def.get('multiple', False)
|
|
276
|
-
|
|
424
|
+
|
|
277
425
|
# Normalize field_value to list for consistent processing
|
|
278
426
|
# Multi-select fields can receive: "value", ["value1", "value2"], or ["value1"]
|
|
279
427
|
# Single-select fields: "value" only
|
|
@@ -283,13 +431,13 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
283
431
|
else:
|
|
284
432
|
# Single-select: keep as single value
|
|
285
433
|
values_to_process = [field_value]
|
|
286
|
-
|
|
434
|
+
|
|
287
435
|
# Validate value(s) for dropdown fields (only if field has allowed values)
|
|
288
436
|
if field_def['values']:
|
|
289
437
|
# Field has allowed values (dropdown/combobox/user fields) - validate strictly
|
|
290
438
|
value_ids = []
|
|
291
439
|
value_names = []
|
|
292
|
-
|
|
440
|
+
|
|
293
441
|
for single_value in values_to_process:
|
|
294
442
|
if single_value not in field_def['values']:
|
|
295
443
|
available = ", ".join(sorted(field_def['values'].keys()))
|
|
@@ -298,15 +446,15 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
298
446
|
f"Allowed values: {available}"
|
|
299
447
|
)
|
|
300
448
|
continue # Skip this value, but continue validating others
|
|
301
|
-
|
|
449
|
+
|
|
302
450
|
# Valid value - add to lists
|
|
303
451
|
value_ids.append(field_def['values'][single_value])
|
|
304
452
|
value_names.append(single_value)
|
|
305
|
-
|
|
453
|
+
|
|
306
454
|
# If all values were invalid, skip this field
|
|
307
455
|
if not value_ids:
|
|
308
456
|
continue
|
|
309
|
-
|
|
457
|
+
|
|
310
458
|
# Format based on field type and value count
|
|
311
459
|
if is_multiple and len(value_ids) == 1:
|
|
312
460
|
# Single value in multi-select field: bracketed string "[419950]"
|
|
@@ -327,7 +475,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
327
475
|
# No validation needed - users can write anything (by design)
|
|
328
476
|
field_value_id = field_value
|
|
329
477
|
field_value_name = field_value if isinstance(field_value, str) else None
|
|
330
|
-
|
|
478
|
+
|
|
331
479
|
# Update or add property (only if no errors for this field)
|
|
332
480
|
props_dict[field_name] = {
|
|
333
481
|
'field_id': field_id,
|
|
@@ -335,7 +483,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
335
483
|
'field_value': field_value_id,
|
|
336
484
|
'field_value_name': field_value_name
|
|
337
485
|
}
|
|
338
|
-
|
|
486
|
+
|
|
339
487
|
# If ANY validation errors found, raise comprehensive error with all issues
|
|
340
488
|
if validation_errors:
|
|
341
489
|
available_fields = ", ".join(sorted(field_definitions.keys()))
|
|
@@ -346,7 +494,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
346
494
|
f"💡 Tip: Use 'get_all_test_cases_fields_for_project' tool to see all fields with their allowed values."
|
|
347
495
|
)
|
|
348
496
|
raise ValueError(error_msg)
|
|
349
|
-
|
|
497
|
+
|
|
350
498
|
# Convert to PropertyResource list, filtering out special fields
|
|
351
499
|
result = []
|
|
352
500
|
for field_name, prop_data in props_dict.items():
|
|
@@ -358,37 +506,44 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
358
506
|
field_value=prop_data['field_value'],
|
|
359
507
|
field_value_name=prop_data.get('field_value_name')
|
|
360
508
|
))
|
|
361
|
-
|
|
509
|
+
|
|
362
510
|
return result
|
|
363
511
|
|
|
364
512
|
def __build_body_for_create_test_case(self, test_cases_data: list[dict],
|
|
365
513
|
folder_to_place_test_cases_to: str = '') -> list:
|
|
366
514
|
# Get field definitions for property mapping (cached for session)
|
|
367
515
|
field_definitions = self.__get_field_definitions_cached()
|
|
368
|
-
|
|
516
|
+
|
|
369
517
|
modules = self._parse_modules()
|
|
370
518
|
parent_id = ''.join(str(module['module_id']) for module in modules if
|
|
371
519
|
folder_to_place_test_cases_to and module['full_module_name'] == folder_to_place_test_cases_to)
|
|
372
|
-
|
|
520
|
+
|
|
373
521
|
bodies = []
|
|
374
522
|
for test_case in test_cases_data:
|
|
375
523
|
# Map properties from user format to API format
|
|
376
524
|
props = self.__map_properties_to_api_format(test_case, field_definitions)
|
|
377
|
-
|
|
525
|
+
|
|
378
526
|
body = swagger_client.TestCaseWithCustomFieldResource(properties=props)
|
|
379
|
-
|
|
380
|
-
#
|
|
381
|
-
#
|
|
527
|
+
|
|
528
|
+
# Handle core fields: Name, Description, Precondition
|
|
529
|
+
# These are set if explicitly provided in the input
|
|
530
|
+
# None or empty string means "clear this field" (except Name which is required)
|
|
382
531
|
if 'Name' in test_case:
|
|
383
|
-
|
|
532
|
+
# Name is required - use 'Untitled' as fallback if null/empty
|
|
533
|
+
name_value = test_case['Name']
|
|
534
|
+
body.name = name_value if name_value else 'Untitled'
|
|
535
|
+
|
|
384
536
|
if 'Precondition' in test_case:
|
|
385
|
-
|
|
537
|
+
# Allow clearing with None or empty string
|
|
538
|
+
body.precondition = test_case['Precondition'] if test_case['Precondition'] is not None else ''
|
|
539
|
+
|
|
386
540
|
if 'Description' in test_case:
|
|
387
|
-
|
|
388
|
-
|
|
541
|
+
# Allow clearing with None or empty string
|
|
542
|
+
body.description = test_case['Description'] if test_case['Description'] is not None else ''
|
|
543
|
+
|
|
389
544
|
if parent_id:
|
|
390
545
|
body.parent_id = parent_id
|
|
391
|
-
|
|
546
|
+
|
|
392
547
|
# Only set test_steps if Steps are provided in the input
|
|
393
548
|
# This prevents overwriting existing steps during partial updates
|
|
394
549
|
if 'Steps' in test_case and test_case['Steps'] is not None:
|
|
@@ -398,7 +553,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
398
553
|
swagger_client.TestStepResource(description=step.get('Test Step Description'),
|
|
399
554
|
expected=step.get('Test Step Expected Result')))
|
|
400
555
|
body.test_steps = test_steps_resources
|
|
401
|
-
|
|
556
|
+
|
|
402
557
|
bodies.append(body)
|
|
403
558
|
return bodies
|
|
404
559
|
|
|
@@ -418,10 +573,10 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
418
573
|
def __get_field_definitions_from_properties_api(self) -> dict:
|
|
419
574
|
"""
|
|
420
575
|
Fallback method: Get field definitions using /properties and /properties-info APIs.
|
|
421
|
-
|
|
576
|
+
|
|
422
577
|
These APIs don't require Field Management permission and are available to all users.
|
|
423
578
|
Requires 2 API calls + 1 search to get a test case ID.
|
|
424
|
-
|
|
579
|
+
|
|
425
580
|
Returns:
|
|
426
581
|
dict: Same structure as __get_project_field_definitions()
|
|
427
582
|
"""
|
|
@@ -429,7 +584,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
429
584
|
"Using properties API fallback (no Field Management permission). "
|
|
430
585
|
"This requires getting a template test case first."
|
|
431
586
|
)
|
|
432
|
-
|
|
587
|
+
|
|
433
588
|
# Step 1: Get any test case ID to query properties
|
|
434
589
|
search_instance = swagger_client.SearchApi(self._client)
|
|
435
590
|
body = swagger_client.ArtifactSearchParams(
|
|
@@ -437,7 +592,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
437
592
|
fields=['*'],
|
|
438
593
|
query='' # Empty query returns all test cases
|
|
439
594
|
)
|
|
440
|
-
|
|
595
|
+
|
|
441
596
|
try:
|
|
442
597
|
# Search for any test case - just need one
|
|
443
598
|
response = search_instance.search_artifact(
|
|
@@ -453,24 +608,24 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
453
608
|
f"Cannot find any test case to query field definitions. "
|
|
454
609
|
f"Please create at least one test case in project {self.qtest_project_id}"
|
|
455
610
|
) from e
|
|
456
|
-
|
|
611
|
+
|
|
457
612
|
if not response or not response.get('items') or len(response['items']) == 0:
|
|
458
613
|
raise ValueError(
|
|
459
614
|
f"No test cases found in project {self.qtest_project_id}. "
|
|
460
615
|
f"Please create at least one test case to retrieve field definitions."
|
|
461
616
|
)
|
|
462
|
-
|
|
617
|
+
|
|
463
618
|
test_case_id = response['items'][0]['id']
|
|
464
619
|
logger.info(f"Using test case ID {test_case_id} to retrieve field definitions")
|
|
465
|
-
|
|
620
|
+
|
|
466
621
|
# Step 2: Call /properties API
|
|
467
622
|
headers = {
|
|
468
623
|
"Authorization": f"Bearer {self.qtest_api_token.get_secret_value()}"
|
|
469
624
|
}
|
|
470
|
-
|
|
625
|
+
|
|
471
626
|
properties_url = f"{self.base_url}/api/v3/projects/{self.qtest_project_id}/test-cases/{test_case_id}/properties"
|
|
472
627
|
properties_info_url = f"{self.base_url}/api/v3/projects/{self.qtest_project_id}/test-cases/{test_case_id}/properties-info"
|
|
473
|
-
|
|
628
|
+
|
|
474
629
|
try:
|
|
475
630
|
# Get properties with current values and field metadata
|
|
476
631
|
props_response = requests.get(
|
|
@@ -480,12 +635,12 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
480
635
|
)
|
|
481
636
|
props_response.raise_for_status()
|
|
482
637
|
properties_data = props_response.json()
|
|
483
|
-
|
|
638
|
+
|
|
484
639
|
# Get properties-info with data types and allowed values
|
|
485
640
|
info_response = requests.get(properties_info_url, headers=headers)
|
|
486
641
|
info_response.raise_for_status()
|
|
487
642
|
info_data = info_response.json()
|
|
488
|
-
|
|
643
|
+
|
|
489
644
|
except requests.exceptions.RequestException as e:
|
|
490
645
|
stacktrace = format_exc()
|
|
491
646
|
logger.error(f"Failed to call properties API: {stacktrace}")
|
|
@@ -493,42 +648,42 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
493
648
|
f"Unable to retrieve field definitions using properties API. "
|
|
494
649
|
f"Error: {stacktrace}"
|
|
495
650
|
) from e
|
|
496
|
-
|
|
651
|
+
|
|
497
652
|
# Step 3: Build field mapping by merging both responses
|
|
498
653
|
field_mapping = {}
|
|
499
|
-
|
|
654
|
+
|
|
500
655
|
# Create lookup by field ID from properties-info
|
|
501
656
|
metadata_by_id = {item['id']: item for item in info_data['metadata']}
|
|
502
|
-
|
|
657
|
+
|
|
503
658
|
# Data type mapping to determine 'multiple' flag
|
|
504
659
|
MULTI_SELECT_TYPES = {
|
|
505
660
|
'UserListDataType',
|
|
506
661
|
'MultiSelectionDataType',
|
|
507
662
|
'CheckListDataType'
|
|
508
663
|
}
|
|
509
|
-
|
|
664
|
+
|
|
510
665
|
USER_FIELD_TYPES = {'UserListDataType'}
|
|
511
|
-
|
|
666
|
+
|
|
512
667
|
# System fields to exclude (same as in property mapping)
|
|
513
668
|
excluded_fields = {'Shared', 'Projects Shared to'}
|
|
514
|
-
|
|
669
|
+
|
|
515
670
|
for prop in properties_data:
|
|
516
671
|
field_name = prop.get('name')
|
|
517
672
|
field_id = prop.get('id')
|
|
518
|
-
|
|
673
|
+
|
|
519
674
|
if not field_name or field_name in excluded_fields:
|
|
520
675
|
continue
|
|
521
|
-
|
|
676
|
+
|
|
522
677
|
# Get metadata for this field
|
|
523
678
|
metadata = metadata_by_id.get(field_id, {})
|
|
524
679
|
data_type_str = metadata.get('data_type')
|
|
525
|
-
|
|
680
|
+
|
|
526
681
|
# Determine data_type number (5 for user fields, None for others)
|
|
527
682
|
data_type = 5 if data_type_str in USER_FIELD_TYPES else None
|
|
528
|
-
|
|
683
|
+
|
|
529
684
|
# Determine if multi-select
|
|
530
685
|
is_multiple = data_type_str in MULTI_SELECT_TYPES
|
|
531
|
-
|
|
686
|
+
|
|
532
687
|
field_mapping[field_name] = {
|
|
533
688
|
'field_id': field_id,
|
|
534
689
|
'required': prop.get('required', False),
|
|
@@ -536,7 +691,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
536
691
|
'multiple': is_multiple,
|
|
537
692
|
'values': {}
|
|
538
693
|
}
|
|
539
|
-
|
|
694
|
+
|
|
540
695
|
# Map allowed values from metadata
|
|
541
696
|
allowed_values = metadata.get('allowed_values', [])
|
|
542
697
|
for allowed_val in allowed_values:
|
|
@@ -544,18 +699,18 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
544
699
|
value_id = allowed_val.get('id')
|
|
545
700
|
if value_text and value_id:
|
|
546
701
|
field_mapping[field_name]['values'][value_text] = value_id
|
|
547
|
-
|
|
702
|
+
|
|
548
703
|
logger.info(
|
|
549
704
|
f"Retrieved {len(field_mapping)} field definitions using properties API. "
|
|
550
705
|
f"This method works for all users without Field Management permission."
|
|
551
706
|
)
|
|
552
|
-
|
|
707
|
+
|
|
553
708
|
return field_mapping
|
|
554
709
|
|
|
555
710
|
def __get_project_field_definitions(self) -> dict:
|
|
556
711
|
"""
|
|
557
712
|
Get structured field definitions for test cases in the project.
|
|
558
|
-
|
|
713
|
+
|
|
559
714
|
Returns:
|
|
560
715
|
dict: Mapping of field names to their IDs and allowed values.
|
|
561
716
|
Example: {
|
|
@@ -573,7 +728,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
573
728
|
"""
|
|
574
729
|
fields_api = self.__instantiate_fields_api_instance()
|
|
575
730
|
qtest_object = 'test-cases'
|
|
576
|
-
|
|
731
|
+
|
|
577
732
|
try:
|
|
578
733
|
fields = fields_api.get_fields(self.qtest_project_id, qtest_object)
|
|
579
734
|
except ApiException as e:
|
|
@@ -584,13 +739,13 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
584
739
|
"Using properties API fallback..."
|
|
585
740
|
)
|
|
586
741
|
return self.__get_field_definitions_from_properties_api()
|
|
587
|
-
|
|
742
|
+
|
|
588
743
|
# Other API errors
|
|
589
744
|
stacktrace = format_exc()
|
|
590
745
|
logger.error(f"Exception when calling FieldAPI->get_fields:\n {stacktrace}")
|
|
591
746
|
raise ValueError(
|
|
592
747
|
f"Unable to get test case fields for project {self.qtest_project_id}. Exception: \n {stacktrace}")
|
|
593
|
-
|
|
748
|
+
|
|
594
749
|
# Build structured mapping
|
|
595
750
|
field_mapping = {}
|
|
596
751
|
for field in fields:
|
|
@@ -602,7 +757,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
602
757
|
'multiple': getattr(field, 'multiple', False), # True = multi-select, needs array format
|
|
603
758
|
'values': {}
|
|
604
759
|
}
|
|
605
|
-
|
|
760
|
+
|
|
606
761
|
# Map allowed values if field has them (dropdown/combobox/user fields)
|
|
607
762
|
# Only include active values (is_active=True)
|
|
608
763
|
if hasattr(field, 'allowed_values') and field.allowed_values:
|
|
@@ -610,51 +765,65 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
610
765
|
# Skip inactive values (deleted/deprecated options)
|
|
611
766
|
if hasattr(allowed_value, 'is_active') and not allowed_value.is_active:
|
|
612
767
|
continue
|
|
613
|
-
|
|
768
|
+
|
|
614
769
|
# AllowedValueResource has 'label' for the display name and 'value' for the ID
|
|
615
770
|
# Note: 'value' is the field_value, not 'id'
|
|
616
771
|
# For user fields (data_type=5), label is user name and value is user ID
|
|
617
772
|
value_label = allowed_value.label
|
|
618
773
|
value_id = allowed_value.value
|
|
619
774
|
field_mapping[field_name]['values'][value_label] = value_id
|
|
620
|
-
|
|
775
|
+
|
|
621
776
|
return field_mapping
|
|
622
777
|
|
|
623
778
|
def __format_field_info_for_display(self, field_definitions: dict) -> str:
|
|
624
779
|
"""
|
|
625
780
|
Format field definitions in human-readable format for LLM.
|
|
626
|
-
|
|
781
|
+
|
|
627
782
|
Args:
|
|
628
783
|
field_definitions: Output from __get_project_field_definitions()
|
|
629
|
-
|
|
784
|
+
|
|
630
785
|
Returns:
|
|
631
786
|
Formatted string with field information
|
|
632
787
|
"""
|
|
633
788
|
output = [f"Available Test Case Fields for Project {self.qtest_project_id}:\n"]
|
|
634
|
-
|
|
789
|
+
|
|
635
790
|
for field_name, field_info in sorted(field_definitions.items()):
|
|
636
791
|
required_marker = " (Required)" if field_info.get('required') else ""
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
792
|
+
has_values = bool(field_info.get('values'))
|
|
793
|
+
is_multiple = field_info.get('multiple', False)
|
|
794
|
+
|
|
795
|
+
# Determine field type label
|
|
796
|
+
if not has_values:
|
|
797
|
+
type_label = "Text"
|
|
798
|
+
elif is_multiple:
|
|
799
|
+
type_label = "Multi-select"
|
|
800
|
+
else:
|
|
801
|
+
type_label = "Single-select"
|
|
802
|
+
|
|
803
|
+
output.append(f"\n{field_name} ({type_label}{required_marker}):")
|
|
804
|
+
|
|
805
|
+
if has_values:
|
|
640
806
|
for value_name, value_id in sorted(field_info['values'].items()):
|
|
641
|
-
output.append(f" - {value_name}
|
|
807
|
+
output.append(f" - {value_name}")
|
|
642
808
|
else:
|
|
643
|
-
output.append("
|
|
644
|
-
|
|
645
|
-
output.append("\n\
|
|
646
|
-
|
|
809
|
+
output.append(" Free text input. Set to null to clear.")
|
|
810
|
+
|
|
811
|
+
output.append("\n\n--- Field Type Guide ---")
|
|
812
|
+
output.append("\nText fields: Use null to clear, provide string value to set.")
|
|
813
|
+
output.append("\nSingle-select: Provide exact value name from the list above. Cannot be cleared via API.")
|
|
814
|
+
output.append("\nMulti-select: Provide value as array [\"val1\", \"val2\"]. Use null to clear.")
|
|
815
|
+
return '\n'.join(output)
|
|
647
816
|
|
|
648
817
|
def get_all_test_cases_fields_for_project(self, force_refresh: bool = False) -> str:
|
|
649
818
|
"""
|
|
650
819
|
Get formatted information about available test case fields and their values.
|
|
651
820
|
This method is exposed as a tool for LLM to query field information.
|
|
652
|
-
|
|
821
|
+
|
|
653
822
|
Args:
|
|
654
823
|
force_refresh: If True, reload field definitions from API instead of using cache.
|
|
655
824
|
Use this if project configuration has changed (new fields added,
|
|
656
825
|
dropdown values modified, etc.).
|
|
657
|
-
|
|
826
|
+
|
|
658
827
|
Returns:
|
|
659
828
|
Formatted string with field names and allowed values
|
|
660
829
|
"""
|
|
@@ -664,6 +833,10 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
664
833
|
return self.__format_field_info_for_display(field_defs)
|
|
665
834
|
|
|
666
835
|
def _parse_modules(self) -> list[dict]:
|
|
836
|
+
"""Get parsed modules list with caching for the session."""
|
|
837
|
+
if self._modules_cache is not None:
|
|
838
|
+
return self._modules_cache
|
|
839
|
+
|
|
667
840
|
modules = self.__get_all_modules_for_project()
|
|
668
841
|
result = []
|
|
669
842
|
|
|
@@ -684,6 +857,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
684
857
|
for module in modules:
|
|
685
858
|
parse_module(module)
|
|
686
859
|
|
|
860
|
+
self._modules_cache = result
|
|
687
861
|
return result
|
|
688
862
|
|
|
689
863
|
def __execute_single_create_test_case_request(self, test_case_api_instance: TestCaseApi, body,
|
|
@@ -700,12 +874,43 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
700
874
|
raise ToolException(
|
|
701
875
|
f"Unable to create test case in project - {self.qtest_project_id} with the following content:\n{test_case_content}.\n\n Stacktrace was {stacktrace}") from e
|
|
702
876
|
|
|
877
|
+
def __format_property_value(self, prop: dict) -> Any:
|
|
878
|
+
"""Format property value for display, detecting field type from response structure.
|
|
879
|
+
|
|
880
|
+
Detection rules based on API response patterns:
|
|
881
|
+
- Text field: field_value_name is empty/None
|
|
882
|
+
- Multi-select: field_value_name starts with '[' and ends with ']'
|
|
883
|
+
- Single-select: field_value_name is plain text (no brackets)
|
|
884
|
+
|
|
885
|
+
Args:
|
|
886
|
+
prop: Property dict from API response with field_value and field_value_name
|
|
887
|
+
|
|
888
|
+
Returns:
|
|
889
|
+
Formatted value: list for multi-select, string for others
|
|
890
|
+
"""
|
|
891
|
+
field_value = prop.get('field_value') or ''
|
|
892
|
+
field_value_name = prop.get('field_value_name')
|
|
893
|
+
|
|
894
|
+
# Text field: no field_value_name, use field_value directly
|
|
895
|
+
if not field_value_name:
|
|
896
|
+
return field_value
|
|
897
|
+
|
|
898
|
+
# Multi-select: field_value_name is bracketed like '[value1, value2]'
|
|
899
|
+
if isinstance(field_value_name, str) and field_value_name.startswith('[') and field_value_name.endswith(']'):
|
|
900
|
+
inner = field_value_name[1:-1].strip() # Remove brackets
|
|
901
|
+
if inner:
|
|
902
|
+
return [v.strip() for v in inner.split(',')]
|
|
903
|
+
return [] # Empty multi-select
|
|
904
|
+
|
|
905
|
+
# Single-select: plain text value
|
|
906
|
+
return field_value_name
|
|
907
|
+
|
|
703
908
|
def __parse_data(self, response_to_parse: dict, parsed_data: list, extract_images: bool=False, prompt: str=None):
|
|
704
909
|
import html
|
|
705
|
-
|
|
910
|
+
|
|
706
911
|
# PERMISSION-FREE: Parse properties directly from API response
|
|
707
912
|
# No get_fields call needed - works for all users
|
|
708
|
-
|
|
913
|
+
|
|
709
914
|
for item in response_to_parse['items']:
|
|
710
915
|
# Start with core fields (always present)
|
|
711
916
|
parsed_data_row = {
|
|
@@ -720,17 +925,16 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
720
925
|
'Test Step Expected Result': self._process_image(step[1]['expected'], extract_images, prompt)
|
|
721
926
|
}, enumerate(item['test_steps']))),
|
|
722
927
|
}
|
|
723
|
-
|
|
928
|
+
|
|
724
929
|
# Add custom fields directly from API response properties
|
|
725
930
|
for prop in item['properties']:
|
|
726
931
|
field_name = prop.get('field_name')
|
|
727
932
|
if not field_name:
|
|
728
933
|
continue
|
|
729
|
-
|
|
730
|
-
#
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
934
|
+
|
|
935
|
+
# Format value based on field type (multi-select as array, etc.)
|
|
936
|
+
parsed_data_row[field_name] = self.__format_property_value(prop)
|
|
937
|
+
|
|
734
938
|
parsed_data.append(parsed_data_row)
|
|
735
939
|
|
|
736
940
|
def _process_image(self, content: str, extract: bool=False, prompt: str=None):
|
|
@@ -788,38 +992,139 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
788
992
|
parsed_data = self.__perform_search_by_dql(dql)
|
|
789
993
|
return parsed_data[0]['QTest Id']
|
|
790
994
|
|
|
791
|
-
def
|
|
792
|
-
"""
|
|
793
|
-
|
|
995
|
+
def __find_qtest_internal_id(self, object_type: str, entity_id: str) -> int:
|
|
996
|
+
"""Generic search for an entity's internal QTest ID using its external ID (e.g., TR-xxx, DF-xxx, RQ-xxx).
|
|
997
|
+
|
|
998
|
+
This is the unified method for looking up internal IDs. Use this instead of
|
|
999
|
+
the entity-specific methods (__find_qtest_requirement_id_by_id, etc.).
|
|
1000
|
+
|
|
794
1001
|
Args:
|
|
795
|
-
|
|
796
|
-
|
|
1002
|
+
object_type: QTest object type ('test-runs', 'defects', 'requirements', etc.)
|
|
1003
|
+
entity_id: Entity ID in format TR-123, DF-456, etc.
|
|
1004
|
+
|
|
797
1005
|
Returns:
|
|
798
|
-
int: Internal QTest ID for the
|
|
799
|
-
|
|
1006
|
+
int: Internal QTest ID for the entity
|
|
1007
|
+
|
|
800
1008
|
Raises:
|
|
801
|
-
ValueError: If
|
|
1009
|
+
ValueError: If entity is not found
|
|
802
1010
|
"""
|
|
803
|
-
dql = f"Id = '{
|
|
1011
|
+
dql = f"Id = '{entity_id}'"
|
|
804
1012
|
search_instance: SearchApi = swagger_client.SearchApi(self._client)
|
|
805
|
-
body = swagger_client.ArtifactSearchParams(object_type=
|
|
806
|
-
|
|
1013
|
+
body = swagger_client.ArtifactSearchParams(object_type=object_type, fields=['*'], query=dql)
|
|
1014
|
+
|
|
807
1015
|
try:
|
|
808
1016
|
response = search_instance.search_artifact(self.qtest_project_id, body)
|
|
809
1017
|
if response['total'] == 0:
|
|
810
1018
|
raise ValueError(
|
|
811
|
-
f"
|
|
812
|
-
f"Please verify the
|
|
1019
|
+
f"{object_type.capitalize()} '{entity_id}' not found in project {self.qtest_project_id}. "
|
|
1020
|
+
f"Please verify the {entity_id} ID exists."
|
|
813
1021
|
)
|
|
814
1022
|
return response['items'][0]['id']
|
|
815
1023
|
except ApiException as e:
|
|
816
1024
|
stacktrace = format_exc()
|
|
817
|
-
logger.error(f"Exception when searching for
|
|
1025
|
+
logger.error(f"Exception when searching for '{object_type}': '{entity_id}': \n {stacktrace}")
|
|
818
1026
|
raise ToolException(
|
|
819
|
-
f"Unable to search for
|
|
1027
|
+
f"Unable to search for {object_type} '{entity_id}' in project {self.qtest_project_id}. "
|
|
820
1028
|
f"Exception: \n{stacktrace}"
|
|
821
1029
|
) from e
|
|
822
1030
|
|
|
1031
|
+
def __find_qtest_requirement_id_by_id(self, requirement_id: str) -> int:
|
|
1032
|
+
"""Search for requirement's internal QTest ID using requirement ID (RQ-xxx format).
|
|
1033
|
+
|
|
1034
|
+
Args:
|
|
1035
|
+
requirement_id: Requirement ID in format RQ-123
|
|
1036
|
+
|
|
1037
|
+
Returns:
|
|
1038
|
+
int: Internal QTest ID for the requirement
|
|
1039
|
+
|
|
1040
|
+
Raises:
|
|
1041
|
+
ValueError: If requirement is not found
|
|
1042
|
+
"""
|
|
1043
|
+
return self.__find_qtest_internal_id('requirements', requirement_id)
|
|
1044
|
+
|
|
1045
|
+
def __find_qtest_defect_id_by_id(self, defect_id: str) -> int:
|
|
1046
|
+
"""Search for defect's internal QTest ID using defect ID (DF-xxx format).
|
|
1047
|
+
|
|
1048
|
+
Args:
|
|
1049
|
+
defect_id: Defect ID in format DF-123
|
|
1050
|
+
|
|
1051
|
+
Returns:
|
|
1052
|
+
int: Internal QTest ID for the defect
|
|
1053
|
+
|
|
1054
|
+
Raises:
|
|
1055
|
+
ValueError: If defect is not found
|
|
1056
|
+
"""
|
|
1057
|
+
return self.__find_qtest_internal_id('defects', defect_id)
|
|
1058
|
+
|
|
1059
|
+
def __search_entity_by_id(self, object_type: str, entity_id: str) -> dict:
|
|
1060
|
+
"""Generic search for any entity by its ID (RQ-xxx, DF-xxx, etc.).
|
|
1061
|
+
|
|
1062
|
+
Uses the unified __parse_entity_item method for consistent parsing.
|
|
1063
|
+
|
|
1064
|
+
Args:
|
|
1065
|
+
object_type: QTest object type ('requirements', 'defects', etc.)
|
|
1066
|
+
entity_id: Entity ID in format prefix-number (RQ-123, DF-456)
|
|
1067
|
+
|
|
1068
|
+
Returns:
|
|
1069
|
+
dict: Entity data with all parsed fields, or None if not found
|
|
1070
|
+
"""
|
|
1071
|
+
dql = f"Id = '{entity_id}'"
|
|
1072
|
+
search_instance: SearchApi = swagger_client.SearchApi(self._client)
|
|
1073
|
+
body = swagger_client.ArtifactSearchParams(object_type=object_type, fields=['*'], query=dql)
|
|
1074
|
+
|
|
1075
|
+
try:
|
|
1076
|
+
response = search_instance.search_artifact(self.qtest_project_id, body)
|
|
1077
|
+
if response['total'] == 0:
|
|
1078
|
+
return None # Not found, but don't raise - caller handles this
|
|
1079
|
+
|
|
1080
|
+
# Use the unified parser
|
|
1081
|
+
return self.__parse_entity_item(object_type, response['items'][0])
|
|
1082
|
+
|
|
1083
|
+
except ApiException as e:
|
|
1084
|
+
logger.warning(f"Could not fetch details for {entity_id}: {e}")
|
|
1085
|
+
return None
|
|
1086
|
+
|
|
1087
|
+
def __get_entity_pid_by_internal_id(self, object_type: str, internal_id: int) -> str:
|
|
1088
|
+
"""Reverse lookup: get entity PID (TC-xxx, TR-xxx, etc.) from internal QTest ID.
|
|
1089
|
+
|
|
1090
|
+
Args:
|
|
1091
|
+
object_type: QTest object type ('test-cases', 'test-runs', 'defects', 'requirements')
|
|
1092
|
+
internal_id: Internal QTest ID (numeric)
|
|
1093
|
+
|
|
1094
|
+
Returns:
|
|
1095
|
+
str: Entity PID in format prefix-number (TC-123, TR-456, etc.) or None if not found
|
|
1096
|
+
"""
|
|
1097
|
+
search_instance = swagger_client.SearchApi(self._client)
|
|
1098
|
+
# Note: 'id' needs quotes for DQL when searching by internal ID
|
|
1099
|
+
body = swagger_client.ArtifactSearchParams(
|
|
1100
|
+
object_type=object_type,
|
|
1101
|
+
fields=['id', 'pid'],
|
|
1102
|
+
query=f"'id' = '{internal_id}'"
|
|
1103
|
+
)
|
|
1104
|
+
|
|
1105
|
+
try:
|
|
1106
|
+
response = search_instance.search_artifact(self.qtest_project_id, body)
|
|
1107
|
+
if response['total'] > 0:
|
|
1108
|
+
return response['items'][0].get('pid')
|
|
1109
|
+
return None
|
|
1110
|
+
except ApiException as e:
|
|
1111
|
+
logger.warning(f"Could not get PID for {object_type} internal ID {internal_id}: {e}")
|
|
1112
|
+
return None
|
|
1113
|
+
|
|
1114
|
+
def __find_qtest_test_run_id_by_id(self, test_run_id: str) -> int:
|
|
1115
|
+
"""Search for test run's internal QTest ID using test run ID (TR-xxx format).
|
|
1116
|
+
|
|
1117
|
+
Args:
|
|
1118
|
+
test_run_id: Test run ID in format TR-123
|
|
1119
|
+
|
|
1120
|
+
Returns:
|
|
1121
|
+
int: Internal QTest ID for the test run
|
|
1122
|
+
|
|
1123
|
+
Raises:
|
|
1124
|
+
ValueError: If test run is not found
|
|
1125
|
+
"""
|
|
1126
|
+
return self.__find_qtest_internal_id('test-runs', test_run_id)
|
|
1127
|
+
|
|
823
1128
|
def __is_jira_requirement_present(self, jira_issue_id: str) -> tuple[bool, dict]:
|
|
824
1129
|
""" Define if particular Jira requirement is present in qtest or not """
|
|
825
1130
|
dql = f"'External Id' = '{jira_issue_id}'"
|
|
@@ -838,13 +1143,13 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
838
1143
|
|
|
839
1144
|
def _get_jira_requirement_id(self, jira_issue_id: str) -> int:
|
|
840
1145
|
"""Search for requirement id using the linked jira_issue_id.
|
|
841
|
-
|
|
1146
|
+
|
|
842
1147
|
Args:
|
|
843
1148
|
jira_issue_id: External Jira issue ID (e.g., PLAN-128)
|
|
844
|
-
|
|
1149
|
+
|
|
845
1150
|
Returns:
|
|
846
1151
|
int: Internal QTest ID for the Jira requirement
|
|
847
|
-
|
|
1152
|
+
|
|
848
1153
|
Raises:
|
|
849
1154
|
ValueError: If Jira requirement is not found in QTest
|
|
850
1155
|
"""
|
|
@@ -859,11 +1164,11 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
859
1164
|
|
|
860
1165
|
def link_tests_to_jira_requirement(self, requirement_external_id: str, json_list_of_test_case_ids: str) -> str:
|
|
861
1166
|
"""Link test cases to external Jira requirement.
|
|
862
|
-
|
|
1167
|
+
|
|
863
1168
|
Args:
|
|
864
1169
|
requirement_external_id: Jira issue ID (e.g., PLAN-128)
|
|
865
1170
|
json_list_of_test_case_ids: JSON array string of test case IDs (e.g., '["TC-123", "TC-234"]')
|
|
866
|
-
|
|
1171
|
+
|
|
867
1172
|
Returns:
|
|
868
1173
|
Success message with linked test case IDs
|
|
869
1174
|
"""
|
|
@@ -876,10 +1181,10 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
876
1181
|
|
|
877
1182
|
try:
|
|
878
1183
|
response = link_object_api_instance.link_artifacts(
|
|
879
|
-
self.qtest_project_id,
|
|
1184
|
+
self.qtest_project_id,
|
|
880
1185
|
object_id=requirement_id,
|
|
881
1186
|
type=linked_type,
|
|
882
|
-
object_type=source_type,
|
|
1187
|
+
object_type=source_type,
|
|
883
1188
|
body=qtest_test_case_ids
|
|
884
1189
|
)
|
|
885
1190
|
linked_test_cases = [link.pid for link in response[0].objects]
|
|
@@ -898,14 +1203,14 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
898
1203
|
|
|
899
1204
|
def link_tests_to_qtest_requirement(self, requirement_id: str, json_list_of_test_case_ids: str) -> str:
|
|
900
1205
|
"""Link test cases to internal QTest requirement.
|
|
901
|
-
|
|
1206
|
+
|
|
902
1207
|
Args:
|
|
903
1208
|
requirement_id: QTest requirement ID in format RQ-123
|
|
904
1209
|
json_list_of_test_case_ids: JSON array string of test case IDs (e.g., '["TC-123", "TC-234"]')
|
|
905
|
-
|
|
1210
|
+
|
|
906
1211
|
Returns:
|
|
907
1212
|
Success message with linked test case IDs
|
|
908
|
-
|
|
1213
|
+
|
|
909
1214
|
Raises:
|
|
910
1215
|
ValueError: If requirement or test cases are not found
|
|
911
1216
|
ToolException: If linking fails
|
|
@@ -913,11 +1218,11 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
913
1218
|
link_object_api_instance = swagger_client.ObjectLinkApi(self._client)
|
|
914
1219
|
source_type = "requirements"
|
|
915
1220
|
linked_type = "test-cases"
|
|
916
|
-
|
|
1221
|
+
|
|
917
1222
|
# Parse and convert test case IDs
|
|
918
1223
|
test_case_ids = json.loads(json_list_of_test_case_ids)
|
|
919
1224
|
qtest_test_case_ids = [self.__find_qtest_id_by_test_id(tc_id) for tc_id in test_case_ids]
|
|
920
|
-
|
|
1225
|
+
|
|
921
1226
|
# Get internal QTest ID for the requirement
|
|
922
1227
|
qtest_requirement_id = self.__find_qtest_requirement_id_by_id(requirement_id)
|
|
923
1228
|
|
|
@@ -943,14 +1248,575 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
943
1248
|
f"in project {self.qtest_project_id}. Exception: \n{stacktrace}"
|
|
944
1249
|
) from e
|
|
945
1250
|
|
|
1251
|
+
def find_test_cases_by_requirement_id(self, requirement_id: str, include_details: bool = False) -> dict:
|
|
1252
|
+
"""Find all test cases linked to a QTest requirement.
|
|
1253
|
+
|
|
1254
|
+
This method uses the ObjectLinkApi.find() to discover test cases that are
|
|
1255
|
+
linked to a specific requirement. This is the correct way to find linked
|
|
1256
|
+
test cases - DQL queries cannot search test cases by linked requirement.
|
|
1257
|
+
|
|
1258
|
+
Args:
|
|
1259
|
+
requirement_id: QTest requirement ID in format RQ-123
|
|
1260
|
+
include_details: If True, fetches full test case details. If False, returns summary with Id, Name, Description.
|
|
1261
|
+
|
|
1262
|
+
Returns:
|
|
1263
|
+
dict with requirement_id, total count, and test_cases list
|
|
1264
|
+
|
|
1265
|
+
Raises:
|
|
1266
|
+
ValueError: If requirement is not found
|
|
1267
|
+
ToolException: If API call fails
|
|
1268
|
+
"""
|
|
1269
|
+
# Get internal QTest ID for the requirement
|
|
1270
|
+
qtest_requirement_id = self.__find_qtest_requirement_id_by_id(requirement_id)
|
|
1271
|
+
|
|
1272
|
+
link_object_api_instance = swagger_client.ObjectLinkApi(self._client)
|
|
1273
|
+
|
|
1274
|
+
try:
|
|
1275
|
+
# Use ObjectLinkApi.find() to get linked artifacts
|
|
1276
|
+
# type='requirements' means we're searching from requirements
|
|
1277
|
+
# ids=[qtest_requirement_id] specifies which requirement(s) to check
|
|
1278
|
+
response = link_object_api_instance.find(
|
|
1279
|
+
self.qtest_project_id,
|
|
1280
|
+
type='requirements',
|
|
1281
|
+
ids=[qtest_requirement_id]
|
|
1282
|
+
)
|
|
1283
|
+
|
|
1284
|
+
# Parse the response to extract linked test cases
|
|
1285
|
+
# Response structure: [{id: req_internal_id, pid: 'RQ-15', objects: [{id: tc_internal_id, pid: 'TC-123'}, ...]}]
|
|
1286
|
+
linked_test_cases = []
|
|
1287
|
+
if response and len(response) > 0:
|
|
1288
|
+
for container in response:
|
|
1289
|
+
# Convert to dict if it's an object
|
|
1290
|
+
container_data = container.to_dict() if hasattr(container, 'to_dict') else container
|
|
1291
|
+
objects = container_data.get('objects', []) if isinstance(container_data, dict) else []
|
|
1292
|
+
|
|
1293
|
+
for obj in objects:
|
|
1294
|
+
obj_data = obj.to_dict() if hasattr(obj, 'to_dict') else obj
|
|
1295
|
+
if isinstance(obj_data, dict):
|
|
1296
|
+
pid = obj_data.get('pid', '')
|
|
1297
|
+
internal_id = obj_data.get('id')
|
|
1298
|
+
if pid and pid.startswith('TC-'):
|
|
1299
|
+
linked_test_cases.append({
|
|
1300
|
+
'Id': pid,
|
|
1301
|
+
QTEST_ID: internal_id
|
|
1302
|
+
})
|
|
1303
|
+
|
|
1304
|
+
if not linked_test_cases:
|
|
1305
|
+
return {
|
|
1306
|
+
'requirement_id': requirement_id,
|
|
1307
|
+
'total': 0,
|
|
1308
|
+
'test_cases': [],
|
|
1309
|
+
'message': f"No test cases are linked to requirement '{requirement_id}'"
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
# Build result based on detail level
|
|
1313
|
+
test_cases_result = []
|
|
1314
|
+
|
|
1315
|
+
if not include_details:
|
|
1316
|
+
# Short view: fetch Name, Description via DQL for each test case
|
|
1317
|
+
for tc in linked_test_cases:
|
|
1318
|
+
try:
|
|
1319
|
+
parsed_data = self.__perform_search_by_dql(f"Id = '{tc['Id']}'")
|
|
1320
|
+
if parsed_data:
|
|
1321
|
+
tc_data = parsed_data[0]
|
|
1322
|
+
test_cases_result.append({
|
|
1323
|
+
'Id': tc['Id'],
|
|
1324
|
+
QTEST_ID: tc[QTEST_ID],
|
|
1325
|
+
'Name': tc_data.get('Name'),
|
|
1326
|
+
'Description': tc_data.get('Description', '')
|
|
1327
|
+
})
|
|
1328
|
+
except Exception as e:
|
|
1329
|
+
logger.warning(f"Could not fetch details for {tc['Id']}: {e}")
|
|
1330
|
+
test_cases_result.append({
|
|
1331
|
+
'Id': tc['Id'],
|
|
1332
|
+
QTEST_ID: tc[QTEST_ID],
|
|
1333
|
+
'Name': 'Unable to fetch',
|
|
1334
|
+
'Description': ''
|
|
1335
|
+
})
|
|
1336
|
+
else:
|
|
1337
|
+
# Full details: fetch complete test case data
|
|
1338
|
+
for tc in linked_test_cases:
|
|
1339
|
+
try:
|
|
1340
|
+
parsed_data = self.__perform_search_by_dql(f"Id = '{tc['Id']}'")
|
|
1341
|
+
if parsed_data:
|
|
1342
|
+
test_cases_result.append(parsed_data[0])
|
|
1343
|
+
except Exception as e:
|
|
1344
|
+
logger.warning(f"Could not fetch details for {tc['Id']}: {e}")
|
|
1345
|
+
test_cases_result.append({
|
|
1346
|
+
'Id': tc['Id'],
|
|
1347
|
+
QTEST_ID: tc[QTEST_ID],
|
|
1348
|
+
'error': f'Unable to fetch details: {str(e)}'
|
|
1349
|
+
})
|
|
1350
|
+
|
|
1351
|
+
return {
|
|
1352
|
+
'requirement_id': requirement_id,
|
|
1353
|
+
'total': len(test_cases_result),
|
|
1354
|
+
'test_cases': test_cases_result
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
except ApiException as e:
|
|
1358
|
+
stacktrace = format_exc()
|
|
1359
|
+
logger.error(f"Error finding test cases by requirement: {stacktrace}")
|
|
1360
|
+
raise ToolException(
|
|
1361
|
+
f"Unable to find test cases linked to requirement '{requirement_id}' "
|
|
1362
|
+
f"in project {self.qtest_project_id}. Exception: \n{stacktrace}"
|
|
1363
|
+
) from e
|
|
1364
|
+
|
|
1365
|
+
def find_requirements_by_test_case_id(self, test_case_id: str) -> dict:
|
|
1366
|
+
"""Find all requirements linked to a test case.
|
|
1367
|
+
|
|
1368
|
+
This method uses the ObjectLinkApi.find() to discover requirements that are
|
|
1369
|
+
linked to a specific test case (reverse lookup).
|
|
1370
|
+
|
|
1371
|
+
Args:
|
|
1372
|
+
test_case_id: Test case ID in format TC-123
|
|
1373
|
+
|
|
1374
|
+
Returns:
|
|
1375
|
+
dict with test_case_id, total count, and requirements list
|
|
1376
|
+
|
|
1377
|
+
Raises:
|
|
1378
|
+
ValueError: If test case is not found
|
|
1379
|
+
ToolException: If API call fails
|
|
1380
|
+
"""
|
|
1381
|
+
# Get internal QTest ID for the test case
|
|
1382
|
+
qtest_test_case_id = self.__find_qtest_id_by_test_id(test_case_id)
|
|
1383
|
+
|
|
1384
|
+
link_object_api_instance = swagger_client.ObjectLinkApi(self._client)
|
|
1385
|
+
|
|
1386
|
+
try:
|
|
1387
|
+
# Use ObjectLinkApi.find() to get linked artifacts
|
|
1388
|
+
# type='test-cases' means we're searching from test cases
|
|
1389
|
+
response = link_object_api_instance.find(
|
|
1390
|
+
self.qtest_project_id,
|
|
1391
|
+
type='test-cases',
|
|
1392
|
+
ids=[qtest_test_case_id]
|
|
1393
|
+
)
|
|
1394
|
+
|
|
1395
|
+
# Parse the response to extract linked requirement IDs
|
|
1396
|
+
linked_requirement_ids = []
|
|
1397
|
+
if response and len(response) > 0:
|
|
1398
|
+
for container in response:
|
|
1399
|
+
container_data = container.to_dict() if hasattr(container, 'to_dict') else container
|
|
1400
|
+
objects = container_data.get('objects', []) if isinstance(container_data, dict) else []
|
|
1401
|
+
|
|
1402
|
+
for obj in objects:
|
|
1403
|
+
obj_data = obj.to_dict() if hasattr(obj, 'to_dict') else obj
|
|
1404
|
+
if isinstance(obj_data, dict):
|
|
1405
|
+
pid = obj_data.get('pid', '')
|
|
1406
|
+
# Requirements have RQ- prefix
|
|
1407
|
+
if pid and pid.startswith('RQ-'):
|
|
1408
|
+
linked_requirement_ids.append(pid)
|
|
1409
|
+
|
|
1410
|
+
if not linked_requirement_ids:
|
|
1411
|
+
return {
|
|
1412
|
+
'test_case_id': test_case_id,
|
|
1413
|
+
'total': 0,
|
|
1414
|
+
'requirements': [],
|
|
1415
|
+
'message': f"No requirements are linked to test case '{test_case_id}'"
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
# Fetch actual requirement details via DQL search
|
|
1419
|
+
requirements_result = []
|
|
1420
|
+
for req_id in linked_requirement_ids:
|
|
1421
|
+
req_data = self.__search_entity_by_id('requirements', req_id)
|
|
1422
|
+
if req_data:
|
|
1423
|
+
requirements_result.append(req_data)
|
|
1424
|
+
else:
|
|
1425
|
+
# Fallback if search fails
|
|
1426
|
+
requirements_result.append({
|
|
1427
|
+
'Id': req_id,
|
|
1428
|
+
'QTest Id': None,
|
|
1429
|
+
'Name': 'Unable to fetch',
|
|
1430
|
+
'Description': ''
|
|
1431
|
+
})
|
|
1432
|
+
|
|
1433
|
+
return {
|
|
1434
|
+
'test_case_id': test_case_id,
|
|
1435
|
+
'total': len(requirements_result),
|
|
1436
|
+
'requirements': requirements_result
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
except ApiException as e:
|
|
1440
|
+
stacktrace = format_exc()
|
|
1441
|
+
logger.error(f"Error finding requirements by test case: {stacktrace}")
|
|
1442
|
+
raise ToolException(
|
|
1443
|
+
f"Unable to find requirements linked to test case '{test_case_id}' "
|
|
1444
|
+
f"in project {self.qtest_project_id}. Exception: \n{stacktrace}"
|
|
1445
|
+
) from e
|
|
1446
|
+
|
|
1447
|
+
def find_test_runs_by_test_case_id(self, test_case_id: str) -> dict:
|
|
1448
|
+
"""Find all test runs associated with a test case.
|
|
1449
|
+
|
|
1450
|
+
A test run represents an execution instance of a test case. Each test run
|
|
1451
|
+
tracks execution details, status, and any defects found during that run.
|
|
1452
|
+
|
|
1453
|
+
IMPORTANT: In QTest's data model, defects are linked to test runs, not directly
|
|
1454
|
+
to test cases. To find defects related to a test case:
|
|
1455
|
+
1. Use this tool to find test runs for the test case
|
|
1456
|
+
2. Use find_defects_by_test_run_id for each test run to get related defects
|
|
1457
|
+
|
|
1458
|
+
Each test run in the result includes 'Test Case Id' showing which test case
|
|
1459
|
+
it executes, and 'Latest Test Log' with execution status and log ID.
|
|
1460
|
+
|
|
1461
|
+
Args:
|
|
1462
|
+
test_case_id: Test case ID in format TC-123
|
|
1463
|
+
|
|
1464
|
+
Returns:
|
|
1465
|
+
dict with test_case_id, total count, and test_runs list with full details
|
|
1466
|
+
|
|
1467
|
+
Raises:
|
|
1468
|
+
ValueError: If test case is not found
|
|
1469
|
+
ToolException: If API call fails
|
|
1470
|
+
"""
|
|
1471
|
+
# Get internal QTest ID for the test case
|
|
1472
|
+
qtest_test_case_id = self.__find_qtest_id_by_test_id(test_case_id)
|
|
1473
|
+
|
|
1474
|
+
link_object_api_instance = swagger_client.ObjectLinkApi(self._client)
|
|
1475
|
+
|
|
1476
|
+
try:
|
|
1477
|
+
# Use ObjectLinkApi.find() to get linked artifacts
|
|
1478
|
+
response = link_object_api_instance.find(
|
|
1479
|
+
self.qtest_project_id,
|
|
1480
|
+
type='test-cases',
|
|
1481
|
+
ids=[qtest_test_case_id]
|
|
1482
|
+
)
|
|
1483
|
+
|
|
1484
|
+
# Parse the response to extract linked test run IDs
|
|
1485
|
+
linked_test_run_ids = []
|
|
1486
|
+
if response and len(response) > 0:
|
|
1487
|
+
for container in response:
|
|
1488
|
+
container_data = container.to_dict() if hasattr(container, 'to_dict') else container
|
|
1489
|
+
objects = container_data.get('objects', []) if isinstance(container_data, dict) else []
|
|
1490
|
+
|
|
1491
|
+
for obj in objects:
|
|
1492
|
+
obj_data = obj.to_dict() if hasattr(obj, 'to_dict') else obj
|
|
1493
|
+
if isinstance(obj_data, dict):
|
|
1494
|
+
pid = obj_data.get('pid', '')
|
|
1495
|
+
# Test runs have TR- prefix
|
|
1496
|
+
if pid and pid.startswith('TR-'):
|
|
1497
|
+
linked_test_run_ids.append(pid)
|
|
1498
|
+
|
|
1499
|
+
if not linked_test_run_ids:
|
|
1500
|
+
return {
|
|
1501
|
+
'test_case_id': test_case_id,
|
|
1502
|
+
'total': 0,
|
|
1503
|
+
'test_runs': [],
|
|
1504
|
+
'message': f"No test runs are associated with test case '{test_case_id}'"
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
# Fetch actual test run details via DQL search
|
|
1508
|
+
test_runs_result = []
|
|
1509
|
+
for tr_id in linked_test_run_ids:
|
|
1510
|
+
tr_data = self.__search_entity_by_id('test-runs', tr_id)
|
|
1511
|
+
if tr_data:
|
|
1512
|
+
test_runs_result.append(tr_data)
|
|
1513
|
+
else:
|
|
1514
|
+
# Fallback if search fails
|
|
1515
|
+
test_runs_result.append({
|
|
1516
|
+
'Id': tr_id,
|
|
1517
|
+
'QTest Id': None,
|
|
1518
|
+
'Name': 'Unable to fetch',
|
|
1519
|
+
'Description': ''
|
|
1520
|
+
})
|
|
1521
|
+
|
|
1522
|
+
return {
|
|
1523
|
+
'test_case_id': test_case_id,
|
|
1524
|
+
'total': len(test_runs_result),
|
|
1525
|
+
'test_runs': test_runs_result,
|
|
1526
|
+
'hint': 'To find defects, use find_defects_by_test_run_id for each test run.'
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
except ApiException as e:
|
|
1530
|
+
stacktrace = format_exc()
|
|
1531
|
+
logger.error(f"Error finding test runs by test case: {stacktrace}")
|
|
1532
|
+
raise ToolException(
|
|
1533
|
+
f"Unable to find test runs associated with test case '{test_case_id}' "
|
|
1534
|
+
f"in project {self.qtest_project_id}. Exception: \n{stacktrace}"
|
|
1535
|
+
) from e
|
|
1536
|
+
|
|
1537
|
+
def find_defects_by_test_run_id(self, test_run_id: str) -> dict:
|
|
1538
|
+
"""Find all defects associated with a test run.
|
|
1539
|
+
|
|
1540
|
+
In QTest, defects are linked to test runs (not directly to test cases).
|
|
1541
|
+
A test run executes a specific test case, so defects found here are
|
|
1542
|
+
related to that test case through the test run execution context.
|
|
1543
|
+
|
|
1544
|
+
Use this tool after find_test_runs_by_test_case_id to discover defects.
|
|
1545
|
+
The result includes source context (test run and test case IDs) for traceability.
|
|
1546
|
+
|
|
1547
|
+
Args:
|
|
1548
|
+
test_run_id: Test run ID in format TR-123
|
|
1549
|
+
|
|
1550
|
+
Returns:
|
|
1551
|
+
dict with test_run_id, source_test_case_id, total count, and defects list with full details
|
|
1552
|
+
|
|
1553
|
+
Raises:
|
|
1554
|
+
ValueError: If test run is not found
|
|
1555
|
+
ToolException: If API call fails
|
|
1556
|
+
"""
|
|
1557
|
+
# First, get test run details to get the source test case context
|
|
1558
|
+
test_run_data = self.__search_entity_by_id('test-runs', test_run_id)
|
|
1559
|
+
source_test_case_id = None
|
|
1560
|
+
if test_run_data:
|
|
1561
|
+
# testCaseId is the internal ID, we need the PID (TC-xxx format)
|
|
1562
|
+
internal_tc_id = test_run_data.get('Test Case Id')
|
|
1563
|
+
if internal_tc_id:
|
|
1564
|
+
source_test_case_id = self.__get_entity_pid_by_internal_id('test-cases', internal_tc_id)
|
|
1565
|
+
else:
|
|
1566
|
+
raise ValueError(f"Test run '{test_run_id}' not found")
|
|
1567
|
+
|
|
1568
|
+
# Get internal QTest ID for the test run from test_run_data (avoids duplicate API call)
|
|
1569
|
+
qtest_test_run_id = test_run_data.get('QTest Id')
|
|
1570
|
+
if not qtest_test_run_id:
|
|
1571
|
+
raise ValueError(f"QTest Id not found in test run data for '{test_run_id}'")
|
|
1572
|
+
|
|
1573
|
+
link_object_api_instance = swagger_client.ObjectLinkApi(self._client)
|
|
1574
|
+
|
|
1575
|
+
try:
|
|
1576
|
+
# Use ObjectLinkApi.find() to get linked artifacts
|
|
1577
|
+
response = link_object_api_instance.find(
|
|
1578
|
+
self.qtest_project_id,
|
|
1579
|
+
type='test-runs',
|
|
1580
|
+
ids=[qtest_test_run_id]
|
|
1581
|
+
)
|
|
1582
|
+
|
|
1583
|
+
# Parse the response to extract linked defect IDs
|
|
1584
|
+
linked_defect_ids = []
|
|
1585
|
+
if response and len(response) > 0:
|
|
1586
|
+
for container in response:
|
|
1587
|
+
container_data = container.to_dict() if hasattr(container, 'to_dict') else container
|
|
1588
|
+
objects = container_data.get('objects', []) if isinstance(container_data, dict) else []
|
|
1589
|
+
|
|
1590
|
+
for obj in objects:
|
|
1591
|
+
obj_data = obj.to_dict() if hasattr(obj, 'to_dict') else obj
|
|
1592
|
+
if isinstance(obj_data, dict):
|
|
1593
|
+
pid = obj_data.get('pid', '')
|
|
1594
|
+
# Defects have DF- prefix
|
|
1595
|
+
if pid and pid.startswith('DF-'):
|
|
1596
|
+
linked_defect_ids.append(pid)
|
|
1597
|
+
|
|
1598
|
+
if not linked_defect_ids:
|
|
1599
|
+
result = {
|
|
1600
|
+
'test_run_id': test_run_id,
|
|
1601
|
+
'total': 0,
|
|
1602
|
+
'defects': [],
|
|
1603
|
+
'message': f"No defects are associated with test run '{test_run_id}'"
|
|
1604
|
+
}
|
|
1605
|
+
if source_test_case_id:
|
|
1606
|
+
result['source_test_case_id'] = source_test_case_id
|
|
1607
|
+
return result
|
|
1608
|
+
|
|
1609
|
+
# Fetch actual defect details via DQL search
|
|
1610
|
+
defects_result = []
|
|
1611
|
+
for defect_id in linked_defect_ids:
|
|
1612
|
+
defect_data = self.__search_entity_by_id('defects', defect_id)
|
|
1613
|
+
if defect_data:
|
|
1614
|
+
defects_result.append(defect_data)
|
|
1615
|
+
else:
|
|
1616
|
+
# Fallback if search fails
|
|
1617
|
+
defects_result.append({
|
|
1618
|
+
'Id': defect_id,
|
|
1619
|
+
'QTest Id': None,
|
|
1620
|
+
'Name': 'Unable to fetch',
|
|
1621
|
+
'Description': ''
|
|
1622
|
+
})
|
|
1623
|
+
|
|
1624
|
+
result = {
|
|
1625
|
+
'test_run_id': test_run_id,
|
|
1626
|
+
'total': len(defects_result),
|
|
1627
|
+
'defects': defects_result
|
|
1628
|
+
}
|
|
1629
|
+
if source_test_case_id:
|
|
1630
|
+
result['source_test_case_id'] = source_test_case_id
|
|
1631
|
+
return result
|
|
1632
|
+
|
|
1633
|
+
except ApiException as e:
|
|
1634
|
+
stacktrace = format_exc()
|
|
1635
|
+
logger.error(f"Error finding defects by test run: {stacktrace}")
|
|
1636
|
+
raise ToolException(
|
|
1637
|
+
f"Unable to find defects associated with test run '{test_run_id}' "
|
|
1638
|
+
f"in project {self.qtest_project_id}. Exception: \n{stacktrace}"
|
|
1639
|
+
) from e
|
|
1640
|
+
|
|
946
1641
|
def search_by_dql(self, dql: str, extract_images:bool=False, prompt: str=None):
|
|
947
1642
|
"""Search for the test cases in qTest using Data Query Language """
|
|
948
1643
|
parsed_data = self.__perform_search_by_dql(dql, extract_images, prompt)
|
|
949
1644
|
return "Found " + str(
|
|
950
1645
|
len(parsed_data)) + f" Qtest test cases:\n" + str(parsed_data[:self.no_of_tests_shown_in_dql_search])
|
|
951
1646
|
|
|
1647
|
+
def search_entities_by_dql(self, object_type: str, dql: str) -> dict:
|
|
1648
|
+
"""Generic DQL search for any entity type (test-cases, requirements, defects, test-runs, etc.).
|
|
1649
|
+
|
|
1650
|
+
This is the unified search method that works for all QTest searchable entity types.
|
|
1651
|
+
Each entity type has its own properties structure, but this method parses
|
|
1652
|
+
them consistently using the generic entity parser.
|
|
1653
|
+
|
|
1654
|
+
Args:
|
|
1655
|
+
object_type: Entity type to search (see QTEST_OBJECT_TYPES and QTEST_SEARCHABLE_ONLY_TYPES)
|
|
1656
|
+
dql: QTest Data Query Language query string
|
|
1657
|
+
|
|
1658
|
+
Returns:
|
|
1659
|
+
dict with object_type, total count, and items list with full entity details
|
|
1660
|
+
"""
|
|
1661
|
+
# Check if object_type is valid (either has prefix or is searchable-only)
|
|
1662
|
+
all_searchable = {**QTEST_OBJECT_TYPES, **QTEST_SEARCHABLE_ONLY_TYPES}
|
|
1663
|
+
if object_type not in all_searchable:
|
|
1664
|
+
raise ValueError(
|
|
1665
|
+
f"Invalid object_type '{object_type}'. "
|
|
1666
|
+
f"Must be one of: {', '.join(all_searchable.keys())}"
|
|
1667
|
+
)
|
|
1668
|
+
|
|
1669
|
+
entity_info = all_searchable[object_type]
|
|
1670
|
+
search_instance = swagger_client.SearchApi(self._client)
|
|
1671
|
+
body = swagger_client.ArtifactSearchParams(
|
|
1672
|
+
object_type=object_type,
|
|
1673
|
+
fields=['*'],
|
|
1674
|
+
query=dql
|
|
1675
|
+
)
|
|
1676
|
+
|
|
1677
|
+
try:
|
|
1678
|
+
response = search_instance.search_artifact(self.qtest_project_id, body)
|
|
1679
|
+
|
|
1680
|
+
# Parse all items using the generic parser
|
|
1681
|
+
items = []
|
|
1682
|
+
for item in response.get('items', []):
|
|
1683
|
+
parsed = self.__parse_entity_item(object_type, item)
|
|
1684
|
+
items.append(parsed)
|
|
1685
|
+
|
|
1686
|
+
return {
|
|
1687
|
+
'object_type': object_type,
|
|
1688
|
+
'entity_name': entity_info['name'],
|
|
1689
|
+
'total': response.get('total', 0),
|
|
1690
|
+
'returned': len(items),
|
|
1691
|
+
'items': items[:self.no_of_tests_shown_in_dql_search]
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
except ApiException as e:
|
|
1695
|
+
stacktrace = format_exc()
|
|
1696
|
+
logger.error(f"Error searching {object_type} by DQL: {stacktrace}")
|
|
1697
|
+
raise ToolException(
|
|
1698
|
+
f"Unable to search {entity_info['name']}s with DQL '{dql}' "
|
|
1699
|
+
f"in project {self.qtest_project_id}. Exception: \n{stacktrace}"
|
|
1700
|
+
) from e
|
|
1701
|
+
|
|
1702
|
+
def find_entity_by_id(self, entity_id: str) -> dict:
|
|
1703
|
+
"""Find any QTest entity by its ID (TC-xxx, RQ-xxx, DF-xxx, TR-xxx).
|
|
1704
|
+
|
|
1705
|
+
This is a universal lookup tool that works for any entity type.
|
|
1706
|
+
The entity type is automatically determined from the ID prefix.
|
|
1707
|
+
|
|
1708
|
+
Args:
|
|
1709
|
+
entity_id: Entity ID with prefix (TC-123, RQ-15, DF-100, TR-39, etc.)
|
|
1710
|
+
|
|
1711
|
+
Returns:
|
|
1712
|
+
dict with full entity details including all properties
|
|
1713
|
+
"""
|
|
1714
|
+
# Determine object type from prefix - dynamically built from registry
|
|
1715
|
+
prefix = entity_id.split('-')[0].upper() if '-' in entity_id else ''
|
|
1716
|
+
|
|
1717
|
+
# Build reverse mapping: prefix -> object_type from QTEST_OBJECT_TYPES
|
|
1718
|
+
prefix_to_type = {
|
|
1719
|
+
info['prefix']: obj_type
|
|
1720
|
+
for obj_type, info in QTEST_OBJECT_TYPES.items()
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
if prefix not in prefix_to_type:
|
|
1724
|
+
valid_prefixes = ', '.join(sorted(prefix_to_type.keys()))
|
|
1725
|
+
raise ValueError(
|
|
1726
|
+
f"Invalid entity ID format '{entity_id}'. "
|
|
1727
|
+
f"Expected prefix to be one of: {valid_prefixes}"
|
|
1728
|
+
)
|
|
1729
|
+
|
|
1730
|
+
object_type = prefix_to_type[prefix]
|
|
1731
|
+
result = self.__search_entity_by_id(object_type, entity_id)
|
|
1732
|
+
|
|
1733
|
+
if result is None:
|
|
1734
|
+
entity_name = QTEST_OBJECT_TYPES[object_type]['name']
|
|
1735
|
+
raise ValueError(
|
|
1736
|
+
f"{entity_name} '{entity_id}' not found in project {self.qtest_project_id}"
|
|
1737
|
+
)
|
|
1738
|
+
|
|
1739
|
+
return result
|
|
1740
|
+
|
|
1741
|
+
def __parse_entity_item(self, object_type: str, item: dict) -> dict:
|
|
1742
|
+
"""Generic parser for any entity type from DQL search response.
|
|
1743
|
+
|
|
1744
|
+
This parses the raw API response item into a clean dictionary,
|
|
1745
|
+
handling the differences between entity types (some have name at top level,
|
|
1746
|
+
some have it in properties as Summary, etc.)
|
|
1747
|
+
|
|
1748
|
+
Args:
|
|
1749
|
+
object_type: QTest object type
|
|
1750
|
+
item: Raw item from search response
|
|
1751
|
+
|
|
1752
|
+
Returns:
|
|
1753
|
+
dict with parsed entity data
|
|
1754
|
+
"""
|
|
1755
|
+
import html
|
|
1756
|
+
|
|
1757
|
+
result = {
|
|
1758
|
+
'Id': item.get('pid'),
|
|
1759
|
+
'QTest Id': item.get('id'),
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
# Add top-level fields if present
|
|
1763
|
+
if item.get('name'):
|
|
1764
|
+
result['Name'] = item.get('name')
|
|
1765
|
+
if item.get('description'):
|
|
1766
|
+
result['Description'] = html.unescape(strip_tags(item.get('description', '') or ''))
|
|
1767
|
+
if item.get('web_url'):
|
|
1768
|
+
result['Web URL'] = item.get('web_url')
|
|
1769
|
+
|
|
1770
|
+
# Test-case specific fields
|
|
1771
|
+
if object_type == 'test-cases':
|
|
1772
|
+
if item.get('precondition'):
|
|
1773
|
+
result['Precondition'] = html.unescape(strip_tags(item.get('precondition', '') or ''))
|
|
1774
|
+
if item.get('test_steps'):
|
|
1775
|
+
result['Steps'] = [
|
|
1776
|
+
{
|
|
1777
|
+
'Test Step Number': idx + 1,
|
|
1778
|
+
'Test Step Description': html.unescape(strip_tags(step.get('description', '') or '')),
|
|
1779
|
+
'Test Step Expected Result': html.unescape(strip_tags(step.get('expected', '') or ''))
|
|
1780
|
+
}
|
|
1781
|
+
for idx, step in enumerate(item.get('test_steps', []))
|
|
1782
|
+
]
|
|
1783
|
+
|
|
1784
|
+
# Test-run specific fields
|
|
1785
|
+
if object_type == 'test-runs':
|
|
1786
|
+
if item.get('testCaseId'):
|
|
1787
|
+
result['Test Case Id'] = item.get('testCaseId')
|
|
1788
|
+
if item.get('automation'):
|
|
1789
|
+
result['Automation'] = item.get('automation')
|
|
1790
|
+
if item.get('latest_test_log'):
|
|
1791
|
+
log = item.get('latest_test_log')
|
|
1792
|
+
result['Latest Test Log'] = {
|
|
1793
|
+
'Log Id': log.get('id'),
|
|
1794
|
+
'Status': log.get('status'),
|
|
1795
|
+
'Execution Start': log.get('exe_start_date'),
|
|
1796
|
+
'Execution End': log.get('exe_end_date')
|
|
1797
|
+
}
|
|
1798
|
+
if item.get('test_case_version'):
|
|
1799
|
+
result['Test Case Version'] = item.get('test_case_version')
|
|
1800
|
+
|
|
1801
|
+
# Parse all properties - works for all entity types
|
|
1802
|
+
for prop in item.get('properties', []):
|
|
1803
|
+
field_name = prop.get('field_name')
|
|
1804
|
+
if not field_name:
|
|
1805
|
+
continue
|
|
1806
|
+
|
|
1807
|
+
# Format value based on field type (multi-select as array, etc.)
|
|
1808
|
+
field_value = self.__format_property_value(prop)
|
|
1809
|
+
|
|
1810
|
+
# Strip HTML from text fields (strings only, not arrays)
|
|
1811
|
+
if isinstance(field_value, str) and ('<' in field_value or '&' in field_value):
|
|
1812
|
+
field_value = html.unescape(strip_tags(field_value))
|
|
1813
|
+
|
|
1814
|
+
result[field_name] = field_value
|
|
1815
|
+
|
|
1816
|
+
return result
|
|
1817
|
+
|
|
952
1818
|
def create_test_cases(self, test_case_content: str, folder_to_place_test_cases_to: str) -> dict:
|
|
953
|
-
""" Create the
|
|
1819
|
+
""" Create the test case based on the incoming content. The input should be in json format. """
|
|
954
1820
|
test_cases_api_instance: TestCaseApi = self.__instantiate_test_api_instance()
|
|
955
1821
|
input_obj = json.loads(test_case_content)
|
|
956
1822
|
test_cases = input_obj if isinstance(input_obj, list) else [input_obj]
|
|
@@ -1030,7 +1896,37 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
1030
1896
|
{
|
|
1031
1897
|
"name": "search_by_dql",
|
|
1032
1898
|
"mode": "search_by_dql",
|
|
1033
|
-
"description":
|
|
1899
|
+
"description": """Search test cases in qTest using Data Query Language (DQL).
|
|
1900
|
+
|
|
1901
|
+
CRITICAL: USE SINGLE QUOTES ONLY - DQL does not support double quotes!
|
|
1902
|
+
- ✓ CORRECT: Description ~ 'Forgot Password'
|
|
1903
|
+
- ✗ WRONG: Description ~ "Forgot Password"
|
|
1904
|
+
|
|
1905
|
+
LIMITATION - CANNOT SEARCH BY LINKED OBJECTS:
|
|
1906
|
+
- ✗ 'Requirement Id' = 'RQ-15' will fail - use 'find_test_cases_by_requirement_id' tool instead
|
|
1907
|
+
- ✗ Linked defects or other relationship queries are not supported
|
|
1908
|
+
|
|
1909
|
+
SEARCHABLE FIELDS:
|
|
1910
|
+
- Direct fields: Id, Name, Description, Status, Type, Priority, Automation, etc.
|
|
1911
|
+
- Module: Use 'Module in' syntax
|
|
1912
|
+
- Custom fields: Use exact field name from project configuration
|
|
1913
|
+
- Date fields: MUST use ISO DateTime format (e.g., '2024-01-01T00:00:00.000Z')
|
|
1914
|
+
|
|
1915
|
+
SYNTAX RULES:
|
|
1916
|
+
1. ALL string values MUST use single quotes (never double quotes)
|
|
1917
|
+
2. Field names with spaces MUST be in single quotes: 'Created Date' > '2024-01-01T00:00:00.000Z'
|
|
1918
|
+
3. Use ~ for 'contains', !~ for 'not contains': Description ~ 'login'
|
|
1919
|
+
4. Use 'is not empty' for non-empty check: Name is 'not empty'
|
|
1920
|
+
5. Operators: =, !=, <, >, <=, >=, in, ~, !~
|
|
1921
|
+
|
|
1922
|
+
EXAMPLES:
|
|
1923
|
+
- Id = 'TC-123'
|
|
1924
|
+
- Description ~ 'Forgot Password'
|
|
1925
|
+
- Status = 'New' and Priority = 'High'
|
|
1926
|
+
- Module in 'MD-78 Master Test Suite'
|
|
1927
|
+
- Name ~ 'login'
|
|
1928
|
+
- 'Created Date' > '2024-01-01T00:00:00.000Z'
|
|
1929
|
+
""",
|
|
1034
1930
|
"args_schema": QtestDataQuerySearch,
|
|
1035
1931
|
"ref": self.search_by_dql,
|
|
1036
1932
|
},
|
|
@@ -1089,5 +1985,160 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
1089
1985
|
"description": "Get information about available test case fields and their valid values for the project. Shows which property values are allowed (e.g., Status: 'New', 'In Progress', 'Completed') based on the project configuration. Use force_refresh=true if project configuration has changed.",
|
|
1090
1986
|
"args_schema": GetAllTestCasesFieldsForProject,
|
|
1091
1987
|
"ref": self.get_all_test_cases_fields_for_project,
|
|
1988
|
+
},
|
|
1989
|
+
{
|
|
1990
|
+
"name": "find_test_cases_by_requirement_id",
|
|
1991
|
+
"mode": "find_test_cases_by_requirement_id",
|
|
1992
|
+
"description": """Find all test cases linked to a QTest requirement.
|
|
1993
|
+
|
|
1994
|
+
Use this tool to find test cases associated with a specific requirement.
|
|
1995
|
+
DQL search cannot query by linked requirement - use this tool instead.
|
|
1996
|
+
|
|
1997
|
+
Parameters:
|
|
1998
|
+
- requirement_id: QTest requirement ID in format RQ-123
|
|
1999
|
+
- include_details: If true, returns full test case data. If false (default), returns Id, QTest Id, Name, and Description.
|
|
2000
|
+
|
|
2001
|
+
Examples:
|
|
2002
|
+
- Find test cases for RQ-15: requirement_id='RQ-15'
|
|
2003
|
+
- Get full details: requirement_id='RQ-15', include_details=true
|
|
2004
|
+
""",
|
|
2005
|
+
"args_schema": FindTestCasesByRequirementId,
|
|
2006
|
+
"ref": self.find_test_cases_by_requirement_id,
|
|
2007
|
+
},
|
|
2008
|
+
{
|
|
2009
|
+
"name": "find_requirements_by_test_case_id",
|
|
2010
|
+
"mode": "find_requirements_by_test_case_id",
|
|
2011
|
+
"description": """Find all requirements linked to a test case (direct link: test-case 'covers' requirements).
|
|
2012
|
+
|
|
2013
|
+
Use this tool to discover which requirements a specific test case covers.
|
|
2014
|
+
|
|
2015
|
+
Parameters:
|
|
2016
|
+
- test_case_id: Test case ID in format TC-123
|
|
2017
|
+
|
|
2018
|
+
Returns: List of linked requirements with Id, QTest Id, Name, and Description.
|
|
2019
|
+
|
|
2020
|
+
Examples:
|
|
2021
|
+
- Find requirements for TC-123: test_case_id='TC-123'
|
|
2022
|
+
""",
|
|
2023
|
+
"args_schema": FindRequirementsByTestCaseId,
|
|
2024
|
+
"ref": self.find_requirements_by_test_case_id,
|
|
2025
|
+
},
|
|
2026
|
+
{
|
|
2027
|
+
"name": "find_test_runs_by_test_case_id",
|
|
2028
|
+
"mode": "find_test_runs_by_test_case_id",
|
|
2029
|
+
"description": """Find all test runs associated with a test case.
|
|
2030
|
+
|
|
2031
|
+
IMPORTANT: In QTest, defects are NOT directly linked to test cases.
|
|
2032
|
+
Defects are linked to TEST RUNS. To find defects related to a test case:
|
|
2033
|
+
1. First use this tool to find test runs for the test case
|
|
2034
|
+
2. Then use find_defects_by_test_run_id for each test run
|
|
2035
|
+
|
|
2036
|
+
Parameters:
|
|
2037
|
+
- test_case_id: Test case ID in format TC-123
|
|
2038
|
+
|
|
2039
|
+
Returns: List of test runs with Id, QTest Id, Name, and Description.
|
|
2040
|
+
Also includes a hint about finding defects via test runs.
|
|
2041
|
+
|
|
2042
|
+
Examples:
|
|
2043
|
+
- Find test runs for TC-123: test_case_id='TC-123'
|
|
2044
|
+
""",
|
|
2045
|
+
"args_schema": FindTestRunsByTestCaseId,
|
|
2046
|
+
"ref": self.find_test_runs_by_test_case_id,
|
|
2047
|
+
},
|
|
2048
|
+
{
|
|
2049
|
+
"name": "find_defects_by_test_run_id",
|
|
2050
|
+
"mode": "find_defects_by_test_run_id",
|
|
2051
|
+
"description": """Find all defects associated with a test run.
|
|
2052
|
+
|
|
2053
|
+
In QTest data model, defects are linked to test runs (not directly to test cases).
|
|
2054
|
+
A defect found here means it was reported during execution of this specific test run.
|
|
2055
|
+
|
|
2056
|
+
To find defects related to a test case:
|
|
2057
|
+
1. First use find_test_runs_by_test_case_id to get test runs
|
|
2058
|
+
2. Then use this tool for each test run
|
|
2059
|
+
|
|
2060
|
+
Parameters:
|
|
2061
|
+
- test_run_id: Test run ID in format TR-123
|
|
2062
|
+
|
|
2063
|
+
Returns: List of defects with Id, QTest Id, Name, and Description.
|
|
2064
|
+
|
|
2065
|
+
Examples:
|
|
2066
|
+
- Find defects for TR-39: test_run_id='TR-39'
|
|
2067
|
+
""",
|
|
2068
|
+
"args_schema": FindDefectsByTestRunId,
|
|
2069
|
+
"ref": self.find_defects_by_test_run_id,
|
|
2070
|
+
},
|
|
2071
|
+
{
|
|
2072
|
+
"name": "search_entities_by_dql",
|
|
2073
|
+
"mode": "search_entities_by_dql",
|
|
2074
|
+
"description": f"""Search any QTest entity type using Data Query Language (DQL).
|
|
2075
|
+
|
|
2076
|
+
This is a unified search tool for all searchable QTest entity types.
|
|
2077
|
+
|
|
2078
|
+
SUPPORTED ENTITY TYPES (object_type parameter):
|
|
2079
|
+
- 'test-cases' (TC-xxx): Test case definitions with steps
|
|
2080
|
+
- 'test-runs' (TR-xxx): Execution instances of test cases
|
|
2081
|
+
- 'defects' (DF-xxx): Bugs/issues found during testing
|
|
2082
|
+
- 'requirements' (RQ-xxx): Requirements to be tested
|
|
2083
|
+
- 'test-suites' (TS-xxx): Collections of test runs
|
|
2084
|
+
- 'test-cycles' (CL-xxx): Test execution cycles
|
|
2085
|
+
- 'test-logs': Execution logs (date queries ONLY - see notes)
|
|
2086
|
+
- 'releases' (RL-xxx): Software releases
|
|
2087
|
+
- 'builds' (BL-xxx): Builds within releases
|
|
2088
|
+
|
|
2089
|
+
NOTES:
|
|
2090
|
+
- Modules (MD-xxx) are NOT searchable via DQL. Use 'get_modules' tool instead.
|
|
2091
|
+
- Test-logs: Only date queries work (Execution Start Date, Execution End Date).
|
|
2092
|
+
For specific test log details, use find_test_runs_by_test_case_id -
|
|
2093
|
+
the test run includes 'Latest Test Log' with status and execution times.
|
|
2094
|
+
|
|
2095
|
+
{DQL_SYNTAX_DOCS}
|
|
2096
|
+
|
|
2097
|
+
EXAMPLES BY ENTITY TYPE:
|
|
2098
|
+
- Test cases: object_type='test-cases', dql="Name ~ 'login'"
|
|
2099
|
+
- Requirements: object_type='requirements', dql="Status = 'Baselined'"
|
|
2100
|
+
- Defects: object_type='defects', dql="Priority = 'High'"
|
|
2101
|
+
- Test runs: object_type='test-runs', dql="Status = 'Failed'"
|
|
2102
|
+
- Test logs: object_type='test-logs', dql="'Execution Start Date' > '2024-01-01T00:00:00.000Z'" (date queries only)
|
|
2103
|
+
- Releases: object_type='releases', dql="Name ~ '2024'"
|
|
2104
|
+
""",
|
|
2105
|
+
"args_schema": GenericDqlSearch,
|
|
2106
|
+
"ref": self.search_entities_by_dql,
|
|
2107
|
+
},
|
|
2108
|
+
{
|
|
2109
|
+
"name": "find_entity_by_id",
|
|
2110
|
+
"mode": "find_entity_by_id",
|
|
2111
|
+
"description": """Find any QTest entity by its ID.
|
|
2112
|
+
|
|
2113
|
+
This universal lookup tool works for entity types that have ID prefixes.
|
|
2114
|
+
The entity type is automatically determined from the ID prefix.
|
|
2115
|
+
|
|
2116
|
+
SUPPORTED ID FORMATS:
|
|
2117
|
+
- TC-123: Test Case
|
|
2118
|
+
- TR-39: Test Run
|
|
2119
|
+
- DF-100: Defect
|
|
2120
|
+
- RQ-15: Requirement
|
|
2121
|
+
- TS-5: Test Suite
|
|
2122
|
+
- CL-3: Test Cycle
|
|
2123
|
+
- RL-1: Release
|
|
2124
|
+
- BL-2: Build
|
|
2125
|
+
|
|
2126
|
+
NOT SUPPORTED (no ID prefix):
|
|
2127
|
+
- Test Logs: Get details from test run's 'Latest Test Log' field (contains Log Id, Status, Execution Start/End Date)
|
|
2128
|
+
- Modules: Use 'get_modules' tool instead
|
|
2129
|
+
|
|
2130
|
+
Parameters:
|
|
2131
|
+
- entity_id: Entity ID with prefix (e.g., TC-123, RQ-15, DF-100, TR-39)
|
|
2132
|
+
|
|
2133
|
+
Returns: Full entity details including all properties.
|
|
2134
|
+
|
|
2135
|
+
Examples:
|
|
2136
|
+
- Find test case: entity_id='TC-123'
|
|
2137
|
+
- Find requirement: entity_id='RQ-15'
|
|
2138
|
+
- Find defect: entity_id='DF-100'
|
|
2139
|
+
- Find test run: entity_id='TR-39'
|
|
2140
|
+
""",
|
|
2141
|
+
"args_schema": FindEntityById,
|
|
2142
|
+
"ref": self.find_entity_by_id,
|
|
1092
2143
|
}
|
|
1093
|
-
]
|
|
2144
|
+
]
|