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.
Files changed (46) hide show
  1. {pytestomatio-2.8.2.dev35/pytestomatio.egg-info → pytestomatio-2.8.2.dev37}/PKG-INFO +12 -9
  2. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/README.md +10 -7
  3. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pyproject.toml +17 -4
  4. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/connect/connector.py +30 -81
  5. pytestomatio-2.8.2.dev37/pytestomatio/connect/s3_connector.py +69 -0
  6. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/main.py +41 -42
  7. pytestomatio-2.8.2.dev37/pytestomatio/testomatio/filter_plugin.py +52 -0
  8. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/testomatio/testRunConfig.py +15 -25
  9. pytestomatio-2.8.2.dev37/pytestomatio/testomatio/testomatio.py +29 -0
  10. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/utils/helper.py +6 -12
  11. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/utils/parser_setup.py +1 -1
  12. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/utils/validations.py +9 -2
  13. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37/pytestomatio.egg-info}/PKG-INFO +12 -9
  14. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio.egg-info/SOURCES.txt +4 -0
  15. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio.egg-info/requires.txt +1 -1
  16. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/tests/sub/sub_mob/sub_sub_class_test.py +4 -0
  17. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/tests/sub/sub_mob/sub_sub_test.py +6 -1
  18. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/tests/sub/test_class_sub.py +3 -0
  19. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/tests/sub/test_sub.py +5 -0
  20. pytestomatio-2.8.2.dev37/tests/test_cli_param_test_id.py +68 -0
  21. pytestomatio-2.8.2.dev37/tests/test_cli_params.py +26 -0
  22. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/tests/test_decorators.py +2 -0
  23. pytestomatio-2.8.2.dev37/tests/test_xdist.py +46 -0
  24. pytestomatio-2.8.2.dev35/pytestomatio/connect/s3_connector.py +0 -121
  25. pytestomatio-2.8.2.dev35/pytestomatio/testomatio/testomatio.py +0 -39
  26. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/LICENSE +0 -0
  27. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/__init__.py +0 -0
  28. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/connect/__init__.py +0 -0
  29. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/decor/__init__.py +0 -0
  30. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/decor/decorator_updater.py +0 -0
  31. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/decor/default.py +0 -0
  32. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/decor/pep8.py +0 -0
  33. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/testing/__init__.py +0 -0
  34. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/testing/code_collector.py +0 -0
  35. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/testing/testItem.py +0 -0
  36. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/testomatio/__init__.py +0 -0
  37. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/testomatio/testomat_item.py +0 -0
  38. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio/utils/__init__.py +0 -0
  39. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio.egg-info/dependency_links.txt +0 -0
  40. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio.egg-info/entry_points.txt +0 -0
  41. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/pytestomatio.egg-info/top_level.txt +0 -0
  42. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/setup.cfg +0 -0
  43. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/tests/sub/__init__.py +0 -0
  44. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/tests/sub/sub_mob/__init__.py +0 -0
  45. {pytestomatio-2.8.2.dev35 → pytestomatio-2.8.2.dev37}/tests/test_class_root.py +0 -0
  46. {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.dev35
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.29.0
20
+ Requires-Dist: commitizen>=3.18.1
21
21
  Requires-Dist: autopep8>=2.1.0
22
22
 
23
23
  [![Support Ukraine Badge](https://bit.ly/support-ukraine-now)](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, BUCKET_PATH`
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
- 1. `pip install -e .`
228
- 2. `cz commit`
229
- 3. `cz bump`
230
- 4. `git push remoteName branchName --tags`
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, BUCKET_PATH`
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
- 1. `pip install -e .`
206
- 2. `cz commit`
207
- 3. `cz bump`
208
- 4. `git push remoteName branchName --tags`
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 = false
13
+ update_changelog_on_bump = true
14
14
  [project]
15
15
  name = "pytestomatio"
16
- version = "2.8.2.dev35"
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.29.0",
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 = 'https://app.testomat.io', api_key: str = None):
13
+ def __init__(self, base_url: str = '', api_key: str = None):
15
14
  self.base_url = base_url
16
- self._session = requests.Session()
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 as ce:
103
- log.error(f'Failed to connect to {self.base_url}: {ce}')
53
+ except ConnectionError:
54
+ log.error(f'Failed to connect to {self.base_url}')
104
55
  return
105
- except HTTPError as he:
106
- log.error(f'HTTP error occurred while connecting to {self.base_url}: {he}')
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'An unexpected exception occurred. Please report an issue: {e}')
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 as ce:
137
- log.error(f'Failed to connect to {self.base_url}: {ce}')
86
+ except ConnectionError:
87
+ log.error(f'Failed to connect to {self.base_url}')
138
88
  return
139
- except HTTPError as he:
140
- log.error(f'HTTP error occurred while connecting to {self.base_url}: {he}')
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'An unexpected exception occurred. Please report an issue: {e}')
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 as ce:
167
- log.error(f'Failed to connect to {self.base_url}: {ce}')
115
+ except ConnectionError:
116
+ log.error(f'Failed to connect to {self.base_url}')
168
117
  return
169
- except HTTPError as he:
170
- log.error(f'HTTP error occurred while connecting to {self.base_url}: {he}')
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'An unexpected exception occurred. Please report an issue: {e}')
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 as ce:
213
- log.error(f'Failed to connect to {self.base_url}: {ce}')
161
+ except ConnectionError:
162
+ log.error(f'Failed to connect to {self.base_url}')
214
163
  return
215
- except HTTPError as he:
216
- log.error(f'HTTP error occurred while connecting to {self.base_url}: {he}')
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'An unexpected exception occurred. Please report an issue: {e}')
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 as ce:
231
- log.error(f'Failed to connect to {self.base_url}: {ce}')
179
+ except ConnectionError:
180
+ log.error(f'Failed to connect to {self.base_url}')
232
181
  return
233
- except HTTPError as he:
234
- log.error(f'HTTP error occurred while connecting to {self.base_url}: {he}')
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'An unexpected exception occurred. Please report an issue: {e}')
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
- import time
3
- from pytest import Parser, Session, Config, Item, CallInfo, hookimpl
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 .testomatio.testomatio import Testomatio
10
- from pytestomatio.utils.helper import add_and_enrich_tests, get_test_mapping, collect_tests
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
- pytest.testomatio = Testomatio(TestRunConfig())
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
- items[:] = selected_items
88
- config.hook.pytest_deselected(items=deselected_items)
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
- log.error('Test run failed to create. Reporting skipped')
119
- return
122
+ raise Exception('Test run failed to create. Reporting skipped')
120
123
 
121
- s3_details = helper.read_env_s3_keys(run_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
@@ -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
- 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
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
- # 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
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
- 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:
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
- 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:
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
- 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)
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