hardpy 0.6.1__py3-none-any.whl → 0.7.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 +28 -26
- hardpy/cli/cli.py +8 -8
- hardpy/cli/template.py +6 -6
- hardpy/common/config.py +15 -14
- hardpy/hardpy_panel/api.py +9 -9
- hardpy/hardpy_panel/frontend/dist/asset-manifest.json +3 -3
- hardpy/hardpy_panel/frontend/dist/index.html +1 -1
- hardpy/hardpy_panel/frontend/dist/static/js/main.942e57d4.js +3 -0
- hardpy/hardpy_panel/frontend/dist/static/js/main.942e57d4.js.map +1 -0
- hardpy/pytest_hardpy/db/__init__.py +3 -4
- hardpy/pytest_hardpy/db/base_connector.py +6 -5
- hardpy/pytest_hardpy/db/base_server.py +1 -1
- hardpy/pytest_hardpy/db/base_store.py +14 -9
- hardpy/pytest_hardpy/db/const.py +3 -1
- hardpy/pytest_hardpy/db/runstore.py +13 -15
- hardpy/pytest_hardpy/db/schema/__init__.py +9 -0
- hardpy/pytest_hardpy/db/{schema.py → schema/v1.py} +120 -79
- hardpy/pytest_hardpy/db/statestore.py +8 -10
- hardpy/pytest_hardpy/plugin.py +103 -48
- hardpy/pytest_hardpy/pytest_call.py +75 -30
- hardpy/pytest_hardpy/pytest_wrapper.py +8 -7
- hardpy/pytest_hardpy/reporter/__init__.py +1 -1
- hardpy/pytest_hardpy/reporter/base.py +32 -7
- hardpy/pytest_hardpy/reporter/hook_reporter.py +65 -37
- hardpy/pytest_hardpy/reporter/runner_reporter.py +6 -8
- hardpy/pytest_hardpy/result/__init__.py +1 -1
- hardpy/pytest_hardpy/result/couchdb_config.py +20 -16
- hardpy/pytest_hardpy/result/report_loader/couchdb_loader.py +2 -2
- hardpy/pytest_hardpy/result/report_reader/couchdb_reader.py +36 -20
- hardpy/pytest_hardpy/utils/__init__.py +20 -19
- hardpy/pytest_hardpy/utils/connection_data.py +6 -8
- hardpy/pytest_hardpy/utils/const.py +1 -1
- hardpy/pytest_hardpy/utils/dialog_box.py +34 -22
- hardpy/pytest_hardpy/utils/exception.py +8 -8
- hardpy/pytest_hardpy/utils/machineid.py +15 -0
- hardpy/pytest_hardpy/utils/node_info.py +45 -16
- hardpy/pytest_hardpy/utils/progress_calculator.py +4 -3
- hardpy/pytest_hardpy/utils/singleton.py +23 -16
- {hardpy-0.6.1.dist-info → hardpy-0.7.0.dist-info}/METADATA +19 -28
- {hardpy-0.6.1.dist-info → hardpy-0.7.0.dist-info}/RECORD +44 -42
- hardpy/hardpy_panel/frontend/dist/static/js/main.7c954faf.js +0 -3
- hardpy/hardpy_panel/frontend/dist/static/js/main.7c954faf.js.map +0 -1
- /hardpy/hardpy_panel/frontend/dist/static/js/{main.7c954faf.js.LICENSE.txt → main.942e57d4.js.LICENSE.txt} +0 -0
- {hardpy-0.6.1.dist-info → hardpy-0.7.0.dist-info}/WHEEL +0 -0
- {hardpy-0.6.1.dist-info → hardpy-0.7.0.dist-info}/entry_points.txt +0 -0
- {hardpy-0.6.1.dist-info → hardpy-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,21 +2,30 @@
|
|
|
2
2
|
# GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
3
|
|
|
4
4
|
from logging import getLogger
|
|
5
|
+
from typing import Any
|
|
5
6
|
|
|
6
|
-
from hardpy.pytest_hardpy.db import
|
|
7
|
+
from hardpy.pytest_hardpy.db import (
|
|
8
|
+
DatabaseField as DF, # noqa: N817
|
|
9
|
+
RunStore,
|
|
10
|
+
StateStore,
|
|
11
|
+
)
|
|
7
12
|
|
|
8
13
|
|
|
9
14
|
class BaseReporter:
|
|
10
15
|
"""Base class for test reporter."""
|
|
11
16
|
|
|
12
|
-
def __init__(self):
|
|
17
|
+
def __init__(self) -> None:
|
|
13
18
|
self._statestore = StateStore()
|
|
14
19
|
self._runstore = RunStore()
|
|
15
20
|
self._log = getLogger(__name__)
|
|
16
21
|
|
|
17
22
|
def set_doc_value(
|
|
18
|
-
self,
|
|
19
|
-
|
|
23
|
+
self,
|
|
24
|
+
key: str,
|
|
25
|
+
value: Any, # noqa: ANN401
|
|
26
|
+
runstore_only: bool = False,
|
|
27
|
+
statestore_only: bool = False,
|
|
28
|
+
) -> None:
|
|
20
29
|
"""Set value to the document.
|
|
21
30
|
|
|
22
31
|
Update a document without writing to the database.
|
|
@@ -33,7 +42,8 @@ class BaseReporter:
|
|
|
33
42
|
ValueError: if both runstore_only and statestore_only are True
|
|
34
43
|
"""
|
|
35
44
|
if runstore_only and statestore_only:
|
|
36
|
-
|
|
45
|
+
msg = "Both runstore_only and statestore_only cannot be True"
|
|
46
|
+
raise ValueError(msg)
|
|
37
47
|
if runstore_only:
|
|
38
48
|
self._runstore.update_doc(key, value)
|
|
39
49
|
return
|
|
@@ -43,12 +53,12 @@ class BaseReporter:
|
|
|
43
53
|
self._runstore.update_doc(key, value)
|
|
44
54
|
self._statestore.update_doc(key, value)
|
|
45
55
|
|
|
46
|
-
def update_db_by_doc(self):
|
|
56
|
+
def update_db_by_doc(self) -> None:
|
|
47
57
|
"""Update database by current document."""
|
|
48
58
|
self._statestore.update_db()
|
|
49
59
|
self._runstore.update_db()
|
|
50
60
|
|
|
51
|
-
def generate_key(self, *args) -> str:
|
|
61
|
+
def generate_key(self, *args: Any) -> str: # noqa: ANN401
|
|
52
62
|
"""Generate key for database.
|
|
53
63
|
|
|
54
64
|
Args:
|
|
@@ -58,3 +68,18 @@ class BaseReporter:
|
|
|
58
68
|
str: database key
|
|
59
69
|
"""
|
|
60
70
|
return ".".join(args)
|
|
71
|
+
|
|
72
|
+
def get_current_attempt(self, module_id: str, case_id: str) -> int:
|
|
73
|
+
"""Get current attempt.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
int: current attempt
|
|
77
|
+
"""
|
|
78
|
+
key = self.generate_key(
|
|
79
|
+
DF.MODULES,
|
|
80
|
+
module_id,
|
|
81
|
+
DF.CASES,
|
|
82
|
+
case_id,
|
|
83
|
+
DF.ATTEMPT,
|
|
84
|
+
)
|
|
85
|
+
return self._statestore.get_field(key)
|
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
# Copyright (c) 2024 Everypin
|
|
2
2
|
# GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
|
+
from __future__ import annotations
|
|
3
4
|
|
|
4
|
-
from time import time, tzname
|
|
5
|
-
from logging import getLogger
|
|
6
5
|
from copy import deepcopy
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from logging import getLogger
|
|
8
|
+
from time import time
|
|
7
9
|
|
|
8
10
|
from natsort import natsorted
|
|
9
11
|
|
|
10
|
-
from hardpy.pytest_hardpy.db import DatabaseField as DF
|
|
12
|
+
from hardpy.pytest_hardpy.db import DatabaseField as DF # noqa: N817
|
|
11
13
|
from hardpy.pytest_hardpy.reporter.base import BaseReporter
|
|
12
|
-
from hardpy.pytest_hardpy.utils import TestStatus,
|
|
14
|
+
from hardpy.pytest_hardpy.utils import NodeInfo, TestStatus, machine_id
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class HookReporter(BaseReporter):
|
|
16
18
|
"""Reporter for using in the hook HardPy plugin's hooks."""
|
|
17
19
|
|
|
18
|
-
def __init__(self, is_clear_database: bool = False)
|
|
20
|
+
def __init__(self, is_clear_database: bool = False) -> None:
|
|
19
21
|
super().__init__()
|
|
20
22
|
if is_clear_database:
|
|
21
23
|
self._statestore.clear()
|
|
22
24
|
self._log = getLogger(__name__)
|
|
23
25
|
|
|
24
|
-
def init_doc(self, doc_name: str):
|
|
26
|
+
def init_doc(self, doc_name: str) -> None:
|
|
25
27
|
"""Initialize document.
|
|
26
28
|
|
|
27
29
|
Args:
|
|
@@ -30,23 +32,26 @@ class HookReporter(BaseReporter):
|
|
|
30
32
|
self.set_doc_value(DF.NAME, doc_name)
|
|
31
33
|
self.set_doc_value(DF.STATUS, TestStatus.READY)
|
|
32
34
|
self.set_doc_value(DF.START_TIME, None)
|
|
33
|
-
self.set_doc_value(DF.TIMEZONE, None)
|
|
34
35
|
self.set_doc_value(DF.STOP_TIME, None)
|
|
35
|
-
self.set_doc_value(DF.PROGRESS, 0)
|
|
36
|
-
self.set_doc_value(DF.DRIVERS, {})
|
|
36
|
+
self.set_doc_value(DF.PROGRESS, 0, statestore_only=True)
|
|
37
37
|
self.set_doc_value(DF.ARTIFACT, {}, runstore_only=True)
|
|
38
38
|
self.set_doc_value(DF.OPERATOR_MSG, {}, statestore_only=True)
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
test_stand_tz = self.generate_key(DF.TEST_STAND, DF.TIMEZONE)
|
|
41
|
+
self.set_doc_value(test_stand_tz, datetime.now().astimezone().tzname())
|
|
42
|
+
|
|
43
|
+
test_stand_id_key = self.generate_key(DF.TEST_STAND, DF.HW_ID)
|
|
44
|
+
self.set_doc_value(test_stand_id_key, machine_id())
|
|
45
|
+
|
|
46
|
+
def start(self) -> None:
|
|
41
47
|
"""Start test."""
|
|
42
48
|
self._log.debug("Starting test run.")
|
|
43
49
|
start_time = int(time())
|
|
44
50
|
self.set_doc_value(DF.START_TIME, start_time)
|
|
45
51
|
self.set_doc_value(DF.STATUS, TestStatus.RUN)
|
|
46
|
-
self.set_doc_value(DF.
|
|
47
|
-
self.set_doc_value(DF.PROGRESS, 0)
|
|
52
|
+
self.set_doc_value(DF.PROGRESS, 0, statestore_only=True)
|
|
48
53
|
|
|
49
|
-
def finish(self, status: TestStatus):
|
|
54
|
+
def finish(self, status: TestStatus) -> None:
|
|
50
55
|
"""Finish test.
|
|
51
56
|
|
|
52
57
|
This method must be called at the end of test run.
|
|
@@ -56,20 +61,20 @@ class HookReporter(BaseReporter):
|
|
|
56
61
|
self.set_doc_value(DF.STOP_TIME, stop_time)
|
|
57
62
|
self.set_doc_value(DF.STATUS, status)
|
|
58
63
|
|
|
59
|
-
def compact_all(self):
|
|
60
|
-
"""Compact all databases"""
|
|
64
|
+
def compact_all(self) -> None:
|
|
65
|
+
"""Compact all databases."""
|
|
61
66
|
self._statestore.compact()
|
|
62
67
|
self._runstore.compact()
|
|
63
68
|
|
|
64
|
-
def set_progress(self, progress: int):
|
|
69
|
+
def set_progress(self, progress: int) -> None:
|
|
65
70
|
"""Set test progress.
|
|
66
71
|
|
|
67
72
|
Args:
|
|
68
73
|
progress (int): test progress
|
|
69
74
|
"""
|
|
70
|
-
self.set_doc_value(DF.PROGRESS, progress)
|
|
75
|
+
self.set_doc_value(DF.PROGRESS, progress, statestore_only=True)
|
|
71
76
|
|
|
72
|
-
def set_assertion_msg(self, module_id: str, case_id: str, msg: str | None):
|
|
77
|
+
def set_assertion_msg(self, module_id: str, case_id: str, msg: str | None) -> None:
|
|
73
78
|
"""Set case assertion message.
|
|
74
79
|
|
|
75
80
|
Args:
|
|
@@ -78,11 +83,15 @@ class HookReporter(BaseReporter):
|
|
|
78
83
|
msg (str): assertion message
|
|
79
84
|
"""
|
|
80
85
|
key = self.generate_key(
|
|
81
|
-
DF.MODULES,
|
|
86
|
+
DF.MODULES,
|
|
87
|
+
module_id,
|
|
88
|
+
DF.CASES,
|
|
89
|
+
case_id,
|
|
90
|
+
DF.ASSERTION_MSG,
|
|
82
91
|
)
|
|
83
92
|
self.set_doc_value(key, msg)
|
|
84
93
|
|
|
85
|
-
def add_case(self, node_info: NodeInfo):
|
|
94
|
+
def add_case(self, node_info: NodeInfo) -> None:
|
|
86
95
|
"""Add test case to document.
|
|
87
96
|
|
|
88
97
|
Args:
|
|
@@ -99,7 +108,7 @@ class HookReporter(BaseReporter):
|
|
|
99
108
|
self.set_doc_value(key, item_statestore, statestore_only=True)
|
|
100
109
|
self.set_doc_value(key, item_runstore, runstore_only=True)
|
|
101
110
|
|
|
102
|
-
def set_case_status(self, module_id: str, case_id: str, status: TestStatus):
|
|
111
|
+
def set_case_status(self, module_id: str, case_id: str, status: TestStatus) -> None:
|
|
103
112
|
"""Set test case status.
|
|
104
113
|
|
|
105
114
|
Args:
|
|
@@ -110,7 +119,7 @@ class HookReporter(BaseReporter):
|
|
|
110
119
|
key = self.generate_key(DF.MODULES, module_id, DF.CASES, case_id, DF.STATUS)
|
|
111
120
|
self.set_doc_value(key, status)
|
|
112
121
|
|
|
113
|
-
def set_case_start_time(self, module_id: str, case_id: str):
|
|
122
|
+
def set_case_start_time(self, module_id: str, case_id: str) -> None:
|
|
114
123
|
"""Set test case start_time.
|
|
115
124
|
|
|
116
125
|
Args:
|
|
@@ -120,7 +129,7 @@ class HookReporter(BaseReporter):
|
|
|
120
129
|
key = self.generate_key(DF.MODULES, module_id, DF.CASES, case_id, DF.START_TIME)
|
|
121
130
|
self._set_time(key)
|
|
122
131
|
|
|
123
|
-
def set_case_stop_time(self, module_id: str, case_id: str):
|
|
132
|
+
def set_case_stop_time(self, module_id: str, case_id: str) -> None:
|
|
124
133
|
"""Set test case start_time.
|
|
125
134
|
|
|
126
135
|
Args:
|
|
@@ -130,7 +139,7 @@ class HookReporter(BaseReporter):
|
|
|
130
139
|
key = self.generate_key(DF.MODULES, module_id, DF.CASES, case_id, DF.STOP_TIME)
|
|
131
140
|
self._set_time(key)
|
|
132
141
|
|
|
133
|
-
def set_module_status(self, module_id: str, status: TestStatus):
|
|
142
|
+
def set_module_status(self, module_id: str, status: TestStatus) -> None:
|
|
134
143
|
"""Set test module status.
|
|
135
144
|
|
|
136
145
|
Args:
|
|
@@ -140,7 +149,7 @@ class HookReporter(BaseReporter):
|
|
|
140
149
|
key = self.generate_key(DF.MODULES, module_id, DF.STATUS)
|
|
141
150
|
self.set_doc_value(key, status)
|
|
142
151
|
|
|
143
|
-
def set_module_start_time(self, module_id: str):
|
|
152
|
+
def set_module_start_time(self, module_id: str) -> None:
|
|
144
153
|
"""Set test module status.
|
|
145
154
|
|
|
146
155
|
Args:
|
|
@@ -149,7 +158,7 @@ class HookReporter(BaseReporter):
|
|
|
149
158
|
key = self.generate_key(DF.MODULES, module_id, DF.START_TIME)
|
|
150
159
|
self._set_time(key)
|
|
151
160
|
|
|
152
|
-
def set_module_stop_time(self, module_id: str):
|
|
161
|
+
def set_module_stop_time(self, module_id: str) -> None:
|
|
153
162
|
"""Set test module status.
|
|
154
163
|
|
|
155
164
|
Args:
|
|
@@ -158,6 +167,23 @@ class HookReporter(BaseReporter):
|
|
|
158
167
|
key = self.generate_key(DF.MODULES, module_id, DF.STOP_TIME)
|
|
159
168
|
self._set_time(key)
|
|
160
169
|
|
|
170
|
+
def set_case_attempt(self, module_id: str, case_id: str, attempt: int) -> None:
|
|
171
|
+
"""Set test case current attempt.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
module_id (str): module id
|
|
175
|
+
case_id (str): case id
|
|
176
|
+
attempt (int): test case current attempt
|
|
177
|
+
"""
|
|
178
|
+
key = self.generate_key(
|
|
179
|
+
DF.MODULES,
|
|
180
|
+
module_id,
|
|
181
|
+
DF.CASES,
|
|
182
|
+
case_id,
|
|
183
|
+
DF.ATTEMPT,
|
|
184
|
+
)
|
|
185
|
+
self.set_doc_value(key, attempt, statestore_only=True)
|
|
186
|
+
|
|
161
187
|
def update_node_order(self, nodes: dict) -> None:
|
|
162
188
|
"""Update node order.
|
|
163
189
|
|
|
@@ -173,7 +199,7 @@ class HookReporter(BaseReporter):
|
|
|
173
199
|
updated_module_order = self._update_module_order(updated_case_order)
|
|
174
200
|
self.set_doc_value(key, updated_module_order, statestore_only=True)
|
|
175
201
|
|
|
176
|
-
def _set_time(self, key: str):
|
|
202
|
+
def _set_time(self, key: str) -> None:
|
|
177
203
|
current_time = self._statestore.get_field(key)
|
|
178
204
|
if current_time is None:
|
|
179
205
|
self.set_doc_value(key, int(time()))
|
|
@@ -184,8 +210,8 @@ class HookReporter(BaseReporter):
|
|
|
184
210
|
node_info: NodeInfo,
|
|
185
211
|
is_only_runstore: bool = False,
|
|
186
212
|
is_only_statestore: bool = False,
|
|
187
|
-
):
|
|
188
|
-
module_default = {
|
|
213
|
+
) -> None:
|
|
214
|
+
module_default = {
|
|
189
215
|
DF.STATUS: TestStatus.READY,
|
|
190
216
|
DF.NAME: self._get_module_name(node_info),
|
|
191
217
|
DF.START_TIME: None,
|
|
@@ -199,13 +225,12 @@ class HookReporter(BaseReporter):
|
|
|
199
225
|
DF.STOP_TIME: None,
|
|
200
226
|
DF.ASSERTION_MSG: None,
|
|
201
227
|
DF.MSG: None,
|
|
202
|
-
DF.ATTEMPT: 0,
|
|
203
228
|
}
|
|
204
229
|
|
|
205
|
-
if item.get(node_info.module_id) is None:
|
|
230
|
+
if item.get(node_info.module_id) is None:
|
|
206
231
|
if is_only_runstore:
|
|
207
232
|
module_default[DF.ARTIFACT] = {}
|
|
208
|
-
item[node_info.module_id] = module_default
|
|
233
|
+
item[node_info.module_id] = module_default
|
|
209
234
|
else:
|
|
210
235
|
item[node_info.module_id][DF.STATUS] = TestStatus.READY
|
|
211
236
|
item[node_info.module_id][DF.NAME] = self._get_module_name(node_info)
|
|
@@ -218,10 +243,14 @@ class HookReporter(BaseReporter):
|
|
|
218
243
|
|
|
219
244
|
if is_only_statestore:
|
|
220
245
|
case_default[DF.DIALOG_BOX] = {}
|
|
246
|
+
case_default[DF.ATTEMPT] = 0
|
|
221
247
|
item[node_info.module_id][DF.CASES][node_info.case_id] = case_default
|
|
222
248
|
|
|
223
249
|
def _remove_outdate_node(
|
|
224
|
-
self,
|
|
250
|
+
self,
|
|
251
|
+
old_modules: dict,
|
|
252
|
+
new_modules: dict,
|
|
253
|
+
nodes: dict,
|
|
225
254
|
) -> dict:
|
|
226
255
|
"""Remove outdated nodes from StateStore database.
|
|
227
256
|
|
|
@@ -277,16 +306,15 @@ class HookReporter(BaseReporter):
|
|
|
277
306
|
Returns:
|
|
278
307
|
dict: list of modules and cases.
|
|
279
308
|
"""
|
|
280
|
-
|
|
281
309
|
sorted_modules = natsorted(modules.items(), key=lambda item: item[0])
|
|
282
310
|
|
|
283
311
|
new_modules = {}
|
|
284
312
|
for module_id, module in sorted_modules:
|
|
285
|
-
new_modules[module_id] = module
|
|
313
|
+
new_modules[module_id] = module # noqa: PERF403
|
|
286
314
|
|
|
287
315
|
return new_modules
|
|
288
316
|
|
|
289
|
-
def _get_module_name(self, node_info) -> str:
|
|
317
|
+
def _get_module_name(self, node_info: NodeInfo) -> str:
|
|
290
318
|
"""Get module name from markers or use default.
|
|
291
319
|
|
|
292
320
|
Args:
|
|
@@ -297,7 +325,7 @@ class HookReporter(BaseReporter):
|
|
|
297
325
|
"""
|
|
298
326
|
return node_info.module_name if node_info.module_name else node_info.module_id
|
|
299
327
|
|
|
300
|
-
def _get_case_name(self, node_info) -> str:
|
|
328
|
+
def _get_case_name(self, node_info: NodeInfo) -> str:
|
|
301
329
|
"""Get case name from markers or use default.
|
|
302
330
|
|
|
303
331
|
Args:
|
|
@@ -5,19 +5,17 @@ from logging import getLogger
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from hardpy.pytest_hardpy.reporter.base import BaseReporter
|
|
8
|
-
from hardpy.pytest_hardpy.utils import
|
|
8
|
+
from hardpy.pytest_hardpy.utils import SingletonMeta
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class RunnerReporter(
|
|
11
|
+
class RunnerReporter(BaseReporter, metaclass=SingletonMeta):
|
|
12
12
|
"""Reporter for using in direct call from test runner with HardPy plugin."""
|
|
13
13
|
|
|
14
|
-
def __init__(self):
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
self._log = getLogger(__name__)
|
|
18
|
-
self._initialized = True
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
super().__init__()
|
|
16
|
+
self._log = getLogger(__name__)
|
|
19
17
|
|
|
20
|
-
def get_field(self, key: str) -> Any:
|
|
18
|
+
def get_field(self, key: str) -> Any: # noqa: ANN401
|
|
21
19
|
"""Get field from the statestore.
|
|
22
20
|
|
|
23
21
|
Args:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Copyright (c) 2024 Everypin
|
|
2
2
|
# GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
3
|
|
|
4
|
-
from hardpy.pytest_hardpy.result.report_reader.couchdb_reader import CouchdbReader
|
|
5
4
|
from hardpy.pytest_hardpy.result.report_loader.couchdb_loader import CouchdbLoader
|
|
5
|
+
from hardpy.pytest_hardpy.result.report_reader.couchdb_reader import CouchdbReader
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"CouchdbReader",
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# Copyright (c) 2024 Everypin
|
|
2
2
|
# GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
|
+
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import ast
|
|
5
|
-
import requests
|
|
6
6
|
import socket
|
|
7
|
-
|
|
8
7
|
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
import requests
|
|
9
10
|
from urllib3 import disable_warnings
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
@dataclass
|
|
13
|
-
class CouchdbConfig:
|
|
14
|
+
class CouchdbConfig:
|
|
14
15
|
"""CouchDB loader config.
|
|
15
16
|
|
|
16
17
|
If `connection_str` arg is not set, it will be created from other args.
|
|
@@ -23,7 +24,7 @@ class CouchdbConfig: # noqa: WPS306
|
|
|
23
24
|
port: int = 5984
|
|
24
25
|
connection_str: str | None = None
|
|
25
26
|
|
|
26
|
-
def __post_init__(self):
|
|
27
|
+
def __post_init__(self) -> None:
|
|
27
28
|
"""Disable urllib3 warnings.
|
|
28
29
|
|
|
29
30
|
More info: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
|
|
@@ -43,14 +44,14 @@ class CouchdbConfig: # noqa: WPS306
|
|
|
43
44
|
if self.connection_str:
|
|
44
45
|
return self.connection_str
|
|
45
46
|
|
|
46
|
-
# TODO: Modify connection string creating based on protocol.
|
|
47
|
+
# TODO(xorialexandrov): Modify connection string creating based on protocol.
|
|
47
48
|
# Some problems with http and https, different ports, local
|
|
48
49
|
# and cloud databases.
|
|
49
50
|
protocol = self._get_protocol()
|
|
50
51
|
|
|
51
52
|
if protocol == "http":
|
|
52
53
|
host_url = f"http://{self.host}:{self.port}"
|
|
53
|
-
uri = f"{self.host}:{
|
|
54
|
+
uri = f"{self.host}:{self.port!s}"
|
|
54
55
|
elif protocol == "https":
|
|
55
56
|
host_url = f"https://{self.host}"
|
|
56
57
|
uri = f"{self.host}"
|
|
@@ -58,20 +59,22 @@ class CouchdbConfig: # noqa: WPS306
|
|
|
58
59
|
try:
|
|
59
60
|
response = requests.get(host_url, timeout=5)
|
|
60
61
|
except requests.exceptions.RequestException:
|
|
61
|
-
|
|
62
|
+
msg = f"Error CouchDB connecting to {host_url}."
|
|
63
|
+
raise RuntimeError(msg) # noqa: B904
|
|
62
64
|
|
|
63
65
|
# fmt: off
|
|
64
66
|
try:
|
|
65
|
-
couchdb_dict = ast.literal_eval(response._content.decode("utf-8")) # noqa:
|
|
67
|
+
couchdb_dict = ast.literal_eval(response._content.decode("utf-8")) # type: ignore # noqa: SLF001
|
|
66
68
|
couchdb_dict.get("couchdb", False)
|
|
67
|
-
except Exception:
|
|
68
|
-
|
|
69
|
+
except Exception: # noqa: BLE001
|
|
70
|
+
msg = f"Address {host_url} does not provide CouchDB attributes."
|
|
71
|
+
raise RuntimeError(msg) # noqa: B904
|
|
69
72
|
# fmt: on
|
|
70
73
|
|
|
71
74
|
credentials = f"{self.user}:{self.password}"
|
|
72
75
|
return f"{protocol}://{credentials}@{uri}/"
|
|
73
76
|
|
|
74
|
-
def _get_protocol(self) -> str:
|
|
77
|
+
def _get_protocol(self) -> str:
|
|
75
78
|
success = 200
|
|
76
79
|
try:
|
|
77
80
|
# HTTPS attempt
|
|
@@ -80,17 +83,18 @@ class CouchdbConfig: # noqa: WPS306
|
|
|
80
83
|
request = f"https://{self.host}"
|
|
81
84
|
if requests.get(request, timeout=5).status_code == success:
|
|
82
85
|
return "https"
|
|
83
|
-
raise OSError
|
|
86
|
+
raise OSError # noqa: TRY301
|
|
84
87
|
except OSError:
|
|
85
|
-
try:
|
|
88
|
+
try:
|
|
86
89
|
# HTTP attempt
|
|
87
90
|
sock = socket.create_connection((self.host, self.port))
|
|
88
91
|
sock.close()
|
|
89
92
|
request = f"http://{self.host}:{self.port}"
|
|
90
93
|
if requests.get(request, timeout=5).status_code == success:
|
|
91
94
|
return "http"
|
|
92
|
-
raise OSError
|
|
95
|
+
raise OSError # noqa: TRY301
|
|
93
96
|
except OSError:
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
msg = f"Error connecting to couchdb server {self.host}:{self.port}."
|
|
98
|
+
raise RuntimeError( # noqa: B904
|
|
99
|
+
msg,
|
|
96
100
|
)
|
|
@@ -5,8 +5,8 @@ from logging import getLogger
|
|
|
5
5
|
from uuid import uuid4
|
|
6
6
|
|
|
7
7
|
from pycouchdb import Server as DbServer
|
|
8
|
-
from pycouchdb.exceptions import Conflict
|
|
9
8
|
from pycouchdb.client import Database
|
|
9
|
+
from pycouchdb.exceptions import Conflict
|
|
10
10
|
|
|
11
11
|
from hardpy.pytest_hardpy.db.schema import ResultRunStore
|
|
12
12
|
from hardpy.pytest_hardpy.result.couchdb_config import CouchdbConfig
|
|
@@ -15,7 +15,7 @@ from hardpy.pytest_hardpy.result.couchdb_config import CouchdbConfig
|
|
|
15
15
|
class CouchdbLoader:
|
|
16
16
|
"""CouchDB report generator."""
|
|
17
17
|
|
|
18
|
-
def __init__(self, config: CouchdbConfig):
|
|
18
|
+
def __init__(self, config: CouchdbConfig) -> None:
|
|
19
19
|
self._log = getLogger(__name__)
|
|
20
20
|
self._config: CouchdbConfig = config
|
|
21
21
|
self._db_srv = DbServer(config.connection_string)
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
# Copyright (c) 2024 Everypin
|
|
2
2
|
# GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
|
+
from __future__ import annotations
|
|
3
4
|
|
|
4
|
-
from logging import getLogger
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from
|
|
6
|
+
from logging import getLogger
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
7
8
|
|
|
8
9
|
from pycouchdb import Server as DbServer
|
|
9
|
-
from pycouchdb.client import Database
|
|
10
10
|
from pycouchdb.exceptions import NotFound
|
|
11
11
|
|
|
12
|
-
from hardpy.pytest_hardpy.db import DatabaseField as DF
|
|
12
|
+
from hardpy.pytest_hardpy.db import DatabaseField as DF # noqa: N817
|
|
13
13
|
from hardpy.pytest_hardpy.utils.const import TestStatus
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pycouchdb.client import Database
|
|
17
|
+
|
|
18
|
+
from hardpy.pytest_hardpy.result.couchdb_config import CouchdbConfig
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
@dataclass
|
|
@@ -22,14 +26,14 @@ class ReportInfo:
|
|
|
22
26
|
status: str
|
|
23
27
|
start_time: str
|
|
24
28
|
end_time: str
|
|
25
|
-
first_failed_test_name:
|
|
26
|
-
first_failed_test_id:
|
|
29
|
+
first_failed_test_name: str | None
|
|
30
|
+
first_failed_test_id: str | None
|
|
27
31
|
|
|
28
32
|
|
|
29
33
|
class CouchdbReader:
|
|
30
34
|
"""CouchDB report info reader."""
|
|
31
35
|
|
|
32
|
-
def __init__(self, config: CouchdbConfig):
|
|
36
|
+
def __init__(self, config: CouchdbConfig) -> None:
|
|
33
37
|
self._log = getLogger(__name__)
|
|
34
38
|
self._config = config
|
|
35
39
|
self._db_srv = DbServer(config.connection_string)
|
|
@@ -58,13 +62,14 @@ class CouchdbReader:
|
|
|
58
62
|
int: number of reports
|
|
59
63
|
"""
|
|
60
64
|
if start_time < 0 or end_time < 0:
|
|
61
|
-
|
|
65
|
+
msg = "Start time and end time must be positive values"
|
|
66
|
+
raise ValueError(msg)
|
|
62
67
|
return sum(
|
|
63
68
|
1
|
|
64
69
|
for report in self._db.all()
|
|
65
70
|
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]),
|
|
71
|
+
self._get_start_time_from_db(report[self._doc_id]), # type: ignore
|
|
72
|
+
self._get_stop_time_from_db(report[self._doc_id]), # type: ignore
|
|
68
73
|
start_time,
|
|
69
74
|
end_time,
|
|
70
75
|
)
|
|
@@ -85,10 +90,11 @@ class CouchdbReader:
|
|
|
85
90
|
doc = self._db.get(report_name)
|
|
86
91
|
status = doc[DF.STATUS]
|
|
87
92
|
if status not in {TestStatus.PASSED, TestStatus.FAILED, TestStatus.SKIPPED}:
|
|
88
|
-
|
|
93
|
+
msg = "Invalid report status"
|
|
94
|
+
raise ValueError(msg)
|
|
89
95
|
return status
|
|
90
96
|
|
|
91
|
-
def get_report_infos(self) ->
|
|
97
|
+
def get_report_infos(self) -> list[ReportInfo]:
|
|
92
98
|
"""Get a list of information about all reports in the database.
|
|
93
99
|
|
|
94
100
|
Returns:
|
|
@@ -102,8 +108,10 @@ class CouchdbReader:
|
|
|
102
108
|
return reports_info
|
|
103
109
|
|
|
104
110
|
def get_report_infos_in_timeframe(
|
|
105
|
-
self,
|
|
106
|
-
|
|
111
|
+
self,
|
|
112
|
+
start_time: int,
|
|
113
|
+
end_time: int,
|
|
114
|
+
) -> list[ReportInfo]:
|
|
107
115
|
"""Get a list of information about reports in a timeframe in the database.
|
|
108
116
|
|
|
109
117
|
Args:
|
|
@@ -117,14 +125,15 @@ class CouchdbReader:
|
|
|
117
125
|
List[ReportInfo]: list of report information
|
|
118
126
|
"""
|
|
119
127
|
if start_time < 0 or end_time < 0:
|
|
120
|
-
|
|
128
|
+
msg = "Start time and end time must be positive values"
|
|
129
|
+
raise ValueError(msg)
|
|
121
130
|
|
|
122
131
|
reports_info = []
|
|
123
132
|
reports = self._db.all()
|
|
124
133
|
for report in reports:
|
|
125
134
|
start_t_db = self._get_start_time_from_db(report[self._doc_id])
|
|
126
135
|
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):
|
|
136
|
+
if self._is_in_timeframe(start_t_db, stop_t_db, start_time, end_time): # type: ignore
|
|
128
137
|
report_info = self._get_single_report_info(report)
|
|
129
138
|
reports_info.append(report_info)
|
|
130
139
|
return reports_info
|
|
@@ -134,7 +143,8 @@ class CouchdbReader:
|
|
|
134
143
|
return self._db_srv.database(self._config.db_name)
|
|
135
144
|
except NotFound as exc:
|
|
136
145
|
self._log.error(f"Error initializing database: {exc}")
|
|
137
|
-
|
|
146
|
+
msg = "Error initializing database"
|
|
147
|
+
raise RuntimeError(msg) from exc
|
|
138
148
|
|
|
139
149
|
def _get_start_time_from_db(self, doc: dict) -> str:
|
|
140
150
|
return doc[DF.START_TIME]
|
|
@@ -146,7 +156,7 @@ class CouchdbReader:
|
|
|
146
156
|
first_failed_test_name = None
|
|
147
157
|
first_failed_test_id = None
|
|
148
158
|
report_doc = report[self._doc_id]
|
|
149
|
-
for
|
|
159
|
+
for module_info in report_doc[DF.MODULES].values():
|
|
150
160
|
for case_name, case_info in module_info[DF.CASES].items():
|
|
151
161
|
if case_info[DF.STATUS] == TestStatus.FAILED:
|
|
152
162
|
first_failed_test_name = case_info[DF.NAME]
|
|
@@ -160,5 +170,11 @@ class CouchdbReader:
|
|
|
160
170
|
first_failed_test_id=first_failed_test_id,
|
|
161
171
|
)
|
|
162
172
|
|
|
163
|
-
def _is_in_timeframe(
|
|
173
|
+
def _is_in_timeframe(
|
|
174
|
+
self,
|
|
175
|
+
start: int,
|
|
176
|
+
end: int,
|
|
177
|
+
timeframe_start: int,
|
|
178
|
+
timeframe_end: int,
|
|
179
|
+
) -> bool:
|
|
164
180
|
return timeframe_start <= start and end <= timeframe_end
|