qase-python-commons 5.1.6__tar.gz → 5.1.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/PKG-INFO +3 -2
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/README.md +2 -1
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/pyproject.toml +1 -1
- qase_python_commons-5.1.7/src/qase/commons/config.py +396 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/batch.py +2 -1
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/run.py +8 -11
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/reporters/core.py +25 -11
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/reporters/report.py +7 -1
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/reporters/testops.py +3 -1
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/reporters/testops_multi.py +4 -1
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase_python_commons.egg-info/PKG-INFO +3 -2
- qase_python_commons-5.1.6/src/qase/commons/config.py +0 -399
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/setup.cfg +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/__init__.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/__init__.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/client/api_v1_client.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/client/api_v2_client.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/client/base_api_client.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/exceptions/reporter.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/loader.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/logger.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/__init__.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/attachment.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/basemodel.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/api.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/connection.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/framework.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/plan.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/qaseconfig.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/report.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/run.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/testops.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/external_link.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/relation.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/result.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/runtime.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/step.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/profilers/__init__.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/profilers/db.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/profilers/network.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/profilers/sleep.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/reporters/__init__.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/retry.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/status_mapping/__init__.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/status_mapping/status_mapping.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/util/__init__.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/util/api_host.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/util/host_data.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/util/token_masker.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/utils.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/validators/base.py +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase_python_commons.egg-info/SOURCES.txt +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase_python_commons.egg-info/dependency_links.txt +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase_python_commons.egg-info/requires.txt +0 -0
- {qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase_python_commons.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qase-python-commons
|
|
3
|
-
Version: 5.1.
|
|
3
|
+
Version: 5.1.7
|
|
4
4
|
Summary: A library for Qase TestOps and Qase Report
|
|
5
5
|
Author-email: Qase Team <support@qase.io>
|
|
6
6
|
Project-URL: Homepage, https://github.com/qase-tms/qase-python/tree/main/qase-python-commons
|
|
@@ -137,7 +137,7 @@ The reporter mode is set via the `mode` option:
|
|
|
137
137
|
| Description | Config file | Environment variable | Default | Required |
|
|
138
138
|
|-------------|-------------|---------------------|---------|----------|
|
|
139
139
|
| API token | `testops.api.token` | `QASE_TESTOPS_API_TOKEN` | — | Yes* |
|
|
140
|
-
| API host | `testops.api.host` | `QASE_TESTOPS_API_HOST` | `qase.io` |
|
|
140
|
+
| Qase API host. `qase.io` is the US cluster; set `eu.qase.io` if your workspace is on the EU cluster (you open Qase at `app-eu.qase.io`) | `testops.api.host` | `QASE_TESTOPS_API_HOST` | `qase.io` | Yes, on the EU cluster |
|
|
141
141
|
| Request timeout, seconds | `testops.api.timeout` | `QASE_TESTOPS_API_TIMEOUT` | `30` | No |
|
|
142
142
|
| Upload attempts | `testops.api.retries` | `QASE_TESTOPS_API_RETRIES` | `3` | No |
|
|
143
143
|
| Retry backoff base, seconds | `testops.api.retryBackoff` | `QASE_TESTOPS_API_RETRY_BACKOFF` | `2` | No |
|
|
@@ -297,6 +297,7 @@ export QASE_DEBUG="false"
|
|
|
297
297
|
|
|
298
298
|
# TestOps
|
|
299
299
|
export QASE_TESTOPS_API_TOKEN="<token>"
|
|
300
|
+
export QASE_TESTOPS_API_HOST="qase.io" # eu.qase.io for the EU cluster
|
|
300
301
|
export QASE_TESTOPS_PROJECT="DEMO"
|
|
301
302
|
export QASE_TESTOPS_RUN_TITLE="Automated Run"
|
|
302
303
|
export QASE_TESTOPS_RUN_COMPLETE="true"
|
|
@@ -102,7 +102,7 @@ The reporter mode is set via the `mode` option:
|
|
|
102
102
|
| Description | Config file | Environment variable | Default | Required |
|
|
103
103
|
|-------------|-------------|---------------------|---------|----------|
|
|
104
104
|
| API token | `testops.api.token` | `QASE_TESTOPS_API_TOKEN` | — | Yes* |
|
|
105
|
-
| API host | `testops.api.host` | `QASE_TESTOPS_API_HOST` | `qase.io` |
|
|
105
|
+
| Qase API host. `qase.io` is the US cluster; set `eu.qase.io` if your workspace is on the EU cluster (you open Qase at `app-eu.qase.io`) | `testops.api.host` | `QASE_TESTOPS_API_HOST` | `qase.io` | Yes, on the EU cluster |
|
|
106
106
|
| Request timeout, seconds | `testops.api.timeout` | `QASE_TESTOPS_API_TIMEOUT` | `30` | No |
|
|
107
107
|
| Upload attempts | `testops.api.retries` | `QASE_TESTOPS_API_RETRIES` | `3` | No |
|
|
108
108
|
| Retry backoff base, seconds | `testops.api.retryBackoff` | `QASE_TESTOPS_API_RETRY_BACKOFF` | `2` | No |
|
|
@@ -262,6 +262,7 @@ export QASE_DEBUG="false"
|
|
|
262
262
|
|
|
263
263
|
# TestOps
|
|
264
264
|
export QASE_TESTOPS_API_TOKEN="<token>"
|
|
265
|
+
export QASE_TESTOPS_API_HOST="qase.io" # eu.qase.io for the EU cluster
|
|
265
266
|
export QASE_TESTOPS_PROJECT="DEMO"
|
|
266
267
|
export QASE_TESTOPS_RUN_TITLE="Automated Run"
|
|
267
268
|
export QASE_TESTOPS_RUN_COMPLETE="true"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "qase-python-commons"
|
|
7
|
-
version = "5.1.
|
|
7
|
+
version = "5.1.7"
|
|
8
8
|
description = "A library for Qase TestOps and Qase Report"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{name = "Qase Team", email = "support@qase.io"}]
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from .logger import Logger, LoggingOptions
|
|
5
|
+
from .models.config.qaseconfig import QaseConfig, Mode
|
|
6
|
+
from .utils import QaseUtils
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConfigManager:
|
|
10
|
+
|
|
11
|
+
def __init__(self, config_file='./qase.config.json'):
|
|
12
|
+
self.__config_file = config_file
|
|
13
|
+
self.config = QaseConfig()
|
|
14
|
+
|
|
15
|
+
# Initialize temporary logger for error handling during config loading
|
|
16
|
+
self.logger = Logger(debug=False)
|
|
17
|
+
|
|
18
|
+
self.__load_file_config()
|
|
19
|
+
self.__load_env_config()
|
|
20
|
+
|
|
21
|
+
# Re-initialize logger with proper logging options after config is loaded
|
|
22
|
+
logging_options = LoggingOptions(
|
|
23
|
+
console=self.config.logging.console if self.config.logging.console is not None else True,
|
|
24
|
+
file=self.config.logging.file if self.config.logging.file is not None else self.config.debug
|
|
25
|
+
)
|
|
26
|
+
self.logger = Logger(debug=self.config.debug, logging_options=logging_options)
|
|
27
|
+
|
|
28
|
+
def validate_config(self):
|
|
29
|
+
errors: list[str] = []
|
|
30
|
+
if self.config.mode is Mode.testops or self.config.fallback is Mode.testops:
|
|
31
|
+
if self.config.testops.api.token is None:
|
|
32
|
+
errors.append("Testops token is not set")
|
|
33
|
+
|
|
34
|
+
if self.config.testops.project is None:
|
|
35
|
+
errors.append("Testops project is not set")
|
|
36
|
+
|
|
37
|
+
if self.config.mode is Mode.testops_multi or self.config.fallback is Mode.testops_multi:
|
|
38
|
+
if self.config.testops.api.token is None:
|
|
39
|
+
errors.append("Testops token is not set")
|
|
40
|
+
|
|
41
|
+
if not self.config.testops_multi.projects or len(self.config.testops_multi.projects) == 0:
|
|
42
|
+
errors.append("Testops multi: at least one project must be configured")
|
|
43
|
+
|
|
44
|
+
for project in self.config.testops_multi.projects:
|
|
45
|
+
if not project.code:
|
|
46
|
+
errors.append(f"Testops multi: project code is required for all projects")
|
|
47
|
+
|
|
48
|
+
if len(errors) > 0:
|
|
49
|
+
self.logger.log("Config validation failed", "error")
|
|
50
|
+
for error in errors:
|
|
51
|
+
self.logger.log(error, "error")
|
|
52
|
+
raise Exception("Config validation failed")
|
|
53
|
+
|
|
54
|
+
def __str__(self):
|
|
55
|
+
return json.dumps(self.config, indent=4, sort_keys=True)
|
|
56
|
+
|
|
57
|
+
@contextmanager
|
|
58
|
+
def __option(self, name: str):
|
|
59
|
+
# One invalid value must not discard the settings loaded after it.
|
|
60
|
+
try:
|
|
61
|
+
yield
|
|
62
|
+
except Exception as e:
|
|
63
|
+
self.logger.log(f"Ignoring invalid value for {name}: {e}", "error")
|
|
64
|
+
|
|
65
|
+
def __load_file_config(self):
|
|
66
|
+
if not os.path.exists(self.__config_file):
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
with open(self.__config_file, "r") as file:
|
|
71
|
+
config = json.load(file)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
self.logger.log(f"Failed to load config from file: {e}", "error")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
with self.__option("mode"):
|
|
77
|
+
if config.get("mode"):
|
|
78
|
+
self.config.set_mode(config.get("mode"))
|
|
79
|
+
|
|
80
|
+
with self.__option("fallback"):
|
|
81
|
+
if config.get("fallback"):
|
|
82
|
+
self.config.set_fallback(config.get("fallback"))
|
|
83
|
+
|
|
84
|
+
with self.__option("environment"):
|
|
85
|
+
if config.get("environment"):
|
|
86
|
+
self.config.set_environment(config.get("environment"))
|
|
87
|
+
|
|
88
|
+
with self.__option("rootSuite"):
|
|
89
|
+
if config.get("rootSuite"):
|
|
90
|
+
self.config.set_root_suite(config.get("rootSuite"))
|
|
91
|
+
|
|
92
|
+
with self.__option("profilers"):
|
|
93
|
+
if config.get("profilers"):
|
|
94
|
+
self.config.set_profilers(config.get("profilers"))
|
|
95
|
+
|
|
96
|
+
with self.__option("debug"):
|
|
97
|
+
if config.get("debug") is not None:
|
|
98
|
+
self.config.set_debug(config.get("debug"))
|
|
99
|
+
|
|
100
|
+
with self.__option("excludeParams"):
|
|
101
|
+
if config.get("excludeParams"):
|
|
102
|
+
self.config.set_exclude_params(config.get("excludeParams"))
|
|
103
|
+
|
|
104
|
+
with self.__option("statusMapping"):
|
|
105
|
+
if config.get("statusMapping"):
|
|
106
|
+
self.config.set_status_mapping(config.get("statusMapping"))
|
|
107
|
+
|
|
108
|
+
with self.__option("executionPlan.path"):
|
|
109
|
+
execution_plan = config.get("executionPlan") or {}
|
|
110
|
+
if execution_plan.get("path"):
|
|
111
|
+
self.config.execution_plan.set_path(execution_plan.get("path"))
|
|
112
|
+
|
|
113
|
+
testops = config.get("testops") or {}
|
|
114
|
+
api = testops.get("api") or {}
|
|
115
|
+
|
|
116
|
+
with self.__option("testops.api.host"):
|
|
117
|
+
if api.get("host"):
|
|
118
|
+
self.config.testops.api.set_host(api.get("host"))
|
|
119
|
+
|
|
120
|
+
with self.__option("testops.api.token"):
|
|
121
|
+
if api.get("token"):
|
|
122
|
+
self.config.testops.api.set_token(api.get("token"))
|
|
123
|
+
|
|
124
|
+
with self.__option("testops.api.timeout"):
|
|
125
|
+
if api.get("timeout"):
|
|
126
|
+
self.config.testops.api.set_timeout(api.get("timeout"))
|
|
127
|
+
|
|
128
|
+
# `is not None`, not truthiness: 0 is a valid value
|
|
129
|
+
# for both, meaning "no retries" / "no backoff".
|
|
130
|
+
with self.__option("testops.api.retries"):
|
|
131
|
+
if api.get("retries") is not None:
|
|
132
|
+
self.config.testops.api.set_retries(api.get("retries"))
|
|
133
|
+
|
|
134
|
+
with self.__option("testops.api.retryBackoff"):
|
|
135
|
+
if api.get("retryBackoff") is not None:
|
|
136
|
+
self.config.testops.api.set_retry_backoff(api.get("retryBackoff"))
|
|
137
|
+
|
|
138
|
+
with self.__option("testops.project"):
|
|
139
|
+
if testops.get("project"):
|
|
140
|
+
self.config.testops.set_project(testops.get("project"))
|
|
141
|
+
|
|
142
|
+
with self.__option("testops.defect"):
|
|
143
|
+
if testops.get("defect") is not None:
|
|
144
|
+
self.config.testops.set_defect(testops.get("defect"))
|
|
145
|
+
|
|
146
|
+
with self.__option("testops.plan.id"):
|
|
147
|
+
plan = testops.get("plan") or {}
|
|
148
|
+
if plan.get("id"):
|
|
149
|
+
self.config.testops.plan.set_id(plan.get("id"))
|
|
150
|
+
|
|
151
|
+
run = testops.get("run") or {}
|
|
152
|
+
|
|
153
|
+
with self.__option("testops.run.id"):
|
|
154
|
+
if run.get("id"):
|
|
155
|
+
self.config.testops.run.set_id(run.get("id"))
|
|
156
|
+
|
|
157
|
+
with self.__option("testops.run.title"):
|
|
158
|
+
if run.get("title"):
|
|
159
|
+
self.config.testops.run.set_title(run.get("title"))
|
|
160
|
+
|
|
161
|
+
with self.__option("testops.run.description"):
|
|
162
|
+
if run.get("description"):
|
|
163
|
+
self.config.testops.run.set_description(run.get("description"))
|
|
164
|
+
|
|
165
|
+
with self.__option("testops.run.complete"):
|
|
166
|
+
if run.get("complete") is not None:
|
|
167
|
+
self.config.testops.run.set_complete(run.get("complete"))
|
|
168
|
+
|
|
169
|
+
with self.__option("testops.run.tags"):
|
|
170
|
+
if run.get("tags"):
|
|
171
|
+
self.config.testops.run.set_tags([tag.strip() for tag in run.get("tags")])
|
|
172
|
+
|
|
173
|
+
with self.__option("testops.run.externalLink"):
|
|
174
|
+
if run.get("externalLink"):
|
|
175
|
+
self.config.testops.run.set_external_link(run.get("externalLink"))
|
|
176
|
+
|
|
177
|
+
with self.__option("testops.batch.size"):
|
|
178
|
+
batch = testops.get("batch") or {}
|
|
179
|
+
if batch.get("size"):
|
|
180
|
+
self.config.testops.batch.set_size(batch.get("size"))
|
|
181
|
+
|
|
182
|
+
configurations = testops.get("configurations") or {}
|
|
183
|
+
|
|
184
|
+
with self.__option("testops.configurations.values"):
|
|
185
|
+
for value in configurations.get("values") or []:
|
|
186
|
+
if value.get("name") and value.get("value"):
|
|
187
|
+
self.config.testops.configurations.add_value(
|
|
188
|
+
value.get("name"), value.get("value"))
|
|
189
|
+
|
|
190
|
+
with self.__option("testops.configurations.createIfNotExists"):
|
|
191
|
+
if configurations.get("createIfNotExists") is not None:
|
|
192
|
+
self.config.testops.configurations.set_create_if_not_exists(
|
|
193
|
+
configurations.get("createIfNotExists"))
|
|
194
|
+
|
|
195
|
+
with self.__option("testops.statusFilter"):
|
|
196
|
+
status_filter = testops.get("statusFilter")
|
|
197
|
+
if status_filter:
|
|
198
|
+
if isinstance(status_filter, list):
|
|
199
|
+
self.config.testops.set_status_filter(status_filter)
|
|
200
|
+
elif isinstance(status_filter, str):
|
|
201
|
+
# Parse comma-separated string
|
|
202
|
+
self.config.testops.set_status_filter([s.strip() for s in status_filter.split(',')])
|
|
203
|
+
|
|
204
|
+
with self.__option("testops.showPublicReportLink"):
|
|
205
|
+
if testops.get("showPublicReportLink") is not None:
|
|
206
|
+
self.config.testops.set_show_public_report_link(testops.get("showPublicReportLink"))
|
|
207
|
+
|
|
208
|
+
with self.__option("testops_multi"):
|
|
209
|
+
if config.get("testops_multi"):
|
|
210
|
+
self.config.set_testops_multi(config.get("testops_multi"))
|
|
211
|
+
|
|
212
|
+
report = config.get("report") or {}
|
|
213
|
+
connection = report.get("connection") or {}
|
|
214
|
+
|
|
215
|
+
with self.__option("report.driver"):
|
|
216
|
+
if report.get("driver"):
|
|
217
|
+
self.config.report.set_driver(report.get("driver"))
|
|
218
|
+
|
|
219
|
+
with self.__option("report.connection.path"):
|
|
220
|
+
if connection.get("path"):
|
|
221
|
+
self.config.report.connection.set_path(connection.get("path"))
|
|
222
|
+
|
|
223
|
+
with self.__option("report.connection.format"):
|
|
224
|
+
if connection.get("format"):
|
|
225
|
+
self.config.report.connection.set_format(connection.get("format"))
|
|
226
|
+
|
|
227
|
+
pytest = (config.get("framework") or {}).get("pytest") or {}
|
|
228
|
+
xfail_status = pytest.get("xfailStatus") or {}
|
|
229
|
+
|
|
230
|
+
with self.__option("framework.pytest.captureLogs"):
|
|
231
|
+
if pytest.get("captureLogs") is not None:
|
|
232
|
+
self.config.framework.pytest.set_capture_logs(pytest.get("captureLogs"))
|
|
233
|
+
|
|
234
|
+
with self.__option("framework.pytest.xfailStatus.xfail"):
|
|
235
|
+
if xfail_status.get("xfail"):
|
|
236
|
+
self.config.framework.pytest.xfail_status.set_xfail(xfail_status.get("xfail"))
|
|
237
|
+
|
|
238
|
+
with self.__option("framework.pytest.xfailStatus.xpass"):
|
|
239
|
+
if xfail_status.get("xpass"):
|
|
240
|
+
self.config.framework.pytest.xfail_status.set_xpass(xfail_status.get("xpass"))
|
|
241
|
+
|
|
242
|
+
with self.__option("logging"):
|
|
243
|
+
if config.get("logging"):
|
|
244
|
+
self.config.set_logging(config.get("logging"))
|
|
245
|
+
|
|
246
|
+
def __load_env_config(self):
|
|
247
|
+
for key, value in os.environ.items():
|
|
248
|
+
# CI templates like QASE_TESTOPS_RUN_ID=${RUN_ID} leave the variable
|
|
249
|
+
# set but empty; that means "not configured", not an invalid value.
|
|
250
|
+
if not key.startswith('QASE_') or not value.strip():
|
|
251
|
+
continue
|
|
252
|
+
with self.__option(key):
|
|
253
|
+
self.__apply_env_var(key, value)
|
|
254
|
+
|
|
255
|
+
def __apply_env_var(self, key: str, value: str):
|
|
256
|
+
if key == 'QASE_MODE':
|
|
257
|
+
self.config.set_mode(value)
|
|
258
|
+
|
|
259
|
+
if key == 'QASE_FALLBACK':
|
|
260
|
+
self.config.set_fallback(value)
|
|
261
|
+
|
|
262
|
+
if key == 'QASE_ENVIRONMENT':
|
|
263
|
+
self.config.set_environment(value)
|
|
264
|
+
|
|
265
|
+
if key == 'QASE_ROOT_SUITE':
|
|
266
|
+
self.config.set_root_suite(value)
|
|
267
|
+
|
|
268
|
+
if key == 'QASE_PROFILERS':
|
|
269
|
+
self.config.set_profilers(value.split(','))
|
|
270
|
+
|
|
271
|
+
if key == 'QASE_PROFILER_NETWORK_EXCLUDE_HOSTS':
|
|
272
|
+
self.config.network_profiler.set_exclude_hosts(
|
|
273
|
+
[h.strip() for h in value.split(',')]
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if key == 'QASE_DEBUG':
|
|
277
|
+
self.config.set_debug(value)
|
|
278
|
+
|
|
279
|
+
if key == 'QASE_EXCLUDE_PARAMS':
|
|
280
|
+
self.config.set_exclude_params(
|
|
281
|
+
[param.strip() for param in value.split(',')])
|
|
282
|
+
|
|
283
|
+
if key == 'QASE_STATUS_MAPPING':
|
|
284
|
+
# Parse status mapping from environment variable
|
|
285
|
+
# Format: "source1=target1,source2=target2"
|
|
286
|
+
if value:
|
|
287
|
+
mapping_dict = {}
|
|
288
|
+
pairs = value.split(',')
|
|
289
|
+
for pair in pairs:
|
|
290
|
+
pair = pair.strip()
|
|
291
|
+
if pair and '=' in pair:
|
|
292
|
+
source_status, target_status = pair.split('=', 1)
|
|
293
|
+
mapping_dict[source_status.strip()] = target_status.strip()
|
|
294
|
+
self.config.set_status_mapping(mapping_dict)
|
|
295
|
+
|
|
296
|
+
if key == 'QASE_EXECUTION_PLAN_PATH':
|
|
297
|
+
self.config.execution_plan.set_path(value)
|
|
298
|
+
|
|
299
|
+
if key == 'QASE_TESTOPS_API_HOST':
|
|
300
|
+
self.config.testops.api.set_host(value)
|
|
301
|
+
|
|
302
|
+
if key == 'QASE_TESTOPS_API_TOKEN':
|
|
303
|
+
self.config.testops.api.set_token(value)
|
|
304
|
+
|
|
305
|
+
if key == 'QASE_TESTOPS_API_TIMEOUT':
|
|
306
|
+
self.config.testops.api.set_timeout(value)
|
|
307
|
+
|
|
308
|
+
if key == 'QASE_TESTOPS_API_RETRIES':
|
|
309
|
+
self.config.testops.api.set_retries(value)
|
|
310
|
+
|
|
311
|
+
if key == 'QASE_TESTOPS_API_RETRY_BACKOFF':
|
|
312
|
+
self.config.testops.api.set_retry_backoff(value)
|
|
313
|
+
|
|
314
|
+
if key == 'QASE_TESTOPS_PROJECT':
|
|
315
|
+
self.config.testops.set_project(value)
|
|
316
|
+
|
|
317
|
+
if key == 'QASE_TESTOPS_DEFECT':
|
|
318
|
+
self.config.testops.set_defect(value)
|
|
319
|
+
|
|
320
|
+
if key == 'QASE_TESTOPS_PLAN_ID':
|
|
321
|
+
self.config.testops.plan.set_id(int(value.strip()))
|
|
322
|
+
|
|
323
|
+
if key == 'QASE_TESTOPS_RUN_ID':
|
|
324
|
+
self.config.testops.run.set_id(int(value.strip()))
|
|
325
|
+
|
|
326
|
+
if key == 'QASE_TESTOPS_RUN_TITLE':
|
|
327
|
+
self.config.testops.run.set_title(value)
|
|
328
|
+
|
|
329
|
+
if key == 'QASE_TESTOPS_RUN_DESCRIPTION':
|
|
330
|
+
self.config.testops.run.set_description(value)
|
|
331
|
+
|
|
332
|
+
if key == 'QASE_TESTOPS_RUN_COMPLETE':
|
|
333
|
+
self.config.testops.run.set_complete(value)
|
|
334
|
+
|
|
335
|
+
if key == 'QASE_TESTOPS_RUN_TAGS':
|
|
336
|
+
self.config.testops.run.set_tags(
|
|
337
|
+
[tag.strip() for tag in value.split(',')])
|
|
338
|
+
|
|
339
|
+
if key == 'QASE_TESTOPS_RUN_EXTERNAL_LINK_TYPE':
|
|
340
|
+
if not self.config.testops.run.external_link:
|
|
341
|
+
from .models.external_link import ExternalLinkConfig
|
|
342
|
+
self.config.testops.run.external_link = ExternalLinkConfig()
|
|
343
|
+
self.config.testops.run.external_link.set_type(value)
|
|
344
|
+
|
|
345
|
+
if key == 'QASE_TESTOPS_RUN_EXTERNAL_LINK_URL':
|
|
346
|
+
if not self.config.testops.run.external_link:
|
|
347
|
+
from .models.external_link import ExternalLinkConfig
|
|
348
|
+
self.config.testops.run.external_link = ExternalLinkConfig()
|
|
349
|
+
self.config.testops.run.external_link.set_link(value)
|
|
350
|
+
|
|
351
|
+
if key == 'QASE_TESTOPS_BATCH_SIZE':
|
|
352
|
+
self.config.testops.batch.set_size(int(value.strip()))
|
|
353
|
+
|
|
354
|
+
if key == 'QASE_TESTOPS_CONFIGURATIONS_VALUES':
|
|
355
|
+
# Parse configurations from environment variable
|
|
356
|
+
# Format: "group1=value1,group2=value2"
|
|
357
|
+
if value:
|
|
358
|
+
config_pairs = value.split(',')
|
|
359
|
+
for pair in config_pairs:
|
|
360
|
+
if '=' in pair:
|
|
361
|
+
name, config_value = pair.split('=', 1)
|
|
362
|
+
self.config.testops.configurations.add_value(name.strip(), config_value.strip())
|
|
363
|
+
|
|
364
|
+
if key == 'QASE_TESTOPS_CONFIGURATIONS_CREATE_IF_NOT_EXISTS':
|
|
365
|
+
self.config.testops.configurations.set_create_if_not_exists(value)
|
|
366
|
+
|
|
367
|
+
if key == 'QASE_TESTOPS_STATUS_FILTER':
|
|
368
|
+
# Parse comma-separated string
|
|
369
|
+
self.config.testops.set_status_filter([s.strip() for s in value.split(',')])
|
|
370
|
+
|
|
371
|
+
if key == 'QASE_TESTOPS_SHOW_PUBLIC_REPORT_LINK':
|
|
372
|
+
self.config.testops.set_show_public_report_link(value)
|
|
373
|
+
|
|
374
|
+
if key == 'QASE_REPORT_DRIVER':
|
|
375
|
+
self.config.report.set_driver(value)
|
|
376
|
+
|
|
377
|
+
if key == 'QASE_REPORT_CONNECTION_PATH':
|
|
378
|
+
self.config.report.connection.set_path(value)
|
|
379
|
+
|
|
380
|
+
if key == 'QASE_REPORT_CONNECTION_FORMAT':
|
|
381
|
+
self.config.report.connection.set_format(value)
|
|
382
|
+
|
|
383
|
+
if key == 'QASE_PYTEST_CAPTURE_LOGS':
|
|
384
|
+
self.config.framework.pytest.set_capture_logs(value)
|
|
385
|
+
|
|
386
|
+
if key == 'QASE_PYTEST_XFAIL_STATUS_XFAIL':
|
|
387
|
+
self.config.framework.pytest.xfail_status.set_xfail(value)
|
|
388
|
+
|
|
389
|
+
if key == 'QASE_PYTEST_XFAIL_STATUS_XPASS':
|
|
390
|
+
self.config.framework.pytest.xfail_status.set_xpass(value)
|
|
391
|
+
|
|
392
|
+
if key == 'QASE_LOGGING_CONSOLE':
|
|
393
|
+
self.config.logging.set_console(QaseUtils.parse_bool(value))
|
|
394
|
+
|
|
395
|
+
if key == 'QASE_LOGGING_FILE':
|
|
396
|
+
self.config.logging.set_file(QaseUtils.parse_bool(value))
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/batch.py
RENAMED
|
@@ -8,6 +8,7 @@ class BatchConfig(BaseModel):
|
|
|
8
8
|
self.size = 200
|
|
9
9
|
|
|
10
10
|
def set_size(self, size: int):
|
|
11
|
-
|
|
11
|
+
size = int(size)
|
|
12
|
+
if size > 200 or size < 1:
|
|
12
13
|
raise ValueError("Batch size should be greater than 0 and no greater than 200")
|
|
13
14
|
self.size = size
|
|
@@ -50,18 +50,18 @@ class Run(BaseModel):
|
|
|
50
50
|
title: str,
|
|
51
51
|
start_time: float,
|
|
52
52
|
end_time: float,
|
|
53
|
-
results: List[str] =
|
|
54
|
-
threads: List[str] =
|
|
55
|
-
suites: List[str] =
|
|
53
|
+
results: Optional[List[str]] = None,
|
|
54
|
+
threads: Optional[List[str]] = None,
|
|
55
|
+
suites: Optional[List[str]] = None,
|
|
56
56
|
environment: Optional[str] = None
|
|
57
57
|
):
|
|
58
58
|
self.host_data = None
|
|
59
59
|
self.title = title
|
|
60
60
|
self.execution = RunExecution(start_time=start_time, end_time=end_time)
|
|
61
61
|
self.stats = RunStats()
|
|
62
|
-
self.results = results
|
|
63
|
-
self.threads = threads
|
|
64
|
-
self.suites = suites
|
|
62
|
+
self.results = results if results is not None else []
|
|
63
|
+
self.threads = threads if threads is not None else []
|
|
64
|
+
self.suites = suites if suites is not None else []
|
|
65
65
|
self.environment = environment
|
|
66
66
|
|
|
67
67
|
def add_result(self, result: dict):
|
|
@@ -84,11 +84,8 @@ class Run(BaseModel):
|
|
|
84
84
|
|
|
85
85
|
def _extract_path_from_relations(self, relations_dict):
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if "data" in relations_dict["relations"]["suite"]:
|
|
90
|
-
data_list = relations_dict["relations"]["suite"]["data"]
|
|
91
|
-
titles = [item["title"] for item in data_list if "title" in item]
|
|
87
|
+
suite = (relations_dict.get("relations") or {}).get("suite") or {}
|
|
88
|
+
titles = [item["title"] for item in suite.get("data") or [] if "title" in item]
|
|
92
89
|
|
|
93
90
|
path = "/".join(titles)
|
|
94
91
|
|
|
@@ -179,9 +179,13 @@ class QaseCoreReporter:
|
|
|
179
179
|
self.overhead += time.time() - ts
|
|
180
180
|
return run_id
|
|
181
181
|
except Exception as e:
|
|
182
|
-
self.logger.log('Failed to start run
|
|
182
|
+
self.logger.log('Failed to start run', 'info')
|
|
183
183
|
self.logger.log(e, 'error')
|
|
184
|
-
self.
|
|
184
|
+
if self._can_fallback():
|
|
185
|
+
self._run_fallback()
|
|
186
|
+
else:
|
|
187
|
+
self.logger.log('Disabling reporting', 'info')
|
|
188
|
+
self.reporter = None
|
|
185
189
|
return None
|
|
186
190
|
|
|
187
191
|
return None
|
|
@@ -198,6 +202,12 @@ class QaseCoreReporter:
|
|
|
198
202
|
self.logger.log('Failed to complete run', 'info')
|
|
199
203
|
self.logger.log(e, 'error')
|
|
200
204
|
|
|
205
|
+
if self._can_fallback() and self._has_undelivered_results():
|
|
206
|
+
self.logger.log('Some results were not delivered, saving all results with the fallback reporter',
|
|
207
|
+
'info')
|
|
208
|
+
self._run_fallback()
|
|
209
|
+
self.complete_run()
|
|
210
|
+
|
|
201
211
|
def add_result(self, result: Result) -> None:
|
|
202
212
|
if self.reporter:
|
|
203
213
|
try:
|
|
@@ -289,21 +299,25 @@ class QaseCoreReporter:
|
|
|
289
299
|
def get_execution_plan(self) -> Union[None, List[int]]:
|
|
290
300
|
return self._execution_plan
|
|
291
301
|
|
|
302
|
+
def _can_fallback(self) -> bool:
|
|
303
|
+
# After a failed TestOps init the fallback is already the active reporter;
|
|
304
|
+
# restarting it would wipe the results it has written.
|
|
305
|
+
return self.fallback is not None and self.reporter is not self.fallback
|
|
306
|
+
|
|
307
|
+
def _has_undelivered_results(self) -> bool:
|
|
308
|
+
lost = getattr(self.reporter, 'lost_results', 0)
|
|
309
|
+
if isinstance(lost, dict):
|
|
310
|
+
lost = sum(lost.values())
|
|
311
|
+
return isinstance(lost, int) and lost > 0
|
|
312
|
+
|
|
292
313
|
def _run_fallback(self) -> None:
|
|
293
|
-
if self.
|
|
314
|
+
if self._can_fallback():
|
|
294
315
|
try:
|
|
295
316
|
results = self.reporter.get_results()
|
|
296
317
|
|
|
297
318
|
self.fallback.start_run()
|
|
298
319
|
self.reporter = self.fallback
|
|
299
|
-
|
|
300
|
-
if isinstance(results, dict):
|
|
301
|
-
# Multi-project mode: results is dict of project -> list of results
|
|
302
|
-
for project_code, project_results in results.items():
|
|
303
|
-
self.reporter.set_results({project_code: project_results})
|
|
304
|
-
else:
|
|
305
|
-
# Single project mode: results is list
|
|
306
|
-
self.reporter.set_results(results)
|
|
320
|
+
self.reporter.set_results(results)
|
|
307
321
|
self.fallback = None
|
|
308
322
|
except Exception as e:
|
|
309
323
|
# Log error, disable reporting and continue
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/reporters/report.py
RENAMED
|
@@ -112,7 +112,13 @@ class QaseReport:
|
|
|
112
112
|
return self.results
|
|
113
113
|
|
|
114
114
|
def set_results(self, results):
|
|
115
|
-
|
|
115
|
+
# Called on fallback with what the TestOps reporter held: a list, or
|
|
116
|
+
# a dict of lists per project. The report is compiled from results/,
|
|
117
|
+
# so they have to be written there, not kept in memory.
|
|
118
|
+
if isinstance(results, dict):
|
|
119
|
+
results = [result for project_results in results.values() for result in project_results]
|
|
120
|
+
for result in results:
|
|
121
|
+
self.add_result(result)
|
|
116
122
|
|
|
117
123
|
def _persist_attachment(self, attachment: Attachment):
|
|
118
124
|
if attachment.content:
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/reporters/testops.py
RENAMED
|
@@ -40,6 +40,7 @@ class QaseTestOps:
|
|
|
40
40
|
self.count_running_threads = 0
|
|
41
41
|
# Results in batches that could not be delivered after all retries.
|
|
42
42
|
self.lost_results = 0
|
|
43
|
+
self.undelivered = []
|
|
43
44
|
|
|
44
45
|
environment = self.config.environment
|
|
45
46
|
if environment:
|
|
@@ -88,6 +89,7 @@ class QaseTestOps:
|
|
|
88
89
|
# nothing acted on, which is how the loss stayed invisible.
|
|
89
90
|
with self.lock:
|
|
90
91
|
self.lost_results += len(results)
|
|
92
|
+
self.undelivered.extend(results)
|
|
91
93
|
self.logger.log(
|
|
92
94
|
f"Failed to send {len(results)} results for run {self.run_id} "
|
|
93
95
|
f"after {self.config.testops.api.retries} attempt(s): {e}",
|
|
@@ -197,7 +199,7 @@ class QaseTestOps:
|
|
|
197
199
|
self._send_results()
|
|
198
200
|
|
|
199
201
|
def get_results(self) -> List:
|
|
200
|
-
return self.results + self.processed
|
|
202
|
+
return self.results + self.processed + self.undelivered
|
|
201
203
|
|
|
202
204
|
def set_results(self, results) -> None:
|
|
203
205
|
self.results = results
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/reporters/testops_multi.py
RENAMED
|
@@ -51,6 +51,7 @@ class QaseTestOpsMulti:
|
|
|
51
51
|
# Results in batches that could not be delivered after all retries,
|
|
52
52
|
# counted per project so one project's loss does not hide another's.
|
|
53
53
|
self.lost_results: Dict[str, int] = {project.code: 0 for project in self.multi_config.projects}
|
|
54
|
+
self.undelivered: Dict[str, List[Result]] = {project.code: [] for project in self.multi_config.projects}
|
|
54
55
|
|
|
55
56
|
# Initialize environment for each project
|
|
56
57
|
for project_code, project_data in self.project_configs.items():
|
|
@@ -122,6 +123,7 @@ class QaseTestOpsMulti:
|
|
|
122
123
|
# nothing acted on, which is how the loss stayed invisible.
|
|
123
124
|
with self.lock:
|
|
124
125
|
self.lost_results[project_code] = self.lost_results.get(project_code, 0) + len(results)
|
|
126
|
+
self.undelivered.setdefault(project_code, []).extend(results)
|
|
125
127
|
self.logger.log(
|
|
126
128
|
f"Failed to send {len(results)} results for project {project_code}, "
|
|
127
129
|
f"run {run_id} after {self.config.testops.api.retries} attempt(s): {e}",
|
|
@@ -347,7 +349,8 @@ class QaseTestOpsMulti:
|
|
|
347
349
|
"""Get all results (pending + processed) grouped by project."""
|
|
348
350
|
all_results = {}
|
|
349
351
|
for project_code in self.project_configs.keys():
|
|
350
|
-
all_results[project_code] = self.project_results[project_code] + self.processed[project_code]
|
|
352
|
+
all_results[project_code] = (self.project_results[project_code] + self.processed[project_code]
|
|
353
|
+
+ self.undelivered.get(project_code, []))
|
|
351
354
|
return all_results
|
|
352
355
|
|
|
353
356
|
def set_results(self, results: Dict[str, List[Result]]) -> None:
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase_python_commons.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qase-python-commons
|
|
3
|
-
Version: 5.1.
|
|
3
|
+
Version: 5.1.7
|
|
4
4
|
Summary: A library for Qase TestOps and Qase Report
|
|
5
5
|
Author-email: Qase Team <support@qase.io>
|
|
6
6
|
Project-URL: Homepage, https://github.com/qase-tms/qase-python/tree/main/qase-python-commons
|
|
@@ -137,7 +137,7 @@ The reporter mode is set via the `mode` option:
|
|
|
137
137
|
| Description | Config file | Environment variable | Default | Required |
|
|
138
138
|
|-------------|-------------|---------------------|---------|----------|
|
|
139
139
|
| API token | `testops.api.token` | `QASE_TESTOPS_API_TOKEN` | — | Yes* |
|
|
140
|
-
| API host | `testops.api.host` | `QASE_TESTOPS_API_HOST` | `qase.io` |
|
|
140
|
+
| Qase API host. `qase.io` is the US cluster; set `eu.qase.io` if your workspace is on the EU cluster (you open Qase at `app-eu.qase.io`) | `testops.api.host` | `QASE_TESTOPS_API_HOST` | `qase.io` | Yes, on the EU cluster |
|
|
141
141
|
| Request timeout, seconds | `testops.api.timeout` | `QASE_TESTOPS_API_TIMEOUT` | `30` | No |
|
|
142
142
|
| Upload attempts | `testops.api.retries` | `QASE_TESTOPS_API_RETRIES` | `3` | No |
|
|
143
143
|
| Retry backoff base, seconds | `testops.api.retryBackoff` | `QASE_TESTOPS_API_RETRY_BACKOFF` | `2` | No |
|
|
@@ -297,6 +297,7 @@ export QASE_DEBUG="false"
|
|
|
297
297
|
|
|
298
298
|
# TestOps
|
|
299
299
|
export QASE_TESTOPS_API_TOKEN="<token>"
|
|
300
|
+
export QASE_TESTOPS_API_HOST="qase.io" # eu.qase.io for the EU cluster
|
|
300
301
|
export QASE_TESTOPS_PROJECT="DEMO"
|
|
301
302
|
export QASE_TESTOPS_RUN_TITLE="Automated Run"
|
|
302
303
|
export QASE_TESTOPS_RUN_COMPLETE="true"
|
|
@@ -1,399 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import json
|
|
3
|
-
from .logger import Logger, LoggingOptions
|
|
4
|
-
from .models.config.qaseconfig import QaseConfig, Mode
|
|
5
|
-
from .utils import QaseUtils
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class ConfigManager:
|
|
9
|
-
|
|
10
|
-
def __init__(self, config_file='./qase.config.json'):
|
|
11
|
-
self.__config_file = config_file
|
|
12
|
-
self.config = QaseConfig()
|
|
13
|
-
|
|
14
|
-
# Initialize temporary logger for error handling during config loading
|
|
15
|
-
self.logger = Logger(debug=False)
|
|
16
|
-
|
|
17
|
-
self.__load_file_config()
|
|
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)
|
|
26
|
-
|
|
27
|
-
def validate_config(self):
|
|
28
|
-
errors: list[str] = []
|
|
29
|
-
if self.config.mode is Mode.testops or self.config.fallback is Mode.testops:
|
|
30
|
-
if self.config.testops.api.token is None:
|
|
31
|
-
errors.append("Testops token is not set")
|
|
32
|
-
|
|
33
|
-
if self.config.testops.project is None:
|
|
34
|
-
errors.append("Testops project is not set")
|
|
35
|
-
|
|
36
|
-
if self.config.mode is Mode.testops_multi or self.config.fallback is Mode.testops_multi:
|
|
37
|
-
if self.config.testops.api.token is None:
|
|
38
|
-
errors.append("Testops token is not set")
|
|
39
|
-
|
|
40
|
-
if not self.config.testops_multi.projects or len(self.config.testops_multi.projects) == 0:
|
|
41
|
-
errors.append("Testops multi: at least one project must be configured")
|
|
42
|
-
|
|
43
|
-
for project in self.config.testops_multi.projects:
|
|
44
|
-
if not project.code:
|
|
45
|
-
errors.append(f"Testops multi: project code is required for all projects")
|
|
46
|
-
|
|
47
|
-
if len(errors) > 0:
|
|
48
|
-
self.logger.log("Config validation failed", "error")
|
|
49
|
-
for error in errors:
|
|
50
|
-
self.logger.log(error, "error")
|
|
51
|
-
raise Exception("Config validation failed")
|
|
52
|
-
|
|
53
|
-
def __str__(self):
|
|
54
|
-
return json.dumps(self.config, indent=4, sort_keys=True)
|
|
55
|
-
|
|
56
|
-
def __load_file_config(self):
|
|
57
|
-
try:
|
|
58
|
-
if os.path.exists(self.__config_file):
|
|
59
|
-
with open(self.__config_file, "r") as file:
|
|
60
|
-
config = json.load(file)
|
|
61
|
-
|
|
62
|
-
if config.get("mode"):
|
|
63
|
-
self.config.set_mode(config.get("mode"))
|
|
64
|
-
|
|
65
|
-
if config.get("fallback"):
|
|
66
|
-
self.config.set_fallback(config.get("fallback"))
|
|
67
|
-
|
|
68
|
-
if config.get("environment"):
|
|
69
|
-
self.config.set_environment(config.get("environment"))
|
|
70
|
-
|
|
71
|
-
if config.get("rootSuite"):
|
|
72
|
-
self.config.set_root_suite(config.get("rootSuite"))
|
|
73
|
-
|
|
74
|
-
if config.get("profilers"):
|
|
75
|
-
self.config.set_profilers(config.get("profilers"))
|
|
76
|
-
|
|
77
|
-
if config.get("debug") is not None:
|
|
78
|
-
self.config.set_debug(
|
|
79
|
-
config.get("debug")
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
if config.get("excludeParams"):
|
|
83
|
-
self.config.set_exclude_params(
|
|
84
|
-
config.get("excludeParams")
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
if config.get("statusMapping"):
|
|
88
|
-
self.config.set_status_mapping(
|
|
89
|
-
config.get("statusMapping")
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
if config.get("executionPlan"):
|
|
93
|
-
execution_plan = config.get("executionPlan")
|
|
94
|
-
if execution_plan.get("path"):
|
|
95
|
-
self.config.execution_plan.set_path(
|
|
96
|
-
execution_plan.get("path"))
|
|
97
|
-
|
|
98
|
-
if config.get("testops"):
|
|
99
|
-
testops = config.get("testops")
|
|
100
|
-
|
|
101
|
-
if testops.get("api"):
|
|
102
|
-
api = testops.get("api")
|
|
103
|
-
|
|
104
|
-
if api.get("host"):
|
|
105
|
-
self.config.testops.api.set_host(
|
|
106
|
-
api.get("host"))
|
|
107
|
-
|
|
108
|
-
if api.get("token"):
|
|
109
|
-
self.config.testops.api.set_token(
|
|
110
|
-
api.get("token"))
|
|
111
|
-
|
|
112
|
-
if api.get("timeout"):
|
|
113
|
-
self.config.testops.api.set_timeout(
|
|
114
|
-
api.get("timeout"))
|
|
115
|
-
|
|
116
|
-
# `is not None`, not truthiness: 0 is a valid value
|
|
117
|
-
# for both, meaning "no retries" / "no backoff".
|
|
118
|
-
if api.get("retries") is not None:
|
|
119
|
-
self.config.testops.api.set_retries(
|
|
120
|
-
api.get("retries"))
|
|
121
|
-
|
|
122
|
-
if api.get("retryBackoff") is not None:
|
|
123
|
-
self.config.testops.api.set_retry_backoff(
|
|
124
|
-
api.get("retryBackoff"))
|
|
125
|
-
|
|
126
|
-
if testops.get("project"):
|
|
127
|
-
self.config.testops.set_project(
|
|
128
|
-
testops.get("project"))
|
|
129
|
-
|
|
130
|
-
if testops.get("defect") is not None:
|
|
131
|
-
self.config.testops.set_defect(
|
|
132
|
-
testops.get("defect")
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
if testops.get("plan"):
|
|
136
|
-
plan = testops.get("plan")
|
|
137
|
-
|
|
138
|
-
if plan.get("id"):
|
|
139
|
-
self.config.testops.plan.set_id(plan.get("id"))
|
|
140
|
-
|
|
141
|
-
if testops.get("run"):
|
|
142
|
-
run = testops.get("run")
|
|
143
|
-
|
|
144
|
-
if run.get("id"):
|
|
145
|
-
self.config.testops.run.set_id(run.get("id"))
|
|
146
|
-
|
|
147
|
-
if run.get("title"):
|
|
148
|
-
self.config.testops.run.set_title(
|
|
149
|
-
run.get("title"))
|
|
150
|
-
|
|
151
|
-
if run.get("description"):
|
|
152
|
-
self.config.testops.run.set_description(
|
|
153
|
-
run.get("description"))
|
|
154
|
-
|
|
155
|
-
if run.get("complete") is not None:
|
|
156
|
-
self.config.testops.run.set_complete(
|
|
157
|
-
run.get("complete")
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
if run.get("tags"):
|
|
161
|
-
self.config.testops.run.set_tags(
|
|
162
|
-
[tag.strip() for tag in run.get("tags")])
|
|
163
|
-
|
|
164
|
-
if run.get("externalLink"):
|
|
165
|
-
self.config.testops.run.set_external_link(
|
|
166
|
-
run.get("externalLink"))
|
|
167
|
-
|
|
168
|
-
if testops.get("batch"):
|
|
169
|
-
batch = testops.get("batch")
|
|
170
|
-
|
|
171
|
-
if batch.get("size"):
|
|
172
|
-
self.config.testops.batch.set_size(
|
|
173
|
-
batch.get("size"))
|
|
174
|
-
|
|
175
|
-
if testops.get("configurations"):
|
|
176
|
-
configurations = testops.get("configurations")
|
|
177
|
-
|
|
178
|
-
if configurations.get("values"):
|
|
179
|
-
values = configurations.get("values")
|
|
180
|
-
for value in values:
|
|
181
|
-
if value.get("name") and value.get("value"):
|
|
182
|
-
self.config.testops.configurations.add_value(
|
|
183
|
-
value.get("name"), value.get("value"))
|
|
184
|
-
|
|
185
|
-
if configurations.get("createIfNotExists") is not None:
|
|
186
|
-
self.config.testops.configurations.set_create_if_not_exists(
|
|
187
|
-
configurations.get("createIfNotExists"))
|
|
188
|
-
|
|
189
|
-
if testops.get("statusFilter"):
|
|
190
|
-
status_filter = testops.get("statusFilter")
|
|
191
|
-
if isinstance(status_filter, list):
|
|
192
|
-
self.config.testops.set_status_filter(status_filter)
|
|
193
|
-
elif isinstance(status_filter, str):
|
|
194
|
-
# Parse comma-separated string
|
|
195
|
-
self.config.testops.set_status_filter([s.strip() for s in status_filter.split(',')])
|
|
196
|
-
|
|
197
|
-
if testops.get("showPublicReportLink") is not None:
|
|
198
|
-
self.config.testops.set_show_public_report_link(
|
|
199
|
-
testops.get("showPublicReportLink")
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
if config.get("testops_multi"):
|
|
203
|
-
self.config.set_testops_multi(config.get("testops_multi"))
|
|
204
|
-
|
|
205
|
-
if config.get("report"):
|
|
206
|
-
report = config.get("report")
|
|
207
|
-
|
|
208
|
-
if report.get("driver"):
|
|
209
|
-
self.config.report.set_driver(report.get("driver"))
|
|
210
|
-
|
|
211
|
-
if report.get("connection"):
|
|
212
|
-
connection = report.get("connection")
|
|
213
|
-
|
|
214
|
-
if connection.get("path"):
|
|
215
|
-
self.config.report.connection.set_path(
|
|
216
|
-
connection.get("path"))
|
|
217
|
-
|
|
218
|
-
if connection.get("format"):
|
|
219
|
-
self.config.report.connection.set_format(
|
|
220
|
-
connection.get("format")
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
if config.get("framework"):
|
|
224
|
-
framework = config.get("framework")
|
|
225
|
-
|
|
226
|
-
if framework.get("pytest"):
|
|
227
|
-
pytest = framework.get("pytest")
|
|
228
|
-
|
|
229
|
-
if pytest.get("captureLogs") is not None:
|
|
230
|
-
self.config.framework.pytest.set_capture_logs(
|
|
231
|
-
pytest.get("captureLogs")
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
if pytest.get("xfailStatus"):
|
|
235
|
-
xfail_status = pytest.get("xfailStatus")
|
|
236
|
-
|
|
237
|
-
if xfail_status.get("xfail"):
|
|
238
|
-
self.config.framework.pytest.xfail_status.set_xfail(
|
|
239
|
-
xfail_status.get("xfail")
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
if xfail_status.get("xpass"):
|
|
243
|
-
self.config.framework.pytest.xfail_status.set_xpass(
|
|
244
|
-
xfail_status.get("xpass")
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
if config.get("logging"):
|
|
248
|
-
self.config.set_logging(config.get("logging"))
|
|
249
|
-
|
|
250
|
-
except Exception as e:
|
|
251
|
-
self.logger.log("Failed to load config from file", "error")
|
|
252
|
-
|
|
253
|
-
def __load_env_config(self):
|
|
254
|
-
try:
|
|
255
|
-
for key, value in os.environ.items():
|
|
256
|
-
if key == 'QASE_MODE':
|
|
257
|
-
self.config.set_mode(value)
|
|
258
|
-
|
|
259
|
-
if key == 'QASE_FALLBACK':
|
|
260
|
-
self.config.set_fallback(value)
|
|
261
|
-
|
|
262
|
-
if key == 'QASE_ENVIRONMENT':
|
|
263
|
-
self.config.set_environment(value)
|
|
264
|
-
|
|
265
|
-
if key == 'QASE_ROOT_SUITE':
|
|
266
|
-
self.config.set_root_suite(value)
|
|
267
|
-
|
|
268
|
-
if key == 'QASE_PROFILERS':
|
|
269
|
-
self.config.set_profilers(value.split(','))
|
|
270
|
-
|
|
271
|
-
if key == 'QASE_PROFILER_NETWORK_EXCLUDE_HOSTS':
|
|
272
|
-
self.config.network_profiler.set_exclude_hosts(
|
|
273
|
-
[h.strip() for h in value.split(',')]
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
if key == 'QASE_DEBUG':
|
|
277
|
-
self.config.set_debug(value)
|
|
278
|
-
|
|
279
|
-
if key == 'QASE_EXCLUDE_PARAMS':
|
|
280
|
-
self.config.set_exclude_params(
|
|
281
|
-
[param.strip() for param in value.split(',')])
|
|
282
|
-
|
|
283
|
-
if key == 'QASE_STATUS_MAPPING':
|
|
284
|
-
# Parse status mapping from environment variable
|
|
285
|
-
# Format: "source1=target1,source2=target2"
|
|
286
|
-
if value:
|
|
287
|
-
mapping_dict = {}
|
|
288
|
-
pairs = value.split(',')
|
|
289
|
-
for pair in pairs:
|
|
290
|
-
pair = pair.strip()
|
|
291
|
-
if pair and '=' in pair:
|
|
292
|
-
source_status, target_status = pair.split('=', 1)
|
|
293
|
-
mapping_dict[source_status.strip()] = target_status.strip()
|
|
294
|
-
self.config.set_status_mapping(mapping_dict)
|
|
295
|
-
|
|
296
|
-
if key == 'QASE_EXECUTION_PLAN_PATH':
|
|
297
|
-
self.config.execution_plan.set_path(value)
|
|
298
|
-
|
|
299
|
-
if key == 'QASE_TESTOPS_API_HOST':
|
|
300
|
-
self.config.testops.api.set_host(value)
|
|
301
|
-
|
|
302
|
-
if key == 'QASE_TESTOPS_API_TOKEN':
|
|
303
|
-
self.config.testops.api.set_token(value)
|
|
304
|
-
|
|
305
|
-
if key == 'QASE_TESTOPS_API_TIMEOUT':
|
|
306
|
-
self.config.testops.api.set_timeout(value)
|
|
307
|
-
|
|
308
|
-
if key == 'QASE_TESTOPS_API_RETRIES':
|
|
309
|
-
self.config.testops.api.set_retries(value)
|
|
310
|
-
|
|
311
|
-
if key == 'QASE_TESTOPS_API_RETRY_BACKOFF':
|
|
312
|
-
self.config.testops.api.set_retry_backoff(value)
|
|
313
|
-
|
|
314
|
-
if key == 'QASE_TESTOPS_PROJECT':
|
|
315
|
-
self.config.testops.set_project(value)
|
|
316
|
-
|
|
317
|
-
if key == 'QASE_TESTOPS_DEFECT':
|
|
318
|
-
self.config.testops.set_defect(value)
|
|
319
|
-
|
|
320
|
-
if key == 'QASE_TESTOPS_PLAN_ID':
|
|
321
|
-
self.config.testops.plan.set_id(int(value.strip()))
|
|
322
|
-
|
|
323
|
-
if key == 'QASE_TESTOPS_RUN_ID':
|
|
324
|
-
self.config.testops.run.set_id(int(value.strip()))
|
|
325
|
-
|
|
326
|
-
if key == 'QASE_TESTOPS_RUN_TITLE':
|
|
327
|
-
self.config.testops.run.set_title(value)
|
|
328
|
-
|
|
329
|
-
if key == 'QASE_TESTOPS_RUN_DESCRIPTION':
|
|
330
|
-
self.config.testops.run.set_description(value)
|
|
331
|
-
|
|
332
|
-
if key == 'QASE_TESTOPS_RUN_COMPLETE':
|
|
333
|
-
self.config.testops.run.set_complete(value)
|
|
334
|
-
|
|
335
|
-
if key == 'QASE_TESTOPS_RUN_TAGS':
|
|
336
|
-
self.config.testops.run.set_tags(
|
|
337
|
-
[tag.strip() for tag in value.split(',')])
|
|
338
|
-
|
|
339
|
-
if key == 'QASE_TESTOPS_RUN_EXTERNAL_LINK_TYPE':
|
|
340
|
-
if not self.config.testops.run.external_link:
|
|
341
|
-
from .models.external_link import ExternalLinkConfig
|
|
342
|
-
self.config.testops.run.external_link = ExternalLinkConfig()
|
|
343
|
-
self.config.testops.run.external_link.set_type(value)
|
|
344
|
-
|
|
345
|
-
if key == 'QASE_TESTOPS_RUN_EXTERNAL_LINK_URL':
|
|
346
|
-
if not self.config.testops.run.external_link:
|
|
347
|
-
from .models.external_link import ExternalLinkConfig
|
|
348
|
-
self.config.testops.run.external_link = ExternalLinkConfig()
|
|
349
|
-
self.config.testops.run.external_link.set_link(value)
|
|
350
|
-
|
|
351
|
-
if key == 'QASE_TESTOPS_BATCH_SIZE':
|
|
352
|
-
self.config.testops.batch.set_size(int(value.strip()))
|
|
353
|
-
|
|
354
|
-
if key == 'QASE_TESTOPS_CONFIGURATIONS_VALUES':
|
|
355
|
-
# Parse configurations from environment variable
|
|
356
|
-
# Format: "group1=value1,group2=value2"
|
|
357
|
-
if value:
|
|
358
|
-
config_pairs = value.split(',')
|
|
359
|
-
for pair in config_pairs:
|
|
360
|
-
if '=' in pair:
|
|
361
|
-
name, config_value = pair.split('=', 1)
|
|
362
|
-
self.config.testops.configurations.add_value(name.strip(), config_value.strip())
|
|
363
|
-
|
|
364
|
-
if key == 'QASE_TESTOPS_CONFIGURATIONS_CREATE_IF_NOT_EXISTS':
|
|
365
|
-
self.config.testops.configurations.set_create_if_not_exists(value)
|
|
366
|
-
|
|
367
|
-
if key == 'QASE_TESTOPS_STATUS_FILTER':
|
|
368
|
-
# Parse comma-separated string
|
|
369
|
-
self.config.testops.set_status_filter([s.strip() for s in value.split(',')])
|
|
370
|
-
|
|
371
|
-
if key == 'QASE_TESTOPS_SHOW_PUBLIC_REPORT_LINK':
|
|
372
|
-
self.config.testops.set_show_public_report_link(value)
|
|
373
|
-
|
|
374
|
-
if key == 'QASE_REPORT_DRIVER':
|
|
375
|
-
self.config.report.set_driver(value)
|
|
376
|
-
|
|
377
|
-
if key == 'QASE_REPORT_CONNECTION_PATH':
|
|
378
|
-
self.config.report.connection.set_path(value)
|
|
379
|
-
|
|
380
|
-
if key == 'QASE_REPORT_CONNECTION_FORMAT':
|
|
381
|
-
self.config.report.connection.set_format(value)
|
|
382
|
-
|
|
383
|
-
if key == 'QASE_PYTEST_CAPTURE_LOGS':
|
|
384
|
-
self.config.framework.pytest.set_capture_logs(value)
|
|
385
|
-
|
|
386
|
-
if key == 'QASE_PYTEST_XFAIL_STATUS_XFAIL':
|
|
387
|
-
self.config.framework.pytest.xfail_status.set_xfail(value)
|
|
388
|
-
|
|
389
|
-
if key == 'QASE_PYTEST_XFAIL_STATUS_XPASS':
|
|
390
|
-
self.config.framework.pytest.xfail_status.set_xpass(value)
|
|
391
|
-
|
|
392
|
-
if key == 'QASE_LOGGING_CONSOLE':
|
|
393
|
-
self.config.logging.set_console(QaseUtils.parse_bool(value))
|
|
394
|
-
|
|
395
|
-
if key == 'QASE_LOGGING_FILE':
|
|
396
|
-
self.config.logging.set_file(QaseUtils.parse_bool(value))
|
|
397
|
-
|
|
398
|
-
except Exception as e:
|
|
399
|
-
self.logger.log(f"Failed to load config from env vars {e}", "error")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/client/api_v1_client.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/client/api_v2_client.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/client/base_api_client.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/exceptions/reporter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/attachment.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/basemodel.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/api.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/connection.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/framework.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/plan.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/qaseconfig.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/report.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/run.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/config/testops.py
RENAMED
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/models/external_link.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/profilers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/profilers/network.py
RENAMED
|
File without changes
|
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/reporters/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/status_mapping/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase/commons/util/token_masker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qase_python_commons-5.1.6 → qase_python_commons-5.1.7}/src/qase_python_commons.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|