esp-test-utils 0.3.0__tar.gz → 0.3.2__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 (120) hide show
  1. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/.gitignore +1 -0
  2. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/.gitlab-ci.yml +71 -4
  3. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/CHANGELOG.md +15 -0
  4. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/PKG-INFO +1 -1
  5. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/docs/conf.py +3 -0
  6. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/PKG-INFO +1 -1
  7. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/SOURCES.txt +1 -0
  8. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/dut/dut_base.py +2 -2
  9. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/dut/wrapper.py +6 -1
  10. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/port/base_port.py +38 -25
  11. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/port/serial_port.py +60 -18
  12. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/port/shell_port.py +100 -11
  13. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/common/compat_typing.py +9 -3
  14. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/common/data_monitor.py +2 -2
  15. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/common/decorators.py +42 -9
  16. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/common/shell.py +18 -0
  17. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/config/env_config.py +4 -2
  18. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/db/runners.py +4 -1
  19. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/interface/port.py +6 -5
  20. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/iperf_utility/iperf_results.py +27 -24
  21. esp_test_utils-0.3.2/esptest/iperf_utility/line_chart.py +244 -0
  22. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/network/netif.py +35 -11
  23. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/scripts/downbin.py +2 -1
  24. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/tools/copy_bin.py +2 -2
  25. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/tools/download_bin.py +70 -14
  26. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/utility/parse_bin_path.py +75 -13
  27. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/pyproject.toml +1 -1
  28. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/adapter/test_Dut.py +62 -1
  29. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/adapter/test_shell_port.py +41 -11
  30. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/basic/test_network.py +39 -3
  31. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/esp_console/test_WifiCmd.py +56 -1
  32. esp_test_utils-0.3.2/tests/iperf_utility/test_chart.py +101 -0
  33. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/iperf_utility/test_iperf_results.py +8 -0
  34. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/test_common.py +7 -2
  35. esp_test_utils-0.3.2/tests/tools/test_download_bin.py +81 -0
  36. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/utility/test_parse_bin_path.py +31 -0
  37. esp_test_utils-0.3.0/esptest/iperf_utility/line_chart.py +0 -86
  38. esp_test_utils-0.3.0/tests/iperf_utility/test_chart.py +0 -46
  39. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/.github/.gitkeep +0 -0
  40. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/.github/workflows/pypi-publish.yml +0 -0
  41. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/.pre-commit-config.yaml +0 -0
  42. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/CONTRIBUTING.md +0 -0
  43. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/LICENSE +0 -0
  44. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/README.md +0 -0
  45. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/docs/Makefile +0 -0
  46. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/docs/index.rst +0 -0
  47. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/docs/make.bat +0 -0
  48. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/dependency_links.txt +0 -0
  49. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/entry_points.txt +0 -0
  50. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/requires.txt +0 -0
  51. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/top_level.txt +0 -0
  52. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/__init__.py +0 -0
  53. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/__main__.py +0 -0
  54. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/__init__.py +0 -0
  55. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/dut/__init__.py +0 -0
  56. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/dut/create_dut.py +0 -0
  57. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/dut/esp_dut.py +0 -0
  58. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/dut/esp_mixin.py +0 -0
  59. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/dut/esp_port.py +0 -0
  60. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/dut/mac_mixin.py +0 -0
  61. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/adapter/port/__init__.py +0 -0
  62. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/all.py +0 -0
  63. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/common/__init__.py +0 -0
  64. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/common/encoding.py +0 -0
  65. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/common/generator.py +0 -0
  66. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/common/timestamp.py +0 -0
  67. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/config/__init__.py +0 -0
  68. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/config/default_config.py +0 -0
  69. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/db/__init__.py +0 -0
  70. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/devices/__init__.py +0 -0
  71. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/devices/attenuator.py +0 -0
  72. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/devices/esp_serial.py +0 -0
  73. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/devices/serial_dut.py +0 -0
  74. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/devices/serial_tools.py +0 -0
  75. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/devices/switch.py +0 -0
  76. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/env/__init__.py +0 -0
  77. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/env/base_env.py +0 -0
  78. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/env/wifi_env.py +0 -0
  79. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/esp_console/__init__.py +0 -0
  80. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/esp_console/wifi_cmd.py +0 -0
  81. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/interface/__init__.py +0 -0
  82. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/interface/dut.py +0 -0
  83. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/iperf_utility/__init__.py +0 -0
  84. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/iperf_utility/iperf_test.py +0 -0
  85. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/iperf_utility/iperf_test.test.py +0 -0
  86. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/logger/__init__.py +0 -0
  87. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/logger/logger.py +0 -0
  88. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/network/__init__.py +0 -0
  89. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/network/mac.py +0 -0
  90. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/network/nic.py +0 -0
  91. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/scripts/list_ports.py +0 -0
  92. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/scripts/monitor.py +0 -0
  93. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/scripts/set_att.py +0 -0
  94. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/tools/http_download.py +0 -0
  95. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/tools/pip_check.py +0 -0
  96. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/tools/uart_monitor.py +0 -0
  97. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/esptest/utility/gen_esp32part.py +0 -0
  98. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/example/jap_test.py +0 -0
  99. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/example/restart_test.py +0 -0
  100. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/setup.cfg +0 -0
  101. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/__init__.py +0 -0
  102. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/basic/test_decorators.py +0 -0
  103. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/conftest.py +0 -0
  104. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/db/test_db_runners.py +0 -0
  105. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/devices/test_switch.py +0 -0
  106. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/esp_console/_files/wifi_cmd_connected_1.log +0 -0
  107. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/esp_console/_files/wifi_cmd_connected_2.log +0 -0
  108. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/esp_console/conftest.py +0 -0
  109. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/iperf_utility/_files/dut_iperf_rx1.log +0 -0
  110. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/iperf_utility/_files/dut_iperf_rx2.log +0 -0
  111. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/iperf_utility/_files/pc_iperf_rx.log +0 -0
  112. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/iperf_utility/_files/pc_iperf_rx2.log +0 -0
  113. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/iperf_utility/test_iperf_util.py +0 -0
  114. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/test_EnvConfig.py +0 -0
  115. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/test_import.py +0 -0
  116. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/tools/test_download_file.py +0 -0
  117. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/tools/test_pip_check.py +0 -0
  118. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/utility/_files/test-bin.zip +0 -0
  119. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tests/utility/_files/test-get-baud/ESP32AT-V4.1.1.0/sdkconfig +0 -0
  120. {esp_test_utils-0.3.0 → esp_test_utils-0.3.2}/tools/ci/check_dev_version.py +0 -0
