esp-test-utils 0.3.3__tar.gz → 0.3.4__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 (130) hide show
  1. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/CHANGELOG.md +9 -0
  2. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/PKG-INFO +1 -1
  3. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esp_test_utils.egg-info/PKG-INFO +1 -1
  4. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esp_test_utils.egg-info/SOURCES.txt +6 -1
  5. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/adapter/dut/dut_base.py +43 -2
  6. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/adapter/dut/esp_dut.py +7 -3
  7. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/adapter/port/base_port.py +122 -41
  8. esp_test_utils-0.3.4/esptest/adapter/port/data_monitor_mixin.py +36 -0
  9. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/adapter/port/serial_port.py +2 -2
  10. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/adapter/port/shell_port.py +3 -1
  11. esp_test_utils-0.3.4/esptest/common/data_monitor.py +134 -0
  12. esp_test_utils-0.3.4/esptest/config/global_config.py +18 -0
  13. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/interface/port.py +14 -0
  14. esp_test_utils-0.3.4/example/use_data_monitor.py +56 -0
  15. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/adapter/test_Dut.py +210 -2
  16. esp_test_utils-0.3.4/tests/adapter/test_base_port.py +160 -0
  17. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/adapter/test_shell_port.py +59 -3
  18. esp_test_utils-0.3.4/tests/basic/test_data_monitor.py +174 -0
  19. esp_test_utils-0.3.4/tests/test_global_config.py +47 -0
  20. esp_test_utils-0.3.3/esptest/common/data_monitor.py +0 -52
  21. esp_test_utils-0.3.3/esptest/config/default_config.py +0 -2
  22. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/.github/.gitkeep +0 -0
  23. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/.github/workflows/pypi-publish.yml +0 -0
  24. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/.gitignore +0 -0
  25. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/.gitlab-ci.yml +0 -0
  26. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/.pre-commit-config.yaml +0 -0
  27. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/CONTRIBUTING.md +0 -0
  28. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/LICENSE +0 -0
  29. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/README.md +0 -0
  30. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/docs/Makefile +0 -0
  31. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/docs/conf.py +0 -0
  32. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/docs/index.rst +0 -0
  33. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/docs/make.bat +0 -0
  34. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esp_test_utils.egg-info/dependency_links.txt +0 -0
  35. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esp_test_utils.egg-info/entry_points.txt +0 -0
  36. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esp_test_utils.egg-info/requires.txt +0 -0
  37. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esp_test_utils.egg-info/top_level.txt +0 -0
  38. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/__init__.py +0 -0
  39. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/__main__.py +0 -0
  40. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/adapter/__init__.py +0 -0
  41. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/adapter/dut/__init__.py +0 -0
  42. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/adapter/dut/create_dut.py +0 -0
  43. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/adapter/dut/esp_mixin.py +0 -0
  44. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/adapter/dut/esp_port.py +0 -0
  45. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/adapter/dut/mac_mixin.py +0 -0
  46. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/adapter/dut/wrapper.py +0 -0
  47. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/adapter/port/__init__.py +0 -0
  48. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/all.py +0 -0
  49. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/common/__init__.py +0 -0
  50. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/common/compat_typing.py +0 -0
  51. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/common/decorators.py +0 -0
  52. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/common/encoding.py +0 -0
  53. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/common/generator.py +0 -0
  54. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/common/parser.py +0 -0
  55. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/common/shell.py +0 -0
  56. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/common/timestamp.py +0 -0
  57. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/config/__init__.py +0 -0
  58. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/config/env_config.py +0 -0
  59. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/db/__init__.py +0 -0
  60. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/db/runners.py +0 -0
  61. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/devices/__init__.py +0 -0
  62. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/devices/attenuator.py +0 -0
  63. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/devices/esp_serial.py +0 -0
  64. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/devices/serial_dut.py +0 -0
  65. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/devices/serial_tools.py +0 -0
  66. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/devices/switch.py +0 -0
  67. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/env/__init__.py +0 -0
  68. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/env/base_env.py +0 -0
  69. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/env/wifi_env.py +0 -0
  70. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/esp_console/__init__.py +0 -0
  71. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/esp_console/wifi_cmd.py +0 -0
  72. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/interface/__init__.py +0 -0
  73. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/interface/dut.py +0 -0
  74. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/iperf_utility/__init__.py +0 -0
  75. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/iperf_utility/iperf_results.py +0 -0
  76. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/iperf_utility/iperf_test.py +0 -0
  77. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/iperf_utility/iperf_test.test.py +0 -0
  78. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/iperf_utility/line_chart.py +0 -0
  79. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/logger/__init__.py +0 -0
  80. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/logger/logger.py +0 -0
  81. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/network/__init__.py +0 -0
  82. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/network/mac.py +0 -0
  83. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/network/netif.py +0 -0
  84. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/network/nic.py +0 -0
  85. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/scripts/downbin.py +0 -0
  86. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/scripts/list_ports.py +0 -0
  87. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/scripts/monitor.py +0 -0
  88. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/scripts/set_att.py +0 -0
  89. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/testcase/__init__.py +0 -0
  90. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/testcase/result.py +0 -0
  91. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/tools/copy_bin.py +0 -0
  92. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/tools/download_bin.py +0 -0
  93. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/tools/http_download.py +0 -0
  94. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/tools/pip_check.py +0 -0
  95. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/tools/uart_monitor.py +0 -0
  96. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/utility/gen_esp32part.py +0 -0
  97. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/esptest/utility/parse_bin_path.py +0 -0
  98. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/example/jap_test.py +0 -0
  99. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/example/restart_test.py +0 -0
  100. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/pyproject.toml +0 -0
  101. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/setup.cfg +0 -0
  102. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/__init__.py +0 -0
  103. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/basic/test_decorators.py +0 -0
  104. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/basic/test_network.py +0 -0
  105. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/conftest.py +0 -0
  106. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/db/test_db_runners.py +0 -0
  107. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/devices/test_switch.py +0 -0
  108. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/esp_console/_files/wifi_cmd_connected_1.log +0 -0
  109. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/esp_console/_files/wifi_cmd_connected_2.log +0 -0
  110. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/esp_console/conftest.py +0 -0
  111. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/esp_console/test_WifiCmd.py +0 -0
  112. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/iperf_utility/_files/dut_iperf_rx1.log +0 -0
  113. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/iperf_utility/_files/dut_iperf_rx2.log +0 -0
  114. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/iperf_utility/_files/pc_iperf_rx.log +0 -0
  115. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/iperf_utility/_files/pc_iperf_rx2.log +0 -0
  116. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/iperf_utility/test_chart.py +0 -0
  117. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/iperf_utility/test_iperf_results.py +0 -0
  118. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/iperf_utility/test_iperf_util.py +0 -0
  119. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/test_EnvConfig.py +0 -0
  120. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/test_common.py +0 -0
  121. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/test_import.py +0 -0
  122. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/test_parse_expand_list.py +0 -0
  123. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/testcase/test_result.py +0 -0
  124. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/tools/test_download_bin.py +0 -0
  125. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/tools/test_download_file.py +0 -0
  126. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/tools/test_pip_check.py +0 -0
  127. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/utility/_files/test-bin.zip +0 -0
  128. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/utility/_files/test-get-baud/ESP32AT-V4.1.1.0/sdkconfig +0 -0
  129. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tests/utility/test_parse_bin_path.py +0 -0
  130. {esp_test_utils-0.3.3 → esp_test_utils-0.3.4}/tools/ci/check_dev_version.py +0 -0
