qase-python-commons 3.4.1__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 +153 -5
- qase/commons/client/api_v2_client.py +34 -5
- qase/commons/client/base_api_client.py +12 -1
- qase/commons/config.py +139 -15
- qase/commons/logger.py +63 -7
- qase/commons/models/attachment.py +11 -8
- qase/commons/models/config/qaseconfig.py +34 -0
- qase/commons/models/config/run.py +19 -0
- qase/commons/models/config/testops.py +46 -0
- qase/commons/models/external_link.py +41 -0
- qase/commons/models/result.py +10 -4
- qase/commons/models/run.py +3 -0
- qase/commons/models/runtime.py +9 -0
- qase/commons/models/step.py +37 -9
- qase/commons/profilers/__init__.py +4 -3
- qase/commons/profilers/db.py +965 -5
- qase/commons/reporters/core.py +42 -6
- qase/commons/reporters/report.py +2 -2
- qase/commons/reporters/testops.py +48 -10
- qase/commons/status_mapping/__init__.py +12 -0
- qase/commons/status_mapping/status_mapping.py +237 -0
- qase/commons/utils.py +95 -0
- {qase_python_commons-3.4.1.dist-info → qase_python_commons-4.1.3.dist-info}/METADATA +12 -7
- qase_python_commons-4.1.3.dist-info/RECORD +45 -0
- {qase_python_commons-3.4.1.dist-info → qase_python_commons-4.1.3.dist-info}/WHEEL +1 -1
- qase_python_commons-3.4.1.dist-info/RECORD +0 -41
- {qase_python_commons-3.4.1.dist-info → qase_python_commons-4.1.3.dist-info}/top_level.txt +0 -0
qase/__init__.py
ADDED
|
@@ -3,7 +3,7 @@ from typing import Union
|
|
|
3
3
|
|
|
4
4
|
import certifi
|
|
5
5
|
from qase.api_client_v1 import ApiClient, ProjectsApi, Project, EnvironmentsApi, RunsApi, AttachmentsApi, \
|
|
6
|
-
AttachmentGet, RunCreate
|
|
6
|
+
AttachmentGet, RunCreate, ConfigurationsApi, ConfigurationCreate, ConfigurationGroupCreate, RunPublic
|
|
7
7
|
from qase.api_client_v1.configuration import Configuration
|
|
8
8
|
from .. import Logger
|
|
9
9
|
from .base_api_client import BaseApiClient
|
|
@@ -11,6 +11,7 @@ from ..exceptions.reporter import ReporterException
|
|
|
11
11
|
from ..models import Attachment
|
|
12
12
|
from ..models.config.framework import Video, Trace
|
|
13
13
|
from ..models.config.qaseconfig import QaseConfig
|
|
14
|
+
from ..models.config.testops import ConfigurationValue
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class ApiV1Client(BaseApiClient):
|
|
@@ -66,7 +67,74 @@ class ApiV1Client(BaseApiClient):
|
|
|
66
67
|
self.logger.log("Exception when calling EnvironmentsApi->get_environments: %s\n" % e, "error")
|
|
67
68
|
raise ReporterException(e)
|
|
68
69
|
|
|
69
|
-
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:
|
|
70
138
|
api_runs = RunsApi(self.client)
|
|
71
139
|
self.logger.log_debug(f"Completing run {run_id}")
|
|
72
140
|
res = api_runs.get_run(project_code, run_id).result
|
|
@@ -94,22 +162,42 @@ class ApiV1Client(BaseApiClient):
|
|
|
94
162
|
|
|
95
163
|
def create_test_run(self, project_code: str, title: str, description: str, plan_id=None,
|
|
96
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
|
+
|
|
97
174
|
kwargs = dict(
|
|
98
175
|
title=title,
|
|
99
176
|
description=description,
|
|
100
177
|
environment_id=(int(environment_id) if environment_id else None),
|
|
101
178
|
plan_id=(int(plan_id) if plan_id else plan_id),
|
|
102
179
|
is_autotest=True,
|
|
103
|
-
start_time=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
180
|
+
start_time=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"),
|
|
181
|
+
tags=self.config.testops.run.tags
|
|
104
182
|
)
|
|
105
|
-
|
|
183
|
+
|
|
184
|
+
# Add configurations if any found
|
|
185
|
+
if configuration_ids:
|
|
186
|
+
kwargs['configurations'] = configuration_ids
|
|
187
|
+
|
|
106
188
|
try:
|
|
107
189
|
result = RunsApi(self.client).create_run(
|
|
108
190
|
code=project_code,
|
|
109
191
|
run_create=RunCreate(**{k: v for k, v in kwargs.items() if v is not None})
|
|
110
192
|
)
|
|
111
193
|
|
|
112
|
-
|
|
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)
|
|
199
|
+
|
|
200
|
+
return run_id
|
|
113
201
|
|
|
114
202
|
except Exception as e:
|
|
115
203
|
self.logger.log(f"Error at creating test run: {e}", "error")
|
|
@@ -122,6 +210,66 @@ class ApiV1Client(BaseApiClient):
|
|
|
122
210
|
return True
|
|
123
211
|
return False
|
|
124
212
|
|
|
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
|
+
"""
|
|
221
|
+
try:
|
|
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
|
|
243
|
+
|
|
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")
|
|
272
|
+
|
|
125
273
|
def __should_skip_attachment(self, attachment, result):
|
|
126
274
|
if (self.config.framework.playwright.video == Video.failed and
|
|
127
275
|
result.execution.status != 'failed' and
|
|
@@ -84,7 +84,7 @@ class ApiV2Client(ApiV1Client):
|
|
|
84
84
|
attachments=[attach.hash for attach in attached],
|
|
85
85
|
steps=steps,
|
|
86
86
|
steps_type=ResultStepsType.CLASSIC,
|
|
87
|
-
params=result.params,
|
|
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
88
|
param_groups=result.param_groups,
|
|
89
89
|
message=result.message,
|
|
90
90
|
defect=self.config.testops.defect,
|
|
@@ -111,10 +111,16 @@ class ApiV2Client(ApiV1Client):
|
|
|
111
111
|
|
|
112
112
|
try:
|
|
113
113
|
prepared_step = {'execution': {}, 'data': {}, 'steps': []}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
118
124
|
|
|
119
125
|
if step.step_type == StepType.TEXT:
|
|
120
126
|
prepared_step['data']['action'] = step.data.action
|
|
@@ -149,6 +155,29 @@ class ApiV2Client(ApiV1Client):
|
|
|
149
155
|
if step.step_type == StepType.SLEEP:
|
|
150
156
|
prepared_step['data']['action'] = f"Sleep for {step.data.duration} seconds"
|
|
151
157
|
|
|
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
|
+
|
|
152
181
|
if step.execution.attachments:
|
|
153
182
|
uploaded_attachments = []
|
|
154
183
|
for file in step.execution.attachments:
|
|
@@ -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
|
qase/commons/config.py
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import json
|
|
3
|
-
from .logger import Logger
|
|
3
|
+
from .logger import Logger, LoggingOptions
|
|
4
4
|
from .models.config.qaseconfig import QaseConfig, Mode
|
|
5
|
+
from .utils import QaseUtils
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class ConfigManager:
|
|
8
9
|
|
|
9
10
|
def __init__(self, config_file='./qase.config.json'):
|
|
10
|
-
self.logger = Logger()
|
|
11
11
|
self.__config_file = config_file
|
|
12
12
|
self.config = QaseConfig()
|
|
13
13
|
|
|
14
|
+
# Initialize temporary logger for error handling during config loading
|
|
15
|
+
self.logger = Logger(debug=False)
|
|
16
|
+
|
|
14
17
|
self.__load_file_config()
|
|
15
18
|
self.__load_env_config()
|
|
19
|
+
|
|
20
|
+
# Re-initialize logger with proper logging options after config is loaded
|
|
21
|
+
logging_options = LoggingOptions(
|
|
22
|
+
console=self.config.logging.console if self.config.logging.console is not None else True,
|
|
23
|
+
file=self.config.logging.file if self.config.logging.file is not None else self.config.debug
|
|
24
|
+
)
|
|
25
|
+
self.logger = Logger(debug=self.config.debug, logging_options=logging_options)
|
|
16
26
|
|
|
17
27
|
def validate_config(self):
|
|
18
28
|
errors: list[str] = []
|
|
@@ -53,15 +63,26 @@ class ConfigManager:
|
|
|
53
63
|
if config.get("profilers"):
|
|
54
64
|
self.config.set_profilers(config.get("profilers"))
|
|
55
65
|
|
|
56
|
-
if config.get("debug"):
|
|
66
|
+
if config.get("debug") is not None:
|
|
57
67
|
self.config.set_debug(
|
|
58
68
|
config.get("debug")
|
|
59
69
|
)
|
|
60
70
|
|
|
71
|
+
if config.get("excludeParams"):
|
|
72
|
+
self.config.set_exclude_params(
|
|
73
|
+
config.get("excludeParams")
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if config.get("statusMapping"):
|
|
77
|
+
self.config.set_status_mapping(
|
|
78
|
+
config.get("statusMapping")
|
|
79
|
+
)
|
|
80
|
+
|
|
61
81
|
if config.get("executionPlan"):
|
|
62
82
|
execution_plan = config.get("executionPlan")
|
|
63
83
|
if execution_plan.get("path"):
|
|
64
|
-
self.config.execution_plan.set_path(
|
|
84
|
+
self.config.execution_plan.set_path(
|
|
85
|
+
execution_plan.get("path"))
|
|
65
86
|
|
|
66
87
|
if config.get("testops"):
|
|
67
88
|
testops = config.get("testops")
|
|
@@ -70,15 +91,18 @@ class ConfigManager:
|
|
|
70
91
|
api = testops.get("api")
|
|
71
92
|
|
|
72
93
|
if api.get("host"):
|
|
73
|
-
self.config.testops.api.set_host(
|
|
94
|
+
self.config.testops.api.set_host(
|
|
95
|
+
api.get("host"))
|
|
74
96
|
|
|
75
97
|
if api.get("token"):
|
|
76
|
-
self.config.testops.api.set_token(
|
|
98
|
+
self.config.testops.api.set_token(
|
|
99
|
+
api.get("token"))
|
|
77
100
|
|
|
78
101
|
if testops.get("project"):
|
|
79
|
-
self.config.testops.set_project(
|
|
102
|
+
self.config.testops.set_project(
|
|
103
|
+
testops.get("project"))
|
|
80
104
|
|
|
81
|
-
if testops.get("defect"):
|
|
105
|
+
if testops.get("defect") is not None:
|
|
82
106
|
self.config.testops.set_defect(
|
|
83
107
|
testops.get("defect")
|
|
84
108
|
)
|
|
@@ -96,21 +120,59 @@ class ConfigManager:
|
|
|
96
120
|
self.config.testops.run.set_id(run.get("id"))
|
|
97
121
|
|
|
98
122
|
if run.get("title"):
|
|
99
|
-
self.config.testops.run.set_title(
|
|
123
|
+
self.config.testops.run.set_title(
|
|
124
|
+
run.get("title"))
|
|
100
125
|
|
|
101
126
|
if run.get("description"):
|
|
102
|
-
self.config.testops.run.set_description(
|
|
127
|
+
self.config.testops.run.set_description(
|
|
128
|
+
run.get("description"))
|
|
103
129
|
|
|
104
|
-
if run.get("complete"):
|
|
130
|
+
if run.get("complete") is not None:
|
|
105
131
|
self.config.testops.run.set_complete(
|
|
106
132
|
run.get("complete")
|
|
107
133
|
)
|
|
108
134
|
|
|
135
|
+
if run.get("tags"):
|
|
136
|
+
self.config.testops.run.set_tags(
|
|
137
|
+
[tag.strip() for tag in run.get("tags")])
|
|
138
|
+
|
|
139
|
+
if run.get("externalLink"):
|
|
140
|
+
self.config.testops.run.set_external_link(
|
|
141
|
+
run.get("externalLink"))
|
|
142
|
+
|
|
109
143
|
if testops.get("batch"):
|
|
110
144
|
batch = testops.get("batch")
|
|
111
145
|
|
|
112
146
|
if batch.get("size"):
|
|
113
|
-
self.config.testops.batch.set_size(
|
|
147
|
+
self.config.testops.batch.set_size(
|
|
148
|
+
batch.get("size"))
|
|
149
|
+
|
|
150
|
+
if testops.get("configurations"):
|
|
151
|
+
configurations = testops.get("configurations")
|
|
152
|
+
|
|
153
|
+
if configurations.get("values"):
|
|
154
|
+
values = configurations.get("values")
|
|
155
|
+
for value in values:
|
|
156
|
+
if value.get("name") and value.get("value"):
|
|
157
|
+
self.config.testops.configurations.add_value(
|
|
158
|
+
value.get("name"), value.get("value"))
|
|
159
|
+
|
|
160
|
+
if configurations.get("createIfNotExists") is not None:
|
|
161
|
+
self.config.testops.configurations.set_create_if_not_exists(
|
|
162
|
+
configurations.get("createIfNotExists"))
|
|
163
|
+
|
|
164
|
+
if testops.get("statusFilter"):
|
|
165
|
+
status_filter = testops.get("statusFilter")
|
|
166
|
+
if isinstance(status_filter, list):
|
|
167
|
+
self.config.testops.set_status_filter(status_filter)
|
|
168
|
+
elif isinstance(status_filter, str):
|
|
169
|
+
# Parse comma-separated string
|
|
170
|
+
self.config.testops.set_status_filter([s.strip() for s in status_filter.split(',')])
|
|
171
|
+
|
|
172
|
+
if testops.get("showPublicReportLink") is not None:
|
|
173
|
+
self.config.testops.set_show_public_report_link(
|
|
174
|
+
testops.get("showPublicReportLink")
|
|
175
|
+
)
|
|
114
176
|
|
|
115
177
|
if config.get("report"):
|
|
116
178
|
report = config.get("report")
|
|
@@ -122,7 +184,8 @@ class ConfigManager:
|
|
|
122
184
|
connection = report.get("connection")
|
|
123
185
|
|
|
124
186
|
if connection.get("path"):
|
|
125
|
-
self.config.report.connection.set_path(
|
|
187
|
+
self.config.report.connection.set_path(
|
|
188
|
+
connection.get("path"))
|
|
126
189
|
|
|
127
190
|
if connection.get("format"):
|
|
128
191
|
self.config.report.connection.set_format(
|
|
@@ -135,7 +198,7 @@ class ConfigManager:
|
|
|
135
198
|
if framework.get("pytest"):
|
|
136
199
|
pytest = framework.get("pytest")
|
|
137
200
|
|
|
138
|
-
if pytest.get("captureLogs"):
|
|
201
|
+
if pytest.get("captureLogs") is not None:
|
|
139
202
|
self.config.framework.pytest.set_capture_logs(
|
|
140
203
|
pytest.get("captureLogs")
|
|
141
204
|
)
|
|
@@ -153,6 +216,9 @@ class ConfigManager:
|
|
|
153
216
|
xfail_status.get("xpass")
|
|
154
217
|
)
|
|
155
218
|
|
|
219
|
+
if config.get("logging"):
|
|
220
|
+
self.config.set_logging(config.get("logging"))
|
|
221
|
+
|
|
156
222
|
except Exception as e:
|
|
157
223
|
self.logger.log("Failed to load config from file", "error")
|
|
158
224
|
|
|
@@ -177,6 +243,23 @@ class ConfigManager:
|
|
|
177
243
|
if key == 'QASE_DEBUG':
|
|
178
244
|
self.config.set_debug(value)
|
|
179
245
|
|
|
246
|
+
if key == 'QASE_EXCLUDE_PARAMS':
|
|
247
|
+
self.config.set_exclude_params(
|
|
248
|
+
[param.strip() for param in value.split(',')])
|
|
249
|
+
|
|
250
|
+
if key == 'QASE_STATUS_MAPPING':
|
|
251
|
+
# Parse status mapping from environment variable
|
|
252
|
+
# Format: "source1=target1,source2=target2"
|
|
253
|
+
if value:
|
|
254
|
+
mapping_dict = {}
|
|
255
|
+
pairs = value.split(',')
|
|
256
|
+
for pair in pairs:
|
|
257
|
+
pair = pair.strip()
|
|
258
|
+
if pair and '=' in pair:
|
|
259
|
+
source_status, target_status = pair.split('=', 1)
|
|
260
|
+
mapping_dict[source_status.strip()] = target_status.strip()
|
|
261
|
+
self.config.set_status_mapping(mapping_dict)
|
|
262
|
+
|
|
180
263
|
if key == 'QASE_EXECUTION_PLAN_PATH':
|
|
181
264
|
self.config.execution_plan.set_path(value)
|
|
182
265
|
|
|
@@ -207,9 +290,45 @@ class ConfigManager:
|
|
|
207
290
|
if key == 'QASE_TESTOPS_RUN_COMPLETE':
|
|
208
291
|
self.config.testops.run.set_complete(value)
|
|
209
292
|
|
|
293
|
+
if key == 'QASE_TESTOPS_RUN_TAGS':
|
|
294
|
+
self.config.testops.run.set_tags(
|
|
295
|
+
[tag.strip() for tag in value.split(',')])
|
|
296
|
+
|
|
297
|
+
if key == 'QASE_TESTOPS_RUN_EXTERNAL_LINK_TYPE':
|
|
298
|
+
if not self.config.testops.run.external_link:
|
|
299
|
+
from .models.external_link import ExternalLinkConfig
|
|
300
|
+
self.config.testops.run.external_link = ExternalLinkConfig()
|
|
301
|
+
self.config.testops.run.external_link.set_type(value)
|
|
302
|
+
|
|
303
|
+
if key == 'QASE_TESTOPS_RUN_EXTERNAL_LINK_URL':
|
|
304
|
+
if not self.config.testops.run.external_link:
|
|
305
|
+
from .models.external_link import ExternalLinkConfig
|
|
306
|
+
self.config.testops.run.external_link = ExternalLinkConfig()
|
|
307
|
+
self.config.testops.run.external_link.set_link(value)
|
|
308
|
+
|
|
210
309
|
if key == 'QASE_TESTOPS_BATCH_SIZE':
|
|
211
310
|
self.config.testops.batch.set_size(value)
|
|
212
311
|
|
|
312
|
+
if key == 'QASE_TESTOPS_CONFIGURATIONS_VALUES':
|
|
313
|
+
# Parse configurations from environment variable
|
|
314
|
+
# Format: "group1=value1,group2=value2"
|
|
315
|
+
if value:
|
|
316
|
+
config_pairs = value.split(',')
|
|
317
|
+
for pair in config_pairs:
|
|
318
|
+
if '=' in pair:
|
|
319
|
+
name, config_value = pair.split('=', 1)
|
|
320
|
+
self.config.testops.configurations.add_value(name.strip(), config_value.strip())
|
|
321
|
+
|
|
322
|
+
if key == 'QASE_TESTOPS_CONFIGURATIONS_CREATE_IF_NOT_EXISTS':
|
|
323
|
+
self.config.testops.configurations.set_create_if_not_exists(value)
|
|
324
|
+
|
|
325
|
+
if key == 'QASE_TESTOPS_STATUS_FILTER':
|
|
326
|
+
# Parse comma-separated string
|
|
327
|
+
self.config.testops.set_status_filter([s.strip() for s in value.split(',')])
|
|
328
|
+
|
|
329
|
+
if key == 'QASE_TESTOPS_SHOW_PUBLIC_REPORT_LINK':
|
|
330
|
+
self.config.testops.set_show_public_report_link(value)
|
|
331
|
+
|
|
213
332
|
if key == 'QASE_REPORT_DRIVER':
|
|
214
333
|
self.config.report.set_driver(value)
|
|
215
334
|
|
|
@@ -228,6 +347,11 @@ class ConfigManager:
|
|
|
228
347
|
if key == 'QASE_PYTEST_XFAIL_STATUS_XPASS':
|
|
229
348
|
self.config.framework.pytest.xfail_status.set_xpass(value)
|
|
230
349
|
|
|
350
|
+
if key == 'QASE_LOGGING_CONSOLE':
|
|
351
|
+
self.config.logging.set_console(QaseUtils.parse_bool(value))
|
|
352
|
+
|
|
353
|
+
if key == 'QASE_LOGGING_FILE':
|
|
354
|
+
self.config.logging.set_file(QaseUtils.parse_bool(value))
|
|
231
355
|
|
|
232
356
|
except Exception as e:
|
|
233
|
-
self.logger.log("Failed to load config from env vars {e}", "error")
|
|
357
|
+
self.logger.log(f"Failed to load config from env vars {e}", "error")
|
qase/commons/logger.py
CHANGED
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import datetime
|
|
3
3
|
import threading
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LoggingOptions:
|
|
8
|
+
def __init__(self, console: bool = True, file: bool = False):
|
|
9
|
+
self.console = console
|
|
10
|
+
self.file = file
|
|
4
11
|
|
|
5
12
|
|
|
6
13
|
class Logger:
|
|
7
14
|
_log_file = None
|
|
8
15
|
|
|
9
|
-
def __init__(self, debug: bool = False, prefix: str = '', dir: str = os.path.join('.', 'logs')
|
|
16
|
+
def __init__(self, debug: bool = False, prefix: str = '', dir: str = os.path.join('.', 'logs'),
|
|
17
|
+
logging_options: Optional[LoggingOptions] = None) -> None:
|
|
10
18
|
self.debug = debug
|
|
11
|
-
|
|
19
|
+
self.prefix = prefix
|
|
20
|
+
self.dir = dir
|
|
21
|
+
|
|
22
|
+
# Initialize logging options
|
|
23
|
+
if logging_options is None:
|
|
24
|
+
# Default behavior: console always enabled, file enabled only in debug mode
|
|
25
|
+
self.logging_options = LoggingOptions(
|
|
26
|
+
console=True,
|
|
27
|
+
file=debug
|
|
28
|
+
)
|
|
29
|
+
else:
|
|
30
|
+
self.logging_options = logging_options
|
|
31
|
+
|
|
32
|
+
# Override with environment variables if set
|
|
33
|
+
self._load_env_logging_options()
|
|
34
|
+
|
|
35
|
+
# Setup file logging if enabled
|
|
36
|
+
if self.logging_options.file:
|
|
12
37
|
if Logger._log_file is None:
|
|
13
38
|
timestamp = self._get_timestamp()
|
|
14
39
|
filename = f'{prefix}_{timestamp}.log'
|
|
@@ -21,16 +46,38 @@ class Logger:
|
|
|
21
46
|
|
|
22
47
|
self.lock = threading.Lock()
|
|
23
48
|
|
|
49
|
+
def _load_env_logging_options(self):
|
|
50
|
+
"""Load logging options from environment variables"""
|
|
51
|
+
# QASE_LOGGING_CONSOLE
|
|
52
|
+
console_env = os.environ.get('QASE_LOGGING_CONSOLE')
|
|
53
|
+
if console_env is not None:
|
|
54
|
+
self.logging_options.console = console_env.lower() in ('true', '1', 'yes', 'on')
|
|
55
|
+
|
|
56
|
+
# QASE_LOGGING_FILE
|
|
57
|
+
file_env = os.environ.get('QASE_LOGGING_FILE')
|
|
58
|
+
if file_env is not None:
|
|
59
|
+
self.logging_options.file = file_env.lower() in ('true', '1', 'yes', 'on')
|
|
60
|
+
|
|
61
|
+
# Legacy QASE_DEBUG support
|
|
62
|
+
debug_env = os.environ.get('QASE_DEBUG')
|
|
63
|
+
if debug_env is not None and debug_env.lower() in ('true', '1', 'yes', 'on'):
|
|
64
|
+
# When debug is enabled via env, enable file logging if not explicitly disabled
|
|
65
|
+
if not hasattr(self.logging_options, 'file') or self.logging_options.file is None:
|
|
66
|
+
self.logging_options.file = True
|
|
67
|
+
|
|
24
68
|
def log(self, message: str, level: str = 'info'):
|
|
25
69
|
time_str = self._get_timestamp("%H:%M:%S")
|
|
26
70
|
log = f"[Qase][{time_str}][{level}] {message}\n"
|
|
27
71
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
72
|
+
# Console output
|
|
73
|
+
if self.logging_options.console:
|
|
74
|
+
try:
|
|
75
|
+
print(log, end='')
|
|
76
|
+
except (OSError, IOError):
|
|
77
|
+
pass
|
|
32
78
|
|
|
33
|
-
|
|
79
|
+
# File output
|
|
80
|
+
if self.logging_options.file:
|
|
34
81
|
with self.lock:
|
|
35
82
|
with open(Logger._log_file, 'a', encoding='utf-8') as f:
|
|
36
83
|
f.write(log)
|
|
@@ -39,6 +86,15 @@ class Logger:
|
|
|
39
86
|
if self.debug:
|
|
40
87
|
self.log(message, 'debug')
|
|
41
88
|
|
|
89
|
+
def log_error(self, message: str):
|
|
90
|
+
self.log(message, 'error')
|
|
91
|
+
|
|
92
|
+
def log_warning(self, message: str):
|
|
93
|
+
self.log(message, 'warning')
|
|
94
|
+
|
|
95
|
+
def log_info(self, message: str):
|
|
96
|
+
self.log(message, 'info')
|
|
97
|
+
|
|
42
98
|
@staticmethod
|
|
43
99
|
def _get_timestamp(fmt: str = "%Y%m%d"):
|
|
44
100
|
now = datetime.datetime.now()
|