esp-test-utils 0.2.2__tar.gz → 0.3.0__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 (118) hide show
  1. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/CHANGELOG.md +16 -0
  2. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/PKG-INFO +3 -1
  3. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esp_test_utils.egg-info/PKG-INFO +3 -1
  4. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esp_test_utils.egg-info/SOURCES.txt +6 -1
  5. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esp_test_utils.egg-info/requires.txt +2 -0
  6. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/adapter/dut/esp_mixin.py +5 -3
  7. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/adapter/port/base_port.py +35 -8
  8. esp_test_utils-0.3.0/esptest/adapter/port/shell_port.py +191 -0
  9. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/common/decorators.py +20 -0
  10. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/config/env_config.py +1 -4
  11. esp_test_utils-0.3.0/esptest/devices/switch.py +544 -0
  12. esp_test_utils-0.3.0/esptest/network/mac.py +31 -0
  13. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/scripts/downbin.py +10 -1
  14. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/scripts/list_ports.py +20 -0
  15. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/tools/download_bin.py +33 -6
  16. esp_test_utils-0.3.0/esptest/tools/uart_monitor.py +324 -0
  17. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/pyproject.toml +5 -0
  18. {esp_test_utils-0.2.2/tests → esp_test_utils-0.3.0/tests/adapter}/test_Dut.py +21 -0
  19. esp_test_utils-0.3.0/tests/adapter/test_shell_port.py +94 -0
  20. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/basic/test_decorators.py +25 -1
  21. esp_test_utils-0.3.0/tests/devices/test_switch.py +217 -0
  22. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/test_EnvConfig.py +24 -0
  23. esp_test_utils-0.2.2/esptest/network/mac.py +0 -5
  24. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/.github/.gitkeep +0 -0
  25. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/.github/workflows/pypi-publish.yml +0 -0
  26. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/.gitignore +0 -0
  27. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/.gitlab-ci.yml +0 -0
  28. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/.pre-commit-config.yaml +0 -0
  29. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/CONTRIBUTING.md +0 -0
  30. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/LICENSE +0 -0
  31. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/README.md +0 -0
  32. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/docs/Makefile +0 -0
  33. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/docs/conf.py +0 -0
  34. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/docs/index.rst +0 -0
  35. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/docs/make.bat +0 -0
  36. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esp_test_utils.egg-info/dependency_links.txt +0 -0
  37. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esp_test_utils.egg-info/entry_points.txt +0 -0
  38. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esp_test_utils.egg-info/top_level.txt +0 -0
  39. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/__init__.py +0 -0
  40. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/__main__.py +0 -0
  41. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/adapter/__init__.py +0 -0
  42. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/adapter/dut/__init__.py +0 -0
  43. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/adapter/dut/create_dut.py +0 -0
  44. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/adapter/dut/dut_base.py +0 -0
  45. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/adapter/dut/esp_dut.py +0 -0
  46. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/adapter/dut/esp_port.py +0 -0
  47. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/adapter/dut/mac_mixin.py +0 -0
  48. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/adapter/dut/wrapper.py +0 -0
  49. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/adapter/port/__init__.py +0 -0
  50. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/adapter/port/serial_port.py +0 -0
  51. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/all.py +0 -0
  52. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/common/__init__.py +0 -0
  53. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/common/compat_typing.py +0 -0
  54. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/common/data_monitor.py +0 -0
  55. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/common/encoding.py +0 -0
  56. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/common/generator.py +0 -0
  57. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/common/shell.py +0 -0
  58. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/common/timestamp.py +0 -0
  59. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/config/__init__.py +0 -0
  60. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/config/default_config.py +0 -0
  61. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/db/__init__.py +0 -0
  62. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/db/runners.py +0 -0
  63. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/devices/__init__.py +0 -0
  64. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/devices/attenuator.py +0 -0
  65. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/devices/esp_serial.py +0 -0
  66. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/devices/serial_dut.py +0 -0
  67. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/devices/serial_tools.py +0 -0
  68. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/env/__init__.py +0 -0
  69. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/env/base_env.py +0 -0
  70. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/env/wifi_env.py +0 -0
  71. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/esp_console/__init__.py +0 -0
  72. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/esp_console/wifi_cmd.py +0 -0
  73. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/interface/__init__.py +0 -0
  74. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/interface/dut.py +0 -0
  75. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/interface/port.py +0 -0
  76. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/iperf_utility/__init__.py +0 -0
  77. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/iperf_utility/iperf_results.py +0 -0
  78. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/iperf_utility/iperf_test.py +0 -0
  79. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/iperf_utility/iperf_test.test.py +0 -0
  80. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/iperf_utility/line_chart.py +0 -0
  81. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/logger/__init__.py +0 -0
  82. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/logger/logger.py +0 -0
  83. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/network/__init__.py +0 -0
  84. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/network/netif.py +0 -0
  85. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/network/nic.py +0 -0
  86. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/scripts/monitor.py +0 -0
  87. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/scripts/set_att.py +0 -0
  88. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/tools/copy_bin.py +0 -0
  89. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/tools/http_download.py +0 -0
  90. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/tools/pip_check.py +0 -0
  91. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/utility/gen_esp32part.py +0 -0
  92. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/esptest/utility/parse_bin_path.py +0 -0
  93. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/example/jap_test.py +0 -0
  94. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/example/restart_test.py +0 -0
  95. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/setup.cfg +0 -0
  96. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/__init__.py +0 -0
  97. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/basic/test_network.py +0 -0
  98. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/conftest.py +0 -0
  99. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/db/test_db_runners.py +0 -0
  100. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/esp_console/_files/wifi_cmd_connected_1.log +0 -0
  101. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/esp_console/_files/wifi_cmd_connected_2.log +0 -0
  102. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/esp_console/conftest.py +0 -0
  103. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/esp_console/test_WifiCmd.py +0 -0
  104. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/iperf_utility/_files/dut_iperf_rx1.log +0 -0
  105. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/iperf_utility/_files/dut_iperf_rx2.log +0 -0
  106. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/iperf_utility/_files/pc_iperf_rx.log +0 -0
  107. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/iperf_utility/_files/pc_iperf_rx2.log +0 -0
  108. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/iperf_utility/test_chart.py +0 -0
  109. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/iperf_utility/test_iperf_results.py +0 -0
  110. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/iperf_utility/test_iperf_util.py +0 -0
  111. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/test_common.py +0 -0
  112. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/test_import.py +0 -0
  113. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/tools/test_download_file.py +0 -0
  114. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/tools/test_pip_check.py +0 -0
  115. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/utility/_files/test-bin.zip +0 -0
  116. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/utility/_files/test-get-baud/ESP32AT-V4.1.1.0/sdkconfig +0 -0
  117. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tests/utility/test_parse_bin_path.py +0 -0
  118. {esp_test_utils-0.2.2 → esp_test_utils-0.3.0}/tools/ci/check_dev_version.py +0 -0