@@ -1,3 +1,12 @@
1
+ ## v0.3.4 (2026-04-27)
2
+
3
+
4
+ - fix: make serial read error reconnect configurable
5
+ - fix: resolve data monitor review findings
6
+ - feat: support add data monitor and callback to port
7
+ - fix(port): propagate monitors and callbacks consistently
8
+ - fix: pexpect buffer maxread limit and data cache overflow
9
+
1
10
  ## v0.3.3 (2026-04-21)
2
11
 
3
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: esp-test-utils
3
- Version: 0.3.3
3
+ Version: 0.3.4
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.3.3
3
+ Version: 0.3.4
4
4
  Summary: ESP Test Utils
5
5
  Author-email: Chen Yudong <chenyudong@espressif.com>
6
6
  License: Apache License
@@ -32,6 +32,7 @@ esptest/adapter/dut/mac_mixin.py
32
32
  esptest/adapter/dut/wrapper.py
33
33
  esptest/adapter/port/__init__.py
34
34
  esptest/adapter/port/base_port.py
35
+ esptest/adapter/port/data_monitor_mixin.py
35
36
  esptest/adapter/port/serial_port.py
36
37
  esptest/adapter/port/shell_port.py
37
38
  esptest/common/__init__.py
@@ -44,8 +45,8 @@ esptest/common/parser.py
44
45
  esptest/common/shell.py
45
46
  esptest/common/timestamp.py
46
47
  esptest/config/__init__.py
47
- esptest/config/default_config.py
48
48
  esptest/config/env_config.py
49
+ esptest/config/global_config.py
49
50
  esptest/db/__init__.py
50
51
  esptest/db/runners.py
