pytest-testinel 0.2.1__py3-none-any.whl → 0.3.1__py3-none-any.whl

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.
@@ -1,4 +1,4 @@
1
- import abc, datetime, json, os, uuid
1
+ import abc, datetime, json, os, queue, threading, uuid
2
2
  from urllib.parse import unquote, urlparse
3
3
 
4
4
  import requests
@@ -14,6 +14,18 @@ class ReportingBackend(abc.ABC):
14
14
  def on_end(self) -> None:
15
15
  return
16
16
 
17
+ def request_upload_link(self, filename: str) -> dict | None:
18
+ return None
19
+
20
+ def upload_screenshot(
21
+ self,
22
+ upload_url: str,
23
+ method: str,
24
+ headers: dict,
25
+ filename: str,
26
+ ) -> None:
27
+ return
28
+
17
29
 
18
30
  class NoopReportingBackend(ReportingBackend):
19
31
  def record_event(self, event: dict) -> None:
@@ -47,6 +59,22 @@ class HttpReportingBackend(ReportingBackend):
47
59
  def record_event(self, event: dict) -> None:
48
60
  requests.post(self.url, json=event, verify=False)
49
61
 
62
+ def request_upload_link(self, filename: str) -> dict | None:
63
+ upload_url = f"{self.url.rstrip('/')}/screenshots/upload-link/"
64
+ response = requests.post(upload_url, json={"filename": filename}, verify=False)
65
+ response.raise_for_status()
66
+ return response.json()
67
+
68
+ def upload_screenshot(
69
+ self,
70
+ upload_url: str,
71
+ method: str,
72
+ headers: dict,
73
+ filename: str,
74
+ ) -> None:
75
+ with open(filename, "rb") as f:
76
+ requests.request(method, upload_url, data=f, headers=headers)
77
+
50
78
 
51
79
  class ResultsReporter:
52
80
  run_id: str
@@ -58,6 +86,9 @@ class ResultsReporter:
58
86
  self.dsn = dsn
59
87
  self.run_id = str(uuid.uuid4())
60
88
  self.tests = []
89
+ self.screenshots: list[str] = []
90
+ self._upload_queue: queue.Queue | None = None
91
+ self._uploader: threading.Thread | None = None
61
92
  if backend:
62
93
  self.backend = backend
63
94
  else:
@@ -94,6 +125,10 @@ class ResultsReporter:
94
125
  )
95
126
 
96
127
  def report_end(self) -> None:
128
+ if self._upload_queue is not None:
129
+ self._upload_queue.put(None)
130
+ if self._uploader is not None:
131
+ self._uploader.join(timeout=10)
97
132
  self.backend.record_event(
98
133
  {
99
134
  "run_id": self.run_id,
@@ -110,5 +145,62 @@ class ResultsReporter:
110
145
  "event": event,
111
146
  "timestamp": datetime.datetime.now(datetime.UTC).isoformat(),
112
147
  "payload": payload,
148
+ "screenshots": self.screenshots
113
149
  }
114
150
  )
151
+ self.screenshots = []
152
+
153
+ def report_screenshot(self, filename: str) -> None:
154
+ upload_info = None
155
+ try:
156
+ upload_info = self.backend.request_upload_link(filename)
157
+ except Exception:
158
+ upload_info = None
159
+
160
+ if not upload_info:
161
+ self.screenshots.append(filename)
162
+ return
163
+
164
+ object_key = upload_info.get("object_key")
165
+ if object_key:
166
+ self.screenshots.append(object_key)
167
+ else:
168
+ self.screenshots.append(filename)
169
+
170
+ self._ensure_uploader()
171
+ upload_url = upload_info.get("upload_url")
172
+ method = upload_info.get("method", "PUT")
173
+ headers = upload_info.get("headers", {})
174
+ if self._upload_queue is not None and upload_url:
175
+ self._upload_queue.put((upload_url, method, headers, filename))
176
+
177
+ def _ensure_uploader(self) -> None:
178
+ if self._uploader is not None:
179
+ return
180
+ self._upload_queue = queue.Queue()
181
+ self._uploader = threading.Thread(
182
+ target=self._upload_loop,
183
+ name="testinel-screenshot-uploader",
184
+ daemon=True,
185
+ )
186
+ self._uploader.start()
187
+
188
+ def _upload_loop(self) -> None:
189
+ if self._upload_queue is None:
190
+ return
191
+ while True:
192
+ item = self._upload_queue.get()
193
+ if item is None:
194
+ break
195
+ upload_url, method, headers, filename = item
196
+ try:
197
+ self.backend.upload_screenshot(
198
+ upload_url=upload_url,
199
+ method=method,
200
+ headers=headers,
201
+ filename=filename,
202
+ )
203
+ except Exception:
204
+ pass
205
+ finally:
206
+ self._upload_queue.task_done()
@@ -50,6 +50,42 @@ def _get_test_reporter() -> ResultsReporter:
50
50
  return _test_reporter
