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/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,24 +91,22 @@ 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
|
)
|
|
85
109
|
|
|
86
|
-
if testops.get("useV2"):
|
|
87
|
-
self.config.testops.set_use_v2(
|
|
88
|
-
testops.get("useV2")
|
|
89
|
-
)
|
|
90
|
-
|
|
91
110
|
if testops.get("plan"):
|
|
92
111
|
plan = testops.get("plan")
|
|
93
112
|
|
|
@@ -101,21 +120,59 @@ class ConfigManager:
|
|
|
101
120
|
self.config.testops.run.set_id(run.get("id"))
|
|
102
121
|
|
|
103
122
|
if run.get("title"):
|
|
104
|
-
self.config.testops.run.set_title(
|
|
123
|
+
self.config.testops.run.set_title(
|
|
124
|
+
run.get("title"))
|
|
105
125
|
|
|
106
126
|
if run.get("description"):
|
|
107
|
-
self.config.testops.run.set_description(
|
|
127
|
+
self.config.testops.run.set_description(
|
|
128
|
+
run.get("description"))
|
|
108
129
|
|
|
109
|
-
if run.get("complete"):
|
|
130
|
+
if run.get("complete") is not None:
|
|
110
131
|
self.config.testops.run.set_complete(
|
|
111
132
|
run.get("complete")
|
|
112
133
|
)
|
|
113
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
|
+
|
|
114
143
|
if testops.get("batch"):
|
|
115
144
|
batch = testops.get("batch")
|
|
116
145
|
|
|
117
146
|
if batch.get("size"):
|
|
118
|
-
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
|
+
)
|
|
119
176
|
|
|
120
177
|
if config.get("report"):
|
|
121
178
|
report = config.get("report")
|
|
@@ -127,7 +184,8 @@ class ConfigManager:
|
|
|
127
184
|
connection = report.get("connection")
|
|
128
185
|
|
|
129
186
|
if connection.get("path"):
|
|
130
|
-
self.config.report.connection.set_path(
|
|
187
|
+
self.config.report.connection.set_path(
|
|
188
|
+
connection.get("path"))
|
|
131
189
|
|
|
132
190
|
if connection.get("format"):
|
|
133
191
|
self.config.report.connection.set_format(
|
|
@@ -140,11 +198,27 @@ class ConfigManager:
|
|
|
140
198
|
if framework.get("pytest"):
|
|
141
199
|
pytest = framework.get("pytest")
|
|
142
200
|
|
|
143
|
-
if pytest.get("captureLogs"):
|
|
201
|
+
if pytest.get("captureLogs") is not None:
|
|
144
202
|
self.config.framework.pytest.set_capture_logs(
|
|
145
203
|
pytest.get("captureLogs")
|
|
146
204
|
)
|
|
147
205
|
|
|
206
|
+
if pytest.get("xfailStatus"):
|
|
207
|
+
xfail_status = pytest.get("xfailStatus")
|
|
208
|
+
|
|
209
|
+
if xfail_status.get("xfail"):
|
|
210
|
+
self.config.framework.pytest.xfail_status.set_xfail(
|
|
211
|
+
xfail_status.get("xfail")
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if xfail_status.get("xpass"):
|
|
215
|
+
self.config.framework.pytest.xfail_status.set_xpass(
|
|
216
|
+
xfail_status.get("xpass")
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if config.get("logging"):
|
|
220
|
+
self.config.set_logging(config.get("logging"))
|
|
221
|
+
|
|
148
222
|
except Exception as e:
|
|
149
223
|
self.logger.log("Failed to load config from file", "error")
|
|
150
224
|
|
|
@@ -169,6 +243,23 @@ class ConfigManager:
|
|
|
169
243
|
if key == 'QASE_DEBUG':
|
|
170
244
|
self.config.set_debug(value)
|
|
171
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
|
+
|
|
172
263
|
if key == 'QASE_EXECUTION_PLAN_PATH':
|
|
173
264
|
self.config.execution_plan.set_path(value)
|
|
174
265
|
|
|
@@ -199,9 +290,45 @@ class ConfigManager:
|
|
|
199
290
|
if key == 'QASE_TESTOPS_RUN_COMPLETE':
|
|
200
291
|
self.config.testops.run.set_complete(value)
|
|
201
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
|
+
|
|
202
309
|
if key == 'QASE_TESTOPS_BATCH_SIZE':
|
|
203
310
|
self.config.testops.batch.set_size(value)
|
|
204
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
|
+
|
|
205
332
|
if key == 'QASE_REPORT_DRIVER':
|
|
206
333
|
self.config.report.set_driver(value)
|
|
207
334
|
|
|
@@ -214,5 +341,17 @@ class ConfigManager:
|
|
|
214
341
|
if key == 'QASE_PYTEST_CAPTURE_LOGS':
|
|
215
342
|
self.config.framework.pytest.set_capture_logs(value)
|
|
216
343
|
|
|
344
|
+
if key == 'QASE_PYTEST_XFAIL_STATUS_XFAIL':
|
|
345
|
+
self.config.framework.pytest.xfail_status.set_xfail(value)
|
|
346
|
+
|
|
347
|
+
if key == 'QASE_PYTEST_XFAIL_STATUS_XPASS':
|
|
348
|
+
self.config.framework.pytest.xfail_status.set_xpass(value)
|
|
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))
|
|
355
|
+
|
|
217
356
|
except Exception as e:
|
|
218
|
-
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,32 +1,101 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import datetime
|
|
3
|
+
import threading
|
|
4
|
+
from typing import Optional
|
|
2
5
|
|
|
3
|
-
|
|
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
|
|
15
|
+
|
|
16
|
+
def __init__(self, debug: bool = False, prefix: str = '', dir: str = os.path.join('.', 'logs'),
|
|
17
|
+
logging_options: Optional[LoggingOptions] = None) -> None:
|
|
8
18
|
self.debug = debug
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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:
|
|
37
|
+
if Logger._log_file is None:
|
|
38
|
+
timestamp = self._get_timestamp()
|
|
39
|
+
filename = f'{prefix}_{timestamp}.log'
|
|
40
|
+
if not os.path.exists(dir):
|
|
41
|
+
os.makedirs(dir)
|
|
42
|
+
Logger._log_file = os.path.join(dir, filename)
|
|
43
|
+
|
|
44
|
+
with open(Logger._log_file, 'a', encoding='utf-8'):
|
|
15
45
|
pass
|
|
16
46
|
|
|
47
|
+
self.lock = threading.Lock()
|
|
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
|
+
|
|
17
68
|
def log(self, message: str, level: str = 'info'):
|
|
18
69
|
time_str = self._get_timestamp("%H:%M:%S")
|
|
19
70
|
log = f"[Qase][{time_str}][{level}] {message}\n"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
71
|
+
|
|
72
|
+
# Console output
|
|
73
|
+
if self.logging_options.console:
|
|
74
|
+
try:
|
|
75
|
+
print(log, end='')
|
|
76
|
+
except (OSError, IOError):
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
# File output
|
|
80
|
+
if self.logging_options.file:
|
|
81
|
+
with self.lock:
|
|
82
|
+
with open(Logger._log_file, 'a', encoding='utf-8') as f:
|
|
83
|
+
f.write(log)
|
|
24
84
|
|
|
25
85
|
def log_debug(self, message: str):
|
|
26
86
|
if self.debug:
|
|
27
87
|
self.log(message, 'debug')
|
|
28
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
|
+
|
|
29
98
|
@staticmethod
|
|
30
|
-
def _get_timestamp(fmt: str = "%Y%m%
|
|
99
|
+
def _get_timestamp(fmt: str = "%Y%m%d"):
|
|
31
100
|
now = datetime.datetime.now()
|
|
32
101
|
return now.strftime(fmt)
|
qase/commons/models/__init__.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from .result import Result, Field
|
|
2
2
|
from .run import Run
|
|
3
3
|
from .attachment import Attachment
|
|
4
|
-
from .suite import Suite
|
|
5
4
|
from .relation import Relation
|
|
6
5
|
from .step import Step
|
|
7
6
|
from .runtime import Runtime
|
|
@@ -10,7 +9,6 @@ __all__ = [
|
|
|
10
9
|
Result,
|
|
11
10
|
Run,
|
|
12
11
|
Attachment,
|
|
13
|
-
Suite,
|
|
14
12
|
Relation,
|
|
15
13
|
Step,
|
|
16
14
|
Runtime,
|
|
@@ -29,16 +29,19 @@ class Attachment(BaseModel):
|
|
|
29
29
|
def get_id(self) -> str:
|
|
30
30
|
return self.id
|
|
31
31
|
|
|
32
|
-
def get_for_upload(self) ->
|
|
32
|
+
def get_for_upload(self) -> tuple:
|
|
33
|
+
"""Returns attachment data in format expected by new API client: (filename, filedata)"""
|
|
34
|
+
|
|
33
35
|
if self.file_path:
|
|
34
36
|
with open(self.file_path, "rb") as fc:
|
|
35
|
-
|
|
37
|
+
filedata = fc.read()
|
|
36
38
|
else:
|
|
37
39
|
if isinstance(self.content, str):
|
|
38
|
-
|
|
40
|
+
filedata = bytes(self.content, 'utf-8')
|
|
39
41
|
elif isinstance(self.content, bytes):
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
filedata = self.content
|
|
43
|
+
else:
|
|
44
|
+
# Handle case where content is not str or bytes (e.g., JSON serialized)
|
|
45
|
+
filedata = bytes(self.content, 'utf-8')
|
|
46
|
+
|
|
47
|
+
return (self.file_name, filedata)
|
qase/commons/models/basemodel.py
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import json
|
|
2
2
|
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
class BaseModel:
|
|
5
|
-
def __str__(self) -> str:
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
def __str__(self, enum_as_name=False) -> str:
|
|
8
|
+
def serialize(o):
|
|
9
|
+
if isinstance(o, Enum):
|
|
10
|
+
return o.name if enum_as_name else o.value
|
|
11
|
+
elif hasattr(o, '__dict__'):
|
|
12
|
+
return o.__dict__
|
|
13
|
+
else:
|
|
14
|
+
return str(o)
|
|
15
|
+
|
|
16
|
+
return json.dumps(self, default=serialize, indent=4, sort_keys=True)
|
|
@@ -1,19 +1,80 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Type, Union
|
|
3
|
+
|
|
1
4
|
from ..basemodel import BaseModel
|
|
2
5
|
from ... import QaseUtils
|
|
3
6
|
|
|
4
7
|
|
|
8
|
+
class Video(Enum):
|
|
9
|
+
off = "off"
|
|
10
|
+
on = "on"
|
|
11
|
+
failed = "retain-on-failure"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Trace(Enum):
|
|
15
|
+
off = "off"
|
|
16
|
+
on = "on"
|
|
17
|
+
failed = "retain-on-failure"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class XFailStatus(BaseModel):
|
|
21
|
+
xfail: str = None
|
|
22
|
+
xpass: str = None
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.xfail = 'skipped'
|
|
26
|
+
self.xpass = 'passed'
|
|
27
|
+
|
|
28
|
+
def set_xfail(self, value: str):
|
|
29
|
+
self.xfail = value
|
|
30
|
+
|
|
31
|
+
def set_xpass(self, value: str):
|
|
32
|
+
self.xpass = value
|
|
33
|
+
|
|
34
|
+
|
|
5
35
|
class PytestConfig(BaseModel):
|
|
6
36
|
capture_logs: bool = None
|
|
37
|
+
xfail_status: XFailStatus = None
|
|
7
38
|
|
|
8
39
|
def __init__(self):
|
|
9
40
|
self.capture_logs = False
|
|
41
|
+
self.xfail_status = XFailStatus()
|
|
10
42
|
|
|
11
43
|
def set_capture_logs(self, capture_logs):
|
|
12
44
|
self.capture_logs = QaseUtils.parse_bool(capture_logs)
|
|
13
45
|
|
|
14
46
|
|
|
47
|
+
class PlaywrightConfig(BaseModel):
|
|
48
|
+
output_dir: str = None
|
|
49
|
+
video: Video = None
|
|
50
|
+
trace: Trace = None
|
|
51
|
+
|
|
52
|
+
def __init__(self):
|
|
53
|
+
self.output_dir = "test-results"
|
|
54
|
+
self.video = Video.off
|
|
55
|
+
self.trace = Trace.off
|
|
56
|
+
|
|
57
|
+
def set_output_dir(self, output_dir: str):
|
|
58
|
+
self.output_dir = output_dir
|
|
59
|
+
|
|
60
|
+
def set_video(self, value: str):
|
|
61
|
+
self.video = self.__set_mode(Video, value)
|
|
62
|
+
|
|
63
|
+
def set_trace(self, value: str):
|
|
64
|
+
self.trace = self.__set_mode(Trace, value)
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def __set_mode(enum_class: Type[Union[Video, Trace]], value: str):
|
|
68
|
+
for mode in enum_class:
|
|
69
|
+
if mode.value == value:
|
|
70
|
+
return mode
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
15
74
|
class Framework(BaseModel):
|
|
16
75
|
pytest: PytestConfig = None
|
|
76
|
+
playwright: PlaywrightConfig = None
|
|
17
77
|
|
|
18
78
|
def __init__(self):
|
|
19
79
|
self.pytest = PytestConfig()
|
|
80
|
+
self.playwright = PlaywrightConfig()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
+
from typing import List, Dict, Optional
|
|
2
3
|
|
|
3
4
|
from .framework import Framework
|
|
4
5
|
from .report import ReportConfig
|
|
@@ -13,6 +14,21 @@ class Mode(Enum):
|
|
|
13
14
|
off = "off"
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
class LoggingConfig(BaseModel):
|
|
18
|
+
console: Optional[bool] = None
|
|
19
|
+
file: Optional[bool] = None
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.console = None
|
|
23
|
+
self.file = None
|
|
24
|
+
|
|
25
|
+
def set_console(self, console: bool):
|
|
26
|
+
self.console = console
|
|
27
|
+
|
|
28
|
+
def set_file(self, file: bool):
|
|
29
|
+
self.file = file
|
|
30
|
+
|
|
31
|
+
|
|
16
32
|
class ExecutionPlan(BaseModel):
|
|
17
33
|
path: str = None
|
|
18
34
|
|
|
@@ -34,6 +50,9 @@ class QaseConfig(BaseModel):
|
|
|
34
50
|
report: ReportConfig = None
|
|
35
51
|
profilers: list = None
|
|
36
52
|
framework: Framework = None
|
|
53
|
+
exclude_params: list = None
|
|
54
|
+
status_mapping: Dict[str, str] = None
|
|
55
|
+
logging: LoggingConfig = None
|
|
37
56
|
|
|
38
57
|
def __init__(self):
|
|
39
58
|
self.mode = Mode.off
|
|
@@ -44,6 +63,9 @@ class QaseConfig(BaseModel):
|
|
|
44
63
|
self.execution_plan = ExecutionPlan()
|
|
45
64
|
self.framework = Framework()
|
|
46
65
|
self.profilers = []
|
|
66
|
+
self.exclude_params = []
|
|
67
|
+
self.status_mapping = {}
|
|
68
|
+
self.logging = LoggingConfig()
|
|
47
69
|
|
|
48
70
|
def set_mode(self, mode: str):
|
|
49
71
|
if any(mode == e.value for e in Mode.__members__.values()):
|
|
@@ -64,3 +86,15 @@ class QaseConfig(BaseModel):
|
|
|
64
86
|
|
|
65
87
|
def set_debug(self, debug):
|
|
66
88
|
self.debug = QaseUtils.parse_bool(debug)
|
|
89
|
+
|
|
90
|
+
def set_exclude_params(self, exclude_params: List[str]):
|
|
91
|
+
self.exclude_params = exclude_params
|
|
92
|
+
|
|
93
|
+
def set_status_mapping(self, status_mapping: Dict[str, str]):
|
|
94
|
+
self.status_mapping = status_mapping
|
|
95
|
+
|
|
96
|
+
def set_logging(self, logging_config: dict):
|
|
97
|
+
if logging_config.get("console") is not None:
|
|
98
|
+
self.logging.set_console(QaseUtils.parse_bool(logging_config.get("console")))
|
|
99
|
+
if logging_config.get("file") is not None:
|
|
100
|
+
self.logging.set_file(QaseUtils.parse_bool(logging_config.get("file")))
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
1
2
|
from ..basemodel import BaseModel
|
|
3
|
+
from ..external_link import ExternalLinkConfig
|
|
2
4
|
from ... import QaseUtils
|
|
3
5
|
|
|
4
6
|
|
|
@@ -7,9 +9,14 @@ class RunConfig(BaseModel):
|
|
|
7
9
|
description: str = None
|
|
8
10
|
complete: bool = None
|
|
9
11
|
id: int = None
|
|
12
|
+
tags: List[str] = None
|
|
13
|
+
external_link: Optional[ExternalLinkConfig] = None
|
|
14
|
+
|
|
10
15
|
|
|
11
16
|
def __init__(self):
|
|
12
17
|
self.complete = True
|
|
18
|
+
self.tags = []
|
|
19
|
+
self.external_link = None
|
|
13
20
|
|
|
14
21
|
def set_title(self, title: str):
|
|
15
22
|
self.title = title
|
|
@@ -22,3 +29,15 @@ class RunConfig(BaseModel):
|
|
|
22
29
|
|
|
23
30
|
def set_id(self, id: int):
|
|
24
31
|
self.id = id
|
|
32
|
+
|
|
33
|
+
def set_tags(self, tags: List[str]):
|
|
34
|
+
self.tags = tags
|
|
35
|
+
|
|
36
|
+
def set_external_link(self, external_link: dict):
|
|
37
|
+
"""Set external link configuration from dictionary"""
|
|
38
|
+
if external_link:
|
|
39
|
+
self.external_link = ExternalLinkConfig()
|
|
40
|
+
if 'type' in external_link:
|
|
41
|
+
self.external_link.set_type(external_link['type'])
|
|
42
|
+
if 'link' in external_link:
|
|
43
|
+
self.external_link.set_link(external_link['link'])
|