Merge branch 'feat/esptool_v5_minimal' into 'master'

Feat: Add minimal esptool v5 support

See merge request espressif/esp-idf!41314
This commit is contained in:
Peter Dragun
2025-08-25 16:28:51 +08:00
7 changed files with 240 additions and 175 deletions

View File

@@ -36,11 +36,14 @@ def _test_flash_encryption(dut: Dut) -> None:
key_bytes = b'\xff' + b'\x00' * 31
aes_xts = True
# Emulate espsecure encrypt_flash_data command
EncryptFlashDataArgs = namedtuple(
'EncryptFlashDataArgs', ['output', 'plaintext_file', 'address', 'keyfile', 'flash_crypt_conf', 'aes_xts']
)
args = EncryptFlashDataArgs(BytesIO(), BytesIO(plain_data), flash_addr, BytesIO(key_bytes), 0xF, aes_xts)
try:
# espsecure 5.0 arguments are passed one by one; the following will convert tuple to dict and unwrap it
espsecure.encrypt_flash_data(**args._asdict())
except TypeError:
espsecure.encrypt_flash_data(args)
expected_ciphertext = args.output.getvalue()

View File

@@ -38,7 +38,7 @@ def get_running_partition(port=None):
try:
# Check what esptool.py finds on what port the device is connected to
output = subprocess.check_output([sys.executable, ESPTOOL_PY, 'chip_id']) # may raise CalledProcessError
pattern = r'Serial port ([\S]+)'
pattern = r'Serial port ([^:\s]+)'
pattern = re.compile(pattern.encode())
port = re.search(pattern, output).group(1) # may raise AttributeError

View File

@@ -39,3 +39,8 @@ warning: unknown kconfig symbol 'UNITY_FREERTOS_STACK_SIZE' assigned to '12288'
warning: unknown kconfig symbol 'WPA3_SAE' assigned to 'y' in .*/components/wpa_supplicant/test_apps/sdkconfig.defaults
ld: warning: ignoring duplicate libraries
archive library: .+ the table of contents is empty
Warning: Deprecated: Option '--flash_size' is deprecated. Use '--flash-size' instead.
Warning: Deprecated: Option '--flash_mode' is deprecated. Use '--flash-mode' instead.
Warning: Deprecated: Option '--flash_freq' is deprecated. Use '--flash-freq' instead.
Warning: Deprecated: Command 'sign_data' is deprecated. Use 'sign-data' instead.
Warning: Deprecated: Command 'extract_public_key' is deprecated. Use 'extract-public-key' instead.

View File