@@ -8,6 +8,7 @@ __pycache__/
8
8
  htmlcov/
9
9
  /.coverage
10
10
  /.coverage.*
11
+ /coverage.xml
11
12
  gl-codequality.json
12
13
 
13
14
  /build/
@@ -30,20 +30,87 @@ pre-commit-check:
30
30
  # ------------------------------------------------------------------------------------------------------
31
31
  # Pytest
32
32
  # ------------------------------------------------------------------------------------------------------
33
- pytest-check:
33
+ pytest-linux:
34
34
  stage: test
35
35
  needs: []
36
+ image: ${IMG}
36
37
  before_script:
37
38
  - pip install -e '.[test]'
38
39
  script:
39
- - pytest --junitxml=reports/junit.xml --cov=esptest --cov-report=term --cov-report xml:reports/coverage.xml
40
- coverage: '/TOTAL.*\s+(\d+)\%/'
40
+ # - pytest --junitxml=reports/junit.xml --cov=esptest --cov-report=term --cov-report xml:reports/coverage.xml
41
+ - if [ "$IMG" = "python:3.11-bookworm" ]; then pip install pyecharts; fi # test with pyecharts installed
42
+ - export COVERAGE_FILE=.coverage.${CI_JOB_ID}
43
+ - pytest --junitxml=reports/junit.xml --cov=esptest --cov-report=term
44
+ # coverage: '/TOTAL.*\s+(\d+)\%/'
41
45
  artifacts:
42
46
  paths:
43
- - reports/
47
+ # - reports/
44
48
  - '.coverage*'
45
49
  reports:
46
50
  junit: reports/junit.xml
