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.
Files changed (47) hide show
  1. hardpy/__init__.py +49 -49
  2. hardpy/cli/cli.py +8 -9
  3. hardpy/cli/template.py +6 -6
  4. hardpy/common/config.py +19 -18
  5. hardpy/hardpy_panel/api.py +9 -9
  6. hardpy/hardpy_panel/frontend/dist/asset-manifest.json +3 -3
  7. hardpy/hardpy_panel/frontend/dist/index.html +1 -1
  8. hardpy/hardpy_panel/frontend/dist/logo192.png +0 -0
  9. hardpy/hardpy_panel/frontend/dist/static/css/main.e8a862f1.css.map +1 -1
  10. hardpy/hardpy_panel/frontend/dist/static/js/main.6f09d61a.js +3 -0
  11. hardpy/hardpy_panel/frontend/dist/static/js/{main.7c954faf.js.map → main.6f09d61a.js.map} +1 -1
  12. hardpy/pytest_hardpy/db/__init__.py +4 -5
  13. hardpy/pytest_hardpy/db/base_connector.py +6 -5
  14. hardpy/pytest_hardpy/db/base_server.py +1 -1
  15. hardpy/pytest_hardpy/db/base_store.py +23 -9
  16. hardpy/pytest_hardpy/db/const.py +3 -1
  17. hardpy/pytest_hardpy/db/runstore.py +13 -15
  18. hardpy/pytest_hardpy/db/schema/__init__.py +9 -0
  19. hardpy/pytest_hardpy/db/{schema.py → schema/v1.py} +120 -79
  20. hardpy/pytest_hardpy/db/statestore.py +7 -20
  21. hardpy/pytest_hardpy/plugin.py +128 -85
  22. hardpy/pytest_hardpy/pytest_call.py +80 -32
  23. hardpy/pytest_hardpy/pytest_wrapper.py +8 -8
  24. hardpy/pytest_hardpy/reporter/__init__.py +2 -2
  25. hardpy/pytest_hardpy/reporter/base.py +32 -7
  26. hardpy/pytest_hardpy/reporter/hook_reporter.py +66 -37
  27. hardpy/pytest_hardpy/reporter/runner_reporter.py +6 -8
  28. hardpy/pytest_hardpy/result/__init__.py +2 -2
  29. hardpy/pytest_hardpy/result/couchdb_config.py +20 -16
  30. hardpy/pytest_hardpy/result/report_loader/couchdb_loader.py +2 -2
  31. hardpy/pytest_hardpy/result/report_reader/couchdb_reader.py +36 -20
  32. hardpy/pytest_hardpy/utils/__init__.py +34 -29
  33. hardpy/pytest_hardpy/utils/connection_data.py +6 -8
  34. hardpy/pytest_hardpy/utils/const.py +1 -1
  35. hardpy/pytest_hardpy/utils/dialog_box.py +105 -66
  36. hardpy/pytest_hardpy/utils/exception.py +14 -8
  37. hardpy/pytest_hardpy/utils/machineid.py +15 -0
  38. hardpy/pytest_hardpy/utils/node_info.py +45 -16
  39. hardpy/pytest_hardpy/utils/progress_calculator.py +4 -3
  40. hardpy/pytest_hardpy/utils/singleton.py +23 -16
  41. {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/METADATA +26 -33
  42. {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/RECORD +46 -43
  43. hardpy/hardpy_panel/frontend/dist/static/js/main.7c954faf.js +0 -3
  44. /hardpy/hardpy_panel/frontend/dist/static/js/{main.7c954faf.js.LICENSE.txt → main.6f09d61a.js.LICENSE.txt} +0 -0
  45. {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/WHEEL +0 -0
  46. {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/entry_points.txt +0 -0
  47. {hardpy-0.6.1.dist-info → hardpy-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
- skip,
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
- ExitCode,
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
- TestStatus,
35
+ ConnectionData,
34
36
  NodeInfo,
35
37
  ProgressCalculator,
36
- ConnectionData,
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="store",
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(early_config, parser, args):
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(is_clear_statestore)
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, session: Session, config: Config, items: list[Item]
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}\n"
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
- self._handle_dependency(node_info)
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, TestStatus.RUN)
209
- self._reporter.set_module_start_time(node_info.module_id)
210
- self._reporter.set_case_status(
211
- node_info.module_id,
212
- node_info.case_id,
213
- TestStatus.RUN,
214
- )
215
- self._reporter.set_case_start_time(
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
- # or continue processing setup and teardown failure (fixture exception handler)
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 # noqa: WPS204
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): # noqa: WPS231
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: ( # noqa: WPS320
337
- ExceptionInfo[BaseException] # noqa: DAR101,DAR201
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( # noqa: WPS337
359
- error.reprcrash, ReprFileLocation
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 _handle_dependency(self, node_info: NodeInfo):
371
- dependency = self._dependencies.get(
372
- TestDependencyInfo(
373
- node_info.module_id,
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
- if dependency and self._is_dependency_failed(dependency):
378
- self._log.debug(f"Skipping test due to dependency: {dependency}")
379
- self._results[node_info.module_id][node_info.case_id] = TestStatus.SKIPPED
380
- self._reporter.set_progress(
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
- return self._results[module_id][case_id] in incorrect_status
395
- return any(
396
- status in incorrect_status
397
- for status in set(self._results[module_id].values())
398
- )
399
- return False
400
-
401
- def _add_dependency(self, node_info, nodes):
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 typing import Optional, Any
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
- DuplicateSerialNumberError,
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(key, serial_number)
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 set_message(msg: str, msg_key: Optional[str] = None) -> None:
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
- raise ValueError("The 'dialog_text' argument cannot be empty.")
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
- if reporter.get_field(key):
291
- raise DuplicateDialogBoxError
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
- DF.OPERATOR_MSG,
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
- raise RuntimeError("PYTEST_CURRENT_TEST variable is not set")
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 socket.error as exc:
355
- raise RuntimeError(f"Error creating socket: {exc}")
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