hardpy 0.13.0__py3-none-any.whl → 0.15.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 (38) hide show
  1. hardpy/__init__.py +45 -10
  2. hardpy/cli/cli.py +32 -28
  3. hardpy/common/config.py +5 -4
  4. hardpy/common/stand_cloud/connector.py +2 -2
  5. hardpy/hardpy_panel/api.py +13 -3
  6. hardpy/hardpy_panel/frontend/dist/assets/{allPaths-Cg7WZDXy.js → allPaths-CV5wjLMB.js} +1 -1
  7. hardpy/hardpy_panel/frontend/dist/assets/{allPathsLoader-C79wUwqR.js → allPathsLoader-JIzW_pSb.js} +2 -2
  8. hardpy/hardpy_panel/frontend/dist/assets/browser-ponyfill-CccdstaD.js +2 -0
  9. hardpy/hardpy_panel/frontend/dist/assets/index-6RIgWzcZ.js +790 -0
  10. hardpy/hardpy_panel/frontend/dist/assets/{splitPathsBySizeLoader-hWuLTMwD.js → splitPathsBySizeLoader-DkZadBcn.js} +1 -1
  11. hardpy/hardpy_panel/frontend/dist/index.html +1 -1
  12. hardpy/hardpy_panel/frontend/dist/locales/de/translation.json +60 -0
  13. hardpy/hardpy_panel/frontend/dist/locales/en/translation.json +60 -0
  14. hardpy/hardpy_panel/frontend/dist/locales/es/translation.json +60 -0
  15. hardpy/hardpy_panel/frontend/dist/locales/fr/translation.json +60 -0
  16. hardpy/hardpy_panel/frontend/dist/locales/ja/translation.json +60 -0
  17. hardpy/hardpy_panel/frontend/dist/locales/ru/translation.json +60 -0
  18. hardpy/hardpy_panel/frontend/dist/locales/zh/translation.json +60 -0
  19. hardpy/pytest_hardpy/db/base_store.py +23 -0
  20. hardpy/pytest_hardpy/db/const.py +40 -19
  21. hardpy/pytest_hardpy/db/schema/v1.py +140 -326
  22. hardpy/pytest_hardpy/plugin.py +32 -1
  23. hardpy/pytest_hardpy/pytest_call.py +331 -22
  24. hardpy/pytest_hardpy/pytest_wrapper.py +25 -34
  25. hardpy/pytest_hardpy/reporter/hook_reporter.py +53 -2
  26. hardpy/pytest_hardpy/result/report_loader/stand_cloud_loader.py +8 -2
  27. hardpy/pytest_hardpy/result/report_reader/couchdb_reader.py +1 -5
  28. hardpy/pytest_hardpy/utils/__init__.py +25 -11
  29. hardpy/pytest_hardpy/utils/const.py +72 -0
  30. hardpy/pytest_hardpy/utils/exception.py +7 -32
  31. hardpy/pytest_hardpy/utils/node_info.py +55 -1
  32. hardpy/pytest_hardpy/utils/stand_type.py +198 -0
  33. {hardpy-0.13.0.dist-info → hardpy-0.15.0.dist-info}/METADATA +2 -1
  34. {hardpy-0.13.0.dist-info → hardpy-0.15.0.dist-info}/RECORD +37 -28
  35. hardpy/hardpy_panel/frontend/dist/assets/index-De5CJ3kt.js +0 -790
  36. {hardpy-0.13.0.dist-info → hardpy-0.15.0.dist-info}/WHEEL +0 -0
  37. {hardpy-0.13.0.dist-info → hardpy-0.15.0.dist-info}/entry_points.txt +0 -0
  38. {hardpy-0.13.0.dist-info → hardpy-0.15.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,11 +2,20 @@
2
2
  # GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
3
3
  from __future__ import annotations
4
4
 
5
+ from abc import ABC
6
+ from collections.abc import Mapping # noqa: TC003
7
+ from datetime import datetime # noqa: TC003
5
8
  from typing import ClassVar
6
9
 
7
10
  from pydantic import BaseModel, ConfigDict, Field
8
11
 
9
- from hardpy.pytest_hardpy.utils import TestStatus as Status # noqa: TC001
12
+ from hardpy.pytest_hardpy.utils import (
13
+ ChartType,
14
+ ComparisonOperation as CompOp,
15
+ Group,
16
+ MeasurementType,
17
+ TestStatus as Status,
18
+ )
10
19
 
11
20
 
12
21
  class IBaseResult(BaseModel):
@@ -21,203 +30,165 @@ class IBaseResult(BaseModel):
21
30
 
22
31
 
23
32
  class CaseStateStore(IBaseResult):
24
- """Test case description.
25
-
26
- Example:
27
- ```
28
- {
29
- "test_one": {
30
- "status": "passed",
31
- "name": "Test 2",
32
- "start_time": 1695817188,
33
- "stop_time": 1695817189,
34
- "assertion_msg": null,
35
- "msg": null,
36
- "attempt": 1,
37
- "dialog_box": {
38
- "title_bar": "Example of text input",
39
- "dialog_text": "Type some text and press the Confirm button",
40
- "widget": {
41
- "info": {
42
- "text": "some text"
43
- },
44
- "type": "textinput"
45
- },
46
- visible: true,
47
- id: "af6ac3e7-7ce8-4a6b-bb9d-88c3e10b5c7a",
48
- font_size: 14
49
- }
50
- }
51
- }
52
- ```
53
- """
33
+ """Test case description."""
54
34
 
55
35
  assertion_msg: str | None = None
56
36
  msg: dict | None = None
57
- dialog_box: dict = {}
37
+ measurements: list[NumericMeasurement | StringMeasurement] = []
38
+ chart: Chart | None = None
58
39
  attempt: int = 0
40
+ group: Group
41
+ dialog_box: dict = {}
59
42
 
60
43
 
61
44
  class CaseRunStore(IBaseResult):
62
- """Test case description with artifact.
63
-
64
- Example:
65
- ```
66
- {
67
- "test_one": {
68
- "status": "passed",
69
- "name": "Test 2",
70
- "start_time": 1695817188,
71
- "stop_time": 1695817189,
72
- "assertion_msg": null,
73
- "msg": null,
74
- "artifact": {}
75
- }
76
- }
77
- ```
78
- """
45
+ """Test case description with artifact."""
79
46
 
80
47
  assertion_msg: str | None = None
81
48
  msg: dict | None = None
49
+ measurements: list[NumericMeasurement | StringMeasurement] = []
50
+ chart: Chart | None = None
51
+ attempt: int = 0
52
+ group: Group
82
53
  artifact: dict = {}
83
54
 
84
55
 
85
56
  class ModuleStateStore(IBaseResult):
86
- """Test module description.
87
-
88
- Example:
89
- ```
90
- {
91
- "test_2_b": {
92
- "status": "passed",
93
- "name": "Module 2",
94
- "start_time": 1695816886,
95
- "stop_time": 1695817016,
96
- "cases": {
97
- "test_one": {
98
- "status": "passed",
99
- "name": "Test 1",
100
- "start_time": 1695817015,
101
- "stop_time": 1695817016,
102
- "assertion_msg": null,
103
- "msg": null
104
- }
105
- }
106
- }
107
- }
108
- ```
109
- """
57
+ """Test module description."""
110
58
 
111
59
  cases: dict[str, CaseStateStore] = {}
60
+ group: Group
112
61
 
113
62
 
114
63
  class ModuleRunStore(IBaseResult):
115
- """Test module description.
116
-
117
- Example:
118
- ```
119
- {
120
- "test_2_b": {
121
- "status": "passed",
122
- "name": "Module 2",
123
- "start_time": 1695816886,
124
- "stop_time": 1695817016,
125
- "artifact": {},
126
- "cases": {
127
- "test_one": {
128
- "status": "passed",
129
- "name": "Test 1",
130
- "start_time": 1695817015,
131
- "stop_time": 1695817016,
132
- "assertion_msg": null,
133
- "msg": null,
134
- "artifact": {}
135
- }
136
- }
137
- }
138
- }
139
- ```
140
- """
64
+ """Test module description."""
141
65
 
142
66
  cases: dict[str, CaseRunStore] = {}
67
+ group: Group
143
68
  artifact: dict = {}
144
69
 
145
70
 
146
71
  class Dut(BaseModel):
147
- """Device under test description.
148
-
149
- Example:
150
- ```
151
- {
152
- "dut": {
153
- "serial_number": "a9ad8dca-2c64-4df8-a358-c21e832a32e4",
154
- "part_number": "part_number_1",
155
- "info": {
156
- "batch": "test_batch",
157
- "board_rev": "rev_1"
158
- }
159
- }
160
- }
161
- ```
162
- """
72
+ """Device under test description."""
73
+
74
+ model_config = ConfigDict(extra="forbid")
75
+
76
+ name: str | None = None
77
+ type: str | None = None
78
+ serial_number: str | None = None
79
+ part_number: str | None = None
80
+ revision: str | None = None
81
+ sub_units: list[SubUnit] = []
82
+ info: Mapping[str, str | int | float | datetime] = {}
83
+
84
+
85
+ class SubUnit(BaseModel):
86
+ """Sub unit of DUT description."""
87
+
88
+ model_config = ConfigDict(extra="forbid")
89
+
90
+ name: str | None = None
91
+ type: str | None = None
92
+ serial_number: str | None = None
93
+ part_number: str | None = None
94
+ revision: str | None = None
95
+ info: Mapping[str, str | int | float | datetime] = {}
96
+
97
+
98
+ class Instrument(BaseModel):
99
+ """Instrument (power supply, oscilloscope and others) description."""
163
100
 
164
101
  model_config = ConfigDict(extra="forbid")
165
102
 
166
- serial_number: str | None
167
- part_number: str | None
168
- info: dict = {}
103
+ name: str | None = None
104
+ revision: str | None = None
105
+ number: int | None = None
106
+ comment: str | None = None
107
+ info: Mapping[str, str | int | float | datetime] = {}
169
108
 
170
109
 
171
110
  class TestStand(BaseModel):
172
- """Test stand description.
173
-
174
- Example:
175
- ```
176
- {
177
- "test_stand": {
178
- "hw_id": "840982098ca2459a7b22cc608eff65d4",
179
- "name": "test_stand_1",
180
- "info": {
181
- "geo": "Belgrade"
182
- },
183
- "timezone": "Europe/Belgrade",
184
- "drivers": {
185
- "driver_1": "driver info",
186
- "driver_2": {
187
- "state": "active",
188
- "port": 8000
189
- }
190
- },
191
- "location": "Belgrade_1",
192
- "number": 2
193
- }
194
- }
195
- ```
196
- """
111
+ """Test stand description."""
197
112
 
198
113
  model_config = ConfigDict(extra="forbid")
199
114
 
200
115
  hw_id: str | None = None
201
116
  name: str | None = None
117
+ revision: str | None = None
202
118
  timezone: str | None = None
203
- drivers: dict = {}
204
- info: dict = {}
205
119
  location: str | None = None
206
120
  number: int | None = None
121
+ drivers: dict = {} # deprecated, remove in v2
122
+ instruments: list[Instrument] = []
123
+ info: Mapping[str, str | int | float | datetime] = {}
124
+
125
+
126
+ class Process(BaseModel):
127
+ """Production process description."""
128
+
129
+ model_config = ConfigDict(extra="forbid")
130
+
131
+ name: str | None = None
132
+ number: int | None = None
133
+ info: Mapping[str, str | int | float | datetime] = {}
134
+
135
+
136
+ class IBaseMeasurement(BaseModel, ABC):
137
+ """Base class for all measurement models."""
138
+
139
+ model_config = ConfigDict(extra="allow")
140
+
141
+ type: MeasurementType
142
+ name: str | None = Field(default=None)
143
+ operation: CompOp | None = Field(default=None)
144
+ result: bool | None = Field(default_factory=lambda: None)
145
+
146
+
147
+ class NumericMeasurement(IBaseMeasurement):
148
+ """Numeric measurement description."""
149
+
150
+ model_config = ConfigDict(extra="forbid")
151
+
152
+ type: MeasurementType = Field(default=MeasurementType.NUMERIC)
153
+ value: int | float
154
+ name: str | None = Field(default=None)
155
+ unit: str | None = Field(default=None)
156
+
157
+ comparison_value: float | int | None = Field(default=None)
158
+
159
+ lower_limit: float | int | None = Field(default=None)
160
+ upper_limit: float | int | None = Field(default=None)
161
+
162
+
163
+ class StringMeasurement(IBaseMeasurement):
164
+ """String measurement description."""
165
+
166
+ model_config = ConfigDict(extra="forbid")
167
+
168
+ type: MeasurementType = Field(default=MeasurementType.STRING)
169
+ value: str
170
+ name: str | None = Field(default=None)
171
+ casesensitive: bool = Field(default=True)
172
+
173
+ comparison_value: str | None = Field(default=None)
174
+
175
+
176
+ class Chart(BaseModel):
177
+ """Chart description."""
178
+
179
+ model_config = ConfigDict(extra="forbid")
180
+
181
+ type: ChartType = Field(default=ChartType.LINE)
182
+ title: str | None = Field(default=None)
183
+ x_label: str | None = Field(default=None)
184
+ y_label: str | None = Field(default=None)
185
+ marker_name: list[str | None] = Field(default=[])
186
+ x_data: list[list[int | float]] = Field(default_factory=lambda: []) # noqa: PIE807
187
+ y_data: list[list[int | float]] = Field(default_factory=lambda: []) # noqa: PIE807
207
188
 
208
189
 
209
190
  class OperatorData(BaseModel):
210
- """Operator data from operator panel.
211
-
212
- Example:
213
- ```
214
- {
215
- "operator_data": {
216
- "dialog": "hello",
217
- }
218
- }
219
- ```
220
- """
191
+ """Operator data from operator panel."""
221
192
 
222
193
  model_config = ConfigDict(extra="forbid")
223
194
 
@@ -225,191 +196,29 @@ class OperatorData(BaseModel):
225
196
 
226
197
 
227
198
  class ResultStateStore(IBaseResult):
228
- """Test run description.
229
-
230
- Example:
231
- ```
232
- {
233
- "_rev": "44867-3888ae85c19c428cc46685845953b483",
234
- "_id": "current",
235
- "progress": 100,
236
- "stop_time": 1695817266,
237
- "start_time": 1695817263,
238
- "status": "failed",
239
- "name": "hardpy-stand",
240
- "alert": "",
241
- "operator_data": {
242
- "dialog": ""
243
- },
244
- "dut": {
245
- "serial_number": "92c5a4bb-ecb0-42c5-89ac-e0caca0919fd",
246
- "part_number": "part_1",
247
- "info": {
248
- "batch": "test_batch",
249
- "board_rev": "rev_1"
250
- }
251
- },
252
- "test_stand": {
253
- "hw_id": "840982098ca2459a7b22cc608eff65d4",
254
- "name": "test_stand_1",
255
- "info": {
256
- "geo": "Belgrade"
257
- },
258
- "timezone": "Europe/Belgrade",
259
- "drivers": {
260
- "driver_1": "driver info",
261
- "driver_2": {
262
- "state": "active",
263
- "port": 8000
264
- }
265
- },
266
- "location": "Belgrade_1",
267
- "number": 2
268
- },
269
- "operator_msg": {
270
- "msg": "Operator message",
271
- "title": "Message",
272
- "visible": true,
273
- "id": "f45ac1e7-2ce8-4a6b-bb9d-8863e30bcc78"
274
- },
275
- "modules": {
276
- "test_1_a": {
277
- "status": "failed",
278
- "name": "Module 1",
279
- "start_time": 1695816884,
280
- "stop_time": 1695817265,
281
- "cases": {
282
- "test_dut_info": {
283
- "status": "passed",
284
- "name": "DUT info ",
285
- "start_time": 1695817263,
286
- "stop_time": 1695817264,
287
- "assertion_msg": null,
288
- "msg": null,
289
- "attempt": 1,
290
- "dialog_box": {
291
- "title_bar": "Example of text input",
292
- "dialog_text": "Type some text and press the Confirm button",
293
- "widget": {
294
- "info": {
295
- "text": "some text"
296
- },
297
- "type": "textinput"
298
- },
299
- visible: true,
300
- id: "f45bc1e7-2c18-4a4b-2b9d-8863e30bcc78",
301
- font_size: 14
302
- }
303
- },
304
- "test_minute_parity": {
305
- "status": "failed",
306
- "name": "Test 1",
307
- "start_time": 1695817264,
308
- "stop_time": 1695817264,
309
- "assertion_msg": "The test failed because minute 21 is odd! Try again!",
310
- "attempt": 1,
311
- "msg": [
312
- "Current minute 21"
313
- ]
314
- }
315
- }
316
- }
317
- }
318
- }
319
- ```
320
- """
199
+ """Test run description."""
321
200
 
322
201
  model_config = ConfigDict(extra="forbid")
323
202
 
324
203
  rev: str = Field(..., alias="_rev")
325
204
  id: str = Field(..., alias="_id")
205
+
326
206
  progress: int
327
207
  test_stand: TestStand
328
208
  dut: Dut
209
+ process: Process
329
210
  modules: dict[str, ModuleStateStore] = {}
211
+ user: str | None = None
212
+ batch_serial_number: str | None = None
213
+ caused_dut_failure_id: str | None = None
214
+ error_code: int | None = None
330
215
  operator_msg: dict = {}
331
216
  alert: str
332
217
  operator_data: OperatorData
333
218
 
334
219
 
335
220
  class ResultRunStore(IBaseResult):
336
- """Test run description.
337
-
338
- Example:
339
- ```
340
- {
341
- "_rev": "44867-3888ae85c19c428cc46685845953b483",
342
- "_id": "current",
343
- "stop_time": 1695817266,
344
- "start_time": 1695817263,
345
- "status": "failed",
346
- "name": "hardpy-stand",
347
- "dut": {
348
- "serial_number": "92c5a4bb-ecb0-42c5-89ac-e0caca0919fd",
349
- "part_number": "part_1",
350
- "info": {
351
- "batch": "test_batch",
352
- "board_rev": "rev_1"
353
- }
354
- },
355
- "test_stand": {
356
- "hw_id": "840982098ca2459a7b22cc608eff65d4",
357
- "name": "test_stand_1",
358
- "info": {
359
- "geo": "Belgrade"
360
- },
361
- "timezone": "Europe/Belgrade",
362
- "drivers": {
363
- "driver_1": "driver info",
364
- "driver_2": {
365
- "state": "active",
366
- "port": 8000
367
- }
368
- },
369
- "location": "Belgrade_1",
370
- "number": 2
371
- },
372
- "artifact": {},
373
- "modules": {
374
- "test_1_a": {
375
- "status": "failed",
376
- "name": "Module 1",
377
- "start_time": 1695816884,
378
- "stop_time": 1695817265,
379
- "artifact": {},
380
- "cases": {
381
- "test_dut_info": {
382
- "status": "passed",
383
- "name": "DUT info",
384
- "start_time": 1695817263,
385
- "stop_time": 1695817264,
386
- "assertion_msg": null,
387
- "msg": null,
388
- "artifact": {}
389
- },
390
- "test_minute_parity": {
391
- "status": "failed",
392
- "name": "Test 1",
393
- "start_time": 1695817264,
394
- "stop_time": 1695817264,
395
- "assertion_msg": "The test failed because minute 21 is odd! Try again!",
396
- "msg": [
397
- "Current minute 21"
398
- ],
399
- "artifact": {
400
- "data_str": "123DATA",
401
- "data_int": 12345,
402
- "data_dict": {
403
- "test_key": "456DATA"
404
- }
405
- }
406
- }
407
- }
408
- }
409
- }
410
- }
411
- ```
412
- """
221
+ """Test run description."""
413
222
 
414
223
  model_config = ConfigDict(extra="forbid")
415
224
  # Create the new schema class with version update
@@ -421,5 +230,10 @@ class ResultRunStore(IBaseResult):
421
230
 
422
231
  test_stand: TestStand
423
232
  dut: Dut
233
+ process: Process
424
234
  modules: dict[str, ModuleRunStore] = {}
235
+ user: str | None = None
236
+ batch_serial_number: str | None = None
237
+ caused_dut_failure_id: str | None = None
238
+ error_code: int | None = None
425
239
  artifact: dict = {}
@@ -2,6 +2,7 @@
2
2
  # GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
3
3
  from __future__ import annotations
4
4
 
5
+ import copy
5
6
  import signal
6
7
  from logging import getLogger
7
8
  from pathlib import Path, PurePath
@@ -98,6 +99,12 @@ def pytest_addoption(parser: Parser) -> None:
98
99
  default=con_data.sc_connection_only,
99
100
  help="check StandCloud availability",
100
101
  )