@@ -7,9 +7,6 @@ import signal
import sys
from pathlib import Path
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
import click
@@ -46,22 +43,22 @@ PORT = {
}
def yellow_print(message: str, newline: Optional[str] = '\n') -> None:
def yellow_print(message: str, newline: str | None = '\n') -> None:
"""Print a message to stderr with yellow highlighting"""
sys.stderr.write('%s%s%s%s' % ('\033[0;33m', message, '\033[0m', newline))
sys.stderr.write(f'\033[0;33m{message}\033[0m{newline}')
sys.stderr.flush()
def action_extensions(base_actions: Dict, project_path: str) -> Dict:
def action_extensions(base_actions: dict, project_path: str) -> dict:
def _get_project_desc(ctx: click.core.Context, args: PropertyDict) -> Any:
desc_path = os.path.join(args.build_dir, 'project_description.json')
if not os.path.exists(desc_path):
ensure_build_directory(args, ctx.info_name)
with open(desc_path, 'r', encoding='utf-8') as f:
with open(desc_path, encoding='utf-8') as f:
project_desc = json.load(f)
return project_desc
def _get_esptool_args(args: PropertyDict) -> List:
def _get_esptool_args(args: PropertyDict) -> list:
esptool_path = os.path.join(os.environ['IDF_PATH'], 'components/esptool_py/esptool/esptool.py')
esptool_wrapper_path = os.environ.get('ESPTOOL_WRAPPER', '')
if args.port is None:
@@ -84,7 +81,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
result += ['--no-stub']
return result
def _get_commandline_options(ctx: click.core.Context) -> List:
def _get_commandline_options(ctx: click.core.Context) -> list:
"""Return all the command line options up to first action"""
# This approach ignores argument parsing done Click
result = []
@@ -185,7 +182,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
monitor_args += ['--disable-auto-color']
idf_py = [PYTHON] + _get_commandline_options(ctx) # commands to re-run idf.py
monitor_args += ['-m', ' '.join("'%s'" % a for a in idf_py)]
monitor_args += ['-m', ' '.join(f"'{a}'" for a in idf_py)]
hints = not args.no_hints
# Temporally ignore SIGINT, which is used in idf_monitor to spawn gdb.
@@ -247,7 +244,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
esptool_args += ['erase_flash']
RunTool('esptool.py', esptool_args, args.build_dir, hints=not args.no_hints)()
def global_callback(ctx: click.core.Context, global_args: Dict, tasks: PropertyDict) -> None:
def global_callback(ctx: click.core.Context, global_args: dict, tasks: PropertyDict) -> None:
encryption = any([task.name in ('encrypted-flash', 'encrypted-app-flash') for task in tasks])
if encryption:
for task in tasks:
@@ -503,7 +500,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
encrypt_nvs_partition_args += [extra_args['partition_size']]
RunTool('espsecure', encrypt_nvs_partition_args, args.project_dir)()
def _parse_efuse_args(ctx: click.core.Context, args: PropertyDict, extra_args: Dict) -> List:
def _parse_efuse_args(ctx: click.core.Context, args: PropertyDict, extra_args: dict) -> list:
efuse_args = []
if args.port:
efuse_args += ['-p', args.port]
@@ -520,18 +517,20 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
efuse_args += ['--do-not-confirm']
return efuse_args
def efuse_burn(action: str, ctx: click.core.Context, args: PropertyDict, **extra_args: Dict) -> None:
def efuse_burn(action: str, ctx: click.core.Context, args: PropertyDict, **extra_args: dict) -> None:
ensure_build_directory(args, ctx.info_name)
burn_efuse_args = [PYTHON, '-mespefuse', 'burn_efuse']
burn_efuse_args = [PYTHON, '-m', 'espefuse']
burn_efuse_args += _parse_efuse_args(ctx, args, extra_args)
burn_efuse_args.append('burn_efuse')
if extra_args['efuse_positional_args']:
burn_efuse_args += list(extra_args['efuse_positional_args'])
RunTool('espefuse', burn_efuse_args, args.build_dir)()
def efuse_burn_key(action: str, ctx: click.core.Context, args: PropertyDict, **extra_args: str) -> None:
ensure_build_directory(args, ctx.info_name)
burn_key_args = [PYTHON, '-mespefuse', 'burn_key']
burn_key_args = [PYTHON, '-m', 'espefuse']
burn_key_args += _parse_efuse_args(ctx, args, extra_args)
burn_key_args.append('burn_key')
if extra_args['no_protect_key']:
burn_key_args += ['--no-protect-key']
if extra_args['force_write_always']:
@@ -543,19 +542,21 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
RunTool('espefuse.py', burn_key_args, args.project_dir, build_dir=args.build_dir)()
def efuse_dump(
action: str, ctx: click.core.Context, args: PropertyDict, file_name: str, **extra_args: Dict
action: str, ctx: click.core.Context, args: PropertyDict, file_name: str, **extra_args: dict
) -> None:
ensure_build_directory(args, ctx.info_name)
dump_args = [PYTHON, '-mespefuse', 'dump']
dump_args = [PYTHON, '-m', 'espefuse']
dump_args += _parse_efuse_args(ctx, args, extra_args)
dump_args.append('dump')
if file_name:
dump_args += ['--file_name', file_name]
RunTool('espefuse', dump_args, args.build_dir)()
def efuse_read_protect(action: str, ctx: click.core.Context, args: PropertyDict, **extra_args: Dict) -> None:
def efuse_read_protect(action: str, ctx: click.core.Context, args: PropertyDict, **extra_args: dict) -> None:
ensure_build_directory(args, ctx.info_name)
read_protect_args = [PYTHON, '-mespefuse', 'read_protect_efuse']
read_protect_args = [PYTHON, '-m', 'espefuse']
read_protect_args += _parse_efuse_args(ctx, args, extra_args)
read_protect_args.append('read_protect_efuse')
if extra_args['efuse_positional_args']:
read_protect_args += list(extra_args['efuse_positional_args'])
RunTool('espefuse', read_protect_args, args.build_dir)()
@@ -565,21 +566,23 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
ctx: click.core.Context,
args: PropertyDict,
format: str, # noqa: A002
**extra_args: Dict,
**extra_args: dict,
) -> None:
ensure_build_directory(args, ctx.info_name)
summary_args = [PYTHON, '-mespefuse', 'summary']
summary_args = [PYTHON, '-m', 'espefuse']
summary_args += _parse_efuse_args(ctx, args, extra_args)
summary_args.append('summary')
if format:
summary_args += [f'--format={format.replace("-", "_")}']
if extra_args['efuse_name']:
summary_args += [str(extra_args['efuse_name'])]
RunTool('espefuse', summary_args, args.build_dir)()
def efuse_write_protect(action: str, ctx: click.core.Context, args: PropertyDict, **extra_args: Dict) -> None:
def efuse_write_protect(action: str, ctx: click.core.Context, args: PropertyDict, **extra_args: dict) -> None:
ensure_build_directory(args, ctx.info_name)
write_protect_args = [PYTHON, '-mespefuse', 'write_protect_efuse']
write_protect_args = [PYTHON, '-m', 'espefuse']
write_protect_args += _parse_efuse_args(ctx, args, extra_args)
write_protect_args.append('write_protect_efuse')
if extra_args['efuse_positional_args']:
write_protect_args += list(extra_args['efuse_positional_args'])
RunTool('espefuse', write_protect_args, args.build_dir)()

