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,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,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_loader.couchdb_loader import (
5
+ CouchdbLoader,
6
+ )
7
+
8
+ __all__ = [
9
+ "CouchdbLoader",
10
+ ]
@@ -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"