102
+ parser.addoption(
103
+ "--hardpy-start-arg",
104
+ action="append",
105
+ default=[],
106
+ help="Dynamic arguments for test execution (key=value format)",
107
+ )
101
108
 
102
109
 
103
110
  # Bootstrapping hooks
@@ -125,6 +132,7 @@ class HardpyPlugin:
125
132
  self._dependencies = {}
126
133
  self._tests_name: str = ""
127
134
  self._is_critical_not_passed = False
135
+ self._start_args = {}
128
136
 
129
137
  if system() == "Linux":
130
138
  signal.signal(signal.SIGTERM, self._stop_handler)
@@ -158,11 +166,17 @@ class HardpyPlugin:
158
166
  if sc_connection_only:
159
167
  con_data.sc_connection_only = bool(sc_connection_only) # type: ignore
160
168
 
169
+ _args = config.getoption("--hardpy-start-arg") or []
170
+ if _args:
171
+ self._start_args = dict(arg.split("=", 1) for arg in _args if "=" in arg)
172
+
161
173
  config.addinivalue_line("markers", "case_name")
162
174
  config.addinivalue_line("markers", "module_name")
163
175
  config.addinivalue_line("markers", "dependency")
164
176
  config.addinivalue_line("markers", "attempt")
165
177
  config.addinivalue_line("markers", "critical")
