pytestomatio 2.8.2.dev35__tar.gz → 2.8.2.dev37__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.dev35/pytestomatio.egg-info → pytestomatio-2.8.2.dev37}/PKG-INFO +12 -9
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/README.md +10 -7
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pyproject.toml +17 -4
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/connect/connector.py +30 -81
- pytestomatio-2.8.2.dev37/pytestomatio/connect/s3_connector.py +69 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/main.py +41 -42
- pytestomatio-2.8.2.dev37/pytestomatio/testomatio/filter_plugin.py +52 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/testomatio/testRunConfig.py +15 -25
- pytestomatio-2.8.2.dev37/pytestomatio/testomatio/testomatio.py +29 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/utils/helper.py +6 -12
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/utils/parser_setup.py +1 -1
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/utils/validations.py +9 -2
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37/pytestomatio.egg-info}/PKG-INFO +12 -9
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio.egg-info/SOURCES.txt +4 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio.egg-info/requires.txt +1 -1
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/tests/sub/sub_mob/sub_sub_class_test.py +4 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/tests/sub/sub_mob/sub_sub_test.py +6 -1
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/tests/sub/test_class_sub.py +3 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/tests/sub/test_sub.py +5 -0
- pytestomatio-2.8.2.dev37/tests/test_cli_param_test_id.py +68 -0
- pytestomatio-2.8.2.dev37/tests/test_cli_params.py +26 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/tests/test_decorators.py +2 -0
- pytestomatio-2.8.2.dev37/tests/test_xdist.py +46 -0
- pytestomatio-2.8.2.dev35/pytestomatio/connect/s3_connector.py +0 -121
- pytestomatio-2.8.2.dev35/pytestomatio/testomatio/testomatio.py +0 -39
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/LICENSE +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/__init__.py +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/connect/__init__.py +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/decor/__init__.py +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/decor/decorator_updater.py +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/decor/default.py +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/decor/pep8.py +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/testing/__init__.py +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/testing/code_collector.py +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/testing/testItem.py +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/testomatio/__init__.py +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/testomatio/testomat_item.py +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/utils/__init__.py +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio.egg-info/dependency_links.txt +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio.egg-info/entry_points.txt +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio.egg-info/top_level.txt +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/setup.cfg +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/tests/sub/__init__.py +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/tests/sub/sub_mob/__init__.py +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/tests/test_class_root.py +0 -0
- {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/tests/test_root.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pytestomatio
|
|
3
|
-
Version: 2.8.2.
|
|
3
|
+
Version: 2.8.2.dev37
|
|
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/
|
|
@@ -17,7 +17,7 @@ Requires-Dist: requests>=2.29.0
|
|
|
17
17
|
Requires-Dist: pytest>7.2.0
|
|
18
18
|
Requires-Dist: boto3>=1.28.28
|
|
19
19
|
Requires-Dist: libcst==1.1.0
|
|
20
|
-
Requires-Dist: commitizen>=3.
|
|
20
|
+
Requires-Dist: commitizen>=3.18.1
|
|
21
21
|
Requires-Dist: autopep8>=2.1.0
|
|
22
22
|
|
|
23
23
|
[](https://github.com/support-ukraine/support-ukraine)
|
|
@@ -114,7 +114,7 @@ https://docs.testomat.io/usage/test-artifacts/
|
|
|
114
114
|
Analyser needs to be aware of the cloud storage credentials.
|
|
115
115
|
There are two options:
|
|
116
116
|
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
|
|
117
|
+
2. Use environment variables `ACCESS_KEY_ID, SECRET_ACCESS_KEY, ENDPOINT, BUCKET`
|
|
118
118
|
|
|
119
119
|
You would need to decide when you want to upload your test artifacts to cloud storage
|
|
120
120
|
|
|
@@ -219,12 +219,15 @@ def test_example():
|
|
|
219
219
|
- test run labels, tags
|
|
220
220
|
|
|
221
221
|
## TODO
|
|
222
|
-
- retry test run update with less attributes, we get 500 from api
|
|
223
|
-
- handler non configured s3 bucket error
|
|
224
222
|
- Fix test duration
|
|
225
223
|
|
|
226
224
|
## Contribution
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
225
|
+
Use python 3.12
|
|
226
|
+
|
|
227
|
+
1. `pip install ".[dev]"` (note, there are still issues with imports in edit mode `pip install -e ".[dev]"`)
|
|
228
|
+
1. `TESTOMATIO_URL=https://beta.testomat.io TESTOMATIO=$TT pytest -p pytester -m smoke`
|
|
229
|
+
1. Test things manually (automated test are WIP)
|
|
230
|
+
1. Verify no regression bugs
|
|
231
|
+
1. `cz commit`
|
|
232
|
+
1. `cz bump`
|
|
233
|
+
1. `git push remoteName branchName --tags`
|
|
@@ -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,12 +197,15 @@ 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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
203
|
+
Use python 3.12
|
|
204
|
+
|
|
205
|
+
1. `pip install ".[dev]"` (note, there are still issues with imports in edit mode `pip install -e ".[dev]"`)
|
|
206
|
+
1. `TESTOMATIO_URL=https://beta.testomat.io TESTOMATIO=$TT pytest -p pytester -m smoke`
|
|
207
|
+
1. Test things manually (automated test are WIP)
|
|
208
|
+
1. Verify no regression bugs
|
|
209
|
+
1. `cz commit`
|
|
210
|
+
1. `cz bump`
|
|
211
|
+
1. `git push remoteName branchName --tags`
|
|
@@ -10,17 +10,17 @@ name = "cz_conventional_commits"
|
|
|
10
10
|
tag_format = "$version"
|
|
11
11
|
version_scheme = "pep440"
|
|
12
12
|
version_provider = "pep621"
|
|
13
|
-
update_changelog_on_bump =
|
|
13
|
+
update_changelog_on_bump = true
|
|
14
14
|
[project]
|
|
15
15
|
name = "pytestomatio"
|
|
16
|
-
version = "2.8.2.
|
|
16
|
+
version = "2.8.2.dev37"
|
|
17
17
|
|
|
18
18
|
dependencies = [
|
|
19
19
|
"requests>=2.29.0",
|
|
20
20
|
"pytest>7.2.0",
|
|
21
21
|
"boto3>=1.28.28",
|
|
22
22
|
"libcst==1.1.0",
|
|
23
|
-
"commitizen>=3.
|
|
23
|
+
"commitizen>=3.18.1",
|
|
24
24
|
"autopep8>=2.1.0"
|
|
25
25
|
]
|
|
26
26
|
|
|
@@ -44,4 +44,17 @@ classifiers = [
|
|
|
44
44
|
"Bug Tracker" = "https://github.com/testomatio/pytestomatio/issues"
|
|
45
45
|
|
|
46
46
|
[project.entry-points.pytest11]
|
|
47
|
-
pytestomatio = "pytestomatio.main"
|
|
47
|
+
pytestomatio = "pytestomatio.main"
|
|
48
|
+
|
|
49
|
+
[options.extras_require]
|
|
50
|
+
dev = [
|
|
51
|
+
"pytest>=7.2.0",
|
|
52
|
+
"pytest-testdox>=2.0.0",
|
|
53
|
+
"pytest-xdist==3.6.1"
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
testpaths = ["tests"]
|
|
58
|
+
markers = [
|
|
59
|
+
"smoke: indicates smoke tests"
|
|
60
|
+
]
|
|
@@ -5,67 +5,18 @@ 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
|
|
|
12
11
|
|
|
13
12
|
class Connector:
|
|
14
|
-
def __init__(self, base_url: str = '
|
|
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}'
|
|
@@ -1,29 +1,39 @@
|
|
|
1
|
-
import os, pytest, logging, json
|
|
2
|
-
|
|
3
|
-
from pytest import Parser, Session, Config, Item, CallInfo
|
|
1
|
+
import os, pytest, logging, json, time
|
|
2
|
+
|
|
3
|
+
from pytest import Parser, Session, Config, Item, CallInfo
|
|
4
4
|
from pytestomatio.connect.connector import Connector
|
|
5
|
-
from pytestomatio.decor.decorator_updater import update_tests
|
|
6
|
-
from pytestomatio.testomatio.testRunConfig import TestRunConfig
|
|
7
|
-
from pytestomatio.testing.testItem import TestItem
|
|
8
5
|
from pytestomatio.connect.s3_connector import S3Connector
|
|
9
|
-
from .
|
|
10
|
-
from pytestomatio.
|
|
6
|
+
from pytestomatio.testing.testItem import TestItem
|
|
7
|
+
from pytestomatio.decor.decorator_updater import update_tests
|
|
8
|
+
|
|
9
|
+
from pytestomatio.utils.helper import add_and_enrich_tests, get_test_mapping, collect_tests, read_env_s3_keys
|
|
11
10
|
from pytestomatio.utils.parser_setup import parser_options
|
|
12
|
-
from pytestomatio.utils import helper
|
|
13
11
|
from pytestomatio.utils import validations
|
|
14
12
|
|
|
13
|
+
from pytestomatio.testomatio.testRunConfig import TestRunConfig
|
|
14
|
+
from pytestomatio.testomatio.testomatio import Testomatio
|
|
15
|
+
from pytestomatio.testomatio.filter_plugin import TestomatioFilterPlugin
|
|
16
|
+
|
|
15
17
|
log = logging.getLogger(__name__)
|
|
16
18
|
log.setLevel('INFO')
|
|
17
19
|
|
|
18
20
|
metadata_file = 'metadata.json'
|
|
19
21
|
decorator_name = 'testomatio'
|
|
20
22
|
testomatio = 'testomatio'
|
|
23
|
+
TESTOMATIO_URL = 'https://app.testomat.io'
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
def pytest_addoption(parser: Parser) -> None:
|
|
24
27
|
parser_options(parser, testomatio)
|
|
25
28
|
|
|
26
29
|
|
|
30
|
+
def pytest_collection(session):
|
|
31
|
+
"""Capture original collected items before any filters are applied."""
|
|
32
|
+
# This hook is called after initial test collection, before other filters.
|
|
33
|
+
# We'll store the items in a session attribute for later use.
|
|
34
|
+
session._pytestomatio_original_collected_items = []
|
|
35
|
+
|
|
36
|
+
|
|
27
37
|
def pytest_configure(config: Config):
|
|
28
38
|
config.addinivalue_line(
|
|
29
39
|
"markers", "testomatio(arg): built in marker to connect test case with testomat.io by unique id"
|
|
@@ -33,9 +43,11 @@ def pytest_configure(config: Config):
|
|
|
33
43
|
if option == 'debug':
|
|
34
44
|
return
|
|
35
45
|
|
|
36
|
-
|
|
46
|
+
is_parallel = config.getoption('numprocesses') is not None
|
|
47
|
+
|
|
48
|
+
pytest.testomatio = Testomatio(TestRunConfig(is_parallel))
|
|
37
49
|
|
|
38
|
-
url = config.getini('testomatio_url')
|
|
50
|
+
url = os.environ.get('TESTOMATIO_URL') or config.getini('testomatio_url') or TESTOMATIO_URL
|
|
39
51
|
project = os.environ.get('TESTOMATIO')
|
|
40
52
|
|
|
41
53
|
pytest.testomatio.connector = Connector(url, project)
|
|
@@ -57,35 +69,27 @@ def pytest_configure(config: Config):
|
|
|
57
69
|
else:
|
|
58
70
|
log.error("Failed to create testrun on Testomat.io")
|
|
59
71
|
|
|
72
|
+
# Mark our pytest_collection_modifyitems hook to run last,
|
|
73
|
+
# so that it sees the effect of all built-in and other filters first.
|
|
74
|
+
# This ensures we only apply our OR logic after other filters have done their job.
|
|
75
|
+
config.pluginmanager.register(TestomatioFilterPlugin(), "testomatio_filter_plugin")
|
|
60
76
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
77
|
+
@pytest.hookimpl(tryfirst=True)
|
|
64
78
|
def pytest_collection_modifyitems(session: Session, config: Config, items: list[Item]) -> None:
|
|
65
79
|
if config.getoption(testomatio) is None:
|
|
66
80
|
return
|
|
67
|
-
|
|
68
|
-
# Filter by --test-id if provided
|
|
69
|
-
test_ids_option = config.getoption("test_id")
|
|
70
|
-
if test_ids_option:
|
|
71
|
-
test_ids = test_ids_option.split("|")
|
|
72
|
-
# Remove "@" from the start of test IDs if present
|
|
73
|
-
test_ids = [test_id.lstrip("@T") for test_id in test_ids]
|
|
74
|
-
selected_items = []
|
|
75
|
-
deselected_items = []
|
|
76
|
-
|
|
77
|
-
for item in items:
|
|
78
|
-
# Check if the test has the marker with the ID we are looking for
|
|
79
|
-
for marker in item.iter_markers(name="testomatio"):
|
|
80
|
-
marker_id = marker.args[0].lstrip("@T") # Strip "@" from the marker argument
|
|
81
|
-
if marker_id in test_ids:
|
|
82
|
-
selected_items.append(item)
|
|
83
|
-
break
|
|
84
|
-
else:
|
|
85
|
-
deselected_items.append(item)
|
|
86
81
|
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
# Store a copy of all initially collected items (the first time this hook runs)
|
|
83
|
+
# The first call to this hook happens before built-in filters like -k, -m fully apply.
|
|
84
|
+
# By the time this runs, items might still be unfiltered or only partially filtered.
|
|
85
|
+
# To ensure we get the full original list, we use pytest_collection hook above.
|
|
86
|
+
if not session._pytestomatio_original_collected_items:
|
|
87
|
+
# The initial call here gives us the full collected list of tests
|
|
88
|
+
session._pytestomatio_original_collected_items = items[:]
|
|
89
|
+
|
|
90
|
+
# At this point, if other plugins or internal filters like -m and -k run,
|
|
91
|
+
# they may modify `items` (removing some tests). We run after them by using a hook wrapper
|
|
92
|
+
# or a trylast marker to ensure our logic runs after most filters.
|
|
89
93
|
|
|
90
94
|
meta, test_files, test_names = collect_tests(items)
|
|
91
95
|
match config.getoption(testomatio):
|
|
@@ -115,17 +119,13 @@ def pytest_collection_modifyitems(session: Session, config: Config, items: list[
|
|
|
115
119
|
run_details = pytest.testomatio.connector.update_test_run(**run.to_dict())
|
|
116
120
|
|
|
117
121
|
if run_details is None:
|
|
118
|
-
|
|
119
|
-
return
|
|
122
|
+
raise Exception('Test run failed to create. Reporting skipped')
|
|
120
123
|
|
|
121
|
-
s3_details =
|
|
124
|
+
s3_details = read_env_s3_keys(run_details)
|
|
122
125
|
|
|
123
126
|
if all(s3_details):
|
|
124
127
|
pytest.testomatio.s3_connector = S3Connector(*s3_details)
|
|
125
128
|
pytest.testomatio.s3_connector.login()
|
|
126
|
-
else:
|
|
127
|
-
# TODO: handle missing credentials
|
|
128
|
-
pytest.testomatio.s3_connector = S3Connector()
|
|
129
129
|
|
|
130
130
|
case 'debug':
|
|
131
131
|
with open(metadata_file, 'w') as file:
|
|
@@ -135,7 +135,6 @@ def pytest_collection_modifyitems(session: Session, config: Config, items: list[
|
|
|
135
135
|
case _:
|
|
136
136
|
raise Exception('Unknown pytestomatio parameter. Use one of: add, remove, sync, debug')
|
|
137
137
|
|
|
138
|
-
|
|
139
138
|
def pytest_runtest_makereport(item: Item, call: CallInfo):
|
|
140
139
|
pytest.testomatio_config_option = item.config.getoption(testomatio)
|
|
141
140
|
if pytest.testomatio_config_option is None or pytest.testomatio_config_option != 'report':
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
class TestomatioFilterPlugin:
|
|
4
|
+
@pytest.hookimpl(trylast=True)
|
|
5
|
+
def pytest_collection_modifyitems(self, session, config, items):
|
|
6
|
+
# By now all other filters (like -m, -k, name-based) have been applied
|
|
7
|
+
# and `items` is the filtered set after all their conditions.
|
|
8
|
+
|
|
9
|
+
test_ids_str = config.getoption("test_id")
|
|
10
|
+
if not test_ids_str:
|
|
11
|
+
# No custom IDs specified, nothing to do
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
test_ids = test_ids_str.split("|")
|
|
15
|
+
# Remove "@" from the start of test IDs if present
|
|
16
|
+
test_ids = [test_id.lstrip("@T") for test_id in test_ids]
|
|
17
|
+
if not test_ids:
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
# Now let's find all tests that match these test IDs from the original full list.
|
|
21
|
+
# We use the originally collected tests to avoid losing tests filtered out by others.
|
|
22
|
+
original_items = session._pytestomatio_original_collected_items
|
|
23
|
+
testomatio_matched = []
|
|
24
|
+
|
|
25
|
+
for item in original_items:
|
|
26
|
+
# Check for testomatio marker
|
|
27
|
+
for marker in item.iter_markers(name="testomatio"):
|
|
28
|
+
|
|
29
|
+
marker_id = marker.args[0].lstrip("@T") # Strip "@" from the marker argument
|
|
30
|
+
if marker_id in test_ids:
|
|
31
|
+
testomatio_matched.append(item)
|
|
32
|
+
break
|
|
33
|
+
|
|
34
|
+
# We'll check common filters: -k, -m and a few others.
|
|
35
|
+
# If they are empty or None, they are not active.
|
|
36
|
+
other_filters_active = bool(
|
|
37
|
+
config.option.keyword or # -k
|
|
38
|
+
config.option.markexpr or # -m
|
|
39
|
+
getattr(config.option, 'last_failed', False) or
|
|
40
|
+
getattr(config.option, 'ff', False) or
|
|
41
|
+
getattr(config.option, 'lf', False) or
|
|
42
|
+
False
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if other_filters_active:
|
|
46
|
+
# If other filters are applied, use OR logic:
|
|
47
|
+
# the final set is all items that passed previous filters plus those matched by test-ids
|
|
48
|
+
items[:] = list(set(items) | set(testomatio_matched))
|
|
49
|
+
else:
|
|
50
|
+
# If no other filters are applied, test-ids filter acts as an exclusive filter:
|
|
51
|
+
# only run tests that match the given test IDs
|
|
52
|
+
items[:] = testomatio_matched
|
{pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/testomatio/testRunConfig.py
RENAMED
|
@@ -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
|