hardpy 0.14.0__py3-none-any.whl → 0.15.1__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.
@@ -2,11 +2,19 @@
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
5
7
  from typing import ClassVar
6
8
 
7
9
  from pydantic import BaseModel, ConfigDict, Field
8
10
 
9
- from hardpy.pytest_hardpy.utils import TestStatus as Status # noqa: TC001
11
+ from hardpy.pytest_hardpy.utils import (
12
+ ChartType,
13
+ ComparisonOperation as CompOp,
14
+ Group,
15
+ MeasurementType,
16
+ TestStatus as Status,
17
+ )
10
18
 
11
19
 
12
20
  class IBaseResult(BaseModel):
@@ -21,203 +29,165 @@ class IBaseResult(BaseModel):
21
29
 
22
30
 
23
31
  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
- """
32
+ """Test case description."""
54
33
 
55
34
  assertion_msg: str | None = None
56
35
  msg: dict | None = None
57
- dialog_box: dict = {}
36
+ measurements: list[NumericMeasurement | StringMeasurement] = []
37
+ chart: Chart | None = None
58
38
  attempt: int = 0
39
+ group: Group
40
+ dialog_box: dict = {}
59
41
 
60
42
 
61
43
  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
- """
44
+ """Test case description with artifact."""
79
45
 
80
46
  assertion_msg: str | None = None
81
47
  msg: dict | None = None
48
+ measurements: list[NumericMeasurement | StringMeasurement] = []
49
+ chart: Chart | None = None
50
+ attempt: int = 0
51
+ group: Group
82
52
  artifact: dict = {}
83
53
 
84
54
 
85
55
  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
- """
56
+ """Test module description."""
110
57
 
111
58
  cases: dict[str, CaseStateStore] = {}
59
+ group: Group
112
60
 
113
61
 
114
62
  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
- """
63
+ """Test module description."""
141
64
 
142
65
  cases: dict[str, CaseRunStore] = {}
66
+ group: Group
143
67
  artifact: dict = {}
144
68
 
145
69
 
146
70
  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
- """
71
+ """Device under test description."""
72
+
73
+ model_config = ConfigDict(extra="forbid")
74
+
75
+ name: str | None = None
76
+ type: str | None = None
77
+ serial_number: str | None = None
78
+ part_number: str | None = None
79
+ revision: str | None = None
80
+ sub_units: list[SubUnit] = []
81
+ info: Mapping[str, str | int | float] = {}
82
+
83
+
84
+ class SubUnit(BaseModel):
85
+ """Sub unit of DUT description."""
86
+
87
+ model_config = ConfigDict(extra="forbid")
88
+
89
+ name: str | None = None
90
+ type: str | None = None
91
+ serial_number: str | None = None
92
+ part_number: str | None = None
93
+ revision: str | None = None
94
+ info: Mapping[str, str | int | float] = {}
95
+
96
+
97
+ class Instrument(BaseModel):
98
+ """Instrument (power supply, oscilloscope and others) description."""
163
99
 
164
100
  model_config = ConfigDict(extra="forbid")
165
101
 
166
- serial_number: str | None
167
- part_number: str | None
168
- info: dict = {}
102
+ name: str | None = None
103
+ revision: str | None = None
104
+ number: int | None = None
105
+ comment: str | None = None
106
+ info: Mapping[str, str | int | float] = {}
169
107
 
170
108
 
171
109
  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
- """
110
+ """Test stand description."""
197
111
 
198
112
  model_config = ConfigDict(extra="forbid")
199
113
 
200
114
  hw_id: str | None = None
201
115
  name: str | None = None
116
+ revision: str | None = None
202
117
  timezone: str | None = None
203
- drivers: dict = {}
204
- info: dict = {}
205
118
  location: str | None = None
206
119
  number: int | None = None