51
+ expire_in: 2 days
52
+ parallel:
53
+ matrix:
54
+ - IMG: "python:3.7-bullseye"
55
+ - IMG: "python:3.11-bookworm"
56
+ - IMG: "python:3.14-trixie"
57
+ tags:
58
+ - host_test
59
+
60
+ pytest-win32:
61
+ stage: test
62
+ needs:
63
+ - job: pytest-linux
64
+ artifacts: false
65
+ before_script:
66
+ - pymanager exec -V:$env:PYTHON_VER -m venv venv
67
+ - 'venv\Scripts\Activate.ps1'
68
+ - python -V
69
+ - pip install '.[test]'
70
+ script:
71
+ # - pytest --junitxml=reports/junit.xml --cov=esptest --cov-report=term --cov-report xml:reports/coverage.xml
72
+ - $env:COVERAGE_FILE = ".coverage.$env:CI_JOB_ID"
73
+ - pytest --junitxml=reports/junit.xml --cov=esptest --cov-report=term
74
+ # coverage: '/TOTAL.*\s+(\d+)\%/'
75
+ artifacts:
76
+ paths:
77
+ # - reports/
78
+ - '.coverage*'
79
+ reports:
80
+ junit: reports/junit.xml
81
+ expire_in: 2 days
82
+ parallel:
83
+ matrix:
84
+ - PYTHON_VER: "3.7"
85
+ - PYTHON_VER: "3.11"
86
+ - PYTHON_VER: "3.14"
87
+ tags:
88
+ - windows
89
+ - pymanager
90
+
91
+ pytest-coverage:
92
+ stage: test
93
+ needs:
94
+ - job: pytest-linux
95
+ artifacts: true
96
+ - job: pytest-win32
97
+ artifacts: false # Do not merge coverage from win32
98
+ before_script:
99
+ - pip install coverage
100
+ script:
101
+ - coverage combine .coverage*
102
+ - coverage report
103
+ - coverage xml
104
+ - coverage html
105
+ coverage: '/TOTAL.*\s+(\d+)\%/'
106
+ artifacts:
107
+ paths:
108
+ - htmlcov/
109
+ - '.coverage'
110
+ - coverage.xml
111
+ # reports:
112
+ # coverage: 'coverage.xml'
113
+ expire_in: 1 week
47
114
  tags:
48
115
  - host_test
49
116
 
@@ -1,3 +1,18 @@
1
+ ## v0.3.2 (2026-03-19)
2
+
3
+
4
+ - feat: add downbin with configs
5
+ - feat: add secure boot match check
6
+ - fix: parse partition for read-only dir
7
+ - change: add debug logs for downbin
8
+
9
+ ## v0.3.1 (2026-01-16)
10
+
11
+
12
+ - feat: add diff values for pyecharts
13
+ - feat: add secure boot check
14
+ - fix: pass pytest on windows
15
+
1
16
  ## v0.3.0 (2025-12-10)
2
17
 
3
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: esp-test-utils
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: ESP Test Utils
5
5
  Author-email: Chen Yudong <chenyudong@espressif.com>
6
6
  License: Apache License
@@ -20,6 +20,9 @@ extensions = [
20
20
  'sphinx.ext.napoleon',
21
21
  ]
22
22
 
23
+ # Use type hints from signature in parameter descriptions (avoids duplicating complex types in docstrings)
24
+ autodoc_typehints = 'description'
25
+
23
26
  templates_path = ['_templates']
24
27
  exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
25
28
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: esp-test-utils
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: ESP Test Utils
5
5
  Author-email: Chen Yudong <chenyudong@espressif.com>
6
6
  License: Apache License
@@ -107,6 +107,7 @@ tests/iperf_utility/_files/dut_iperf_rx1.log
107
107
  tests/iperf_utility/_files/dut_iperf_rx2.log
108
108
  tests/iperf_utility/_files/pc_iperf_rx.log
109
109
  tests/iperf_utility/_files/pc_iperf_rx2.log
110
+ tests/tools/test_download_bin.py
110
111
  tests/tools/test_download_file.py
111
112
  tests/tools/test_pip_check.py
112
113
  tests/utility/test_parse_bin_path.py
@@ -258,9 +258,9 @@ class DutBase(VariablesMixin, DutInterface): # pylint: disable=too-many-public-
258
258
  @overload
259
259
  def expect(self, pattern: bytes, timeout: float = 30) -> None: ...
260
260
  @overload
261
- def expect(self, pattern: re.Pattern[str], timeout: float = 30) -> re.Match[str]: ...
261
+ def expect(self, pattern: 're.Pattern[str]', timeout: float = 30) -> 're.Match[str]': ...
262
262
  @overload
263
- def expect(self, pattern: re.Pattern[bytes], timeout: float = 30) -> re.Match[bytes]: ...
263
+ def expect(self, pattern: 're.Pattern[bytes]', timeout: float = 30) -> 're.Match[bytes]': ...
264
264
 
265
265
  def expect(self, pattern, timeout=30): # type: ignore
266
266
  if self._base_port_proxy:
@@ -6,7 +6,7 @@ from ...common.generator import get_next_index
6
6
  from ...interface.dut import DutInterface
7
7
  from ...logger import get_logger
