hardpy 0.3.0__py3-none-any.whl → 0.5.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.
@@ -21,6 +21,8 @@ def run():
21
21
  parser.add_argument("-dbh", "--db_host", type=str, default=config.db_host, help="database hostname") # noqa: E501
22
22
  parser.add_argument("-wh", "--web_host", type=str, default=config.web_host, help="web operator panel hostname") # noqa: E501
23
23
  parser.add_argument("-wp", "--web_port", type=str, default=config.web_port, help="web operator panel port") # noqa: E501
24
+ parser.add_argument("-sp", "--sck_port", type=int, default=config.socket_port, help="internal socket port") # noqa: E501
25
+ parser.add_argument("-sa", "--sck_addr", type=int, default=config.socket_port, help="internal socket address") # noqa: E501
24
26
  parser.add_argument("path", type=str, nargs='?', help="path to test directory")
25
27
  # fmt: on
26
28
 
@@ -23,3 +23,4 @@ class DatabaseField(str, Enum): # noqa: WPS600
23
23
  TEST_STAND = "test_stand"
24
24
  SERIAL_NUMBER = "serial_number"
25
25
  DRIVERS = "drivers"
26
+ DIALOG_BOX = "dialog_box"
@@ -29,12 +29,23 @@ class CaseStateStore(IBaseResult):
29
29
  "start_time": 1695817188,
30
30
  "stop_time": 1695817189,
31
31
  "assertion_msg": null,
32
- "msg": null
32
+ "msg": null,
33
+ "dialog_box": {
34
+ "title_bar": "Example of text input",
35
+ "dialog_text": "Type some text and press the Confirm button",
36
+ "widget": {
37
+ "info": {
38
+ "text": "some text"
39
+ },
40
+ "type": "textinput"
41
+ }
42
+ }
33
43
  }