51
51
 
52
52
 
53
+ def _safe_path(value: object) -> str:
54
+ try:
55
+ path = os.fspath(value)
56
+ except TypeError:
57
+ return str(value)
58
+ if isinstance(path, bytes):
59
+ try:
60
+ return path.decode()
61
+ except Exception:
62
+ return str(path)
63
+ return path
64
+
65
+
66
+ def _patch_selenium_save_screenshot() -> None:
67
+ try:
68
+ from selenium.webdriver.remote.webdriver import WebDriver
69
+ except Exception:
70
+ return
71
+
72
+ original = getattr(WebDriver, "save_screenshot", None)
73
+ if original is None or getattr(original, "_testinel_patched", False):
74
+ return
75
+
76
+ def patched(self, filename, *args, **kwargs):
77
+ result = original(self, filename, *args, **kwargs)
78
+ try:
79
+ _get_test_reporter().report_screenshot(_safe_path(filename))
80
+ except Exception:
81
+ return result
82
+ return result
83
+
84
+ patched._testinel_patched = True
85
+ patched._testinel_original = original
86
+ WebDriver.save_screenshot = patched
87
+
88
+
53
89
  def serialize_repr(long_repr: ExceptionChainRepr) -> dict:
54
90
  return asdict(long_repr)
55
91
 
@@ -143,3 +179,6 @@ def reporter(request):
143
179
  def pytest_collection_finish(session):
144
180
  tests = [to_test_dict(item) for item in session.items]
145
181
  _get_test_reporter().tests = tests
182
+
183
+
184
+ _patch_selenium_save_screenshot()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-testinel
3
- Version: 0.2.1
3
+ Version: 0.3.1
4
4
  Summary: Testinel’s pytest plugin captures structured test execution data directly from pytest and sends it to Testinel, where your test results become searchable, comparable, and actually useful.
5
5
  Author: Volodymyr Obrizan
6
6
  Author-email: Volodymyr Obrizan <obrizan@first.institute>
@@ -0,0 +1,8 @@
1
+ pytest_testinel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ pytest_testinel/results_reporter.py,sha256=vxifbwbc9NEslwjRqJPxp7C1LFCqV6dw0YUf1UC5aQQ,6280
3
+ pytest_testinel/testinel.py,sha256=NXwUWpE_zSQ_c8BKr64WdZV0vsnnveOF1-vsHEO6Sqs,5166
4
+ pytest_testinel-0.3.1.dist-info/licenses/LICENSE,sha256=g-9SNaCTpjdaj63-SMa03OHuKdcZXsXk4cRKZcLfD1c,1065
5
+ pytest_testinel-0.3.1.dist-info/WHEEL,sha256=iHtWm8nRfs0VRdCYVXocAWFW8ppjHL-uTJkAdZJKOBM,80
6
+ pytest_testinel-0.3.1.dist-info/entry_points.txt,sha256=-vjKXhLEcZLenCKBgXoFNmlNc5DM7qI04yqsDUWH-Ok,49
7
+ pytest_testinel-0.3.1.dist-info/METADATA,sha256=Kzmt48bwY3QHwKqyFrc_9ZUBXg2KKwepqPfgtfZi4yg,1903
8
+ pytest_testinel-0.3.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.26
2
+ Generator: uv 0.9.30
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,8 +0,0 @@
1
- pytest_testinel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- pytest_testinel/results_reporter.py,sha256=HSLdSumSjHswJ2oVbVLVMKVk1dY8sxaGKGfVtp977ic,3283
3
- pytest_testinel/testinel.py,sha256=pIA05QYyqAex5Vn9NEsApBRi7rDIbYtnsKTfX4l3rT0,4152
4
- pytest_testinel-0.2.1.dist-info/licenses/LICENSE,sha256=g-9SNaCTpjdaj63-SMa03OHuKdcZXsXk4cRKZcLfD1c,1065
5
- pytest_testinel-0.2.1.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
6
- pytest_testinel-0.2.1.dist-info/entry_points.txt,sha256=-vjKXhLEcZLenCKBgXoFNmlNc5DM7qI04yqsDUWH-Ok,49
7
- pytest_testinel-0.2.1.dist-info/METADATA,sha256=4TrN9WmDuDh3j31aN9XBjY1CymVqk4-LvX08yrs_QPA,1903
8
- pytest_testinel-0.2.1.dist-info/RECORD,,