hardpy 0.6.1__py3-none-any.whl → 0.8.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 +49 -49
- hardpy/cli/cli.py +8 -9
- hardpy/cli/template.py +6 -6
- hardpy/common/config.py +19 -18
- hardpy/hardpy_panel/api.py +9 -9
- hardpy/hardpy_panel/frontend/dist/asset-manifest.json +3 -3
- hardpy/hardpy_panel/frontend/dist/index.html +1 -1
- hardpy/hardpy_panel/frontend/dist/logo192.png +0 -0
- hardpy/hardpy_panel/frontend/dist/static/css/main.e8a862f1.css.map +1 -1
- hardpy/hardpy_panel/frontend/dist/static/js/main.6f09d61a.js +3 -0
- hardpy/hardpy_panel/frontend/dist/static/js/{main.7c954faf.js.map → main.6f09d61a.js.map} +1 -1
- hardpy/pytest_hardpy/db/__init__.py +4 -5
- hardpy/pytest_hardpy/db/base_connector.py +6 -5
- hardpy/pytest_hardpy/db/base_server.py +1 -1
- hardpy/pytest_hardpy/db/base_store.py +23 -9
- hardpy/pytest_hardpy/db/const.py +3 -1
- hardpy/pytest_hardpy/db/runstore.py +13 -15
- hardpy/pytest_hardpy/db/schema/__init__.py +9 -0
- hardpy/pytest_hardpy/db/{schema.py → schema/v1.py} +120 -79
- hardpy/pytest_hardpy/db/statestore.py +7 -20
- hardpy/pytest_hardpy/plugin.py +128 -85
- hardpy/pytest_hardpy/pytest_call.py +80 -32
- hardpy/pytest_hardpy/pytest_wrapper.py +8 -8
- hardpy/pytest_hardpy/reporter/__init__.py +2 -2
- hardpy/pytest_hardpy/reporter/base.py +32 -7
- hardpy/pytest_hardpy/reporter/hook_reporter.py +66 -37
- hardpy/pytest_hardpy/reporter/runner_reporter.py +6 -8
- hardpy/pytest_hardpy/result/__init__.py +2 -2
- hardpy/pytest_hardpy/result/couchdb_config.py +20 -16
- hardpy/pytest_hardpy/result/report_loader/couchdb_loader.py +2 -2
- hardpy/pytest_hardpy/result/report_reader/couchdb_reader.py +36 -20
- hardpy/pytest_hardpy/utils/__init__.py +34 -29
- hardpy/pytest_hardpy/utils/connection_data.py +6 -8
- hardpy/pytest_hardpy/utils/const.py +1 -1
- hardpy/pytest_hardpy/utils/dialog_box.py +105 -66
- hardpy/pytest_hardpy/utils/exception.py +14 -8
- hardpy/pytest_hardpy/utils/machineid.py +15 -0
- hardpy/pytest_hardpy/utils/node_info.py +45 -16
- hardpy/pytest_hardpy/utils/progress_calculator.py +4 -3
- hardpy/pytest_hardpy/utils/singleton.py +23 -16
- {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/METADATA +26 -33
- {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/RECORD +46 -43
- hardpy/hardpy_panel/frontend/dist/static/js/main.7c954faf.js +0 -3
- /hardpy/hardpy_panel/frontend/dist/static/js/{main.7c954faf.js.LICENSE.txt → main.6f09d61a.js.LICENSE.txt} +0 -0
- {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/WHEEL +0 -0
- {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/entry_points.txt +0 -0
- {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/licenses/LICENSE +0 -0
hardpy/pytest_hardpy/plugin.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Copyright (c) 2024 Everypin
|
|
2
2
|
# GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
|
+
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import signal
|
|
5
6
|
from logging import getLogger
|
|
@@ -8,37 +9,38 @@ from platform import system
|
|
|
8
9
|
from re import compile as re_compile
|
|
9
10
|
from typing import Any, Callable
|
|
10
11
|
|
|
12
|
+
from _pytest._code.code import (
|
|
13
|
+
ExceptionInfo,
|
|
14
|
+
ExceptionRepr,
|
|
15
|
+
ReprExceptionInfo,
|
|
16
|
+
ReprFileLocation,
|
|
17
|
+
TerminalRepr,
|
|
18
|
+
)
|
|
11
19
|
from natsort import natsorted
|
|
12
20
|
from pytest import (
|
|
13
|
-
|
|
14
|
-
exit,
|
|
15
|
-
TestReport,
|
|
16
|
-
Item,
|
|
17
|
-
Session,
|
|
21
|
+
CallInfo,
|
|
18
22
|
Config,
|
|
23
|
+
ExitCode,
|
|
24
|
+
Item,
|
|
19
25
|
Parser,
|
|
26
|
+
Session,
|
|
27
|
+
TestReport,
|
|
28
|
+
exit, # noqa: A004
|
|
20
29
|
fixture,
|
|
21
|
-
|
|
22
|
-
)
|
|
23
|
-
from _pytest._code.code import (
|
|
24
|
-
ExceptionRepr,
|
|
25
|
-
ReprFileLocation,
|
|
26
|
-
ExceptionInfo,
|
|
27
|
-
ReprExceptionInfo,
|
|
28
|
-
TerminalRepr,
|
|
30
|
+
skip,
|
|
29
31
|
)
|
|
30
32
|
|
|
31
33
|
from hardpy.pytest_hardpy.reporter import HookReporter
|
|
32
34
|
from hardpy.pytest_hardpy.utils import (
|
|
33
|
-
|
|
35
|
+
ConnectionData,
|
|
34
36
|
NodeInfo,
|
|
35
37
|
ProgressCalculator,
|
|
36
|
-
|
|
38
|
+
TestStatus,
|
|
37
39
|
)
|
|
38
40
|
from hardpy.pytest_hardpy.utils.node_info import TestDependencyInfo
|
|
39
41
|
|
|
40
42
|
|
|
41
|
-
def pytest_addoption(parser: Parser):
|
|
43
|
+
def pytest_addoption(parser: Parser) -> None:
|
|
42
44
|
"""Register argparse-style options."""
|
|
43
45
|
con_data = ConnectionData()
|
|
44
46
|
parser.addoption(
|
|
@@ -61,7 +63,7 @@ def pytest_addoption(parser: Parser):
|
|
|
61
63
|
)
|
|
62
64
|
parser.addoption(
|
|
63
65
|
"--hardpy-clear-database",
|
|
64
|
-
action="
|
|
66
|
+
action="store_true",
|
|
65
67
|
default=False,
|
|
66
68
|
help="clear hardpy local database",
|
|
67
69
|
)
|
|
@@ -74,7 +76,12 @@ def pytest_addoption(parser: Parser):
|
|
|
74
76
|
|
|
75
77
|
|
|
76
78
|
# Bootstrapping hooks
|
|
77
|
-
def pytest_load_initial_conftests(
|
|
79
|
+
def pytest_load_initial_conftests(
|
|
80
|
+
early_config: Config,
|
|
81
|
+
parser: Parser, # noqa: ARG001
|
|
82
|
+
args: Any, # noqa: ANN401
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Load initial conftests."""
|
|
78
85
|
if "--hardpy-pt" in args:
|
|
79
86
|
plugin = HardpyPlugin()
|
|
80
87
|
early_config.pluginmanager.register(plugin)
|
|
@@ -86,7 +93,7 @@ class HardpyPlugin:
|
|
|
86
93
|
Extends hook functions from pytest API.
|
|
87
94
|
"""
|
|
88
95
|
|
|
89
|
-
def __init__(self):
|
|
96
|
+
def __init__(self) -> None:
|
|
90
97
|
self._progress = ProgressCalculator()
|
|
91
98
|
self._results = {}
|
|
92
99
|
self._post_run_functions: list[Callable] = []
|
|
@@ -95,21 +102,20 @@ class HardpyPlugin:
|
|
|
95
102
|
if system() == "Linux":
|
|
96
103
|
signal.signal(signal.SIGTERM, self._stop_handler)
|
|
97
104
|
elif system() == "Windows":
|
|
98
|
-
signal.signal(signal.SIGBREAK, self._stop_handler)
|
|
105
|
+
signal.signal(signal.SIGBREAK, self._stop_handler) # type: ignore
|
|
99
106
|
self._log = getLogger(__name__)
|
|
100
107
|
|
|
101
108
|
# Initialization hooks
|
|
102
109
|
|
|
103
|
-
def pytest_configure(self, config: Config):
|
|
110
|
+
def pytest_configure(self, config: Config) -> None:
|
|
104
111
|
"""Configure pytest."""
|
|
105
112
|
con_data = ConnectionData()
|
|
106
113
|
|
|
107
114
|
database_url = config.getoption("--hardpy-db-url")
|
|
108
115
|
if database_url:
|
|
109
|
-
con_data.database_url = str(database_url)
|
|
116
|
+
con_data.database_url = str(database_url) # type: ignore
|
|
110
117
|
|
|
111
118
|
is_clear_database = config.getoption("--hardpy-clear-database")
|
|
112
|
-
is_clear_statestore = is_clear_database == str(True)
|
|
113
119
|
|
|
114
120
|
socket_port = config.getoption("--hardpy-sp")
|
|
115
121
|
if socket_port:
|
|
@@ -117,19 +123,20 @@ class HardpyPlugin:
|
|
|
117
123
|
|
|
118
124
|
socket_host = config.getoption("--hardpy-sh")
|
|
119
125
|
if socket_host:
|
|
120
|
-
con_data.socket_host = str(socket_host)
|
|
126
|
+
con_data.socket_host = str(socket_host) # type: ignore
|
|
121
127
|
|
|
122
128
|
config.addinivalue_line("markers", "case_name")
|
|
123
129
|
config.addinivalue_line("markers", "module_name")
|
|
124
130
|
config.addinivalue_line("markers", "dependency")
|
|
131
|
+
config.addinivalue_line("markers", "attempt")
|
|
125
132
|
|
|
126
133
|
# must be init after config data is set
|
|
127
134
|
try:
|
|
128
|
-
self._reporter = HookReporter(
|
|
135
|
+
self._reporter = HookReporter(bool(is_clear_database))
|
|
129
136
|
except RuntimeError as exc:
|
|
130
137
|
exit(str(exc), 1)
|
|
131
138
|
|
|
132
|
-
def pytest_sessionfinish(self, session: Session, exitstatus: int):
|
|
139
|
+
def pytest_sessionfinish(self, session: Session, exitstatus: int) -> None:
|
|
133
140
|
"""Call at the end of test session."""
|
|
134
141
|
if "--collect-only" in session.config.invocation_params.args:
|
|
135
142
|
return
|
|
@@ -146,8 +153,11 @@ class HardpyPlugin:
|
|
|
146
153
|
# Collection hooks
|
|
147
154
|
|
|
148
155
|
def pytest_collection_modifyitems(
|
|
149
|
-
self,
|
|
150
|
-
|
|
156
|
+
self,
|
|
157
|
+
session: Session,
|
|
158
|
+
config: Config,
|
|
159
|
+
items: list[Item], # noqa: ARG002
|
|
160
|
+
) -> None:
|
|
151
161
|
"""Call after collection phase."""
|
|
152
162
|
self._reporter.init_doc(str(PurePath(config.rootpath).name))
|
|
153
163
|
|
|
@@ -163,8 +173,8 @@ class HardpyPlugin:
|
|
|
163
173
|
continue
|
|
164
174
|
try:
|
|
165
175
|
node_info = NodeInfo(item)
|
|
166
|
-
except ValueError:
|
|
167
|
-
error_msg = f"Error creating NodeInfo for item: {item}
|
|
176
|
+
except ValueError as exc:
|
|
177
|
+
error_msg = f"Error creating NodeInfo for item: {item}. {exc}"
|
|
168
178
|
exit(error_msg, 1)
|
|
169
179
|
|
|
170
180
|
self._init_case_result(node_info.module_id, node_info.case_id)
|
|
@@ -184,7 +194,7 @@ class HardpyPlugin:
|
|
|
184
194
|
|
|
185
195
|
# Test running (runtest) hooks
|
|
186
196
|
|
|
187
|
-
def pytest_runtestloop(self, session: Session):
|
|
197
|
+
def pytest_runtestloop(self, session: Session) -> bool | None:
|
|
188
198
|
"""Call at the start of test run."""
|
|
189
199
|
self._progress.set_test_amount(session.testscollected)
|
|
190
200
|
if session.config.option.collectonly:
|
|
@@ -194,8 +204,9 @@ class HardpyPlugin:
|
|
|
194
204
|
# testrun entrypoint
|
|
195
205
|
self._reporter.start()
|
|
196
206
|
self._reporter.update_db_by_doc()
|
|
207
|
+
return None
|
|
197
208
|
|
|
198
|
-
def pytest_runtest_setup(self, item: Item):
|
|
209
|
+
def pytest_runtest_setup(self, item: Item) -> None:
|
|
199
210
|
"""Call before each test setup phase."""
|
|
200
211
|
if item.parent is None:
|
|
201
212
|
self._log.error(f"Test module name for test {item.name} not found.")
|
|
@@ -203,28 +214,68 @@ class HardpyPlugin:
|
|
|
203
214
|
|
|
204
215
|
node_info = NodeInfo(item)
|
|
205
216
|
|
|
206
|
-
|
|
217
|
+
status = TestStatus.RUN
|
|
218
|
+
is_skip_test = self._is_skip_test(node_info)
|
|
219
|
+
if not is_skip_test:
|
|
220
|
+
self._reporter.set_module_start_time(node_info.module_id)
|
|
221
|
+
self._reporter.set_case_start_time(node_info.module_id, node_info.case_id)
|
|
222
|
+
else:
|
|
223
|
+
status = TestStatus.SKIPPED
|
|
224
|
+
self._results[node_info.module_id][node_info.case_id] = status
|
|
225
|
+
progress = self._progress.calculate(item.nodeid)
|
|
226
|
+
self._reporter.set_progress(progress)
|
|
207
227
|
|
|
208
|
-
self._reporter.set_module_status(node_info.module_id,
|
|
209
|
-
self._reporter.
|
|
210
|
-
self._reporter.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
228
|
+
self._reporter.set_module_status(node_info.module_id, status)
|
|
229
|
+
self._reporter.set_case_status(node_info.module_id, node_info.case_id, status)
|
|
230
|
+
self._reporter.update_db_by_doc()
|
|
231
|
+
|
|
232
|
+
if is_skip_test:
|
|
233
|
+
skip(f"Test {item.nodeid} is skipped")
|
|
234
|
+
|
|
235
|
+
def pytest_runtest_call(self, item: Item) -> None:
|
|
236
|
+
"""Call the test item."""
|
|
237
|
+
node_info = NodeInfo(item)
|
|
238
|
+
self._reporter.set_case_attempt(
|
|
216
239
|
node_info.module_id,
|
|
217
240
|
node_info.case_id,
|
|
241
|
+
1,
|
|
218
242
|
)
|
|
219
243
|
self._reporter.update_db_by_doc()
|
|
220
244
|
|
|
245
|
+
def pytest_runtest_makereport(self, item: Item, call: CallInfo) -> None:
|
|
246
|
+
"""Call after call of each test item."""
|
|
247
|
+
if call.when != "call" or not call.excinfo:
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
node_info = NodeInfo(item)
|
|
251
|
+
attempt = node_info.attempt
|
|
252
|
+
module_id = node_info.module_id
|
|
253
|
+
case_id = node_info.case_id
|
|
254
|
+
|
|
255
|
+
# first attempt was in pytest_runtest_call
|
|
256
|
+
for current_attempt in range(2, attempt + 1):
|
|
257
|
+
self._reporter.set_module_status(module_id, TestStatus.RUN)
|
|
258
|
+
self._reporter.set_case_status(module_id, case_id, TestStatus.RUN)
|
|
259
|
+
self._reporter.set_case_attempt(module_id, case_id, current_attempt)
|
|
260
|
+
self._reporter.update_db_by_doc()
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
item.runtest()
|
|
264
|
+
call.excinfo = None
|
|
265
|
+
self._reporter.set_case_status(module_id, case_id, TestStatus.PASSED)
|
|
266
|
+
break
|
|
267
|
+
except AssertionError:
|
|
268
|
+
self._reporter.set_case_status(module_id, case_id, TestStatus.FAILED)
|
|
269
|
+
if current_attempt == attempt:
|
|
270
|
+
return
|
|
271
|
+
|
|
221
272
|
# Reporting hooks
|
|
222
273
|
|
|
223
|
-
def pytest_runtest_logreport(self, report: TestReport):
|
|
274
|
+
def pytest_runtest_logreport(self, report: TestReport) -> bool | None:
|
|
224
275
|
"""Call after call of each test item."""
|
|
225
276
|
if report.when != "call" and report.failed is False:
|
|
226
|
-
# ignore setup and teardown phase
|
|
227
|
-
#
|
|
277
|
+
# ignore setup and teardown phase or continue processing setup
|
|
278
|
+
# and teardown failure (fixture exception handler)
|
|
228
279
|
return True
|
|
229
280
|
|
|
230
281
|
module_id = Path(report.fspath).stem
|
|
@@ -243,11 +294,12 @@ class HardpyPlugin:
|
|
|
243
294
|
assertion_msg = self._decode_assertion_msg(report.longrepr)
|
|
244
295
|
self._reporter.set_assertion_msg(module_id, case_id, assertion_msg)
|
|
245
296
|
self._reporter.set_progress(self._progress.calculate(report.nodeid))
|
|
246
|
-
self._results[module_id][case_id] = report.outcome
|
|
297
|
+
self._results[module_id][case_id] = report.outcome
|
|
247
298
|
|
|
248
299
|
if None not in self._results[module_id].values():
|
|
249
300
|
self._collect_module_result(module_id)
|
|
250
301
|
self._reporter.update_db_by_doc()
|
|
302
|
+
return None
|
|
251
303
|
|
|
252
304
|
# Fixture
|
|
253
305
|
|
|
@@ -265,10 +317,10 @@ class HardpyPlugin:
|
|
|
265
317
|
|
|
266
318
|
# Not hooks
|
|
267
319
|
|
|
268
|
-
def _stop_handler(self, signum: int, frame: Any):
|
|
320
|
+
def _stop_handler(self, signum: int, frame: Any) -> None: # noqa: ANN401, ARG002
|
|
269
321
|
exit("Tests stopped by user")
|
|
270
322
|
|
|
271
|
-
def _init_case_result(self, module_id: str, case_id: str):
|
|
323
|
+
def _init_case_result(self, module_id: str, case_id: str) -> None:
|
|
272
324
|
if self._results.get(module_id) is None:
|
|
273
325
|
self._results[module_id] = {
|
|
274
326
|
"module_status": TestStatus.READY,
|
|
@@ -277,7 +329,7 @@ class HardpyPlugin:
|
|
|
277
329
|
else:
|
|
278
330
|
self._results[module_id][case_id] = None
|
|
279
331
|
|
|
280
|
-
def _collect_module_result(self, module_id: str):
|
|
332
|
+
def _collect_module_result(self, module_id: str) -> None:
|
|
281
333
|
if TestStatus.ERROR in self._results[module_id].values():
|
|
282
334
|
status = TestStatus.ERROR
|
|
283
335
|
elif TestStatus.FAILED in self._results[module_id].values():
|
|
@@ -302,7 +354,7 @@ class HardpyPlugin:
|
|
|
302
354
|
case _:
|
|
303
355
|
return TestStatus.ERROR
|
|
304
356
|
|
|
305
|
-
def _stop_tests(self)
|
|
357
|
+
def _stop_tests(self) -> None:
|
|
306
358
|
"""Update module and case statuses from READY or RUN to STOPPED."""
|
|
307
359
|
for module_id, module_data in self._results.items():
|
|
308
360
|
module_status = module_data["module_status"]
|
|
@@ -333,8 +385,8 @@ class HardpyPlugin:
|
|
|
333
385
|
|
|
334
386
|
def _decode_assertion_msg(
|
|
335
387
|
self,
|
|
336
|
-
error: (
|
|
337
|
-
ExceptionInfo[BaseException]
|
|
388
|
+
error: (
|
|
389
|
+
ExceptionInfo[BaseException]
|
|
338
390
|
| tuple[str, int, str]
|
|
339
391
|
| str
|
|
340
392
|
| TerminalRepr
|
|
@@ -348,57 +400,48 @@ class HardpyPlugin:
|
|
|
348
400
|
match error:
|
|
349
401
|
case str():
|
|
350
402
|
return error
|
|
351
|
-
case tuple() if len(error) == 3:
|
|
403
|
+
case tuple() if len(error) == 3: # noqa: PLR2004
|
|
352
404
|
return error[2]
|
|
353
405
|
case ExceptionInfo():
|
|
354
406
|
error_repr = error.getrepr()
|
|
355
407
|
if isinstance(error_repr, ReprExceptionInfo) and error_repr.reprcrash:
|
|
356
408
|
return error_repr.reprcrash.message
|
|
409
|
+
return None
|
|
357
410
|
case TerminalRepr():
|
|
358
|
-
if isinstance(error, ExceptionRepr) and isinstance(
|
|
359
|
-
error.reprcrash,
|
|
411
|
+
if isinstance(error, ExceptionRepr) and isinstance(
|
|
412
|
+
error.reprcrash,
|
|
413
|
+
ReprFileLocation,
|
|
360
414
|
):
|
|
361
415
|
# remove ansi codes
|
|
362
416
|
ansi_pattern = re_compile(
|
|
363
|
-
r"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])" # noqa: E501
|
|
417
|
+
r"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])", # noqa: E501
|
|
364
418
|
)
|
|
365
419
|
return ansi_pattern.sub("", error.reprcrash.message)
|
|
366
420
|
return str(error)
|
|
367
421
|
case _:
|
|
368
422
|
return None
|
|
369
423
|
|
|
370
|
-
def
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
node_info.case_id,
|
|
375
|
-
)
|
|
424
|
+
def _is_skip_test(self, node_info: NodeInfo) -> bool:
|
|
425
|
+
"""Is need to skip a test because it depends on another test."""
|
|
426
|
+
is_dependency_test_exist = self._dependencies.get(
|
|
427
|
+
TestDependencyInfo(node_info.module_id, node_info.case_id),
|
|
376
428
|
)
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
self._progress.calculate(f"{node_info.module_id}::{node_info.case_id}")
|
|
382
|
-
)
|
|
383
|
-
skip(f"Test {node_info.module_id}::{node_info.case_id} is skipped")
|
|
384
|
-
|
|
385
|
-
def _is_dependency_failed(self, dependency) -> bool:
|
|
386
|
-
if isinstance(dependency, TestDependencyInfo):
|
|
387
|
-
incorrect_status = {
|
|
388
|
-
TestStatus.FAILED,
|
|
389
|
-
TestStatus.SKIPPED,
|
|
390
|
-
TestStatus.ERROR,
|
|
391
|
-
}
|
|
392
|
-
module_id, case_id = dependency
|
|
429
|
+
is_dependency_test_failed = False
|
|
430
|
+
if is_dependency_test_exist:
|
|
431
|
+
wrong_status = {TestStatus.FAILED, TestStatus.SKIPPED, TestStatus.ERROR}
|
|
432
|
+
module_id, case_id = is_dependency_test_exist
|
|
393
433
|
if case_id is not None:
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
434
|
+
is_dependency_test_failed = (
|
|
435
|
+
self._results[module_id][case_id] in wrong_status
|
|
436
|
+
)
|
|
437
|
+
else:
|
|
438
|
+
result_set = set(self._results[module_id].values())
|
|
439
|
+
is_dependency_test_failed = any(
|
|
440
|
+
status in wrong_status for status in result_set
|
|
441
|
+
)
|
|
442
|
+
return bool(is_dependency_test_exist and is_dependency_test_failed)
|
|
443
|
+
|
|
444
|
+
def _add_dependency(self, node_info: NodeInfo, nodes: dict) -> None:
|
|
402
445
|
dependency = node_info.dependency
|
|
403
446
|
if dependency is None or dependency == "":
|
|
404
447
|
return
|
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
# Copyright (c) 2024 Everypin
|
|
2
2
|
# GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
|
+
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import socket
|
|
5
|
-
from os import environ
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from
|
|
7
|
+
from os import environ
|
|
8
|
+
from time import sleep
|
|
9
|
+
from typing import Any
|
|
8
10
|
from uuid import uuid4
|
|
9
11
|
|
|
10
12
|
from pycouchdb.exceptions import NotFound
|
|
11
13
|
from pydantic import ValidationError
|
|
12
14
|
|
|
13
15
|
from hardpy.pytest_hardpy.db import (
|
|
14
|
-
DatabaseField as DF,
|
|
16
|
+
DatabaseField as DF, # noqa: N817
|
|
15
17
|
ResultRunStore,
|
|
16
18
|
RunStore,
|
|
17
19
|
)
|
|
20
|
+
from hardpy.pytest_hardpy.reporter import RunnerReporter
|
|
18
21
|
from hardpy.pytest_hardpy.utils import (
|
|
19
22
|
ConnectionData,
|
|
20
|
-
|
|
23
|
+
DialogBox,
|
|
21
24
|
DuplicatePartNumberError,
|
|
25
|
+
DuplicateSerialNumberError,
|
|
26
|
+
DuplicateTestStandLocationError,
|
|
22
27
|
DuplicateTestStandNameError,
|
|
23
|
-
DuplicateDialogBoxError,
|
|
24
|
-
DialogBox,
|
|
25
28
|
)
|
|
26
|
-
from hardpy.pytest_hardpy.reporter import RunnerReporter
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
@dataclass
|
|
@@ -42,7 +44,7 @@ def get_current_report() -> ResultRunStore | None:
|
|
|
42
44
|
"""
|
|
43
45
|
runstore = RunStore()
|
|
44
46
|
try:
|
|
45
|
-
return runstore.get_document()
|
|
47
|
+
return runstore.get_document() # type: ignore
|
|
46
48
|
except NotFound:
|
|
47
49
|
return None
|
|
48
50
|
except ValidationError:
|
|
@@ -51,7 +53,7 @@ def get_current_report() -> ResultRunStore | None:
|
|
|
51
53
|
return None
|
|
52
54
|
|
|
53
55
|
|
|
54
|
-
def set_dut_info(info: dict):
|
|
56
|
+
def set_dut_info(info: dict) -> None:
|
|
55
57
|
"""Add DUT info to document.
|
|
56
58
|
|
|
57
59
|
Args:
|
|
@@ -64,7 +66,7 @@ def set_dut_info(info: dict):
|
|
|
64
66
|
reporter.update_db_by_doc()
|
|
65
67
|
|
|
66
68
|
|
|
67
|
-
def set_dut_serial_number(serial_number: str):
|
|
69
|
+
def set_dut_serial_number(serial_number: str) -> None:
|
|
68
70
|
"""Add DUT serial number to document.
|
|
69
71
|
|
|
70
72
|
Args:
|
|
@@ -77,11 +79,14 @@ def set_dut_serial_number(serial_number: str):
|
|
|
77
79
|
key = reporter.generate_key(DF.DUT, DF.SERIAL_NUMBER)
|
|
78
80
|
if reporter.get_field(key):
|
|
79
81
|
raise DuplicateSerialNumberError
|
|
80
|
-
reporter.set_doc_value(
|
|
82
|
+
reporter.set_doc_value(
|
|
83
|
+
key,
|
|
84
|
+
serial_number if isinstance(serial_number, str) else str(serial_number),
|
|
85
|
+
)
|
|
81
86
|
reporter.update_db_by_doc()
|
|
82
87
|
|
|
83
88
|
|
|
84
|
-
def set_dut_part_number(part_number: str):
|
|
89
|
+
def set_dut_part_number(part_number: str) -> None:
|
|
85
90
|
"""Add DUT part number to document.
|
|
86
91
|
|
|
87
92
|
Args:
|
|
@@ -98,7 +103,7 @@ def set_dut_part_number(part_number: str):
|
|
|
98
103
|
reporter.update_db_by_doc()
|
|
99
104
|
|
|
100
105
|
|
|
101
|
-
def set_stand_name(name: str):
|
|
106
|
+
def set_stand_name(name: str) -> None:
|
|
102
107
|
"""Add test stand name to document.
|
|
103
108
|
|
|
104
109
|
Args:
|
|
@@ -115,7 +120,7 @@ def set_stand_name(name: str):
|
|
|
115
120
|
reporter.update_db_by_doc()
|
|
116
121
|
|
|
117
122
|
|
|
118
|
-
def set_stand_info(info: dict):
|
|
123
|
+
def set_stand_info(info: dict) -> None:
|
|
119
124
|
"""Add test stand info to document.
|
|
120
125
|
|
|
121
126
|
Args:
|
|
@@ -128,7 +133,21 @@ def set_stand_info(info: dict):
|
|
|
128
133
|
reporter.update_db_by_doc()
|
|
129
134
|
|
|
130
135
|
|
|
131
|
-
def
|
|
136
|
+
def set_stand_location(location: str) -> None:
|
|
137
|
+
"""Add test stand location to document.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
location (str): test stand location
|
|
141
|
+
"""
|
|
142
|
+
reporter = RunnerReporter()
|
|
143
|
+
key = reporter.generate_key(DF.TEST_STAND, DF.LOCATION)
|
|
144
|
+
if reporter.get_field(key):
|
|
145
|
+
raise DuplicateTestStandLocationError
|
|
146
|
+
reporter.set_doc_value(key, location)
|
|
147
|
+
reporter.update_db_by_doc()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def set_message(msg: str, msg_key: str | None = None) -> None:
|
|
132
151
|
"""Add or update message in current test.
|
|
133
152
|
|
|
134
153
|
Args:
|
|
@@ -160,7 +179,7 @@ def set_message(msg: str, msg_key: Optional[str] = None) -> None:
|
|
|
160
179
|
reporter.update_db_by_doc()
|
|
161
180
|
|
|
162
181
|
|
|
163
|
-
def set_case_artifact(data: dict):
|
|
182
|
+
def set_case_artifact(data: dict) -> None:
|
|
164
183
|
"""Add data to current test case.
|
|
165
184
|
|
|
166
185
|
Artifact saves only in RunStore database
|
|
@@ -184,7 +203,7 @@ def set_case_artifact(data: dict):
|
|
|
184
203
|
reporter.update_db_by_doc()
|
|
185
204
|
|
|
186
205
|
|
|
187
|
-
def set_module_artifact(data: dict):
|
|
206
|
+
def set_module_artifact(data: dict) -> None:
|
|
188
207
|
"""Add data to current test module.
|
|
189
208
|
|
|
190
209
|
Artifact saves only in RunStore database
|
|
@@ -206,7 +225,7 @@ def set_module_artifact(data: dict):
|
|
|
206
225
|
reporter.update_db_by_doc()
|
|
207
226
|
|
|
208
227
|
|
|
209
|
-
def set_run_artifact(data: dict):
|
|
228
|
+
def set_run_artifact(data: dict) -> None:
|
|
210
229
|
"""Add data to current test run.
|
|
211
230
|
|
|
212
231
|
Artifact saves only in RunStore database
|
|
@@ -226,7 +245,7 @@ def set_run_artifact(data: dict):
|
|
|
226
245
|
|
|
227
246
|
|
|
228
247
|
def set_driver_info(drivers: dict) -> None:
|
|
229
|
-
"""Add or update drivers data.
|
|
248
|
+
"""Add or update test stand drivers data.
|
|
230
249
|
|
|
231
250
|
Driver data is stored in both StateStore and RunStore databases.
|
|
232
251
|
|
|
@@ -238,6 +257,7 @@ def set_driver_info(drivers: dict) -> None:
|
|
|
238
257
|
|
|
239
258
|
for driver_name, driver_data in drivers.items():
|
|
240
259
|
key = reporter.generate_key(
|
|
260
|
+
DF.TEST_STAND,
|
|
241
261
|
DF.DRIVERS,
|
|
242
262
|
driver_name,
|
|
243
263
|
)
|
|
@@ -245,7 +265,7 @@ def set_driver_info(drivers: dict) -> None:
|
|
|
245
265
|
reporter.update_db_by_doc()
|
|
246
266
|
|
|
247
267
|
|
|
248
|
-
def run_dialog_box(dialog_box_data: DialogBox) -> Any:
|
|
268
|
+
def run_dialog_box(dialog_box_data: DialogBox) -> Any: # noqa: ANN401
|
|
249
269
|
"""Display a dialog box.
|
|
250
270
|
|
|
251
271
|
Args:
|
|
@@ -257,6 +277,7 @@ def run_dialog_box(dialog_box_data: DialogBox) -> Any:
|
|
|
257
277
|
- title_bar (str | None): The title bar of the dialog box.
|
|
258
278
|
If the title_bar field is missing, it is the case name.
|
|
259
279
|
- widget (DialogBoxWidget | None): Widget information.
|
|
280
|
+
- image (ImageComponent | None): Image information.
|
|
260
281
|
|
|
261
282
|
Returns:
|
|
262
283
|
Any: An object containing the user's response.
|
|
@@ -268,16 +289,14 @@ def run_dialog_box(dialog_box_data: DialogBox) -> Any:
|
|
|
268
289
|
- NUMERIC_INPUT: float.
|
|
269
290
|
- RADIOBUTTON: str.
|
|
270
291
|
- CHECKBOX: list[str].
|
|
271
|
-
- IMAGE: bool.
|
|
272
292
|
- MULTISTEP: bool.
|
|
273
293
|
|
|
274
294
|
Raises:
|
|
275
295
|
ValueError: If the 'message' argument is empty.
|
|
276
|
-
DuplicateDialogBoxError: If the dialog box is already caused.
|
|
277
296
|
"""
|
|
278
297
|
if not dialog_box_data.dialog_text:
|
|
279
|
-
|
|
280
|
-
|
|
298
|
+
msg = "The 'dialog_text' argument cannot be empty."
|
|
299
|
+
raise ValueError(msg)
|
|
281
300
|
current_test = _get_current_test()
|
|
282
301
|
reporter = RunnerReporter()
|
|
283
302
|
key = reporter.generate_key(
|
|
@@ -287,13 +306,20 @@ def run_dialog_box(dialog_box_data: DialogBox) -> Any:
|
|
|
287
306
|
current_test.case_id,
|
|
288
307
|
DF.DIALOG_BOX,
|
|
289
308
|
)
|
|
290
|
-
|
|
291
|
-
|
|
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)
|
|
292
314
|
|
|
293
315
|
reporter.set_doc_value(key, dialog_box_data.to_dict(), statestore_only=True)
|
|
294
316
|
reporter.update_db_by_doc()
|
|
295
317
|
|
|
296
318
|
input_dbx_data = _get_socket_raw_data()
|
|
319
|
+
|
|
320
|
+
# cleanup widget
|
|
321
|
+
reporter.set_doc_value(key, {}, statestore_only=True)
|
|
322
|
+
reporter.update_db_by_doc()
|
|
297
323
|
return dialog_box_data.widget.convert_data(input_dbx_data)
|
|
298
324
|
|
|
299
325
|
|
|
@@ -308,9 +334,13 @@ def set_operator_message(msg: str, title: str | None = None) -> None:
|
|
|
308
334
|
title (str | None): Title
|
|
309
335
|
"""
|
|
310
336
|
reporter = RunnerReporter()
|
|
311
|
-
key = reporter.generate_key(
|
|
312
|
-
|
|
313
|
-
)
|
|
337
|
+
key = reporter.generate_key(DF.OPERATOR_MSG)
|
|
338
|
+
|
|
339
|
+
reporter.set_doc_value(key, {}, statestore_only=True)
|
|
340
|
+
reporter.update_db_by_doc()
|
|
341
|
+
debounce_time = 0.2
|
|
342
|
+
sleep(debounce_time)
|
|
343
|
+
|
|
314
344
|
msg_data = {"msg": msg, "title": title, "visible": True}
|
|
315
345
|
reporter.set_doc_value(key, msg_data, statestore_only=True)
|
|
316
346
|
reporter.update_db_by_doc()
|
|
@@ -319,12 +349,28 @@ def set_operator_message(msg: str, title: str | None = None) -> None:
|
|
|
319
349
|
reporter.set_doc_value(key, msg_data, statestore_only=True)
|
|
320
350
|
reporter.update_db_by_doc()
|
|
321
351
|
|
|
352
|
+
# cleanup widget
|
|
353
|
+
reporter.set_doc_value(key, {}, statestore_only=True)
|
|
354
|
+
reporter.update_db_by_doc()
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def get_current_attempt() -> int:
|
|
358
|
+
"""Get current attempt.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
int: current attempt
|
|
362
|
+
"""
|
|
363
|
+
reporter = RunnerReporter()
|
|
364
|
+
module_id, case_id = _get_current_test().module_id, _get_current_test().case_id
|
|
365
|
+
return reporter.get_current_attempt(module_id, case_id)
|
|
366
|
+
|
|
322
367
|
|
|
323
368
|
def _get_current_test() -> CurrentTestInfo:
|
|
324
369
|
current_node = environ.get("PYTEST_CURRENT_TEST")
|
|
325
370
|
|
|
326
371
|
if current_node is None:
|
|
327
|
-
|
|
372
|
+
msg = "PYTEST_CURRENT_TEST variable is not set"
|
|
373
|
+
raise RuntimeError(msg)
|
|
328
374
|
|
|
329
375
|
module_delimiter = ".py::"
|
|
330
376
|
module_id_end_index = current_node.find(module_delimiter)
|
|
@@ -351,8 +397,10 @@ def _get_socket_raw_data() -> str:
|
|
|
351
397
|
|
|
352
398
|
try:
|
|
353
399
|
server.bind((con_data.socket_host, con_data.socket_port))
|
|
354
|
-
except
|
|
355
|
-
|
|
400
|
+
except OSError as exc:
|
|
401
|
+
msg = "Socket creating error"
|
|
402
|
+
server.close()
|
|
403
|
+
raise RuntimeError(msg) from exc
|
|
356
404
|
server.listen(1)
|
|
357
405
|
client, _ = server.accept()
|
|
358
406
|
|