51
52
  esptest/devices/__init__.py
@@ -88,14 +89,18 @@ esptest/utility/gen_esp32part.py
88
89
  esptest/utility/parse_bin_path.py
89
90
  example/jap_test.py
90
91
  example/restart_test.py
92
+ example/use_data_monitor.py
91
93
  tests/__init__.py
92
94
  tests/conftest.py
93
95
  tests/test_EnvConfig.py
94
96
  tests/test_common.py
97
+ tests/test_global_config.py
95
98
  tests/test_import.py
96
99
  tests/test_parse_expand_list.py
97
100
  tests/adapter/test_Dut.py
101
+ tests/adapter/test_base_port.py
98
102
  tests/adapter/test_shell_port.py
103
+ tests/basic/test_data_monitor.py
99
104
  tests/basic/test_decorators.py
100
105
  tests/basic/test_network.py
101
106
  tests/db/test_db_runners.py
@@ -11,12 +11,14 @@ from esptool.loader import ESPLoader
11
11
 
12
12
  import esptest.common.compat_typing as t
13
13
 
14
+ from ...common.data_monitor import DataMonitor
14
15
  from ...common.timestamp import timestamp_slug
15
16
  from ...interface.dut import DutInterface
16
17
  from ...interface.port import PortInterface
17
18
  from ...logger import get_logger
18
19
  from ...utility.parse_bin_path import ParseBinPath, get_baud_from_bin_path
19
20
  from ..port.base_port import BasePort, RawPort
21
+ from ..port.data_monitor_mixin import DataMonitorMixin
20
22
  from ..port.serial_port import SerialPort
21
23
 
22
24
  logger = get_logger('dut')
@@ -131,16 +133,30 @@ class VariablesMixin:
131
133
  self._dynamic_variables.pop(name)
132
134
 
133
135
 
134
- class DutBase(VariablesMixin, DutInterface): # pylint: disable=too-many-public-methods
136
+ class _DutBase(DutInterface):
137
+ def __init__(self, *args: t.Any, **_kwargs: t.Any) -> None: # pylint: disable=unused-argument
138
+ # kwargs are kept for BasePort creation and should not be forwarded
139
+ # to the end of MRO where object.__init__ rejects extra arguments.
140
+ super().__init__()
141
+
142
+
143
+ class DutBase(VariablesMixin, DataMonitorMixin, _DutBase): # pylint: disable=too-many-public-methods
135
144
  """A base Dut class"""
136
145
 
137
146
  BASE_PORT_PROXY_METHODS = list(PortInterface.__abstractmethods__)
138
147
 
139
148
  def __init__(self, *, dut_config: DutConfig, **kwargs: t.Any) -> None:
149
+ # pass extra parameters to base port
150
+ self._kwargs = kwargs
151
+ if dut_config.monitors:
152
+ self._kwargs['monitors'] = dut_config.monitors
153
+ if dut_config.rx_log_callback:
154
+ self._kwargs['rx_log_callback'] = dut_config.rx_log_callback
155
+ # kwargs are kept for BasePort creation and should not be forwarded
156
+ # to the end of MRO where object.__init__ rejects extra arguments.
140
157
  super().__init__(**kwargs)
141
158
  # args and kwargs may be used by mixins
142
159
  self._dut_config: DutConfig = dut_config
143
- self._kwargs = kwargs
144
160
  self._raw_port: t.Optional[RawPort] = None
145
161
  self._base_port_proxy: t.Optional[BasePort] = None
146
162
  self._dut_logger: logging.Logger = self._create_dut_logger()
@@ -235,6 +251,31 @@ class DutBase(VariablesMixin, DutInterface): # pylint: disable=too-many-public-
235
251
  return self._base_port_proxy.log_file
236
252
  return None
237
253
 
254
+ @property
255
+ def rx_log_callback(self) -> t.Optional[t.Callable[[str, bytes], None]]:
256
+ """Get Current dut log file."""
257
+ if self._base_port_proxy:
258
+ return self._base_port_proxy.rx_log_callback
259
+ return t.cast(t.Optional[t.Callable[[str, bytes], None]], self._kwargs.get('rx_log_callback', None))
260
+
261
+ def set_rx_log_callback(self, new_callback: t.Optional[t.Callable[[str, bytes], None]]) -> None:
262
+ self._kwargs['rx_log_callback'] = new_callback
263
+ if self._base_port_proxy:
264
+ self._base_port_proxy.set_rx_log_callback(new_callback)
265
+
266
+ @property
267
+ def monitors(self) -> t.List[DataMonitor]:
268
+ if self._base_port_proxy:
269
+ return self._base_port_proxy.monitors
270
+ return t.cast(t.List[DataMonitor], self._kwargs.setdefault('monitors', []))
271
+
272
+ @monitors.setter
273
+ def monitors(self, new_monitors: t.List[DataMonitor]) -> None:
274
+ synced_monitors = list(new_monitors)
275
+ self._kwargs['monitors'] = synced_monitors
276
+ if self._base_port_proxy:
277
+ self._base_port_proxy.monitors = synced_monitors
278
+
238
279
  @property