8
8
  from ..port.base_port import RawPort
9
- from ..port.serial_port import SerialExt, SerialPort
9
+ from ..port.serial_port import SerialExt, SerialPort, serial_add_mixin
10
10
  from .create_dut import create_dut
11
11
  from .dut_base import DutBase, DutConfig
12
12
  from .esp_dut import EspDut
@@ -62,6 +62,11 @@ def dut_wrapper(dut, name='', log_file='', wrap_cls=None): # type: ignore
62
62
  dut.__class__ = SerialExt
63
63
  dut_config = DutConfig(opened_port=dut, name=_name, log_file=log_file)
64
64
  wrap_dut = wrap_cls(dut_config=dut_config)
65
+ elif isinstance(dut, serial.SerialBase):
66
+ _name = name or dut.port.split('/')[-1]
67
+ dut.__class__ = serial_add_mixin(dut.__class__)
68
+ dut_config = DutConfig(opened_port=dut, name=_name, log_file=log_file)
69
+ wrap_dut = wrap_cls(dut_config=dut_config)
65
70
  elif isinstance(dut, RawPort):
66
71
  _name = name
67
72
  if not _name:
@@ -5,13 +5,12 @@ import logging
5
5
  import os
6
6
  import queue
7
7
  import re
8
+ import sys
8
9
  import threading
9
10
  import time
10
11
  from dataclasses import dataclass
11
12
  from typing import overload
12
13
 
13
- import pexpect.spawnbase
14
-
15
14
  import esptest.common.compat_typing as t
16
15
 
17
16
  from ...common import timestamp_str, to_bytes, to_str
@@ -19,6 +18,18 @@ from ...common.decorators import deprecated
19
18
  from ...interface.port import PortInterface
20
19
  from ...logger import get_logger
21
20
 
21
+ if sys.platform == 'win32':
22
+ import pexpect
23
+ from pexpect.exceptions import ExceptionPexpect
24
+ from pexpect.spawnbase import SpawnBase
25
+ # from wexpect import SpawnPipe as SpawnBase
26
+ # from wexpect import ExceptionPexpect
27
+ else:
28
+ import pexpect
29
+ from pexpect.exceptions import ExceptionPexpect
30
+ from pexpect.spawnbase import SpawnBase
31
+
32
+
22
33
  logger = get_logger('port')
23
34
  NEVER_MATCHED_MAGIC_STRING = 'o6K,Q.(w+~yr~N9R'
24
35
 
@@ -81,7 +92,7 @@ class SpawnConfig:
81
92
  # TODO: monitors
82
93
 
83
94
 
84
- class PortSpawn(pexpect.spawnbase.SpawnBase, t.Generic[T]):
95
+ class PortSpawn(SpawnBase, t.Generic[T]):
85
96
  """Create a new class for pexpect with port read()/write() method.
86
97
 
87
98
  There's some reason that we can not use pyserial with pexpect.fdpexpect directly:
@@ -268,6 +279,27 @@ class PortSpawn(pexpect.spawnbase.SpawnBase, t.Generic[T]):
268
279
  self._line_cache = b''
269
280
 
270
281
 
282
+ def handle_expect_timeout(func: t.Callable) -> t.Callable:
283
+ """Raise same type exception ExpectTimeout for ports from different frameworks"""
284
+
285
+ @functools.wraps(func)
286
+ def wrap(obj: 'BasePort', *args, **kwargs): # type: ignore
287
+ try:
288
+ result = func(obj, *args, **kwargs)
289
+ except obj.expect_timeout_exceptions as e:
290
+ data_in_buffer = ''
291
+ try:
292
+ if obj._pexpect_spawn: # pylint: disable=protected-access
293
+ data_in_buffer = obj._pexpect_spawn.before # pylint: disable=protected-access
294
+ except AttributeError:
295
+ pass # ignore
296
+ obj.logger.debug(f'ExpectTimeout: {str(e)}, data_in_buffer={repr(data_in_buffer)}')
297
+ raise ExpectTimeout(str(e), data_in_buffer=data_in_buffer) from e
298
+ return result
299
+
300
+ return wrap
301
+
302
+
271
303
  class BasePort(PortInterface, t.Generic[T]):
272
304
  """A class to simply port methods for all devices / shell / sockets to similar usage
273
305
 
@@ -278,7 +310,7 @@ class BasePort(PortInterface, t.Generic[T]):
278
310
 