@@ -1,3 +1,19 @@
1
+ ## v0.3.0 (2025-12-10)
2
+
3
+
4
+ - feat: esp-listports support monitor mode
5
+ - feat: add more logs to H3CSwitch
6
+ - feat: esp-downbin support argument --force-no-stub
7
+ - feat: add h3c switch device control
8
+ - feat: add decorator timeit
9
+
10
+ ## v0.2.3 (2025-11-14)
11
+
12
+
13
+ - feat: add shell port support
14
+ - fix: Fix esptool connect to given port
15
+ - fix: log of env config search dirs
16
+
1
17
  ## v0.2.2 (2025-10-29)
2
18
 
3
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: esp-test-utils
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: ESP Test Utils
5
5
  Author-email: Chen Yudong <chenyudong@espressif.com>
6
6
  License: Apache License
@@ -228,8 +228,10 @@ Requires-Dist: pyserial
228
228
  Requires-Dist: PyYAML
229
229
  Requires-Dist: pexpect
230
230
  Requires-Dist: pyusb
231
+ Requires-Dist: pyudev
231
232
  Requires-Dist: esptool
232
233
  Requires-Dist: packaging
234
+ Requires-Dist: rich
233
235
  Requires-Dist: sqlalchemy
234
236
  Requires-Dist: typing_extensions; python_version < "3.11"
235
237
  Provides-Extra: idfci
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: esp-test-utils
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: ESP Test Utils
5
5
  Author-email: Chen Yudong <chenyudong@espressif.com>
6
6
  License: Apache License
@@ -228,8 +228,10 @@ Requires-Dist: pyserial
228
228
  Requires-Dist: PyYAML
229
229
  Requires-Dist: pexpect