239
280
  def name(self) -> t.Any:
240
281
  return self.dut_config.name
@@ -34,11 +34,15 @@ class EspDut(_DefaultMixins, DutBase):
34
34
  if _config.opened_port:
35
35
  if isinstance(_config.opened_port, BasePort):
36
36
  _base_port = _config.opened_port
37
+ if self._kwargs.get('monitors') is not None:
38
+ _base_port.monitors = self._kwargs['monitors']
39
+ if self._kwargs.get('rx_log_callback'):
40
+ _base_port.set_rx_log_callback(self._kwargs['rx_log_callback'])
37
41
  self._close_base_port_when_exit = False
38
42
  self._close_raw_port_when_exit = False
39
43
  return _base_port
40
44
  if isinstance(_config.opened_port, RawPort):
41
- _base_port = BasePort(_config.opened_port, name=_config.name, log_file=_config.log_file)
45
+ _base_port = BasePort(_config.opened_port, name=_config.name, log_file=_config.log_file, **self._kwargs)
42
46
  self._close_raw_port_when_exit = False
43
47
  return _base_port
44
48
  raise TypeError(f'Can not create dut from {type(_config.opened_port)}')
@@ -51,10 +55,10 @@ class EspDut(_DefaultMixins, DutBase):
51
55
  _esp._port.timeout = _config.serial_read_timeout # pylint: disable=protected-access
52
56
  _esp.hard_reset()
53
57
  self._raw_port = _esp
54
- return BasePort(EspSerial(self._raw_port), name=_config.name, log_file=_config.log_file)
58
+ return BasePort(EspSerial(self._raw_port), name=_config.name, log_file=_config.log_file, **self._kwargs)
55
59
  # create basic serial port
56
60
  self._raw_port = SerialExt(port=_device, baudrate=_config.baudrate, **(_config.serial_configs or {}))
57
- return SerialPort(self._raw_port, name=_config.name, log_file=_config.log_file)
61
+ return SerialPort(self._raw_port, name=_config.name, log_file=_config.log_file, **self._kwargs)
58
62
 
59
63
  def close(self) -> None:
60
64
  if self._close_base_port_when_exit:
@@ -8,15 +8,17 @@ import re
8
8
  import sys
9
9
  import threading
10
10
  import time
11
- from dataclasses import dataclass
12
11
  from typing import overload
13
12
 
14
13
  import esptest.common.compat_typing as t
15
14
 
16
15
  from ...common import timestamp_str, to_bytes, to_str
16
+ from ...common.data_monitor import DataMonitor
17
17
  from ...common.decorators import deprecated
18
+ from ...config.global_config import g
18
19
  from ...interface.port import PortInterface
19
20
  from ...logger import get_logger
21
+ from .data_monitor_mixin import DataMonitorMixin
20
22
 
21
23
  if sys.platform == 'win32':
22
24
  import pexpect
@@ -32,6 +34,7 @@ else:
32
34
 
33
35
  logger = get_logger('port')
34
36
  NEVER_MATCHED_MAGIC_STRING = 'o6K,Q.(w+~yr~N9R'
37
+ PEXPECT_DEFAULT_TIMEOUT = g.PORT_EXPECT_TIMEOUT
35
38
 
36
39
 
37
40
  class ExpectTimeout(TimeoutError):
@@ -77,21 +80,6 @@ class RawPort(metaclass=abc.ABCMeta):
77
80
  T = t.TypeVar('T', bound=RawPort)
78
81
 
79
82
 
80
- @dataclass
81
- class SpawnConfig:
82
- name: str = '' # mandatory
83
- timeout: float = 30.0
84
- read_interval: float = 0
85
- # save rx log to log file
86
- log_file: t.Optional[str] = None
87
- # using logger.info for logs
88
- logger: logging.Logger = logger
89
- # callback cb(data,name)
90
- tx_callbacks: t.Optional[t.List[t.Callable[[bytes, str], None]]] = None
91
- rx_callbacks: t.Optional[t.List[t.Callable[[bytes, str], None]]] = None
92
- # TODO: monitors
93
-
94
-
95
83
  class PortSpawn(SpawnBase, t.Generic[T]):