279
311
  EXPECT_TIMEOUT_EXCEPTIONS: t.Tuple[t.Type[Exception], ...] = (
280
312
  TimeoutError,
281
- pexpect.exceptions.ExceptionPexpect,
313
+ ExceptionPexpect,
282
314
  )
283
315
  INIT_START_REDIRECT_THREAD: bool = True
284
316
  PEXPECT_DEFAULT_TIMEOUT: float = 30
@@ -406,25 +438,6 @@ class BasePort(PortInterface, t.Generic[T]):
406
438
  if stopped:
407
439
  self.start_redirect_thread()
408
440
 
409
- @staticmethod
410
- def handle_expect_timeout(func: t.Callable) -> t.Callable:
411
- """Raise same type exception ExpectTimeout for ports from different frameworks"""
412
-
413
- @functools.wraps(func)
414
- def wrap(self, *args, **kwargs): # type: ignore
415
- try:
416
- result = func(self, *args, **kwargs)
417
- except self.expect_timeout_exceptions as 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
424
- return result
425
-
426
- return wrap
427
-
428
441
  def write(self, data: t.AnyStr) -> None:
429
442
  if self._pexpect_spawn:
430
443
  return self._pexpect_spawn.write(data)
@@ -446,9 +459,9 @@ class BasePort(PortInterface, t.Generic[T]):
446
459
  @overload
447
460
  def expect(self, pattern: bytes, timeout: float = 30) -> None: ...
448
461
  @overload
449
- def expect(self, pattern: re.Pattern[str], timeout: float = 30) -> re.Match[str]: ...
462
+ def expect(self, pattern: 're.Pattern[str]', timeout: float = 30) -> 're.Match[str]': ...
450
463
  @overload
451
- def expect(self, pattern: re.Pattern[bytes], timeout: float = 30) -> re.Match[bytes]: ...
464
+ def expect(self, pattern: 're.Pattern[bytes]', timeout: float = 30) -> 're.Match[bytes]': ...
452
465
 
453
466
  @handle_expect_timeout
454
467
  def expect(self, pattern, timeout=PEXPECT_DEFAULT_TIMEOUT): # type: ignore
@@ -1,28 +1,43 @@
1
1
  import time
2
- from typing import TYPE_CHECKING, Any, AnyStr, Dict, Optional, TypeAlias
2
+ from typing import TYPE_CHECKING
3
3
 
4
4
  import serial
5
- from serial import Serial
5
+ from serial import Serial, SerialBase
6
+
7
+ import esptest.common.compat_typing as t
6
8
 
7
9
  from ...common import to_bytes
8
10
  from ...logger import get_logger
9
11
  from .base_port import BasePort
10
12
 
11
13
  if TYPE_CHECKING:
12
- MixinBase: TypeAlias = 'BasePort'
14
+ MixinBase: t.TypeAlias = 'BasePort'
13
15
  else:
14
16
  MixinBase = object
15
17
 
16
18
  logger = get_logger('ser_port')
17
19
 
18
20
 
19
- class SerialExt(Serial):
20
- """Add RawPort methods to serial.Serial"""
21
+ class SerialBaseProtocol(t.Protocol):
22
+ @property
23
+ def port(self) -> t.Optional[str]: ...
24
+
25
+ @property
26
+ def baudrate(self) -> t.Optional[int]: ...
27
+
28
+ @property
29
+ def timeout(self) -> t.Optional[float]: ...
30
+
31
+ def read(self, size: int = 1) -> bytes: ...
32
+
33
+ def write(self, data: t.AnyStr) -> int: ...
21
34
 
35
+
36
+ class SerMixin(SerialBaseProtocol):
22
37
  @property
23
38
  def read_timeout(self) -> float:
24
39
  # For PortSpawn
25
- return super().timeout or 0.001 # type: ignore
40
+ return self.timeout or 0.001 # type: ignore
26
41
 
27
42
  def read_bytes(self, timeout: float = 0.001) -> bytes:
28
43
  # For PortSpawn
@@ -30,25 +45,52 @@ class SerialExt(Serial):
30
45
  assert self.timeout >= 0.001
31
46
  if timeout > self.timeout:
32
47
  time.sleep(timeout - self.timeout)
33
- return super().read(1024) # type: ignore
48
+ return self.read(1024) # type: ignore
34
49
 
35
- def write_bytes(self, data: AnyStr) -> None:
50
+ def write_bytes(self, data: t.AnyStr) -> int:
36
51
  # For PortSpawn
37
- super().write(to_bytes(data))
52
+ self.write(to_bytes(data))
53
+ return len(to_bytes(data))
38
54
 