178
+ config.addinivalue_line("markers", "case_group")
179
+ config.addinivalue_line("markers", "module_group")
166
180
 
167
181
  # must be init after config data is set
168
182
  try:
@@ -301,10 +315,13 @@ class HardpyPlugin:
301
315
  if call.when != "call" or not call.excinfo:
302
316
  return
303
317
 
318
+ # failure item
304
319
  node_info = NodeInfo(item)
305
320
  attempt = node_info.attempt
306
321
  module_id = node_info.module_id
307
322
  case_id = node_info.case_id
323
+ casusd_dut_failure_id = self._reporter.get_caused_dut_failure_id()
324
+ is_dut_failure = True
308
325
 
309
326
  if node_info.critical:
310
327
  self._is_critical_not_passed = True
@@ -323,12 +340,17 @@ class HardpyPlugin:
323
340
  item.runtest()
324
341
  call.excinfo = None
325
342
  self._is_critical_not_passed = False
343
+ is_dut_failure = False
326
344
  self._reporter.set_case_status(module_id, case_id, TestStatus.PASSED)
327
345
  break
328
346
  except AssertionError:
329
347
  self._reporter.set_case_status(module_id, case_id, TestStatus.FAILED)
348
+ is_dut_failure = True
330
349
  if current_attempt == attempt:
331
- return
350
+ break
351
+
352
+ if is_dut_failure and casusd_dut_failure_id is None:
353
+ self._reporter.set_caused_dut_failure_id(module_id, case_id)
332
354
 
333
355
  # Reporting hooks
334
356
 
@@ -375,6 +397,15 @@ class HardpyPlugin:
375
397
  """
376
398
  return self._post_run_functions
377
399
 
400
+ @fixture(scope="session")
401
+ def hardpy_start_args(self) -> dict:
402
+ """Get HardPy start arguments.
403
+
404
+ Returns:
405
+ dict: Parsed start arguments (key-value pairs)
406
+ """
407
+ return copy.deepcopy(self._start_args)
408
+
378
409
  # Not hooks
379
410
 
380
411
  def _stop_handler(self, signum: int, frame: Any) -> None: # noqa: ANN401, ARG002