96
84
  """Create a new class for pexpect with port read()/write() method.
97
85
 
@@ -110,7 +98,7 @@ class PortSpawn(SpawnBase, t.Generic[T]):
110
98
  raw_port: T,
111
99
  name: str = '',
112
100
  log_file: t.Optional[str] = None,
113
- timeout: float = 30,
101
+ timeout: float = PEXPECT_DEFAULT_TIMEOUT,
114
102
  **kwargs: t.Any,
115
103
  ) -> None:
116
104
  """PortSpawn for pexpect
@@ -123,6 +111,7 @@ class PortSpawn(SpawnBase, t.Generic[T]):
123
111
  """
124
112
  super().__init__(timeout=timeout)
125
113
  assert isinstance(raw_port, RawPort)
114
+ self.maxread = kwargs.get('maxread', g.PORT_SPAWN_MAXREAD)
126
115
  self.name = name
127
116
  self._raw_port = raw_port
128
117
  if not self.name and hasattr(self.raw_port, 'name'):
@@ -138,10 +127,23 @@ class PortSpawn(SpawnBase, t.Generic[T]):
138
127
  # Create a new thread to read data from serial port
139
128
  self._read_queue: queue.Queue = queue.Queue()
140
129
  self._read_thread_stop_event = threading.Event()
130
+ # callbacks
131
+ self._rx_log_callback: t.Optional[t.Callable[[str, bytes], None]] = kwargs.get('rx_log_callback', None)
132
+ # monitors
133
+ self._monitors: t.Optional[t.List[DataMonitor]] = kwargs.get('monitors', None)
134
+ self._serial_error_reconnect_count_left = max(0, int(g.ALLOW_SERIAL_ERROR_RECONNECT_COUNT))
141
135
  self._read_thread = threading.Thread(target=self._read_incoming, name=f'Spawn_{self.name}')
142
136
  self._read_thread.daemon = True
143
137
  self._read_thread.start()
144
- self.receive_callback: t.Optional[t.Callable[[str, t.AnyStr], None]] = None
138
+
139
+ @property
140
+ def receive_callback(self) -> t.Optional[t.Callable[[str, bytes], None]]:
141
+ return self._rx_log_callback
142
+
143
+ @receive_callback.setter
144
+ @deprecated('set receive_callback directly is deprecated, use rx_log_callback instead')
145
+ def receive_callback(self, new_callback: t.Optional[t.Callable[[str, bytes], None]]) -> None:
146
+ self._rx_log_callback = new_callback
145
147
 
146
148
  @property
147
149
  def raw_port(self) -> T:
@@ -189,6 +191,32 @@ class PortSpawn(SpawnBase, t.Generic[T]):
189
191
  else:
190
192
  self.logger.debug(f'[{self.name}]: {to_str(data_to_write)}')
191
193
 
194
+ def _try_reconnect_after_error(self, err: Exception) -> bool:
195
+ if self._serial_error_reconnect_count_left <= 0:
196
+ return False
197
+ raw_port_close = getattr(self.raw_port, 'close', None)
198
+ raw_port_open = getattr(self.raw_port, 'open', None)
199
+ if not callable(raw_port_close) or not callable(raw_port_open):
200
+ self.logger.warning(f'Skip serial reconnect after serial error on {self.name}: raw port missing close/open')
201
+ return False
202
+ try:
203
+ self._serial_error_reconnect_count_left -= 1
204
+ raw_port_close()
205
+ # Keep a short gap to avoid immediate open/read race on some serial drivers.
206
+ time.sleep(0.1)
207
+ raw_port_open()
208
+ self.logger.warning(
209
+ f'{self.name} got serial error, reopened serial, '
210
+ f'reconnect_left={self._serial_error_reconnect_count_left}'
211
+ )
212
+ return True
213
+ except Exception as reconnect_err: # pylint: disable=broad-except
214
+ self.logger.exception(
215
+ f'{self.name} failed to reconnect serial after error '
216
+ f'{type(err)}: {str(err)}, reconnect_error={type(reconnect_err)}: {str(reconnect_err)}'
217
+ )
218
+ return False
219
+
192
220
  def _read_incoming(self) -> None:
193
221
  """Running in a thread to read serial output and save to data cache."""
194
222
  self.logger.debug(f'Start serial {self.name} read thread.')
@@ -204,15 +232,23 @@ class PortSpawn(SpawnBase, t.Generic[T]):
204
232
  # some port instances do not support changing read timeout, therefore use default timeout of the
205
233
  new_data = self.raw_port.read_bytes(timeout=self.read_timeout)
206
234
  except Exception as e: # pylint: disable=W0718
207
- self._log(to_bytes(f'PortRead {type(e)}: {str(e)}'), 'read')
208
- self._write_port_log(to_bytes(f'SerialException: {str(e)}'))
209
- self.logger.exception(f'{self.name} reading thread stopped {type(e)}: {str(e)}')
210
- return
235
+ self._log(to_bytes(f'PortReadError {type(e)}: {str(e)}'), 'read')
236
+ self.logger.exception(f'{self.name} port read error {type(e)}: {str(e)}')
237
+ time.sleep(0.01) # avoid busy loop
238
+ if self._try_reconnect_after_error(e):
239
+ self.logger.critical(f'{self.name} reconnected after error {type(e)}: {str(e)}')
240
+ new_data = f'[PortException] reconnected after error {type(e)}: {str(e)}\n'.encode()
241
+ else:
242
+ self._write_port_log(to_bytes(f'[PortException] {type(e)}: {str(e)}\n'))
243
+ return
211
244
  if new_data:
212
245
  self._read_queue.put(new_data)
213
- if self.receive_callback and callable(self.receive_callback):
246
+ if self._rx_log_callback:
214
247
  # https://stackoverflow.com/questions/69732212/pylint-self-xxx-is-not-callable
215
- self.receive_callback(self.name, new_data) # pylint: disable=E1102
248
+ self._rx_log_callback(self.name, new_data) # pylint: disable=E1102
249
+ if self._monitors:
250
+ for monitor in self._monitors:
251
+ monitor.append_data(self.name, new_data)
216
252
  # the last line may be cached, to make the file more readable after adding timestamp
217
253
  # always check need write to file or not whether there's new data
218
254
  self._write_port_log(new_data)
@@ -254,6 +290,9 @@ class PortSpawn(SpawnBase, t.Generic[T]):
254
290
  except queue.Empty:
255
291
  break
256
292
  time_left = t0 + timeout - time.time()
293
+ # clear older data cache if it is larger than 2x limit
294
+ if len(self._data_cache) >= g.DATA_CACHE_SIZE_LIMIT * 2:
295
+ self._data_cache = self._data_cache[-g.DATA_CACHE_SIZE_LIMIT :]
257
296
  # Returned data should not more than given size.
258
297
  if self._data_cache:
259
298
  ret_data = self._data_cache[:size]
@@ -274,7 +313,8 @@ class PortSpawn(SpawnBase, t.Generic[T]):
274
313
  self._read_thread_stop_event.set()
275
314
  self._read_thread.join()
276
315
  self._read_queue.empty()
277
- self.receive_callback = None
316
+ self._rx_log_callback = None
317
+ self._monitors = []
278
318
  self._data_cache = b''
279
319
  self._line_cache = b''
280
320
 
@@ -300,7 +340,14 @@ def handle_expect_timeout(func: t.Callable) -> t.Callable:
300
340
  return wrap
301
341
 
302
342
 
303
- class BasePort(PortInterface, t.Generic[T]):
343
+ class _BasePort(PortInterface):
344
+ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: # pylint: disable=unused-argument
345
+ # kwargs are kept for BasePort creation and should not be forwarded
346
+ # to the end of MRO where object.__init__ rejects extra arguments.
347
+ super().__init__()
348
+
349
+
350
+ class BasePort(DataMonitorMixin, _BasePort, t.Generic[T]): # pylint: disable=too-many-public-methods
304
351
  """A class to simply port methods for all devices / shell / sockets to similar usage
