hardpy 0.1.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 (71) hide show
  1. hardpy/__init__.py +34 -0
  2. hardpy/hardpy_panel/__init__.py +0 -0
  3. hardpy/hardpy_panel/api.py +71 -0
  4. hardpy/hardpy_panel/frontend/dist/asset-manifest.json +36 -0
  5. hardpy/hardpy_panel/frontend/dist/favicon.ico +0 -0
  6. hardpy/hardpy_panel/frontend/dist/index.html +1 -0
  7. hardpy/hardpy_panel/frontend/dist/logo512.png +0 -0
  8. hardpy/hardpy_panel/frontend/dist/manifest.json +25 -0
  9. hardpy/hardpy_panel/frontend/dist/static/css/main.e8a862f1.css +2 -0
  10. hardpy/hardpy_panel/frontend/dist/static/css/main.e8a862f1.css.map +1 -0
  11. hardpy/hardpy_panel/frontend/dist/static/js/808.ce070002.chunk.js +2 -0
  12. hardpy/hardpy_panel/frontend/dist/static/js/808.ce070002.chunk.js.map +1 -0
  13. hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-16px-paths.d605910e.chunk.js +2 -0
  14. hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-16px-paths.d605910e.chunk.js.map +1 -0
  15. hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-20px-paths.7ee05cc8.chunk.js +2 -0
  16. hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-20px-paths.7ee05cc8.chunk.js.map +1 -0
  17. hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-all-paths-loader.0aa89747.chunk.js +2 -0
  18. hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-all-paths-loader.0aa89747.chunk.js.map +1 -0
  19. hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-all-paths.f63155c9.chunk.js +2 -0
  20. hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-all-paths.f63155c9.chunk.js.map +1 -0
  21. hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-split-paths-by-size-loader.52a072d3.chunk.js +2 -0
  22. hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-split-paths-by-size-loader.52a072d3.chunk.js.map +1 -0
  23. hardpy/hardpy_panel/frontend/dist/static/js/main.8ef63e9b.js +3 -0
  24. hardpy/hardpy_panel/frontend/dist/static/js/main.8ef63e9b.js.LICENSE.txt +90 -0
  25. hardpy/hardpy_panel/frontend/dist/static/js/main.8ef63e9b.js.map +1 -0
  26. hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-16.520846c6beb41df528c8.eot +0 -0
  27. hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-16.5c52b39c697f2323ce8b.svg +1806 -0
  28. hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-16.84db1772f4bfb529f64f.woff +0 -0
  29. hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-16.b67ee1736e20e37a3225.woff2 +0 -0
  30. hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-16.e02ecf515378db143652.ttf +0 -0
  31. hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-20.429cacb8accf72488451.ttf +0 -0
  32. hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-20.6ae3791ee2d86fc228a6.svg +1806 -0
  33. hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-20.8cecf62de42997e4d82f.woff2 +0 -0
  34. hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-20.afbadb627d43b7857223.eot +0 -0
  35. hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-20.e857f5a5132b8bfa71a1.woff +0 -0
  36. hardpy/hardpy_panel/frontend/dist/static/media/logo_smol.5b16f92447a4a9e80331.png +0 -0
  37. hardpy/hardpy_panel/runner.py +52 -0
  38. hardpy/pytest_hardpy/__init__.py +0 -0
  39. hardpy/pytest_hardpy/db/__init__.py +18 -0
  40. hardpy/pytest_hardpy/db/base_connector.py +24 -0
  41. hardpy/pytest_hardpy/db/base_server.py +14 -0
  42. hardpy/pytest_hardpy/db/base_store.py +88 -0
  43. hardpy/pytest_hardpy/db/const.py +25 -0
  44. hardpy/pytest_hardpy/db/runstore.py +30 -0
  45. hardpy/pytest_hardpy/db/schema.py +292 -0
  46. hardpy/pytest_hardpy/db/statestore.py +19 -0
  47. hardpy/pytest_hardpy/plugin.py +244 -0
  48. hardpy/pytest_hardpy/pytest_call.py +218 -0
  49. hardpy/pytest_hardpy/pytest_wrapper.py +117 -0
  50. hardpy/pytest_hardpy/reporter/__init__.py +10 -0
  51. hardpy/pytest_hardpy/reporter/base.py +42 -0
  52. hardpy/pytest_hardpy/reporter/hook_reporter.py +307 -0
  53. hardpy/pytest_hardpy/reporter/runner_reporter.py +29 -0
  54. hardpy/pytest_hardpy/result/__init__.py +10 -0
  55. hardpy/pytest_hardpy/result/couchdb_config.py +22 -0
  56. hardpy/pytest_hardpy/result/report_loader/__init__.py +10 -0
  57. hardpy/pytest_hardpy/result/report_loader/couchdb_loader.py +62 -0
  58. hardpy/pytest_hardpy/result/report_reader/__init__.py +0 -0
  59. hardpy/pytest_hardpy/result/report_reader/couchdb_reader.py +164 -0
  60. hardpy/pytest_hardpy/utils/__init__.py +19 -0
  61. hardpy/pytest_hardpy/utils/config_data.py +31 -0
  62. hardpy/pytest_hardpy/utils/const.py +29 -0
  63. hardpy/pytest_hardpy/utils/exception.py +16 -0
  64. hardpy/pytest_hardpy/utils/node_info.py +59 -0
  65. hardpy/pytest_hardpy/utils/progress_calculator.py +38 -0
  66. hardpy/pytest_hardpy/utils/singleton.py +23 -0
  67. hardpy-0.1.0.dist-info/METADATA +129 -0
  68. hardpy-0.1.0.dist-info/RECORD +71 -0
  69. hardpy-0.1.0.dist-info/WHEEL +4 -0
  70. hardpy-0.1.0.dist-info/entry_points.txt +5 -0
  71. hardpy-0.1.0.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,244 @@
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
+
4
+ import signal
5
+ from typing import Any, Callable
6
+ from logging import getLogger
7
+ from pathlib import Path, PurePath
8
+ from platform import system
9
+
10
+ from natsort import natsorted
11
+ from pytest import (
12
+ exit,
13
+ TestReport,
14
+ Item,
15
+ Session,
16
+ Config,
17
+ Parser,
18
+ fixture,
19
+ ExitCode,
20
+ )
21
+
22
+ from hardpy.pytest_hardpy.reporter import HookReporter
23
+ from hardpy.pytest_hardpy.utils import (
24
+ TestStatus,
25
+ RunStatus,
26
+ NodeInfo,
27
+ ProgressCalculator,
28
+ ConfigData,
29
+ )
30
+
31
+
32
+ def pytest_addoption(parser: Parser):
33
+ """Register argparse-style options."""
34
+ config_data = ConfigData()
35
+ # fmt: off
36
+ parser.addoption("--hardpy-dbu", action="store", default=config_data.db_user, help="database user") # noqa: E501
37
+ parser.addoption("--hardpy-dbpw", action="store", default=config_data.db_pswd, help="database user password") # noqa: E501
38
+ parser.addoption("--hardpy-dbp", action="store", default=config_data.db_port, help="database port number") # noqa: E501
39
+ parser.addoption("--hardpy-dbh", action="store", default=config_data.db_host, help="database hostname") # noqa: E501
40
+ # fmt: on
41
+
42
+
43
+ class HardpyPlugin(object):
44
+ """HardPy integration plugin for pytest.
45
+
46
+ Extends hook functions from pytest API.
47
+ """
48
+
49
+ def __init__(self):
50
+ self._progress = ProgressCalculator()
51
+ self._results = {}
52
+ self._post_run_functions: list[Callable] = []
53
+
54
+ if system() == "Linux":
55
+ signal.signal(signal.SIGTERM, self._stop_handler)
56
+ elif system() == "Windows":
57
+ signal.signal(signal.SIGBREAK, self._stop_handler)
58
+ self._log = getLogger(__name__)
59
+
60
+ # Initialization hooks
61
+
62
+ def pytest_configure(self, config: Config):
63
+ """Configure pytest."""
64
+ config_data = ConfigData()
65
+ config_data.db_user = config.getoption("--hardpy-dbu")
66
+ config_data.db_host = config.getoption("--hardpy-dbh")
67
+ config_data.db_pswd = config.getoption("--hardpy-dbpw")
68
+ config_data.db_port = config.getoption("--hardpy-dbp")
69
+
70
+ config.addinivalue_line("markers", "case_name")
71
+ config.addinivalue_line("markers", "module_name")
72
+
73
+ # must be init after config data is set
74
+ self._reporter = HookReporter()
75
+
76
+ def pytest_sessionfinish(self, session: Session, exitstatus: int):
77
+ """Call at the end of test session.
78
+
79
+ Args:
80
+ session (Session): session description
81
+ exitstatus (int): exit test status
82
+ """
83
+ if "--collect-only" in session.config.invocation_params.args:
84
+ return
85
+ status = self._get_run_status(exitstatus)
86
+ self._reporter.finish(status)
87
+
88
+ # call post run methods
89
+ if self._post_run_functions:
90
+ for action in self._post_run_functions:
91
+ action()
92
+
93
+ # Collection hooks
94
+
95
+ def pytest_collection_modifyitems(
96
+ self, session: Session, config: Config, items: list[Item]
97
+ ):
98
+ """Call after collection phase."""
99
+ self._reporter.init_doc(str(PurePath(config.rootpath).name))
100
+
101
+ nodes = {}
102
+
103
+ session.items = natsorted(
104
+ session.items,
105
+ key=lambda x: x.parent.name if x.parent is not None else x.name,
106
+ )
107
+ for item in session.items:
108
+ if item.parent is None:
109
+ continue
110
+ node_info = NodeInfo(item)
111
+
112
+ self._init_case_result(node_info.module_id, node_info.case_id)
113
+
114
+ if node_info.module_id not in nodes:
115
+ nodes[node_info.module_id] = [node_info.case_id]
116
+ else:
117
+ nodes[node_info.module_id].append(node_info.case_id)
118
+
119
+ self._reporter.add_case(node_info)
120
+ self._reporter.set_module_status(node_info.module_id, TestStatus.READY)
121
+ self._reporter.update_node_order(nodes)
122
+
123
+ # Test running (runtest) hooks
124
+
125
+ def pytest_runtestloop(self, session: Session):
126
+ """Call at the start of test run."""
127
+ self._progress.set_test_amount(session.testscollected)
128
+ if session.config.option.collectonly:
129
+ # ignore collect only mode
130
+ return True
131
+
132
+ # testrun entrypoint
133
+ self._reporter.start()
134
+
135
+ def pytest_runtest_setup(self, item: Item):
136
+ """Call before each test setup phase."""
137
+ if item.parent is None:
138
+ self._log.error(f"Test module name for test {item.name} not found.")
139
+ return
140
+
141
+ node_info = NodeInfo(item)
142
+
143
+ self._reporter.set_module_status(node_info.module_id, TestStatus.RUN)
144
+ self._reporter.set_module_start_time(node_info.module_id)
145
+ self._reporter.set_case_status(
146
+ node_info.module_id,
147
+ node_info.case_id,
148
+ TestStatus.RUN,
149
+ )
150
+ self._reporter.set_case_start_time(
151
+ node_info.module_id,
152
+ node_info.case_id,
153
+ )
154
+
155
+ # Reporting hooks
156
+
157
+ def pytest_runtest_logreport(self, report: TestReport):
158
+ """Call after call of each test item."""
159
+ if report.when != "call":
160
+ # ignore setup and teardown phase
161
+ return True
162
+
163
+ module_id = Path(report.fspath).stem
164
+ case_id = report.nodeid.rpartition("::")[2]
165
+
166
+ self._reporter.set_case_status(
167
+ module_id,
168
+ case_id,
169
+ TestStatus(report.outcome),
170
+ )
171
+ self._reporter.set_case_stop_time(
172
+ module_id,
173
+ case_id,
174
+ )
175
+
176
+ assertion_msg = self._decode_assertion_msg(report.longreprtext)
177
+ self._reporter.set_assertion_msg(module_id, case_id, assertion_msg)
178
+ self._reporter.set_progress(self._progress.calculate(report.nodeid))
179
+ self._results[module_id][case_id] = report.outcome # noqa: WPS204
180
+
181
+ if None not in self._results[module_id].values():
182
+ self._collect_module_result(module_id)
183
+
184
+ # Fixture
185
+
186
+ @fixture(scope="session")
187
+ def post_run_functions(self) -> list[Callable]:
188
+ """Get post run methods list.
189
+
190
+ Fill this list in conftest.py and functions from this
191
+ list will be called after tests running (in the end of pytest_sessionfinish).
192
+
193
+ Returns:
194
+ list[Callable]: list of post run methods
195
+ """
196
+ return self._post_run_functions
197
+
198
+ # Not hooks
199
+
200
+ def _stop_handler(self, signum: int, frame: Any):
201
+ exit("Tests stopped by user")
202
+
203
+ def _init_case_result(self, module_id: str, case_id: str):
204
+ if self._results.get(module_id) is None:
205
+ self._results[module_id] = {
206
+ "module_status": TestStatus.READY,
207
+ case_id: None,
208
+ }
209
+ else:
210
+ self._results[module_id][case_id] = None
211
+
212
+ def _collect_module_result(self, module_id: str):
213
+ if TestStatus.ERROR in self._results[module_id].values():
214
+ status = TestStatus.ERROR
215
+ elif TestStatus.FAILED in self._results[module_id].values():
216
+ status = TestStatus.FAILED
217
+ elif TestStatus.SKIPPED in self._results[module_id].values():
218
+ status = TestStatus.SKIPPED
219
+ else:
220
+ status = TestStatus.PASSED
221
+ self._results[module_id]["module_status"] = status
222
+ self._reporter.set_module_status(module_id, status)
223
+ self._reporter.set_module_stop_time(module_id)
224
+
225
+ def _get_run_status(self, exitstatus: int) -> RunStatus:
226
+ match exitstatus:
227
+ case ExitCode.OK:
228
+ return RunStatus.PASSED
229
+ case ExitCode.TESTS_FAILED:
230
+ return RunStatus.FAILED
231
+ case ExitCode.INTERRUPTED:
232
+ return RunStatus.STOPPED
233
+ case _:
234
+ return RunStatus.ERROR
235
+
236
+ def _decode_assertion_msg(self, msg: str) -> str | None:
237
+ assertion_str = "AssertionError: "
238
+
239
+ if assertion_str in msg:
240
+ index = msg.find(assertion_str)
241
+ report = msg[index + len(assertion_str) :]
242
+ index = report.find("\nE")
243
+ return report[:index]
244
+ return None
@@ -0,0 +1,218 @@
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
+
4
+ from os import environ
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+ from uuid import uuid4
8
+
9
+ from pycouchdb.exceptions import NotFound
10
+ from pydantic import ValidationError
11
+
12
+ from hardpy.pytest_hardpy.db import (
13
+ DatabaseField as DF,
14
+ ResultRunStore,
15
+ RunStore,
16
+ )
17
+ from hardpy.pytest_hardpy.utils import DuplicateSerialNumberError
18
+ from hardpy.pytest_hardpy.reporter import RunnerReporter
19
+
20
+
21
+ @dataclass
22
+ class CurrentTestInfo(object):
23
+ """Current test info."""
24
+
25
+ module_id: str
26
+ case_id: str
27
+
28
+
29
+ def get_current_report() -> ResultRunStore | None:
30
+ """Get current report from runstore database.
31
+
32
+ Returns:
33
+ ResultRunStore | None: report, or None if not found or invalid
34
+ """
35
+ runstore = RunStore()
36
+ try:
37
+ return runstore.get_document()
38
+ except NotFound:
39
+ return None
40
+ except ValidationError:
41
+ return None
42
+ except TypeError:
43
+ return None
44
+
45
+
46
+ def set_dut_info(info: dict):
47
+ """Add DUT info to document.
48
+
49
+ Args:
50
+ info (dict): DUT info
51
+ """
52
+ reporter = RunnerReporter()
53
+ for dut_key, dut_value in info.items():
54
+ key = reporter.generate_key(DF.DUT, DF.INFO, dut_key)
55
+ reporter.set_db_value(key, dut_value)
56
+
57
+
58
+ def set_dut_serial_number(serial_number: str):
59
+ """Add DUT serial number to document.
60
+
61
+ Args:
62
+ serial_number (str): DUT serial number
63
+
64
+ Raises:
65
+ DuplicateSerialNumberError: if serial number is already set
66
+ """
67
+ reporter = RunnerReporter()
68
+ key = reporter.generate_key(DF.DUT, DF.SERIAL_NUMBER)
69
+ if reporter.get_field(key):
70
+ raise DuplicateSerialNumberError
71
+ reporter.set_db_value(key, serial_number)
72
+
73
+
74
+ def set_stand_info(info: dict):
75
+ """Add test stand info to document.
76
+
77
+ Args:
78
+ info (dict): test stand info
79
+ """
80
+ reporter = RunnerReporter()
81
+ for stand_key, stand_value in info.items():
82
+ key = reporter.generate_key(DF.TEST_STAND, stand_key)
83
+ reporter.set_db_value(key, stand_value)
84
+
85
+
86
+ def set_message(msg: str, msg_key: Optional[str] = None) -> None:
87
+ """Add or update message in current test.
88
+
89
+ Args:
90
+ msg (str): Message content.
91
+ msg_key (Optional[str]): Message ID.
92
+ If not specified, a random ID will be generated.
93
+ """
94
+ current_test = _get_current_test()
95
+ reporter = RunnerReporter()
96
+
97
+ if msg_key is None:
98
+ msg_key = str(uuid4())
99
+
100
+ key = reporter.generate_key(
101
+ DF.MODULES,
102
+ current_test.module_id,
103
+ DF.CASES,
104
+ current_test.case_id,
105
+ DF.MSG,
106
+ )
107
+
108
+ msgs = reporter.get_field(key)
109
+ if msgs is None:
110
+ msgs = {}
111
+
112
+ msgs[msg_key] = msg
113
+
114
+ reporter.set_db_value(key, msgs)
115
+
116
+
117
+ def set_case_artifact(data: dict):
118
+ """Add data to current test case.
119
+
120
+ Artifact saves only in RunStore database
121
+ because state in StateStore and case artifact must be separated.
122
+
123
+ Args:
124
+ data (dict): data
125
+ """
126
+ current_test = _get_current_test()
127
+ reporter = RunnerReporter()
128
+ for stand_key, stand_value in data.items():
129
+ key = reporter.generate_key(
130
+ DF.MODULES,
131
+ current_test.module_id,
132
+ DF.CASES,
133
+ current_test.case_id,
134
+ DF.ARTIFACT,
135
+ stand_key,
136
+ )
137
+ reporter.set_db_value(key, stand_value, is_statestore=False)
138
+
139
+
140
+ def set_module_artifact(data: dict):
141
+ """Add data to current test module.
142
+
143
+ Artifact saves only in RunStore database
144
+ because state in StateStore and module artifact must be separated.
145
+
146
+ Args:
147
+ data (dict): data
148
+ """
149
+ current_test = _get_current_test()
150
+ reporter = RunnerReporter()
151
+ for artifact_key, artifact_value in data.items():
152
+ key = reporter.generate_key(
153
+ DF.MODULES,
154
+ current_test.module_id,
155
+ DF.ARTIFACT,
156
+ artifact_key,
157
+ )
158
+ reporter.set_db_value(key, artifact_value, is_statestore=False)
159
+
160
+
161
+ def set_run_artifact(data: dict):
162
+ """Add data to current test run.
163
+
164
+ Artifact saves only in RunStore database
165
+ because state in StateStore and run artifact must be separated.
166
+
167
+ Args:
168
+ data (dict): data
169
+ """
170
+ reporter = RunnerReporter()
171
+ for artifact_key, artifact_value in data.items():
172
+ key = reporter.generate_key(
173
+ DF.ARTIFACT,
174
+ artifact_key,
175
+ )
176
+ reporter.set_db_value(key, artifact_value, is_statestore=False)
177
+
178
+
179
+ def set_driver_info(drivers: dict) -> None:
180
+ """Adds or updates drivers data.
181
+
182
+ Driver data is stored in both StateStore and RunStore databases.
183
+
184
+ Args:
185
+ drivers (dict): A dictionary of drivers, where keys are driver names
186
+ and values are driver-specific data.
187
+ """
188
+ reporter = RunnerReporter()
189
+
190
+ for driver_name, driver_data in drivers.items():
191
+ key = reporter.generate_key(
192
+ DF.DRIVERS,
193
+ driver_name,
194
+ )
195
+ reporter.set_db_value(key, driver_data)
196
+
197
+
198
+ def _get_current_test() -> CurrentTestInfo:
199
+ current_node = environ.get("PYTEST_CURRENT_TEST")
200
+
201
+ if current_node is None:
202
+ raise RuntimeError("PYTEST_CURRENT_TEST variable is not set")
203
+
204
+ module_delimiter = ".py::"
205
+ module_id_end_index = current_node.find(module_delimiter)
206
+ module_id = current_node[:module_id_end_index]
207
+
208
+ folder_delimeter = "/"
209
+ folder_delimeter_index = current_node.rfind(folder_delimeter)
210
+ if folder_delimeter_index != -1:
211
+ module_id = module_id[folder_delimeter_index + 1 :]
212
+
213
+ case_with_stage = current_node[module_id_end_index + len(module_delimiter) :]
214
+ case_delimiter = " "
215
+ case_id_end_index = case_with_stage.find(case_delimiter)
216
+ case_id = case_with_stage[:case_id_end_index]
217
+
218
+ return CurrentTestInfo(module_id=module_id, case_id=case_id)
@@ -0,0 +1,117 @@
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
+
4
+ import sys
5
+ import signal
6
+ import subprocess
7
+ from platform import system
8
+
9
+ from hardpy.pytest_hardpy.utils.config_data import ConfigData
10
+
11
+
12
+ class PyTestWrapper(object):
13
+ """Wrapper for pytest subprocess."""
14
+
15
+ def __init__(self):
16
+ self._proc = None
17
+ self.python_executable = sys.executable
18
+
19
+ # Make sure test structure is stored in DB
20
+ # before clients come in
21
+ self.config = ConfigData()
22
+ self.collect()
23
+
24
+ def start(self):
25
+ """Start pytest subprocess.
26
+
27
+ Returns True if pytest was started.
28
+ """
29
+ if self.python_executable is None:
30
+ return False
31
+
32
+ if self.is_running():
33
+ return False
34
+
35
+ if system() == "Linux":
36
+ self._proc = subprocess.Popen( # noqa: S603
37
+ [
38
+ self.python_executable,
39
+ "-m",
40
+ "pytest",
41
+ "--hardpy-dbp",
42
+ str(self.config.db_port),
43
+ "--hardpy-dbh",
44
+ self.config.db_host,
45
+ "--hardpy-dbu",
46
+ self.config.db_user,
47
+ "--hardpy-dbpw",
48
+ self.config.db_pswd,
49
+ ],
50
+ cwd=self.config.tests_dir.absolute(),
51
+ )
52
+ elif system() == "Windows":
53
+ self._proc = subprocess.Popen( # noqa: S603
54
+ [
55
+ self.python_executable,
56
+ "-m",
57
+ "pytest",
58
+ "--hardpy-dbp",
59
+ str(self.config.db_port),
60
+ "--hardpy-dbh",
61
+ self.config.db_host,
62
+ "--hardpy-dbu",
63
+ self.config.db_user,
64
+ "--hardpy-dbpw",
65
+ self.config.db_pswd,
66
+ ],
67
+ cwd=self.config.tests_dir.absolute(),
68
+ creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
69
+ )
70
+
71
+ return True
72
+
73
+ def stop(self) -> bool:
74
+ """Stop pytest subprocess.
75
+
76
+ Returns True if pytest was running and stopped.
77
+ """
78
+ if self.is_running() and self._proc:
79
+ if system() == "Linux":
80
+ self._proc.terminate()
81
+ elif system() == "Windows":
82
+ self._proc.send_signal(signal.CTRL_BREAK_EVENT)
83
+ return True
84
+ return False
85
+
86
+ def collect(self) -> bool:
87
+ """Perform pytest collection.
88
+
89
+ Returns True if collection was started.
90
+ """
91
+ if self.python_executable is None:
92
+ return False
93
+
94
+ if self.is_running():
95
+ return False
96
+
97
+ subprocess.Popen( # noqa: S603
98
+ [
99
+ self.python_executable,
100
+ "-m" "pytest",
101
+ "--collect-only",
102
+ "--hardpy-dbp",
103
+ str(self.config.db_port),
104
+ "--hardpy-dbh",
105
+ self.config.db_host,
106
+ "--hardpy-dbu",
107
+ self.config.db_user,
108
+ "--hardpy-dbpw",
109
+ self.config.db_pswd,
110
+ ],
111
+ cwd=self.config.tests_dir.absolute(),
112
+ )
113
+ return True
114
+
115
+ def is_running(self) -> bool | None:
116
+ """Check if pytest is running."""
117
+ return self._proc and self._proc.poll() is None
@@ -0,0 +1,10 @@
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
+
4
+ from hardpy.pytest_hardpy.reporter.runner_reporter import RunnerReporter
5
+ from hardpy.pytest_hardpy.reporter.hook_reporter import HookReporter
6
+
7
+ __all__ = [
8
+ "RunnerReporter",
9
+ "HookReporter",
10
+ ]
@@ -0,0 +1,42 @@
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
+
4
+ from logging import getLogger
5
+
6
+ from hardpy.pytest_hardpy.db import StateStore, RunStore
7
+
8
+
9
+ class BaseReporter(object):
10
+ """Base class for test reporter."""
11
+
12
+ def __init__(self):
13
+ self._statestore = StateStore()
14
+ self._runstore = RunStore()
15
+ self._log = getLogger(__name__)
16
+
17
+ def set_db_value(self, key: str, value, is_statestore=True, is_runstore=True):
18
+ """Set value to database.
19
+
20
+ Args:
21
+ key (str): database key
22
+ value (_type_): database value
23
+ is_statestore (bool, optional): indicates whether data should
24
+ be written to StateStore. Defaults to True.
25
+ is_runstore (bool, optional): indicates whether data should
26
+ be written to RunStore. Defaults to True.
27
+ """
28
+ if is_statestore:
29
+ self._statestore.set_value(key, value)
30
+ if is_runstore:
31
+ self._runstore.set_value(key, value)
32
+
33
+ def generate_key(self, *args) -> str:
34
+ """Generate key for database.
35
+
36
+ Args:
37
+ args: list of database keys
38
+
39
+ Returns:
40
+ str: database key
41
+ """
42
+ return ".".join(args)