pytestomatio 2.8.2.dev40__tar.gz → 2.8.2.dev41__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.dev40/pytestomatio.egg-info → pytestomatio-2.8.2.dev41}/PKG-INFO +4 -2
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/README.md +3 -1
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pyproject.toml +1 -1
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/connect/connector.py +34 -31
- pytestomatio-2.8.2.dev41/pytestomatio/connect/s3_connector.py +121 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/main.py +2 -1
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testomatio/testRunConfig.py +25 -15
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testomatio/testomatio.py +15 -5
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/utils/helper.py +12 -6
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41/pytestomatio.egg-info}/PKG-INFO +4 -2
- pytestomatio-2.8.2.dev40/pytestomatio/connect/s3_connector.py +0 -69
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/LICENSE +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/__init__.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/connect/__init__.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/decor/__init__.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/decor/decorator_updater.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/decor/default.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/decor/pep8.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testing/__init__.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testing/code_collector.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testing/testItem.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testomatio/__init__.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testomatio/filter_plugin.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testomatio/testomat_item.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/utils/__init__.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/utils/parser_setup.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/utils/validations.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio.egg-info/SOURCES.txt +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio.egg-info/dependency_links.txt +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio.egg-info/entry_points.txt +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio.egg-info/requires.txt +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio.egg-info/top_level.txt +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/setup.cfg +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/sub/__init__.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/sub/sub_mob/__init__.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/sub/sub_mob/sub_sub_class_test.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/sub/sub_mob/sub_sub_test.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/sub/test_class_sub.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/sub/test_sub.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/test_class_root.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/test_cli_param_test_id.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/test_cli_params.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/test_decorators.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/test_root.py +0 -0
- {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/test_xdist.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.dev41
|
|
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/
|
|
@@ -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, BUCKET_PATH`
|
|
118
118
|
|
|
119
119
|
You would need to decide when you want to upload your test artifacts to cloud storage
|
|
120
120
|
|
|
@@ -219,6 +219,8 @@ 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
|
|
222
224
|
- Fix test duration
|
|
223
225
|
|
|
224
226
|
## Contribution
|
|
@@ -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, BUCKET_PATH`
|
|
96
96
|
|
|
97
97
|
You would need to decide when you want to upload your test artifacts to cloud storage
|
|
98
98
|
|
|
@@ -197,6 +197,8 @@ 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
|
|
200
202
|
- Fix test duration
|
|
201
203
|
|
|
202
204
|
## Contribution
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import requests
|
|
1
|
+
import requests
|
|
2
2
|
from requests.exceptions import HTTPError, ConnectionError
|
|
3
|
+
import logging
|
|
3
4
|
from os.path import join, normpath
|
|
4
5
|
from os import getenv
|
|
5
6
|
from pytestomatio.utils.helper import safe_string_list
|
|
6
7
|
from pytestomatio.testing.testItem import TestItem
|
|
8
|
+
import time
|
|
7
9
|
|
|
8
10
|
log = logging.getLogger('pytestomatio')
|
|
9
11
|
|
|
@@ -11,8 +13,7 @@ log = logging.getLogger('pytestomatio')
|
|
|
11
13
|
class Connector:
|
|
12
14
|
def __init__(self, base_url: str = '', api_key: str = None):
|
|
13
15
|
self.base_url = base_url
|
|
14
|
-
self.
|
|
15
|
-
self.session.verify = True
|
|
16
|
+
self._session = requests.Session()
|
|
16
17
|
self.jwt: str = ''
|
|
17
18
|
self.api_key = api_key
|
|
18
19
|
|
|
@@ -61,7 +62,7 @@ class Connector:
|
|
|
61
62
|
except requests.exceptions.RequestException as e:
|
|
62
63
|
log.error("Internet connection is unavailable. Error: %s", e)
|
|
63
64
|
time.sleep(retry_interval)
|
|
64
|
-
|
|
65
|
+
|
|
65
66
|
log.error("Internet connection check timed out after %d seconds.", timeout)
|
|
66
67
|
return False
|
|
67
68
|
|
|
@@ -98,14 +99,14 @@ class Connector:
|
|
|
98
99
|
|
|
99
100
|
try:
|
|
100
101
|
response = self.session.post(f'{self.base_url}/api/load?api_key={self.api_key}', json=request)
|
|
101
|
-
except ConnectionError:
|
|
102
|
-
log.error(f'Failed to connect to {self.base_url}')
|
|
102
|
+
except ConnectionError as ce:
|
|
103
|
+
log.error(f'Failed to connect to {self.base_url}: {ce}')
|
|
103
104
|
return
|
|
104
|
-
except HTTPError:
|
|
105
|
-
log.error(f'
|
|
105
|
+
except HTTPError as he:
|
|
106
|
+
log.error(f'HTTP error occurred while connecting to {self.base_url}: {he}')
|
|
106
107
|
return
|
|
107
108
|
except Exception as e:
|
|
108
|
-
log.error(f'
|
|
109
|
+
log.error(f'An unexpected exception occurred. Please report an issue: {e}')
|
|
109
110
|
return
|
|
110
111
|
|
|
111
112
|
if response.status_code < 400:
|
|
@@ -127,18 +128,19 @@ class Connector:
|
|
|
127
128
|
"label": label,
|
|
128
129
|
"parallel": parallel,
|
|
129
130
|
"ci_build_url": ci_build_url,
|
|
131
|
+
"shared_run": shared_run
|
|
130
132
|
}
|
|
131
133
|
filtered_request = {k: v for k, v in request.items() if v is not None}
|
|
132
134
|
try:
|
|
133
135
|
response = self.session.post(f'{self.base_url}/api/reporter', json=filtered_request)
|
|
134
|
-
except ConnectionError:
|
|
135
|
-
log.error(f'Failed to connect to {self.base_url}')
|
|
136
|
+
except ConnectionError as ce:
|
|
137
|
+
log.error(f'Failed to connect to {self.base_url}: {ce}')
|
|
136
138
|
return
|
|
137
|
-
except HTTPError:
|
|
138
|
-
log.error(f'
|
|
139
|
+
except HTTPError as he:
|
|
140
|
+
log.error(f'HTTP error occurred while connecting to {self.base_url}: {he}')
|
|
139
141
|
return
|
|
140
142
|
except Exception as e:
|
|
141
|
-
log.error(f'
|
|
143
|
+
log.error(f'An unexpected exception occurred. Please report an issue: {e}')
|
|
142
144
|
return
|
|
143
145
|
|
|
144
146
|
if response.status_code == 200:
|
|
@@ -151,23 +153,24 @@ class Connector:
|
|
|
151
153
|
"api_key": self.api_key,
|
|
152
154
|
"title": title,
|
|
153
155
|
"group_title": group_title,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
"env": env,
|
|
157
|
+
"label": label,
|
|
156
158
|
"parallel": parallel,
|
|
157
159
|
"ci_build_url": ci_build_url,
|
|
160
|
+
"shared_run": shared_run
|
|
158
161
|
}
|
|
159
162
|
filtered_request = {k: v for k, v in request.items() if v is not None}
|
|
160
163
|
|
|
161
164
|
try:
|
|
162
165
|
response = self.session.put(f'{self.base_url}/api/reporter/{id}', json=filtered_request)
|
|
163
|
-
except ConnectionError:
|
|
164
|
-
log.error(f'Failed to connect to {self.base_url}')
|
|
166
|
+
except ConnectionError as ce:
|
|
167
|
+
log.error(f'Failed to connect to {self.base_url}: {ce}')
|
|
165
168
|
return
|
|
166
|
-
except HTTPError:
|
|
167
|
-
log.error(f'
|
|
169
|
+
except HTTPError as he:
|
|
170
|
+
log.error(f'HTTP error occurred while connecting to {self.base_url}: {he}')
|
|
168
171
|
return
|
|
169
172
|
except Exception as e:
|
|
170
|
-
log.error(f'
|
|
173
|
+
log.error(f'An unexpected exception occurred. Please report an issue: {e}')
|
|
171
174
|
return
|
|
172
175
|
|
|
173
176
|
if response.status_code == 200:
|
|
@@ -206,14 +209,14 @@ class Connector:
|
|
|
206
209
|
try:
|
|
207
210
|
response = self.session.post(f'{self.base_url}/api/reporter/{run_id}/testrun?api_key={self.api_key}',
|
|
208
211
|
json=filtered_request)
|
|
209
|
-
except ConnectionError:
|
|
210
|
-
log.error(f'Failed to connect to {self.base_url}')
|
|
212
|
+
except ConnectionError as ce:
|
|
213
|
+
log.error(f'Failed to connect to {self.base_url}: {ce}')
|
|
211
214
|
return
|
|
212
|
-
except HTTPError:
|
|
213
|
-
log.error(f'
|
|
215
|
+
except HTTPError as he:
|
|
216
|
+
log.error(f'HTTP error occurred while connecting to {self.base_url}: {he}')
|
|
214
217
|
return
|
|
215
218
|
except Exception as e:
|
|
216
|
-
log.error(f'
|
|
219
|
+
log.error(f'An unexpected exception occurred. Please report an issue: {e}')
|
|
217
220
|
return
|
|
218
221
|
if response.status_code == 200:
|
|
219
222
|
log.info('Test status updated')
|
|
@@ -224,14 +227,14 @@ class Connector:
|
|
|
224
227
|
try:
|
|
225
228
|
self.session.put(f'{self.base_url}/api/reporter/{run_id}?api_key={self.api_key}',
|
|
226
229
|
json={"status_event": status_event})
|
|
227
|
-
except ConnectionError:
|
|
228
|
-
log.error(f'Failed to connect to {self.base_url}')
|
|
230
|
+
except ConnectionError as ce:
|
|
231
|
+
log.error(f'Failed to connect to {self.base_url}: {ce}')
|
|
229
232
|
return
|
|
230
|
-
except HTTPError:
|
|
231
|
-
log.error(f'
|
|
233
|
+
except HTTPError as he:
|
|
234
|
+
log.error(f'HTTP error occurred while connecting to {self.base_url}: {he}')
|
|
232
235
|
return
|
|
233
236
|
except Exception as e:
|
|
234
|
-
log.error(f'
|
|
237
|
+
log.error(f'An unexpected exception occurred. Please report an issue: {e}')
|
|
235
238
|
return
|
|
236
239
|
|
|
237
240
|
def disconnect(self):
|
|
@@ -0,0 +1,121 @@
|
|
|
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}')
|
|
@@ -119,7 +119,8 @@ def pytest_collection_modifyitems(session: Session, config: Config, items: list[
|
|
|
119
119
|
run_details = pytest.testomatio.connector.update_test_run(**run.to_dict())
|
|
120
120
|
|
|
121
121
|
if run_details is None:
|
|
122
|
-
|
|
122
|
+
log.error('Test run failed to create. Reporting skipped')
|
|
123
|
+
return
|
|
123
124
|
|
|
124
125
|
s3_details = read_env_s3_keys(run_details)
|
|
125
126
|
|
{pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testomatio/testRunConfig.py
RENAMED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import datetime as dt
|
|
3
|
+
import tempfile
|
|
3
4
|
from pytestomatio.utils.helper import safe_string_list
|
|
4
5
|
from typing import Optional
|
|
5
6
|
|
|
7
|
+
TESTOMATIO_TEST_RUN_LOCK_FILE = ".testomatio_test_run_id_lock"
|
|
6
8
|
|
|
7
9
|
class TestRunConfig:
|
|
8
|
-
def __init__(self
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
self.title =
|
|
10
|
+
def __init__(self):
|
|
11
|
+
run_id = os.environ.get('TESTOMATIO_RUN_ID') or os.environ.get('TESTOMATIO_RUN')
|
|
12
|
+
title = os.environ.get('TESTOMATIO_TITLE') if os.environ.get('TESTOMATIO_TITLE') else 'test run at ' + dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
13
|
+
shared_run = os.environ.get('TESTOMATIO_SHARED_RUN') in ['True', 'true', '1']
|
|
14
|
+
self.test_run_id = run_id
|
|
15
|
+
self.title = title
|
|
14
16
|
self.environment = safe_string_list(os.environ.get('TESTOMATIO_ENV'))
|
|
15
17
|
self.label = safe_string_list(os.environ.get('TESTOMATIO_LABEL'))
|
|
16
|
-
self.group_title = os.environ.get('TESTOMATIO_RUNGROUP_TITLE')
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
self.group_title = os.environ.get('TESTOMATIO_RUNGROUP_TITLE')
|
|
19
|
+
# This allows to report tests to the test run by it's id. https://docs.testomat.io/getting-started/running-automated-tests/#reporting-parallel-tests
|
|
20
|
+
self.parallel = False if shared_run else True
|
|
21
|
+
# This allows using test run title to group tests under a single test run. This is needed when running tests in different processes or servers.
|
|
22
|
+
self.shared_run = shared_run
|
|
20
23
|
self.status_request = {}
|
|
21
24
|
self.build_url = self.resolve_build_url()
|
|
22
25
|
|
|
@@ -38,21 +41,28 @@ class TestRunConfig:
|
|
|
38
41
|
|
|
39
42
|
def save_run_id(self, run_id: str) -> None:
|
|
40
43
|
self.test_run_id = run_id
|
|
41
|
-
|
|
44
|
+
temp_dir = tempfile.gettempdir()
|
|
45
|
+
temp_file_path = os.path.join(temp_dir, TESTOMATIO_TEST_RUN_LOCK_FILE)
|
|
46
|
+
with open(temp_file_path, 'w') as f:
|
|
42
47
|
f.write(run_id)
|
|
43
48
|
|
|
49
|
+
|
|
44
50
|
def get_run_id(self) -> Optional[str]:
|
|
45
51
|
if self.test_run_id:
|
|
46
52
|
return self.test_run_id
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
temp_dir = tempfile.gettempdir()
|
|
54
|
+
temp_file_path = os.path.join(temp_dir, TESTOMATIO_TEST_RUN_LOCK_FILE)
|
|
55
|
+
if os.path.exists(temp_file_path):
|
|
56
|
+
with open(temp_file_path, 'r') as f:
|
|
49
57
|
self.test_run_id = f.read()
|
|
50
58
|
return self.test_run_id
|
|
51
59
|
return None
|
|
52
60
|
|
|
53
61
|
def clear_run_id(self) -> None:
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
temp_dir = tempfile.gettempdir()
|
|
63
|
+
temp_file_path = os.path.join(temp_dir, TESTOMATIO_TEST_RUN_LOCK_FILE)
|
|
64
|
+
if os.path.exists(temp_file_path):
|
|
65
|
+
os.remove(temp_file_path)
|
|
56
66
|
|
|
57
67
|
def resolve_build_url(self) -> Optional[str]:
|
|
58
68
|
# You might not always want the build URL to change in the Testomat.io test run
|
|
@@ -2,6 +2,9 @@ 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__)
|
|
5
8
|
|
|
6
9
|
|
|
7
10
|
class Testomatio:
|
|
@@ -11,19 +14,26 @@ class Testomatio:
|
|
|
11
14
|
self.test_run_config: TestRunConfig = test_run_config
|
|
12
15
|
self.connector: Connector = None
|
|
13
16
|
|
|
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
|
+
|
|
14
23
|
def upload_file(self, file_path: str, key: str = None, bucket_name: str = None) -> str:
|
|
15
24
|
if self.test_run_config.test_run_id is None:
|
|
16
|
-
|
|
25
|
+
log.debug("Skipping file upload when testomatio test run is not created")
|
|
17
26
|
return ""
|
|
18
27
|
return self.s3_connector.upload_file(file_path, key, bucket_name)
|
|
19
28
|
|
|
20
29
|
def upload_file_object(self, file_bytes: bytes, key: str, bucket_name: str = None) -> str:
|
|
21
30
|
if self.test_run_config.test_run_id is None:
|
|
22
|
-
|
|
31
|
+
log.debug("Skipping file upload when testomatio test run is not created")
|
|
23
32
|
return ""
|
|
24
33
|
return self.s3_connector.upload_file_object(file_bytes, key, bucket_name)
|
|
25
34
|
|
|
26
|
-
def add_artifacts(self, node: Function,
|
|
35
|
+
def add_artifacts(self, node: Function, url_list) -> None:
|
|
27
36
|
artifact_urls = node.stash.get("artifact_urls", [])
|
|
28
|
-
artifact_urls.extend(
|
|
29
|
-
node.stash["artifact_urls"] = artifact_urls
|
|
37
|
+
artifact_urls.extend(url_list)
|
|
38
|
+
node.stash["artifact_urls"] = [ url for url in artifact_urls if url is not None]
|
|
39
|
+
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
from os import getenv
|
|
2
2
|
from os.path import basename
|
|
3
3
|
from pytest import Item
|
|
4
4
|
from pytestomatio.testomatio.testomat_item import TestomatItem
|
|
@@ -84,12 +84,18 @@ 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(
|
|
87
|
+
def read_env_s3_keys(testRunConfig: dict) -> tuple:
|
|
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"
|
|
88
91
|
return (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
getenv('REGION') or getenv('S3_REGION') or artifacts.get('REGION'),
|
|
93
|
+
getenv('ACCESS_KEY_ID') or getenv('S3_ACCESS_KEY_ID') or artifacts.get('ACCESS_KEY_ID'),
|
|
94
|
+
getenv('SECRET_ACCESS_KEY') or getenv('S3_SECRET_ACCESS_KEY') or artifacts.get('SECRET_ACCESS_KEY'),
|
|
95
|
+
getenv('ENDPOINT') or getenv('S3_ENDPOINT') or artifacts.get('ENDPOINT'),
|
|
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
|
|
93
99
|
)
|
|
94
100
|
|
|
95
101
|
def safe_string_list(param: str):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pytestomatio
|
|
3
|
-
Version: 2.8.2.
|
|
3
|
+
Version: 2.8.2.dev41
|
|
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/
|
|
@@ -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, BUCKET_PATH`
|
|
118
118
|
|
|
119
119
|
You would need to decide when you want to upload your test artifacts to cloud storage
|
|
120
120
|
|
|
@@ -219,6 +219,8 @@ 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
|
|
222
224
|
- Fix test duration
|
|
223
225
|
|
|
224
226
|
## Contribution
|
|
@@ -1,69 +0,0 @@
|
|
|
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}'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/decor/decorator_updater.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testing/code_collector.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testomatio/filter_plugin.py
RENAMED
|
File without changes
|
{pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testomatio/testomat_item.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/sub/sub_mob/sub_sub_class_test.py
RENAMED
|
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
|