305
352
 
306
353
  - Create receive thread and pexpect spawn process for data read/expect
@@ -313,7 +360,6 @@ class BasePort(PortInterface, t.Generic[T]):
313
360
  ExceptionPexpect,
314
361
  )
315
362
  INIT_START_REDIRECT_THREAD: bool = True
316
- PEXPECT_DEFAULT_TIMEOUT: float = 30
317
363
 
318
364
  def __init__(
319
365
  self,
@@ -322,6 +368,7 @@ class BasePort(PortInterface, t.Generic[T]):
322
368
  log_file: str = '',
323
369
  **kwargs: t.Any,
324
370
  ) -> None:
371
+ super().__init__(**kwargs)
325
372
  self._raw_port = raw_port
326
373
  self._name = name
327
374
  self._log_file = log_file
@@ -332,7 +379,7 @@ class BasePort(PortInterface, t.Generic[T]):
332
379
  self._close_redirect_thread_when_exit = kwargs['close_redirect_thread_when_exit']
333
380
  # redirect thread (pexpect spawn)
334
381
  self.expect_timeout_exceptions = self.EXPECT_TIMEOUT_EXCEPTIONS
335
- self.timeout = self.PEXPECT_DEFAULT_TIMEOUT
382
+ self.timeout = kwargs.get('timeout', PEXPECT_DEFAULT_TIMEOUT)
336
383
  self._pexpect_spawn: t.Optional[PortSpawn] = None
