"""nrf52 sdk-nrf build: the hex file used for the Adafruit UF2 and DFU package.""" from collections.abc import Iterator from pathlib import Path from unittest.mock import Mock, patch import pytest from esphome.components import nrf52 from esphome.components.nrf52.const import BOOTLOADER_ADAFRUIT_NRF52_SD140_V7 from esphome.components.zephyr.const import KEY_BOARD, KEY_BOOTLOADER import esphome.config_validation as cv from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION, Toolchain from esphome.core import CORE @pytest.fixture def build_dir(tmp_path: Path) -> Path: CORE.config_path = tmp_path / "test.yaml" CORE.build_path = tmp_path / "build" CORE.name = "livingroom" CORE.toolchain = Toolchain.SDK_NRF return CORE.relative_pioenvs_path(CORE.name) @pytest.fixture def run_cmd(tmp_path: Path) -> Iterator[Mock]: with ( patch.object(nrf52, "check_and_install"), patch.object(nrf52, "_generate_cmake_lists", return_value=False), patch.object( nrf52, "get_build_paths", return_value={"python_executable": "python3", "framework_path": tmp_path}, ), patch.object(nrf52, "get_build_env", return_value={}), patch.object( nrf52, "zephyr_data", return_value={ KEY_BOARD: "board", KEY_BOOTLOADER: BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, }, ), patch.object(nrf52.pch, "pch_enabled", return_value=False), patch.object(nrf52, "run_command_ok", return_value=True) as run, ): yield run def _uf2_hex(run_cmd: Mock) -> str: cmd = next(c.args[0] for c in run_cmd.call_args_list if "-f" in c.args[0]) return cmd[-1] def _dfu_hex(run_cmd: Mock) -> str: cmd = next(c.args[0] for c in run_cmd.call_args_list if "genpkg" in c.args[0]) return cmd[cmd.index("--application") + 1] def _build( build_dir: Path, version: cv.Version, outputs: list[str], run_cmd: Mock, leftovers: tuple[str, ...] = (), ) -> None: """Run run_compile; *outputs* appear when west builds, *leftovers* before.""" CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: version} (build_dir / "zephyr" / "zephyr").mkdir(parents=True) # Without a CMake cache run_compile wipes the build dir first for name in ["CMakeCache.txt", *leftovers]: (build_dir / name).write_text("") def west_build(cmd: list[str], **kwargs: object) -> bool: if "west" in cmd: for name in outputs: (build_dir / name).write_text("") return True run_cmd.side_effect = west_build nrf52.run_compile(None, {}) def test_uf2_and_dfu_use_merged_hex(build_dir: Path, run_cmd: Mock) -> None: _build( build_dir, cv.Version(2, 9, 2), ["merged.hex", "zephyr/zephyr/zephyr.hex"], run_cmd, ) merged = str(build_dir / "zephyr" / "merged.hex") assert _uf2_hex(run_cmd) == merged assert _dfu_hex(run_cmd) == merged def test_uf2_and_dfu_fall_back_to_zephyr_hex(build_dir: Path, run_cmd: Mock) -> None: """The nRF Connect SDK 3.4.0 build produces no merged.hex.""" _build(build_dir, cv.Version(3, 4, 0), ["zephyr/zephyr/zephyr.hex"], run_cmd) app_hex = build_dir / "zephyr" / "zephyr.hex" assert app_hex.is_file() assert _uf2_hex(run_cmd) == str(app_hex) assert _dfu_hex(run_cmd) == str(app_hex) def test_older_sdk_does_not_fall_back_to_zephyr_hex( build_dir: Path, run_cmd: Mock ) -> None: """Before SDK 3.4.0 a missing merged.hex must not be replaced by zephyr.hex.""" _build(build_dir, cv.Version(3, 2, 0), ["zephyr/zephyr/zephyr.hex"], run_cmd) assert not any("-f" in c.args[0] for c in run_cmd.call_args_list) assert _dfu_hex(run_cmd) == str(build_dir / "zephyr" / "merged.hex") def test_stale_merged_hex_is_removed_before_building( build_dir: Path, run_cmd: Mock ) -> None: """A merged.hex left by an older SDK build must not be packaged.""" _build( build_dir, cv.Version(3, 4, 0), ["zephyr/zephyr/zephyr.hex"], run_cmd, leftovers=("merged.hex", "zephyr/merged.hex"), ) assert not (build_dir / "merged.hex").exists() assert not (build_dir / "zephyr" / "merged.hex").exists() app_hex = str(build_dir / "zephyr" / "zephyr.hex") assert _uf2_hex(run_cmd) == app_hex assert _dfu_hex(run_cmd) == app_hex