120
+ drivers: dict = {} # deprecated, remove in v2
121
+ instruments: list[Instrument] = []
122
+ info: Mapping[str, str | int | float] = {}
123
+
124
+
125
+ class Process(BaseModel):
126
+ """Production process description."""
127
+
128
+ model_config = ConfigDict(extra="forbid")
129
+
130
+ name: str | None = None
131
+ number: int | None = None
132
+ info: Mapping[str, str | int | float] = {}
133
+
134
+
135
+ class IBaseMeasurement(BaseModel, ABC):
136
+ """Base class for all measurement models."""
137
+
138
+ model_config = ConfigDict(extra="allow")
139
+
140
+ type: MeasurementType
141
+ name: str | None = Field(default=None)
142
+ operation: CompOp | None = Field(default=None)
143
+ result: bool | None = Field(default_factory=lambda: None)
144
+
145
+
146
+ class NumericMeasurement(IBaseMeasurement):
147
+ """Numeric measurement description."""
148
+
149
+ model_config = ConfigDict(extra="forbid")
150
+
151
+ type: MeasurementType = Field(default=MeasurementType.NUMERIC)
152
+ value: int | float
153
+ name: str | None = Field(default=None)
154
+ unit: str | None = Field(default=None)
155
+
156
+ comparison_value: float | int | None = Field(default=None)
157
+
158
+ lower_limit: float | int | None = Field(default=None)
159
+ upper_limit: float | int | None = Field(default=None)
160
+
161
+
162
+ class StringMeasurement(IBaseMeasurement):
163
+ """String measurement description."""
164
+
165
+ model_config = ConfigDict(extra="forbid")
166
+
167
+ type: MeasurementType = Field(default=MeasurementType.STRING)
168
+ value: str
169
+ name: str | None = Field(default=None)
170
+ casesensitive: bool = Field(default=True)
171
+
172
+ comparison_value: str | None = Field(default=None)
173
+
174
+
175
+ class Chart(BaseModel):
176
+ """Chart description."""
177
+
178
+ model_config = ConfigDict(extra="forbid")
179
+
180
+ type: ChartType = Field(default=ChartType.LINE)
181
+ title: str | None = Field(default=None)
182
+ x_label: str | None = Field(default=None)
183
+ y_label: str | None = Field(default=None)
184
+ marker_name: list[str | None] = Field(default=[])
185
+ x_data: list[list[int | float]] = Field(default_factory=lambda: []) # noqa: PIE807
186
+ y_data: list[list[int | float]] = Field(default_factory=lambda: []) # noqa: PIE807
207
187
 
208
188
 
209
189
  class OperatorData(BaseModel):
210
- """Operator data from operator panel.
211
-
212
- Example:
213
- ```
214
- {
215
- "operator_data": {
216
- "dialog": "hello",
217
- }
218
- }
219
- ```
220
- """
190
+ """Operator data from operator panel."""
221
191
 
222
192
  model_config = ConfigDict(extra="forbid")
223
193
 
@@ -225,191 +195,29 @@ class OperatorData(BaseModel):
225
195
 
226
196
 
227
197
  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
- """
198
+ """Test run description."""
321
199
 
322
200
  model_config = ConfigDict(extra="forbid")
323
201
 
324
202
  rev: str = Field(..., alias="_rev")
325
203
  id: str = Field(..., alias="_id")
204
+
326
205
  progress: int
327
206
  test_stand: TestStand
328
207
  dut: Dut
208
+ process: Process
329
209
  modules: dict[str, ModuleStateStore] = {}
210
+ user: str | None = None
211
+ batch_serial_number: str | None = None
212
+ caused_dut_failure_id: str | None = None
213
+ error_code: int | None = None
330
214
  operator_msg: dict = {}
331
215
  alert: str
332
216
  operator_data: OperatorData
333
217
 
334
218
 
335
219
  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
- """
220
+ """Test run description."""
413
221
 
414
222
  model_config = ConfigDict(extra="forbid")
415
223
  # Create the new schema class with version update
@@ -421,5 +229,10 @@ class ResultRunStore(IBaseResult):
421
229
 
422
230
  test_stand: TestStand
423
231
  dut: Dut
232
+ process: Process
424
233
  modules: dict[str, ModuleRunStore] = {}
234
+ user: str | None = None
235
+ batch_serial_number: str | None = None
236
+ caused_dut_failure_id: str | None = None
237
+ error_code: int | None = None
425
238
  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