337
384
  # logger
338
385
  self._logger = self._get_logger()
@@ -408,6 +455,27 @@ class BasePort(PortInterface, t.Generic[T]):
408
455
  self._pexpect_spawn.log_file = new_log_file
409
456
  self._log_file = new_log_file
410
457
 
458
+ @property
459
+ def rx_log_callback(self) -> t.Optional[t.Callable[[str, bytes], None]]:
460
+ """Get Current dut log file."""
461
+ return t.cast(t.Optional[t.Callable[[str, bytes], None]], self._kwargs.get('rx_log_callback', None))
462
+
463
+ def set_rx_log_callback(self, new_callback: t.Optional[t.Callable[[str, bytes], None]]) -> None:
464
+ self._kwargs['rx_log_callback'] = new_callback
465
+ if self._pexpect_spawn:
466
+ self._pexpect_spawn._rx_log_callback = new_callback # pylint: disable=protected-access
467
+
468
+ @property
469
+ def monitors(self) -> t.List[DataMonitor]:
470
+ return t.cast(t.List[DataMonitor], self._kwargs.setdefault('monitors', []))
471
+
472
+ @monitors.setter
473
+ def monitors(self, new_monitors: t.List[DataMonitor]) -> None:
474
+ synced_monitors = list(new_monitors)
475
+ self._kwargs['monitors'] = synced_monitors
476
+ if self._pexpect_spawn:
477
+ self._pexpect_spawn._monitors = synced_monitors # pylint: disable=protected-access
478
+
411
479
  @property
412
480
  def spawn(self) -> t.Optional[PortSpawn]:
413
481
  """Allow the use of pexpect spawn enhancements, if pexpect process is available"""
@@ -419,7 +487,7 @@ class BasePort(PortInterface, t.Generic[T]):
419
487
  return
420
488
  self._init_log_file()
421
489
  self._pexpect_spawn = PortSpawn(
422
- self.raw_port, self.name, self.log_file, self.PEXPECT_DEFAULT_TIMEOUT, **self._kwargs
490
+ self.raw_port, self.name, self.log_file, PEXPECT_DEFAULT_TIMEOUT, **self._kwargs
423
491
  )
424
492
 
425
493
  def stop_redirect_thread(self) -> bool:
@@ -455,13 +523,13 @@ class BasePort(PortInterface, t.Generic[T]):
455
523
  raise NotImplementedError()
456
524
 
457
525
  @overload
458
- def expect(self, pattern: str, timeout: float = 30) -> None: ...
526
+ def expect(self, pattern: str, timeout: float = PEXPECT_DEFAULT_TIMEOUT) -> None: ...
459
527
  @overload
460
- def expect(self, pattern: bytes, timeout: float = 30) -> None: ...
528
+ def expect(self, pattern: bytes, timeout: float = PEXPECT_DEFAULT_TIMEOUT) -> None: ...
461
529
  @overload
462
- def expect(self, pattern: 're.Pattern[str]', timeout: float = 30) -> 're.Match[str]': ...
530
+ def expect(self, pattern: 're.Pattern[str]', timeout: float = PEXPECT_DEFAULT_TIMEOUT) -> 're.Match[str]': ...
463
531
  @overload
464
- def expect(self, pattern: 're.Pattern[bytes]', timeout: float = 30) -> 're.Match[bytes]': ...
532
+ def expect(self, pattern: 're.Pattern[bytes]', timeout: float = PEXPECT_DEFAULT_TIMEOUT) -> 're.Match[bytes]': ...
465
533
 
466
534
  @handle_expect_timeout
467
535
  def expect(self, pattern, timeout=PEXPECT_DEFAULT_TIMEOUT): # type: ignore
@@ -473,6 +541,12 @@ class BasePort(PortInterface, t.Generic[T]):
473
541
  If the pattern type is re.Pattern, this method will return a re.Match object if the pattern is matched.
474
542
  Can read all output data by pattern=re.compile('.+', re.DOTALL)
475
543
 
544
+ Note:
545
+ When matching very long data in a single read, pexpect may truncate the buffer
546
+ due to its ``maxread`` limit. If the expected pattern can
547
+ span a large chunk of output, increase ``maxread`` on the underlying pexpect
548
+ spawn accordingly.
549
+
476
550
  Args:
477
551
  pattern (t.Union[str, bytes, re.Pattern]): pattern to match
