esp-test-utils 0.2.1__tar.gz → 0.2.3__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.
Files changed (114) hide show
  1. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/CHANGELOG.md +13 -0
  2. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/PKG-INFO +1 -1
  3. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esp_test_utils.egg-info/PKG-INFO +1 -1
  4. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esp_test_utils.egg-info/SOURCES.txt +3 -1
  5. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/adapter/dut/dut_base.py +8 -2
  6. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/adapter/dut/esp_mixin.py +11 -4
  7. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/adapter/port/base_port.py +22 -7
  8. esp_test_utils-0.2.3/esptest/adapter/port/shell_port.py +191 -0
  9. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/config/env_config.py +38 -11
  10. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/devices/esp_serial.py +11 -0
  11. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/tools/download_bin.py +1 -1
  12. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/utility/parse_bin_path.py +8 -0
  13. esp_test_utils-0.2.3/tests/adapter/test_shell_port.py +94 -0
  14. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/test_EnvConfig.py +24 -0
  15. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/.github/.gitkeep +0 -0
  16. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/.github/workflows/pypi-publish.yml +0 -0
  17. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/.gitignore +0 -0
  18. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/.gitlab-ci.yml +0 -0
  19. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/.pre-commit-config.yaml +0 -0
  20. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/CONTRIBUTING.md +0 -0
  21. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/LICENSE +0 -0
  22. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/README.md +0 -0
  23. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/docs/Makefile +0 -0
  24. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/docs/conf.py +0 -0
  25. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/docs/index.rst +0 -0
  26. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/docs/make.bat +0 -0
  27. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esp_test_utils.egg-info/dependency_links.txt +0 -0
  28. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esp_test_utils.egg-info/entry_points.txt +0 -0
  29. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esp_test_utils.egg-info/requires.txt +0 -0
  30. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esp_test_utils.egg-info/top_level.txt +0 -0
  31. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/__init__.py +0 -0
  32. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/__main__.py +0 -0
  33. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/adapter/__init__.py +0 -0
  34. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/adapter/dut/__init__.py +0 -0
  35. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/adapter/dut/create_dut.py +0 -0
  36. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/adapter/dut/esp_dut.py +0 -0
  37. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/adapter/dut/esp_port.py +0 -0
  38. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/adapter/dut/mac_mixin.py +0 -0
  39. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/adapter/dut/wrapper.py +0 -0
  40. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/adapter/port/__init__.py +0 -0
  41. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/adapter/port/serial_port.py +0 -0
  42. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/all.py +0 -0
  43. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/common/__init__.py +0 -0
  44. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/common/compat_typing.py +0 -0
  45. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/common/data_monitor.py +0 -0
  46. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/common/decorators.py +0 -0
  47. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/common/encoding.py +0 -0
  48. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/common/generator.py +0 -0
  49. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/common/shell.py +0 -0
  50. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/common/timestamp.py +0 -0
  51. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/config/__init__.py +0 -0
  52. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/config/default_config.py +0 -0
  53. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/db/__init__.py +0 -0
  54. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/db/runners.py +0 -0
  55. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/devices/__init__.py +0 -0
  56. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/devices/attenuator.py +0 -0
  57. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/devices/serial_dut.py +0 -0
  58. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/devices/serial_tools.py +0 -0
  59. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/env/__init__.py +0 -0
  60. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/env/base_env.py +0 -0
  61. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/env/wifi_env.py +0 -0
  62. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/esp_console/__init__.py +0 -0
  63. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/esp_console/wifi_cmd.py +0 -0
  64. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/interface/__init__.py +0 -0
  65. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/interface/dut.py +0 -0
  66. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/interface/port.py +0 -0
  67. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/iperf_utility/__init__.py +0 -0
  68. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/iperf_utility/iperf_results.py +0 -0
  69. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/iperf_utility/iperf_test.py +0 -0
  70. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/iperf_utility/iperf_test.test.py +0 -0
  71. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/iperf_utility/line_chart.py +0 -0
  72. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/logger/__init__.py +0 -0
  73. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/logger/logger.py +0 -0
  74. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/network/__init__.py +0 -0
  75. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/network/mac.py +0 -0
  76. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/network/netif.py +0 -0
  77. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/network/nic.py +0 -0
  78. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/scripts/downbin.py +0 -0
  79. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/scripts/list_ports.py +0 -0
  80. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/scripts/monitor.py +0 -0
  81. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/scripts/set_att.py +0 -0
  82. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/tools/copy_bin.py +0 -0
  83. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/tools/http_download.py +0 -0
  84. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/tools/pip_check.py +0 -0
  85. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/esptest/utility/gen_esp32part.py +0 -0
  86. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/example/jap_test.py +0 -0
  87. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/example/restart_test.py +0 -0
  88. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/pyproject.toml +0 -0
  89. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/setup.cfg +0 -0
  90. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/__init__.py +0 -0
  91. {esp_test_utils-0.2.1/tests → esp_test_utils-0.2.3/tests/adapter}/test_Dut.py +0 -0
  92. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/basic/test_decorators.py +0 -0
  93. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/basic/test_network.py +0 -0
  94. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/conftest.py +0 -0
  95. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/db/test_db_runners.py +0 -0
  96. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/esp_console/_files/wifi_cmd_connected_1.log +0 -0
  97. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/esp_console/_files/wifi_cmd_connected_2.log +0 -0
  98. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/esp_console/conftest.py +0 -0
  99. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/esp_console/test_WifiCmd.py +0 -0
  100. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/iperf_utility/_files/dut_iperf_rx1.log +0 -0
  101. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/iperf_utility/_files/dut_iperf_rx2.log +0 -0
  102. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/iperf_utility/_files/pc_iperf_rx.log +0 -0
  103. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/iperf_utility/_files/pc_iperf_rx2.log +0 -0
  104. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/iperf_utility/test_chart.py +0 -0
  105. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/iperf_utility/test_iperf_results.py +0 -0
  106. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/iperf_utility/test_iperf_util.py +0 -0
  107. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/test_common.py +0 -0
  108. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/test_import.py +0 -0
  109. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/tools/test_download_file.py +0 -0
  110. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/tools/test_pip_check.py +0 -0
  111. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/utility/_files/test-bin.zip +0 -0
  112. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/utility/_files/test-get-baud/ESP32AT-V4.1.1.0/sdkconfig +0 -0
  113. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tests/utility/test_parse_bin_path.py +0 -0
  114. {esp_test_utils-0.2.1 → esp_test_utils-0.2.3}/tools/ci/check_dev_version.py +0 -0
