hardpy 0.8.0__py3-none-any.whl → 0.10.0__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.
- hardpy/__init__.py +6 -1
- hardpy/cli/cli.py +78 -11
- hardpy/common/config.py +16 -0
- hardpy/common/stand_cloud/__init__.py +13 -0
- hardpy/common/stand_cloud/connector.py +227 -0
- hardpy/common/stand_cloud/exception.py +9 -0
- hardpy/common/stand_cloud/oauth_callback.py +95 -0
- hardpy/common/stand_cloud/registration.py +209 -0
- hardpy/common/stand_cloud/token_storage.py +23 -0
- hardpy/hardpy_panel/api.py +10 -8
- hardpy/hardpy_panel/frontend/dist/asset-manifest.json +3 -3
- hardpy/hardpy_panel/frontend/dist/index.html +1 -1
- hardpy/hardpy_panel/frontend/dist/static/js/main.8a7d8f7d.js +3 -0
- hardpy/hardpy_panel/frontend/dist/static/js/main.8a7d8f7d.js.map +1 -0
- hardpy/pytest_hardpy/db/const.py +6 -0
- hardpy/pytest_hardpy/db/schema/v1.py +12 -3
- hardpy/pytest_hardpy/plugin.py +56 -4
- hardpy/pytest_hardpy/pytest_call.py +79 -42
- hardpy/pytest_hardpy/pytest_wrapper.py +14 -4
- hardpy/pytest_hardpy/reporter/hook_reporter.py +10 -0
- hardpy/pytest_hardpy/result/__init__.py +4 -0
- hardpy/pytest_hardpy/result/report_loader/__init__.py +4 -0
- hardpy/pytest_hardpy/result/report_loader/stand_cloud_loader.py +72 -0
- hardpy/pytest_hardpy/utils/connection_data.py +2 -0
- hardpy/pytest_hardpy/utils/dialog_box.py +10 -0
- {hardpy-0.8.0.dist-info → hardpy-0.10.0.dist-info}/METADATA +9 -2
- {hardpy-0.8.0.dist-info → hardpy-0.10.0.dist-info}/RECORD +31 -24
- {hardpy-0.8.0.dist-info → hardpy-0.10.0.dist-info}/WHEEL +1 -1
- hardpy/hardpy_panel/frontend/dist/static/js/main.6f09d61a.js +0 -3
- hardpy/hardpy_panel/frontend/dist/static/js/main.6f09d61a.js.map +0 -1
- /hardpy/hardpy_panel/frontend/dist/static/js/{main.6f09d61a.js.LICENSE.txt → main.8a7d8f7d.js.LICENSE.txt} +0 -0
- {hardpy-0.8.0.dist-info → hardpy-0.10.0.dist-info}/entry_points.txt +0 -0
- {hardpy-0.8.0.dist-info → hardpy-0.10.0.dist-info}/licenses/LICENSE +0 -0
hardpy/pytest_hardpy/db/const.py
CHANGED
|
@@ -42,7 +42,10 @@ class CaseStateStore(IBaseResult):
|
|
|
42
42
|
"text": "some text"
|
|
43
43
|
},
|
|
44
44
|
"type": "textinput"
|
|
45
|
-
}
|
|
45
|
+
},
|
|
46
|
+
visible: true,
|
|
47
|
+
id: "af6ac3e7-7ce8-4a6b-bb9d-88c3e10b5c7a",
|
|
48
|
+
font_size: 14
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
}
|
|
@@ -214,6 +217,7 @@ class ResultStateStore(IBaseResult):
|
|
|
214
217
|
"start_time": 1695817263,
|
|
215
218
|
"status": "failed",
|
|
216
219
|
"name": "hardpy-stand",
|
|
220
|
+
"alert": "",
|
|
217
221
|
"dut": {
|
|
218
222
|
"serial_number": "92c5a4bb-ecb0-42c5-89ac-e0caca0919fd",
|
|
219
223
|
"part_number": "part_1",
|
|
@@ -241,7 +245,8 @@ class ResultStateStore(IBaseResult):
|
|
|
241
245
|
"operator_msg": {
|
|
242
246
|
"msg": "Operator message",
|
|
243
247
|
"title": "Message",
|
|
244
|
-
"visible":
|
|
248
|
+
"visible": true,
|
|
249
|
+
"id": "f45ac1e7-2ce8-4a6b-bb9d-8863e30bcc78"
|
|
245
250
|
},
|
|
246
251
|
"modules": {
|
|
247
252
|
"test_1_a": {
|
|
@@ -266,7 +271,10 @@ class ResultStateStore(IBaseResult):
|
|
|
266
271
|
"text": "some text"
|
|
267
272
|
},
|
|
268
273
|
"type": "textinput"
|
|
269
|
-
}
|
|
274
|
+
},
|
|
275
|
+
visible: true,
|
|
276
|
+
id: "f45bc1e7-2c18-4a4b-2b9d-8863e30bcc78",
|
|
277
|
+
font_size: 14
|
|
270
278
|
}
|
|
271
279
|
},
|
|
272
280
|
"test_minute_parity": {
|
|
@@ -296,6 +304,7 @@ class ResultStateStore(IBaseResult):
|
|
|
296
304
|
dut: Dut
|
|
297
305
|
modules: dict[str, ModuleStateStore] = {}
|
|
298
306
|
operator_msg: dict = {}
|
|
307
|
+
alert: str
|
|
299
308
|
|
|
300
309
|
|
|
301
310
|
class ResultRunStore(IBaseResult):
|
hardpy/pytest_hardpy/plugin.py
CHANGED
|
@@ -7,6 +7,7 @@ from logging import getLogger
|
|
|
7
7
|
from pathlib import Path, PurePath
|
|
8
8
|
from platform import system
|
|
9
9
|
from re import compile as re_compile
|
|
10
|
+
from time import sleep
|
|
10
11
|
from typing import Any, Callable
|
|
11
12
|
|
|
12
13
|
from _pytest._code.code import (
|
|
@@ -30,6 +31,7 @@ from pytest import (
|
|
|
30
31
|
skip,
|
|
31
32
|
)
|
|
32
33
|
|
|
34
|
+
from hardpy.common.stand_cloud.connector import StandCloudConnector, StandCloudError
|
|
33
35
|
from hardpy.pytest_hardpy.reporter import HookReporter
|
|
34
36
|
from hardpy.pytest_hardpy.utils import (
|
|
35
37
|
ConnectionData,
|
|
@@ -39,6 +41,11 @@ from hardpy.pytest_hardpy.utils import (
|
|
|
39
41
|
)
|
|
40
42
|
from hardpy.pytest_hardpy.utils.node_info import TestDependencyInfo
|
|
41
43
|
|
|
44
|
+
if __debug__:
|
|
45
|
+
from urllib3 import disable_warnings
|
|
46
|
+
from urllib3.exceptions import InsecureRequestWarning
|
|
47
|
+
|
|
48
|
+
disable_warnings(InsecureRequestWarning)
|
|
42
49
|
|
|
43
50
|
def pytest_addoption(parser: Parser) -> None:
|
|
44
51
|
"""Register argparse-style options."""
|
|
@@ -73,6 +80,18 @@ def pytest_addoption(parser: Parser) -> None:
|
|
|
73
80
|
default=False,
|
|
74
81
|
help="enable pytest-hardpy plugin",
|
|
75
82
|
)
|
|
83
|
+
parser.addoption(
|
|
84
|
+
"--sc-address",
|
|
85
|
+
action="store",
|
|
86
|
+
default=con_data.sc_address,
|
|
87
|
+
help="StandCloud address",
|
|
88
|
+
)
|
|
89
|
+
parser.addoption(
|
|
90
|
+
"--sc-connection-only",
|
|
91
|
+
action="store_true",
|
|
92
|
+
default=con_data.sc_connection_only,
|
|
93
|
+
help="check StandCloud availability",
|
|
94
|
+
)
|
|
76
95
|
|
|
77
96
|
|
|
78
97
|
# Bootstrapping hooks
|
|
@@ -125,6 +144,14 @@ class HardpyPlugin:
|
|
|
125
144
|
if socket_host:
|
|
126
145
|
con_data.socket_host = str(socket_host) # type: ignore
|
|
127
146
|
|
|
147
|
+
sc_address = config.getoption("--sc-address")
|
|
148
|
+
if sc_address:
|
|
149
|
+
con_data.sc_address = str(sc_address) # type: ignore
|
|
150
|
+
|
|
151
|
+
sc_connection_only = config.getoption("--sc-connection-only")
|
|
152
|
+
if sc_connection_only:
|
|
153
|
+
con_data.sc_connection_only = bool(sc_connection_only) # type: ignore
|
|
154
|
+
|
|
128
155
|
config.addinivalue_line("markers", "case_name")
|
|
129
156
|
config.addinivalue_line("markers", "module_name")
|
|
130
157
|
config.addinivalue_line("markers", "dependency")
|
|
@@ -201,6 +228,27 @@ class HardpyPlugin:
|
|
|
201
228
|
# ignore collect only mode
|
|
202
229
|
return True
|
|
203
230
|
|
|
231
|
+
con_data = ConnectionData()
|
|
232
|
+
|
|
233
|
+
if con_data.sc_connection_only: # check
|
|
234
|
+
try:
|
|
235
|
+
sc_connector = StandCloudConnector(addr=con_data.sc_address)
|
|
236
|
+
except StandCloudError as exc:
|
|
237
|
+
msg = str(exc)
|
|
238
|
+
self._reporter.set_alert(msg)
|
|
239
|
+
exit(msg)
|
|
240
|
+
try:
|
|
241
|
+
sc_connector.healthcheck()
|
|
242
|
+
except Exception: # noqa: BLE001
|
|
243
|
+
addr = con_data.sc_address
|
|
244
|
+
msg = (
|
|
245
|
+
f"StandCloud service at the address {addr} "
|
|
246
|
+
"not available or HardPy user is not authorized"
|
|
247
|
+
)
|
|
248
|
+
self._reporter.set_alert(msg)
|
|
249
|
+
self._reporter.update_db_by_doc()
|
|
250
|
+
exit(msg)
|
|
251
|
+
|
|
204
252
|
# testrun entrypoint
|
|
205
253
|
self._reporter.start()
|
|
206
254
|
self._reporter.update_db_by_doc()
|
|
@@ -254,6 +302,9 @@ class HardpyPlugin:
|
|
|
254
302
|
|
|
255
303
|
# first attempt was in pytest_runtest_call
|
|
256
304
|
for current_attempt in range(2, attempt + 1):
|
|
305
|
+
# add pause between attempts to verify STOP condition
|
|
306
|
+
sleep(1)
|
|
307
|
+
|
|
257
308
|
self._reporter.set_module_status(module_id, TestStatus.RUN)
|
|
258
309
|
self._reporter.set_case_status(module_id, case_id, TestStatus.RUN)
|
|
259
310
|
self._reporter.set_case_attempt(module_id, case_id, current_attempt)
|
|
@@ -330,9 +381,10 @@ class HardpyPlugin:
|
|
|
330
381
|
self._results[module_id][case_id] = None
|
|
331
382
|
|
|
332
383
|
def _collect_module_result(self, module_id: str) -> None:
|
|
333
|
-
if
|
|
334
|
-
|
|
335
|
-
|
|
384
|
+
if (
|
|
385
|
+
TestStatus.FAILED in self._results[module_id].values()
|
|
386
|
+
or TestStatus.ERROR in self._results[module_id].values()
|
|
387
|
+
):
|
|
336
388
|
status = TestStatus.FAILED
|
|
337
389
|
elif TestStatus.SKIPPED in self._results[module_id].values():
|
|
338
390
|
status = TestStatus.SKIPPED
|
|
@@ -352,7 +404,7 @@ class HardpyPlugin:
|
|
|
352
404
|
self._stop_tests()
|
|
353
405
|
return TestStatus.STOPPED
|
|
354
406
|
case _:
|
|
355
|
-
return TestStatus.
|
|
407
|
+
return TestStatus.FAILED
|
|
356
408
|
|
|
357
409
|
def _stop_tests(self) -> None:
|
|
358
410
|
"""Update module and case statuses from READY or RUN to STOPPED."""
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import socket
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from os import environ
|
|
8
|
-
from
|
|
8
|
+
from select import select
|
|
9
9
|
from typing import Any
|
|
10
10
|
from uuid import uuid4
|
|
11
11
|
|
|
@@ -25,6 +25,7 @@ from hardpy.pytest_hardpy.utils import (
|
|
|
25
25
|
DuplicateSerialNumberError,
|
|
26
26
|
DuplicateTestStandLocationError,
|
|
27
27
|
DuplicateTestStandNameError,
|
|
28
|
+
ImageComponent,
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
|
|
@@ -306,52 +307,76 @@ def run_dialog_box(dialog_box_data: DialogBox) -> Any: # noqa: ANN401
|
|
|
306
307
|
current_test.case_id,
|
|
307
308
|
DF.DIALOG_BOX,
|
|
308
309
|
)
|
|
309
|
-
|
|
310
|
-
reporter.set_doc_value(key, {}, statestore_only=True)
|
|
311
|
-
reporter.update_db_by_doc()
|
|
312
|
-
debounce_time = 0.2
|
|
313
|
-
sleep(debounce_time)
|
|
310
|
+
_cleanup_widget(reporter, key)
|
|
314
311
|
|
|
315
312
|
reporter.set_doc_value(key, dialog_box_data.to_dict(), statestore_only=True)
|
|
316
313
|
reporter.update_db_by_doc()
|
|
317
314
|
|
|
315
|
+
# get socket data
|
|
318
316
|
input_dbx_data = _get_socket_raw_data()
|
|
319
317
|
|
|
320
|
-
|
|
321
|
-
reporter.set_doc_value(key, {}, statestore_only=True)
|
|
322
|
-
reporter.update_db_by_doc()
|
|
318
|
+
_cleanup_widget(reporter, key)
|
|
323
319
|
return dialog_box_data.widget.convert_data(input_dbx_data)
|
|
324
320
|
|
|
325
321
|
|
|
326
|
-
def set_operator_message(
|
|
322
|
+
def set_operator_message(
|
|
323
|
+
msg: str,
|
|
324
|
+
title: str | None = None,
|
|
325
|
+
block: bool = True,
|
|
326
|
+
image: ImageComponent | None = None,
|
|
327
|
+
font_size: int = 14,
|
|
328
|
+
) -> None:
|
|
327
329
|
"""Set operator message.
|
|
328
330
|
|
|
329
331
|
The function should be used to handle events outside of testing.
|
|
330
332
|
For messages to the operator during testing, there is the function `run_dialog_box`.
|
|
331
333
|
|
|
332
334
|
Args:
|
|
333
|
-
msg (str):
|
|
334
|
-
title (str | None):
|
|
335
|
+
msg (str): message
|
|
336
|
+
title (str | None): title
|
|
337
|
+
image (ImageComponent | None): operator message info
|
|
338
|
+
block (bool): if True, the function will block until the message is closed
|
|
339
|
+
font_size (int): font size
|
|
335
340
|
"""
|
|
336
341
|
reporter = RunnerReporter()
|
|
337
342
|
key = reporter.generate_key(DF.OPERATOR_MSG)
|
|
343
|
+
_cleanup_widget(reporter, key)
|
|
338
344
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
sleep(debounce_time)
|
|
345
|
+
if font_size < 1:
|
|
346
|
+
msg = "The 'font_size' argument cannot be less than 1"
|
|
347
|
+
raise ValueError(msg)
|
|
343
348
|
|
|
344
|
-
msg_data = {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
+
msg_data = {
|
|
350
|
+
DF.MSG: msg,
|
|
351
|
+
DF.TITLE: title,
|
|
352
|
+
DF.VISIBLE: True,
|
|
353
|
+
DF.IMAGE: image.to_dict() if image else None,
|
|
354
|
+
DF.ID: str(uuid4()),
|
|
355
|
+
DF.FONT_SIZE: int(font_size),
|
|
356
|
+
}
|
|
349
357
|
reporter.set_doc_value(key, msg_data, statestore_only=True)
|
|
350
358
|
reporter.update_db_by_doc()
|
|
351
359
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
360
|
+
if block:
|
|
361
|
+
# get socket data
|
|
362
|
+
is_msg_visible = _get_socket_raw_data()
|
|
363
|
+
|
|
364
|
+
msg_data[DF.VISIBLE] = is_msg_visible
|
|
365
|
+
reporter.set_doc_value(key, msg_data, statestore_only=True)
|
|
366
|
+
reporter.update_db_by_doc()
|
|
367
|
+
|
|
368
|
+
_cleanup_widget(reporter, key)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def clear_operator_message() -> None:
|
|
372
|
+
"""Clear operator message.
|
|
373
|
+
|
|
374
|
+
Clears the current message to the operator if it exists, otherwise does nothing.
|
|
375
|
+
"""
|
|
376
|
+
reporter = RunnerReporter()
|
|
377
|
+
key = reporter.generate_key(DF.OPERATOR_MSG)
|
|
378
|
+
|
|
379
|
+
_cleanup_widget(reporter, key)
|
|
355
380
|
|
|
356
381
|
|
|
357
382
|
def get_current_attempt() -> int:
|
|
@@ -391,25 +416,37 @@ def _get_current_test() -> CurrentTestInfo:
|
|
|
391
416
|
|
|
392
417
|
def _get_socket_raw_data() -> str:
|
|
393
418
|
# create socket connection
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
419
|
+
with socket.socket() as server:
|
|
420
|
+
server.setblocking(False)
|
|
421
|
+
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
422
|
+
|
|
423
|
+
con_data = ConnectionData()
|
|
424
|
+
try:
|
|
425
|
+
server.bind((con_data.socket_host, con_data.socket_port))
|
|
426
|
+
except OSError as exc:
|
|
427
|
+
msg = "Socket creating error"
|
|
428
|
+
server.close()
|
|
429
|
+
raise RuntimeError(msg) from exc
|
|
430
|
+
server.listen(1)
|
|
431
|
+
|
|
432
|
+
client = None
|
|
433
|
+
while True:
|
|
434
|
+
ready_to_read, _, _ = select([server], [], [], 0.5)
|
|
435
|
+
if ready_to_read:
|
|
436
|
+
client, _ = server.accept()
|
|
437
|
+
break
|
|
438
|
+
|
|
439
|
+
# receive data
|
|
440
|
+
max_input_data_len = 1024
|
|
441
|
+
socket_data = client.recv(max_input_data_len).decode("utf-8")
|
|
442
|
+
|
|
443
|
+
# close connection
|
|
444
|
+
client.close()
|
|
402
445
|
server.close()
|
|
403
|
-
raise RuntimeError(msg) from exc
|
|
404
|
-
server.listen(1)
|
|
405
|
-
client, _ = server.accept()
|
|
406
446
|
|
|
407
|
-
|
|
408
|
-
max_input_data_len = 1024
|
|
409
|
-
socket_data = client.recv(max_input_data_len).decode("utf-8")
|
|
447
|
+
return socket_data
|
|
410
448
|
|
|
411
|
-
# close connection
|
|
412
|
-
client.close()
|
|
413
|
-
server.close()
|
|
414
449
|
|
|
415
|
-
|
|
450
|
+
def _cleanup_widget(reporter: RunnerReporter, key: str) -> None:
|
|
451
|
+
reporter.set_doc_value(key, {}, statestore_only=True)
|
|
452
|
+
reporter.update_db_by_doc()
|
|
@@ -47,6 +47,11 @@ class PyTestWrapper:
|
|
|
47
47
|
str(self.config.socket.port),
|
|
48
48
|
"--hardpy-sh",
|
|
49
49
|
self.config.socket.host,
|
|
50
|
+
"--sc-address",
|
|
51
|
+
self.config.stand_cloud.address,
|
|
52
|
+
"--sc-connection-only"
|
|
53
|
+
if self.config.stand_cloud.connection_only
|
|
54
|
+
else "",
|
|
50
55
|
"--hardpy-pt",
|
|
51
56
|
],
|
|
52
57
|
cwd=ConfigManager().get_tests_path(),
|
|
@@ -63,6 +68,11 @@ class PyTestWrapper:
|
|
|
63
68
|
str(self.config.socket.port),
|
|
64
69
|
"--hardpy-sh",
|
|
65
70
|
self.config.socket.host,
|
|
71
|
+
"--sc-address",
|
|
72
|
+
self.config.stand_cloud.address,
|
|
73
|
+
"--sc-connection-only"
|
|
74
|
+
if self.config.stand_cloud.connection_only
|
|
75
|
+
else "",
|
|
66
76
|
"--hardpy-pt",
|
|
67
77
|
],
|
|
68
78
|
cwd=ConfigManager().get_tests_path(),
|
|
@@ -134,10 +144,10 @@ class PyTestWrapper:
|
|
|
134
144
|
bool: True if dialog box was confirmed/closed, else False
|
|
135
145
|
"""
|
|
136
146
|
try:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
with socket() as client:
|
|
148
|
+
client.connect((self.config.socket.host, self.config.socket.port))
|
|
149
|
+
client.sendall(data.encode("utf-8"))
|
|
150
|
+
client.close()
|
|
141
151
|
except Exception: # noqa: BLE001
|
|
142
152
|
return False
|
|
143
153
|
return True
|
|
@@ -37,6 +37,7 @@ class HookReporter(BaseReporter):
|
|
|
37
37
|
self.set_doc_value(DF.PROGRESS, 0, statestore_only=True)
|
|
38
38
|
self.set_doc_value(DF.ARTIFACT, {}, runstore_only=True)
|
|
39
39
|
self.set_doc_value(DF.OPERATOR_MSG, {}, statestore_only=True)
|
|
40
|
+
self.set_doc_value(DF.ALERT, "", statestore_only=True)
|
|
40
41
|
|
|
41
42
|
test_stand_tz = self.generate_key(DF.TEST_STAND, DF.TIMEZONE)
|
|
42
43
|
self.set_doc_value(test_stand_tz, str(get_localzone().key))
|
|
@@ -51,6 +52,7 @@ class HookReporter(BaseReporter):
|
|
|
51
52
|
self.set_doc_value(DF.START_TIME, start_time)
|
|
52
53
|
self.set_doc_value(DF.STATUS, TestStatus.RUN)
|
|
53
54
|
self.set_doc_value(DF.PROGRESS, 0, statestore_only=True)
|
|
55
|
+
self.set_doc_value(DF.ALERT, "", statestore_only=True)
|
|
54
56
|
|
|
55
57
|
def finish(self, status: TestStatus) -> None:
|
|
56
58
|
"""Finish test.
|
|
@@ -185,6 +187,14 @@ class HookReporter(BaseReporter):
|
|
|
185
187
|
)
|
|
186
188
|
self.set_doc_value(key, attempt, statestore_only=True)
|
|
187
189
|
|
|
190
|
+
def set_alert(self, alert: str) -> None:
|
|
191
|
+
"""Set alert message.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
alert (str): alert message
|
|
195
|
+
"""
|
|
196
|
+
self.set_doc_value(DF.ALERT, alert, statestore_only=True)
|
|
197
|
+
|
|
188
198
|
def update_node_order(self, nodes: dict) -> None:
|
|
189
199
|
"""Update node order.
|
|
190
200
|
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
# GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
3
|
|
|
4
4
|
from hardpy.pytest_hardpy.result.report_loader.couchdb_loader import CouchdbLoader
|
|
5
|
+
from hardpy.pytest_hardpy.result.report_loader.stand_cloud_loader import (
|
|
6
|
+
StandCloudLoader,
|
|
7
|
+
)
|
|
5
8
|
from hardpy.pytest_hardpy.result.report_reader.couchdb_reader import CouchdbReader
|
|
6
9
|
|
|
7
10
|
__all__ = [
|
|
8
11
|
"CouchdbLoader",
|
|
9
12
|
"CouchdbReader",
|
|
13
|
+
"StandCloudLoader",
|
|
10
14
|
]
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
from hardpy.pytest_hardpy.result.report_loader.couchdb_loader import (
|
|
5
5
|
CouchdbLoader,
|
|
6
6
|
)
|
|
7
|
+
from hardpy.pytest_hardpy.result.report_loader.stand_cloud_loader import (
|
|
8
|
+
StandCloudLoader,
|
|
9
|
+
)
|
|
7
10
|
|
|
8
11
|
__all__ = [
|
|
9
12
|
"CouchdbLoader",
|
|
13
|
+
"StandCloudLoader",
|
|
10
14
|
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Copyright (c) 2024 Everypin
|
|
2
|
+
# GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from oauthlib.oauth2.rfc6749.errors import InvalidGrantError, TokenExpiredError
|
|
8
|
+
from requests.exceptions import HTTPError
|
|
9
|
+
from requests_oauth2client.tokens import ExpiredAccessToken
|
|
10
|
+
|
|
11
|
+
from hardpy.common.stand_cloud.connector import StandCloudConnector, StandCloudError
|
|
12
|
+
from hardpy.pytest_hardpy.utils import ConnectionData
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from requests import Response
|
|
16
|
+
|
|
17
|
+
from hardpy.pytest_hardpy.db.schema import ResultRunStore
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StandCloudLoader:
|
|
21
|
+
"""StandCloud report generator."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, address: str | None = None) -> None:
|
|
24
|
+
"""Create StandCLoud loader.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
address (str | None, optional): StandCloud address.
|
|
28
|
+
Defaults to None (the value is taken from the.toml).
|
|
29
|
+
Can be used outside of HardPy applications.
|
|
30
|
+
"""
|
|
31
|
+
self._verify_ssl = not __debug__
|
|
32
|
+
connection_data = ConnectionData()
|
|
33
|
+
sc_addr = address if address else connection_data.sc_address
|
|
34
|
+
self._sc_connector = StandCloudConnector(sc_addr)
|
|
35
|
+
|
|
36
|
+
def load(self, report: ResultRunStore) -> Response:
|
|
37
|
+
"""Load report to the StandCloud.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
report (ResultRunStore): report
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Response: StandCloud load response, must be 201
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
StandCloudError: if report not uploaded to StandCloud
|
|
47
|
+
"""
|
|
48
|
+
api = self._sc_connector.get_api("api/test_run")
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
resp = api.post(verify=self._verify_ssl, json=report.model_dump())
|
|
52
|
+
except ExpiredAccessToken as exc:
|
|
53
|
+
raise StandCloudError(str(exc)) # type: ignore
|
|
54
|
+
except TokenExpiredError as exc:
|
|
55
|
+
raise StandCloudError(exc.description)
|
|
56
|
+
except InvalidGrantError as exc:
|
|
57
|
+
raise StandCloudError(exc.description)
|
|
58
|
+
except HTTPError as exc:
|
|
59
|
+
return exc.response # type: ignore
|
|
60
|
+
|
|
61
|
+
return resp
|
|
62
|
+
|
|
63
|
+
def healthcheck(self) -> Response:
|
|
64
|
+
"""Healthcheck of StandCloud API.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Response: StandCloud healthcheck response, must be 200
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
StandCloudError: if StandCloud is unavailable
|
|
71
|
+
"""
|
|
72
|
+
return self._sc_connector.healthcheck()
|
|
@@ -9,6 +9,7 @@ from copy import deepcopy
|
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
from enum import Enum
|
|
11
11
|
from typing import Any, Final
|
|
12
|
+
from uuid import uuid4
|
|
12
13
|
|
|
13
14
|
from hardpy.pytest_hardpy.utils.exception import ImageError, WidgetInfoError
|
|
14
15
|
|
|
@@ -315,6 +316,7 @@ class DialogBox:
|
|
|
315
316
|
title_bar (str | None): title bar
|
|
316
317
|
widget (IWidget | None): widget info
|
|
317
318
|
image (ImageComponent | None): image
|
|
319
|
+
font_size (int): font size
|
|
318
320
|
"""
|
|
319
321
|
|
|
320
322
|
def __init__(
|
|
@@ -323,11 +325,19 @@ class DialogBox:
|
|
|
323
325
|
title_bar: str | None = None,
|
|
324
326
|
widget: IWidget | None = None,
|
|
325
327
|
image: ImageComponent | None = None,
|
|
328
|
+
font_size: int = 14,
|
|
326
329
|
) -> None:
|
|
327
330
|
self.widget: IWidget = BaseWidget() if widget is None else widget
|
|
328
331
|
self.image: ImageComponent | None = image
|
|
329
332
|
self.dialog_text: str = dialog_text
|
|
330
333
|
self.title_bar: str | None = title_bar
|
|
334
|
+
self.visible: bool = True
|
|
335
|
+
self.id = str(uuid4())
|
|
336
|
+
self.font_size = font_size
|
|
337
|
+
|
|
338
|
+
if font_size < 1:
|
|
339
|
+
msg = "The 'font_size' argument cannot be less than 1"
|
|
340
|
+
raise ValueError(msg)
|
|
331
341
|
|
|
332
342
|
def to_dict(self) -> dict:
|
|
333
343
|
"""Convert DialogBox to dictionary.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: hardpy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: HardPy library for device testing
|
|
5
5
|
Project-URL: Homepage, https://github.com/everypinio/hardpy/
|
|
6
6
|
Project-URL: Documentation, https://everypinio.github.io/hardpy/
|
|
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
22
|
Classifier: Topic :: Software Development :: Libraries
|
|
22
23
|
Classifier: Topic :: Software Development :: Testing
|
|
23
24
|
Classifier: Topic :: Software Development :: Testing :: Acceptance
|
|
@@ -25,11 +26,17 @@ Classifier: Topic :: Utilities
|
|
|
25
26
|
Requires-Python: >=3.10
|
|
26
27
|
Requires-Dist: fastapi>=0.100.1
|
|
27
28
|
Requires-Dist: glom>=23.3.0
|
|
29
|
+
Requires-Dist: jinja2<4,>=3
|
|
30
|
+
Requires-Dist: keyring<26,>=25.0.0
|
|
28
31
|
Requires-Dist: natsort>=8.4.0
|
|
32
|
+
Requires-Dist: oauthlib<4,>=3.1.0
|
|
29
33
|
Requires-Dist: py-machineid~=0.6.0
|
|
30
34
|
Requires-Dist: pycouchdb<2,>=1.14.2
|
|
31
35
|
Requires-Dist: pydantic<3,>=2.4.0
|
|
32
36
|
Requires-Dist: pytest<9,>=7
|
|
37
|
+
Requires-Dist: requests-oauth2client<2,>=1.5.0
|
|
38
|
+
Requires-Dist: requests-oauthlib<3,>=2.0.0
|
|
39
|
+
Requires-Dist: requests<3,>=2.30.0
|
|
33
40
|
Requires-Dist: tomli-w<2,>=1.1.0
|
|
34
41
|
Requires-Dist: tomli<3,>=2.0.1
|
|
35
42
|
Requires-Dist: typer<1,>=0.12
|