From bf67b49104ef7575ba9214be3ef48cedaa79a265 Mon Sep 17 00:00:00 2001 From: Panoplian <153834574+albert-canfield@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:47:52 +0100 Subject: [PATCH] [i2c] Let host unit tests with I2C build on macOS (#20738) --- esphome/components/i2c/__init__.py | 21 +++++++++- tests/unit_tests/components/test_i2c.py | 51 +++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests/unit_tests/components/test_i2c.py diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 453c2513fa7..5bf64609f53 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -144,10 +144,15 @@ def validate_config(config: ConfigType) -> ConfigType: return config +def _host_is_linux() -> bool: + """The host I2C bus needs Linux i2c-dev.""" + return sys.platform.lower().startswith("linux") + + def validate_host_config(config: ConfigType) -> ConfigType: if CORE.is_host: # Host I2C is currently only supported on Linux - if not sys.platform.lower().startswith("linux"): + if not _host_is_linux(): raise cv.Invalid( "I2C is only supported on Linux for the host platform. " f"Current platform: {sys.platform}" @@ -446,7 +451,7 @@ def final_validate_device_schema( ) -FILTER_SOURCE_FILES = filter_source_files_from_platform( +_platform_filter = filter_source_files_from_platform( { "i2c_bus_arduino.cpp": { PlatformFramework.ESP8266_ARDUINO, @@ -463,3 +468,15 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( "i2c_bus_host.cpp": {PlatformFramework.HOST_NATIVE}, } ) + + +def FILTER_SOURCE_FILES() -> list[str]: + excluded = _platform_filter() + # An empty list means the target isn't known yet. Otherwise the host bus is + # only kept for the host platform, and it needs Linux i2c-dev + # (validate_host_config rejects host I2C elsewhere). Leaving it out lets host + # builds without a bus, like the C++ unit tests with their fake bus, build on + # macOS too. + if excluded and "i2c_bus_host.cpp" not in excluded and not _host_is_linux(): + excluded.append("i2c_bus_host.cpp") + return excluded diff --git a/tests/unit_tests/components/test_i2c.py b/tests/unit_tests/components/test_i2c.py new file mode 100644 index 00000000000..91304d69cf9 --- /dev/null +++ b/tests/unit_tests/components/test_i2c.py @@ -0,0 +1,51 @@ +"""I2C backend source selection.""" + +from unittest.mock import patch + +import pytest + +from esphome.components import i2c +from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM + + +@pytest.mark.parametrize( + "build_os, host_bus_built", + [ + ("linux", True), + ("darwin", False), + ], +) +def test_host_bus_is_only_built_on_linux(build_os: str, host_bus_built: bool) -> None: + core_data = {KEY_CORE: {KEY_TARGET_PLATFORM: "host", KEY_TARGET_FRAMEWORK: "host"}} + with ( + patch("esphome.config_helpers.CORE.data", core_data), + patch.object(i2c.sys, "platform", build_os), + ): + excluded = i2c.FILTER_SOURCE_FILES() + assert ("i2c_bus_host.cpp" not in excluded) is host_bus_built + assert {"i2c_bus_arduino.cpp", "i2c_bus_esp_idf.cpp", "i2c_bus_zephyr.cpp"} <= set( + excluded + ) + + +@pytest.mark.parametrize("build_os", ["linux", "darwin"]) +def test_nothing_excluded_before_the_target_is_known(build_os: str) -> None: + with ( + patch("esphome.config_helpers.CORE.data", {}), + patch.object(i2c.sys, "platform", build_os), + ): + assert i2c.FILTER_SOURCE_FILES() == [] + + +@pytest.mark.parametrize("build_os", ["linux", "darwin"]) +def test_host_bus_left_out_for_other_targets(build_os: str) -> None: + core_data = { + KEY_CORE: {KEY_TARGET_PLATFORM: "esp32", KEY_TARGET_FRAMEWORK: "esp-idf"} + } + with ( + patch("esphome.config_helpers.CORE.data", core_data), + patch.object(i2c.sys, "platform", build_os), + ): + excluded = i2c.FILTER_SOURCE_FILES() + assert excluded.count("i2c_bus_host.cpp") == 1 + assert "i2c_bus_esp_idf.cpp" not in excluded