pytest-embedded 1.2.5__tar.gz → 1.3.1__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.2
2
2
  Name: pytest-embedded
3
- Version: 1.2.5
3
+ Version: 1.3.1
4
4
  Summary: pytest embedded plugin
5
5
  Home-page: https://docs.espressif.com/projects/pytest-embedded/en/latest/
6
6
  Author: Fu Hanxi
@@ -10,10 +10,6 @@ Description: ### pytest-embedded
10
10
 
11
11
  A pytest plugin for embedded systems. Could activate different services for extra functionalities.
12
12
 
13
- Used CLI Options:
14
-
15
- - `app_path`
16
-
17
13
  Platform: UNKNOWN
18
14
  Classifier: Framework :: Pytest
19
15
  Classifier: Intended Audience :: Developers
@@ -1,7 +1,3 @@
1
1
  ### pytest-embedded
2
2
 
3
3
  A pytest plugin for embedded systems. Could activate different services for extra functionalities.
4
-
5
- Used CLI Options:
6
-
7
- - `app_path`
@@ -9,10 +9,10 @@ import pexpect
9
9
  from .app import App
10
10
  from .log import PexpectProcess
11
11
  from .unity import UNITY_SUMMARY_LINE_REGEX, TestSuite
12
- from .utils import Meta, remove_asci_color_code, to_bytes, to_list
12
+ from .utils import Meta, _InjectMixinCls, remove_asci_color_code, to_bytes, to_list
13
13
 
14
14
 
15
- class Dut:
15
+ class Dut(_InjectMixinCls):
16
16
  """
17
17
  Device under test (DUT) base class
18
18
 
@@ -101,7 +101,7 @@ class Dut:
101
101
  @_pexpect_func # noqa
102
102
  def expect(self, pattern, **kwargs) -> Match: # noqa
103
103
  """
104
- Expect the `pattern` from the internal buffer. All the arguments would pass to `pexpect.expect()`.
104
+ Expect the `pattern` from the internal buffer. All the arguments will be passed to `pexpect.expect()`.
105
105
 
106
106
  Args:
107
107
  pattern: string, or compiled regex, or a list of string and compiled regex.
@@ -112,16 +112,17 @@ class Dut:
112
112
  Otherwise match any of them could pass
113
113
 
114
114
  Returns:
115
- (AnyStr): if you're matching pexpect.EOF or pexpect.TIMEOUT to get all the current buffers.
115
+ `AnyStr` or `re.Match`
116
116
 
117
- (re.Match): if matched given string.
117
+ - `AnyStr`: if you're matching `pexpect.EOF` or `pexpect.TIMEOUT` to get all the current buffers.
118
+ - `re.Match`: if matched given string.
118
119
  """
119
120
  return self.pexpect_proc.expect(pattern, **kwargs)
120
121
 
121
122
  @_pexpect_func # noqa
122
123
  def expect_exact(self, pattern, **kwargs) -> Match: # noqa
123
124
  """
124
- Expect the `pattern` from the internal buffer. All the arguments would pass to `pexpect.expect_exact()`.
125
+ Expect the `pattern` from the internal buffer. All the arguments will be passed to `pexpect.expect_exact()`.
125
126
 
126
127
  Args:
127
128
  pattern: string, or a list of string
@@ -132,9 +133,10 @@ class Dut:
132
133
  Otherwise match any of them could pass
133
134
 
134
135
  Returns:
135
- (AnyStr): if you're matching pexpect.EOF or pexpect.TIMEOUT to get all the current buffers.
136
+ `AnyStr` or `re.Match`
136
137
 
137
- (re.Match): if matched given string.
138
+ - `AnyStr`: if you're matching `pexpect.EOF` or `pexpect.TIMEOUT` to get all the current buffers.
139
+ - `re.Match`: if matched given string.
138
140
  """
139
141
  return self.pexpect_proc.expect_exact(pattern, **kwargs)
140
142
 
@@ -147,7 +149,7 @@ class Dut:
147
149
  """
148
150
  Expect a unity test summary block and parse the output into junit report.
149
151
 
150
- Would combine the junit report into the main one if you use `pytest --junitxml` feature.
152
+ Would combine the junit report into the main one if you use ``pytest --junitxml`` feature.
151
153
 
152
154
  Args:
153
155
  remove_asci_escape_code: remove asci escape code in the message field. (default: True)
@@ -155,12 +157,12 @@ class Dut:
155
157
  extra_before: would append before the expected bytes.
156
158
  Use this argument when need to run `expect` functions between one unity test call.
157
159
 
