Files
esphome/tests/unit_tests/conftest.py
T

209 lines
5.9 KiB
Python

"""
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")