478
552
  timeout (int, optional): seconds of waiting for new data if match failed. Defaults to 30s.
@@ -518,18 +592,25 @@ class BasePort(PortInterface, t.Generic[T]):
518
592
  """
519
593
  buffer = b''
520
594
  if flush:
521
- # pexpect may return empty bytes if b'(.*)' is used
522
- try:
523
- match = self.expect(re.compile(b'(.+)', re.DOTALL), timeout=0)
524
- assert match
525
- buffer = match.group(0)
526
- except TimeoutError:
527
- pass
595
+ while True:
596
+ new_data = b''
597
+ # pexpect may return empty bytes if b'(.*)' is used
598
+ try:
599
+ match = self.expect(re.compile(b'(.+)', re.DOTALL), timeout=0)
600
+ assert match
601
+ new_data = match.group(0)
602
+ except TimeoutError:
603
+ pass
604
+ if not new_data:
605
+ break
606
+ buffer += new_data
528
607
  else:
529
- # flush spawn buffer
608
+ # update spawn buffer
530
609
  assert self._pexpect_spawn
531
610
  self._pexpect_spawn.expect_exact(pexpect.TIMEOUT, timeout=0)
532
611
  buffer = to_bytes(self._pexpect_spawn.buffer)
612
+ if hasattr(self._pexpect_spawn, 'data_cache'):
613
+ buffer += to_bytes(self._pexpect_spawn.data_cache)
533
614
  assert isinstance(buffer, bytes)
534
615
  return buffer
535
616
 
@@ -0,0 +1,36 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ import esptest.common.compat_typing as t
4
+
5
+ from ...common.data_monitor import DataMonitor
6
+
7
+ if TYPE_CHECKING:
8
+
9
+ class _SupportsMonitors(t.Protocol):
10
+ @property
11
+ def monitors(self) -> t.List[DataMonitor]: ...
12
+
13
+ @monitors.setter
14
+ def monitors(self, new_monitors: t.List[DataMonitor]) -> None: ...
15
+
16
+ else:
17
+ _SupportsMonitors = object
18
+
19
+
20
+ class DataMonitorMixin(_SupportsMonitors):
21
+ def add_monitor(self, monitor: DataMonitor) -> None:
22
+ new_monitors = list(self.monitors)
23
+ if monitor in new_monitors:
24
+ return
25
+ new_monitors.append(monitor)
26
+ self.monitors = new_monitors
27
+
28
+ def remove_monitor(self, monitor: DataMonitor) -> None:
29
+ new_monitors = list(self.monitors)
30
+ if monitor not in new_monitors:
31
+ return
32
+ new_monitors.remove(monitor)
33
+ self.monitors = new_monitors
34
+
35
+ def clear_monitors(self) -> None:
36
+ self.monitors = []
@@ -87,9 +87,9 @@ class SerialPortMixin(MixinBase):
87
87
  elif issubclass(original_type, SerialBase):
88
88
  raw_port.__class__ = serial_add_mixin(original_type)
89
89
 
90
- def __init__(self, raw_port: t.Any, name: str, log_file: str = '') -> None:
90
+ def __init__(self, raw_port: t.Any, name: str, log_file: str = '', **kwargs: t.Any) -> None:
91
91
  self._add_mixin_by_type(raw_port)
92
- super().__init__(raw_port, name, log_file)
92
+ super().__init__(raw_port, name, log_file, **kwargs)
93
93
  self._serial_config: t.Dict[str, t.Any] = {}
94
94
 
95
95
  def start_redirect_thread(self) -> None:
@@ -11,6 +11,7 @@ import psutil
11
11
  import esptest.common.compat_typing as t
12
12
 
13
13
  from ...common.shell import ensure_windows_env
14
+ from ...config.global_config import g
14
15
  from ...logger import get_logger
15
16
  from .base_port import BasePort, RawPort
16
17
 
@@ -270,7 +271,8 @@ class PexpectPort(BasePort[InvalidRaw]):
270
271
  self._init_log_file()
271
272
  env = os.environ.copy()
272
273
  env['PYTHONUNBUFFERED'] = 'true' # for python scripts, disable output buffering
273
- self._pexpect_spawn = pexpect.spawn(self._cmd, maxread=8192, echo=False, env=env) # type: ignore
274
+ maxread = self._kwargs.get('maxread', g.PORT_SPAWN_MAXREAD)
275
+ self._pexpect_spawn = pexpect.spawn(self._cmd, maxread=maxread, echo=False, env=env) # type: ignore
274
276
  self._pexpect_spawn.logfile = self.log_file_f # type: ignore
275
277
  # self._pexpect_spawn.delaybeforesend = 0.001
276
278