@@ -1,3 +1,16 @@
1
+ ## v0.2.3 (2025-11-14)
2
+
3
+
4
+ - feat: add shell port support
5
+ - fix: Fix esptool connect to given port
6
+ - fix: log of env config search dirs
7
+
8
+ ## v0.2.2 (2025-10-29)
9
+
10
+
11
+ - feat: get test variables from shell env
12
+ - fix: flash esp32 option no-stub
13
+
1
14
  ## v0.2.1 (2025-09-24)
2
15
 
3
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: esp-test-utils
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: ESP Test Utils
5
5
  Author-email: Chen Yudong <chenyudong@espressif.com>
6
6
  License: Apache License
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: esp-test-utils
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: ESP Test Utils
5
5
  Author-email: Chen Yudong <chenyudong@espressif.com>
6
6
  License: Apache License
@@ -33,6 +33,7 @@ esptest/adapter/dut/wrapper.py
33
33
  esptest/adapter/port/__init__.py
34
34
  esptest/adapter/port/base_port.py
35
35
  esptest/adapter/port/serial_port.py
36
+ esptest/adapter/port/shell_port.py
36
37
  esptest/common/__init__.py
37
38
  esptest/common/compat_typing.py
38
39
  esptest/common/data_monitor.py
@@ -84,10 +85,11 @@ example/jap_test.py
84
85
  example/restart_test.py
85
86
  tests/__init__.py
86
87
  tests/conftest.py
87
- tests/test_Dut.py
88
88
  tests/test_EnvConfig.py
89
89
  tests/test_common.py
90
90
  tests/test_import.py
91
+ tests/adapter/test_Dut.py
92
+ tests/adapter/test_shell_port.py
91
93
  tests/basic/test_decorators.py
92
94
  tests/basic/test_network.py
93
95
  tests/db/test_db_runners.py
@@ -11,7 +11,7 @@ from esptool.loader import ESPLoader
11
11
 
12
12
  import esptest.common.compat_typing as t
13
13
 
14
- from ...common.timestamp import timestamp_str
14
+ from ...common.timestamp import timestamp_slug
15
15
  from ...interface.dut import DutInterface
16
16
  from ...interface.port import PortInterface
17
17
  from ...logger import get_logger
@@ -62,7 +62,7 @@ class DutConfig:
62
62
  self._auto_gen_name()
63
63
  if not self.log_file:
