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.
- hardpy/__init__.py +34 -0
- hardpy/hardpy_panel/__init__.py +0 -0
- hardpy/hardpy_panel/api.py +71 -0
- hardpy/hardpy_panel/frontend/dist/asset-manifest.json +36 -0
- hardpy/hardpy_panel/frontend/dist/favicon.ico +0 -0
- hardpy/hardpy_panel/frontend/dist/index.html +1 -0
- hardpy/hardpy_panel/frontend/dist/logo512.png +0 -0
- hardpy/hardpy_panel/frontend/dist/manifest.json +25 -0
- hardpy/hardpy_panel/frontend/dist/static/css/main.e8a862f1.css +2 -0
- hardpy/hardpy_panel/frontend/dist/static/css/main.e8a862f1.css.map +1 -0
- hardpy/hardpy_panel/frontend/dist/static/js/808.ce070002.chunk.js +2 -0
- hardpy/hardpy_panel/frontend/dist/static/js/808.ce070002.chunk.js.map +1 -0
- hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-16px-paths.d605910e.chunk.js +2 -0
- hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-16px-paths.d605910e.chunk.js.map +1 -0
- hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-20px-paths.7ee05cc8.chunk.js +2 -0
- hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-20px-paths.7ee05cc8.chunk.js.map +1 -0
- hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-all-paths-loader.0aa89747.chunk.js +2 -0
- hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-all-paths-loader.0aa89747.chunk.js.map +1 -0
- hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-all-paths.f63155c9.chunk.js +2 -0
- hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-all-paths.f63155c9.chunk.js.map +1 -0
- hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-split-paths-by-size-loader.52a072d3.chunk.js +2 -0
- hardpy/hardpy_panel/frontend/dist/static/js/blueprint-icons-split-paths-by-size-loader.52a072d3.chunk.js.map +1 -0
- hardpy/hardpy_panel/frontend/dist/static/js/main.8ef63e9b.js +3 -0
- hardpy/hardpy_panel/frontend/dist/static/js/main.8ef63e9b.js.LICENSE.txt +90 -0
- hardpy/hardpy_panel/frontend/dist/static/js/main.8ef63e9b.js.map +1 -0
- hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-16.520846c6beb41df528c8.eot +0 -0
- hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-16.5c52b39c697f2323ce8b.svg +1806 -0
- hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-16.84db1772f4bfb529f64f.woff +0 -0
- hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-16.b67ee1736e20e37a3225.woff2 +0 -0
- hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-16.e02ecf515378db143652.ttf +0 -0
- hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-20.429cacb8accf72488451.ttf +0 -0
- hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-20.6ae3791ee2d86fc228a6.svg +1806 -0
- hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-20.8cecf62de42997e4d82f.woff2 +0 -0
- hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-20.afbadb627d43b7857223.eot +0 -0
- hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-20.e857f5a5132b8bfa71a1.woff +0 -0
- hardpy/hardpy_panel/frontend/dist/static/media/logo_smol.5b16f92447a4a9e80331.png +0 -0
- hardpy/hardpy_panel/runner.py +52 -0
- hardpy/pytest_hardpy/__init__.py +0 -0
- hardpy/pytest_hardpy/db/__init__.py +18 -0
- hardpy/pytest_hardpy/db/base_connector.py +24 -0
- hardpy/pytest_hardpy/db/base_server.py +14 -0
- hardpy/pytest_hardpy/db/base_store.py +88 -0
- hardpy/pytest_hardpy/db/const.py +25 -0
- hardpy/pytest_hardpy/db/runstore.py +30 -0
- hardpy/pytest_hardpy/db/schema.py +292 -0
- hardpy/pytest_hardpy/db/statestore.py +19 -0
- hardpy/pytest_hardpy/plugin.py +244 -0
- hardpy/pytest_hardpy/pytest_call.py +218 -0
- hardpy/pytest_hardpy/pytest_wrapper.py +117 -0
- hardpy/pytest_hardpy/reporter/__init__.py +10 -0
- hardpy/pytest_hardpy/reporter/base.py +42 -0
- hardpy/pytest_hardpy/reporter/hook_reporter.py +307 -0
- hardpy/pytest_hardpy/reporter/runner_reporter.py +29 -0
- hardpy/pytest_hardpy/result/__init__.py +10 -0
- hardpy/pytest_hardpy/result/couchdb_config.py +22 -0
- hardpy/pytest_hardpy/result/report_loader/__init__.py +10 -0
- hardpy/pytest_hardpy/result/report_loader/couchdb_loader.py +62 -0
- hardpy/pytest_hardpy/result/report_reader/__init__.py +0 -0
- hardpy/pytest_hardpy/result/report_reader/couchdb_reader.py +164 -0
- hardpy/pytest_hardpy/utils/__init__.py +19 -0
- hardpy/pytest_hardpy/utils/config_data.py +31 -0
- hardpy/pytest_hardpy/utils/const.py +29 -0
- hardpy/pytest_hardpy/utils/exception.py +16 -0
- hardpy/pytest_hardpy/utils/node_info.py +59 -0
- hardpy/pytest_hardpy/utils/progress_calculator.py +38 -0
- hardpy/pytest_hardpy/utils/singleton.py +23 -0
- hardpy-0.1.0.dist-info/METADATA +129 -0
- hardpy-0.1.0.dist-info/RECORD +71 -0
- hardpy-0.1.0.dist-info/WHEEL +4 -0
- hardpy-0.1.0.dist-info/entry_points.txt +5 -0
- 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)
|