230
230
  Requires-Dist: pyusb
231
+ Requires-Dist: pyudev
231
232
  Requires-Dist: esptool
232
233
  Requires-Dist: packaging
234
+ Requires-Dist: rich
233
235
  Requires-Dist: sqlalchemy
234
236
  Requires-Dist: typing_extensions; python_version < "3.11"
235
237
  Provides-Extra: idfci
@@ -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
@@ -51,6 +52,7 @@ esptest/devices/attenuator.py
51
52
  esptest/devices/esp_serial.py
52
53
  esptest/devices/serial_dut.py
53
54
  esptest/devices/serial_tools.py
55
+ esptest/devices/switch.py
54
56
  esptest/env/__init__.py
55
57
  esptest/env/base_env.py
56
58
  esptest/env/wifi_env.py
@@ -78,19 +80,22 @@ esptest/tools/copy_bin.py
78
80
  esptest/tools/download_bin.py
79
81
  esptest/tools/http_download.py
80
82
  esptest/tools/pip_check.py
83
+ esptest/tools/uart_monitor.py
81
84
  esptest/utility/gen_esp32part.py
82
85
  esptest/utility/parse_bin_path.py
83
86
  example/jap_test.py
84
87
  example/restart_test.py
85
88
  tests/__init__.py
86
89
  tests/conftest.py
87
- tests/test_Dut.py
88
90
  tests/test_EnvConfig.py
89
91
  tests/test_common.py
90
92
  tests/test_import.py
93
+ tests/adapter/test_Dut.py
94
+ tests/adapter/test_shell_port.py
91
95
  tests/basic/test_decorators.py
92
96
  tests/basic/test_network.py
93
97
  tests/db/test_db_runners.py
98
+ tests/devices/test_switch.py
94
99
  tests/esp_console/conftest.py
95
100
  tests/esp_console/test_WifiCmd.py
96
101
  tests/esp_console/_files/wifi_cmd_connected_1.log
@@ -3,8 +3,10 @@ pyserial
3
3
  PyYAML
4
4
  pexpect
5
5
  pyusb
6
+ pyudev
6
7
  esptool
7
8
  packaging
9
+ rich
8
10
  sqlalchemy
9
11
 
10
12
  [:python_version < "3.11"]
@@ -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:
@@ -26,14 +26,24 @@ NEVER_MATCHED_MAGIC_STRING = 'o6K,Q.(w+~yr~N9R'
26
26
  class ExpectTimeout(TimeoutError):
27
27
  """raise same ExpectTimeout rather than different Exception from different framework"""
28
28
 
29
+ def __init__(self, message: str, data_in_buffer: t.Union[str, bytes] = b'') -> None:
30
+ super().__init__(message)
31
+ self.data_in_buffer: t.Union[str, bytes] = data_in_buffer
32
+
33
+ def __str__(self) -> str:
34
+ return f'{super().__str__()}\n data_in_buffer={repr(self.data_in_buffer)}'
35
+
29
36
 
30
37
  class RawPort(metaclass=abc.ABCMeta):
31
38
  """Define a minimum Dut class, the dut objects should at least support these methods
32
39
 
33
40
  the dut should at least support these attributes:
34
- - attribute name with type str
35
41
  - method: write_bytes() with parameters: data[bytes]
36
42
  - method: read_bytes() with parameters: timeout[float]
43
+
44
+ optional attribute & method:
45
+ - attribute: name with type str
46
+ - attribute: read_timeout with type float
37
47
  """
38
48
 
39
49
  @classmethod
@@ -243,7 +253,11 @@ class PortSpawn(pexpect.spawnbase.SpawnBase, t.Generic[T]):
243
253
  self._log(ret_data, 'read') # type: ignore
244
254
  return ret_data
245
255
 
256
+ @deprecated('Should use close() for Spawn')
246
257
  def stop(self) -> None:
258
+ self.close()
259
+
260
+ def close(self) -> None:
247
261
  """Stop and clean up"""
248
262
  self.logger.debug(f'Stopping SerialSpawn {self.name}')
249
263
  self._read_thread_stop_event.set()
@@ -359,7 +373,7 @@ class BasePort(PortInterface, t.Generic[T]):
359
373
  if new_log_file == self._log_file:
360
374
  return
361
375
  if self._pexpect_spawn:
362
- self._pexpect_spawn.serial_log_file = new_log_file
376
+ self._pexpect_spawn.log_file = new_log_file
363
377
  self._log_file = new_log_file
364
378
 
365
379
  @property
@@ -381,7 +395,7 @@ class BasePort(PortInterface, t.Generic[T]):
381
395
  if not self._pexpect_spawn:
382
396
  return False
383
397
  self._init_log_file()
384
- self._pexpect_spawn.stop()
398
+ self._pexpect_spawn.close()
385
399
  self._pexpect_spawn = None
386
400
  return True
387
401
 
@@ -401,7 +415,12 @@ class BasePort(PortInterface, t.Generic[T]):
401
415
  try:
402
416
  result = func(self, *args, **kwargs)
403
417
  except self.expect_timeout_exceptions as e:
404
- raise ExpectTimeout(str(e)) from e
418
+ try:
419
+ data_in_buffer = self._pexpect_spawn.before # pylint: disable=protected-access
420
+ except AttributeError:
421
+ data_in_buffer = ''
422
+ self.logger.debug(f'ExpectTimeout: {str(e)}, data_in_buffer={repr(data_in_buffer)}')
423
+ raise ExpectTimeout(str(e), data_in_buffer=data_in_buffer) from e
405
424
  return result
406
425
 
407
426
  return wrap
@@ -486,9 +505,13 @@ class BasePort(PortInterface, t.Generic[T]):
486
505
  """
487
506
  buffer = b''
488
507
  if flush:
489
- match = self.expect(re.compile(b'.*', re.DOTALL), timeout=0)
490
- assert match
491
- buffer = match.group(0)
508
+ # pexpect may return empty bytes if b'(.*)' is used
509
+ try:
510
+ match = self.expect(re.compile(b'(.+)', re.DOTALL), timeout=0)
511
+ assert match
512
+ buffer = match.group(0)
513
+ except TimeoutError:
514
+ pass
492
515
  else:
493
516
  # flush spawn buffer
494
517
  assert self._pexpect_spawn
@@ -499,7 +522,11 @@ class BasePort(PortInterface, t.Generic[T]):
499
522
 
500
523
  def close(self) -> None:
501
524
  if self._close_redirect_thread_when_exit and self._pexpect_spawn:
502
- self._pexpect_spawn.stop()
525
+ self._pexpect_spawn.close()
526
+ if self.raw_port:
527
+ if hasattr(self.raw_port, 'close'):
528
+ assert callable(self.raw_port.close) # type: ignore
529
+ self.raw_port.close() # type: ignore
503
530
 
504
531
  def __enter__(self) -> 't.Self':
505
532
  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
@@ -109,3 +109,23 @@ def suppress_stdout() -> t.Callable[[GenericFunc], GenericFunc]:
109
109
  return t.cast(GenericFunc, wrapper)
110
110
 
111
111
  return decorator
112
+
113
+
114
+ def timeit(
115
+ print_func: t.Callable[[str], None] = logger.critical,
116
+ format_str: str = 'Func {func_name} time used: {time_used:.2f} s',
117
+ ) -> t.Callable[[GenericFunc], GenericFunc]:
118
+ """Show time used when method is called"""
119
+
120
+ def decorator(func: GenericFunc) -> GenericFunc:
121
+ @wraps(func)
122
+ def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
123
+ start_time = time.perf_counter()
124
+ ret = func(*args, **kwargs)
125
+ end_time = time.perf_counter()
126
+ print_func(format_str.format(func_name=func.__name__, time_used=end_time - start_time))
127
+ return ret
128
+
129
+ return t.cast(GenericFunc, wrapper)
130
+
131
+ return decorator
@@ -113,10 +113,7 @@ class EnvConfig:
113
113
  continue
114
114
  config_file = os.path.join(_dir, cls.ENV_CONFIG_FILE_BASE_NAME)
115
115
  if not config_file:
116
- _msg = (
117
- 'Can not find env config file from:\n ',
118
- ' \n'.join(cls._search_dirs()),
119
- )
116
+ _msg = 'Can not find env config file from:\n ' + ' \n'.join(cls._search_dirs())
120
117
  logging.warning(_msg)
121
118
  if not cls.ALLOW_INPUT:
122
119
  raise FileNotFoundError(f'Could not find config file: {cls.ENV_CONFIG_FILE_BASE_NAME}')