64
64
  _log_path = self.log_path or './dut_logs'
65
- _file_name = f'{self.name}_{timestamp_str()}.log'.replace(':', '-')
65
+ _file_name = f'{self.name}_{timestamp_slug()}.log'.replace(':', '-')
66
66
  self.log_file = str(Path(_log_path) / _file_name)
67
67
  # serial configs
68
68
  _serial_configs = DEFAULT_SERIAL_CONFIGS.copy()
@@ -229,6 +229,12 @@ class DutBase(VariablesMixin, DutInterface): # pylint: disable=too-many-public-
229
229
  return self._base_port_proxy.raw_port
230
230
  raise NotImplementedError()
231
231
 
232
+ @property
233
+ def log_file(self) -> t.Any:
234
+ if self._base_port_proxy:
235
+ return self._base_port_proxy.log_file
236
+ return None
237
+
232
238
  @property
233
239
  def name(self) -> t.Any:
234
240
  return self.dut_config.name
@@ -51,15 +51,17 @@ class EspSerial:
51
51
 
52
52
  class EspMixin(BaseProtocol):
53
53
  def _esptool_open_port(self, port: str, initial_baud: int, **kwargs: t.Any) -> esptool.ESPLoader:
54
- ports = [p.device for p in get_all_serial_ports()]
55
54
  port = compute_serial_port(port) if port else ''
55
+ serial_list = [port] if port else [p.device for p in get_all_serial_ports()]
56
+ # esptool.get_default_connected_device always detect_chip from serial_list
56
57
  esp = esptool.get_default_connected_device(
57
- ports,
58
- port,
58
+ serial_list,
59
+ port=port or None, # type: ignore
59
60
  connect_attempts=3,
60
61
  initial_baud=initial_baud,
61
62
  chip=kwargs.get('chip', 'auto'),
62
63
  )
64
+ assert esp, f'Failed to connect to {port}'
63
65
  return esp
64
66
 
65
67
  def _esptool_path(self, use_esptool: str = '') -> str:
@@ -95,7 +97,7 @@ class EspMixin(BaseProtocol):
95
97
  esptool=self.dut_config.use_esptool,
96
98
  erase_nvs=erase_nvs,
97
99
  )
98
- if not self.esp.IS_STUB:
100
+ if not self.esp.IS_STUB and self.esp.CHIP_NAME not in ['ESP32']:
99
101
  # preview or dev targets
100
102
  down_bin_tool.force_no_stub = True
101
103
  with self.disable_redirect_thread():
@@ -105,6 +107,11 @@ class EspMixin(BaseProtocol):
105
107
  def start_redirect_thread(self) -> None:
106
108
  if self.esp:
107
109
  self.esp._port.open() # pylint: disable=protected-access
110
+ if self.log_file:
111
+ with open(self.log_file, 'a', encoding='utf-8') as log_f:
112
+ log_f.write(
113
+ f'------------ reopen port: {self.esp._port.port} {self.esp._port.baudrate} --------------- \n' # pylint: disable=protected-access
114
+ )
108
115
  super().start_redirect_thread()
109
116
 
110
117
  def stop_redirect_thread(self) -> bool:
@@ -31,9 +31,12 @@ class RawPort(metaclass=abc.ABCMeta):
31
31
  """Define a minimum Dut class, the dut objects should at least support these methods
32
32
 
33
33
  the dut should at least support these attributes:
34
- - attribute name with type str
35
34
  - method: write_bytes() with parameters: data[bytes]
36
35
  - method: read_bytes() with parameters: timeout[float]
36
+
37
+ optional attribute & method:
38
+ - attribute: name with type str
39
+ - attribute: read_timeout with type float
37
40
  """
38
41
 
39
42
  @classmethod
@@ -243,7 +246,11 @@ class PortSpawn(pexpect.spawnbase.SpawnBase, t.Generic[T]):
243
246
  self._log(ret_data, 'read') # type: ignore
244
247
  return ret_data
245
248
 
249
+ @deprecated('Should use close() for Spawn')
246
250
  def stop(self) -> None:
251
+ self.close()
252
+
253
+ def close(self) -> None:
247
254
  """Stop and clean up"""
248
255
  self.logger.debug(f'Stopping SerialSpawn {self.name}')
249
256
  self._read_thread_stop_event.set()
@@ -359,7 +366,7 @@ class BasePort(PortInterface, t.Generic[T]):
359
366
  if new_log_file == self._log_file:
360
367
  return
361
368
  if self._pexpect_spawn:
362
- self._pexpect_spawn.serial_log_file = new_log_file
369
+ self._pexpect_spawn.log_file = new_log_file
363
370
  self._log_file = new_log_file
364
371
 
365
372
  @property
@@ -381,7 +388,7 @@ class BasePort(PortInterface, t.Generic[T]):
381
388
  if not self._pexpect_spawn:
382
389
  return False
383
390
  self._init_log_file()
384
- self._pexpect_spawn.stop()
391
+ self._pexpect_spawn.close()
385
392
  self._pexpect_spawn = None
386
393
  return True
387
394
 
@@ -486,9 +493,13 @@ class BasePort(PortInterface, t.Generic[T]):
486
493
  """
487
494
  buffer = b''
488
495
  if flush:
489
- match = self.expect(re.compile(b'.*', re.DOTALL), timeout=0)
490
- assert match
491
- buffer = match.group(0)
496
+ # pexpect may return empty bytes if b'(.*)' is used
497
+ try:
498
+ match = self.expect(re.compile(b'(.+)', re.DOTALL), timeout=0)
499
+ assert match
500
+ buffer = match.group(0)
501
+ except TimeoutError:
502
+ pass
492
503
  else:
493
504
  # flush spawn buffer
494
505
  assert self._pexpect_spawn
@@ -499,7 +510,11 @@ class BasePort(PortInterface, t.Generic[T]):
499
510
 
500
511
  def close(self) -> None:
501
512
  if self._close_redirect_thread_when_exit and self._pexpect_spawn:
502
- self._pexpect_spawn.stop()
513
+ self._pexpect_spawn.close()
514
+ if self.raw_port:
515
+ if hasattr(self.raw_port, 'close'):
516
+ assert callable(self.raw_port.close) # type: ignore
517
+ self.raw_port.close() # type: ignore
503
518
 
504
519
  def __enter__(self) -> 't.Self':
505
520
  return self
@@ -0,0 +1,191 @@
1
+ import os
2
+ import subprocess
3
+ import time
4
+
5
+ import pexpect
6
+ import psutil
7
+
8
+ import esptest.common.compat_typing as t
9
+
10
+ from ...logger import get_logger
11
+ from .base_port import BasePort, RawPort
12
+
13
+ logger = get_logger('shell_port')
14
+
15
+
16
+ class ShellRaw(RawPort):
17
+ """A subprocess Raw Port class that supports shell read, write
18
+
19
+ is a subclass of RawPort
20
+ """
21
+
22
+ def __init__(self, cmd: str = '/bin/bash', env: t.Optional[t.Dict[str, str]] = None) -> None:
23
+ self.env = env or os.environ.copy()
24
+ self.env['PYTHONUNBUFFERED'] = 'true' # for python scripts, disable output buffering
25
+ self.cmd = cmd
26
+ self.proc: t.Optional[subprocess.Popen] = None
27
+ self.read_timeout = 0.002 # default read_timeout
28
+ self.open()
29
+
30
+ def open(self) -> None:
31
+ if not self.proc:
32
+ self.proc = subprocess.Popen( # pylint: disable=consider-using-with
33
+ self.cmd,
34
+ shell=True,
35
+ env=self.env,
36
+ stdin=subprocess.PIPE,
37
+ stdout=subprocess.PIPE,
38
+ stderr=subprocess.STDOUT,
39
+ )
40
+ # Set stdout to non-blocking
41
+ os.set_blocking(self.proc.stdout.fileno(), False) # type: ignore
42
+
43
+ def close(self) -> None:
44
+ """Close subprocess."""
45
+ if self.proc:
46
+ if self.proc.pid:
47
+ try:
48
+ proc = psutil.Process(self.proc.pid)
49
+ for child in proc.children(recursive=True):
50
+ child.kill()
51
+ proc.kill()
52
+ time.sleep(0.01)
53
+ except psutil.Error:
54
+ pass
55
+ # # Unix / Linux - does not work
56
+ # try:
57
+ # os.killpg(self.proc.pid, signal.SIGTERM) # send SIGTERM to all in the group
58
+ # os.killpg(self.proc.pid, signal.SIGKILL) # send SIGTERM to all in the group
59
+ # except ProcessLookupError:
60
+ # pass
61
+ self.proc.terminate()
62
+ self.proc.kill()
63
+ self.proc.wait()
64
+ logger.info(f'shell command [{self.cmd}] was killed')
65
+ self.proc = None
66
+
67
+ def write_bytes(self, data: bytes) -> None:
68
+ """Write bytes to subprocess stdin."""
69
+ if self.proc:
70
+ self.proc.stdin.write(data) # type: ignore
71
+ self.proc.stdin.flush() # type: ignore
72
+ return
73
+ raise ValueError('Subprocess not initialized.')
74
+
75
+ def read_bytes(self, timeout: float = 0) -> bytes:
76
+ """blocking read bytes"""
77
+ data = self.read_bytes_nonblocking()
78
+ if not data and timeout > 0:
79
+ time.sleep(timeout) # blocking read
80
+ data = self.read_bytes_nonblocking()
81
+ if data:
82
+ logger.debug(f'[{self.cmd}] read_bytes timeout={timeout}, data={str(data)}')
83
+ return data
84
+
85
+ def read_bytes_nonblocking(self, size: int = -1) -> bytes:
86
+ """non-blocking read bytes"""
87
+ if self.proc:
88
+ self.proc.stdout.flush() # type: ignore
89
+ return self.proc.stdout.read(size) # type: ignore
90
+ return b''
91
+
92
+
93
+ class ShellPort(BasePort[ShellRaw]):
94
+ """A combined port class that supports shell read, write, expect"""
95
+
96
+ def __init__(
97
+ self,
98
+ cmd: str = '/bin/bash',
99
+ env: t.Optional[dict[str, str]] = None,
100
+ name: str = '',
101
+ log_file: str = '',
102
+ **kwargs: t.Any,
103
+ ) -> None:
104
+ raw_port = ShellRaw(cmd=cmd, env=env)
105
+ super().__init__(raw_port, name, log_file, **kwargs)
106
+
107
+
108
+ class InvalidRaw(RawPort):
109
+ """A invalid Raw Port class that always raise NotImplementedError to pass type check"""
110
+
111
+ def write_bytes(self, data: bytes) -> None:
112
+ """Write bytes to subprocess stdin."""
113
+ raise NotImplementedError('Invalid Raw Port.')
114
+
115
+ def read_bytes(self, timeout: float = 0) -> bytes:
116
+ """blocking read bytes"""
117
+ raise NotImplementedError('Invalid Raw Port.')
118
+
119
+
120
+ class PexpectPort(BasePort[InvalidRaw]):
121
+ """A pexpect Port class that supports shell read, write, expect
122
+
123
+ based on pexpect.spawn but use different expect method
124
+ """
125
+
126
+ def __init__(
127
+ self,
128
+ cmd: str = '/bin/bash',
129
+ name: str = '',
130
+ log_file: str = '',
131
+ **kwargs: t.Any,
132
+ ) -> None:
133
+ self._cmd = cmd
134
+ raw_port = InvalidRaw()
135
+ self._pexpect_spawn: t.Optional[pexpect.spawn] = None # change type
136
+ self.log_file_f = open(log_file, 'wb') if log_file else None # pylint: disable=consider-using-with
137
+ super().__init__(raw_port, name, log_file, **kwargs)
138
+
139
+ @property
140
+ def log_file(self) -> str:
141
+ """Get Current dut log file."""
142
+ if not self._log_file:
143
+ return ''
144
+ return os.path.abspath(self._log_file)
145
+
146
+ @log_file.setter
147
+ def log_file(self, new_log_file: str) -> None:
148
+ """Set Current dut log file."""
149
+ if new_log_file == self._log_file:
150
+ return
151
+ if self.log_file_f:
152
+ if self._pexpect_spawn:
153
+ self._pexpect_spawn.logfile = None # type: ignore
154
+ self.log_file_f.close()
155
+ if self._pexpect_spawn:
156
+ self.log_file_f = open(new_log_file, 'wb') if new_log_file else None # pylint: disable=consider-using-with
157
+ self._pexpect_spawn.logfile = self.log_file_f # type: ignore
158
+ self._log_file = new_log_file
159
+
160
+ @property
161
+ def spawn(self) -> t.Optional[pexpect.spawn]: # type: ignore
162
+ """Allow the use of pexpect spawn enhancements, if pexpect process is available"""
163
+ return self._pexpect_spawn
164
+
165
+ def start_redirect_thread(self) -> None:
166
+ """Start a new thread to read data from port and save to data cache."""
167
+ if self._pexpect_spawn:
168
+ return
169
+ self._init_log_file()
170
+ env = os.environ.copy()
171
+ env['PYTHONUNBUFFERED'] = 'true' # for python scripts, disable output buffering
172
+ self._pexpect_spawn = pexpect.spawn(self._cmd, maxread=8192, echo=False, env=env) # type: ignore
173
+ self._pexpect_spawn.logfile = self.log_file_f # type: ignore
174
+ # self._pexpect_spawn.delaybeforesend = 0.001
175
+
176
+ def stop_redirect_thread(self) -> bool:
177
+ """Stop the redirect thread and pexpect process."""
178
+ if not self._pexpect_spawn:
179
+ return False
180
+ self._init_log_file()
181
+ self._pexpect_spawn.close()
182
+ self._pexpect_spawn = None # type: ignore
183
+ return True
184
+
185
+ def close(self) -> None:
186
+ """Close pexpect process."""
187
+ super().close()
188
+ self.stop_redirect_thread()
189
+ if self.log_file_f:
190
+ self.log_file_f.close()
191
+ self.log_file_f = None
@@ -5,6 +5,30 @@ from typing import Any, List, Optional
5
5
 
6
6
  import yaml
7
7
 
8
+ VAR_NAME_MAPPING = {
9
+ 'ap_ssid': ['RUNNER_WIFI_SSID', 'RUNNER_AP_SSID'],
10
+ 'ap_password': ['RUNNER_WIFI_PASSWORD', 'RUNNER_AP_PASSWORD'],
11
+ 'pc_nic': ['RUNNER_PC_NIC'],
12
+ 'dut1': ['ESPPORT1'],
13
+ 'dut2': ['ESPPORT2'],
14
+ 'dut3': ['ESPPORT3'],
15
+ }
16
+
17
+
18
+ def get_variable_from_env(key: str) -> Any:
19
+ """Get test variable from shell environment
20
+
21
+ Args:
22
+ key (str): which variable to get
23
+ """
24
+ if key in VAR_NAME_MAPPING:
25
+ for var_name in VAR_NAME_MAPPING[key]:
26
+ var = os.getenv(var_name)
27
+ if var is not None:
28
+ logging.debug(f'Got env variable from shell env {var_name}: {var}')
29
+ return var
30
+ return None
31
+
8
32
 
9
33
  class EnvConfig:
10
34
  """Get test environment variables from config file.
