pytestomatio 2.8.2.dev43__tar.gz → 2.9.0__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.
- {pytestomatio-2.8.2.dev43/pytestomatio.egg-info → pytestomatio-2.9.0}/PKG-INFO +9 -5
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/README.md +2 -4
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pyproject.toml +22 -21
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/connect/connector.py +29 -80
- pytestomatio-2.9.0/pytestomatio/connect/s3_connector.py +69 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/main.py +2 -2
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/testing/testItem.py +27 -8
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/testomatio/testRunConfig.py +15 -25
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/testomatio/testomatio.py +5 -15
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/utils/helper.py +6 -12
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0/pytestomatio.egg-info}/PKG-INFO +9 -5
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio.egg-info/SOURCES.txt +1 -0
- pytestomatio-2.9.0/pytestomatio.egg-info/entry_points.txt +2 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio.egg-info/requires.txt +6 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/tests/test_cli_param_test_id.py +12 -12
- pytestomatio-2.9.0/tests/test_parameters.py +67 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/tests/test_sync.py +2 -2
- pytestomatio-2.8.2.dev43/pytestomatio/connect/s3_connector.py +0 -121
- pytestomatio-2.8.2.dev43/tests/test_parameters.py +0 -36
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/LICENSE +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/__init__.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/connect/__init__.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/decor/__init__.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/decor/decorator_updater.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/decor/default.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/decor/pep8.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/testing/__init__.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/testing/code_collector.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/testomatio/__init__.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/testomatio/filter_plugin.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/testomatio/testomat_item.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/utils/__init__.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/utils/parser_setup.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio/utils/validations.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio.egg-info/dependency_links.txt +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/pytestomatio.egg-info/top_level.txt +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/setup.cfg +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/tests/sub/__init__.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/tests/sub/sub_mob/__init__.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/tests/sub/sub_mob/sub_sub_class_test.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/tests/sub/sub_mob/sub_sub_test.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/tests/sub/test_class_sub.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/tests/sub/test_sub.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/tests/test_class_root.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/tests/test_cli_params.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/tests/test_decorators.py +0 -0
- {pytestomatio-2.8.2.dev43 → pytestomatio-2.9.0}/tests/test_root.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pytestomatio
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.0
|
|
4
4
|
Summary: Pytest plugin to sync test with testomat.io
|
|
5
5
|
Author: Oleksii Ostapov, TikoQA
|
|
6
6
|
Project-URL: Testomat.io, https://testomat.io/
|
|
@@ -13,6 +13,12 @@ Classifier: Operating System :: OS Independent
|
|
|
13
13
|
Requires-Python: >=3.10
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
+
Requires-Dist: requests>=2.29.0
|
|
17
|
+
Requires-Dist: pytest>7.2.0
|
|
18
|
+
Requires-Dist: boto3>=1.28.28
|
|
19
|
+
Requires-Dist: libcst==1.1.0
|
|
20
|
+
Requires-Dist: commitizen>=3.18.1
|
|
21
|
+
Requires-Dist: autopep8>=2.1.0
|
|
16
22
|
Provides-Extra: dev
|
|
17
23
|
Requires-Dist: pytest>=7.2.0; extra == "dev"
|
|
18
24
|
Requires-Dist: pytest-testdox>=2.0.0; extra == "dev"
|
|
@@ -114,7 +120,7 @@ https://docs.testomat.io/usage/test-artifacts/
|
|
|
114
120
|
Analyser needs to be aware of the cloud storage credentials.
|
|
115
121
|
There are two options:
|
|
116
122
|
1. Enable **Share credentials with testomat.io Reporter** option in testomat.io Settings -> Artifacts.
|
|
117
|
-
2. Use environment variables `ACCESS_KEY_ID, SECRET_ACCESS_KEY, ENDPOINT, BUCKET
|
|
123
|
+
2. Use environment variables `ACCESS_KEY_ID, SECRET_ACCESS_KEY, ENDPOINT, BUCKET`
|
|
118
124
|
|
|
119
125
|
You would need to decide when you want to upload your test artifacts to cloud storage
|
|
120
126
|
|
|
@@ -219,14 +225,12 @@ def test_example():
|
|
|
219
225
|
- test run labels, tags
|
|
220
226
|
|
|
221
227
|
## TODO
|
|
222
|
-
- retry test run update with less attributes, we get 500 from api
|
|
223
|
-
- handler non configured s3 bucket error
|
|
224
228
|
- Fix test duration
|
|
225
229
|
|
|
226
230
|
## Contribution
|
|
227
231
|
Use python 3.12
|
|
228
232
|
|
|
229
|
-
1. `pip install
|
|
233
|
+
1. `pip install ".[dev]"`
|
|
230
234
|
1. `python ./smoke.py`
|
|
231
235
|
1. Test things manually
|
|
232
236
|
1. Verify no regression bugs
|
|
@@ -92,7 +92,7 @@ https://docs.testomat.io/usage/test-artifacts/
|
|
|
92
92
|
Analyser needs to be aware of the cloud storage credentials.
|
|
93
93
|
There are two options:
|
|
94
94
|
1. Enable **Share credentials with testomat.io Reporter** option in testomat.io Settings -> Artifacts.
|
|
95
|
-
2. Use environment variables `ACCESS_KEY_ID, SECRET_ACCESS_KEY, ENDPOINT, BUCKET
|
|
95
|
+
2. Use environment variables `ACCESS_KEY_ID, SECRET_ACCESS_KEY, ENDPOINT, BUCKET`
|
|
96
96
|
|
|
97
97
|
You would need to decide when you want to upload your test artifacts to cloud storage
|
|
98
98
|
|
|
@@ -197,14 +197,12 @@ def test_example():
|
|
|
197
197
|
- test run labels, tags
|
|
198
198
|
|
|
199
199
|
## TODO
|
|
200
|
-
- retry test run update with less attributes, we get 500 from api
|
|
201
|
-
- handler non configured s3 bucket error
|
|
202
200
|
- Fix test duration
|
|
203
201
|
|
|
204
202
|
## Contribution
|
|
205
203
|
Use python 3.12
|
|
206
204
|
|
|
207
|
-
1. `pip install
|
|
205
|
+
1. `pip install ".[dev]"`
|
|
208
206
|
1. `python ./smoke.py`
|
|
209
207
|
1. Test things manually
|
|
210
208
|
1. Verify no regression bugs
|
|
@@ -11,6 +11,9 @@ tag_format = "$version"
|
|
|
11
11
|
version_scheme = "pep440"
|
|
12
12
|
version_provider = "pep621"
|
|
13
13
|
update_changelog_on_bump = false
|
|
14
|
+
[project]
|
|
15
|
+
name = "pytestomatio"
|
|
16
|
+
version = "2.9.0"
|
|
14
17
|
|
|
15
18
|
dependencies = [
|
|
16
19
|
"requests>=2.29.0",
|
|
@@ -21,31 +24,11 @@ dependencies = [
|
|
|
21
24
|
"autopep8>=2.1.0"
|
|
22
25
|
]
|
|
23
26
|
|
|
24
|
-
[project.optional-dependencies]
|
|
25
|
-
dev = [
|
|
26
|
-
"pytest>=7.2.0",
|
|
27
|
-
"pytest-testdox>=2.0.0",
|
|
28
|
-
"pytest-xdist==3.6.1",
|
|
29
|
-
"python-dotenv==1.0.1",
|
|
30
|
-
"toml==0.10.2"
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
[tool.pytest.ini_options]
|
|
34
|
-
minversion = "6.0"
|
|
35
|
-
addopts = "-ra -q"
|
|
36
|
-
testpaths = ["tests"]
|
|
37
|
-
markers = [
|
|
38
|
-
"smoke: Run smoke tests",
|
|
39
|
-
]
|
|
40
|
-
|
|
41
|
-
[project]
|
|
42
|
-
version = "2.8.2.dev43"
|
|
43
|
-
name = "pytestomatio"
|
|
44
|
-
description = "Pytest plugin to sync test with testomat.io"
|
|
45
27
|
authors = [
|
|
46
28
|
{ name = "Oleksii Ostapov" },
|
|
47
29
|
{ name = "TikoQA" },
|
|
48
30
|
]
|
|
31
|
+
description = "Pytest plugin to sync test with testomat.io"
|
|
49
32
|
readme = "README.md"
|
|
50
33
|
requires-python = ">=3.10"
|
|
51
34
|
classifiers = [
|
|
@@ -59,3 +42,21 @@ classifiers = [
|
|
|
59
42
|
"Testomat.io" = "https://testomat.io/"
|
|
60
43
|
"Homepage" = "https://github.com/testomatio/pytestomatio"
|
|
61
44
|
"Bug Tracker" = "https://github.com/testomatio/pytestomatio/issues"
|
|
45
|
+
|
|
46
|
+
[project.entry-points.pytest11]
|
|
47
|
+
pytestomatio = "pytestomatio.main"
|
|
48
|
+
|
|
49
|
+
[project.optional-dependencies]
|
|
50
|
+
dev = [
|
|
51
|
+
"pytest>=7.2.0",
|
|
52
|
+
"pytest-testdox>=2.0.0",
|
|
53
|
+
"pytest-xdist==3.6.1",
|
|
54
|
+
"python-dotenv==1.0.1",
|
|
55
|
+
"toml==0.10.2"
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
markers = [
|
|
61
|
+
"smoke: indicates smoke tests"
|
|
62
|
+
]
|
|
@@ -5,7 +5,6 @@ from os.path import join, normpath
|
|
|
5
5
|
from os import getenv
|
|
6
6
|
from pytestomatio.utils.helper import safe_string_list
|
|
7
7
|
from pytestomatio.testing.testItem import TestItem
|
|
8
|
-
import time
|
|
9
8
|
|
|
10
9
|
log = logging.getLogger('pytestomatio')
|
|
11
10
|
|
|
@@ -13,59 +12,11 @@ log = logging.getLogger('pytestomatio')
|
|
|
13
12
|
class Connector:
|
|
14
13
|
def __init__(self, base_url: str = '', api_key: str = None):
|
|
15
14
|
self.base_url = base_url
|
|
16
|
-
self.
|
|
15
|
+
self.session = requests.Session()
|
|
16
|
+
self.session.verify = True
|
|
17
17
|
self.jwt: str = ''
|
|
18
18
|
self.api_key = api_key
|
|
19
19
|
|
|
20
|
-
@property
|
|
21
|
-
def session(self):
|
|
22
|
-
"""Get the session, creating it and applying proxy settings if necessary."""
|
|
23
|
-
self._apply_proxy_settings()
|
|
24
|
-
return self._session
|
|
25
|
-
|
|
26
|
-
@session.setter
|
|
27
|
-
def session(self, value):
|
|
28
|
-
"""Allow setting a custom session, while still applying proxy settings."""
|
|
29
|
-
self._session = value
|
|
30
|
-
self._apply_proxy_settings()
|
|
31
|
-
|
|
32
|
-
def _apply_proxy_settings(self):
|
|
33
|
-
"""Apply proxy settings based on environment variables, fallback to no proxy if unavailable."""
|
|
34
|
-
http_proxy = getenv("HTTP_PROXY")
|
|
35
|
-
log.debug(f"HTTP_PROXY: {http_proxy}")
|
|
36
|
-
if http_proxy:
|
|
37
|
-
self._session.proxies = {"http": http_proxy, "https": http_proxy}
|
|
38
|
-
self._session.verify = False
|
|
39
|
-
log.debug(f"Proxy settings applied: {self._session.proxies}")
|
|
40
|
-
|
|
41
|
-
if not self._test_proxy_connection(timeout=1):
|
|
42
|
-
log.debug("Proxy is unavailable. Falling back to a direct connection.")
|
|
43
|
-
self._session.proxies.clear()
|
|
44
|
-
self._session.verify = True
|
|
45
|
-
else:
|
|
46
|
-
log.debug("No proxy settings found. Using a direct connection.")
|
|
47
|
-
self._session.proxies.clear()
|
|
48
|
-
self._session.verify = True
|
|
49
|
-
self._test_proxy_connection()
|
|
50
|
-
|
|
51
|
-
def _test_proxy_connection(self, test_url="https://api.ipify.org?format=json", timeout=30, retry_interval=1):
|
|
52
|
-
log.debug("Current session: %s", self._session.proxies)
|
|
53
|
-
log.debug("Current verify: %s", self._session.verify)
|
|
54
|
-
|
|
55
|
-
start_time = time.time()
|
|
56
|
-
while time.time() - start_time < timeout:
|
|
57
|
-
try:
|
|
58
|
-
response = self._session.get(test_url, timeout=5)
|
|
59
|
-
response.raise_for_status()
|
|
60
|
-
log.debug("Internet connection is available.")
|
|
61
|
-
return True
|
|
62
|
-
except requests.exceptions.RequestException as e:
|
|
63
|
-
log.error("Internet connection is unavailable. Error: %s", e)
|
|
64
|
-
time.sleep(retry_interval)
|
|
65
|
-
|
|
66
|
-
log.error("Internet connection check timed out after %d seconds.", timeout)
|
|
67
|
-
return False
|
|
68
|
-
|
|
69
20
|
def load_tests(
|
|
70
21
|
self,
|
|
71
22
|
tests: list[TestItem],
|
|
@@ -99,14 +50,14 @@ class Connector:
|
|
|
99
50
|
|
|
100
51
|
try:
|
|
101
52
|
response = self.session.post(f'{self.base_url}/api/load?api_key={self.api_key}', json=request)
|
|
102
|
-
except ConnectionError
|
|
103
|
-
log.error(f'Failed to connect to {self.base_url}
|
|
53
|
+
except ConnectionError:
|
|
54
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
104
55
|
return
|
|
105
|
-
except HTTPError
|
|
106
|
-
log.error(f'
|
|
56
|
+
except HTTPError:
|
|
57
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
107
58
|
return
|
|
108
59
|
except Exception as e:
|
|
109
|
-
log.error(f'
|
|
60
|
+
log.error(f'Generic exception happened. Please report an issue. {e}')
|
|
110
61
|
return
|
|
111
62
|
|
|
112
63
|
if response.status_code < 400:
|
|
@@ -128,19 +79,18 @@ class Connector:
|
|
|
128
79
|
"label": label,
|
|
129
80
|
"parallel": parallel,
|
|
130
81
|
"ci_build_url": ci_build_url,
|
|
131
|
-
"shared_run": shared_run
|
|
132
82
|
}
|
|
133
83
|
filtered_request = {k: v for k, v in request.items() if v is not None}
|
|
134
84
|
try:
|
|
135
85
|
response = self.session.post(f'{self.base_url}/api/reporter', json=filtered_request)
|
|
136
|
-
except ConnectionError
|
|
137
|
-
log.error(f'Failed to connect to {self.base_url}
|
|
86
|
+
except ConnectionError:
|
|
87
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
138
88
|
return
|
|
139
|
-
except HTTPError
|
|
140
|
-
log.error(f'
|
|
89
|
+
except HTTPError:
|
|
90
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
141
91
|
return
|
|
142
92
|
except Exception as e:
|
|
143
|
-
log.error(f'
|
|
93
|
+
log.error(f'Generic exception happened. Please report an issue. {e}')
|
|
144
94
|
return
|
|
145
95
|
|
|
146
96
|
if response.status_code == 200:
|
|
@@ -153,24 +103,23 @@ class Connector:
|
|
|
153
103
|
"api_key": self.api_key,
|
|
154
104
|
"title": title,
|
|
155
105
|
"group_title": group_title,
|
|
156
|
-
"env": env,
|
|
157
|
-
"label": label,
|
|
106
|
+
# "env": env, TODO: enabled when bug with 500 response fixed
|
|
107
|
+
# "label": label, TODO: enabled when bug with 500 response fixed
|
|
158
108
|
"parallel": parallel,
|
|
159
109
|
"ci_build_url": ci_build_url,
|
|
160
|
-
"shared_run": shared_run
|
|
161
110
|
}
|
|
162
111
|
filtered_request = {k: v for k, v in request.items() if v is not None}
|
|
163
112
|
|
|
164
113
|
try:
|
|
165
114
|
response = self.session.put(f'{self.base_url}/api/reporter/{id}', json=filtered_request)
|
|
166
|
-
except ConnectionError
|
|
167
|
-
log.error(f'Failed to connect to {self.base_url}
|
|
115
|
+
except ConnectionError:
|
|
116
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
168
117
|
return
|
|
169
|
-
except HTTPError
|
|
170
|
-
log.error(f'
|
|
118
|
+
except HTTPError:
|
|
119
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
171
120
|
return
|
|
172
121
|
except Exception as e:
|
|
173
|
-
log.error(f'
|
|
122
|
+
log.error(f'Generic exception happened. Please report an issue. {e}')
|
|
174
123
|
return
|
|
175
124
|
|
|
176
125
|
if response.status_code == 200:
|
|
@@ -209,14 +158,14 @@ class Connector:
|
|
|
209
158
|
try:
|
|
210
159
|
response = self.session.post(f'{self.base_url}/api/reporter/{run_id}/testrun?api_key={self.api_key}',
|
|
211
160
|
json=filtered_request)
|
|
212
|
-
except ConnectionError
|
|
213
|
-
log.error(f'Failed to connect to {self.base_url}
|
|
161
|
+
except ConnectionError:
|
|
162
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
214
163
|
return
|
|
215
|
-
except HTTPError
|
|
216
|
-
log.error(f'
|
|
164
|
+
except HTTPError:
|
|
165
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
217
166
|
return
|
|
218
167
|
except Exception as e:
|
|
219
|
-
log.error(f'
|
|
168
|
+
log.error(f'Generic exception happened. Please report an issue. {e}')
|
|
220
169
|
return
|
|
221
170
|
if response.status_code == 200:
|
|
222
171
|
log.info('Test status updated')
|
|
@@ -227,14 +176,14 @@ class Connector:
|
|
|
227
176
|
try:
|
|
228
177
|
self.session.put(f'{self.base_url}/api/reporter/{run_id}?api_key={self.api_key}',
|
|
229
178
|
json={"status_event": status_event})
|
|
230
|
-
except ConnectionError
|
|
231
|
-
log.error(f'Failed to connect to {self.base_url}
|
|
179
|
+
except ConnectionError:
|
|
180
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
232
181
|
return
|
|
233
|
-
except HTTPError
|
|
234
|
-
log.error(f'
|
|
182
|
+
except HTTPError:
|
|
183
|
+
log.error(f'Failed to connect to {self.base_url}')
|
|
235
184
|
return
|
|
236
185
|
except Exception as e:
|
|
237
|
-
log.error(f'
|
|
186
|
+
log.error(f'Generic exception happened. Please report an issue. {e}')
|
|
238
187
|
return
|
|
239
188
|
|
|
240
189
|
def disconnect(self):
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
import logging
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
|
|
5
|
+
log = logging.getLogger(__name__)
|
|
6
|
+
log.setLevel('INFO')
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_endpoint(endpoint: str or None) -> str or None:
|
|
10
|
+
if endpoint is None:
|
|
11
|
+
return
|
|
12
|
+
if endpoint.startswith('https://'):
|
|
13
|
+
return endpoint[8:]
|
|
14
|
+
elif endpoint.startswith('http://'):
|
|
15
|
+
return endpoint[7:]
|
|
16
|
+
return endpoint
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class S3Connector:
|
|
20
|
+
def __init__(self, aws_access_key_id: str or None = None,
|
|
21
|
+
aws_secret_access_key: str or None = None,
|
|
22
|
+
endpoint: str or None = None,
|
|
23
|
+
bucket_name: str or None = None):
|
|
24
|
+
|
|
25
|
+
self.endpoint = parse_endpoint(endpoint)
|
|
26
|
+
self.bucket_name = bucket_name
|
|
27
|
+
self.client = None
|
|
28
|
+
self._is_logged_in = False
|
|
29
|
+
self.aws_access_key_id = aws_access_key_id
|
|
30
|
+
self.aws_secret_access_key = aws_secret_access_key
|
|
31
|
+
|
|
32
|
+
def login(self):
|
|
33
|
+
log.debug('creating s3 session')
|
|
34
|
+
self.client = boto3.client(
|
|
35
|
+
's3',
|
|
36
|
+
endpoint_url=f'https://{self.endpoint}',
|
|
37
|
+
aws_access_key_id=self.aws_access_key_id,
|
|
38
|
+
aws_secret_access_key=self.aws_secret_access_key)
|
|
39
|
+
self._is_logged_in = True
|
|
40
|
+
log.info('s3 session created')
|
|
41
|
+
|
|
42
|
+
def upload_file(self, file_path: str, key: str = None, bucket_name: str = None) -> str or None:
|
|
43
|
+
if not self._is_logged_in:
|
|
44
|
+
log.warning('s3 session is not created, creating new one')
|
|
45
|
+
return
|
|
46
|
+
if not key:
|
|
47
|
+
key = file_path
|
|
48
|
+
if not bucket_name:
|
|
49
|
+
bucket_name = self.bucket_name
|
|
50
|
+
if bucket_name is None:
|
|
51
|
+
raise Exception('bucket name is not defined')
|
|
52
|
+
log.info(f'uploading artifact {file_path} to s3://{bucket_name}/{key}')
|
|
53
|
+
self.client.upload_file(file_path, bucket_name, key)
|
|
54
|
+
log.info(f'artifact {file_path} uploaded to s3://{bucket_name}/{key}')
|
|
55
|
+
return f'https://{bucket_name}.{self.endpoint}/{key}'
|
|
56
|
+
|
|
57
|
+
def upload_file_object(self, file_bytes: bytes, key: str, bucket_name: str = None) -> str or None:
|
|
58
|
+
if not self._is_logged_in:
|
|
59
|
+
log.warning('s3 session is not created, creating new one')
|
|
60
|
+
return
|
|
61
|
+
file = BytesIO(file_bytes)
|
|
62
|
+
if not bucket_name:
|
|
63
|
+
bucket_name = self.bucket_name
|
|
64
|
+
if bucket_name is None:
|
|
65
|
+
raise Exception('bucket name is not defined')
|
|
66
|
+
log.info(f'uploading artifact {key} to s3://{bucket_name}/{key}')
|
|
67
|
+
self.client.upload_fileobj(file, bucket_name, key)
|
|
68
|
+
log.info(f'artifact {key} uploaded to s3://{bucket_name}/{key}')
|
|
69
|
+
return f'https://{bucket_name}.{self.endpoint}/{key}'
|
|
@@ -9,6 +9,7 @@ from pytestomatio.decor.decorator_updater import update_tests
|
|
|
9
9
|
from pytestomatio.utils.helper import add_and_enrich_tests, get_test_mapping, collect_tests, read_env_s3_keys
|
|
10
10
|
from pytestomatio.utils.parser_setup import parser_options
|
|
11
11
|
from pytestomatio.utils import validations
|
|
12
|
+
from xdist.plugin import is_xdist_controller, get_xdist_worker_id
|
|
12
13
|
|
|
13
14
|
from pytestomatio.testomatio.testRunConfig import TestRunConfig
|
|
14
15
|
from pytestomatio.testomatio.testomatio import Testomatio
|
|
@@ -119,8 +120,7 @@ def pytest_collection_modifyitems(session: Session, config: Config, items: list[
|
|
|
119
120
|
run_details = pytest.testomatio.connector.update_test_run(**run.to_dict())
|
|
120
121
|
|
|
121
122
|
if run_details is None:
|
|
122
|
-
|
|
123
|
-
return
|
|
123
|
+
raise Exception('Test run failed to create. Reporting skipped')
|
|
124
124
|
|
|
125
125
|
s3_details = read_env_s3_keys(run_details)
|
|
126
126
|
|
|
@@ -95,15 +95,33 @@ class TestItem:
|
|
|
95
95
|
else:
|
|
96
96
|
return name
|
|
97
97
|
|
|
98
|
-
def _get_test_parameter_key(self, item: Item)
|
|
99
|
-
|
|
98
|
+
def _get_test_parameter_key(self, item: Item):
|
|
99
|
+
"""Return a list of parameter names for a given test item."""
|
|
100
|
+
param_names = set()
|
|
101
|
+
|
|
102
|
+
# 1) Look for @pytest.mark.parametrize
|
|
100
103
|
for mark in item.iter_markers('parametrize'):
|
|
101
|
-
|
|
102
|
-
if
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
# mark.args[0] is often a string like "param1,param2"
|
|
105
|
+
# or just "param1" if there's only one.
|
|
106
|
+
if len(mark.args) > 0 and isinstance(mark.args[0], str):
|
|
107
|
+
arg_string = mark.args[0]
|
|
108
|
+
# If the string has commas, split it into multiple names
|
|
109
|
+
if ',' in arg_string:
|
|
110
|
+
param_names.update(name.strip() for name in arg_string.split(','))
|
|
111
|
+
else:
|
|
112
|
+
param_names.add(arg_string.strip())
|
|
113
|
+
|
|
114
|
+
# 2) Look for fixture parameterization (including dynamically generated)
|
|
115
|
+
# via callspec, which holds *all* final parameters for an item.
|
|
116
|
+
callspec = getattr(item, 'callspec', None)
|
|
117
|
+
if callspec:
|
|
118
|
+
# callspec.params is a dict: fixture_name -> parameter_value
|
|
119
|
+
# We only want fixture names, not the values.
|
|
120
|
+
param_names.update(callspec.params.keys())
|
|
121
|
+
|
|
122
|
+
# Return them as a list, or keep it as a set—whatever you prefer.
|
|
123
|
+
return list(param_names)
|
|
124
|
+
|
|
107
125
|
|
|
108
126
|
def _resolve_parameter_key_in_test_name(self, item: Item, test_name: str) -> str:
|
|
109
127
|
test_params = self._get_test_parameter_key(item)
|
|
@@ -120,6 +138,7 @@ class TestItem:
|
|
|
120
138
|
def _resolve_parameter_value_in_test_name(self, item: Item, test_name: str) -> str:
|
|
121
139
|
param_keys = self._get_test_parameter_key(item)
|
|
122
140
|
sync_title = self._get_sync_test_title(item)
|
|
141
|
+
|
|
123
142
|
if not param_keys:
|
|
124
143
|
return test_name
|
|
125
144
|
if not item.callspec:
|
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import datetime as dt
|
|
3
|
-
import tempfile
|
|
4
3
|
from pytestomatio.utils.helper import safe_string_list
|
|
5
4
|
from typing import Optional
|
|
6
5
|
|
|
7
|
-
TESTOMATIO_TEST_RUN_LOCK_FILE = ".testomatio_test_run_id_lock"
|
|
8
6
|
|
|
9
7
|
class TestRunConfig:
|
|
10
|
-
def __init__(self):
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
self.title =
|
|
8
|
+
def __init__(self, parallel: bool = True):
|
|
9
|
+
self.test_run_id = os.environ.get('TESTOMATIO_RUN_ID') or None
|
|
10
|
+
run = os.environ.get('TESTOMATIO_RUN') or None
|
|
11
|
+
title = os.environ.get('TESTOMATIO_TITLE') or None
|
|
12
|
+
run_or_title = run if run else title
|
|
13
|
+
self.title = run_or_title if run_or_title else 'test run at ' + dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
16
14
|
self.environment = safe_string_list(os.environ.get('TESTOMATIO_ENV'))
|
|
17
15
|
self.label = safe_string_list(os.environ.get('TESTOMATIO_LABEL'))
|
|
18
|
-
self.group_title = os.environ.get('TESTOMATIO_RUNGROUP_TITLE')
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
self.shared_run = shared_run
|
|
16
|
+
self.group_title = os.environ.get('TESTOMATIO_RUNGROUP_TITLE') or None
|
|
17
|
+
self.parallel = parallel
|
|
18
|
+
# stands for run with shards
|
|
19
|
+
self.shared_run = run_or_title is not None
|
|
23
20
|
self.status_request = {}
|
|
24
21
|
self.build_url = self.resolve_build_url()
|
|
25
22
|
|
|
@@ -41,28 +38,21 @@ class TestRunConfig:
|
|
|
41
38
|
|
|
42
39
|
def save_run_id(self, run_id: str) -> None:
|
|
43
40
|
self.test_run_id = run_id
|
|
44
|
-
|
|
45
|
-
temp_file_path = os.path.join(temp_dir, TESTOMATIO_TEST_RUN_LOCK_FILE)
|
|
46
|
-
with open(temp_file_path, 'w') as f:
|
|
41
|
+
with open('.temp_test_run_id', 'w') as f:
|
|
47
42
|
f.write(run_id)
|
|
48
43
|
|
|
49
|
-
|
|
50
44
|
def get_run_id(self) -> Optional[str]:
|
|
51
45
|
if self.test_run_id:
|
|
52
46
|
return self.test_run_id
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if os.path.exists(temp_file_path):
|
|
56
|
-
with open(temp_file_path, 'r') as f:
|
|
47
|
+
if os.path.exists('.temp_test_run_id'):
|
|
48
|
+
with open('.temp_test_run_id', 'r') as f:
|
|
57
49
|
self.test_run_id = f.read()
|
|
58
50
|
return self.test_run_id
|
|
59
51
|
return None
|
|
60
52
|
|
|
61
53
|
def clear_run_id(self) -> None:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if os.path.exists(temp_file_path):
|
|
65
|
-
os.remove(temp_file_path)
|
|
54
|
+
if os.path.exists('.temp_test_run_id'):
|
|
55
|
+
os.remove('.temp_test_run_id')
|
|
66
56
|
|
|
67
57
|
def resolve_build_url(self) -> Optional[str]:
|
|
68
58
|
# You might not always want the build URL to change in the Testomat.io test run
|
|
@@ -2,9 +2,6 @@ from _pytest.python import Function
|
|
|
2
2
|
from .testRunConfig import TestRunConfig
|
|
3
3
|
from pytestomatio.connect.s3_connector import S3Connector
|
|
4
4
|
from pytestomatio.connect.connector import Connector
|
|
5
|
-
import logging
|
|
6
|
-
|
|
7
|
-
log = logging.getLogger(__name__)
|
|
8
5
|
|
|
9
6
|
|
|
10
7
|
class Testomatio:
|
|
@@ -14,26 +11,19 @@ class Testomatio:
|
|
|
14
11
|
self.test_run_config: TestRunConfig = test_run_config
|
|
15
12
|
self.connector: Connector = None
|
|
16
13
|
|
|
17
|
-
def upload_files(self, files_list, bucket_name: str = None) -> str:
|
|
18
|
-
if self.test_run_config.test_run_id is None:
|
|
19
|
-
log.debug("Skipping file upload when testomatio test run is not created")
|
|
20
|
-
return ""
|
|
21
|
-
return self.s3_connector.upload_files(files_list, bucket_name)
|
|
22
|
-
|
|
23
14
|
def upload_file(self, file_path: str, key: str = None, bucket_name: str = None) -> str:
|
|
24
15
|
if self.test_run_config.test_run_id is None:
|
|
25
|
-
|
|
16
|
+
print("Skipping file upload when testomatio test run is not created")
|
|
26
17
|
return ""
|
|
27
18
|
return self.s3_connector.upload_file(file_path, key, bucket_name)
|
|
28
19
|
|
|
29
20
|
def upload_file_object(self, file_bytes: bytes, key: str, bucket_name: str = None) -> str:
|
|
30
21
|
if self.test_run_config.test_run_id is None:
|
|
31
|
-
|
|
22
|
+
print("Skipping file upload when testomatio test run is not created")
|
|
32
23
|
return ""
|
|
33
24
|
return self.s3_connector.upload_file_object(file_bytes, key, bucket_name)
|
|
34
25
|
|
|
35
|
-
def add_artifacts(self, node: Function,
|
|
26
|
+
def add_artifacts(self, node: Function, urls: list[str]) -> None:
|
|
36
27
|
artifact_urls = node.stash.get("artifact_urls", [])
|
|
37
|
-
artifact_urls.extend(
|
|
38
|
-
node.stash["artifact_urls"] =
|
|
39
|
-
|
|
28
|
+
artifact_urls.extend(urls)
|
|
29
|
+
node.stash["artifact_urls"] = artifact_urls
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import os
|
|
2
2
|
from os.path import basename
|
|
3
3
|
from pytest import Item
|
|
4
4
|
from pytestomatio.testomatio.testomat_item import TestomatItem
|
|
@@ -84,18 +84,12 @@ def add_and_enrich_tests(meta: list[TestItem], test_files: set,
|
|
|
84
84
|
update_tests(test_file, mapping, test_names, decorator_name)
|
|
85
85
|
|
|
86
86
|
|
|
87
|
-
def read_env_s3_keys(
|
|
88
|
-
artifacts = testRunConfig.get('artifacts', {})
|
|
89
|
-
bucket_path = (getenv('BUCKET_PATH') or getenv('S3_BUCKET_PATH'))
|
|
90
|
-
acl = 'private' if (getenv('TESTOMATIO_PRIVATE_ARTIFACTS') or artifacts.get('presign')) else "public-read"
|
|
87
|
+
def read_env_s3_keys(artifact: dict) -> tuple:
|
|
91
88
|
return (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
getenv('BUCKET') or getenv('S3_BUCKET') or artifacts.get('BUCKET'),
|
|
97
|
-
bucket_path + "/" + testRunConfig.get("uid") if bucket_path else testRunConfig.get("uid"),
|
|
98
|
-
acl
|
|
89
|
+
os.environ.get('ACCESS_KEY_ID') or artifact.get('ACCESS_KEY_ID'),
|
|
90
|
+
os.environ.get('SECRET_ACCESS_KEY') or artifact.get('SECRET_ACCESS_KEY'),
|
|
91
|
+
os.environ.get('ENDPOINT') or artifact.get('ENDPOINT'),
|
|
92
|
+
os.environ.get('BUCKET') or artifact.get('BUCKET')
|
|
99
93
|
)
|
|
100
94
|
|
|
101
95
|
def safe_string_list(param: str):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pytestomatio
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.0
|
|
4
4
|
Summary: Pytest plugin to sync test with testomat.io
|
|
5
5
|
Author: Oleksii Ostapov, TikoQA
|
|
6
6
|
Project-URL: Testomat.io, https://testomat.io/
|
|
@@ -13,6 +13,12 @@ Classifier: Operating System :: OS Independent
|
|
|
13
13
|
Requires-Python: >=3.10
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
+
Requires-Dist: requests>=2.29.0
|
|
17
|
+
Requires-Dist: pytest>7.2.0
|
|
18
|
+
Requires-Dist: boto3>=1.28.28
|
|
19
|
+
Requires-Dist: libcst==1.1.0
|
|
20
|
+
Requires-Dist: commitizen>=3.18.1
|
|
21
|
+
Requires-Dist: autopep8>=2.1.0
|
|
16
22
|
Provides-Extra: dev
|
|
17
23
|
Requires-Dist: pytest>=7.2.0; extra == "dev"
|
|
18
24
|
Requires-Dist: pytest-testdox>=2.0.0; extra == "dev"
|
|
@@ -114,7 +120,7 @@ https://docs.testomat.io/usage/test-artifacts/
|
|
|
114
120
|
Analyser needs to be aware of the cloud storage credentials.
|
|
115
121
|
There are two options:
|
|
116
122
|
1. Enable **Share credentials with testomat.io Reporter** option in testomat.io Settings -> Artifacts.
|
|
117
|
-
2. Use environment variables `ACCESS_KEY_ID, SECRET_ACCESS_KEY, ENDPOINT, BUCKET
|
|
123
|
+
2. Use environment variables `ACCESS_KEY_ID, SECRET_ACCESS_KEY, ENDPOINT, BUCKET`
|
|
118
124
|
|
|
119
125
|
You would need to decide when you want to upload your test artifacts to cloud storage
|
|
120
126
|
|
|
@@ -219,14 +225,12 @@ def test_example():
|
|
|
219
225
|
- test run labels, tags
|
|
220
226
|
|
|
221
227
|
## TODO
|
|
222
|
-
- retry test run update with less attributes, we get 500 from api
|
|
223
|
-
- handler non configured s3 bucket error
|
|
224
228
|
- Fix test duration
|
|
225
229
|
|
|
226
230
|
## Contribution
|
|
227
231
|
Use python 3.12
|
|
228
232
|
|
|
229
|
-
1. `pip install
|
|
233
|
+
1. `pip install ".[dev]"`
|
|
230
234
|
1. `python ./smoke.py`
|
|
231
235
|
1. Test things manually
|
|
232
236
|
1. Verify no regression bugs
|
|
@@ -6,6 +6,7 @@ pytestomatio/main.py
|
|
|
6
6
|
pytestomatio.egg-info/PKG-INFO
|
|
7
7
|
pytestomatio.egg-info/SOURCES.txt
|
|
8
8
|
pytestomatio.egg-info/dependency_links.txt
|
|
9
|
+
pytestomatio.egg-info/entry_points.txt
|
|
9
10
|
pytestomatio.egg-info/requires.txt
|
|
10
11
|
pytestomatio.egg-info/top_level.txt
|
|
11
12
|
pytestomatio/connect/__init__.py
|
|
@@ -23,34 +23,34 @@ test_file = """
|
|
|
23
23
|
def test_cli_param_test_id_without_filters(pytester):
|
|
24
24
|
pytester.makepyfile(test_file)
|
|
25
25
|
|
|
26
|
-
result = pytester.
|
|
26
|
+
result = pytester.runpytest_subprocess("--testomatio", "report", "-vv")
|
|
27
27
|
result.assert_outcomes(passed=4, failed=0, skipped=0)
|
|
28
28
|
result.stdout.fnmatch_lines([
|
|
29
|
-
"*::test_smoke
|
|
30
|
-
"*::test_testomatio_only
|
|
31
|
-
"*::test_smoke_and_testomatio
|
|
32
|
-
"*::test_neither_marker
|
|
29
|
+
"*::test_smoke*",
|
|
30
|
+
"*::test_testomatio_only*",
|
|
31
|
+
"*::test_smoke_and_testomatio*",
|
|
32
|
+
"*::test_neither_marker*",
|
|
33
33
|
])
|
|
34
34
|
|
|
35
35
|
@pytest.mark.testomatio("@T3cf626ca")
|
|
36
36
|
def test_cli_param_test_id_with_k_filter(pytester):
|
|
37
37
|
pytester.makepyfile(test_file)
|
|
38
38
|
|
|
39
|
-
result = pytester.
|
|
39
|
+
result = pytester.runpytest_subprocess("--testomatio" ,"report", "-vv", "-k", "test_neither_marker")
|
|
40
40
|
result.assert_outcomes(passed=1, failed=0, skipped=0)
|
|
41
41
|
result.stdout.fnmatch_lines([
|
|
42
|
-
"*::test_neither_marker
|
|
42
|
+
"*::test_neither_marker*",
|
|
43
43
|
])
|
|
44
44
|
|
|
45
45
|
@pytest.mark.testomatio("@T709adc8a")
|
|
46
46
|
def test_cli_param_test_id_without_k_filter_matching_2_tests(pytester):
|
|
47
47
|
pytester.makepyfile(test_file)
|
|
48
48
|
|
|
49
|
-
result = pytester.
|
|
49
|
+
result = pytester.runpytest_subprocess("--testomatio", "report", "-vv", "-k", "test_smoke")
|
|
50
50
|
result.assert_outcomes(passed=2, failed=0, skipped=0)
|
|
51
51
|
result.stdout.fnmatch_lines([
|
|
52
|
-
"*::test_smoke
|
|
53
|
-
"*::test_smoke_and_testomatio
|
|
52
|
+
"*::test_smoke*",
|
|
53
|
+
"*::test_smoke_and_testomatio*",
|
|
54
54
|
])
|
|
55
55
|
|
|
56
56
|
# TODO: troubleshoot pytester env
|
|
@@ -61,8 +61,8 @@ def test_cli_param_test_id_with_test_id_filter(pytester):
|
|
|
61
61
|
pytest.skip()
|
|
62
62
|
pytester.makepyfile(test_file)
|
|
63
63
|
|
|
64
|
-
result = pytester.
|
|
64
|
+
result = pytester.runpytest_subprocess("--testomatio", "report", '--test-id="@T123"', "-vv")
|
|
65
65
|
result.assert_outcomes(passed=1, failed=0, skipped=0)
|
|
66
66
|
result.stdout.fnmatch_lines([
|
|
67
|
-
"*::test_testomatio_only
|
|
67
|
+
"*::test_testomatio_only*",
|
|
68
68
|
])
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
pytestmark = pytest.mark.smoke
|
|
3
|
+
|
|
4
|
+
test_file = """
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
# Define some dummy callables
|
|
8
|
+
def add(a, b):
|
|
9
|
+
return a + b
|
|
10
|
+
|
|
11
|
+
def multiply(a, b):
|
|
12
|
+
return a * b
|
|
13
|
+
|
|
14
|
+
@pytest.mark.testomatio("@Tbca18714")
|
|
15
|
+
@pytest.mark.parametrize(
|
|
16
|
+
"operation, a, b, expected",
|
|
17
|
+
[
|
|
18
|
+
(add, 2, 3, 5), # Test add function
|
|
19
|
+
(multiply, 2, 3, 6), # Test multiply function
|
|
20
|
+
],
|
|
21
|
+
)
|
|
22
|
+
def test_operations(operation, a, b, expected):
|
|
23
|
+
# Call the provided operation
|
|
24
|
+
result = operation(a, b)
|
|
25
|
+
assert result == expected, f"Expected {expected}, got {result}"
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@pytest.mark.testomatio("@Tb8930394")
|
|
29
|
+
def test_callable_in_params(pytester):
|
|
30
|
+
pytester.makepyfile(test_file)
|
|
31
|
+
|
|
32
|
+
pytester.runpytest("--testomatio", "sync", "-n", "0", "--no-detach")
|
|
33
|
+
result = pytester.runpytest("--testomatio", "report", "-vv")
|
|
34
|
+
result.assert_outcomes(passed=2, failed=0, skipped=0)
|
|
35
|
+
cleaned_lines = [line.strip() for line in result.stdout.lines if line.strip()]
|
|
36
|
+
|
|
37
|
+
assert any("test_callable_in_params.py::test_operations[add-2-3-5]" in line for line in cleaned_lines)
|
|
38
|
+
assert any("test_callable_in_params.py::test_operations[multiply-2-3-6]" in line for line in cleaned_lines)
|
|
39
|
+
|
|
40
|
+
session_fixture_file = """
|
|
41
|
+
import pytest
|
|
42
|
+
|
|
43
|
+
@pytest.fixture(scope="session", params=["db_connection_1", "db_connection_2"])
|
|
44
|
+
def session_fixture(request):
|
|
45
|
+
# Simulate setting up a database connection
|
|
46
|
+
db_connection = request.param
|
|
47
|
+
yield db_connection
|
|
48
|
+
# Simulate tearing down the database connection
|
|
49
|
+
|
|
50
|
+
def test_session_fixture_usage(session_fixture):
|
|
51
|
+
assert session_fixture in ["db_connection_1", "db_connection_2"], (
|
|
52
|
+
f"Unexpected session fixture value: {session_fixture}"
|
|
53
|
+
)
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def test_session_fixture_with_param(pytester):
|
|
57
|
+
pytester.makepyfile(session_fixture_file)
|
|
58
|
+
|
|
59
|
+
pytester.runpytest("--testomatio", "sync", "-n", "0", "--no-detach")
|
|
60
|
+
result = pytester.runpytest("--testomatio", "report", "-vv", "--full-trace")
|
|
61
|
+
result.assert_outcomes(passed=2, failed=0, skipped=0)
|
|
62
|
+
|
|
63
|
+
cleaned_lines = [line.strip() for line in result.stdout.lines if line.strip()]
|
|
64
|
+
|
|
65
|
+
assert any("test_session_fixture_usage[db_connection_1]" in line for line in cleaned_lines)
|
|
66
|
+
assert any("test_session_fixture_usage[db_connection_2]" in line for line in cleaned_lines)
|
|
67
|
+
|
|
@@ -14,7 +14,7 @@ def test_sync_stop_when_xdist_in_use(pytester):
|
|
|
14
14
|
# if option == 'sync' and parallel_set:
|
|
15
15
|
# raise UsageError("The 'sync' mode does not support parallel execution! In order to synchronise test run command sync as '--testomatio sync -n 0'")
|
|
16
16
|
|
|
17
|
-
result = pytester.
|
|
17
|
+
result = pytester.runpytest('-p', 'xdist', '--testomatio', 'sync', '-vv')
|
|
18
18
|
|
|
19
19
|
# Match the entire error line as it appears in stderr
|
|
20
20
|
result.stderr.fnmatch_lines([
|
|
@@ -31,7 +31,7 @@ def test_sync_works_with_xdist_set_to_0(pytester):
|
|
|
31
31
|
assert True
|
|
32
32
|
""")
|
|
33
33
|
|
|
34
|
-
result = pytester.
|
|
34
|
+
result = pytester.runpytest_subprocess('-p', 'xdist', '--testomatio', 'sync', '-n', '0', '-vv')
|
|
35
35
|
|
|
36
36
|
# Assert that the special exit message is printed to stderr
|
|
37
37
|
result.stdout.fnmatch_lines([
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
import boto3
|
|
3
|
-
import logging
|
|
4
|
-
from io import BytesIO
|
|
5
|
-
import mimetypes
|
|
6
|
-
|
|
7
|
-
log = logging.getLogger(__name__)
|
|
8
|
-
log.setLevel('INFO')
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def parse_endpoint(endpoint: str = None) -> Optional[str]:
|
|
12
|
-
if endpoint.startswith('https://'):
|
|
13
|
-
return endpoint[8:]
|
|
14
|
-
elif endpoint.startswith('http://'):
|
|
15
|
-
return endpoint[7:]
|
|
16
|
-
return endpoint
|
|
17
|
-
|
|
18
|
-
# TODO: review error handling. It should be save, and only create log entries without effecting test execution.
|
|
19
|
-
class S3Connector:
|
|
20
|
-
def __init__(self,
|
|
21
|
-
aws_region_name: Optional[str],
|
|
22
|
-
aws_access_key_id: Optional[str],
|
|
23
|
-
aws_secret_access_key: Optional[str],
|
|
24
|
-
endpoint: Optional[str],
|
|
25
|
-
bucket_name: Optional[str],
|
|
26
|
-
bucker_prefix: Optional[str],
|
|
27
|
-
acl: Optional[str] = 'public-read'
|
|
28
|
-
):
|
|
29
|
-
|
|
30
|
-
self.aws_region_name = aws_region_name
|
|
31
|
-
self.endpoint = parse_endpoint(endpoint)
|
|
32
|
-
self.bucket_name = bucket_name
|
|
33
|
-
self.bucker_prefix = bucker_prefix
|
|
34
|
-
self.client = None
|
|
35
|
-
self._is_logged_in = False
|
|
36
|
-
self.aws_access_key_id = aws_access_key_id
|
|
37
|
-
self.aws_secret_access_key = aws_secret_access_key
|
|
38
|
-
self.acl = acl
|
|
39
|
-
|
|
40
|
-
def login(self):
|
|
41
|
-
log.debug('creating s3 session')
|
|
42
|
-
self.client = boto3.client(
|
|
43
|
-
's3',
|
|
44
|
-
endpoint_url=f'https://{self.endpoint}',
|
|
45
|
-
aws_access_key_id=self.aws_access_key_id,
|
|
46
|
-
aws_secret_access_key=self.aws_secret_access_key,
|
|
47
|
-
region_name=self.aws_region_name
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
self._is_logged_in = True
|
|
51
|
-
log.info('s3 session created')
|
|
52
|
-
|
|
53
|
-
# TODO: upload files async
|
|
54
|
-
def upload_files(self, file_list, bucket_name: str = None):
|
|
55
|
-
links = []
|
|
56
|
-
for file_path, key in file_list:
|
|
57
|
-
link = self.upload_file(file_path=file_path, key=key, bucket_name=bucket_name)
|
|
58
|
-
links.append(link)
|
|
59
|
-
return [link for link in links if link is not None]
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def upload_file(self, file_path: str, key: str = None, bucket_name: str = None) -> Optional[str]:
|
|
63
|
-
if not self._is_logged_in:
|
|
64
|
-
log.warning('s3 session is not created, creating new one')
|
|
65
|
-
return
|
|
66
|
-
if not key:
|
|
67
|
-
key = file_path
|
|
68
|
-
key = f"{self.bucker_prefix}/{key}"
|
|
69
|
-
if not bucket_name:
|
|
70
|
-
bucket_name = self.bucket_name
|
|
71
|
-
|
|
72
|
-
content_type, _ = mimetypes.guess_type(key)
|
|
73
|
-
if content_type is None:
|
|
74
|
-
content_type = 'application/octet-stream'
|
|
75
|
-
|
|
76
|
-
try:
|
|
77
|
-
log.info(f'uploading artifact {file_path} to s3://{bucket_name}/{key}')
|
|
78
|
-
self.client.upload_file(
|
|
79
|
-
file_path,
|
|
80
|
-
bucket_name,
|
|
81
|
-
key,
|
|
82
|
-
ExtraArgs={
|
|
83
|
-
'ACL': self.acl,
|
|
84
|
-
'ContentType': content_type,
|
|
85
|
-
'ContentDisposition': 'inline'
|
|
86
|
-
}
|
|
87
|
-
)
|
|
88
|
-
log.info(f'artifact {file_path} uploaded to s3://{bucket_name}/{key}')
|
|
89
|
-
return f'https://{bucket_name}.{self.endpoint}/{key}'
|
|
90
|
-
except Exception as e:
|
|
91
|
-
log.error(f'failed to upload file {file_path} to s3://{bucket_name}/{key}: {e}')
|
|
92
|
-
|
|
93
|
-
def upload_file_object(self, file_bytes: bytes, key: str, bucket_name: str = None) -> Optional[str]:
|
|
94
|
-
if not self._is_logged_in:
|
|
95
|
-
log.warning('s3 session is not created, creating new one')
|
|
96
|
-
return
|
|
97
|
-
file = BytesIO(file_bytes)
|
|
98
|
-
if not bucket_name:
|
|
99
|
-
bucket_name = self.bucket_name
|
|
100
|
-
key = f"{self.bucker_prefix}/{key}"
|
|
101
|
-
|
|
102
|
-
content_type, _ = mimetypes.guess_type(key)
|
|
103
|
-
if content_type is None:
|
|
104
|
-
content_type = 'application/octet-stream'
|
|
105
|
-
|
|
106
|
-
try:
|
|
107
|
-
log.info(f'uploading artifact {key} to s3://{bucket_name}/{key}')
|
|
108
|
-
self.client.upload_fileobj(
|
|
109
|
-
file,
|
|
110
|
-
bucket_name,
|
|
111
|
-
key,
|
|
112
|
-
ExtraArgs={
|
|
113
|
-
'ACL': self.acl,
|
|
114
|
-
'ContentType': content_type,
|
|
115
|
-
'ContentDisposition': 'inline'
|
|
116
|
-
}
|
|
117
|
-
)
|
|
118
|
-
log.info(f'artifact {key} uploaded to s3://{bucket_name}/{key}')
|
|
119
|
-
return f'https://{bucket_name}.{self.endpoint}/{key}'
|
|
120
|
-
except Exception as e:
|
|
121
|
-
log.error(f'failed to upload file {key} to s3://{bucket_name}/{key}: {e}')
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
pytestmark = pytest.mark.smoke
|
|
3
|
-
|
|
4
|
-
test_file = """
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
# Define some dummy callables
|
|
8
|
-
def add(a, b):
|
|
9
|
-
return a + b
|
|
10
|
-
|
|
11
|
-
def multiply(a, b):
|
|
12
|
-
return a * b
|
|
13
|
-
|
|
14
|
-
@pytest.mark.testomatio("@Tbca18714")
|
|
15
|
-
@pytest.mark.parametrize(
|
|
16
|
-
"operation, a, b, expected",
|
|
17
|
-
[
|
|
18
|
-
(add, 2, 3, 5), # Test add function
|
|
19
|
-
(multiply, 2, 3, 6), # Test multiply function
|
|
20
|
-
],
|
|
21
|
-
)
|
|
22
|
-
def test_operations(operation, a, b, expected):
|
|
23
|
-
# Call the provided operation
|
|
24
|
-
result = operation(a, b)
|
|
25
|
-
assert result == expected, f"Expected {expected}, got {result}"
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
@pytest.mark.testomatio("@Tb8930394")
|
|
29
|
-
def test_callable_in_params(pytester):
|
|
30
|
-
pytester.makepyfile(test_file)
|
|
31
|
-
|
|
32
|
-
result = pytester.runpytest("--testomatio", "report", "-vv")
|
|
33
|
-
result.assert_outcomes(passed=2, failed=0, skipped=0)
|
|
34
|
-
assert "test_callable_in_params.py::test_operations[add-2-3-5] PASSED" in result.stdout.str()
|
|
35
|
-
assert "test_callable_in_params.py::test_operations[multiply-2-3-6] PASSED" in result.stdout.str()
|
|
36
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|