""" ESPHome Unittests ~~~~~~~~~~~~~~~~~ Configuration file for unit tests. If adding unit tests ensure that they are fast. Slower integration tests should not be part of a unit test suite. """ from collections.abc import Callable, Generator import os from pathlib import Path import sys from unittest.mock import Mock, patch import pytest from esphome.core import CORE from esphome.espidf import toolchain here = Path(__file__).parent # Configure location of package root package_root = here.parent.parent sys.path.insert(0, package_root.as_posix()) @pytest.fixture(autouse=True) def _no_idf_install(monkeypatch: pytest.MonkeyPatch) -> None: """Fail any test that would install ESP-IDF for real. An unmocked ``_get_idf_env`` reaches ``check_esp_idf_install``, which downloads the framework on a bare CI runner (minutes on Windows). The install path's own tests call it through ``esphome.espidf.framework``. """ def refuse(*args: object, **kwargs: object) -> None: raise AssertionError("test would install ESP-IDF; mock _get_idf_env") monkeypatch.setattr(toolchain, "check_esp_idf_install", refuse) @pytest.fixture(autouse=True) def reset_core(): """Reset CORE after each test.""" yield CORE.reset() @pytest.fixture def fixture_path() -> Path: """ Location of all fixture files. """ return here / "fixtures" @pytest.fixture def probe_env() -> dict[str, str]: """Environment for running fixture probe scripts as subprocesses. Running a script file drops the cwd from sys.path, so prepend the repo root for the child. """ python_path = str(package_root) if ambient := os.environ.get("PYTHONPATH"): python_path = os.pathsep.join((python_path, ambient)) return os.environ | {"PYTHONPATH": python_path} @pytest.fixture def setup_core(tmp_path: Path) -> Path: """Set up CORE with test paths.""" CORE.config_path = tmp_path / "test.yaml" return tmp_path @pytest.fixture def mock_write_file_if_changed() -> Generator[Mock, None, None]: """Mock write_file_if_changed for storage_json.""" with patch("esphome.storage_json.write_file_if_changed") as mock: yield mock @pytest.fixture def mock_copy_file_if_changed() -> Generator[Mock, None, None]: """Mock copy_file_if_changed for core.config.""" with patch("esphome.core.config.copy_file_if_changed") as mock: mock.return_value = True yield mock @pytest.fixture def mock_run_platformio_cli() -> Generator[Mock, None, None]: """Mock run_platformio_cli for platformio toolchain.""" with patch("esphome.platformio.toolchain.run_platformio_cli") as mock: yield mock @pytest.fixture def mock_run_platformio_cli_run() -> Generator[Mock, None, None]: """Mock run_platformio_cli_run for platformio toolchain.""" with patch("esphome.platformio.toolchain.run_platformio_cli_run") as mock: yield mock @pytest.fixture def mock_esp32_decode_pc() -> Generator[Mock, None, None]: """Mock _decode_pc for esp32.""" with patch("esphome.components.esp32._decode_pc") as mock: yield mock @pytest.fixture def mock_esp8266_decode_pc() -> Generator[Mock, None, None]: """Mock _decode_pc for esp8266.""" with patch("esphome.components.esp8266._decode_pc") as mock: yield mock @pytest.fixture def mock_run_external_process() -> Generator[Mock, None, None]: """Mock run_external_process for platformio toolchain.""" with patch("esphome.platformio.toolchain.run_external_process") as mock: yield mock @pytest.fixture def mock_run_git_command() -> Generator[Mock, None, None]: """Mock run_git_command for git module.""" with patch("esphome.git.run_git_command") as mock: yield mock @pytest.fixture def mock_subprocess_run() -> Generator[Mock, None, None]: """Mock subprocess.run for testing.""" with patch("subprocess.run") as mock: yield mock @pytest.fixture def mock_get_idedata() -> Generator[Mock, None, None]: """Mock get_idedata for platformio toolchain.""" with patch("esphome.platformio.toolchain.get_idedata") as mock: yield mock @pytest.fixture def mock_get_component() -> Generator[Mock, None, None]: """Mock get_component for config module.""" with patch("esphome.config.get_component") as mock: yield mock @pytest.fixture def held_lock() -> Callable[..., Callable[..., None]]: """Factory for a ``FileLock.acquire`` fake held by another downloader. Each poll writes the next chunk to ``part`` (or runs it, for a callable) and raises ``Timeout``; when the chunks run out the part is removed, ``land()`` runs, and the acquire succeeds (also for any later job, so ``land`` must be idempotent). """ from filelock import Timeout def make( part: Path, chunks: list[bytes | Callable[[], None]], land: Callable[[], None], ) -> Callable[..., None]: polls = iter(chunks) def acquire(*args, **kwargs) -> None: try: chunk = next(polls) except StopIteration: part.unlink(missing_ok=True) land() return if callable(chunk): chunk() else: part.parent.mkdir(parents=True, exist_ok=True) part.write_bytes(chunk) raise Timeout("held") return acquire return make @pytest.fixture(autouse=True) def _default_pch_knobs(monkeypatch: pytest.MonkeyPatch) -> None: """The knob changes what the generators emit: no ambient value, and on, so the Windows runner asks no compiler.""" monkeypatch.setenv("ESPHOME_PCH_ENABLE", "1") @pytest.fixture def windows_gcc_rule(monkeypatch: pytest.MonkeyPatch) -> None: """Windows host, knob unset: the compiler version decides.""" from esphome.build_helpers import pch monkeypatch.delenv("ESPHOME_PCH_ENABLE") monkeypatch.setattr(pch.sys, "platform", "win32")