@@ -32,6 +56,8 @@ class EnvConfig:
32
56
  # Find env config file from project root path
33
57
  # CI_PROJECT_DIR was set by gitlab CI
34
58
  PROJECT_ROOT_DIR = os.getenv('PROJECT_ROOT_DIR') or os.getenv('CI_PROJECT_DIR', '')
59
+ # allow EnvConfig load shell env variables, default enabled
60
+ DISABLE_LOAD_SHELL_ENV = os.getenv('ESPTEST_DISABLE_LOAD_SHELL_ENV', '').lower() in ('true', '1', 'yes', 'y')
35
61
 
36
62
  # Allow input variables from terminal during local debugging
37
63
  ALLOW_INPUT = not os.getenv('CI')
@@ -87,10 +113,7 @@ class EnvConfig:
87
113
  continue
88
114
  config_file = os.path.join(_dir, cls.ENV_CONFIG_FILE_BASE_NAME)
89
115
  if not config_file:
90
- _msg = (
91
- 'Can not find env config file from:\n ',
92
- ' \n'.join(cls._search_dirs()),
93
- )
116
+ _msg = 'Can not find env config file from:\n ' + ' \n'.join(cls._search_dirs())
94
117
  logging.warning(_msg)
95
118
  if not cls.ALLOW_INPUT:
96
119
  raise FileNotFoundError(f'Could not find config file: {cls.ENV_CONFIG_FILE_BASE_NAME}')
@@ -115,14 +138,18 @@ class EnvConfig:
115
138
  # do not use dict.get because we can input the variable for local tests
116
139
  if key in self.config_data:
117
140
  var = self.config_data[key]
118
- elif default:
141
+ elif default is not None:
119
142
  var = default
120
143
  else:
121
- logging.warning(f'Failed to get env variable {self.env_tag}/{key}.')
122
- logging.info(self.__doc__)
123
- if not self.ALLOW_INPUT:
124
- raise ValueError(f'Env variable not found: {self.env_tag}/{key}')
125
- # For local test, support input the variable from console
126
- var = input('You can input the variable now:')
144
+ if not self.DISABLE_LOAD_SHELL_ENV:
145
+ # Try to get from shell environment variables
146
+ var = get_variable_from_env(key)
147
+ if var is None:
148
+ logging.warning(f'Failed to get env variable {self.env_tag}/{key}.')
149
+ logging.info(self.__doc__)
150
+ if not self.ALLOW_INPUT:
151
+ raise ValueError(f'Env variable not found: {self.env_tag}/{key}')
152
+ # For local test, support input the variable from console
153
+ var = input('You can input the variable now:')
127
154
  logging.debug(f'Got env variable {self.env_tag}/{key}: {var}')