158
- Notes:
160
+ Note:
159
161
  - Would raise AssertionError at the end of the test if any unity test case result is "FAIL"
160
- - Would raise TIMEOUT exception at the end of the test if any unity test case execution took longer
161
- than timeout value
162
+ - Would raise TIMEOUT exception at the end of the test \
163
+ if any unity test case execution took longer than timeout value
162
164
 
163
- Warnings:
165
+ Warning:
164
166
  - All unity test cases record would be missed if the final report block is uncaught.
165
167
  """
166
168
  self.expect(UNITY_SUMMARY_LINE_REGEX, timeout=timeout)
@@ -174,3 +176,25 @@ class Dut:
174
176
  log = remove_asci_color_code(log)
175
177
 
176
178
  self.testsuite.add_unity_test_cases(log)
179
+
180
+ @_InjectMixinCls.require_services('idf')
181
+ def run_all_single_board_cases(
182
+ self,
183
+ group: Optional[str] = None,
184
+ reset: bool = False,
185
+ timeout: float = 30,
186
+ run_ignore_cases: bool = False,
187
+ ) -> None:
188
+ """
189
+ Run all multi_stage cases
190
+
191
+ Args:
192
+ group: test case group
193
+ reset: whether to perform a hardware reset before running a case
194
+ timeout: timeout. (Default: 30 seconds)
195
+ run_ignore_cases: run ignored test cases or not
196
+
197
+ Warning:
198
+ requires enable service ``idf``
199
+ """
200
+ pass
@@ -55,17 +55,15 @@ class PexpectProcess(pexpect.fdpexpect.fdspawn):
55
55
  placeholder=f'... (total {len(self.buffer)} bytes)',
56
56
  )
57
57
 
58
- def read_nonblocking(self, size=1, timeout=-1) -> bytes:
58
+ def read_nonblocking(self, size: int = 1, timeout: int = -1) -> bytes:
59
59
  """
60
60
  Since we're using real file stream, here we only raise an EOF error when the file stream has been closed.
61
61
  This could solve the `os.read()` blocked issue.
62
62
 
63
- :return: String containing the bytes read
64
-
65
63
  Args:
66
- size (int): Read at most *size* bytes.
67
- timeout (float): Wait timeout seconds for file descriptor to be
68
- ready to read. When -1 (default), use self.timeout. When 0, poll.
64
+ size: Read at most *size* bytes.
65
+ timeout: Wait timeout seconds for file descriptor to be
66
+ ready to read. When -1 (default), use `self.timeout`. When 0, poll.
69
67
 
70
68
  Returns:
71
69
  String containing the bytes read
