mirror of
https://github.com/esphome/esphome.git
synced 2026-10-05 10:31:29 +00:00
[espidf] Add --skip-bootloader for OTA only builds (#19925)
This commit is contained in:
@@ -8,11 +8,16 @@ import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from unittest.mock import call, patch
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_IDF_VERSION, KEY_VARIANT
|
||||
from esphome.components.esp32.const import (
|
||||
KEY_ESP32,
|
||||
KEY_FLASH_SIZE,
|
||||
KEY_IDF_VERSION,
|
||||
KEY_VARIANT,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_COMPILE_PROCESS_LIMIT,
|
||||
@@ -893,6 +898,7 @@ def test_run_reconfigure_cmake_argv_matches_idf_py(setup_core: Path) -> None:
|
||||
"-DESP_PLATFORM=1",
|
||||
f"-DSDKCONFIG={sdkconfig}",
|
||||
"-DCCACHE_ENABLE=0",
|
||||
"-DESPHOME_SKIP_BOOTLOADER=0",
|
||||
project,
|
||||
]
|
||||
kwargs = mock_run.call_args.kwargs
|
||||
@@ -927,6 +933,7 @@ def test_run_reconfigure_cmake_argv_matches_idf6_py(
|
||||
"-DPYTHON=/tools/python",
|
||||
"-DESP_PLATFORM=1",
|
||||
f"-DCCACHE_ENABLE={expected}",
|
||||
"-DESPHOME_SKIP_BOOTLOADER=0",
|
||||
str(build_dir.parent),
|
||||
]
|
||||
|
||||
@@ -938,7 +945,8 @@ def test_run_reconfigure_without_sdkconfig_or_filter(setup_core: Path) -> None:
|
||||
assert toolchain.run_reconfigure(verbose=True) == 0
|
||||
cmd = mock_run.call_args.args[0]
|
||||
assert not any(arg.startswith("-DSDKCONFIG=") for arg in cmd)
|
||||
assert cmd[-2] == "-DCCACHE_ENABLE=0"
|
||||
assert cmd[-3] == "-DCCACHE_ENABLE=0"
|
||||
assert cmd[-2] == "-DESPHOME_SKIP_BOOTLOADER=0"
|
||||
assert mock_run.call_args.kwargs["filter_lines"] is None
|
||||
|
||||
|
||||
@@ -1005,6 +1013,7 @@ _CONFIGURED = (
|
||||
"PYTHON_DEPS_CHECKED:UNINITIALIZED=1\n"
|
||||
"PYTHON:UNINITIALIZED=/tools/python\n"
|
||||
"ESP_PLATFORM:UNINITIALIZED=1\n"
|
||||
"ESPHOME_SKIP_BOOTLOADER:UNINITIALIZED=0\n"
|
||||
)
|
||||
|
||||
|
||||
@@ -1256,3 +1265,206 @@ def test_get_cmake_cache_value_reads_the_configured_cache(setup_core: Path) -> N
|
||||
with patch.object(toolchain, "_get_cmake_output", return_value=output):
|
||||
assert toolchain.get_cmake_cache_value("ESPHOME_PCH") == "OFF"
|
||||
assert toolchain.get_cmake_cache_value("ESPHOME_MISSING") is None
|
||||
|
||||
|
||||
def test_skip_bootloader_requires_flag_and_matching_macro(
|
||||
setup_core: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""The switch is explicit, and an IDF macro change wins over it."""
|
||||
from esphome.build_gen import espidf as build_gen
|
||||
|
||||
assert toolchain._skip_bootloader() is False
|
||||
CORE.skip_bootloader = True
|
||||
toolchain._cache().skip_bootloader = None # decision is memoized per run
|
||||
with (
|
||||
patch.object(toolchain, "_get_idf_path", return_value=Path("/idf")),
|
||||
patch.object(build_gen, "idf_macro_matches", return_value=True),
|
||||
):
|
||||
assert toolchain._skip_bootloader() is True
|
||||
toolchain._cache().skip_bootloader = None
|
||||
with (
|
||||
patch.object(toolchain, "_get_idf_path", return_value=Path("/idf")),
|
||||
patch.object(build_gen, "idf_macro_matches", return_value=False) as mock_match,
|
||||
caplog.at_level("WARNING"),
|
||||
):
|
||||
assert toolchain._skip_bootloader() is False
|
||||
assert toolchain._skip_bootloader() is False
|
||||
mock_match.assert_called_once() # the memo also dedupes the warning
|
||||
assert "--skip-bootloader ignored" in caplog.text
|
||||
|
||||
|
||||
def test_configure_defines_follow_skip_bootloader(setup_core: Path) -> None:
|
||||
with (
|
||||
patch.object(toolchain, "_get_idf_tool", return_value="/tools/python"),
|
||||
patch.object(toolchain, "_cache_entries", return_value={}),
|
||||
):
|
||||
with patch.object(toolchain, "_skip_bootloader", return_value=True):
|
||||
assert (
|
||||
toolchain._configure_defines()[toolchain.SKIP_BOOTLOADER_DEFINE] == "1"
|
||||
)
|
||||
with patch.object(toolchain, "_skip_bootloader", return_value=False):
|
||||
assert (
|
||||
toolchain._configure_defines()[toolchain.SKIP_BOOTLOADER_DEFINE] == "0"
|
||||
)
|
||||
|
||||
|
||||
def test_tree_skips_bootloader_reads_the_define(tmp_path: Path) -> None:
|
||||
build = tmp_path / "build"
|
||||
build.mkdir()
|
||||
assert toolchain.tree_skips_bootloader(build) is False # not configured
|
||||
cache = build / "CMakeCache.txt"
|
||||
cache.write_text(f"{toolchain.SKIP_BOOTLOADER_DEFINE}:UNINITIALIZED=1\n")
|
||||
assert toolchain.tree_skips_bootloader(build) is True
|
||||
cache.write_text(f"{toolchain.SKIP_BOOTLOADER_DEFINE}:UNINITIALIZED=0\n")
|
||||
assert toolchain.tree_skips_bootloader(build) is False
|
||||
|
||||
|
||||
def test_tree_skips_bootloader_unreadable_cache_reads_full(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Unreadable trees fall back to the safe full build, with a trace."""
|
||||
build = tmp_path / "build"
|
||||
(build / "CMakeCache.txt").mkdir(parents=True) # read raises OSError
|
||||
with caplog.at_level("DEBUG"):
|
||||
assert toolchain.tree_skips_bootloader(build) is False
|
||||
assert "assuming a full build" in caplog.text
|
||||
|
||||
|
||||
def test_create_factory_bin_merges_the_listed_flash_files(
|
||||
setup_core: Path,
|
||||
) -> None:
|
||||
"""Every listed file lands in the esptool merge argv, address sorted."""
|
||||
_setup_build(setup_core)
|
||||
CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = "4MB"
|
||||
build = CORE.relative_build_path("build")
|
||||
build.mkdir(parents=True)
|
||||
(build / "CMakeCache.txt").write_text(
|
||||
f"{toolchain.SKIP_BOOTLOADER_DEFINE}:UNINITIALIZED=0\n"
|
||||
)
|
||||
(build / "app.bin").write_bytes(b"app")
|
||||
(build / "boot.bin").write_bytes(b"boot")
|
||||
(build / "flasher_args.json").write_text(
|
||||
'{"flash_files": {"0x10000": "app.bin", "0x0": "boot.bin"},'
|
||||
' "extra_esptool_args": {"chip": "esp32"}}'
|
||||
)
|
||||
with (
|
||||
patch.object(toolchain, "_get_idf_env", return_value={}),
|
||||
patch.object(toolchain, "_get_idf_tool", return_value="/py"),
|
||||
patch.object(
|
||||
toolchain.subprocess, "run", return_value=MagicMock(returncode=0)
|
||||
) as mock_run,
|
||||
):
|
||||
assert toolchain.create_factory_bin() is True
|
||||
argv = mock_run.call_args.args[0]
|
||||
boot = argv.index("0x0")
|
||||
assert argv[boot + 1].endswith("boot.bin")
|
||||
assert argv[boot + 2] == "0x10000"
|
||||
|
||||
|
||||
def test_create_factory_bin_fails_on_a_missing_listed_flash_file(
|
||||
setup_core: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""A partial factory image would not boot; never write one."""
|
||||
_setup_build(setup_core)
|
||||
CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = "4MB"
|
||||
build = CORE.relative_build_path("build")
|
||||
build.mkdir(parents=True)
|
||||
(build / "CMakeCache.txt").write_text(
|
||||
f"{toolchain.SKIP_BOOTLOADER_DEFINE}:UNINITIALIZED=0\n"
|
||||
)
|
||||
(build / "flasher_args.json").write_text('{"flash_files": {"0x0": "missing.bin"}}')
|
||||
stale = toolchain.get_factory_firmware_path()
|
||||
stale.write_bytes(b"old")
|
||||
with patch.object(toolchain.subprocess, "run") as mock_run:
|
||||
assert toolchain.create_factory_bin() is False
|
||||
mock_run.assert_not_called()
|
||||
assert "Flash file not found" in caplog.text
|
||||
# The image from an earlier build must not be served as this one.
|
||||
assert not stale.exists()
|
||||
|
||||
|
||||
def test_create_factory_bin_skip_mode_creates_nothing(
|
||||
setup_core: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""The flip into skip mode already cleaned up; the merge just no-ops."""
|
||||
_setup_build(setup_core)
|
||||
build = CORE.relative_build_path("build")
|
||||
build.mkdir(parents=True)
|
||||
(build / "CMakeCache.txt").write_text(
|
||||
f"{toolchain.SKIP_BOOTLOADER_DEFINE}:UNINITIALIZED=1\n"
|
||||
)
|
||||
with (
|
||||
patch.object(toolchain.subprocess, "run") as mock_run,
|
||||
caplog.at_level("INFO"),
|
||||
):
|
||||
assert toolchain.create_factory_bin() is True
|
||||
mock_run.assert_not_called()
|
||||
assert "no factory image" in caplog.text
|
||||
|
||||
|
||||
def test_run_reconfigure_flip_into_skip_mode_cleans_up(setup_core: Path) -> None:
|
||||
"""Full-mode leftovers are stale for OTA --bootloader and downloads, and
|
||||
a partial cleanup would poison the flip back to full mode."""
|
||||
_setup_build(setup_core)
|
||||
build = CORE.relative_build_path("build")
|
||||
(build / "bootloader").mkdir(parents=True)
|
||||
(build / "bootloader" / "bootloader.bin").write_bytes(b"old")
|
||||
# bootloader-prefix deliberately absent: cleanup skips what is not there.
|
||||
stale = toolchain.get_factory_firmware_path()
|
||||
stale.parent.mkdir(parents=True, exist_ok=True)
|
||||
stale.write_bytes(b"old")
|
||||
with (
|
||||
patch.object(toolchain, "_skip_bootloader", return_value=True),
|
||||
patch.object(toolchain, "_get_idf_tool", side_effect=lambda n: f"/tools/{n}"),
|
||||
patch.object(toolchain, "_tool_env", return_value={}),
|
||||
patch.object(toolchain, "run_build_tool", return_value=0),
|
||||
patch.object(toolchain, "_idf_py") as mock_idf_py,
|
||||
):
|
||||
mock_idf_py.return_value.binary_dir_arg = False
|
||||
assert toolchain.run_reconfigure() == 0
|
||||
assert not (build / "bootloader").exists()
|
||||
assert not stale.exists()
|
||||
|
||||
|
||||
def test_run_reconfigure_skip_steady_state_cleans_nothing(setup_core: Path) -> None:
|
||||
"""Cleanup belongs to the flip; a reconfigure of a skip tree touches nothing."""
|
||||
_setup_build(setup_core)
|
||||
build = CORE.relative_build_path("build")
|
||||
build.mkdir(parents=True)
|
||||
(build / "CMakeCache.txt").write_text(
|
||||
f"{toolchain.SKIP_BOOTLOADER_DEFINE}:UNINITIALIZED=1\n"
|
||||
)
|
||||
marker = build / "bootloader"
|
||||
marker.mkdir()
|
||||
with (
|
||||
patch.object(toolchain, "_skip_bootloader", return_value=True),
|
||||
patch.object(toolchain, "_get_idf_tool", side_effect=lambda n: f"/tools/{n}"),
|
||||
patch.object(toolchain, "_tool_env", return_value={}),
|
||||
patch.object(toolchain, "run_build_tool", return_value=0),
|
||||
patch.object(toolchain, "_idf_py") as mock_idf_py,
|
||||
):
|
||||
mock_idf_py.return_value.binary_dir_arg = False
|
||||
assert toolchain.run_reconfigure() == 0
|
||||
assert marker.exists()
|
||||
|
||||
|
||||
def test_create_factory_bin_full_mode_needs_flasher_args(setup_core: Path) -> None:
|
||||
"""Past the skip gate, a full tree without flasher_args fails as before."""
|
||||
_setup_build(setup_core)
|
||||
build = CORE.relative_build_path("build")
|
||||
build.mkdir(parents=True)
|
||||
(build / "CMakeCache.txt").write_text(
|
||||
f"{toolchain.SKIP_BOOTLOADER_DEFINE}:UNINITIALIZED=0\n"
|
||||
)
|
||||
assert toolchain.create_factory_bin() is False
|
||||
|
||||
|
||||
def test_missing_image_hint_names_the_flag(setup_core: Path) -> None:
|
||||
_setup_build(setup_core)
|
||||
build = CORE.relative_build_path("build")
|
||||
build.mkdir(parents=True)
|
||||
assert toolchain.missing_image_hint() is None # stock tree
|
||||
(build / "CMakeCache.txt").write_text(
|
||||
f"{toolchain.SKIP_BOOTLOADER_DEFINE}:UNINITIALIZED=1\n"
|
||||
)
|
||||
assert "--skip-bootloader" in toolchain.missing_image_hint()
|
||||
|
||||
Reference in New Issue
Block a user