View File

@@ -1,24 +1,25 @@
# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import logging
from typing import Any, Optional
from collections import namedtuple
from typing import Any
import esptool
from pytest_embedded_idf.app import IdfApp
from pytest_embedded_serial_esp.serial import EspSerial, EsptoolArgs
from pytest_embedded_serial_esp.serial import EspSerial
class LoadableAppSerial(EspSerial):
def __init__(
self,
app: IdfApp,
target: Optional[str] = None,
target: str | None = None,
**kwargs: Any,
) -> None:
self.app = app
if not hasattr(self.app, 'target'):
raise ValueError(f'Idf app not parsable. Please check if it\'s valid: {self.app.binary_path}')
raise ValueError(f"Idf app not parsable. Please check if it's valid: {self.app.binary_path}")
if target and self.app.target and self.app.target != target:
raise ValueError(f'Targets do not match. App target: {self.app.target}, Cmd target: {target}.')
@@ -37,17 +38,10 @@ class LoadableAppSerial(EspSerial):
logging.error('No image file detected. Skipping load ram...')
return
f_bin_file = open(self.app.bin_file, 'rb')
default_kwargs = {
'filename': f_bin_file,
'chip': self.esp.CHIP_NAME.lower().replace('-', ''),
}
load_ram_args = EsptoolArgs(**default_kwargs)
try:
with open(self.app.bin_file, 'rb') as f_bin_file:
self.esp.change_baud(460800)
esptool.load_ram(self.esp, load_ram_args)
finally:
f_bin_file.close()
try:
# esptool v5.0+
esptool.load_ram(self.esp, input=f_bin_file)
except TypeError:
esptool.load_ram(self.esp, namedtuple('args', 'filename')(f_bin_file))

View File

