Files
esphome/tests/unit_tests/test_nrf52_framework.py
2026-06-17 21:17:07 -04:00

229 lines
8.5 KiB
Python

"""Tests for esphome.components.nrf52.framework helpers."""
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from esphome.components.nrf52.framework import (
_TOOLCHAIN_VERSION,
_get_toolchain_platform_info,
check_and_install,
)
from esphome.config_validation import Version
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION
from esphome.core import CORE, EsphomeError
@pytest.mark.parametrize(
("system", "machine", "expected"),
[
# default — no branch hit
("Linux", "x86_64", ("linux", "x86_64", "tar.xz")),
# arm64 → aarch64 rename
("Linux", "arm64", ("linux", "aarch64", "tar.xz")),
# darwin → macos rename only
("Darwin", "x86_64", ("macos", "x86_64", "tar.xz")),
# both renames apply
("Darwin", "arm64", ("macos", "aarch64", "tar.xz")),
# windows forces x86_64 + 7z; arm64 rename is overwritten
("Windows", "arm64", ("windows", "x86_64", "7z")),
],
)
def test_get_toolchain_platform_info(
system: str, machine: str, expected: tuple[str, str, str]
) -> None:
with (
patch("platform.system", return_value=system),
patch("platform.machine", return_value=machine),
):
assert _get_toolchain_platform_info() == expected
# ---------------------------------------------------------------------------
# Helpers and fixtures for check_and_install tests
# ---------------------------------------------------------------------------
_TEST_SDK_VERSION = "2.9.0"
@pytest.fixture
def nrf52_dirs(setup_core: Path) -> SimpleNamespace:
"""Populate CORE and pre-create SDK directories so sentinel.touch() succeeds."""
CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: Version.parse(_TEST_SDK_VERSION)}
tools = CORE.data_dir / "sdk-nrf"
python_env = tools / "penvs" / f"v{_TEST_SDK_VERSION}"
framework = tools / "frameworks" / f"v{_TEST_SDK_VERSION}"
toolchain_dir = tools / "toolchains" / _TOOLCHAIN_VERSION
for d in (python_env, framework, toolchain_dir):
d.mkdir(parents=True, exist_ok=True)
zephyr_scripts = framework / "zephyr" / "scripts"
zephyr_scripts.mkdir(parents=True, exist_ok=True)
(zephyr_scripts / "requirements.txt").touch()
return SimpleNamespace(
python_env=python_env,
framework=framework,
toolchain=toolchain_dir,
)
@pytest.fixture
def mock_nrf52_ops():
"""Patch all heavy I/O operations used by check_and_install."""
with (
patch("esphome.components.nrf52.framework.rmdir") as mock_rmdir,
patch("esphome.components.nrf52.framework.create_venv") as mock_create_venv,
patch(
"esphome.components.nrf52.framework.run_command_ok", return_value=True
) as mock_run_cmd,
patch(
"esphome.components.nrf52.framework.download_from_mirrors",
return_value="https://example.com/tc.tar.xz",
) as mock_download,
patch("esphome.components.nrf52.framework.archive_extract_all") as mock_extract,
):
yield SimpleNamespace(
rmdir=mock_rmdir,
create_venv=mock_create_venv,
run_command_ok=mock_run_cmd,
download_from_mirrors=mock_download,
archive_extract_all=mock_extract,
)
# ---------------------------------------------------------------------------
# check_and_install tests
# ---------------------------------------------------------------------------
class TestCheckAndInstall:
def test_all_installed_skips_all_steps(
self,
nrf52_dirs: SimpleNamespace,
mock_nrf52_ops: SimpleNamespace,
) -> None:
"""All three sentinels present → nothing downloaded or compiled."""
(nrf52_dirs.python_env / ".ready").touch()
(nrf52_dirs.python_env / ".zephyr_reqs_ready").touch()
(nrf52_dirs.framework / ".ready").touch()
(nrf52_dirs.toolchain / ".ready").touch()
check_and_install()
mock_nrf52_ops.create_venv.assert_not_called()
mock_nrf52_ops.run_command_ok.assert_not_called()
mock_nrf52_ops.download_from_mirrors.assert_not_called()
mock_nrf52_ops.archive_extract_all.assert_not_called()
def test_fresh_install_runs_all_steps(
self,
nrf52_dirs: SimpleNamespace,
mock_nrf52_ops: SimpleNamespace,
) -> None:
"""No sentinels → venv created, west installed, SDK init+update, toolchain downloaded."""
check_and_install()
mock_nrf52_ops.create_venv.assert_called_once()
# pip install requirements, west init, west update, pip install zephyr reqs
assert mock_nrf52_ops.run_command_ok.call_count == 4
# minimal SDK + per-arch toolchain
assert mock_nrf52_ops.download_from_mirrors.call_count == 2
assert mock_nrf52_ops.archive_extract_all.call_count == 2
assert (nrf52_dirs.python_env / ".ready").exists()
assert (nrf52_dirs.python_env / ".zephyr_reqs_ready").exists()
assert (nrf52_dirs.framework / ".ready").exists()
assert (nrf52_dirs.toolchain / ".ready").exists()
def test_venv_exists_installs_framework_and_toolchain(
self,
nrf52_dirs: SimpleNamespace,
mock_nrf52_ops: SimpleNamespace,
) -> None:
"""Venv ready but framework missing → skip venv creation, run SDK init+update."""
(nrf52_dirs.python_env / ".ready").touch()
check_and_install()
mock_nrf52_ops.create_venv.assert_not_called()
# west init, west update, pip install zephyr reqs
assert mock_nrf52_ops.run_command_ok.call_count == 3
# minimal SDK + per-arch toolchain
assert mock_nrf52_ops.download_from_mirrors.call_count == 2
def test_toolchain_only_missing(
self,
nrf52_dirs: SimpleNamespace,
mock_nrf52_ops: SimpleNamespace,
) -> None:
"""Venv and framework ready → only toolchain downloaded and extracted."""
(nrf52_dirs.python_env / ".ready").touch()
(nrf52_dirs.python_env / ".zephyr_reqs_ready").touch()
(nrf52_dirs.framework / ".ready").touch()
check_and_install()
mock_nrf52_ops.create_venv.assert_not_called()
mock_nrf52_ops.run_command_ok.assert_not_called()
# minimal SDK + per-arch toolchain
assert mock_nrf52_ops.download_from_mirrors.call_count == 2
assert mock_nrf52_ops.archive_extract_all.call_count == 2
def test_requirements_install_failure_raises(
self,
nrf52_dirs: SimpleNamespace,
mock_nrf52_ops: SimpleNamespace,
) -> None:
"""Failing pip install -r requirements.txt raises EsphomeError."""
mock_nrf52_ops.run_command_ok.return_value = False
with pytest.raises(EsphomeError, match="Install requirements"):
check_and_install()
def test_framework_init_failure_raises(
self,
nrf52_dirs: SimpleNamespace,
mock_nrf52_ops: SimpleNamespace,
) -> None:
"""Failing west init raises EsphomeError."""
(nrf52_dirs.python_env / ".ready").touch()
mock_nrf52_ops.run_command_ok.return_value = False
with pytest.raises(EsphomeError, match="Can't initialize"):
check_and_install()
def test_framework_update_failure_raises(
self,
nrf52_dirs: SimpleNamespace,
mock_nrf52_ops: SimpleNamespace,
) -> None:
"""Failing west update raises EsphomeError."""
(nrf52_dirs.python_env / ".ready").touch()
# init succeeds, update fails
mock_nrf52_ops.run_command_ok.side_effect = [True, False]
with pytest.raises(EsphomeError, match="Can't update"):
check_and_install()
def test_toolchain_download_passes_platform_substitutions(
self,
nrf52_dirs: SimpleNamespace,
mock_nrf52_ops: SimpleNamespace,
) -> None:
"""download_from_mirrors receives VERSION + platform triple from _get_toolchain_platform_info."""
(nrf52_dirs.python_env / ".ready").touch()
(nrf52_dirs.framework / ".ready").touch()
with patch(
"esphome.components.nrf52.framework._get_toolchain_platform_info",
return_value=("linux", "x86_64", "tar.xz"),
):
check_and_install()
args, _ = mock_nrf52_ops.download_from_mirrors.call_args
substitutions = args[1]
assert substitutions["VERSION"] == _TOOLCHAIN_VERSION
assert substitutions["sysname"] == "linux"
assert substitutions["machine"] == "x86_64"
assert substitutions["extension"] == "tar.xz"