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.
- pytest_testinel/results_reporter.py +93 -1
- pytest_testinel/testinel.py +39 -0
- {pytest_testinel-0.2.1.dist-info → pytest_testinel-0.3.1.dist-info}/METADATA +1 -1
- pytest_testinel-0.3.1.dist-info/RECORD +8 -0
- {pytest_testinel-0.2.1.dist-info → pytest_testinel-0.3.1.dist-info}/WHEEL +1 -1
- pytest_testinel-0.2.1.dist-info/RECORD +0 -8
- {pytest_testinel-0.2.1.dist-info → pytest_testinel-0.3.1.dist-info}/entry_points.txt +0 -0
- {pytest_testinel-0.2.1.dist-info → pytest_testinel-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -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()
|
pytest_testinel/testinel.py
CHANGED
|
@@ -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.
|
|
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,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,,
|
|
File without changes
|
|
File without changes
|