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.
Files changed (45) hide show
  1. {pytestomatio-2.8.2.dev40/pytestomatio.egg-info → pytestomatio-2.8.2.dev41}/PKG-INFO +4 -2
  2. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/README.md +3 -1
  3. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pyproject.toml +1 -1
  4. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/connect/connector.py +34 -31
  5. pytestomatio-2.8.2.dev41/pytestomatio/connect/s3_connector.py +121 -0
  6. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/main.py +2 -1
  7. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testomatio/testRunConfig.py +25 -15
  8. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testomatio/testomatio.py +15 -5
  9. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/utils/helper.py +12 -6
  10. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41/pytestomatio.egg-info}/PKG-INFO +4 -2
  11. pytestomatio-2.8.2.dev40/pytestomatio/connect/s3_connector.py +0 -69
  12. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/LICENSE +0 -0
  13. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/__init__.py +0 -0
  14. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/connect/__init__.py +0 -0
  15. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/decor/__init__.py +0 -0
  16. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/decor/decorator_updater.py +0 -0
  17. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/decor/default.py +0 -0
  18. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/decor/pep8.py +0 -0
  19. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testing/__init__.py +0 -0
  20. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testing/code_collector.py +0 -0
  21. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testing/testItem.py +0 -0
  22. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testomatio/__init__.py +0 -0
  23. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testomatio/filter_plugin.py +0 -0
  24. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/testomatio/testomat_item.py +0 -0
  25. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/utils/__init__.py +0 -0
  26. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/utils/parser_setup.py +0 -0
  27. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio/utils/validations.py +0 -0
  28. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio.egg-info/SOURCES.txt +0 -0
  29. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio.egg-info/dependency_links.txt +0 -0
  30. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio.egg-info/entry_points.txt +0 -0
  31. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio.egg-info/requires.txt +0 -0
  32. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/pytestomatio.egg-info/top_level.txt +0 -0
  33. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/setup.cfg +0 -0
  34. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/sub/__init__.py +0 -0
  35. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/sub/sub_mob/__init__.py +0 -0
  36. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/sub/sub_mob/sub_sub_class_test.py +0 -0
  37. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/sub/sub_mob/sub_sub_test.py +0 -0
  38. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/sub/test_class_sub.py +0 -0
  39. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/sub/test_sub.py +0 -0
  40. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/test_class_root.py +0 -0
  41. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/test_cli_param_test_id.py +0 -0
  42. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/test_cli_params.py +0 -0
  43. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/test_decorators.py +0 -0
  44. {pytestomatio-2.8.2.dev40 → pytestomatio-2.8.2.dev41}/tests/test_root.py +0 -0
  45. {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.dev40
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
@@ -13,7 +13,7 @@ version_provider = "pep621"
13
13
  update_changelog_on_bump = false
14
14
  [project]
15
15
  name = "pytestomatio"
16
- version = "2.8.2.dev40"
16
+ version = "2.8.2.dev41"
17
17
 
18
18
  dependencies = [
19
19
  "requests>=2.29.0",
@@ -1,9 +1,11 @@
1
- import requests, time, logging
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.session = requests.Session()
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'Failed to connect to {self.base_url}')
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'Generic exception happened. Please report an issue. {e}')
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'Failed to connect to {self.base_url}')
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'Generic exception happened. Please report an issue. {e}')
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
- # "env": env, TODO: enabled when bug with 500 response fixed
155
- # "label": label, TODO: enabled when bug with 500 response fixed
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'Failed to connect to {self.base_url}')
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'Generic exception happened. Please report an issue. {e}')
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'Failed to connect to {self.base_url}')
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'Generic exception happened. Please report an issue. {e}')
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'Failed to connect to {self.base_url}')
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'Generic exception happened. Please report an issue. {e}')
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
- raise Exception('Test run failed to create. Reporting skipped')
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
 
@@ -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, 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")
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') or None
17
- self.parallel = parallel
18
- # stands for run with shards
19
- self.shared_run = run_or_title is not None
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
- with open('.temp_test_run_id', 'w') as f:
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
- if os.path.exists('.temp_test_run_id'):
48
- with open('.temp_test_run_id', 'r') as f:
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
- if os.path.exists('.temp_test_run_id'):
55
- os.remove('.temp_test_run_id')
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
- print("Skipping file upload when testomatio test run is not created")
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
- print("Skipping file upload when testomatio test run is not created")
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, urls: list[str]) -> None:
35
+ def add_artifacts(self, node: Function, url_list) -> None:
27
36
  artifact_urls = node.stash.get("artifact_urls", [])
28
- artifact_urls.extend(urls)
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 os
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(artifact: dict) -> tuple:
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
- 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')
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.dev40
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}'