mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/dev' into integration
This commit is contained in:
		| @@ -1226,6 +1226,18 @@ def test_has_mqtt_logging_no_log_topic() -> None: | ||||
|     setup_core(config={}) | ||||
|     assert has_mqtt_logging() is False | ||||
|  | ||||
|     # Setup MQTT config with CONF_LOG_TOPIC but no CONF_LEVEL (regression test for #10771) | ||||
|     # This simulates the default configuration created by validate_config in the MQTT component | ||||
|     setup_core( | ||||
|         config={ | ||||
|             CONF_MQTT: { | ||||
|                 CONF_BROKER: "mqtt.local", | ||||
|                 CONF_LOG_TOPIC: {CONF_TOPIC: "esphome/debug"}, | ||||
|             } | ||||
|         } | ||||
|     ) | ||||
|     assert has_mqtt_logging() is True | ||||
|  | ||||
|  | ||||
| def test_has_mqtt() -> None: | ||||
|     """Test has_mqtt function.""" | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| """Tests for the wizard.py file.""" | ||||
|  | ||||
| import os | ||||
| from pathlib import Path | ||||
| from typing import Any | ||||
| from unittest.mock import MagicMock | ||||
|  | ||||
| import pytest | ||||
| from pytest import MonkeyPatch | ||||
|  | ||||
| from esphome.components.bk72xx.boards import BK72XX_BOARD_PINS | ||||
| from esphome.components.esp32.boards import ESP32_BOARD_PINS | ||||
| @@ -15,7 +18,7 @@ import esphome.wizard as wz | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def default_config(): | ||||
| def default_config() -> dict[str, Any]: | ||||
|     return { | ||||
|         "type": "basic", | ||||
|         "name": "test-name", | ||||
| @@ -28,7 +31,7 @@ def default_config(): | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def wizard_answers(): | ||||
| def wizard_answers() -> list[str]: | ||||
|     return [ | ||||
|         "test-node",  # Name of the node | ||||
|         "ESP8266",  # platform | ||||
| @@ -53,7 +56,9 @@ def test_sanitize_quotes_replaces_with_escaped_char(): | ||||
|     assert output_str == '\\"key\\": \\"value\\"' | ||||
|  | ||||
|  | ||||
| def test_config_file_fallback_ap_includes_descriptive_name(default_config): | ||||
| def test_config_file_fallback_ap_includes_descriptive_name( | ||||
|     default_config: dict[str, Any], | ||||
| ): | ||||
|     """ | ||||
|     The fallback AP should include the node and a descriptive name | ||||
|     """ | ||||
| @@ -67,7 +72,9 @@ def test_config_file_fallback_ap_includes_descriptive_name(default_config): | ||||
|     assert 'ssid: "Test Node Fallback Hotspot"' in config | ||||
|  | ||||
|  | ||||
| def test_config_file_fallback_ap_name_less_than_32_chars(default_config): | ||||
| def test_config_file_fallback_ap_name_less_than_32_chars( | ||||
|     default_config: dict[str, Any], | ||||
| ): | ||||
|     """ | ||||
|     The fallback AP name must be less than 32 chars. | ||||
|     Since it is composed of the node name and "Fallback Hotspot" this can be too long and needs truncating | ||||
| @@ -82,7 +89,7 @@ def test_config_file_fallback_ap_name_less_than_32_chars(default_config): | ||||
|     assert 'ssid: "A Very Long Name For This Node"' in config | ||||
|  | ||||
|  | ||||
| def test_config_file_should_include_ota(default_config): | ||||
| def test_config_file_should_include_ota(default_config: dict[str, Any]): | ||||
|     """ | ||||
|     The Over-The-Air update should be enabled by default | ||||
|     """ | ||||
| @@ -95,7 +102,9 @@ def test_config_file_should_include_ota(default_config): | ||||
|     assert "ota:" in config | ||||
|  | ||||
|  | ||||
| def test_config_file_should_include_ota_when_password_set(default_config): | ||||
| def test_config_file_should_include_ota_when_password_set( | ||||
|     default_config: dict[str, Any], | ||||
| ): | ||||
|     """ | ||||
|     The Over-The-Air update should be enabled when a password is set | ||||
|     """ | ||||
| @@ -109,7 +118,9 @@ def test_config_file_should_include_ota_when_password_set(default_config): | ||||
|     assert "ota:" in config | ||||
|  | ||||
|  | ||||
| def test_wizard_write_sets_platform(default_config, tmp_path, monkeypatch): | ||||
| def test_wizard_write_sets_platform( | ||||
|     default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch | ||||
| ): | ||||
|     """ | ||||
|     If the platform is not explicitly set, use "ESP8266" if the board is one of the ESP8266 boards | ||||
|     """ | ||||
| @@ -126,7 +137,7 @@ def test_wizard_write_sets_platform(default_config, tmp_path, monkeypatch): | ||||
|     assert "esp8266:" in generated_config | ||||
|  | ||||
|  | ||||
| def test_wizard_empty_config(tmp_path, monkeypatch): | ||||
| def test_wizard_empty_config(tmp_path: Path, monkeypatch: MonkeyPatch): | ||||
|     """ | ||||
|     The wizard should be able to create an empty configuration | ||||
|     """ | ||||
| @@ -146,7 +157,7 @@ def test_wizard_empty_config(tmp_path, monkeypatch): | ||||
|     assert generated_config == "" | ||||
|  | ||||
|  | ||||
| def test_wizard_upload_config(tmp_path, monkeypatch): | ||||
| def test_wizard_upload_config(tmp_path: Path, monkeypatch: MonkeyPatch): | ||||
|     """ | ||||
|     The wizard should be able to import an base64 encoded configuration | ||||
|     """ | ||||
| @@ -168,7 +179,7 @@ def test_wizard_upload_config(tmp_path, monkeypatch): | ||||
|  | ||||
|  | ||||
| def test_wizard_write_defaults_platform_from_board_esp8266( | ||||
|     default_config, tmp_path, monkeypatch | ||||
|     default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch | ||||
| ): | ||||
|     """ | ||||
|     If the platform is not explicitly set, use "ESP8266" if the board is one of the ESP8266 boards | ||||
| @@ -189,7 +200,7 @@ def test_wizard_write_defaults_platform_from_board_esp8266( | ||||
|  | ||||
|  | ||||
| def test_wizard_write_defaults_platform_from_board_esp32( | ||||
|     default_config, tmp_path, monkeypatch | ||||
|     default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch | ||||
| ): | ||||
|     """ | ||||
|     If the platform is not explicitly set, use "ESP32" if the board is one of the ESP32 boards | ||||
| @@ -210,7 +221,7 @@ def test_wizard_write_defaults_platform_from_board_esp32( | ||||
|  | ||||
|  | ||||
| def test_wizard_write_defaults_platform_from_board_bk72xx( | ||||
|     default_config, tmp_path, monkeypatch | ||||
|     default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch | ||||
| ): | ||||
|     """ | ||||
|     If the platform is not explicitly set, use "BK72XX" if the board is one of BK72XX boards | ||||
| @@ -231,7 +242,7 @@ def test_wizard_write_defaults_platform_from_board_bk72xx( | ||||
|  | ||||
|  | ||||
| def test_wizard_write_defaults_platform_from_board_ln882x( | ||||
|     default_config, tmp_path, monkeypatch | ||||
|     default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch | ||||
| ): | ||||
|     """ | ||||
|     If the platform is not explicitly set, use "LN882X" if the board is one of LN882X boards | ||||
| @@ -252,7 +263,7 @@ def test_wizard_write_defaults_platform_from_board_ln882x( | ||||
|  | ||||
|  | ||||
| def test_wizard_write_defaults_platform_from_board_rtl87xx( | ||||
|     default_config, tmp_path, monkeypatch | ||||
|     default_config: dict[str, Any], tmp_path: Path, monkeypatch: MonkeyPatch | ||||
| ): | ||||
|     """ | ||||
|     If the platform is not explicitly set, use "RTL87XX" if the board is one of RTL87XX boards | ||||
| @@ -272,7 +283,7 @@ def test_wizard_write_defaults_platform_from_board_rtl87xx( | ||||
|     assert "rtl87xx:" in generated_config | ||||
|  | ||||
|  | ||||
| def test_safe_print_step_prints_step_number_and_description(monkeypatch): | ||||
| def test_safe_print_step_prints_step_number_and_description(monkeypatch: MonkeyPatch): | ||||
|     """ | ||||
|     The safe_print_step function prints the step number and the passed description | ||||
|     """ | ||||
| @@ -296,7 +307,7 @@ def test_safe_print_step_prints_step_number_and_description(monkeypatch): | ||||
|     assert any(f"STEP {step_num}" in arg for arg in all_args) | ||||
|  | ||||
|  | ||||
| def test_default_input_uses_default_if_no_input_supplied(monkeypatch): | ||||
| def test_default_input_uses_default_if_no_input_supplied(monkeypatch: MonkeyPatch): | ||||
|     """ | ||||
|     The default_input() function should return the supplied default value if the user doesn't enter anything | ||||
|     """ | ||||
| @@ -312,7 +323,7 @@ def test_default_input_uses_default_if_no_input_supplied(monkeypatch): | ||||
|     assert retval == default_string | ||||
|  | ||||
|  | ||||
| def test_default_input_uses_user_supplied_value(monkeypatch): | ||||
| def test_default_input_uses_user_supplied_value(monkeypatch: MonkeyPatch): | ||||
|     """ | ||||
|     The default_input() function should return the value that the user enters | ||||
|     """ | ||||
| @@ -376,7 +387,9 @@ def test_wizard_rejects_existing_files(tmpdir): | ||||
|     assert retval == 2 | ||||
|  | ||||
|  | ||||
| def test_wizard_accepts_default_answers_esp8266(tmpdir, monkeypatch, wizard_answers): | ||||
| def test_wizard_accepts_default_answers_esp8266( | ||||
|     tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str] | ||||
| ): | ||||
|     """ | ||||
|     The wizard should accept the given default answers for esp8266 | ||||
|     """ | ||||
| @@ -396,7 +409,9 @@ def test_wizard_accepts_default_answers_esp8266(tmpdir, monkeypatch, wizard_answ | ||||
|     assert retval == 0 | ||||
|  | ||||
|  | ||||
| def test_wizard_accepts_default_answers_esp32(tmpdir, monkeypatch, wizard_answers): | ||||
| def test_wizard_accepts_default_answers_esp32( | ||||
|     tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str] | ||||
| ): | ||||
|     """ | ||||
|     The wizard should accept the given default answers for esp32 | ||||
|     """ | ||||
| @@ -418,7 +433,9 @@ def test_wizard_accepts_default_answers_esp32(tmpdir, monkeypatch, wizard_answer | ||||
|     assert retval == 0 | ||||
|  | ||||
|  | ||||
| def test_wizard_offers_better_node_name(tmpdir, monkeypatch, wizard_answers): | ||||
| def test_wizard_offers_better_node_name( | ||||
|     tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str] | ||||
| ): | ||||
|     """ | ||||
|     When the node name does not conform, a better alternative is offered | ||||
|     * Removes special chars | ||||
| @@ -449,7 +466,9 @@ def test_wizard_offers_better_node_name(tmpdir, monkeypatch, wizard_answers): | ||||
|     assert wz.default_input.call_args.args[1] == expected_name | ||||
|  | ||||
|  | ||||
| def test_wizard_requires_correct_platform(tmpdir, monkeypatch, wizard_answers): | ||||
| def test_wizard_requires_correct_platform( | ||||
|     tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str] | ||||
| ): | ||||
|     """ | ||||
|     When the platform is not either esp32 or esp8266, the wizard should reject it | ||||
|     """ | ||||
| @@ -471,7 +490,9 @@ def test_wizard_requires_correct_platform(tmpdir, monkeypatch, wizard_answers): | ||||
|     assert retval == 0 | ||||
|  | ||||
|  | ||||
| def test_wizard_requires_correct_board(tmpdir, monkeypatch, wizard_answers): | ||||
| def test_wizard_requires_correct_board( | ||||
|     tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str] | ||||
| ): | ||||
|     """ | ||||
|     When the board is not a valid esp8266 board, the wizard should reject it | ||||
|     """ | ||||
| @@ -493,7 +514,9 @@ def test_wizard_requires_correct_board(tmpdir, monkeypatch, wizard_answers): | ||||
|     assert retval == 0 | ||||
|  | ||||
|  | ||||
| def test_wizard_requires_valid_ssid(tmpdir, monkeypatch, wizard_answers): | ||||
| def test_wizard_requires_valid_ssid( | ||||
|     tmpdir, monkeypatch: MonkeyPatch, wizard_answers: list[str] | ||||
| ): | ||||
|     """ | ||||
|     When the board is not a valid esp8266 board, the wizard should reject it | ||||
|     """ | ||||
| @@ -515,7 +538,9 @@ def test_wizard_requires_valid_ssid(tmpdir, monkeypatch, wizard_answers): | ||||
|     assert retval == 0 | ||||
|  | ||||
|  | ||||
| def test_wizard_write_protects_existing_config(tmpdir, default_config, monkeypatch): | ||||
| def test_wizard_write_protects_existing_config( | ||||
|     tmpdir, default_config: dict[str, Any], monkeypatch: MonkeyPatch | ||||
| ): | ||||
|     """ | ||||
|     The wizard_write function should not overwrite existing config files and return False | ||||
|     """ | ||||
|   | ||||
| @@ -369,9 +369,15 @@ def test_clean_build( | ||||
|     assert dependencies_lock.exists() | ||||
|     assert platformio_cache_dir.exists() | ||||
|  | ||||
|     # Call the function | ||||
|     with caplog.at_level("INFO"): | ||||
|         clean_build() | ||||
|     # Mock PlatformIO's get_project_cache_dir | ||||
|     with patch( | ||||
|         "platformio.project.helpers.get_project_cache_dir" | ||||
|     ) as mock_get_cache_dir: | ||||
|         mock_get_cache_dir.return_value = str(platformio_cache_dir) | ||||
|  | ||||
|         # Call the function | ||||
|         with caplog.at_level("INFO"): | ||||
|             clean_build() | ||||
|  | ||||
|     # Verify all were removed | ||||
|     assert not pioenvs_dir.exists() | ||||
| @@ -458,6 +464,86 @@ def test_clean_build_nothing_exists( | ||||
|     assert not dependencies_lock.exists() | ||||
|  | ||||
|  | ||||
| @patch("esphome.writer.CORE") | ||||
| def test_clean_build_platformio_not_available( | ||||
|     mock_core: MagicMock, | ||||
|     tmp_path: Path, | ||||
|     caplog: pytest.LogCaptureFixture, | ||||
| ) -> None: | ||||
|     """Test clean_build when PlatformIO is not available.""" | ||||
|     # Create directory structure and files | ||||
|     pioenvs_dir = tmp_path / ".pioenvs" | ||||
|     pioenvs_dir.mkdir() | ||||
|  | ||||
|     piolibdeps_dir = tmp_path / ".piolibdeps" | ||||
|     piolibdeps_dir.mkdir() | ||||
|  | ||||
|     dependencies_lock = tmp_path / "dependencies.lock" | ||||
|     dependencies_lock.write_text("lock file") | ||||
|  | ||||
|     # Setup mocks | ||||
|     mock_core.relative_pioenvs_path.return_value = str(pioenvs_dir) | ||||
|     mock_core.relative_piolibdeps_path.return_value = str(piolibdeps_dir) | ||||
|     mock_core.relative_build_path.return_value = str(dependencies_lock) | ||||
|  | ||||
|     # Verify all exist before | ||||
|     assert pioenvs_dir.exists() | ||||
|     assert piolibdeps_dir.exists() | ||||
|     assert dependencies_lock.exists() | ||||
|  | ||||
|     # Mock import error for platformio | ||||
|     with ( | ||||
|         patch.dict("sys.modules", {"platformio.project.helpers": None}), | ||||
|         caplog.at_level("INFO"), | ||||
|     ): | ||||
|         # Call the function | ||||
|         clean_build() | ||||
|  | ||||
|     # Verify standard paths were removed but no cache cleaning attempted | ||||
|     assert not pioenvs_dir.exists() | ||||
|     assert not piolibdeps_dir.exists() | ||||
|     assert not dependencies_lock.exists() | ||||
|  | ||||
|     # Verify no cache logging | ||||
|     assert "PlatformIO cache" not in caplog.text | ||||
|  | ||||
|  | ||||
| @patch("esphome.writer.CORE") | ||||
| def test_clean_build_empty_cache_dir( | ||||
|     mock_core: MagicMock, | ||||
|     tmp_path: Path, | ||||
|     caplog: pytest.LogCaptureFixture, | ||||
| ) -> None: | ||||
|     """Test clean_build when get_project_cache_dir returns empty/whitespace.""" | ||||
|     # Create directory structure and files | ||||
|     pioenvs_dir = tmp_path / ".pioenvs" | ||||
|     pioenvs_dir.mkdir() | ||||
|  | ||||
|     # Setup mocks | ||||
|     mock_core.relative_pioenvs_path.return_value = str(pioenvs_dir) | ||||
|     mock_core.relative_piolibdeps_path.return_value = str(tmp_path / ".piolibdeps") | ||||
|     mock_core.relative_build_path.return_value = str(tmp_path / "dependencies.lock") | ||||
|  | ||||
|     # Verify pioenvs exists before | ||||
|     assert pioenvs_dir.exists() | ||||
|  | ||||
|     # Mock PlatformIO's get_project_cache_dir to return whitespace | ||||
|     with patch( | ||||
|         "platformio.project.helpers.get_project_cache_dir" | ||||
|     ) as mock_get_cache_dir: | ||||
|         mock_get_cache_dir.return_value = "   "  # Whitespace only | ||||
|  | ||||
|         # Call the function | ||||
|         with caplog.at_level("INFO"): | ||||
|             clean_build() | ||||
|  | ||||
|     # Verify pioenvs was removed | ||||
|     assert not pioenvs_dir.exists() | ||||
|  | ||||
|     # Verify no cache cleaning was attempted due to empty string | ||||
|     assert "PlatformIO cache" not in caplog.text | ||||
|  | ||||
|  | ||||
| @patch("esphome.writer.CORE") | ||||
| def test_write_gitignore_creates_new_file( | ||||
|     mock_core: MagicMock, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user