34
44
  """
35
45
 
36
46
  assertion_msg: str | None = None
37
47
  msg: dict | None = None
48
+ dialog_box: dict = {}
38
49
 
39
50
 
40
51
  class CaseRunStore(IBaseResult):
@@ -175,7 +186,17 @@ class ResultStateStore(IBaseResult):
175
186
  "start_time": 1695817263,
176
187
  "stop_time": 1695817264,
177
188
  "assertion_msg": null,
178
- "msg": null
189
+ "msg": null,
190
+ "dialog_box": {
191
+ "title_bar": "Example of text input",
192
+ "dialog_text": "Type some text and press the Confirm button",
193
+ "widget": {
194
+ "info": {
195
+ "text": "some text"
196
+ },
197
+ "type": "textinput"
198
+ }
199
+ }
179
200
  },
180
201
  "test_minute_parity": {
181
202
  "status": "failed",
@@ -6,6 +6,7 @@ from typing import Any, Callable
6
6
  from logging import getLogger
7
7
  from pathlib import Path, PurePath
8
8
  from platform import system
9
+ from re import compile as re_compile
9
10
 
10
11
  from natsort import natsorted
11
12
  from pytest import (
@@ -19,6 +20,13 @@ from pytest import (
19
20
  fixture,
20
21
  ExitCode,
21
22
  )
23
+ from _pytest._code.code import (
24
+ ExceptionRepr,
25
+ ReprFileLocation,
26
+ ExceptionInfo,
27
+ ReprExceptionInfo,
28
+ TerminalRepr,
29
+ )
22
30
 
23
31
  from hardpy.pytest_hardpy.reporter import HookReporter
24
32
  from hardpy.pytest_hardpy.utils import (
@@ -40,6 +48,8 @@ def pytest_addoption(parser: Parser):
40
48
  parser.addoption("--hardpy-dbp", action="store", default=config_data.db_port, help="database port number") # noqa: E501
41
49
  parser.addoption("--hardpy-dbh", action="store", default=config_data.db_host, help="database hostname") # noqa: E501
42
50
  parser.addoption("--hardpy-pt", action="store_true", default=False, help="enable pytest-hardpy plugin") # noqa: E501
51
+ parser.addoption("--hardpy-sp", action="store", default=config_data.socket_port, help="internal socket port") # noqa: E501
52
+ parser.addoption("--hardpy-sa", action="store", default=config_data.socket_addr, help="internal socket address") # noqa: E501
43
53
  # fmt: on
44
54
 
45
55
 
@@ -77,6 +87,8 @@ class HardpyPlugin(object):
77
87
  config_data.db_host = config.getoption("--hardpy-dbh")
78
88
  config_data.db_pswd = config.getoption("--hardpy-dbpw")
79
89
  config_data.db_port = config.getoption("--hardpy-dbp")
90
+ config_data.socket_port = int(config.getoption("--hardpy-sp"))
91
+ config_data.socket_addr = config.getoption("--hardpy-sa")
80
92
 
81
93
  config.addinivalue_line("markers", "case_name")
82
94
  config.addinivalue_line("markers", "module_name")
@@ -183,8 +195,9 @@ class HardpyPlugin(object):
183
195
 
184
196
  def pytest_runtest_logreport(self, report: TestReport):
185
197
  """Call after call of each test item."""
186
- if report.when != "call":
198
+ if report.when != "call" and report.failed is False:
187
199
  # ignore setup and teardown phase
200
+ # or continue processing setup and teardown failure (fixture exception handler)
188
201
  return True
189
202
 
190
203
  module_id = Path(report.fspath).stem
@@ -200,7 +213,7 @@ class HardpyPlugin(object):
200
213
  case_id,
201
214
  )
202
215
 
203
- assertion_msg = self._decode_assertion_msg(report.longreprtext)
216
+ assertion_msg = self._decode_assertion_msg(report.longrepr)
204
217
  self._reporter.set_assertion_msg(module_id, case_id, assertion_msg)
205
218
  self._reporter.set_progress(self._progress.calculate(report.nodeid))
206
219
  self._results[module_id][case_id] = report.outcome # noqa: WPS204
@@ -261,15 +274,41 @@ class HardpyPlugin(object):
261
274
  case _:
262
275
  return RunStatus.ERROR
263
276
 
264
- def _decode_assertion_msg(self, msg: str) -> str | None:
265
- assertion_str = "AssertionError: "
266
-
267
- if assertion_str in msg:
268
- index = msg.find(assertion_str)
269
- report = msg[index + len(assertion_str) :]
270
- index = report.find("\nE")
271
- return report[:index]
272
- return None
277
+ def _decode_assertion_msg(
278
+ self,
279
+ error: ( # noqa: WPS320
280
+ ExceptionInfo[BaseException] # noqa: DAR101,DAR201
281
+ | tuple[str, int, str]
282
+ | str
283
+ | TerminalRepr
284
+ | None
285
+ ),
286
+ ) -> str | None:
287
+ """Parse pytest assertion error message."""
288
+ if error is None:
289
+ return None
290
+
291
+ match error:
292
+ case str():
293
+ return error
294
+ case tuple() if len(error) == 3:
295
+ return error[2]
296
+ case ExceptionInfo():
297
+ error_repr = error.getrepr()
298
+ if isinstance(error_repr, ReprExceptionInfo) and error_repr.reprcrash:
299
+ return error_repr.reprcrash.message
300
+ case TerminalRepr():
301
+ if isinstance(error, ExceptionRepr) and isinstance( # noqa: WPS337
302
+ error.reprcrash, ReprFileLocation
303
+ ):
304
+ # remove ansi codes
305
+ ansi_pattern = re_compile(
306
+ r"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])" # noqa: E501
307
+ )
308
+ return ansi_pattern.sub("", error.reprcrash.message)
309
+ return str(error)
310
+ case _:
311
+ return None
273
312
 
274
313
  def _handle_dependency(self, node_info: NodeInfo):
275
314
  dependency = self._dependencies.get(
@@ -1,9 +1,10 @@
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
3
 
4
+ import socket
4
5
  from os import environ
5
6
  from dataclasses import dataclass
6
- from typing import Optional
7
+ from typing import Optional, Any
7
8
  from uuid import uuid4
8
9
 
9
10
  from pycouchdb.exceptions import NotFound
@@ -14,7 +15,12 @@ from hardpy.pytest_hardpy.db import (
14
15
  ResultRunStore,
15
16
  RunStore,
16
17
  )
17
- from hardpy.pytest_hardpy.utils import DuplicateSerialNumberError
18
+ from hardpy.pytest_hardpy.utils import (
19
+ DuplicateSerialNumberError,
20
+ DuplicateDialogBoxError,
21
+ DialogBox,
22
+ ConfigData,
23
+ )
18
24
  from hardpy.pytest_hardpy.reporter import RunnerReporter
19
25
 
20
26
 
@@ -184,7 +190,7 @@ def set_run_artifact(data: dict):
184
190
 
185
191
 
186
192
  def set_driver_info(drivers: dict) -> None:
187
- """Adds or updates drivers data.
193
+ """Add or update drivers data.
188
194
 
189
195
  Driver data is stored in both StateStore and RunStore databases.
190
196
 
@@ -203,6 +209,58 @@ def set_driver_info(drivers: dict) -> None:
203
209
  reporter.update_db_by_doc()
204
210
 
205
211
 
212
+ def run_dialog_box(dialog_box_data: DialogBox) -> Any:
213
+ """Display a dialog box.
214
+
215
+ Args:
216
+ dialog_box_data (DialogBox): Data for creating the dialog box.
217
+
218
+ DialogBox attributes:
219
+
220
+ - dialog_text (str): The text of the dialog box.
221
+ - title_bar (str | None): The title bar of the dialog box.
222
+ If the title_bar field is missing, it is the case name.
223
+ - widget (DialogBoxWidget | None): Widget information.
224
+
225
+ Returns:
226
+ Any: An object containing the user's response.
227
+
228
+ The type of the return value depends on the widget type:
229
+
230
+ - BASE: bool.
231
+ - TEXT_INPUT: str.
232
+ - NUMERIC_INPUT: float.
233
+ - RADIOBUTTON: str.
234
+ - CHECKBOX: list[str].
235
+ - IMAGE: bool.
236
+ - MULTISTEP: bool.
237
+
238
+ Raises:
239
+ ValueError: If the 'message' argument is empty.
240
+ DuplicateDialogBoxError: If the dialog box is already caused.
241
+ """
242
+ if not dialog_box_data.dialog_text:
243
+ raise ValueError("The 'dialog_text' argument cannot be empty.")
244
+
245
+ current_test = _get_current_test()
246
+ reporter = RunnerReporter()
247
+ key = reporter.generate_key(
248
+ DF.MODULES,
249
+ current_test.module_id,
250
+ DF.CASES,
251
+ current_test.case_id,
252
+ DF.DIALOG_BOX,
253
+ )
254
+ if reporter.get_field(key):
255
+ raise DuplicateDialogBoxError
256
+
257
+ reporter.set_doc_value(key, dialog_box_data.to_dict(), statestore_only=True)
258
+ reporter.update_db_by_doc()
259
+
260
+ input_dbx_data = _get_socket_raw_data()
261
+ return dialog_box_data.widget.convert_data(input_dbx_data)
262
+
263
+
206
264
  def _get_current_test() -> CurrentTestInfo:
207
265
  current_node = environ.get("PYTEST_CURRENT_TEST")
208
266
 
@@ -224,3 +282,26 @@ def _get_current_test() -> CurrentTestInfo:
224
282
  case_id = case_with_stage[:case_id_end_index]
225
283
 
226
284
  return CurrentTestInfo(module_id=module_id, case_id=case_id)
285
+
286
+
287
+ def _get_socket_raw_data() -> str:
288
+ # create socket connection
289
+ server = socket.socket()
290
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
291
+ config_data = ConfigData()
292
+ try:
293
+ server.bind((config_data.socket_addr, config_data.socket_port))
294
+ except socket.error as exc:
295
+ raise RuntimeError(f"Error creating socket: {exc}")
296
+ server.listen(1)
297
+ client, _ = server.accept()
298
+
299
+ # receive data
300
+ max_input_data_len = 1024
301
+ socket_data = client.recv(max_input_data_len).decode("utf-8")
302
+
303
+ # close connection
304
+ client.close()
305
+ server.close()
306
+
307
+ return socket_data
@@ -4,6 +4,7 @@
4
4
  import sys
5
5
  import signal
6
6
  import subprocess
7
+ from socket import socket
7
8
  from platform import system
8
9
 
9
10
  from hardpy.pytest_hardpy.utils.config_data import ConfigData
@@ -46,6 +47,10 @@ class PyTestWrapper(object):
46
47
  self.config.db_user,
47
48
  "--hardpy-dbpw",
48
49
  self.config.db_pswd,
50
+ "--hardpy-sp",
51
+ str(self.config.socket_port),
52
+ "--hardpy-sa",
53
+ self.config.socket_addr,
49
54
  "--hardpy-pt",
50
55
  ],
51
56
  cwd=self.config.tests_dir.absolute(),
@@ -64,6 +69,10 @@ class PyTestWrapper(object):
64
69
  self.config.db_user,
65
70
  "--hardpy-dbpw",
66
71
  self.config.db_pswd,
72
+ "--hardpy-sp",
73
+ str(self.config.socket_port),
74
+ "--hardpy-sa",
75
+ self.config.socket_addr,
67
76
  "--hardpy-pt",
68
77
  ],
69
78
  cwd=self.config.tests_dir.absolute(),
@@ -115,6 +124,26 @@ class PyTestWrapper(object):
115
124
  )
116
125
  return True
117
126
 
127
+ def confirm_dialog_box(self, dialog_box_output: str):
128
+ """Set dialog box data to pytest subprocess.
129
+
130
+ Args:
131
+ dialog_box_output (str): dialog box output data
132
+
133
+ Returns:
134
+ bool: True if dialog box was confirmed, else False
135
+ """
136
+ config_data = ConfigData()
137
+
138
+ try:
139
+ client = socket()
140
+ client.connect((config_data.socket_addr, config_data.socket_port))
141
+ client.sendall(dialog_box_output.encode("utf-8"))
142
+ client.close()
143
+ except Exception:
144
+ return False
145
+ return True
146
+
118
147
  def is_running(self) -> bool | None:
119
148
  """Check if pytest is running."""
120
149
  return self._proc and self._proc.poll() is None
@@ -90,8 +90,8 @@ class HookReporter(BaseReporter):
90
90
  item_statestore = self._statestore.get_field(key)
91
91
  item_runstore = self._runstore.get_field(key)
92
92
 
93
- self._init_case(item_statestore, node_info)
94
- self._init_case(item_runstore, node_info, is_use_artifact=True)
93
+ self._init_case(item_statestore, node_info, is_only_statestore=True)
94
+ self._init_case(item_runstore, node_info, is_only_runstore=True)
95
95
 
96
96
  self.set_doc_value(key, item_statestore, statestore_only=True)
97
97
  self.set_doc_value(key, item_runstore, runstore_only=True)
@@ -176,7 +176,11 @@ class HookReporter(BaseReporter):
176
176
  self.set_doc_value(key, int(time()))
177
177
 
178
178
  def _init_case(
179
- self, item: dict, node_info: NodeInfo, is_use_artifact: bool = False
179
+ self,
180
+ item: dict,
181
+ node_info: NodeInfo,
182
+ is_only_runstore: bool = False,
183
+ is_only_statestore: bool = False,
180
184
  ):
181
185
  module_default = { # noqa: WPS204
182
186
  DF.STATUS: TestStatus.READY,
@@ -195,7 +199,7 @@ class HookReporter(BaseReporter):
195
199
  }
196
200
 
197
201
  if item.get(node_info.module_id) is None: # noqa: WPS204
198
- if is_use_artifact:
202
+ if is_only_runstore:
199
203
  module_default[DF.ARTIFACT] = {}
200
204
  item[node_info.module_id] = module_default # noqa: WPS204
201
205
  else:
@@ -205,8 +209,11 @@ class HookReporter(BaseReporter):
205
209
  item[node_info.module_id][DF.STOP_TIME] = None
206
210
  item[node_info.module_id][DF.NAME] = self._get_module_name(node_info)
207
211
 
208
- if is_use_artifact:
212
+ if is_only_runstore:
209
213
  case_default[DF.ARTIFACT] = {}
214
+
215
+ if is_only_statestore:
216
+ case_default[DF.DIALOG_BOX] = {}
210
217
  item[node_info.module_id][DF.CASES][node_info.case_id] = case_default
211
218
 
212
219
  def _remove_outdate_node(
@@ -6,7 +6,22 @@ from hardpy.pytest_hardpy.utils.progress_calculator import ProgressCalculator
6
6
  from hardpy.pytest_hardpy.utils.const import TestStatus, RunStatus
7
7
  from hardpy.pytest_hardpy.utils.singleton import Singleton
8
8
  from hardpy.pytest_hardpy.utils.config_data import ConfigData
9
- from hardpy.pytest_hardpy.utils.exception import DuplicateSerialNumberError
9
+ from hardpy.pytest_hardpy.utils.exception import (
10
+ DuplicateSerialNumberError,
11
+ DuplicateDialogBoxError,
12
+ WidgetInfoError,
13
+ )
14
+ from hardpy.pytest_hardpy.utils.dialog_box import (
15
+ DialogBox,
16
+ TextInputWidget,
17
+ NumericInputWidget,
18
+ CheckboxWidget,
19
+ RadiobuttonWidget,
20
+ ImageWidget,
21
+ MultistepWidget,
22
+ StepWidget,
23
+ )
24
+
10
25
 
11
26
  __all__ = [
12
27
  "NodeInfo",
@@ -16,4 +31,14 @@ __all__ = [
16
31
  "Singleton",
17
32
  "ConfigData",
18
33
  "DuplicateSerialNumberError",
34
+ "DuplicateDialogBoxError",
35
+ "WidgetInfoError",
36
+ "DialogBox",
37
+ "TextInputWidget",
38
+ "NumericInputWidget",
39
+ "CheckboxWidget",
40
+ "RadiobuttonWidget",
41
+ "ImageWidget",
42
+ "MultistepWidget",
43
+ "StepWidget"
19
44
  ]
@@ -1,9 +1,11 @@
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
3
 
4
- from hardpy.pytest_hardpy.utils.singleton import Singleton
4
+ from socket import gethostname
5
5
  from pathlib import Path
6
6
 
7
+ from hardpy.pytest_hardpy.utils.singleton import Singleton
8
+
7
9
 
8
10
  class ConfigData(Singleton):
9
11
  """Web connection data storage."""
@@ -17,6 +19,8 @@ class ConfigData(Singleton):
17
19
  self.web_host: str = "0.0.0.0"
18
20
  self.web_port: int = 8000
19
21
  self.tests_dir = Path.cwd()
22
+ self.socket_port: int = 6525
23
+ self.socket_addr: str = gethostname()
20
24
  self._initialized = True
21
25
 
22
26
  @property