39
55
  def __str__(self) -> str:
40
- """SerialPort<device=xxx,baudrate=xxx,timeout=xxx>"""
41
- return f'SerialPort<device={self.port},baudrate={self.baudrate},timeout={self.timeout}>'
56
+ """SerialExt<device=xxx,baudrate=xxx,timeout=xxx>"""
57
+ return f'SerialExt<device={self.port},baudrate={self.baudrate},timeout={self.timeout}>'
58
+
59
+
60
+ class SerialExt(Serial, SerMixin):
61
+ """Add RawPort methods to serial.Serial"""
62
+
63
+
64
+ def serial_add_mixin(cls: t.Type[t.Any]) -> t.Type[t.Any]:
65
+ """动态为类添加 SerMixin"""
66
+ # 创建一个新的类,继承自原始类和 SerMixin
67
+ # 基类顺序与 SerialExt(Serial, SerMixin) 保持一致
68
+ return type(f'{cls.__name__}Ext', (cls, SerMixin), {})
42
69
 
43
70
 
44
71
  class SerialPortMixin(MixinBase):
45
72
  """Add RawPort methods to serial.Serial"""
46
73
 
47
- def __init__(self, raw_port: Any, name: str, log_file: str = '') -> None:
48
- if isinstance(raw_port, Serial):
74
+ @staticmethod
75
+ def _add_mixin_by_type(raw_port: t.Any) -> None:
76
+ """根据原始类型添加对应的 mixin"""
77
+ if raw_port is None:
78
+ return
79
+ original_type = type(raw_port)
80
+ # If the original type already includes SerMixin, do nothing.
81
+ # This prevents repeatedly nesting mixin classes when the serial
82
+ # object is reassigned and _add_mixin_by_type is called multiple times.
83
+ if issubclass(original_type, SerMixin):
84
+ return
85
+ if issubclass(original_type, Serial):
49
86
  raw_port.__class__ = SerialExt
87
+ elif issubclass(original_type, SerialBase):
88
+ raw_port.__class__ = serial_add_mixin(original_type)
89
+
90
+ def __init__(self, raw_port: t.Any, name: str, log_file: str = '') -> None:
91
+ self._add_mixin_by_type(raw_port)
50
92
  super().__init__(raw_port, name, log_file)
51
- self._serial_config: Dict[str, Any] = {}
93
+ self._serial_config: t.Dict[str, t.Any] = {}
52
94
 
53
95
  def start_redirect_thread(self) -> None:
54
96
  if not self.serial:
@@ -72,12 +114,12 @@ class SerialPortMixin(MixinBase):
72
114
  super().start_redirect_thread()
73
115
 
74
116
  @property
75
- def serial(self) -> Optional[SerialExt]:
117
+ def serial(self) -> t.Optional[SerialExt]:
76
118
  """Get Current serial instance."""
77
119
  return self._raw_port # type: ignore
78
120
 
79
121
  @serial.setter
80
- def serial(self, serial_instance: Optional[Serial]) -> None:
122
+ def serial(self, serial_instance: t.Optional[Serial]) -> None:
81
123
  """Set serial instance, will close and clean up the old serial resources"""
82
124
  if self._raw_port:
83
125
  # Close pexpect proc
@@ -87,7 +129,7 @@ class SerialPortMixin(MixinBase):
87
129
  # self._port.close()
88
130
  if serial_instance:
89
131
  self._raw_port = serial_instance
90
- self._raw_port.__class__ = SerialExt
132
+ self._add_mixin_by_type(serial_instance)
91
133
  self.start_redirect_thread()
92
134
 
93
135
  def close(self) -> None:
@@ -107,7 +149,7 @@ class SerialPort(SerialPortMixin, BasePort):
107
149
  This class using serial with pexpect.