@@ -115,7 +113,7 @@ def live_print_call(*args, msg_queue: Optional[MessageQueue] = None, expect_retu
115
113
  msg_queue: `MessageQueue` instance, would redirect to message queue instead of sys.stdout if specified
116
114
  expect_returncode: expect return code. (Default 0). Would raise exception when return code is different
117
115
 
118
- Notes:
116
+ Note:
119
117
  This function behaves the same as `subprocess.call()`, it would block your current process.
120
118
  """
121
119
  default_kwargs = {
@@ -210,12 +208,11 @@ class DuplicateStdoutPopen(subprocess.Popen):
210
208
  super().terminate()
211
209
 
212
210
  def write(self, s: AnyStr) -> None:
213
- """
211
+ r"""
214
212
  Write to `stdin` via `stdin.write`.
215
213
 
216
- If the input is `str`, will encode to `bytes` and add a b'\\n' automatically in the end.
217
-
218
- if the input is `bytes`, will pass this directly.
214
+ - If the input is `str`, will encode to `bytes` and add a b'\\n' automatically in the end.
215
+ - If the input is `bytes`, will pass this directly.
219
216
 
220
217
  Args:
221
218
  s: bytes or str
@@ -216,7 +216,7 @@ def pytest_addoption(parser):
216
216
  )
217
217
  qemu_group.addoption(
218
218
  '--qemu-cli-args',
219
- help='QEMU cli default arguments. (Default: "-nographic -no-reboot -machine esp32")',
219
+ help='QEMU cli default arguments. (Default: "-nographic -machine esp32")',
220
220
  )
221
221
  qemu_group.addoption(
222
222
  '--qemu-extra-args',
@@ -227,6 +227,15 @@ def pytest_addoption(parser):
227
227
  help='y/yes/true for True and n/no/false for False. '
228
228
  'Set to True to disable auto regenerate image. (Default: False)',
229
229
  )
230
+ qemu_group.addoption(
231
+ '--encrypt',
232
+ help='y/yes/true for True and n/no/false for False. '
233
+ 'Set to True for pre-encryption workflow (Default: False)',
234
+ )
235
+ qemu_group.addoption(
236
+ '--keyfile',
237
+ help='Flash Encryption (pre-encrypted workflow) key path. (Default: None)',
238
+ )
230
239
 
231
240
 
232
241
  ###########
@@ -332,7 +341,7 @@ def multi_dut_fixture(func) -> t.Callable[..., t.Union[t.Any, t.Tuple[t.Any]]]:
332
341
  """
333
342
  Apply the multi-dut arguments to each fixture.
334
343
 
335
- Notes:
344
+ Note:
336
345
  Run the `func(*args, **kwargs)` for multiple times by iterating all `kwargs` via `itemgetter`
337
346
 
338
347
  For example:
@@ -378,7 +387,7 @@ def multi_dut_generator_fixture(
378
387
  """
379
388
  Apply the multi-dut arguments to each fixture.
380
389
 
381
- Notes:
390
+ Note:
382
391
  Run the `func()` for multiple times by iterating all `kwargs` via `itemgetter`. Auto call `close()` or
383
392
  `terminate()` method of the object after it yield back.
384
393
 
@@ -898,6 +907,20 @@ def skip_regenerate_image(request: FixtureRequest) -> t.Optional[str]:
898
907
  return _request_param_or_config_option_or_default(request, 'skip_regenerate_image', None)
899
908
 
900
909
 
910
+ @pytest.fixture
911
+ @multi_dut_argument
912
+ def encrypt(request: FixtureRequest) -> t.Optional[str]:
913
+ """Enable parametrization for the same cli option"""
914
+ return _request_param_or_config_option_or_default(request, 'encrypt', None)
915
+
916
+
917
+ @pytest.fixture
918
+ @multi_dut_argument
919
+ def keyfile(request: FixtureRequest) -> t.Optional[str]:
920
+ """Enable parametrization for the same cli option"""
921
+ return _request_param_or_config_option_or_default(request, 'keyfile', None)
922
+
923
+
901
924
  ####################
902
925
  # Private Fixtures #
903
926
  ####################
@@ -924,6 +947,7 @@ def _services(embedded_services: t.Optional[str]) -> t.List[str]:
924
947
  @dataclass
925
948
  class ClassCliOptions:
926
949
  classes: t.Dict[str, type]
950
+ mixins: t.Dict[str, t.List[type]]
927
951
  kwargs: t.Dict[str, t.Dict[str, t.Any]]
928
952
 
929
953
 
@@ -958,6 +982,8 @@ def _fixture_classes_and_options(
958
982
  qemu_cli_args,
959
983
  qemu_extra_args,
960
984
  skip_regenerate_image,
985
+ encrypt,
986
+ keyfile,
961
987
  # pre-initialized fixtures
962
988
  dut_index,
963
989
  _pexpect_logfile,
@@ -985,6 +1011,7 @@ def _fixture_classes_and_options(
985
1011
  }
986
1012
  """
987
1013
  classes: t.Dict[str, type] = {}
1014
+ mixins: t.Dict[str, t.List[type]] = defaultdict(list)
988
1015
  kwargs: t.Dict[str, t.Dict[str, t.Any]] = defaultdict(dict)
989
1016
 
990
1017
  for fixture in FIXTURES_SERVICES.keys():
@@ -1001,6 +1028,8 @@ def _fixture_classes_and_options(
1001
1028
  'part_tool': part_tool,
1002
1029
  'qemu_image_path': qemu_image_path,
1003
1030
  'skip_regenerate_image': skip_regenerate_image,
1031
+ 'encrypt': encrypt,
1032
+ 'keyfile': keyfile,
1004
1033
  }
1005
1034
  )
1006
1035
  else:
@@ -1099,16 +1128,23 @@ def _fixture_classes_and_options(
1099
1128
  }
1100
1129
  elif fixture == 'qemu':
1101
1130
  if 'qemu' in _services:
1102
- from pytest_embedded_qemu import DEFAULT_IMAGE_FN, Qemu
1131
+ from pytest_embedded_qemu import (
1132
+ DEFAULT_IMAGE_FN,
1133
+ ENCRYPTED_IMAGE_FN,
1134
+ Qemu,
1135
+ )
1103
1136
 
1104
1137
  classes[fixture] = Qemu
1105
1138
  kwargs[fixture] = {
1106
1139
  'msg_queue': msg_queue,
1107
1140
  'qemu_image_path': qemu_image_path
1108
- or os.path.join(app_path or '', build_dir or 'build', DEFAULT_IMAGE_FN),
1141
+ or os.path.join(
1142
+ app_path or '', build_dir or 'build', ENCRYPTED_IMAGE_FN if encrypt else DEFAULT_IMAGE_FN
1143
+ ),
1109
1144
  'qemu_prog_path': qemu_prog_path,
1110
1145
  'qemu_cli_args': qemu_cli_args,
1111
1146
  'qemu_extra_args': qemu_extra_args,
1147
+ 'app': None,
1112
1148
  'meta': _meta,
1113
1149
  }
1114
1150
  elif fixture == 'dut':
@@ -1130,6 +1166,11 @@ def _fixture_classes_and_options(
1130
1166
  'qemu': None,
1131
1167
  }
1132
1168
  )
