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,307 @@
|
|
|
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 time import time, tzname
|
|
5
|
+
from logging import getLogger
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
|
|
8
|
+
from natsort import natsorted
|
|
9
|
+
|
|
10
|
+
from hardpy.pytest_hardpy.db import DatabaseField as DF
|
|
11
|
+
from hardpy.pytest_hardpy.reporter.base import BaseReporter
|
|
12
|
+
from hardpy.pytest_hardpy.utils import TestStatus, RunStatus, NodeInfo
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HookReporter(BaseReporter):
|
|
16
|
+
"""Reporter for using in the hook HardPy plugin's hooks."""
|
|
17
|
+
|
|
18
|
+
def __init__(self): # noqa: WPS612
|
|
19
|
+
super().__init__()
|
|
20
|
+
self._log = getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
def init_doc(self, doc_name: str):
|
|
23
|
+
"""Initialize document.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
doc_name (str): test run name
|
|
27
|
+
"""
|
|
28
|
+
self.set_db_value(DF.NAME, doc_name)
|
|
29
|
+
self.set_db_value(DF.STATUS, TestStatus.READY)
|
|
30
|
+
self.set_db_value(DF.START_TIME, None)
|
|
31
|
+
self.set_db_value(DF.TIMEZONE, None)
|
|
32
|
+
self.set_db_value(DF.STOP_TIME, None)
|
|
33
|
+
self.set_db_value(DF.PROGRESS, 0)
|
|
34
|
+
self.set_db_value(DF.DRIVERS, {})
|
|
35
|
+
self.set_db_value(DF.ARTIFACT, {}, is_statestore=False)
|
|
36
|
+
|
|
37
|
+
def start(self):
|
|
38
|
+
"""Start test."""
|
|
39
|
+
self._log.debug("Starting test run.")
|
|
40
|
+
start_time = int(time())
|
|
41
|
+
self.set_db_value(DF.START_TIME, start_time)
|
|
42
|
+
self.set_db_value(DF.STATUS, TestStatus.RUN)
|
|
43
|
+
self.set_db_value(DF.TIMEZONE, tzname) # noqa: WPS432
|
|
44
|
+
self.set_db_value(DF.PROGRESS, 0)
|
|
45
|
+
|
|
46
|
+
def finish(self, status: RunStatus):
|
|
47
|
+
"""Finish test.
|
|
48
|
+
|
|
49
|
+
This method must be called at the end of test run.
|
|
50
|
+
"""
|
|
51
|
+
self._log.debug("Finishing test run.")
|
|
52
|
+
stop_time = int(time())
|
|
53
|
+
self.set_db_value(DF.STOP_TIME, stop_time)
|
|
54
|
+
self.set_db_value(DF.STATUS, status)
|
|
55
|
+
|
|
56
|
+
if self._statestore.get_document():
|
|
57
|
+
self._log.debug("Report StateStore has been successfully validated.")
|
|
58
|
+
|
|
59
|
+
if self._runstore.get_document():
|
|
60
|
+
self._log.debug("Report RunStore has been successfully validated.")
|
|
61
|
+
|
|
62
|
+
self._statestore.compact()
|
|
63
|
+
self._runstore.compact()
|
|
64
|
+
|
|
65
|
+
def set_progress(self, progress: int):
|
|
66
|
+
"""Set test progress.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
progress (int): test progress
|
|
70
|
+
"""
|
|
71
|
+
self.set_db_value(DF.PROGRESS, progress)
|
|
72
|
+
|
|
73
|
+
def set_assertion_msg(self, module_id: str, case_id: str, msg: str | None):
|
|
74
|
+
"""Set case assertion message.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
module_id (str): test module id
|
|
78
|
+
case_id (str): test case id
|
|
79
|
+
msg (str): assertion message
|
|
80
|
+
"""
|
|
81
|
+
key = self.generate_key(
|
|
82
|
+
DF.MODULES, module_id, DF.CASES, case_id, DF.ASSERTION_MSG
|
|
83
|
+
)
|
|
84
|
+
self.set_db_value(key, msg)
|
|
85
|
+
|
|
86
|
+
def add_case(self, node_info: NodeInfo):
|
|
87
|
+
"""Add test case to document.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
node_info (NodeInfo): node info
|
|
91
|
+
"""
|
|
92
|
+
key = DF.MODULES
|
|
93
|
+
item_statestore = self._statestore.get_field(key)
|
|
94
|
+
item_runstore = self._runstore.get_field(key)
|
|
95
|
+
|
|
96
|
+
new_item_statestore = self._init_case(item_statestore, node_info)
|
|
97
|
+
new_item_runstore = self._init_case(
|
|
98
|
+
item_runstore,
|
|
99
|
+
node_info,
|
|
100
|
+
is_use_artifact=True,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
self.set_db_value(key, new_item_statestore, is_runstore=False)
|
|
104
|
+
self.set_db_value(key, new_item_runstore, is_statestore=False)
|
|
105
|
+
|
|
106
|
+
def set_case_status(self, module_id: str, case_id: str, status: TestStatus):
|
|
107
|
+
"""Set test case status.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
module_id (str): module id
|
|
111
|
+
case_id (str): case id
|
|
112
|
+
status (TestStatus): test case status
|
|
113
|
+
"""
|
|
114
|
+
key = self.generate_key(DF.MODULES, module_id, DF.CASES, case_id, DF.STATUS)
|
|
115
|
+
self.set_db_value(key, status)
|
|
116
|
+
|
|
117
|
+
def set_case_start_time(self, module_id: str, case_id: str):
|
|
118
|
+
"""Set test case start_time.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
module_id (str): module id
|
|
122
|
+
case_id (str): case id
|
|
123
|
+
"""
|
|
124
|
+
key = self.generate_key(DF.MODULES, module_id, DF.CASES, case_id, DF.START_TIME)
|
|
125
|
+
self._set_time(key)
|
|
126
|
+
|
|
127
|
+
def set_case_stop_time(self, module_id: str, case_id: str):
|
|
128
|
+
"""Set test case start_time.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
module_id (str): module id
|
|
132
|
+
case_id (str): case id
|
|
133
|
+
"""
|
|
134
|
+
key = self.generate_key(DF.MODULES, module_id, DF.CASES, case_id, DF.STOP_TIME)
|
|
135
|
+
self._set_time(key)
|
|
136
|
+
|
|
137
|
+
def set_module_status(self, module_id: str, status: TestStatus):
|
|
138
|
+
"""Set test module status.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
module_id (str): module id
|
|
142
|
+
status (TestStatus): test module status
|
|
143
|
+
"""
|
|
144
|
+
key = self.generate_key(DF.MODULES, module_id, DF.STATUS)
|
|
145
|
+
self.set_db_value(key, status)
|
|
146
|
+
|
|
147
|
+
def set_module_start_time(self, module_id: str):
|
|
148
|
+
"""Set test module status.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
module_id (str): module id
|
|
152
|
+
"""
|
|
153
|
+
key = self.generate_key(DF.MODULES, module_id, DF.START_TIME)
|
|
154
|
+
self._set_time(key)
|
|
155
|
+
|
|
156
|
+
def set_module_stop_time(self, module_id: str):
|
|
157
|
+
"""Set test module status.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
module_id (str): module id
|
|
161
|
+
"""
|
|
162
|
+
key = self.generate_key(DF.MODULES, module_id, DF.STOP_TIME)
|
|
163
|
+
self._set_time(key)
|
|
164
|
+
|
|
165
|
+
def update_node_order(self, nodes: dict) -> None:
|
|
166
|
+
"""Update node order.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
nodes (dict): modules and cases.
|
|
170
|
+
"""
|
|
171
|
+
key = DF.MODULES
|
|
172
|
+
old_modules = self._statestore.get_field(key)
|
|
173
|
+
modules_copy = deepcopy(old_modules)
|
|
174
|
+
|
|
175
|
+
rm_outdated_nodes = self._remove_outdate_node(old_modules, modules_copy, nodes)
|
|
176
|
+
updated_case_order = self._update_case_order(rm_outdated_nodes, nodes)
|
|
177
|
+
updated_module_order = self._update_module_order(updated_case_order)
|
|
178
|
+
self.set_db_value(key, updated_module_order, is_runstore=False)
|
|
179
|
+
|
|
180
|
+
def _set_time(self, key: str):
|
|
181
|
+
current_time = self._statestore.get_field(key)
|
|
182
|
+
if current_time is None:
|
|
183
|
+
self.set_db_value(key, int(time()))
|
|
184
|
+
|
|
185
|
+
def _init_case(
|
|
186
|
+
self, item: dict, node_info: NodeInfo, is_use_artifact: bool = False
|
|
187
|
+
) -> dict:
|
|
188
|
+
module_default = { # noqa: WPS204
|
|
189
|
+
DF.STATUS: TestStatus.READY,
|
|
190
|
+
DF.NAME: self._get_module_name(node_info),
|
|
191
|
+
DF.START_TIME: None,
|
|
192
|
+
DF.STOP_TIME: None,
|
|
193
|
+
DF.CASES: {},
|
|
194
|
+
}
|
|
195
|
+
case_default = {
|
|
196
|
+
DF.STATUS: TestStatus.READY,
|
|
197
|
+
DF.NAME: self._get_case_name(node_info),
|
|
198
|
+
DF.START_TIME: None,
|
|
199
|
+
DF.STOP_TIME: None,
|
|
200
|
+
DF.ASSERTION_MSG: None,
|
|
201
|
+
DF.MSG: None,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if item.get(node_info.module_id) is None: # noqa: WPS204
|
|
205
|
+
if is_use_artifact:
|
|
206
|
+
module_default[DF.ARTIFACT] = {}
|
|
207
|
+
item[node_info.module_id] = module_default # noqa: WPS204
|
|
208
|
+
else:
|
|
209
|
+
item[node_info.module_id][DF.STATUS] = TestStatus.READY
|
|
210
|
+
item[node_info.module_id][DF.NAME] = self._get_module_name(node_info)
|
|
211
|
+
item[node_info.module_id][DF.START_TIME] = None
|
|
212
|
+
item[node_info.module_id][DF.STOP_TIME] = None
|
|
213
|
+
item[node_info.module_id][DF.NAME] = self._get_module_name(node_info)
|
|
214
|
+
|
|
215
|
+
if is_use_artifact:
|
|
216
|
+
case_default[DF.ARTIFACT] = {}
|
|
217
|
+
item[node_info.module_id][DF.CASES][node_info.case_id] = case_default
|
|
218
|
+
|
|
219
|
+
return item
|
|
220
|
+
|
|
221
|
+
def _remove_outdate_node(
|
|
222
|
+
self, old_modules: dict, new_modules: dict, nodes: dict
|
|
223
|
+
) -> dict:
|
|
224
|
+
"""Remove outdated nodes from StateStore database.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
old_modules (dict): list of modules and cases.
|
|
228
|
+
new_modules (dict): list of modules and cases.
|
|
229
|
+
nodes (dict): modules and cases.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
dict: list of modules and cases.
|
|
233
|
+
"""
|
|
234
|
+
for module_id, module in old_modules.items():
|
|
235
|
+
# remove outdated modules
|
|
236
|
+
if module_id not in nodes:
|
|
237
|
+
new_modules.pop(module_id)
|
|
238
|
+
continue
|
|
239
|
+
# remove outdated cases in module
|
|
240
|
+
for case_id in module[DF.CASES]:
|
|
241
|
+
if case_id not in nodes[module_id]:
|
|
242
|
+
new_modules[module_id][DF.CASES].pop(case_id)
|
|
243
|
+
|
|
244
|
+
return new_modules
|
|
245
|
+
|
|
246
|
+
def _update_case_order(self, modules: dict, nodes: dict) -> dict:
|
|
247
|
+
"""Update test order for StateStore database.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
modules (dict): list of modules and cases.
|
|
251
|
+
nodes (dict): modules and cases.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
dict: list of modules and cases.
|
|
255
|
+
"""
|
|
256
|
+
for module_id, module in modules.items():
|
|
257
|
+
nodes_order = nodes.get(module_id, [])
|
|
258
|
+
modules_order = list(module.get(DF.CASES).keys())
|
|
259
|
+
if nodes_order != modules_order:
|
|
260
|
+
# sort cases in module
|
|
261
|
+
sorted_cases = {}
|
|
262
|
+
for case_name in nodes_order:
|
|
263
|
+
case_data = module[DF.CASES].get(case_name, {})
|
|
264
|
+
sorted_cases[case_name] = case_data
|
|
265
|
+
module[DF.CASES] = sorted_cases
|
|
266
|
+
|
|
267
|
+
return modules
|
|
268
|
+
|
|
269
|
+
def _update_module_order(self, modules: dict) -> dict:
|
|
270
|
+
"""Update test order for StateStore database.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
modules (dict): list of modules and cases.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
dict: list of modules and cases.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
sorted_modules = natsorted(modules.items(), key=lambda item: item[0])
|
|
280
|
+
|
|
281
|
+
new_modules = {}
|
|
282
|
+
for module_id, module in sorted_modules:
|
|
283
|
+
new_modules[module_id] = module
|
|
284
|
+
|
|
285
|
+
return new_modules
|
|
286
|
+
|
|
287
|
+
def _get_module_name(self, node_info) -> str:
|
|
288
|
+
"""Get module name from markers or use default.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
node_info (NodeInfo): node info
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
str: module name
|
|
295
|
+
"""
|
|
296
|
+
return node_info.module_name if node_info.module_name else node_info.module_id
|
|
297
|
+
|
|
298
|
+
def _get_case_name(self, node_info) -> str:
|
|
299
|
+
"""Get case name from markers or use default.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
node_info (NodeInfo): node info
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
str: case name
|
|
306
|
+
"""
|
|
307
|
+
return node_info.case_name if node_info.case_name else node_info.case_id
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from hardpy.pytest_hardpy.reporter.base import BaseReporter
|
|
8
|
+
from hardpy.pytest_hardpy.utils import Singleton
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RunnerReporter(Singleton, BaseReporter):
|
|
12
|
+
"""Reporter for using in direct call from test runner with HardPy plugin."""
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
if not self._initialized:
|
|
16
|
+
super().__init__()
|
|
17
|
+
self._log = getLogger(__name__)
|
|
18
|
+
self._initialized = True
|
|
19
|
+
|
|
20
|
+
def get_field(self, key: str) -> Any:
|
|
21
|
+
"""Get field from the statestore.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
key (str): field name
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Any: field value
|
|
28
|
+
"""
|
|
29
|
+
return self._statestore.get_field(key)
|
|
@@ -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.result.report_reader.couchdb_reader import CouchdbReader
|
|
5
|
+
from hardpy.pytest_hardpy.result.report_loader.couchdb_loader import CouchdbLoader
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"CouchdbReader",
|
|
9
|
+
"CouchdbLoader",
|
|
10
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
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 dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class CouchdbConfig: # noqa: WPS306
|
|
9
|
+
"""CouchDB loader config."""
|
|
10
|
+
|
|
11
|
+
db_name: str = "report"
|
|
12
|
+
user: str = "dev"
|
|
13
|
+
password: str = "dev"
|
|
14
|
+
host: str = "localhost"
|
|
15
|
+
port: int = 5984
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def connection_string(self) -> str:
|
|
19
|
+
"""Get couchdb connection string."""
|
|
20
|
+
credentials = f"{self.user}:{self.password}"
|
|
21
|
+
uri = f"{self.host}:{str(self.port)}"
|
|
22
|
+
return f"http://{credentials}@{uri}/"
|
|
@@ -0,0 +1,62 @@
|
|
|
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 pycouchdb import Server as DbServer
|
|
7
|
+
from pycouchdb.exceptions import Conflict
|
|
8
|
+
from pycouchdb.client import Database
|
|
9
|
+
|
|
10
|
+
from hardpy.pytest_hardpy.db.schema import ResultRunStore
|
|
11
|
+
from hardpy.pytest_hardpy.result.couchdb_config import CouchdbConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CouchdbLoader(object):
|
|
15
|
+
"""CouchDB report generator."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, config: CouchdbConfig):
|
|
18
|
+
self._log = getLogger(__name__)
|
|
19
|
+
self._config: CouchdbConfig = config
|
|
20
|
+
self._db_srv = DbServer(config.connection_string)
|
|
21
|
+
self._db: Database = self._init_db()
|
|
22
|
+
|
|
23
|
+
def load(self, report: ResultRunStore) -> bool:
|
|
24
|
+
"""Load report to the report database.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
report (ResultRunStore): report
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
bool: True if success, else False
|
|
31
|
+
"""
|
|
32
|
+
report_id = self._get_report_id(report)
|
|
33
|
+
report_dict = self._schema_to_dict(report, report_id)
|
|
34
|
+
try:
|
|
35
|
+
self._db.save(report_dict)
|
|
36
|
+
except Conflict as exc:
|
|
37
|
+
self._log.error(f"Error while saving report {report_id}: {exc}")
|
|
38
|
+
return False
|
|
39
|
+
self._log.debug(f"Report saved with id: {report_id}")
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
def _init_db(self) -> Database:
|
|
43
|
+
try:
|
|
44
|
+
return self._db_srv.create(self._config.db_name) # type: ignore
|
|
45
|
+
except Conflict:
|
|
46
|
+
# database is already created
|
|
47
|
+
return self._db_srv.database(self._config.db_name)
|
|
48
|
+
|
|
49
|
+
def _get_report_id(self, report: ResultRunStore) -> str:
|
|
50
|
+
timestamp = report.stop_time
|
|
51
|
+
device_serial_number = report.dut.serial_number
|
|
52
|
+
if not device_serial_number:
|
|
53
|
+
self._log.warning("Device serial number is not provided in the report.")
|
|
54
|
+
return f"report_{timestamp}"
|
|
55
|
+
return f"report_{timestamp}_{device_serial_number}"
|
|
56
|
+
|
|
57
|
+
def _schema_to_dict(self, report: ResultRunStore, report_id: str) -> dict:
|
|
58
|
+
report_dict = report.model_dump()
|
|
59
|
+
report_dict.pop("rev")
|
|
60
|
+
report_dict.pop("id")
|
|
61
|
+
report_dict["_id"] = report_id
|
|
62
|
+
return report_dict
|
|
File without changes
|
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
from pycouchdb import Server as DbServer
|
|
9
|
+
from pycouchdb.client import Database
|
|
10
|
+
from pycouchdb.exceptions import NotFound
|
|
11
|
+
|
|
12
|
+
from hardpy.pytest_hardpy.db import DatabaseField as DF
|
|
13
|
+
from hardpy.pytest_hardpy.utils.const import TestStatus
|
|
14
|
+
from hardpy.pytest_hardpy.result.couchdb_config import CouchdbConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ReportInfo(object):
|
|
19
|
+
"""CouchDB report info."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
status: str
|
|
23
|
+
start_time: str
|
|
24
|
+
end_time: str
|
|
25
|
+
first_failed_test_name: Optional[str]
|
|
26
|
+
first_failed_test_id: Optional[str]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CouchdbReader(object):
|
|
30
|
+
"""CouchDB report info reader."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, config: CouchdbConfig):
|
|
33
|
+
self._log = getLogger(__name__)
|
|
34
|
+
self._config = config
|
|
35
|
+
self._db_srv = DbServer(config.connection_string)
|
|
36
|
+
self._db: Database = self._init_db()
|
|
37
|
+
self._doc_id = "doc"
|
|
38
|
+
|
|
39
|
+
def get_report_total_count(self) -> int:
|
|
40
|
+
"""Get the total number of reports in the database.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
int: total number of reports
|
|
44
|
+
"""
|
|
45
|
+
return sum(1 for _ in self._db.all())
|
|
46
|
+
|
|
47
|
+
def get_report_count_in_timeframe(self, start_time: int, end_time: int) -> int:
|
|
48
|
+
"""Get the number of reports in the database within the specified timeframe.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
start_time (int): start time
|
|
52
|
+
end_time (int): end time
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: if start time or end time is negative
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
int: number of reports
|
|
59
|
+
"""
|
|
60
|
+
if start_time < 0 or end_time < 0:
|
|
61
|
+
raise ValueError("Start time and end time must be positive values")
|
|
62
|
+
return sum(
|
|
63
|
+
1
|
|
64
|
+
for report in self._db.all()
|
|
65
|
+
if self._is_in_timeframe(
|
|
66
|
+
self._get_start_time_from_db(report[self._doc_id]),
|
|
67
|
+
self._get_stop_time_from_db(report[self._doc_id]),
|
|
68
|
+
start_time,
|
|
69
|
+
end_time,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def get_report_status(self, report_name: str) -> str:
|
|
74
|
+
"""Get the status of a report by its name.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
report_name (str): report name
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
ValueError: if the report status is not valid
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
str: report status
|
|
84
|
+
"""
|
|
85
|
+
doc = self._db.get(report_name)
|
|
86
|
+
status = doc[DF.STATUS]
|
|
87
|
+
if status not in {TestStatus.PASSED, TestStatus.FAILED, TestStatus.SKIPPED}:
|
|
88
|
+
raise ValueError("Invalid report status")
|
|
89
|
+
return status
|
|
90
|
+
|
|
91
|
+
def get_report_infos(self) -> List[ReportInfo]:
|
|
92
|
+
"""Get a list of information about all reports in the database.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
List[ReportInfo]: list of report information
|
|
96
|
+
"""
|
|
97
|
+
reports_info = []
|
|
98
|
+
reports = self._db.all()
|
|
99
|
+
for report in reports:
|
|
100
|
+
report_info = self._get_single_report_info(report)
|
|
101
|
+
reports_info.append(report_info)
|
|
102
|
+
return reports_info
|
|
103
|
+
|
|
104
|
+
def get_report_infos_in_timeframe(
|
|
105
|
+
self, start_time: int, end_time: int
|
|
106
|
+
) -> List[ReportInfo]:
|
|
107
|
+
"""Get a list of information about reports in a timeframe in the database.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
start_time (int): start time
|
|
111
|
+
end_time (int): end time
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
ValueError: if start time or end time is negative
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List[ReportInfo]: list of report information
|
|
118
|
+
"""
|
|
119
|
+
if start_time < 0 or end_time < 0:
|
|
120
|
+
raise ValueError("Start time and end time must be positive values")
|
|
121
|
+
|
|
122
|
+
reports_info = []
|
|
123
|
+
reports = self._db.all()
|
|
124
|
+
for report in reports:
|
|
125
|
+
start_t_db = self._get_start_time_from_db(report[self._doc_id])
|
|
126
|
+
stop_t_db = self._get_stop_time_from_db(report[self._doc_id])
|
|
127
|
+
if self._is_in_timeframe(start_t_db, stop_t_db, start_time, end_time):
|
|
128
|
+
report_info = self._get_single_report_info(report)
|
|
129
|
+
reports_info.append(report_info)
|
|
130
|
+
return reports_info
|
|
131
|
+
|
|
132
|
+
def _init_db(self) -> Database:
|
|
133
|
+
try:
|
|
134
|
+
return self._db_srv.database(self._config.db_name)
|
|
135
|
+
except NotFound as exc:
|
|
136
|
+
self._log.error(f"Error initializing database: {exc}")
|
|
137
|
+
raise RuntimeError("Error initializing database") from exc
|
|
138
|
+
|
|
139
|
+
def _get_start_time_from_db(self, doc: dict) -> str:
|
|
140
|
+
return doc[DF.START_TIME]
|
|
141
|
+
|
|
142
|
+
def _get_stop_time_from_db(self, doc: dict) -> str:
|
|
143
|
+
return doc[DF.STOP_TIME]
|
|
144
|
+
|
|
145
|
+
def _get_single_report_info(self, report: dict) -> ReportInfo:
|
|
146
|
+
first_failed_test_name = None
|
|
147
|
+
first_failed_test_id = None
|
|
148
|
+
report_doc = report[self._doc_id]
|
|
149
|
+
for _module_name, module_info in report_doc[DF.MODULES].items():
|
|
150
|
+
for case_name, case_info in module_info[DF.CASES].items():
|
|
151
|
+
if case_info[DF.STATUS] == TestStatus.FAILED:
|
|
152
|
+
first_failed_test_name = case_info[DF.NAME]
|
|
153
|
+
first_failed_test_id = case_name
|
|
154
|
+
return ReportInfo(
|
|
155
|
+
name=report_doc["_id"],
|
|
156
|
+
status=report_doc[DF.STATUS],
|
|
157
|
+
start_time=self._get_start_time_from_db(report_doc),
|
|
158
|
+
end_time=self._get_stop_time_from_db(report_doc),
|
|
159
|
+
first_failed_test_name=first_failed_test_name,
|
|
160
|
+
first_failed_test_id=first_failed_test_id,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def _is_in_timeframe(self, start, end, timeframe_start, timeframe_end):
|
|
164
|
+
return timeframe_start <= start and end <= timeframe_end
|
|
@@ -0,0 +1,19 @@
|
|
|
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.utils.node_info import NodeInfo
|
|
5
|
+
from hardpy.pytest_hardpy.utils.progress_calculator import ProgressCalculator
|
|
6
|
+
from hardpy.pytest_hardpy.utils.const import TestStatus, RunStatus
|
|
7
|
+
from hardpy.pytest_hardpy.utils.singleton import Singleton
|
|
8
|
+
from hardpy.pytest_hardpy.utils.config_data import ConfigData
|
|
9
|
+
from hardpy.pytest_hardpy.utils.exception import DuplicateSerialNumberError
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"NodeInfo",
|
|
13
|
+
"ProgressCalculator",
|
|
14
|
+
"TestStatus",
|
|
15
|
+
"RunStatus",
|
|
16
|
+
"Singleton",
|
|
17
|
+
"ConfigData",
|
|
18
|
+
"DuplicateSerialNumberError",
|
|
19
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
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.utils.singleton import Singleton
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConfigData(Singleton):
|
|
9
|
+
"""Web connection data storage."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
if not self._initialized:
|
|
13
|
+
self.db_host: str = "localhost"
|
|
14
|
+
self.db_user: str = "dev"
|
|
15
|
+
self.db_pswd: str = "dev"
|
|
16
|
+
self.db_port: int = 5984
|
|
17
|
+
self.web_host: str = "0.0.0.0"
|
|
18
|
+
self.web_port: int = 8000
|
|
19
|
+
self.tests_dir = Path.cwd()
|
|
20
|
+
self._initialized = True
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def connection_string(self) -> str:
|
|
24
|
+
"""Get couchdb connection string.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
str: couchdb connection string
|
|
28
|
+
"""
|
|
29
|
+
credentials = f"{self.db_user}:{self.db_pswd}"
|
|
30
|
+
uri = f"{self.db_host}:{str(self.db_port)}" # noqa: WPS237
|
|
31
|
+
return f"http://{credentials}@{uri}/"
|
|
@@ -0,0 +1,29 @@
|
|
|
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 enum import Enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestStatus(str, Enum): # noqa: WPS600
|
|
8
|
+
"""Pytest test status."""
|
|
9
|
+
|
|
10
|
+
PASSED = "passed"
|
|
11
|
+
FAILED = "failed"
|
|
12
|
+
SKIPPED = "skipped"
|
|
13
|
+
ERROR = "error"
|
|
14
|
+
RUN = "run"
|
|
15
|
+
READY = "ready"
|
|
16
|
+
STOPPED = "stopped"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RunStatus(str, Enum): # noqa: WPS600
|
|
20
|
+
"""Pytest run status."""
|
|
21
|
+
|
|
22
|
+
PASSED = "passed"
|
|
23
|
+
FAILED = "failed"
|
|
24
|
+
STOPPED = "stopped"
|
|
25
|
+
STARTED = "started"
|
|
26
|
+
COLLECTED = "collected"
|
|
27
|
+
BUSY = "busy"
|
|
28
|
+
READY = "ready"
|
|
29
|
+
ERROR = "error"
|