qase-python-commons 3.1.3__py3-none-any.whl → 4.1.3__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.
Potentially problematic release.
This version of qase-python-commons might be problematic. Click here for more details.
- qase/__init__.py +3 -0
- qase/commons/client/api_v1_client.py +169 -143
- qase/commons/client/api_v2_client.py +77 -23
- qase/commons/client/base_api_client.py +12 -1
- qase/commons/config.py +159 -20
- qase/commons/logger.py +82 -13
- qase/commons/models/__init__.py +0 -2
- qase/commons/models/attachment.py +11 -8
- qase/commons/models/basemodel.py +12 -3
- qase/commons/models/config/framework.py +61 -0
- qase/commons/models/config/qaseconfig.py +34 -0
- qase/commons/models/config/run.py +19 -0
- qase/commons/models/config/testops.py +45 -3
- qase/commons/models/external_link.py +41 -0
- qase/commons/models/relation.py +16 -6
- qase/commons/models/result.py +16 -31
- qase/commons/models/run.py +17 -2
- qase/commons/models/runtime.py +15 -1
- qase/commons/models/step.py +43 -11
- qase/commons/profilers/__init__.py +4 -3
- qase/commons/profilers/db.py +965 -5
- qase/commons/profilers/network.py +5 -1
- qase/commons/reporters/core.py +50 -9
- qase/commons/reporters/report.py +11 -6
- qase/commons/reporters/testops.py +56 -22
- qase/commons/status_mapping/__init__.py +12 -0
- qase/commons/status_mapping/status_mapping.py +237 -0
- qase/commons/util/__init__.py +9 -0
- qase/commons/util/host_data.py +140 -0
- qase/commons/utils.py +95 -0
- {qase_python_commons-3.1.3.dist-info → qase_python_commons-4.1.3.dist-info}/METADATA +16 -11
- qase_python_commons-4.1.3.dist-info/RECORD +45 -0
- {qase_python_commons-3.1.3.dist-info → qase_python_commons-4.1.3.dist-info}/WHEEL +1 -1
- qase/commons/models/suite.py +0 -13
- qase_python_commons-3.1.3.dist-info/RECORD +0 -40
- {qase_python_commons-3.1.3.dist-info → qase_python_commons-4.1.3.dist-info}/top_level.txt +0 -0
qase/__init__.py
ADDED
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
from
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from typing import Union
|
|
2
3
|
|
|
3
4
|
import certifi
|
|
4
5
|
from qase.api_client_v1 import ApiClient, ProjectsApi, Project, EnvironmentsApi, RunsApi, AttachmentsApi, \
|
|
5
|
-
AttachmentGet, RunCreate,
|
|
6
|
+
AttachmentGet, RunCreate, ConfigurationsApi, ConfigurationCreate, ConfigurationGroupCreate, RunPublic
|
|
6
7
|
from qase.api_client_v1.configuration import Configuration
|
|
7
8
|
from .. import Logger
|
|
8
9
|
from .base_api_client import BaseApiClient
|
|
9
10
|
from ..exceptions.reporter import ReporterException
|
|
10
|
-
from ..models import Attachment
|
|
11
|
+
from ..models import Attachment
|
|
12
|
+
from ..models.config.framework import Video, Trace
|
|
11
13
|
from ..models.config.qaseconfig import QaseConfig
|
|
12
|
-
from ..models.
|
|
14
|
+
from ..models.config.testops import ConfigurationValue
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class ApiV1Client(BaseApiClient):
|
|
16
18
|
def __init__(self, config: QaseConfig, logger: Logger):
|
|
17
19
|
self.logger = logger
|
|
18
20
|
self.config = config
|
|
21
|
+
self.__authors = {}
|
|
19
22
|
|
|
20
23
|
try:
|
|
21
24
|
self.logger.log_debug("Preparing API client")
|
|
@@ -64,7 +67,74 @@ class ApiV1Client(BaseApiClient):
|
|
|
64
67
|
self.logger.log("Exception when calling EnvironmentsApi->get_environments: %s\n" % e, "error")
|
|
65
68
|
raise ReporterException(e)
|
|
66
69
|
|
|
67
|
-
def
|
|
70
|
+
def get_configurations(self, project_code: str):
|
|
71
|
+
"""Get all configurations for the project"""
|
|
72
|
+
try:
|
|
73
|
+
self.logger.log_debug(f"Getting configurations for project {project_code}")
|
|
74
|
+
api_instance = ConfigurationsApi(self.client)
|
|
75
|
+
response = api_instance.get_configurations(code=project_code)
|
|
76
|
+
if hasattr(response, 'result') and hasattr(response.result, 'entities'):
|
|
77
|
+
return response.result.entities
|
|
78
|
+
return []
|
|
79
|
+
except Exception as e:
|
|
80
|
+
self.logger.log(f"Exception when calling ConfigurationsApi->get_configurations: {e}", "error")
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
def find_or_create_configuration(self, project_code: str, config_value: ConfigurationValue) -> Union[int, None]:
|
|
84
|
+
"""Find existing configuration or create new one if createIfNotExists is True"""
|
|
85
|
+
try:
|
|
86
|
+
configurations = self.get_configurations(project_code)
|
|
87
|
+
|
|
88
|
+
# Search for existing configuration
|
|
89
|
+
for group in configurations:
|
|
90
|
+
if hasattr(group, 'configurations'):
|
|
91
|
+
for config in group.configurations:
|
|
92
|
+
# API returns configurations with 'title' field, not 'name' and 'value'
|
|
93
|
+
# We need to match group.title with config_value.name and config.title with config_value.value
|
|
94
|
+
config_title = config.title if hasattr(config, 'title') else 'No title'
|
|
95
|
+
group_title = group.title if hasattr(group, 'title') else 'No title'
|
|
96
|
+
|
|
97
|
+
if (group_title == config_value.name and config_title == config_value.value):
|
|
98
|
+
return config.id
|
|
99
|
+
|
|
100
|
+
# Configuration not found
|
|
101
|
+
if not self.config.testops.configurations.create_if_not_exists:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
# Create new configuration
|
|
105
|
+
# First, try to find existing group or create new one
|
|
106
|
+
group_id = None
|
|
107
|
+
for group in configurations:
|
|
108
|
+
if hasattr(group, 'title') and group.title == config_value.name:
|
|
109
|
+
group_id = group.id
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
if group_id is None:
|
|
113
|
+
# Create new group
|
|
114
|
+
group_create = ConfigurationGroupCreate(title=config_value.name)
|
|
115
|
+
group_response = ConfigurationsApi(self.client).create_configuration_group(
|
|
116
|
+
code=project_code,
|
|
117
|
+
configuration_group_create=group_create
|
|
118
|
+
)
|
|
119
|
+
group_id = group_response.result.id
|
|
120
|
+
|
|
121
|
+
# Create configuration in the group
|
|
122
|
+
config_create = ConfigurationCreate(
|
|
123
|
+
title=config_value.value,
|
|
124
|
+
group_id=group_id
|
|
125
|
+
)
|
|
126
|
+
config_response = ConfigurationsApi(self.client).create_configuration(
|
|
127
|
+
code=project_code,
|
|
128
|
+
configuration_create=config_create
|
|
129
|
+
)
|
|
130
|
+
config_id = config_response.result.id
|
|
131
|
+
return config_id
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
self.logger.log(f"Error at finding/creating configuration {config_value.name}={config_value.value}: {e}", "error")
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
def complete_run(self, project_code: str, run_id: int) -> None:
|
|
68
138
|
api_runs = RunsApi(self.client)
|
|
69
139
|
self.logger.log_debug(f"Completing run {run_id}")
|
|
70
140
|
res = api_runs.get_run(project_code, run_id).result
|
|
@@ -73,7 +143,7 @@ class ApiV1Client(BaseApiClient):
|
|
|
73
143
|
return
|
|
74
144
|
try:
|
|
75
145
|
api_runs.complete_run(project_code, run_id)
|
|
76
|
-
self.logger.log(f"
|
|
146
|
+
self.logger.log(f"Test run link: {self.web}/run/{project_code}/dashboard/{run_id}", "info")
|
|
77
147
|
except Exception as e:
|
|
78
148
|
self.logger.log(f"Error at completing run {run_id}: {e}", "error")
|
|
79
149
|
raise ReporterException(e)
|
|
@@ -88,27 +158,46 @@ class ApiV1Client(BaseApiClient):
|
|
|
88
158
|
|
|
89
159
|
except Exception as e:
|
|
90
160
|
self.logger.log(f"Error at uploading attachment: {e}", "error")
|
|
91
|
-
|
|
161
|
+
return None
|
|
92
162
|
|
|
93
163
|
def create_test_run(self, project_code: str, title: str, description: str, plan_id=None,
|
|
94
164
|
environment_id=None) -> str:
|
|
165
|
+
# Process configurations
|
|
166
|
+
configuration_ids = []
|
|
167
|
+
|
|
168
|
+
if self.config.testops.configurations and self.config.testops.configurations.values:
|
|
169
|
+
for config_value in self.config.testops.configurations.values:
|
|
170
|
+
config_id = self.find_or_create_configuration(project_code, config_value)
|
|
171
|
+
if config_id:
|
|
172
|
+
configuration_ids.append(config_id)
|
|
173
|
+
|
|
95
174
|
kwargs = dict(
|
|
96
175
|
title=title,
|
|
97
176
|
description=description,
|
|
98
177
|
environment_id=(int(environment_id) if environment_id else None),
|
|
99
178
|
plan_id=(int(plan_id) if plan_id else plan_id),
|
|
100
|
-
is_autotest=True
|
|
179
|
+
is_autotest=True,
|
|
180
|
+
start_time=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"),
|
|
181
|
+
tags=self.config.testops.run.tags
|
|
101
182
|
)
|
|
102
|
-
|
|
183
|
+
|
|
184
|
+
# Add configurations if any found
|
|
185
|
+
if configuration_ids:
|
|
186
|
+
kwargs['configurations'] = configuration_ids
|
|
187
|
+
|
|
103
188
|
try:
|
|
104
189
|
result = RunsApi(self.client).create_run(
|
|
105
190
|
code=project_code,
|
|
106
191
|
run_create=RunCreate(**{k: v for k, v in kwargs.items() if v is not None})
|
|
107
192
|
)
|
|
108
193
|
|
|
109
|
-
|
|
194
|
+
run_id = result.result.id
|
|
195
|
+
|
|
196
|
+
# Update external link if configured
|
|
197
|
+
if self.config.testops.run.external_link and run_id:
|
|
198
|
+
self.update_external_link(project_code, run_id)
|
|
110
199
|
|
|
111
|
-
return
|
|
200
|
+
return run_id
|
|
112
201
|
|
|
113
202
|
except Exception as e:
|
|
114
203
|
self.logger.log(f"Error at creating test run: {e}", "error")
|
|
@@ -121,139 +210,76 @@ class ApiV1Client(BaseApiClient):
|
|
|
121
210
|
return True
|
|
122
211
|
return False
|
|
123
212
|
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
results=results_to_send
|
|
133
|
-
)
|
|
134
|
-
)
|
|
135
|
-
self.logger.log_debug(f"Results for run {run_id} sent successfully")
|
|
136
|
-
|
|
137
|
-
def _prepare_result(self, project_code: str, result: Result) -> Dict:
|
|
138
|
-
attached = []
|
|
139
|
-
if result.attachments:
|
|
140
|
-
for attachment in result.attachments:
|
|
141
|
-
attached.extend(self._upload_attachment(project_code, attachment))
|
|
142
|
-
|
|
143
|
-
steps = []
|
|
144
|
-
for step in result.steps:
|
|
145
|
-
prepared = self._prepare_step(project_code, step)
|
|
146
|
-
steps.append(prepared)
|
|
147
|
-
|
|
148
|
-
case_data = {
|
|
149
|
-
"title": result.get_title(),
|
|
150
|
-
"description": result.get_field('description'),
|
|
151
|
-
"preconditions": result.get_field('preconditions'),
|
|
152
|
-
"postconditions": result.get_field('postconditions'),
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
for key, param in result.params.items():
|
|
156
|
-
# Hack to match old TestOps API
|
|
157
|
-
if param == "":
|
|
158
|
-
result.params[key] = "empty"
|
|
159
|
-
|
|
160
|
-
if result.get_field('severity'):
|
|
161
|
-
case_data["severity"] = result.get_field('severity')
|
|
162
|
-
|
|
163
|
-
if result.get_field('priority'):
|
|
164
|
-
case_data["priority"] = result.get_field('priority')
|
|
165
|
-
|
|
166
|
-
if result.get_field('layer'):
|
|
167
|
-
case_data["layer"] = result.get_field('layer')
|
|
168
|
-
|
|
169
|
-
suite = None
|
|
170
|
-
if result.get_suite_title():
|
|
171
|
-
suite = "\t".join(result.get_suite_title().split("."))
|
|
172
|
-
|
|
173
|
-
if result.get_field('suite'):
|
|
174
|
-
suite = result.get_field('suite')
|
|
175
|
-
|
|
176
|
-
root_suite = self.config.root_suite
|
|
177
|
-
if root_suite:
|
|
178
|
-
suite = f"{root_suite}\t{suite}"
|
|
179
|
-
|
|
180
|
-
if suite:
|
|
181
|
-
case_data["suite_title"] = suite
|
|
182
|
-
|
|
183
|
-
result_model = {
|
|
184
|
-
"status": result.execution.status,
|
|
185
|
-
"stacktrace": result.execution.stacktrace,
|
|
186
|
-
"time_ms": result.execution.duration,
|
|
187
|
-
"comment": result.message,
|
|
188
|
-
"attachments": [attach.hash for attach in attached],
|
|
189
|
-
"steps": steps,
|
|
190
|
-
"param": result.params,
|
|
191
|
-
"param_groups": result.param_groups,
|
|
192
|
-
"defect": self.config.testops.defect,
|
|
193
|
-
"case": case_data
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
test_ops_id = result.get_testops_id()
|
|
197
|
-
|
|
198
|
-
if test_ops_id:
|
|
199
|
-
result_model["case_id"] = test_ops_id
|
|
200
|
-
|
|
201
|
-
self.logger.log_debug(f"Prepared result: {result_model}")
|
|
202
|
-
|
|
203
|
-
return result_model
|
|
204
|
-
|
|
205
|
-
def _prepare_step(self, project_code: str, step: Step) -> Dict:
|
|
206
|
-
prepared_children = []
|
|
207
|
-
|
|
213
|
+
def enable_public_report(self, project_code: str, run_id: int) -> str:
|
|
214
|
+
"""
|
|
215
|
+
Enable public report for a test run and return the public link
|
|
216
|
+
|
|
217
|
+
:param project_code: project code
|
|
218
|
+
:param run_id: test run id
|
|
219
|
+
:return: public report link or None if failed
|
|
220
|
+
"""
|
|
208
221
|
try:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
Attachment(file_name='request_headers.txt', content=step.data.request_headers,
|
|
231
|
-
mime_type='text/plain', temporary=True))
|
|
232
|
-
if step.data.response_body:
|
|
233
|
-
step.attachments.append(Attachment(file_name='response_body.txt', content=step.data.response_body,
|
|
234
|
-
mime_type='text/plain', temporary=True))
|
|
235
|
-
if step.data.response_headers:
|
|
236
|
-
step.attachments.append(
|
|
237
|
-
Attachment(file_name='response_headers.txt', content=step.data.response_headers,
|
|
238
|
-
mime_type='text/plain', temporary=True))
|
|
239
|
-
|
|
240
|
-
if step.step_type == StepType.GHERKIN:
|
|
241
|
-
prepared_step['action'] = step.data.keyword
|
|
222
|
+
self.logger.log_debug(f"Enabling public report for run {run_id}")
|
|
223
|
+
api_runs = RunsApi(self.client)
|
|
224
|
+
|
|
225
|
+
# Create RunPublic object with status=True
|
|
226
|
+
run_public = RunPublic(status=True)
|
|
227
|
+
|
|
228
|
+
# Call the API to enable public report
|
|
229
|
+
response = api_runs.update_run_publicity(project_code, run_id, run_public)
|
|
230
|
+
|
|
231
|
+
# Extract the public URL from response
|
|
232
|
+
if response.result and response.result.url:
|
|
233
|
+
public_url = response.result.url
|
|
234
|
+
self.logger.log_debug(f"Public report enabled for run {run_id}: {public_url}")
|
|
235
|
+
return public_url
|
|
236
|
+
else:
|
|
237
|
+
self.logger.log_debug(f"Public report enabled for run {run_id} but no URL returned")
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
self.logger.log(f"Error at enabling public report for run {run_id}: {e}", "error")
|
|
242
|
+
return None
|
|
242
243
|
|
|
243
|
-
|
|
244
|
-
|
|
244
|
+
def update_external_link(self, project_code: str, run_id: int):
|
|
245
|
+
"""Update external link for a test run"""
|
|
246
|
+
try:
|
|
247
|
+
from qase.api_client_v1.models.runexternal_issues import RunexternalIssues
|
|
248
|
+
from qase.api_client_v1.models.runexternal_issues_links_inner import RunexternalIssuesLinksInner
|
|
249
|
+
|
|
250
|
+
external_link = self.config.testops.run.external_link
|
|
251
|
+
api_type = external_link.to_api_type()
|
|
252
|
+
|
|
253
|
+
run_external_issues = RunexternalIssues(
|
|
254
|
+
type=api_type,
|
|
255
|
+
links=[
|
|
256
|
+
RunexternalIssuesLinksInner(
|
|
257
|
+
run_id=run_id,
|
|
258
|
+
external_issue=external_link.link
|
|
259
|
+
)
|
|
260
|
+
]
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
RunsApi(self.client).run_update_external_issue(
|
|
264
|
+
code=project_code,
|
|
265
|
+
runexternal_issues=run_external_issues
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
self.logger.log(f"External link updated for run {run_id}: {external_link.link}", "debug")
|
|
269
|
+
|
|
270
|
+
except Exception as e:
|
|
271
|
+
self.logger.log(f"Error at updating external link: {e}", "error")
|
|
245
272
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
273
|
+
def __should_skip_attachment(self, attachment, result):
|
|
274
|
+
if (self.config.framework.playwright.video == Video.failed and
|
|
275
|
+
result.execution.status != 'failed' and
|
|
276
|
+
attachment.file_name == 'video.webm'):
|
|
277
|
+
return True
|
|
278
|
+
if (self.config.framework.playwright.trace == Trace.failed and
|
|
279
|
+
result.execution.status != 'failed' and
|
|
280
|
+
attachment.file_name == 'trace.zip'):
|
|
281
|
+
return True
|
|
282
|
+
return False
|
|
251
283
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
prepared_children.append(self._prepare_step(project_code, substep))
|
|
255
|
-
prepared_step["steps"] = prepared_children
|
|
256
|
-
return prepared_step
|
|
257
|
-
except Exception as e:
|
|
258
|
-
self.logger.log(f"Error at preparing step: {e}", "error")
|
|
259
|
-
raise ReporterException(e)
|
|
284
|
+
def send_results(self, project_code: str, run_id: str, results: []) -> None:
|
|
285
|
+
raise NotImplementedError("use ApiV2Client instead")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import Dict
|
|
2
2
|
|
|
3
3
|
import certifi
|
|
4
|
-
from qase.api_client_v2 import ResultsApi
|
|
4
|
+
from qase.api_client_v2 import ResultsApi, ResultCreateFields
|
|
5
5
|
from qase.api_client_v2.api_client import ApiClient
|
|
6
6
|
from qase.api_client_v2.configuration import Configuration
|
|
7
7
|
from qase.api_client_v2.models.create_results_request_v2 import CreateResultsRequestV2
|
|
@@ -16,6 +16,7 @@ from qase.api_client_v2.models.result_steps_type import ResultStepsType
|
|
|
16
16
|
from .api_v1_client import ApiV1Client
|
|
17
17
|
from .. import Logger
|
|
18
18
|
from ..exceptions.reporter import ReporterException
|
|
19
|
+
from ..models.config.framework import Video, Trace
|
|
19
20
|
from ..models import Attachment, Result
|
|
20
21
|
from ..models.config.qaseconfig import QaseConfig
|
|
21
22
|
from ..models.step import StepType, Step
|
|
@@ -56,7 +57,11 @@ class ApiV2Client(ApiV1Client):
|
|
|
56
57
|
attached = []
|
|
57
58
|
if result.attachments:
|
|
58
59
|
for attachment in result.attachments:
|
|
59
|
-
|
|
60
|
+
if self.__should_skip_attachment(attachment, result):
|
|
61
|
+
continue
|
|
62
|
+
attach_id = self._upload_attachment(project_code, attachment)
|
|
63
|
+
if attach_id:
|
|
64
|
+
attached.extend(attach_id)
|
|
60
65
|
|
|
61
66
|
steps = []
|
|
62
67
|
for step in result.steps:
|
|
@@ -71,27 +76,29 @@ class ApiV2Client(ApiV1Client):
|
|
|
71
76
|
result_model_v2 = ResultCreate(
|
|
72
77
|
title=result.get_title(),
|
|
73
78
|
signature=result.signature,
|
|
74
|
-
|
|
75
|
-
execution=ResultExecution(
|
|
76
|
-
|
|
79
|
+
testops_ids=result.get_testops_ids(),
|
|
80
|
+
execution=ResultExecution(status=result.execution.status, duration=result.execution.duration,
|
|
81
|
+
start_time=result.execution.start_time, end_time=result.execution.end_time,
|
|
77
82
|
stacktrace=result.execution.stacktrace, thread=result.execution.thread),
|
|
78
|
-
fields=result.fields,
|
|
83
|
+
fields=ResultCreateFields.from_dict(result.fields),
|
|
79
84
|
attachments=[attach.hash for attach in attached],
|
|
80
85
|
steps=steps,
|
|
81
|
-
|
|
82
|
-
params=result.params,
|
|
83
|
-
|
|
86
|
+
steps_type=ResultStepsType.CLASSIC,
|
|
87
|
+
params=result.params if not self.config.exclude_params else {key: value for key, value in result.params.items() if key not in self.config.exclude_params},
|
|
88
|
+
param_groups=result.param_groups,
|
|
84
89
|
message=result.message,
|
|
90
|
+
defect=self.config.testops.defect,
|
|
85
91
|
)
|
|
86
92
|
|
|
87
|
-
if result.
|
|
93
|
+
if result.relations is not None and result.relations.suite is not None and len(
|
|
94
|
+
result.relations.suite.data) != 0:
|
|
88
95
|
data = []
|
|
89
96
|
root_suite = self.config.root_suite
|
|
90
97
|
if root_suite:
|
|
91
98
|
data.append(RelationSuiteItem(title=root_suite))
|
|
92
99
|
|
|
93
|
-
for
|
|
94
|
-
data.append(RelationSuiteItem(title=
|
|
100
|
+
for raw in result.relations.suite.data:
|
|
101
|
+
data.append(RelationSuiteItem(title=raw.title))
|
|
95
102
|
|
|
96
103
|
result_model_v2.relations = ResultRelations(suite=RelationSuite(data=data))
|
|
97
104
|
|
|
@@ -104,8 +111,16 @@ class ApiV2Client(ApiV1Client):
|
|
|
104
111
|
|
|
105
112
|
try:
|
|
106
113
|
prepared_step = {'execution': {}, 'data': {}, 'steps': []}
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
if step.execution.status == 'untested':
|
|
115
|
+
prepared_step['execution']['status'] = ResultStepStatus('skipped')
|
|
116
|
+
prepared_step['execution']['duration'] = 0
|
|
117
|
+
prepared_step['execution']['start_time'] = None
|
|
118
|
+
prepared_step['execution']['end_time'] = None
|
|
119
|
+
else:
|
|
120
|
+
prepared_step['execution']['status'] = ResultStepStatus(step.execution.status)
|
|
121
|
+
prepared_step['execution']['duration'] = step.execution.duration
|
|
122
|
+
prepared_step['execution']['start_time'] = step.execution.start_time
|
|
123
|
+
prepared_step['execution']['end_time'] = step.execution.end_time
|
|
109
124
|
|
|
110
125
|
if step.step_type == StepType.TEXT:
|
|
111
126
|
prepared_step['data']['action'] = step.data.action
|
|
@@ -116,31 +131,59 @@ class ApiV2Client(ApiV1Client):
|
|
|
116
131
|
prepared_step['data']['action'] = step.data.request_method + " " + step.data.request_url
|
|
117
132
|
|
|
118
133
|
if step.data.request_body:
|
|
119
|
-
step.
|
|
134
|
+
step.add_attachment(
|
|
120
135
|
Attachment(file_name='request_body.txt', content=step.data.request_body, mime_type='text/plain',
|
|
121
136
|
temporary=True))
|
|
122
137
|
if step.data.request_headers:
|
|
123
|
-
step.
|
|
138
|
+
step.add_attachment(
|
|
124
139
|
Attachment(file_name='request_headers.txt', content=step.data.request_headers,
|
|
125
140
|
mime_type='text/plain', temporary=True))
|
|
126
141
|
if step.data.response_body:
|
|
127
|
-
step.
|
|
128
|
-
|
|
142
|
+
step.add_attachment(Attachment(file_name='response_body.txt', content=step.data.response_body,
|
|
143
|
+
mime_type='text/plain', temporary=True))
|
|
129
144
|
if step.data.response_headers:
|
|
130
|
-
step.
|
|
145
|
+
step.add_attachment(
|
|
131
146
|
Attachment(file_name='response_headers.txt', content=step.data.response_headers,
|
|
132
147
|
mime_type='text/plain', temporary=True))
|
|
133
148
|
|
|
134
149
|
if step.step_type == StepType.GHERKIN:
|
|
135
|
-
|
|
150
|
+
action = step.data.keyword
|
|
151
|
+
if step.data.keyword != step.data.name:
|
|
152
|
+
action += " " + step.data.name
|
|
153
|
+
prepared_step['data']['action'] = action
|
|
136
154
|
|
|
137
155
|
if step.step_type == StepType.SLEEP:
|
|
138
156
|
prepared_step['data']['action'] = f"Sleep for {step.data.duration} seconds"
|
|
139
157
|
|
|
140
|
-
if step.
|
|
158
|
+
if step.step_type == StepType.DB_QUERY:
|
|
159
|
+
# Format database query as action
|
|
160
|
+
action_parts = []
|
|
161
|
+
if step.data.database_type:
|
|
162
|
+
action_parts.append(f"[{step.data.database_type}]")
|
|
163
|
+
action_parts.append(step.data.query)
|
|
164
|
+
prepared_step['data']['action'] = " ".join(action_parts)
|
|
165
|
+
|
|
166
|
+
# Add expected_result if available
|
|
167
|
+
if step.data.expected_result:
|
|
168
|
+
prepared_step['data']['expected_result'] = step.data.expected_result
|
|
169
|
+
|
|
170
|
+
# Add connection info and execution time as input_data
|
|
171
|
+
info_parts = []
|
|
172
|
+
if step.data.connection_info:
|
|
173
|
+
info_parts.append(f"Connection: {step.data.connection_info}")
|
|
174
|
+
if step.data.execution_time is not None:
|
|
175
|
+
info_parts.append(f"Execution time: {step.data.execution_time:.3f}s")
|
|
176
|
+
if step.data.rows_affected is not None:
|
|
177
|
+
info_parts.append(f"Rows affected: {step.data.rows_affected}")
|
|
178
|
+
if info_parts:
|
|
179
|
+
prepared_step['data']['input_data'] = " | ".join(info_parts)
|
|
180
|
+
|
|
181
|
+
if step.execution.attachments:
|
|
141
182
|
uploaded_attachments = []
|
|
142
|
-
for file in step.attachments:
|
|
143
|
-
|
|
183
|
+
for file in step.execution.attachments:
|
|
184
|
+
attach_id = self._upload_attachment(project_code, file)
|
|
185
|
+
if attach_id:
|
|
186
|
+
uploaded_attachments.extend(attach_id)
|
|
144
187
|
|
|
145
188
|
prepared_step['execution']['attachments'] = [attach.hash for attach in uploaded_attachments]
|
|
146
189
|
|
|
@@ -152,3 +195,14 @@ class ApiV2Client(ApiV1Client):
|
|
|
152
195
|
except Exception as e:
|
|
153
196
|
self.logger.log(f"Error at preparing step: {e}", "error")
|
|
154
197
|
raise ReporterException(e)
|
|
198
|
+
|
|
199
|
+
def __should_skip_attachment(self, attachment, result):
|
|
200
|
+
if (self.config.framework.playwright.video == Video.failed and
|
|
201
|
+
result.execution.status != 'failed' and
|
|
202
|
+
attachment.file_name == 'video.webm'):
|
|
203
|
+
return True
|
|
204
|
+
if (self.config.framework.playwright.trace == Trace.failed and
|
|
205
|
+
result.execution.status != 'failed' and
|
|
206
|
+
attachment.file_name == 'trace.zip'):
|
|
207
|
+
return True
|
|
208
|
+
return False
|
|
@@ -30,7 +30,7 @@ class BaseApiClient(abc.ABC):
|
|
|
30
30
|
pass
|
|
31
31
|
|
|
32
32
|
@abc.abstractmethod
|
|
33
|
-
def complete_run(self, project_code: str, run_id:
|
|
33
|
+
def complete_run(self, project_code: str, run_id: int) -> None:
|
|
34
34
|
"""
|
|
35
35
|
Complete a test run in Qase TestOps
|
|
36
36
|
|
|
@@ -86,3 +86,14 @@ class BaseApiClient(abc.ABC):
|
|
|
86
86
|
:return: None
|
|
87
87
|
"""
|
|
88
88
|
pass
|
|
89
|
+
|
|
90
|
+
@abc.abstractmethod
|
|
91
|
+
def enable_public_report(self, project_code: str, run_id: int) -> str:
|
|
92
|
+
"""
|
|
93
|
+
Enable public report for a test run and return the public link
|
|
94
|
+
|
|
95
|
+
:param project_code: project code
|
|
96
|
+
:param run_id: test run id
|
|
97
|
+
:return: public report link or None if failed
|
|
98
|
+
"""
|
|
99
|
+
pass
|