108
150
  """
109
151
 
110
- def __init__(self, dut: Serial, name: str, log_file: str = '', **kwargs: Any) -> None:
152
+ def __init__(self, dut: Serial, name: str, log_file: str = '', **kwargs: t.Any) -> None:
111
153
  if not dut:
112
154
  self.INIT_START_REDIRECT_THREAD = False # pylint: disable=invalid-name
113
155
  super().__init__(dut, name, log_file, **kwargs)
@@ -1,47 +1,101 @@
1
+ import io
1
2
  import os
3
+ import queue
2
4
  import subprocess
5
+ import sys
6
+ import threading
3
7
  import time
4
8
 
5
- import pexpect
6
9
  import psutil
7
10
 
8
11
  import esptest.common.compat_typing as t
9
12
 
13
+ from ...common.shell import ensure_windows_env
10
14
  from ...logger import get_logger
11
15
  from .base_port import BasePort, RawPort
12
16
 
13
17
  logger = get_logger('shell_port')
14
18
 
15
19
 
20
+ if sys.platform == 'win32':
21
+ # windows does not support pexpect.spawn
22
+ # import wexpect as pexpect # pexpect.spawn
23
+ import pexpect # pexpect.spawn
24
+
25
+ DEFAULT_SHELL = 'cmd.exe'
26
+ ensure_windows_env()
27
+ else:
28
+ import pexpect
29
+
30
+ DEFAULT_SHELL = '/bin/bash'
31
+
32
+
16
33
  class ShellRaw(RawPort):
17
34
  """A subprocess Raw Port class that supports shell read, write
18
35
 
19
36
  is a subclass of RawPort