128
155
  return var
@@ -84,3 +84,14 @@ def list_all_esp_ports() -> t.List[EspPortInfo]:
84
84
  for port in get_all_serial_ports():
85
85
  esp_ports.append(detect_one_port(port))
86
86
  return esp_ports
87
+
88
+
89
+ def get_available_ports(target: str, max_num: int = 0) -> t.List[EspPortInfo]:
90
+ detect_ports = []
91
+ for port in get_all_serial_ports():
92
+ esp_port = detect_one_port(port)
93
+ if esp_port.target == target:
94
+ detect_ports.append(esp_port)
95
+ if max_num > 0 and len(detect_ports) >= max_num: # pylint: disable=chained-comparison
96
+ return detect_ports
97
+ return detect_ports
@@ -77,7 +77,7 @@ class DownBinTool:
77
77
  parttool: str = '',
78
78
  esptool: str = '',
79
79
  erase_nvs: bool = True,
80
- force_no_stub: bool = True,
80
+ force_no_stub: bool = False,
81
81
  ): # pylint: disable=too-many-positional-arguments,too-many-arguments
82
82
  self.bin_path = bin_path
83
83
  self.port = compute_serial_port(port, strict=True)
@@ -4,6 +4,7 @@ import os
4
4
  import subprocess
5
5
  import tempfile
6
6
  from dataclasses import dataclass
7
+ from functools import lru_cache
7
8
  from pathlib import Path
8
9
 
9
10
  import esptest.common.compat_typing as t
@@ -181,6 +182,7 @@ class ParseBinPath:
181
182
  except subprocess.SubprocessError as e:
182
183
  logger.error(f'Failed to gen partition-table.csv: {str(e)}')
183
184
 
185
+ @lru_cache()
184
186
  def parse_partitions(self) -> t.List[PartitionInfo]:
185
187
  """Parse partitions from partition-table.csv"""
186
188
  self._gen_partition_table()
@@ -293,3 +295,9 @@ class ParseBinPath:
293
295
  # erase nvs
294
296
  return args + list(self._gen_erase_nvs_bin())
295
297
  raise ValueError('Can not find nvs partition info')
