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
|
@@ -1,40 +1,40 @@
|
|
|
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.utils.node_info import NodeInfo
|
|
5
|
-
from hardpy.pytest_hardpy.utils.progress_calculator import ProgressCalculator
|
|
6
|
-
from hardpy.pytest_hardpy.utils.const import TestStatus
|
|
7
|
-
from hardpy.pytest_hardpy.utils.singleton import Singleton
|
|
8
4
|
from hardpy.pytest_hardpy.utils.connection_data import ConnectionData
|
|
9
|
-
from hardpy.pytest_hardpy.utils.
|
|
10
|
-
DuplicateSerialNumberError,
|
|
11
|
-
DuplicatePartNumberError,
|
|
12
|
-
DuplicateTestStandNameError,
|
|
13
|
-
DuplicateDialogBoxError,
|
|
14
|
-
WidgetInfoError,
|
|
15
|
-
)
|
|
5
|
+
from hardpy.pytest_hardpy.utils.const import TestStatus
|
|
16
6
|
from hardpy.pytest_hardpy.utils.dialog_box import (
|
|
17
|
-
DialogBox,
|
|
18
|
-
TextInputWidget,
|
|
19
|
-
NumericInputWidget,
|
|
20
7
|
CheckboxWidget,
|
|
21
|
-
|
|
8
|
+
DialogBox,
|
|
22
9
|
ImageWidget,
|
|
23
10
|
MultistepWidget,
|
|
11
|
+
NumericInputWidget,
|
|
12
|
+
RadiobuttonWidget,
|
|
24
13
|
StepWidget,
|
|
14
|
+
TextInputWidget,
|
|
25
15
|
)
|
|
26
|
-
|
|
16
|
+
from hardpy.pytest_hardpy.utils.exception import (
|
|
17
|
+
DuplicatePartNumberError,
|
|
18
|
+
DuplicateSerialNumberError,
|
|
19
|
+
DuplicateTestStandLocationError,
|
|
20
|
+
DuplicateTestStandNameError,
|
|
21
|
+
WidgetInfoError,
|
|
22
|
+
)
|
|
23
|
+
from hardpy.pytest_hardpy.utils.machineid import machine_id
|
|
24
|
+
from hardpy.pytest_hardpy.utils.node_info import NodeInfo
|
|
25
|
+
from hardpy.pytest_hardpy.utils.progress_calculator import ProgressCalculator
|
|
26
|
+
from hardpy.pytest_hardpy.utils.singleton import SingletonMeta
|
|
27
27
|
|
|
28
28
|
__all__ = [
|
|
29
29
|
"NodeInfo",
|
|
30
30
|
"ProgressCalculator",
|
|
31
31
|
"TestStatus",
|
|
32
|
-
"
|
|
32
|
+
"SingletonMeta",
|
|
33
33
|
"ConnectionData",
|
|
34
34
|
"DuplicateSerialNumberError",
|
|
35
35
|
"DuplicatePartNumberError",
|
|
36
|
+
"DuplicateTestStandLocationError",
|
|
36
37
|
"DuplicateTestStandNameError",
|
|
37
|
-
"DuplicateDialogBoxError",
|
|
38
38
|
"WidgetInfoError",
|
|
39
39
|
"DialogBox",
|
|
40
40
|
"TextInputWidget",
|
|
@@ -43,5 +43,6 @@ __all__ = [
|
|
|
43
43
|
"RadiobuttonWidget",
|
|
44
44
|
"ImageWidget",
|
|
45
45
|
"MultistepWidget",
|
|
46
|
-
"StepWidget"
|
|
46
|
+
"StepWidget",
|
|
47
|
+
"machine_id",
|
|
47
48
|
]
|
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
from socket import gethostname
|
|
5
5
|
|
|
6
|
-
from hardpy.pytest_hardpy.utils.singleton import
|
|
6
|
+
from hardpy.pytest_hardpy.utils.singleton import SingletonMeta
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class ConnectionData(
|
|
9
|
+
class ConnectionData(metaclass=SingletonMeta):
|
|
10
10
|
"""Connection data storage."""
|
|
11
11
|
|
|
12
|
-
def __init__(self):
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
self.socket_port: int = 6525
|
|
17
|
-
self._initialized = True
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
self.database_url: str = "http://dev:dev@localhost:5984/"
|
|
14
|
+
self.socket_host: str = gethostname()
|
|
15
|
+
self.socket_port: int = 6525
|
|
@@ -1,5 +1,6 @@
|
|
|
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 base64
|
|
5
6
|
from abc import ABC, abstractmethod
|
|
@@ -28,12 +29,12 @@ class WidgetType(Enum):
|
|
|
28
29
|
class IWidget(ABC):
|
|
29
30
|
"""Dialog box widget interface."""
|
|
30
31
|
|
|
31
|
-
def __init__(self, widget_type: WidgetType):
|
|
32
|
+
def __init__(self, widget_type: WidgetType) -> None:
|
|
32
33
|
self.type: Final[str] = widget_type.value
|
|
33
34
|
self.info: dict = {}
|
|
34
35
|
|
|
35
36
|
@abstractmethod
|
|
36
|
-
def convert_data(self, input_data: str | None) -> Any | None:
|
|
37
|
+
def convert_data(self, input_data: str | None) -> Any | None: # noqa: ANN401
|
|
37
38
|
"""Get the widget data in the correct format.
|
|
38
39
|
|
|
39
40
|
Args:
|
|
@@ -48,10 +49,10 @@ class IWidget(ABC):
|
|
|
48
49
|
class BaseWidget(IWidget):
|
|
49
50
|
"""Widget info interface."""
|
|
50
51
|
|
|
51
|
-
def __init__(self, widget_type: WidgetType = WidgetType.BASE):
|
|
52
|
+
def __init__(self, widget_type: WidgetType = WidgetType.BASE) -> None: # noqa: ARG002
|
|
52
53
|
super().__init__(WidgetType.BASE)
|
|
53
54
|
|
|
54
|
-
def convert_data(self, input_data: str | None = None) -> bool: # noqa:
|
|
55
|
+
def convert_data(self, input_data: str | None = None) -> bool: # noqa: ARG002
|
|
55
56
|
"""Get base widget data, i.e. None.
|
|
56
57
|
|
|
57
58
|
Args:
|
|
@@ -66,7 +67,7 @@ class BaseWidget(IWidget):
|
|
|
66
67
|
class TextInputWidget(IWidget):
|
|
67
68
|
"""Text input widget."""
|
|
68
69
|
|
|
69
|
-
def __init__(self):
|
|
70
|
+
def __init__(self) -> None:
|
|
70
71
|
"""Initialize the TextInputWidget."""
|
|
71
72
|
super().__init__(WidgetType.TEXT_INPUT)
|
|
72
73
|
|
|
@@ -85,7 +86,7 @@ class TextInputWidget(IWidget):
|
|
|
85
86
|
class NumericInputWidget(IWidget):
|
|
86
87
|
"""Numeric input widget."""
|
|
87
88
|
|
|
88
|
-
def __init__(self):
|
|
89
|
+
def __init__(self) -> None:
|
|
89
90
|
"""Initialize the NumericInputWidget."""
|
|
90
91
|
super().__init__(WidgetType.NUMERIC_INPUT)
|
|
91
92
|
|
|
@@ -107,7 +108,7 @@ class NumericInputWidget(IWidget):
|
|
|
107
108
|
class RadiobuttonWidget(IWidget):
|
|
108
109
|
"""Radiobutton widget."""
|
|
109
110
|
|
|
110
|
-
def __init__(self, fields: list[str]):
|
|
111
|
+
def __init__(self, fields: list[str]) -> None:
|
|
111
112
|
"""Initialize the RadiobuttonWidget.
|
|
112
113
|
|
|
113
114
|
Args:
|
|
@@ -118,7 +119,8 @@ class RadiobuttonWidget(IWidget):
|
|
|
118
119
|
"""
|
|
119
120
|
super().__init__(WidgetType.RADIOBUTTON)
|
|
120
121
|
if not fields:
|
|
121
|
-
|
|
122
|
+
msg = "RadiobuttonWidget must have at least one field"
|
|
123
|
+
raise ValueError(msg)
|
|
122
124
|
self.info["fields"] = fields
|
|
123
125
|
|
|
124
126
|
def convert_data(self, input_data: str) -> str:
|
|
@@ -136,7 +138,7 @@ class RadiobuttonWidget(IWidget):
|
|
|
136
138
|
class CheckboxWidget(IWidget):
|
|
137
139
|
"""Checkbox widget."""
|
|
138
140
|
|
|
139
|
-
def __init__(self, fields: list[str]):
|
|
141
|
+
def __init__(self, fields: list[str]) -> None:
|
|
140
142
|
"""Initialize the CheckboxWidget.
|
|
141
143
|
|
|
142
144
|
Args:
|
|
@@ -147,7 +149,8 @@ class CheckboxWidget(IWidget):
|
|
|
147
149
|
"""
|
|
148
150
|
super().__init__(WidgetType.CHECKBOX)
|
|
149
151
|
if not fields:
|
|
150
|
-
|
|
152
|
+
msg = "Checkbox must have at least one field"
|
|
153
|
+
raise ValueError(msg)
|
|
151
154
|
self.info["fields"] = fields
|
|
152
155
|
|
|
153
156
|
def convert_data(self, input_data: str) -> list[str] | None:
|
|
@@ -168,7 +171,7 @@ class CheckboxWidget(IWidget):
|
|
|
168
171
|
class ImageWidget(IWidget):
|
|
169
172
|
"""Image widget."""
|
|
170
173
|
|
|
171
|
-
def __init__(self, address: str, format: str = "image", width: int = 100):
|
|
174
|
+
def __init__(self, address: str, format: str = "image", width: int = 100) -> None: # noqa: A002
|
|
172
175
|
"""Validate the image fields and defines the base64 if it does not exist.
|
|
173
176
|
|
|
174
177
|
Args:
|
|
@@ -182,20 +185,22 @@ class ImageWidget(IWidget):
|
|
|
182
185
|
super().__init__(WidgetType.IMAGE)
|
|
183
186
|
|
|
184
187
|
if width < 1:
|
|
185
|
-
|
|
188
|
+
msg = "Width must be positive"
|
|
189
|
+
raise WidgetInfoError(msg)
|
|
186
190
|
|
|
187
191
|
self.info["address"] = address
|
|
188
192
|
self.info["format"] = format
|
|
189
193
|
self.info["width"] = width
|
|
190
194
|
|
|
191
195
|
try:
|
|
192
|
-
with open(address, "rb") as file:
|
|
196
|
+
with open(address, "rb") as file: # noqa: PTH123
|
|
193
197
|
file_data = file.read()
|
|
194
198
|
except FileNotFoundError:
|
|
195
|
-
|
|
199
|
+
msg = "The image address is invalid"
|
|
200
|
+
raise WidgetInfoError(msg) # noqa: B904
|
|
196
201
|
self.info["base64"] = base64.b64encode(file_data).decode("utf-8")
|
|
197
202
|
|
|
198
|
-
def convert_data(self, input_data: str | None = None) -> bool:
|
|
203
|
+
def convert_data(self, input_data: str | None = None) -> bool: # noqa: ARG002
|
|
199
204
|
"""Get the image widget data, i.e. None.
|
|
200
205
|
|
|
201
206
|
Args:
|
|
@@ -219,17 +224,23 @@ class StepWidget(IWidget):
|
|
|
219
224
|
WidgetInfoError: If the text or widget are not provided.
|
|
220
225
|
"""
|
|
221
226
|
|
|
222
|
-
def __init__(
|
|
227
|
+
def __init__(
|
|
228
|
+
self,
|
|
229
|
+
title: str,
|
|
230
|
+
text: str | None,
|
|
231
|
+
widget: ImageWidget | None,
|
|
232
|
+
) -> None:
|
|
223
233
|
super().__init__(WidgetType.STEP)
|
|
224
234
|
if text is None and widget is None:
|
|
225
|
-
|
|
235
|
+
msg = "Text or widget must be provided"
|
|
236
|
+
raise WidgetInfoError(msg)
|
|
226
237
|
self.info["title"] = title
|
|
227
238
|
if isinstance(text, str):
|
|
228
239
|
self.info["text"] = text
|
|
229
240
|
if isinstance(widget, ImageWidget):
|
|
230
241
|
self.info["widget"] = widget.__dict__
|
|
231
242
|
|
|
232
|
-
def convert_data(self, input_data: str) -> bool:
|
|
243
|
+
def convert_data(self, input_data: str) -> bool: # noqa: ARG002
|
|
233
244
|
"""Get the step widget data in the correct format.
|
|
234
245
|
|
|
235
246
|
Args:
|
|
@@ -244,7 +255,7 @@ class StepWidget(IWidget):
|
|
|
244
255
|
class MultistepWidget(IWidget):
|
|
245
256
|
"""Multistep widget."""
|
|
246
257
|
|
|
247
|
-
def __init__(self, steps: list[StepWidget]):
|
|
258
|
+
def __init__(self, steps: list[StepWidget]) -> None:
|
|
248
259
|
"""Initialize the MultistepWidget.
|
|
249
260
|
|
|
250
261
|
Args:
|
|
@@ -255,12 +266,13 @@ class MultistepWidget(IWidget):
|
|
|
255
266
|
"""
|
|
256
267
|
super().__init__(WidgetType.MULTISTEP)
|
|
257
268
|
if not steps:
|
|
258
|
-
|
|
269
|
+
msg = "MultistepWidget must have at least one step"
|
|
270
|
+
raise ValueError(msg)
|
|
259
271
|
self.info["steps"] = []
|
|
260
272
|
for step in steps:
|
|
261
273
|
self.info["steps"].append(step.__dict__)
|
|
262
274
|
|
|
263
|
-
def convert_data(self, input_data: str) -> bool:
|
|
275
|
+
def convert_data(self, input_data: str) -> bool: # noqa: ARG002
|
|
264
276
|
"""Get the multistep widget data in the correct format.
|
|
265
277
|
|
|
266
278
|
Args:
|
|
@@ -287,7 +299,7 @@ class DialogBox:
|
|
|
287
299
|
dialog_text: str,
|
|
288
300
|
title_bar: str | None = None,
|
|
289
301
|
widget: IWidget | None = None,
|
|
290
|
-
):
|
|
302
|
+
) -> None:
|
|
291
303
|
self.widget: IWidget = BaseWidget() if widget is None else widget
|
|
292
304
|
self.dialog_text: str = dialog_text
|
|
293
305
|
self.title_bar: str | None = title_bar
|
|
@@ -5,40 +5,40 @@
|
|
|
5
5
|
class HardpyError(Exception):
|
|
6
6
|
"""Base HardPy exception."""
|
|
7
7
|
|
|
8
|
-
def __init__(self, msg: str):
|
|
8
|
+
def __init__(self, msg: str) -> None:
|
|
9
9
|
super().__init__(f"HardPy error: {msg}")
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class DuplicateSerialNumberError(HardpyError):
|
|
13
13
|
"""The serial number has already been determined."""
|
|
14
14
|
|
|
15
|
-
def __init__(self):
|
|
15
|
+
def __init__(self) -> None:
|
|
16
16
|
super().__init__(self.__doc__) # type: ignore
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class DuplicatePartNumberError(HardpyError):
|
|
20
20
|
"""The part number has already been determined."""
|
|
21
21
|
|
|
22
|
-
def __init__(self):
|
|
22
|
+
def __init__(self) -> None:
|
|
23
23
|
super().__init__(self.__doc__) # type: ignore
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class DuplicateTestStandNameError(HardpyError):
|
|
27
27
|
"""The test stand name has already been determined."""
|
|
28
28
|
|
|
29
|
-
def __init__(self):
|
|
29
|
+
def __init__(self) -> None:
|
|
30
30
|
super().__init__(self.__doc__) # type: ignore
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
class
|
|
34
|
-
"""The
|
|
33
|
+
class DuplicateTestStandLocationError(HardpyError):
|
|
34
|
+
"""The test stand location has already been determined."""
|
|
35
35
|
|
|
36
|
-
def __init__(self):
|
|
36
|
+
def __init__(self) -> None:
|
|
37
37
|
super().__init__(self.__doc__) # type: ignore
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
class WidgetInfoError(HardpyError):
|
|
41
41
|
"""The widget info is not correct."""
|
|
42
42
|
|
|
43
|
-
def __init__(self, message):
|
|
43
|
+
def __init__(self, message: str) -> None:
|
|
44
44
|
super().__init__(message)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from platform import node as platform_node
|
|
2
|
+
|
|
3
|
+
import machineid
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def machine_id() -> str:
|
|
7
|
+
"""Get machine id.
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
str: id, if available, otherwise MAC address
|
|
11
|
+
"""
|
|
12
|
+
try:
|
|
13
|
+
return machineid.id()
|
|
14
|
+
except machineid.MachineIdNotFound:
|
|
15
|
+
return platform_node()
|
|
@@ -1,12 +1,14 @@
|
|
|
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 re
|
|
5
6
|
from logging import getLogger
|
|
6
7
|
from pathlib import Path
|
|
7
|
-
from typing import NamedTuple
|
|
8
|
+
from typing import TYPE_CHECKING, NamedTuple
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pytest import Item, Mark
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
class TestDependencyInfo(NamedTuple):
|
|
@@ -22,7 +24,7 @@ class TestDependencyInfo(NamedTuple):
|
|
|
22
24
|
class NodeInfo:
|
|
23
25
|
"""Test node info."""
|
|
24
26
|
|
|
25
|
-
def __init__(self, item: Item):
|
|
27
|
+
def __init__(self, item: Item) -> None:
|
|
26
28
|
self._item = item
|
|
27
29
|
self._log = getLogger(__name__)
|
|
28
30
|
|
|
@@ -31,19 +33,21 @@ class NodeInfo:
|
|
|
31
33
|
"case_name",
|
|
32
34
|
)
|
|
33
35
|
self._module_name = self._get_human_name(
|
|
34
|
-
item.parent.own_markers,
|
|
36
|
+
item.parent.own_markers, # type: ignore
|
|
35
37
|
"module_name",
|
|
36
38
|
)
|
|
37
39
|
|
|
38
40
|
self._dependency = self._get_dependency_info(
|
|
39
|
-
item.own_markers + item.parent.own_markers
|
|
41
|
+
item.own_markers + item.parent.own_markers, # type: ignore
|
|
40
42
|
)
|
|
41
43
|
|
|
42
|
-
self.
|
|
44
|
+
self._attempt = self._get_attempt(item.own_markers)
|
|
45
|
+
|
|
46
|
+
self._module_id = Path(item.parent.nodeid).stem # type: ignore
|
|
43
47
|
self._case_id = item.name
|
|
44
48
|
|
|
45
49
|
@property
|
|
46
|
-
def module_id(self):
|
|
50
|
+
def module_id(self) -> str:
|
|
47
51
|
"""Get module id.
|
|
48
52
|
|
|
49
53
|
Returns:
|
|
@@ -52,7 +56,7 @@ class NodeInfo:
|
|
|
52
56
|
return self._module_id
|
|
53
57
|
|
|
54
58
|
@property
|
|
55
|
-
def case_id(self):
|
|
59
|
+
def case_id(self) -> str:
|
|
56
60
|
"""Get case id.
|
|
57
61
|
|
|
58
62
|
Returns:
|
|
@@ -61,7 +65,7 @@ class NodeInfo:
|
|
|
61
65
|
return self._case_id
|
|
62
66
|
|
|
63
67
|
@property
|
|
64
|
-
def module_name(self):
|
|
68
|
+
def module_name(self) -> str:
|
|
65
69
|
"""Get module name.
|
|
66
70
|
|
|
67
71
|
Returns:
|
|
@@ -70,7 +74,7 @@ class NodeInfo:
|
|
|
70
74
|
return self._module_name
|
|
71
75
|
|
|
72
76
|
@property
|
|
73
|
-
def case_name(self):
|
|
77
|
+
def case_name(self) -> str:
|
|
74
78
|
"""Get case name.
|
|
75
79
|
|
|
76
80
|
Returns:
|
|
@@ -79,7 +83,7 @@ class NodeInfo:
|
|
|
79
83
|
return self._case_name
|
|
80
84
|
|
|
81
85
|
@property
|
|
82
|
-
def dependency(self):
|
|
86
|
+
def dependency(self) -> TestDependencyInfo | str:
|
|
83
87
|
"""Get dependency information.
|
|
84
88
|
|
|
85
89
|
Returns:
|
|
@@ -87,6 +91,15 @@ class NodeInfo:
|
|
|
87
91
|
"""
|
|
88
92
|
return self._dependency
|
|
89
93
|
|
|
94
|
+
@property
|
|
95
|
+
def attempt(self) -> int:
|
|
96
|
+
"""Get attempt.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
int: attempt number
|
|
100
|
+
"""
|
|
101
|
+
return self._attempt
|
|
102
|
+
|
|
90
103
|
def _get_human_name(self, markers: list[Mark], marker_name: str) -> str:
|
|
91
104
|
"""Get human name from markers.
|
|
92
105
|
|
|
@@ -98,10 +111,8 @@ class NodeInfo:
|
|
|
98
111
|
str: human name by user
|
|
99
112
|
"""
|
|
100
113
|
for marker in markers:
|
|
101
|
-
if marker.name == marker_name:
|
|
102
|
-
|
|
103
|
-
return marker.args[0]
|
|
104
|
-
|
|
114
|
+
if marker.name == marker_name and marker.args:
|
|
115
|
+
return marker.args[0]
|
|
105
116
|
return ""
|
|
106
117
|
|
|
107
118
|
def _get_dependency_info(self, markers: list[Mark]) -> TestDependencyInfo | str:
|
|
@@ -118,8 +129,26 @@ class NodeInfo:
|
|
|
118
129
|
dependency_data = re.search(r"(\w+)::(\w+)", dependency_value)
|
|
119
130
|
if dependency_data:
|
|
120
131
|
return TestDependencyInfo(*dependency_data.groups())
|
|
121
|
-
elif re.search(r"^\w+$", dependency_value):
|
|
132
|
+
elif re.search(r"^\w+$", dependency_value): # noqa: RET505
|
|
122
133
|
return TestDependencyInfo(dependency_value, None)
|
|
123
134
|
elif dependency_data is None and dependency_value == "":
|
|
124
135
|
return ""
|
|
125
136
|
raise ValueError
|
|
137
|
+
|
|
138
|
+
def _get_attempt(self, markers: list[Mark]) -> int:
|
|
139
|
+
"""Get the number of attempts.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
markers (list[Mark]): item markers list
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
int: number of attempts
|
|
146
|
+
"""
|
|
147
|
+
attempt: int = 1
|
|
148
|
+
for marker in markers:
|
|
149
|
+
if marker.name == "attempt" and marker.args:
|
|
150
|
+
attempt = marker.args[0]
|
|
151
|
+
if not isinstance(attempt, int) or attempt < 1:
|
|
152
|
+
msg = "The 'attempt' marker value must be a positive integer."
|
|
153
|
+
raise ValueError(msg)
|
|
154
|
+
return attempt
|
|
@@ -7,12 +7,12 @@ from logging import getLogger
|
|
|
7
7
|
class ProgressCalculator:
|
|
8
8
|
"""Test run progress calculator."""
|
|
9
9
|
|
|
10
|
-
def __init__(self):
|
|
10
|
+
def __init__(self) -> None:
|
|
11
11
|
self._progress_nodeids: set[str] = set()
|
|
12
12
|
self._tests_amount: int = 1
|
|
13
13
|
self._log = getLogger(__name__)
|
|
14
14
|
|
|
15
|
-
def set_test_amount(self, amount: int):
|
|
15
|
+
def set_test_amount(self, amount: int) -> None:
|
|
16
16
|
"""Set test amount.
|
|
17
17
|
|
|
18
18
|
Raises:
|
|
@@ -22,7 +22,8 @@ class ProgressCalculator:
|
|
|
22
22
|
amount (int): test amount
|
|
23
23
|
"""
|
|
24
24
|
if amount <= 0:
|
|
25
|
-
|
|
25
|
+
msg = "Test amount must be greater than 0."
|
|
26
|
+
raise ValueError(msg)
|
|
26
27
|
self._tests_amount = amount
|
|
27
28
|
|
|
28
29
|
def calculate(self, nodeid: str) -> int:
|
|
@@ -1,23 +1,30 @@
|
|
|
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
|
-
"""Singleton class.
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
T = TypeVar("T")
|
|
8
8
|
|
|
9
|
-
def __init__(self):
|
|
10
|
-
if not self._initialized:
|
|
11
|
-
...
|
|
12
|
-
your code
|
|
13
|
-
...
|
|
14
|
-
self._initialized = True
|
|
15
|
-
"""
|
|
16
9
|
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
class SingletonMeta(type, Generic[T]): # noqa: D101
|
|
11
|
+
_instances: dict[SingletonMeta[T], T] = {} # noqa: RUF012
|
|
19
12
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
def __call__(cls, *args, **kwargs) -> T: # noqa: ANN002, ANN003
|
|
14
|
+
"""Magic method to create an instance of the class.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
*args: Variable length argument list
|
|
18
|
+
**kwargs: Arbitrary keyword arguments
|
|
19
|
+
Returns:
|
|
20
|
+
object: An instance of the class
|
|
21
|
+
"""
|
|
22
|
+
if cls not in cls._instances:
|
|
23
|
+
cls._instances[cls] = super().__call__(
|
|
24
|
+
*args,
|
|
25
|
+
**kwargs,
|
|
26
|
+
)
|
|
27
|
+
return cls._instances[cls]
|
|
28
|
+
|
|
29
|
+
def __instancecheck__(cls, instance: object) -> bool:
|
|
30
|
+
return cls.__subclasscheck__(type(instance))
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: hardpy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: HardPy library for device testing
|
|
5
5
|
Project-URL: Homepage, https://github.com/everypinio/hardpy/
|
|
6
6
|
Project-URL: Documentation, https://everypinio.github.io/hardpy/
|
|
7
7
|
Project-URL: Repository, https://github.com/everypinio/hardpy/
|
|
8
8
|
Project-URL: Changelog, https://everypinio.github.io/hardpy/changelog/
|
|
9
|
-
Author: Everypin
|
|
9
|
+
Author-email: Everypin <info@everypin.io>
|
|
10
|
+
License-Expression: GPL-3.0-or-later
|
|
10
11
|
License-File: LICENSE
|
|
11
12
|
Classifier: Development Status :: 4 - Beta
|
|
12
13
|
Classifier: Framework :: Pytest
|
|
13
14
|
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
15
15
|
Classifier: Operating System :: OS Independent
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
17
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
@@ -22,10 +22,11 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
22
22
|
Classifier: Topic :: Software Development :: Testing
|
|
23
23
|
Classifier: Topic :: Software Development :: Testing :: Acceptance
|
|
24
24
|
Classifier: Topic :: Utilities
|
|
25
|
-
Requires-Python:
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
26
|
Requires-Dist: fastapi>=0.100.1
|
|
27
27
|
Requires-Dist: glom>=23.3.0
|
|
28
28
|
Requires-Dist: natsort>=8.4.0
|
|
29
|
+
Requires-Dist: py-machineid~=0.6.0
|
|
29
30
|
Requires-Dist: pycouchdb>=1.14.2
|
|
30
31
|
Requires-Dist: pydantic>=2.4.0
|
|
31
32
|
Requires-Dist: pytest<9,>=7
|
|
@@ -35,30 +36,27 @@ Requires-Dist: uvicorn>=0.23.2
|
|
|
35
36
|
Provides-Extra: build
|
|
36
37
|
Requires-Dist: build==1.0.3; extra == 'build'
|
|
37
38
|
Provides-Extra: dev
|
|
38
|
-
Requires-Dist:
|
|
39
|
-
Requires-Dist:
|
|
40
|
-
Requires-Dist: wemake-python-styleguide; extra == 'dev'
|
|
39
|
+
Requires-Dist: mypy>=1.11.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: ruff>=0.5.6; extra == 'dev'
|
|
41
|
+
Requires-Dist: wemake-python-styleguide>=0.19.2; extra == 'dev'
|
|
41
42
|
Description-Content-Type: text/markdown
|
|
42
43
|
|
|
43
44
|
<h1 align="center">
|
|
44
|
-
<img src="https://
|
|
45
|
-
</h1>
|
|
46
|
-
|
|
47
|
-
<h1 align="center">
|
|
48
|
-
<b>HardPy</b>
|
|
45
|
+
<img src="https://raw.githubusercontent.com/everypinio/hardpy/main/docs/img/logo256.png" alt="HardPy" style="width:150px;">
|
|
49
46
|
</h1>
|
|
50
47
|
|
|
51
48
|
<p align="center">
|
|
52
49
|
HardPy is a python library for creating a test bench for devices.
|
|
53
50
|
</p>
|
|
54
51
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
**Documentation**: <a href=https://everypinio.github.io/hardpy/ target="_blank">https://everypinio.github.io/hardpy/</a>
|
|
52
|
+
<div align="center">
|
|
58
53
|
|
|
59
|
-
|
|
54
|
+
[](https://badge.fury.io/py/hardpy)
|
|
55
|
+

|
|
56
|
+
[](https://docs.pytest.org/en/latest/)
|
|
57
|
+
[](https://everypinio.github.io/hardpy/)
|
|
60
58
|
|
|
61
|
-
|
|
59
|
+
</div>
|
|
62
60
|
|
|
63
61
|
---
|
|
64
62
|
|
|
@@ -70,6 +68,10 @@ HardPy allows you to:
|
|
|
70
68
|
* Use a browser to view, start, stop, and interact with tests;
|
|
71
69
|
* Store test results in the [CouchDB](https://couchdb.apache.org/) database.
|
|
72
70
|
|
|
71
|
+
<h1 align="center">
|
|
72
|
+
<img src="https://raw.githubusercontent.com/everypinio/hardpy/main/docs/img/hardpy_panel.gif" alt="hardpy panel" style="width:550px;">
|
|
73
|
+
</h1>
|
|
74
|
+
|
|
73
75
|
## To Install
|
|
74
76
|
|
|
75
77
|
```bash
|
|
@@ -92,21 +94,10 @@ docker compose up -d
|
|
|
92
94
|
hardpy run
|
|
93
95
|
```
|
|
94
96
|
4. View operator panel in browser: http://localhost:8000/
|
|
95
|
-
|
|
96
|
-
<h1 align="center">
|
|
97
|
-
<img src="https://everypinio.github.io/hardpy/img/hardpy_operator_panel_hello_hardpy.png"
|
|
98
|
-
alt="hardpy operator panel" style="width:600px;">
|
|
99
|
-
</h1>
|
|
100
|
-
|
|
101
97
|
5. View the latest test report: http://localhost:5984/_utils
|
|
102
98
|
|
|
103
99
|
Login and password: **dev**, database - **runstore**, document - **current**.
|
|
104
100
|
|
|
105
|
-
<h1 align="center">
|
|
106
|
-
<img src="https://everypinio.github.io/hardpy/img/runstore_hello_hardpy.png"
|
|
107
|
-
alt="hardpy runstore" style="width:500px;">
|
|
108
|
-
</h1>
|
|
109
|
-
|
|
110
101
|
## Examples
|
|
111
102
|
|
|
112
103
|
Find more examples of using the **HardPy** in the [examples](https://github.com/everypinio/hardpy/tree/main/examples) folder.
|