@@ -2,13 +2,12 @@
# SPDX-License-Identifier: Apache-2.0
import logging
import os
import re
import shutil
import stat
import sys
import textwrap
from pathlib import Path
from typing import List
from typing import Union
import pytest
from test_build_system_helpers import APP_BINS
@@ -22,7 +21,7 @@ from test_build_system_helpers import replace_in_file
from test_build_system_helpers import run_cmake_and_build
def assert_built(paths: Union[List[str], List[Path]]) -> None:
def assert_built(paths: list[str] | list[Path]) -> None:
for path in paths:
assert os.path.exists(path)
@@ -92,7 +91,7 @@ def test_build_with_cmake_and_idf_path_unset(idf_py: IdfPyFunc, test_app_copy: P
logging.info('Can build with IDF_PATH set via cmake cache not environment')
replace_in_file('CMakeLists.txt', 'ENV{IDF_PATH}', '{IDF_PATH}')
run_cmake_and_build('-G', 'Ninja', '..', '-DIDF_PATH={}'.format(idf_path), env=env)
run_cmake_and_build('-G', 'Ninja', '..', f'-DIDF_PATH={idf_path}', env=env)
assert_built(BOOTLOADER_BINS + APP_BINS + PARTITION_BIN)
idf_py('fullclean')
@@ -100,7 +99,7 @@ def test_build_with_cmake_and_idf_path_unset(idf_py: IdfPyFunc, test_app_copy: P
# working with already changed CMakeLists.txt
kconfig_file = test_app_copy / 'main' / 'Kconfig.projbuild'
kconfig_file.write_text('source "$IDF_PATH/examples/wifi/getting_started/station/main/Kconfig.projbuild"')
run_cmake_and_build('-G', 'Ninja', '..', '-DIDF_PATH={}'.format(idf_path), env=env)
run_cmake_and_build('-G', 'Ninja', '..', f'-DIDF_PATH={idf_path}', env=env)
assert_built(BOOTLOADER_BINS + APP_BINS + PARTITION_BIN)
kconfig_file.unlink() # remove file to not affect following sub-test
idf_py('fullclean')
@@ -108,7 +107,7 @@ def test_build_with_cmake_and_idf_path_unset(idf_py: IdfPyFunc, test_app_copy: P
logging.info('Can build with IDF_PATH unset and inferred by build system')
# replacing {IDF_PATH} not ENV{IDF_PATH} since CMakeLists.txt was already changed in this test
replace_in_file('CMakeLists.txt', '{IDF_PATH}', '{ci_idf_path}')
run_cmake_and_build('-G', 'Ninja', '-D', 'ci_idf_path={}'.format(idf_path), '..', env=env)
run_cmake_and_build('-G', 'Ninja', '-D', f'ci_idf_path={idf_path}', '..', env=env)
assert_built(BOOTLOADER_BINS + APP_BINS + PARTITION_BIN)
@@ -184,16 +183,18 @@ def test_build_dfu(idf_py: IdfPyFunc) -> None:
def test_build_uf2(idf_py: IdfPyFunc) -> None:
logging.info('UF2 build works')
ret = idf_py('uf2')
assert 'build/uf2.bin, ready to be flashed with any ESP USB Bridge' in ret.stdout, 'UF2 build should work for esp32'
assert re.search(r"build/uf2.bin'?, ready to be flashed with any ESP USB Bridge", ret.stdout) is not None, (
'UF2 build should work for esp32'
)
assert_built(BOOTLOADER_BINS + APP_BINS + PARTITION_BIN + ['build/uf2.bin'])
ret = idf_py('uf2-app')
assert 'build/uf2-app.bin, ready to be flashed with any ESP USB Bridge' in ret.stdout, (
assert re.search(r"build/uf2-app.bin'?, ready to be flashed with any ESP USB Bridge", ret.stdout) is not None, (
'UF2 build should work for application binary'
)
assert_built(['build/uf2-app.bin'])
idf_py('set-target', 'esp32s2')
ret = idf_py('uf2')
assert 'build/uf2.bin, ready to be flashed with any ESP USB Bridge' in ret.stdout, (
assert re.search(r"build/uf2.bin'?, ready to be flashed with any ESP USB Bridge", ret.stdout) is not None, (
'UF2 build should work for esp32s2'
)
assert_built(BOOTLOADER_BINS + APP_BINS + PARTITION_BIN + ['build/uf2.bin'])
@@ -253,7 +254,7 @@ def test_build_with_misspelled_kconfig(idf_py: IdfPyFunc, test_app_copy: Path) -
ret = idf_py('build')
assert " file should be named 'Kconfig.projbuild'" in ret.stderr, 'Misspelled Kconfig file should be detected'
assert_built(BOOTLOADER_BINS + APP_BINS + PARTITION_BIN)
with open(test_app_copy / 'sdkconfig', 'r') as f:
with open(test_app_copy / 'sdkconfig') as f:
sdkconfig = f.read()
assert 'CONFIG_FROM_MISSPELLED_KCONFIG=y' in sdkconfig, (
'There should be a config from the misspelled Kconfig file in sdkconfig'

View File

@@ -8,10 +8,9 @@ import shutil
import subprocess
import sys
from typing import Any
from typing import List
from unittest import TestCase
from unittest import main
from unittest import mock
from unittest import TestCase
import jsonschema
@@ -37,10 +36,13 @@ class TestWithoutExtensions(TestCase):
@classmethod
def setUpClass(cls):
# Disable the component manager and extra extensions for these tests
cls.env_patcher = mock.patch.dict(os.environ, {
cls.env_patcher = mock.patch.dict(
os.environ,
{
'IDF_COMPONENT_MANAGER': '0',
'IDF_EXTRA_ACTIONS_PATH': '',
})
},
)
cls.env_patcher.start()
super().setUpClass()
@@ -51,8 +53,9 @@ class TestExtensions(TestWithoutExtensions):
try:
os.symlink(extension_path, link_path)
os.environ['IDF_EXTRA_ACTIONS_PATH'] = os.path.join(current_dir, 'extra_path')
output = subprocess.check_output([sys.executable, idf_py_path, '--help'],
env=os.environ).decode('utf-8', 'ignore')
output = subprocess.check_output([sys.executable, idf_py_path, '--help'], env=os.environ).decode(
'utf-8', 'ignore'
)
self.assertIn('--test-extension-option', output)
self.assertIn('test_subcommand', output)
@@ -67,7 +70,8 @@ class TestExtensions(TestWithoutExtensions):
os.environ['IDF_EXTRA_ACTIONS_PATH'] = ';'.join([os.path.join(current_dir, 'extra_path')])
output = subprocess.check_output(
[sys.executable, idf_py_path, '--some-extension-option=awesome', 'test_subcommand', 'extra_subcommand'],
env=os.environ).decode('utf-8', 'ignore')
env=os.environ,
).decode('utf-8', 'ignore')
self.assertIn('!!! From some global callback: awesome', output)
self.assertIn('!!! From some subcommand', output)
self.assertIn('!!! From test global callback: test', output)
@@ -79,8 +83,9 @@ class TestExtensions(TestWithoutExtensions):
try:
os.symlink(extension_path, link_path)
os.environ['IDF_EXTRA_ACTIONS_PATH'] = ';'.join([os.path.join(current_dir, 'extra_path')])
output = subprocess.check_output([sys.executable, idf_py_path, '--help'],
env=os.environ).decode('utf-8', 'ignore')
output = subprocess.check_output([sys.executable, idf_py_path, '--help'], env=os.environ).decode(
'utf-8', 'ignore'
)
self.assertIn('test_subcommand', output)
self.assertNotIn('hidden_one', output)
@@ -127,7 +132,8 @@ class TestDependencyManagement(TestWithoutExtensions):
sys.stderr = sys.__stderr__
self.assertIn(
'WARNING: Commands "all", "clean" are found in the list of commands more than once.',
capturedOutput.getvalue())
capturedOutput.getvalue(),
)
sys.stderr = capturedOutput
idf.init_cli()(
@@ -136,7 +142,8 @@ class TestDependencyManagement(TestWithoutExtensions):
)
sys.stderr = sys.__stderr__
self.assertIn(
'WARNING: Command "clean" is found in the list of commands more than once.', capturedOutput.getvalue())
'WARNING: Command "clean" is found in the list of commands more than once.', capturedOutput.getvalue()
)
class TestVerboseFlag(TestWithoutExtensions):
@@ -145,10 +152,13 @@ class TestVerboseFlag(TestWithoutExtensions):
[
sys.executable,
idf_py_path,
'-C%s' % current_dir,
'-C',
current_dir,
'-v',
'test-verbose',
], env=os.environ).decode('utf-8', 'ignore')
],
env=os.environ,
).decode('utf-8', 'ignore')
self.assertIn('Verbose mode on', output)
@@ -157,9 +167,12 @@ class TestVerboseFlag(TestWithoutExtensions):
[
sys.executable,
idf_py_path,
'-C%s' % current_dir,
'-C',
current_dir,
'test-verbose',
], env=os.environ).decode('utf-8', 'ignore')
],
env=os.environ,
).decode('utf-8', 'ignore')
self.assertIn('Output from test-verbose', output)
self.assertNotIn('Verbose mode on', output)
@@ -188,27 +201,31 @@ class TestDeprecations(TestWithoutExtensions):
def test_exit_with_error_for_subcommand(self):
try:
subprocess.check_output(
[sys.executable, idf_py_path, '-C%s' % current_dir, 'test-2'], env=os.environ, stderr=subprocess.STDOUT)
[sys.executable, idf_py_path, '-C', current_dir, 'test-2'], env=os.environ, stderr=subprocess.STDOUT
)
except subprocess.CalledProcessError as e:
self.assertIn('Error: Command "test-2" is deprecated and was removed.', e.output.decode('utf-8', 'ignore'))
def test_exit_with_error_for_option(self):
try:
subprocess.check_output(
[sys.executable, idf_py_path, '-C%s' % current_dir, '--test-5=asdf'],
[sys.executable, idf_py_path, '-C', current_dir, '--test-5=asdf'],
env=os.environ,
stderr=subprocess.STDOUT)
stderr=subprocess.STDOUT,
)
except subprocess.CalledProcessError as e:
self.assertIn(
'Error: Option "test_5" is deprecated since v2.0 and was removed in v3.0.',
e.output.decode('utf-8', 'ignore'))
e.output.decode('utf-8', 'ignore'),
)
def test_deprecation_messages(self):
output = subprocess.check_output(
[
sys.executable,
idf_py_path,
'-C%s' % current_dir,
'-C',
current_dir,
'--test-0=a',
'--test-1=b',
'--test-2=c',
@@ -220,23 +237,28 @@ class TestDeprecations(TestWithoutExtensions):
'test-1',
],
env=os.environ,
stderr=subprocess.STDOUT).decode('utf-8', 'ignore')
stderr=subprocess.STDOUT,
).decode('utf-8', 'ignore')
self.assertIn('Warning: Option "test_sub_1" is deprecated and will be removed in future versions.', output)
self.assertIn(
'Warning: Command "test-1" is deprecated and will be removed in future versions. '
'Please use alternative command.', output)
'Please use alternative command.',
output,
)
self.assertIn('Warning: Option "test_1" is deprecated and will be removed in future versions.', output)
self.assertIn(
'Warning: Option "test_2" is deprecated and will be removed in future versions. '
'Please update your parameters.', output)
'Please update your parameters.',
output,
)
self.assertIn('Warning: Option "test_3" is deprecated and will be removed in future versions.', output)
self.assertNotIn('"test-0" is deprecated', output)
self.assertNotIn('"test_0" is deprecated', output)
class TestHelpOutput(TestWithoutExtensions):
def action_test_idf_py(self, commands: List[str], schema: Any) -> None:
def action_test_idf_py(self, commands: list[str], schema: Any) -> None:
env = dict(**os.environ)
python = shutil.which('python', path=env['PATH'])
if python is None:
@@ -244,20 +266,17 @@ class TestHelpOutput(TestWithoutExtensions):
idf_path = env.get('IDF_PATH')
if idf_path is None:
raise ValueError('Empty IDF_PATH')
idf_py_cmd = [
python,
os.path.join(idf_path, 'tools', 'idf.py')
]
idf_py_cmd = [python, os.path.join(idf_path, 'tools', 'idf.py')]
commands = idf_py_cmd + commands
output_file = 'idf_py_help_output.json'
with open(output_file, 'w') as outfile:
subprocess.run(commands, env=env, stdout=outfile)
with open(output_file, 'r') as outfile:
with open(output_file) as outfile:
help_obj = json.load(outfile)
self.assertIsNone(jsonschema.validate(help_obj, schema))
def test_output(self):
with open(os.path.join(current_dir, 'idf_py_help_schema.json'), 'r') as schema_file:
with open(os.path.join(current_dir, 'idf_py_help_schema.json')) as schema_file:
schema_json = json.load(schema_file)
self.action_test_idf_py(['help', '--json'], schema_json)
self.action_test_idf_py(['help', '--json', '--add-options'], schema_json)
@@ -270,7 +289,8 @@ class TestFileArgumentExpansion(TestCase):
output = subprocess.check_output(
[sys.executable, idf_py_path, '--version', '@file_args_expansion_inputs/args_a'],
env=os.environ,
stderr=subprocess.STDOUT).decode('utf-8', 'ignore')
stderr=subprocess.STDOUT,
).decode('utf-8', 'ignore')
self.assertIn('Running: idf.py --version DAAA DBBB', output)
except subprocess.CalledProcessError as e:
self.fail(f'Process should have exited normally, but it exited with a return code of {e.returncode}')
@@ -279,9 +299,16 @@ class TestFileArgumentExpansion(TestCase):
"""Test multiple @filename arguments"""
try:
output = subprocess.check_output(
[sys.executable, idf_py_path, '--version', '@file_args_expansion_inputs/args_a', '@file_args_expansion_inputs/args_b'],
[
sys.executable,
idf_py_path,
'--version',
'@file_args_expansion_inputs/args_a',
'@file_args_expansion_inputs/args_b',
],
env=os.environ,
stderr=subprocess.STDOUT).decode('utf-8', 'ignore')
stderr=subprocess.STDOUT,
).decode('utf-8', 'ignore')
self.assertIn('Running: idf.py --version DAAA DBBB DCCC DDDD', output)
except subprocess.CalledProcessError as e:
self.fail(f'Process should have exited normally, but it exited with a return code of {e.returncode}')
@@ -292,7 +319,8 @@ class TestFileArgumentExpansion(TestCase):
output = subprocess.check_output(
[sys.executable, idf_py_path, '--version', '@file_args_expansion_inputs/args_recursive'],
env=os.environ,
stderr=subprocess.STDOUT).decode('utf-8', 'ignore')
stderr=subprocess.STDOUT,
).decode('utf-8', 'ignore')
self.assertIn('Running: idf.py --version DAAA DBBB DEEE DFFF', output)
except subprocess.CalledProcessError as e:
self.fail(f'Process should have exited normally, but it exited with a return code of {e.returncode}')
@@ -303,7 +331,8 @@ class TestFileArgumentExpansion(TestCase):
subprocess.check_output(
[sys.executable, idf_py_path, '--version', '@file_args_expansion_inputs/args_circular_a'],
env=os.environ,
stderr=subprocess.STDOUT).decode('utf-8', 'ignore')
stderr=subprocess.STDOUT,
).decode('utf-8', 'ignore')
self.assertIn('Circular dependency in file argument expansion', cm.exception.output.decode('utf-8', 'ignore'))
def test_missing_file(self):
@@ -312,8 +341,11 @@ class TestFileArgumentExpansion(TestCase):
subprocess.check_output(
[sys.executable, idf_py_path, '--version', '@args_non_existent'],
env=os.environ,
stderr=subprocess.STDOUT).decode('utf-8', 'ignore')
self.assertIn('(expansion of @args_non_existent) could not be opened', cm.exception.output.decode('utf-8', 'ignore'))
stderr=subprocess.STDOUT,
).decode('utf-8', 'ignore')
self.assertIn(
'(expansion of @args_non_existent) could not be opened', cm.exception.output.decode('utf-8', 'ignore')
)
class TestWrapperCommands(TestCase):
@@ -323,12 +355,11 @@ class TestWrapperCommands(TestCase):
os.chdir(cls.sample_project_dir)
super().setUpClass()
def call_command(self, command: List[str]) -> str:
def call_command(self, command: list[str]) -> str:
try:
output = subprocess.check_output(
command,
env=os.environ,
stderr=subprocess.STDOUT).decode('utf-8', 'ignore')
output = subprocess.check_output(command, env=os.environ, stderr=subprocess.STDOUT).decode(
'utf-8', 'ignore'
)
return output
except subprocess.CalledProcessError as e:
self.fail(f'Process should have exited normally, but it exited with a return code of {e.returncode}')
@@ -343,7 +374,8 @@ class TestWrapperCommands(TestCase):
class TestEFuseCommands(TestWrapperCommands):
"""
Test if wrapper commands for espefuse.py are working as expected.
The goal is NOT to test the functionality of espefuse.py, but to test if the wrapper commands are working as expected.
The goal is NOT to test the functionality of espefuse.py, but to test if the wrapper commands
are working as expected.
"""
def test_efuse_summary(self):
@@ -351,17 +383,17 @@ class TestEFuseCommands(TestWrapperCommands):
output = self.call_command(summary_command)
self.assertIn('EFUSE_NAME (Block) Description = [Meaningful Value] [Readable/Writeable] (Hex Value)', output)
output = self.call_command(summary_command + ['--format','summary'])
output = self.call_command(summary_command + ['--format', 'summary'])
self.assertIn('00:00:00:00:00:00', output)
self.assertIn('MAC address', output)
output = self.call_command(summary_command + ['--format','value-only', 'WR_DIS'])
output = self.call_command(summary_command + ['--format', 'value-only', 'WR_DIS'])
self.assertIn('0', output)
def test_efuse_burn(self):
burn_command = [sys.executable, idf_py_path, 'efuse-burn', '--virt', '--do-not-confirm']
output = self.call_command(burn_command + ['WR_DIS', '1'])
self.assertIn('\'WR_DIS\' (Efuse write disable mask) 0x0000 -> 0x0001', output)
self.assertIn("'WR_DIS' (Efuse write disable mask) 0x0000 -> 0x0001", output)
self.assertIn('Successful', output)
output = self.call_command(burn_command + ['WR_DIS', '1', 'RD_DIS', '1'])
@@ -371,9 +403,14 @@ class TestEFuseCommands(TestWrapperCommands):
def test_efuse_burn_key(self):
key_name = 'efuse_test_key.bin'
subprocess.run([sys.executable, idf_py_path, 'secure-generate-flash-encryption-key', os.path.join(current_dir, key_name)], stdout=subprocess.DEVNULL)
subprocess.run(
[sys.executable, idf_py_path, 'secure-generate-flash-encryption-key', os.path.join(current_dir, key_name)],
stdout=subprocess.DEVNULL,
)
burn_key_command = [sys.executable, idf_py_path, 'efuse-burn-key', '--virt', '--do-not-confirm']
output = self.call_command(burn_key_command + ['--show-sensitive-info', 'secure_boot_v1', os.path.join(current_dir, key_name)])
output = self.call_command(
burn_key_command + ['--show-sensitive-info', 'secure_boot_v1', os.path.join(current_dir, key_name)]
)
self.assertIn('Burn keys to blocks:', output)
self.assertIn('Successful', output)
@@ -401,8 +438,10 @@ class TestEFuseCommands(TestWrapperCommands):
class TestSecureCommands(TestWrapperCommands):
"""
Test if wrapper commands for espsecure.py are working as expected.
The goal is NOT to test the functionality of espsecure.py, but to test if the wrapper commands are working as expected.
The goal is NOT to test the functionality of espsecure.py, but to test if the wrapper commands are
working as expected.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
@@ -412,13 +451,19 @@ class TestSecureCommands(TestWrapperCommands):
cls.nvs_partition_key = 'nvs_partition_key.bin'
def secure_generate_flash_encryption_key(self):
generate_key_command = [sys.executable, idf_py_path, 'secure-generate-flash-encryption-key', self.flash_encryption_key]
generate_key_command = [
sys.executable,
idf_py_path,
'secure-generate-flash-encryption-key',
self.flash_encryption_key,
]
output = self.call_command(generate_key_command)
self.assertIn(f'Writing 256 random bits to key file {self.flash_encryption_key}', output)
self.assertRegex(output, f'Writing 256 random bits to key file "?{self.flash_encryption_key}"?')
def secure_encrypt_flash_data(self):
self.secure_generate_flash_encryption_key()
encrypt_command = [sys.executable,
encrypt_command = [
sys.executable,
idf_py_path,
'secure-encrypt-flash-data',
'--aes-xts',
@@ -428,14 +473,16 @@ class TestSecureCommands(TestWrapperCommands):
'0x1000',
'--output',
'bootloader-enc.bin',
'bootloader/bootloader.bin']
'bootloader/bootloader.bin',
]
output = self.call_command(encrypt_command)
self.assertIn('Using 256-bit key', output)
self.assertIn('Done', output)
def test_secure_decrypt_flash_data(self):
self.secure_encrypt_flash_data()
decrypt_command = [sys.executable,
decrypt_command = [
sys.executable,
idf_py_path,
'secure-decrypt-flash-data',
'--aes-xts',
@@ -445,14 +492,16 @@ class TestSecureCommands(TestWrapperCommands):
'0x1000',
'--output',
'bootloader-dec.bin',
'bootloader-enc.bin']
'bootloader-enc.bin',
]
output = self.call_command(decrypt_command)
self.assertIn('Using 256-bit key', output)
self.assertIn('Done', output)
def secure_sign_data(self):
self.secure_generate_signing_key()
sign_command = [sys.executable,
sign_command = [
sys.executable,
idf_py_path,
'secure-sign-data',
'--version',
@@ -461,49 +510,57 @@ class TestSecureCommands(TestWrapperCommands):
f'../{self.signing_key}',
'--output',
'bootloader-signed.bin',
'bootloader/bootloader.bin']
'bootloader/bootloader.bin',
]
output = self.call_command(sign_command)
self.assertIn('Signed', output)
def secure_verify_signature(self):
self.secure_sign_data()
sign_command = [sys.executable,
sign_command = [
sys.executable,
idf_py_path,
'secure-verify-signature',
'--version',
'2',
'--keyfile',
f'../{self.signing_key}',
'bootloader-signed.bin']
'bootloader-signed.bin',
]
output = self.call_command(sign_command)
self.assertIn('verification successful', output)
def secure_generate_signing_key(self):
generate_key_command = [sys.executable,
generate_key_command = [
sys.executable,
idf_py_path,
'secure-generate-signing-key',
'--version',
'2',
'--scheme',
'rsa3072',
self.signing_key]
self.signing_key,
]
output = self.call_command(generate_key_command)
self.assertIn(f'RSA 3072 private key in PEM format written to {self.signing_key}', output)
self.assertRegex(output, f'RSA 3072 private key in PEM format written to "?{self.signing_key}"?')
def test_secure_generate_key_digest(self):
self.secure_generate_signing_key()
digest_command = [sys.executable,
digest_command = [
sys.executable,
idf_py_path,
'secure-generate-key-digest',
'--keyfile',
f'{self.signing_key}',
'--output',
'key_digest.bin']
'key_digest.bin',
]
output = self.call_command(digest_command)
self.assertIn(f'Writing the public key digest of {self.signing_key} to key_digest.bin', output)
self.assertRegex(output, f'Writing the public key digest of "?{self.signing_key}"? to "?key_digest.bin"?.')
def test_secure_generate_nvs_partition_key(self):
generate_key_command = [sys.executable,
generate_key_command = [
sys.executable,
idf_py_path,
'secure-generate-nvs-partition-key',
'--keyfile',
@@ -511,7 +568,8 @@ class TestSecureCommands(TestWrapperCommands):
'--encryption-scheme',
'HMAC',
'--hmac-keyfile',
'nvs_partition_key.bin']
'nvs_partition_key.bin',
]
output = self.call_command(generate_key_command)
self.assertIn('Created encryption keys:', output)
@@ -519,14 +577,15 @@ class TestSecureCommands(TestWrapperCommands):
class TestMergeBinCommands(TestWrapperCommands):
"""
Test if merge-bin command is invoked as expected.
This test is not testing the functionality of esptool.py merge_bin command, but the invocation of the command from idf.py.
This test is not testing the functionality of esptool.py merge_bin command, but the invocation of
the command from idf.py.
"""
def test_merge_bin(self):
merge_bin_command = [sys.executable, idf_py_path, 'merge-bin']
merged_binary_name = 'test-merge-binary.bin'
output = self.call_command(merge_bin_command + ['--output', merged_binary_name])
self.assertIn(f'file {merged_binary_name}, ready to flash to offset 0x0', output)
self.assertRegex(output, f"file '?{merged_binary_name}'?, ready to flash to offset 0x0")
self.assertIn(f'Merged binary {merged_binary_name} will be created in the build directory...', output)