298
+
299
+ def get_partition_info(self, part_name: str) -> PartitionInfo:
300
+ for part in self.parse_partitions():
301
+ if part.name == part_name:
302
+ return part
303
+ raise ValueError('Can not find nvs partition info')
@@ -0,0 +1,94 @@
1
+ import random
2
+ import re
3
+ import subprocess
4
+ import time
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from esptest.adapter.port.shell_port import PexpectPort, ShellPort, ShellRaw
10
+
11
+
12
+ def test_shell_raw_open_close() -> None:
13
+ ran_int = random.randint(12345678, 87654321)
14
+ raw_port = ShellRaw(cmd=f'sleep {ran_int}')
15
+ assert raw_port.proc is not None
16
+ output = subprocess.check_output(f'ps -ef | grep {ran_int}', shell=True).decode('utf-8')
17
+ assert f'sleep {ran_int}' in output
18
+ raw_port.close()
19
+ output = subprocess.check_output(f'ps -ef | grep {ran_int}', shell=True).decode('utf-8')
20
+ assert f'sleep {ran_int}' not in output
21
+
22
+
23
+ def test_shell_port_open_close() -> None:
24
+ ran_int = random.randint(12345678, 87654321)
25
+ # close by close method
26
+ port = ShellPort(cmd=f'sleep {ran_int}')
27
+ assert isinstance(port.raw_port, ShellRaw)
28
+ output = subprocess.check_output(f'ps -ef | grep {ran_int}', shell=True).decode('utf-8')
29
+ assert f'sleep {ran_int}' in output
30
+ port.close()
31
+ output = subprocess.check_output(f'ps -ef | grep {ran_int}', shell=True).decode('utf-8')
32
+ assert f'sleep {ran_int}' not in output
33
+ # close by with statement
34
+ with ShellPort(cmd=f'sleep {ran_int}') as port:
35
+ assert isinstance(port.raw_port, ShellRaw)
36
+ output = subprocess.check_output(f'ps -ef | grep {ran_int}', shell=True).decode('utf-8')
37
+ assert f'sleep {ran_int}' in output
38
+ output = subprocess.check_output(f'ps -ef | grep {ran_int}', shell=True).decode('utf-8')
39
+ assert f'sleep {ran_int}' not in output
40
+
41
+
42
+ def test_shell_port_read_write() -> None:
43
+ with ShellPort(cmd='/bin/bash') as port:
44
+ port.write_line('echo hello')
45
+ time.sleep(0.1) # wait for the receive thread
46
+ assert 'hello' in port.read_all_data()
47
+ port.write_line('sleep 0.1 && echo world')
48
+ assert 'world' not in port.read_all_data()
49
+ match = port.expect(re.compile('world'))
50
+ assert match.group(0) == 'world'
51
+
52
+
53
+ def test_shell_port_logfile(tmp_path: Path) -> None:
54
+ log_file = tmp_path / 'shell_port1.log'
55
+ with ShellPort(cmd='/bin/bash', log_file=str(log_file)) as port:
56
+ port.write_line('echo hello')
57
+ time.sleep(0.1) # wait for the receive thread
58
+ with open(str(log_file), 'r') as f:
59
+ assert 'hello' in f.read()
60
+ port.log_file = str(tmp_path / 'shell_port2.log')
61
+ port.write_line('echo world')
62
+ time.sleep(0.1) # wait for the receive thread
63
+ with open(str(tmp_path / 'shell_port2.log'), 'r') as f:
64
+ assert 'world' in f.read()
65
+
66
+
67
+ def test_pexpect_spawn_port_read_write() -> None:
68
+ with PexpectPort(cmd='/bin/bash') as port:
69
+ port.write_line('echo hello')
70
+ time.sleep(0.5) # wait for the receive thread
71
+ assert 'hello' in port.read_all_data()
72
+ port.write_line('sleep 0.1 && echo world')
73
+ assert 'world' not in port.read_all_data()
74
+ match = port.expect(re.compile('world'))
75
+ assert match.group(0) == 'world'
76
+
77
+
78
+ def test_pexpect_spawn_port_logfile(tmp_path: Path) -> None:
79
+ log_file = tmp_path / 'shell_port1.log'
80
+ with PexpectPort(cmd='/bin/bash', log_file=str(log_file)) as port:
81
+ port.write_line('echo hello')
82
+ time.sleep(0.5) # wait for the receive thread
83
+ with open(str(log_file), 'r') as f:
84
+ assert 'hello' in f.read()
85
+ port.log_file = str(tmp_path / 'shell_port2.log')
86
+ port.write_line('echo world')
87
+ time.sleep(0.1) # wait for the receive thread
88
+ with open(str(tmp_path / 'shell_port2.log'), 'r') as f:
89
+ assert 'world' in f.read()
90
+
91
+
92
+ if __name__ == '__main__':
93
+ # Breakpoints do not work with coverage, disable coverage for debugging
94
+ pytest.main([__file__, '--no-cov', '--log-cli-level=DEBUG'])
@@ -65,6 +65,30 @@ def test_env_config_get_var(tmp_path: Path) -> None:
65
65
  env_config.get_variable('dut_port')
66
66
 
67
67
 
68
+ def test_env_config_from_shell_env(tmp_path: Path) -> None:
69
+ # Test Get variable from console
70
+ config_file = tmp_path / 'not_exist_config.yml'
71
+ env = {
72
+ 'TEST_ENV_CONFIG_FILE': str(config_file),
73
+ }
74
+ with reload_envconfig(env):
75
+ try:
76
+ os.environ.pop('RUNNER_WIFI_SSID')
77
+ except KeyError:
78
+ pass
79
+ try:
80
+ os.environ.pop('RUNNER_AP_SSID')
81
+ except KeyError:
82
+ pass
83
+ env_config = EnvConfig()
84
+ env_config.ALLOW_INPUT = False
85
+ with pytest.raises(ValueError):
86
+ var = env_config.get_variable('ap_ssid')
87
+ os.environ['RUNNER_AP_SSID'] = 'ssid_from_env'
88
+ var = env_config.get_variable('ap_ssid')
89
+ assert var == 'ssid_from_env'
90
+
91
+
68
92
  def test_env_config_from_console(tmp_path, monkeypatch): # type: ignore
69
93
  # Test Get variable from console
70
94
  config_file = tmp_path / 'not_exist_config.yml'
File without changes
File without changes
File without changes