1169
+
1170
+ if 'idf' in _services:
1171
+ from pytest_embedded_idf.unity_tester import IdfUnityDutMixin
1172
+
1173
+ mixins[fixture].append(IdfUnityDutMixin)
1133
1174
  elif 'jtag' in _services:
1134
1175
  if 'idf' in _services:
1135
1176
  from pytest_embedded_idf import IdfDut
@@ -1169,7 +1210,7 @@ def _fixture_classes_and_options(
1169
1210
  }
1170
1211
  )
1171
1212
 
1172
- return ClassCliOptions(classes, kwargs)
1213
+ return ClassCliOptions(classes, mixins, kwargs)
1173
1214
 
1174
1215
 
1175
1216
  ####################
@@ -1268,6 +1309,7 @@ def dut(
1268
1309
  return cls(**kwargs)
1269
1310
 
1270
1311
  cls = _fixture_classes_and_options.classes['dut']
1312
+ mixins = _fixture_classes_and_options.mixins['dut']
1271
1313
  kwargs = _fixture_classes_and_options.kwargs['dut']
1272
1314
 
1273
1315
  for k, v in kwargs.items():
@@ -1283,7 +1325,7 @@ def dut(
1283
1325
  elif k == 'qemu':
1284
1326
  kwargs[k] = qemu
1285
1327
 
1286
- return cls(**_drop_none_kwargs(kwargs))
1328
+ return cls(**_drop_none_kwargs(kwargs), mixins=mixins)
1287
1329
 
1288
1330
 
1289
1331
  @pytest.fixture
@@ -1,4 +1,5 @@
1
1
  import dataclasses
2
+ import functools
2
3
  import importlib
3
4
  import logging
4
5
  import os
@@ -33,6 +34,12 @@ FIXTURES_SERVICES = {
33
34
 
34
35
  _T = t.TypeVar('_T')
35
36
 
37
+ _MIXIN_REQUIRED_SERVICES = {
38
+ 'IdfUnityMixin': ['idf'],
39
+ }
40
+
41
+ _MIXIN_REQUIRED_SERVICES_KEY = '_based_on_services'
42
+
36
43
 
37
44
  #######################
38
45
  # Errors and Warnings #
@@ -54,6 +61,16 @@ class PackageNotInstalledError(SystemExit):
54
61
  )
55
62
 
56
63
 
64
+ class RequireServiceError(SystemExit):
65
+ def __init__(self, func_name: str, services: t.Union[str, t.List[str]]) -> None:
66
+ services_str = ','.join(to_list(services))
67
+ super().__init__(
68
+ f'function {func_name} requires enabling one of the service(s) {services_str}. '
69
+ f'Please enable by passing CLI options "--embedded-services {services_str}". '
70
+ f'For more details, please refer to "pytest --help".'
71
+ )
72
+
73
+
57
74
  #####################
58
75
  # Utility Functions #
59
76
  #####################
@@ -101,10 +118,11 @@ def to_list(s: _T) -> t.List[_T]:
101
118
  s: Anything
102
119
 
103
120
  Returns:
104
- List (list[_T]):
105
- - `list(s)` (List. If `s` is a tuple or a set.
106
- - itself. If `s` is a list.
107
- - `[s]`. If `s` is other types.
121
+ List (list[_T])
122
+
123
+ - `list(s)` (List. If `s` is a tuple or a set.
124
+ - itself. If `s` is a list.
125
+ - `[s]`. If `s` is other types.
108
126
  """
109
127
  if not s:
110
128
  return s
@@ -215,22 +233,23 @@ def lazy_load(
215
233
  Returns:
216
234
  __getattr__ function
217
235
 
218
- Examples:
219
- ```python
220
- __getattr__ = lazy_load(
221
- importlib.import_module(__name__),
222
- {
223
- 'IdfApp': IdfApp,
224
- 'LinuxDut': LinuxDut,
225
- 'LinuxSerial': LinuxSerial,
226
- 'CaseTester': CaseTester,
227
- },
228
- {
229
- 'IdfSerial': '.serial',
230
- 'IdfDut': '.dut',
231
- },
232
- )
233
- ```
236
+ Example:
237
+
238
+ ::
239
+
240
+ __getattr__ = lazy_load(
241
+ importlib.import_module(__name__),
242
+ {
243
+ 'IdfApp': IdfApp,
244
+ 'LinuxDut': LinuxDut,
245
+ 'LinuxSerial': LinuxSerial,
246
+ 'CaseTester': CaseTester,
247
+ },
248
+ {
249
+ 'IdfSerial': '.serial',
250
+ 'IdfDut': '.dut',
251
+ },
252
+ )
234
253
  """
235
254
 
236
255
  def __getattr__(object_name):
@@ -245,3 +264,83 @@ def lazy_load(
245
264
  raise AttributeError('Attribute %s not found in module %s', object_name, base_module.__name__)
246
265
 
247
266
  return __getattr__
267
+
268
+
269
+ class _InjectMixinMeta(type):
270
+ def __call__(cls, *args, **kwargs):
271
+ try:
272
+ mixins = kwargs.pop('mixins', None)
273
+ if mixins:
274
+ mixins = to_list(mixins)
275
+ name = cls.__name__ + 'With' + 'And'.join([m.__name__ for m in mixins])
276
+ cls = type(name, tuple([*mixins, cls]), {})
277
+
278
+ # users should only know concept "services", not mixins
279
+ _based_on_services = set()
280
+ for m in mixins:
281
+ if m.__name__ not in _MIXIN_REQUIRED_SERVICES:
282
+ continue
283
+
284
+ _based_on_services.update(to_list(_MIXIN_REQUIRED_SERVICES[m.__name__]))
285
+ kwargs[_MIXIN_REQUIRED_SERVICES_KEY] = sorted(_based_on_services)
286
+ except KeyError:
287
+ pass
288
+ return type.__call__(cls, *args, **kwargs)
289
+
290
+
291
+ class _InjectMixinCls(metaclass=_InjectMixinMeta):
292
+ """
293
+ This class provide a check function `require_services()` to check if the function is injected by
294
+ enabling one of the required services.
295
+
296
+ The benefits are:
297
+ - provide the autocompletion for the functions provided by the mixins
298
+ - check the requirement at runtime
299
+
300
+ Example:
301
+
302
+ ::
303
+
304
+ class IdfUnityMixin:
305
+ def foo(self):
306
+ print('foo from MixinOne')
307
+
308
+
309
+ class Test(_InjectMixinCls):
310
+ def __init__(self, **kwargs):
311
+ for k, v in kwargs.items():
312
+ setattr(self, k, v)
313
+
314
+ @_InjectMixinCls.require_services('idf')
315
+ def foo(self):
316
+ pass
317
+
318
+
319
+ # mro(): TestWithIdfUnityMixin, IdfUnityMixin, Test, object
320
+ s1 = Test(mixins=IdfUnityMixin)
321
+ # mro(): Test, object
322
+ s2 = Test()
323
+ s1.foo() # foo from IdfUnityMixin
324
+ s2.foo() # function foo requires enabling one of the service(s) idf.
325
+ """
326
+
327
+ def require_services(*services):
328
+ def decorator(func):
329
+ @functools.wraps(func)
330
+ def wrapped(self, *args, **kwargs):
331
+ based = False
332
+ for service in services:
333
+ if hasattr(self, _MIXIN_REQUIRED_SERVICES_KEY) and service in getattr(
334
+ self, _MIXIN_REQUIRED_SERVICES_KEY
335
+ ):
336
+ based = True
337
+ break
338
+
339
+ if based:
340
+ return func(self, *args, **kwargs)
341
+ else:
342
+ raise RequireServiceError(func.__name__, services)
343
+
344
+ return wrapped
345
+
346
+ return decorator
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.2
2
2
  Name: pytest-embedded
3
- Version: 1.2.5
3
+ Version: 1.3.1
4
4
  Summary: pytest embedded plugin
5
5
  Home-page: https://docs.espressif.com/projects/pytest-embedded/en/latest/
6
6
  Author: Fu Hanxi
@@ -10,10 +10,6 @@ Description: ### pytest-embedded
10
10
 
11
11
  A pytest plugin for embedded systems. Could activate different services for extra functionalities.
12
12
 
13
- Used CLI Options:
14
-
15
- - `app_path`
16
-
17
13
  Platform: UNKNOWN
18
14
  Classifier: Framework :: Pytest
19
15
  Classifier: Intended Audience :: Developers