"""Tests for esphome.build_gen.platformio module.""" from __future__ import annotations from collections.abc import Generator from pathlib import Path from unittest.mock import MagicMock, patch import pytest from esphome.build_gen import platformio from esphome.core import CORE @pytest.fixture def mock_write_file_if_changed() -> Generator[MagicMock]: """Mock write_file_if_changed for tests.""" with patch("esphome.build_gen.platformio.write_file_if_changed") as mock: yield mock def test_write_ini_creates_new_file(tmp_path: Path) -> None: """Test write_ini creates a new platformio.ini file.""" CORE.build_path = str(tmp_path) content = """ [env:test] platform = espressif32 board = esp32dev framework = arduino """ platformio.write_ini(content) ini_file = tmp_path / "platformio.ini" assert ini_file.exists() file_content = ini_file.read_text() assert content in file_content assert platformio.INI_AUTO_GENERATE_BEGIN in file_content assert platformio.INI_AUTO_GENERATE_END in file_content def test_write_ini_updates_existing_file(tmp_path: Path) -> None: """Test write_ini updates existing platformio.ini file.""" CORE.build_path = str(tmp_path) # Create existing file with custom content ini_file = tmp_path / "platformio.ini" existing_content = f""" ; Custom header [platformio] default_envs = test {platformio.INI_AUTO_GENERATE_BEGIN} ; Old auto-generated content [env:old] platform = old {platformio.INI_AUTO_GENERATE_END} ; Custom footer """ ini_file.write_text(existing_content) # New content to write new_content = """ [env:test] platform = espressif32 board = esp32dev framework = arduino """ platformio.write_ini(new_content) file_content = ini_file.read_text() # Check that custom parts are preserved assert "; Custom header" in file_content assert "[platformio]" in file_content assert "default_envs = test" in file_content assert "; Custom footer" in file_content # Check that new content replaced old auto-generated content assert new_content in file_content assert "[env:old]" not in file_content assert "platform = old" not in file_content def test_write_ini_preserves_custom_sections(tmp_path: Path) -> None: """Test write_ini preserves custom sections outside auto-generate markers.""" CORE.build_path = str(tmp_path) # Create existing file with multiple custom sections ini_file = tmp_path / "platformio.ini" existing_content = f""" [platformio] src_dir = . include_dir = . [common] lib_deps = Wire SPI {platformio.INI_AUTO_GENERATE_BEGIN} [env:old] platform = old {platformio.INI_AUTO_GENERATE_END} [env:custom] upload_speed = 921600 monitor_speed = 115200 """ ini_file.write_text(existing_content) new_content = "[env:auto]\nplatform = new" platformio.write_ini(new_content) file_content = ini_file.read_text() # All custom sections should be preserved assert "[platformio]" in file_content assert "src_dir = ." in file_content assert "[common]" in file_content assert "lib_deps" in file_content assert "[env:custom]" in file_content assert "upload_speed = 921600" in file_content # New auto-generated content should replace old assert "[env:auto]" in file_content assert "platform = new" in file_content assert "[env:old]" not in file_content def test_write_ini_no_change_when_content_same( tmp_path: Path, mock_write_file_if_changed: MagicMock, ) -> None: """Test write_ini doesn't rewrite file when content is unchanged.""" CORE.build_path = str(tmp_path) content = "[env:test]\nplatform = esp32" full_content = ( f"{platformio.INI_BASE_FORMAT[0]}" f"{platformio.INI_AUTO_GENERATE_BEGIN}\n" f"{content}" f"{platformio.INI_AUTO_GENERATE_END}" f"{platformio.INI_BASE_FORMAT[1]}" ) ini_file = tmp_path / "platformio.ini" ini_file.write_text(full_content) mock_write_file_if_changed.return_value = False # Indicate no change platformio.write_ini(content) # write_file_if_changed should be called with the same content mock_write_file_if_changed.assert_called_once() call_args = mock_write_file_if_changed.call_args[0] assert call_args[0] == ini_file assert content in call_args[1] @pytest.fixture def clean_core(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(CORE, "name", "test") monkeypatch.setattr(CORE, "platformio_options", {}) monkeypatch.setattr(CORE, "platformio_libraries", {}) monkeypatch.setattr(CORE, "build_flags", set()) monkeypatch.setattr(CORE, "build_unflags", set()) def test_get_ini_content_pins_cpp_standard( clean_core: None, monkeypatch: pytest.MonkeyPatch ) -> None: """cg.set_cpp_standard() pins -std via build_flags and unflags every other known standard so the platform/framework default is stripped.""" monkeypatch.setattr(CORE, "cpp_standard", "gnu++20") content = platformio.get_ini_content() flags_section = content.split("build_flags =")[1].split("build_unflags =")[0] unflags_section = content.split("build_unflags =")[1].split("extra_scripts")[0] assert "-std=gnu++20\n" in flags_section # Both the GNU and strict dialects of every other standard are stripped. for year in ("11", "14", "17", "23", "26", "2a", "2b", "2c"): assert f"-std=gnu++{year}\n" in unflags_section assert f"-std=c++{year}\n" in unflags_section assert "-std=c++20\n" in unflags_section # The selected standard must not unflag itself. assert "-std=gnu++20\n" not in unflags_section def test_get_ini_content_no_cpp_standard( clean_core: None, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setattr(CORE, "cpp_standard", None) content = platformio.get_ini_content() assert "-std=" not in content