20
37
  """
21
38
 
22
- def __init__(self, cmd: str = '/bin/bash', env: t.Optional[t.Dict[str, str]] = None) -> None:
39
+ def __init__(self, cmd: t.Union[str, t.List[str]] = '', env: t.Optional[t.Dict[str, str]] = None) -> None:
40
+ ensure_windows_env()
23
41
  self.env = env or os.environ.copy()
24
42
  self.env['PYTHONUNBUFFERED'] = 'true' # for python scripts, disable output buffering
25
- self.cmd = cmd
43
+ self.cmd = cmd or DEFAULT_SHELL
26
44
  self.proc: t.Optional[subprocess.Popen] = None
27
45
  self.read_timeout = 0.002 # default read_timeout
46
+ # For Windows: use a thread and queue for non-blocking reads
47
+ self._read_queue: t.Optional[queue.Queue] = None
48
+ self._read_thread: t.Optional[threading.Thread] = None
49
+ self._read_thread_stop = threading.Event()
28
50
  self.open()
29
51
 
30
52
  def open(self) -> None:
31
53
  if not self.proc:
32
54
  self.proc = subprocess.Popen( # pylint: disable=consider-using-with
33
55
  self.cmd,
34
- shell=True,
56
+ shell=bool(not isinstance(self.cmd, list)),
35
57
  env=self.env,
36
58
  stdin=subprocess.PIPE,
37
59
  stdout=subprocess.PIPE,
38
60
  stderr=subprocess.STDOUT,
39
61
  )
40
62
  # Set stdout to non-blocking
41
- os.set_blocking(self.proc.stdout.fileno(), False) # type: ignore
63
+ if sys.platform != 'win32':
64
+ os.set_blocking(self.proc.stdout.fileno(), False) # type: ignore
65
+ else:
66
+ # Windows: subprocess pipes are blocking by default and cannot be set to non-blocking
67
+ # Use a background thread to read from the pipe
68
+ self._read_queue = queue.Queue()
69
+ self._read_thread_stop.clear()
70
+ self._read_thread = threading.Thread(target=self._read_stdout_thread, daemon=True)
71
+ self._read_thread.start()
72
+
73
+ def _read_stdout_thread(self) -> None:
74
+ """Background thread to read from stdout on Windows (where pipes are blocking)."""
75
+ if not self.proc or not self._read_queue:
76
+ return
77
+ try:
78
+ while not self._read_thread_stop.is_set():
79
+ try:
80
+ # Read line by line to avoid blocking too long on Windows
81
+ data = self.proc.stdout.readline() # type: ignore
82
+ if data:
83
+ self._read_queue.put(data)
84
+ elif self.proc.poll() is not None:
85
+ # Process has ended
86
+ break
87
+ except (OSError, ValueError):
88
+ break
89
+ except Exception as e: # pylint: disable=broad-exception-caught
90
+ logger.error(f'Error in read_stdout_thread {type(e)}: {str(e)}')
42
91
 
43
92
  def close(self) -> None:
44
93
  """Close subprocess."""
94
+ # Stop the read thread on Windows
95
+ if self._read_thread_stop:
96
+ self._read_thread_stop.set()
97
+ if self._read_thread:
98
+ self._read_thread.join(timeout=0.1)
45
99
  if self.proc:
46
100
  if self.proc.pid:
47
101
  try:
@@ -84,10 +138,34 @@ class ShellRaw(RawPort):
84
138
 
85
139
  def read_bytes_nonblocking(self, size: int = -1) -> bytes:
86
140
  """non-blocking read bytes"""
87
- if self.proc:
141
+ if not self.proc:
142
+ return b''
143
+ if sys.platform != 'win32':
88
144
  self.proc.stdout.flush() # type: ignore
89
145
  return self.proc.stdout.read(size) # type: ignore
90
- return b''
146
+ # Windows: read from the queue
147
+ try:
148
+ if not self._read_queue:
149
+ return b''
150
+ # On Windows, use the queue from the background thread
151
+ data = b''
152
+ while True:
153
+ try:
154
+ self.proc.stdout.flush() # type: ignore
155
+ chunk = self._read_queue.get_nowait()
156
+ data += chunk
157
+ # If we have enough data and size is specified, stop reading
158
+ if size > 0 and len(data) >= size: # pylint: disable=chained-comparison
159
+ break
160
+ except queue.Empty:
161
+ break
162
+ # Return the requested amount or all available data
163
+ if size > 0:
164
+ return data[:size]
165
+ return data
166
+ except (OSError, ValueError) as e:
167
+ logger.error(f'Error in read_bytes_nonblocking {type(e)}: {str(e)}')
168
+ raise
91
169
 
92
170
 
93
171
  class ShellPort(BasePort[ShellRaw]):
@@ -96,7 +174,7 @@ class ShellPort(BasePort[ShellRaw]):
96
174
  def __init__(
97
175
  self,
98
176
  cmd: str = '/bin/bash',
99
- env: t.Optional[dict[str, str]] = None,
177
+ env: t.Optional[t.Dict[str, str]] = None,
100
178
  name: str = '',
101
179
  log_file: str = '',
102
180
  **kwargs: t.Any,
@@ -130,10 +208,17 @@ class PexpectPort(BasePort[InvalidRaw]):
130
208
  log_file: str = '',
131
209
  **kwargs: t.Any,
132
210
  ) -> None:
211
+ if sys.platform == 'win32':
212
+ raise NotImplementedError('PexpectPort is not supported on Windows now.')
133
213
  self._cmd = cmd
134
214
  raw_port = InvalidRaw()
135
215
  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
216
+ self.log_file_f: t.Optional[io.BufferedWriter] = None
217
+ if log_file:
218
+ os.makedirs(os.path.dirname(log_file) or '.', exist_ok=True)
219
+ self.log_file_f = open(log_file, 'wb') # pylint: disable=consider-using-with
220
+ else:
221
+ self.log_file_f = None
137
222
  super().__init__(raw_port, name, log_file, **kwargs)
138
223
 
139
224
  @property
@@ -152,13 +237,17 @@ class PexpectPort(BasePort[InvalidRaw]):
152
237
  if self._pexpect_spawn:
153
238
  self._pexpect_spawn.logfile = None # type: ignore
154
239
  self.log_file_f.close()
240
+ if new_log_file:
241
+ os.makedirs(os.path.dirname(new_log_file) or '.', exist_ok=True)
242
+ self.log_file_f = open(new_log_file, 'wb') # pylint: disable=consider-using-with
243
+ else:
244
+ self.log_file_f = None
155
245
  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
246
  self._pexpect_spawn.logfile = self.log_file_f # type: ignore
158
247
  self._log_file = new_log_file
159
248
 
160
249
  @property
161
- def spawn(self) -> t.Optional[pexpect.spawn]: # type: ignore
250
+ def spawn(self) -> 't.Optional[pexpect.spawn]': # type: ignore
162
251
  """Allow the use of pexpect spawn enhancements, if pexpect process is available"""
163
252
  return self._pexpect_spawn
164
253
 
@@ -1,6 +1,6 @@
1
1
  # pylint: disable=unused-import
2
- # flake8: noqa: F401
3
- # ruff: noqa: F401
2
+ # flake8: noqa: F401,F404
3
+ # ruff: noqa: F401,F404
4
4
  import sys
5
5
  from typing import (
6
6
  IO,
@@ -15,7 +15,7 @@ from typing import (
15
15
  Iterator,
16
16
  List,
17
17
  Optional,
18
- Protocol,
18
+ Sequence,
19
19
  Set,
20
20
  Tuple,
21
21
  Type,
@@ -25,6 +25,12 @@ from typing import (
25
25
  overload,
26
26
  )
27
27
 
28
+ if sys.version_info >= (3, 8):
29
+ from typing import Protocol
30
+ else:
31
+ from typing_extensions import Protocol
32
+
33
+
28
34
  if sys.version_info >= (3, 9):
29
35
  from contextlib import AbstractContextManager as ContextManager
30
36
  else: