esp-test-utils 0.3.1__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.
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/CHANGELOG.md +8 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/PKG-INFO +1 -1
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/docs/conf.py +3 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/PKG-INFO +1 -1
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/SOURCES.txt +1 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/common/decorators.py +42 -9
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/scripts/downbin.py +2 -1
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/tools/download_bin.py +62 -11
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/utility/parse_bin_path.py +51 -9
- esp_test_utils-0.3.2/tests/tools/test_download_bin.py +81 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/utility/test_parse_bin_path.py +31 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/.github/.gitkeep +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/.github/workflows/pypi-publish.yml +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/.gitignore +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/.gitlab-ci.yml +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/.pre-commit-config.yaml +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/CONTRIBUTING.md +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/LICENSE +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/README.md +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/docs/Makefile +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/docs/index.rst +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/docs/make.bat +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/dependency_links.txt +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/entry_points.txt +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/requires.txt +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esp_test_utils.egg-info/top_level.txt +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/__init__.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/__main__.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/adapter/__init__.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/adapter/dut/__init__.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/adapter/dut/create_dut.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/adapter/dut/dut_base.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/adapter/dut/esp_dut.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/adapter/dut/esp_mixin.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/adapter/dut/esp_port.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/adapter/dut/mac_mixin.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/adapter/dut/wrapper.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/adapter/port/__init__.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/adapter/port/base_port.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/adapter/port/serial_port.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/adapter/port/shell_port.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/all.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/common/__init__.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/common/compat_typing.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/common/data_monitor.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/common/encoding.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/common/generator.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/common/shell.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/common/timestamp.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/config/__init__.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/config/default_config.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/config/env_config.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/db/__init__.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/db/runners.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/devices/__init__.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/devices/attenuator.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/devices/esp_serial.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/devices/serial_dut.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/devices/serial_tools.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/devices/switch.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/env/__init__.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/env/base_env.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/env/wifi_env.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/esp_console/__init__.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/esp_console/wifi_cmd.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/interface/__init__.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/interface/dut.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/interface/port.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/iperf_utility/__init__.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/iperf_utility/iperf_results.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/iperf_utility/iperf_test.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/iperf_utility/iperf_test.test.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/iperf_utility/line_chart.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/logger/__init__.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/logger/logger.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/network/__init__.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/network/mac.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/network/netif.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/network/nic.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/scripts/list_ports.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/scripts/monitor.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/scripts/set_att.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/tools/copy_bin.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/tools/http_download.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/tools/pip_check.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/tools/uart_monitor.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/esptest/utility/gen_esp32part.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/example/jap_test.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/example/restart_test.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/pyproject.toml +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/setup.cfg +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/__init__.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/adapter/test_Dut.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/adapter/test_shell_port.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/basic/test_decorators.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/basic/test_network.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/conftest.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/db/test_db_runners.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/devices/test_switch.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/esp_console/_files/wifi_cmd_connected_1.log +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/esp_console/_files/wifi_cmd_connected_2.log +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/esp_console/conftest.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/esp_console/test_WifiCmd.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/iperf_utility/_files/dut_iperf_rx1.log +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/iperf_utility/_files/dut_iperf_rx2.log +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/iperf_utility/_files/pc_iperf_rx.log +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/iperf_utility/_files/pc_iperf_rx2.log +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/iperf_utility/test_chart.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/iperf_utility/test_iperf_results.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/iperf_utility/test_iperf_util.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/test_EnvConfig.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/test_common.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/test_import.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/tools/test_download_file.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/tools/test_pip_check.py +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/utility/_files/test-bin.zip +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/utility/_files/test-get-baud/ESP32AT-V4.1.1.0/sdkconfig +0 -0
- {esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tools/ci/check_dev_version.py +0 -0
|
@@ -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
|
|
|
@@ -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
|
|
@@ -16,6 +16,18 @@ GenericFunc = t.TypeVar('GenericFunc', bound=t.Callable[..., t.Any])
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def enhance_import_error_message(message: str) -> t.Callable[[GenericFunc], GenericFunc]:
|
|
19
|
+
"""Decorator that enriches ImportError with function name and custom message.
|
|
20
|
+
|
|
21
|
+
When the decorated function raises an ImportError, the exception message
|
|
22
|
+
is appended with `` from {func.__name__}: {message}`` to aid fixing.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
message (str): Extra hint to append to the ImportError message.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
t.Callable[[GenericFunc], GenericFunc]: A decorator for the target function.
|
|
29
|
+
"""
|
|
30
|
+
|
|
19
31
|
def decorator(func: GenericFunc) -> GenericFunc:
|
|
20
32
|
@wraps(func)
|
|
21
33
|
def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
@@ -42,17 +54,26 @@ def retry(
|
|
|
42
54
|
) -> t.Callable[[GenericFunc], GenericFunc]:
|
|
43
55
|
"""Retry decorator
|
|
44
56
|
|
|
45
|
-
|
|
57
|
+
The decorated function is called at most ``max_retry`` times. A retry happens when:
|
|
58
|
+
- The return value fails the ``on_result`` check (if configured), or
|
|
59
|
+
- An exception matching ``on_exception`` is raised (if configured).
|
|
46
60
|
|
|
61
|
+
**on_result** controls retry based on return value. It can be:
|
|
62
|
+
- **list**: Retry when the return value is **not** in the list; stop and return when it is in the list.
|
|
63
|
+
- **callable**: Retry when the callable returns True (result unacceptable); stop and return when it returns False.
|
|
64
|
+
Default is a callable that always returns False, so no retry based on result.
|
|
65
|
+
|
|
66
|
+
**on_exception** limits which exceptions trigger a retry. Only exceptions whose type is in this tuple
|
|
67
|
+
are caught and cause a retry; others are re-raised. Default uses an internal sentinel so no retry on exception.
|
|
47
68
|
|
|
48
69
|
Args:
|
|
49
|
-
max_retry
|
|
50
|
-
on_result
|
|
51
|
-
on_exception
|
|
52
|
-
delay
|
|
70
|
+
max_retry: Maximum number of total calls. Defaults to 3.
|
|
71
|
+
on_result: Retry based on return value, see description above. Default: no retry on result.
|
|
72
|
+
on_exception: Retry when one of these exceptions is raised. Default: no exception handled.
|
|
73
|
+
delay: Delay before next retry. Defaults to 0.
|
|
53
74
|
|
|
54
75
|
Returns:
|
|
55
|
-
t.Callable[[GenericFunc], GenericFunc]: decorator
|
|
76
|
+
t.Callable[[GenericFunc], GenericFunc]: A decorator for the target function.
|
|
56
77
|
"""
|
|
57
78
|
|
|
58
79
|
def decorator(func: GenericFunc) -> GenericFunc:
|
|
@@ -98,12 +119,13 @@ def deprecated(reason: str = '') -> t.Callable[[GenericFunc], GenericFunc]:
|
|
|
98
119
|
|
|
99
120
|
|
|
100
121
|
def suppress_stdout() -> t.Callable[[GenericFunc], GenericFunc]:
|
|
101
|
-
"""
|
|
122
|
+
"""Redirect stdout and stderr to discard output during the decorated function's execution."""
|
|
102
123
|
|
|
103
124
|
def decorator(func: GenericFunc) -> GenericFunc:
|
|
104
125
|
@wraps(func)
|
|
105
126
|
def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
106
|
-
|
|
127
|
+
devnull = io.StringIO()
|
|
128
|
+
with contextlib.redirect_stdout(devnull), contextlib.redirect_stderr(devnull):
|
|
107
129
|
return func(*args, **kwargs)
|
|
108
130
|
|
|
109
131
|
return t.cast(GenericFunc, wrapper)
|
|
@@ -115,7 +137,18 @@ def timeit(
|
|
|
115
137
|
print_func: t.Callable[[str], None] = logger.critical,
|
|
116
138
|
format_str: str = 'Func {func_name} time used: {time_used:.2f} s',
|
|
117
139
|
) -> t.Callable[[GenericFunc], GenericFunc]:
|
|
118
|
-
"""Show time used
|
|
140
|
+
"""Show time used after method is called.
|
|
141
|
+
|
|
142
|
+
After the function returns, ``print_func`` is called with the formatted string
|
|
143
|
+
(supports ``{func_name}`` and ``{time_used}`` placeholders).
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
print_func callable[[str], None]: Callable to output the timing message. Defaults to logger.critical.
|
|
147
|
+
format_str str: Format string for the message. Defaults to 'Func {func_name} time used: {time_used:.2f} s'.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
t.Callable[[GenericFunc], GenericFunc]: A decorator for the target function.
|
|
151
|
+
"""
|
|
119
152
|
|
|
120
153
|
def decorator(func: GenericFunc) -> GenericFunc:
|
|
121
154
|
@wraps(func)
|
|
@@ -29,7 +29,8 @@ def main() -> None:
|
|
|
29
29
|
|
|
30
30
|
log_level = [logging.WARNING, logging.INFO, logging.DEBUG]
|
|
31
31
|
logging.basicConfig(
|
|
32
|
-
level=log_level[min(args.verbose, len(log_level) - 1)],
|
|
32
|
+
level=log_level[min(args.verbose, len(log_level) - 1)],
|
|
33
|
+
format='%(asctime)s %(levelname)s %(module)s :: %(message)s',
|
|
33
34
|
)
|
|
34
35
|
|
|
35
36
|
bin_path = args.bin_path or './build'
|
|
@@ -8,6 +8,7 @@ import sys
|
|
|
8
8
|
import tempfile
|
|
9
9
|
import zipfile
|
|
10
10
|
from asyncio.events import AbstractEventLoop
|
|
11
|
+
from dataclasses import dataclass
|
|
11
12
|
from functools import lru_cache, partial
|
|
12
13
|
|
|
13
14
|
from esptool import get_default_connected_device
|
|
@@ -19,6 +20,8 @@ from esptest.tools.http_download import download_file
|
|
|
19
20
|
from esptest.utility.parse_bin_path import ParseBinPath
|
|
20
21
|
|
|
21
22
|
logger = get_logger('download_bin')
|
|
23
|
+
FLASH_CRYPT_CNT_PATTERN = re.compile(r'(?:FLASH_CRYPT_CNT|SPI_BOOT_CRYPT_CNT).*\(0b([01]+)')
|
|
24
|
+
SECURE_BOOT_EN_PATTERN = re.compile(r'(?:ABS_DONE_1|SECURE_BOOT_EN).*?\((0b[01]+)\)')
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
@lru_cache()
|
|
@@ -72,10 +75,25 @@ def _filter_esptool_log(log: str) -> str:
|
|
|
72
75
|
return new_log
|
|
73
76
|
|
|
74
77
|
|
|
78
|
+
def check_flash_encrypted(efuse_summary: str) -> bool:
|
|
79
|
+
"""Check whether flash encryption is enabled from efuse summary."""
|
|
80
|
+
match = FLASH_CRYPT_CNT_PATTERN.search(efuse_summary)
|
|
81
|
+
if match:
|
|
82
|
+
return match.group(1).count('1') % 2 == 1
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def check_secure_boot_enabled(efuse_summary: str) -> bool:
|
|
87
|
+
"""Check whether secure boot is enabled from efuse summary."""
|
|
88
|
+
match = SECURE_BOOT_EN_PATTERN.search(efuse_summary)
|
|
89
|
+
if match:
|
|
90
|
+
return match.group(1) == '0b1'
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
75
94
|
class DownBinTool:
|
|
76
95
|
# RETRY_CNT = 2
|
|
77
96
|
DEFAULT_BAUD_LIST = [921600, 460800]
|
|
78
|
-
FLASH_CRYPT_CNT_PATTERN = re.compile(r'(?:FLASH_CRYPT_CNT|SPI_BOOT_CRYPT_CNT).*\(0b([01]+)')
|
|
79
97
|
|
|
80
98
|
def __init__(
|
|
81
99
|
self,
|
|
@@ -101,12 +119,6 @@ class DownBinTool:
|
|
|
101
119
|
self.force_no_stub = force_no_stub
|
|
102
120
|
self.check_no_stub = check_no_stub
|
|
103
121
|
|
|
104
|
-
def check_flash_encrypted(self, efuse_summary: str) -> bool:
|
|
105
|
-
match = self.FLASH_CRYPT_CNT_PATTERN.search(efuse_summary)
|
|
106
|
-
if match:
|
|
107
|
-
return match.group(1).count('1') % 2 == 1
|
|
108
|
-
return False
|
|
109
|
-
|
|
110
122
|
def download(self) -> None:
|
|
111
123
|
efuse_cmd = self.espefuse.split()
|
|
112
124
|
try:
|
|
@@ -116,8 +128,8 @@ class DownBinTool:
|
|
|
116
128
|
except subprocess.CalledProcessError as err:
|
|
117
129
|
logger.error(err.output)
|
|
118
130
|
raise RuntimeError(f'Failed to get efuse information from {self.port}') from err
|
|
119
|
-
|
|
120
|
-
|
|
131
|
+
encrypted_indicator = ' [encrypted]' if check_flash_encrypted(summary) else ''
|
|
132
|
+
secure_boot_indicator = ' [secure_boot]' if check_secure_boot_enabled(summary) else ''
|
|
121
133
|
|
|
122
134
|
download_log = ''
|
|
123
135
|
for baud in self.baud_list:
|
|
@@ -133,16 +145,20 @@ class DownBinTool:
|
|
|
133
145
|
chip='auto',
|
|
134
146
|
)
|
|
135
147
|
if not esp.IS_STUB: # type: ignore
|
|
148
|
+
logger.debug(f'Add --no-stub for device: {self.port}')
|
|
136
149
|
args += ['--no-stub'] if '--no-stub' not in args else []
|
|
137
150
|
args += ['-p', self.port]
|
|
138
151
|
args += ['-b', f'{baud}']
|
|
139
|
-
args += self.bin_parser.flash_bin_args(
|
|
152
|
+
args += self.bin_parser.flash_bin_args(
|
|
153
|
+
erase_nvs=self.erase_nvs, encrypted=bool(encrypted_indicator), secure_boot=bool(secure_boot_indicator)
|
|
154
|
+
)
|
|
140
155
|
|
|
141
|
-
logger.info(f'Downloading {self.port}@{baud}{
|
|
156
|
+
logger.info(f'Downloading {self.port}@{baud}{encrypted_indicator}{secure_boot_indicator}: {self.bin_path}')
|
|
142
157
|
logger.debug(f'esptool cmd: {" ".join(args)}')
|
|
143
158
|
# get return code rather than check
|
|
144
159
|
ret = subprocess.run(args, capture_output=True, text=True, check=False)
|
|
145
160
|
if ret.returncode == 0:
|
|
161
|
+
logger.info(f'Download success: [{self.port}@{baud}]')
|
|
146
162
|
return # succeed
|
|
147
163
|
# failed
|
|
148
164
|
download_log += f'esptool cmd failed ({ret.returncode}): ' + ' '.join(args)
|
|
@@ -193,3 +209,38 @@ def download_bin_to_ports( # pylint: disable=too-many-positional-arguments
|
|
|
193
209
|
check_no_stub: bool = False,
|
|
194
210
|
) -> None:
|
|
195
211
|
asyncio.run(async_download_bin_scheduler(bin_path, ports, erase_nvs, max_workers, force_no_stub, check_no_stub))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass
|
|
215
|
+
class BinConfig:
|
|
216
|
+
bin_path: str
|
|
217
|
+
port: str
|
|
218
|
+
erase_nvs: bool = True
|
|
219
|
+
force_no_stub: bool = False
|
|
220
|
+
check_no_stub: bool = False
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
async def async_downbin_scheduler(
|
|
224
|
+
bin_configs: t.List[BinConfig],
|
|
225
|
+
max_workers: int = 0,
|
|
226
|
+
) -> None:
|
|
227
|
+
max_workers = max_workers or len(bin_configs)
|
|
228
|
+
loop = asyncio.get_running_loop()
|
|
229
|
+
loop.set_default_executor(concurrent.futures.ThreadPoolExecutor(max_workers=max_workers))
|
|
230
|
+
|
|
231
|
+
coroutines = []
|
|
232
|
+
for cfg in bin_configs:
|
|
233
|
+
down_tool = DownBinTool(
|
|
234
|
+
cfg.bin_path,
|
|
235
|
+
cfg.port,
|
|
236
|
+
erase_nvs=cfg.erase_nvs,
|
|
237
|
+
force_no_stub=cfg.force_no_stub,
|
|
238
|
+
check_no_stub=cfg.check_no_stub,
|
|
239
|
+
)
|
|
240
|
+
coroutines.append(_async_download_bin(down_tool, loop))
|
|
241
|
+
|
|
242
|
+
await asyncio.gather(*coroutines)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def download_bins(bin_configs: t.List[BinConfig], max_workers: int = 0) -> None:
|
|
246
|
+
asyncio.run(async_downbin_scheduler(bin_configs, max_workers))
|
|
@@ -131,6 +131,7 @@ class ParseBinPath:
|
|
|
131
131
|
self._parttool = parttool
|
|
132
132
|
self._flasher_args: t.Dict[str, t.Any] = {}
|
|
133
133
|
self._sdkconfig: SDKConfig = SDKConfig()
|
|
134
|
+
self._partition_table_csv_path: str = '' # set when partition_table dir is read-only
|
|
134
135
|
|
|
135
136
|
@property
|
|
136
137
|
def sdkconfig(self) -> SDKConfig:
|
|
@@ -192,21 +193,39 @@ class ParseBinPath:
|
|
|
192
193
|
"""Check if esptool stub is used"""
|
|
193
194
|
return bool(self.flasher_args['extra_esptool_args'].get('stub', False))
|
|
194
195
|
|
|
195
|
-
|
|
196
|
+
@property
|
|
197
|
+
def partition_table_csv_path(self) -> Path:
|
|
198
|
+
"""Get partition-table.csv path"""
|
|
199
|
+
if self._partition_table_csv_path:
|
|
200
|
+
return Path(self._partition_table_csv_path)
|
|
201
|
+
return Path(self.bin_path) / 'partition_table' / 'partition-table.csv'
|
|
202
|
+
|
|
203
|
+
def _gen_partition_table(self, part_csv: t.Optional[Path] = None) -> None:
|
|
196
204
|
part_csv = Path(self.bin_path) / 'partition_table' / 'partition-table.csv'
|
|
197
205
|
part_bin = Path(self.bin_path) / 'partition_table' / 'partition-table.bin'
|
|
198
|
-
if
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
206
|
+
if part_csv.is_file():
|
|
207
|
+
# already exists
|
|
208
|
+
return
|
|
209
|
+
if not self.parttool_path or not part_bin.is_file():
|
|
210
|
+
logger.error('Can not gen partition-table.csv: parttool_path or partition-table.bin not found')
|
|
211
|
+
return
|
|
212
|
+
if not os.access(part_csv.parent, os.W_OK):
|
|
213
|
+
# partition_table dir is read-only, use tmp dir for .csv
|
|
214
|
+
part_csv = Path(tempfile.mktemp(suffix='.csv'))
|
|
215
|
+
self._partition_table_csv_path = str(part_csv)
|
|
216
|
+
logger.debug(f'Generating partition-table.csv to {part_csv}')
|
|
217
|
+
try:
|
|
218
|
+
_cmd = ['python', self.parttool_path, str(part_bin), str(part_csv)]
|
|
219
|
+
subprocess.check_call(_cmd, shell=False)
|
|
220
|
+
except subprocess.SubprocessError as e:
|
|
221
|
+
logger.error(f'Failed to gen partition-table.csv: {str(e)}')
|
|
222
|
+
raise e
|
|
204
223
|
|
|
205
224
|
@lru_cache()
|
|
206
225
|
def parse_partitions(self) -> t.List[PartitionInfo]:
|
|
207
226
|
"""Parse partitions from partition-table.csv"""
|
|
208
227
|
self._gen_partition_table()
|
|
209
|
-
partition_table_file =
|
|
228
|
+
partition_table_file = self.partition_table_csv_path
|
|
210
229
|
if not partition_table_file.is_file():
|
|
211
230
|
raise ValueError('Can not parse partition table')
|
|
212
231
|
# # Name, Type, SubType, Offset, Size, Flags
|
|
@@ -287,17 +306,40 @@ class ParseBinPath:
|
|
|
287
306
|
args += ['erase_flash']
|
|
288
307
|
return args
|
|
289
308
|
|
|
290
|
-
def
|
|
309
|
+
def _check_secure_boot_match(self, secure_boot: bool) -> None:
|
|
310
|
+
if secure_boot != self.sdkconfig.secure_boot_config:
|
|
311
|
+
msg = (
|
|
312
|
+
f'Secure Boot status mismatch! '
|
|
313
|
+
f'SDKConfig.secure_boot={self.sdkconfig.secure_boot_config}, '
|
|
314
|
+
f'efuse secure_boot_enabled={secure_boot}. '
|
|
315
|
+
f'Refusing to flash bin.'
|
|
316
|
+
)
|
|
317
|
+
raise RuntimeError(msg)
|
|
318
|
+
|
|
319
|
+
def flash_bin_args(
|
|
320
|
+
self,
|
|
321
|
+
baudrate: int = 0,
|
|
322
|
+
erase_nvs: bool = True,
|
|
323
|
+
encrypted: bool = False,
|
|
324
|
+
secure_boot: bool = False,
|
|
325
|
+
) -> t.List[str]:
|
|
291
326
|
"""Get write_flash args / command for esptool.
|
|
292
327
|
|
|
293
328
|
Args:
|
|
294
329
|
baudrate (int, optional): baudrate for flashing.
|
|
295
330
|
erase_nvs (bool, optional): whether to erase nvs partition.
|
|
296
331
|
encrypted (bool, optional): whether to flash with encryption.
|
|
332
|
+
secure_boot (bool, optional): whether to flash with secure boot.
|
|
297
333
|
"""
|
|
298
334
|
args = self._write_flash_args_common(baudrate)
|
|
299
335
|
if encrypted:
|
|
300
336
|
args += ['--encrypt']
|
|
337
|
+
if secure_boot:
|
|
338
|
+
# Secure Boot blocks writes to protected regions without --force
|
|
339
|
+
# Can't use idf.py flash, can use python -m esptool command in build_log
|
|
340
|
+
args += ['--force']
|
|
341
|
+
# always check secure boot match because efuse will be auto-flashed before idf v6.1 if secure boot is enabled
|
|
342
|
+
self._check_secure_boot_match(secure_boot)
|
|
301
343
|
for offset, bin_file in self.flasher_args['flash_files'].items():
|
|
302
344
|
args += [offset, str(Path(self.bin_path) / bin_file)]
|
|
303
345
|
if erase_nvs:
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from unittest import mock
|
|
2
|
+
|
|
3
|
+
import esptest.tools.download_bin as download_bin_module
|
|
4
|
+
from esptest.tools.download_bin import BinConfig, download_bin_to_ports, download_bins
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# 使用 patch.object(module, ...) 而非 patch('esptest.tools...'),避免 Py 3.7 下 esptest.tools 未加载时的 AttributeError
|
|
8
|
+
# @mock.patch('esptest.tools.download_bin.DownBinTool')
|
|
9
|
+
@mock.patch.object(download_bin_module, 'DownBinTool')
|
|
10
|
+
def test_download_bin_to_ports_calls_down_tool_per_port(
|
|
11
|
+
mock_down_bin_tool: mock.MagicMock,
|
|
12
|
+
) -> None:
|
|
13
|
+
"""download_bin_to_ports 应对每个 port 用同一 bin_path 创建 DownBinTool 并调用 download。"""
|
|
14
|
+
bin_path = '/path/to/bin'
|
|
15
|
+
ports = ['/dev/ttyUSB0', '/dev/ttyUSB1']
|
|
16
|
+
download_bin_to_ports(bin_path, ports, erase_nvs=True, max_workers=2)
|
|
17
|
+
assert mock_down_bin_tool.call_count == 2
|
|
18
|
+
mock_down_bin_tool.assert_any_call(
|
|
19
|
+
bin_path, '/dev/ttyUSB0', erase_nvs=True, force_no_stub=False, check_no_stub=False
|
|
20
|
+
)
|
|
21
|
+
mock_down_bin_tool.assert_any_call(
|
|
22
|
+
bin_path, '/dev/ttyUSB1', erase_nvs=True, force_no_stub=False, check_no_stub=False
|
|
23
|
+
)
|
|
24
|
+
assert mock_down_bin_tool.return_value.download.call_count == 2
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@mock.patch.object(download_bin_module, 'DownBinTool')
|
|
28
|
+
def test_download_bins_calls_down_tool_per_config(
|
|
29
|
+
mock_down_bin_tool: mock.MagicMock,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""download_bins 应对每个 BinConfig 创建 DownBinTool 并调用 download。"""
|
|
32
|
+
configs = [
|
|
33
|
+
BinConfig(bin_path='/path/to/bin1', port='/dev/ttyUSB0'),
|
|
34
|
+
BinConfig(bin_path='/path/to/bin2', port='/dev/ttyUSB1', erase_nvs=False),
|
|
35
|
+
]
|
|
36
|
+
download_bins(configs, max_workers=2)
|
|
37
|
+
assert mock_down_bin_tool.call_count == 2
|
|
38
|
+
mock_down_bin_tool.assert_any_call(
|
|
39
|
+
'/path/to/bin1', '/dev/ttyUSB0', erase_nvs=True, force_no_stub=False, check_no_stub=False
|
|
40
|
+
)
|
|
41
|
+
mock_down_bin_tool.assert_any_call(
|
|
42
|
+
'/path/to/bin2', '/dev/ttyUSB1', erase_nvs=False, force_no_stub=False, check_no_stub=False
|
|
43
|
+
)
|
|
44
|
+
assert mock_down_bin_tool.return_value.download.call_count == 2
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@mock.patch.object(download_bin_module, 'DownBinTool')
|
|
48
|
+
def test_download_bins_empty_list(mock_down_bin_tool: mock.MagicMock) -> None:
|
|
49
|
+
"""空配置列表时不应创建 DownBinTool,不抛错。"""
|
|
50
|
+
download_bins([], max_workers=1)
|
|
51
|
+
mock_down_bin_tool.assert_not_called()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@mock.patch.object(download_bin_module, 'DownBinTool')
|
|
55
|
+
def test_download_bins_default_max_workers(mock_down_bin_tool: mock.MagicMock) -> None:
|
|
56
|
+
"""单配置时创建一次 DownBinTool 并调用 download。"""
|
|
57
|
+
configs = [BinConfig(bin_path='/bin/path', port='/dev/ttyUSB0')]
|
|
58
|
+
download_bins(configs)
|
|
59
|
+
mock_down_bin_tool.assert_called_once_with(
|
|
60
|
+
'/bin/path', '/dev/ttyUSB0', erase_nvs=True, force_no_stub=False, check_no_stub=False
|
|
61
|
+
)
|
|
62
|
+
mock_down_bin_tool.return_value.download.assert_called_once()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@mock.patch.object(download_bin_module, 'DownBinTool')
|
|
66
|
+
def test_download_bins_bin_config_options(mock_down_bin_tool: mock.MagicMock) -> None:
|
|
67
|
+
"""BinConfig 的 erase_nvs/force_no_stub/check_no_stub 应传入 DownBinTool。"""
|
|
68
|
+
configs = [
|
|
69
|
+
BinConfig(
|
|
70
|
+
bin_path='/path/bin',
|
|
71
|
+
port='/dev/ttyUSB0',
|
|
72
|
+
erase_nvs=False,
|
|
73
|
+
force_no_stub=True,
|
|
74
|
+
check_no_stub=True,
|
|
75
|
+
),
|
|
76
|
+
]
|
|
77
|
+
download_bins(configs, max_workers=1)
|
|
78
|
+
mock_down_bin_tool.assert_called_once_with(
|
|
79
|
+
'/path/bin', '/dev/ttyUSB0', erase_nvs=False, force_no_stub=True, check_no_stub=True
|
|
80
|
+
)
|
|
81
|
+
mock_down_bin_tool.return_value.download.assert_called_once()
|
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
import shutil
|
|
3
3
|
import zipfile
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
from unittest.mock import patch
|
|
5
6
|
|
|
6
7
|
import pytest
|
|
7
8
|
|
|
@@ -135,6 +136,36 @@ def test_parse_bin_gen_part(test_bin_path: Path) -> None:
|
|
|
135
136
|
assert set([p.name for p in partitions]) == set(['nvs', 'phy_init', 'factory', 'wpa2_cer', 'wpa2_key', 'wpa2_ca'])
|
|
136
137
|
|
|
137
138
|
|
|
139
|
+
def test_parse_partitions_when_partition_table_dir_read_only(test_bin_path: Path) -> None:
|
|
140
|
+
"""When partition_table dir is read-only, csv is generated in a tmp path and parse_partitions still works.
|
|
141
|
+
Use mock for os.access instead of real chmod so the test works in CI/Docker and on any filesystem.
|
|
142
|
+
"""
|
|
143
|
+
part_dir = test_bin_path / 'partition_table'
|
|
144
|
+
partition_csv = part_dir / 'partition-table.csv'
|
|
145
|
+
partition_bin = part_dir / 'partition-table.bin'
|
|
146
|
+
assert partition_bin.is_file(), 'test data must have partition-table.bin'
|
|
147
|
+
os.remove(str(partition_csv))
|
|
148
|
+
assert not partition_csv.is_file()
|
|
149
|
+
|
|
150
|
+
real_access = os.access
|
|
151
|
+
|
|
152
|
+
def fake_access(path: Path, mode: int) -> bool:
|
|
153
|
+
if Path(path).resolve() == part_dir.resolve() and mode == os.W_OK:
|
|
154
|
+
return False
|
|
155
|
+
return real_access(path, mode)
|
|
156
|
+
|
|
157
|
+
# Patch os.access where it is used (global os module) to avoid AttributeError
|
|
158
|
+
# when 'esptest.utility' is not visible in the test environment.
|
|
159
|
+
with patch('os.access', side_effect=fake_access):
|
|
160
|
+
parser = ParseBinPath(test_bin_path)
|
|
161
|
+
partitions = parser.parse_partitions()
|
|
162
|
+
|
|
163
|
+
assert not partition_csv.is_file() # csv file should not be created to read-only dir
|
|
164
|
+
assert len(partitions) == 6
|
|
165
|
+
assert set(p.name for p in partitions) == {'nvs', 'phy_init', 'factory', 'wpa2_cer', 'wpa2_key', 'wpa2_ca'}
|
|
166
|
+
assert parser.partition_table_csv_path # attribute should be set
|
|
167
|
+
|
|
168
|
+
|
|
138
169
|
def test_bin_path_to_dir() -> None:
|
|
139
170
|
bin_path = bin_path_to_dir(str(TEST_FILE_PATH / 'test-bin.zip'))
|
|
140
171
|
parse_bin_path = ParseBinPath(bin_path)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/esp_console/_files/wifi_cmd_connected_1.log
RENAMED
|
File without changes
|
{esp_test_utils-0.3.1 → esp_test_utils-0.3.2}/tests/esp_console/_files/wifi_cmd_connected_2.log
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|