mirror of
https://github.com/esphome/esphome.git
synced 2025-09-22 13:12:22 +01:00
@@ -87,3 +87,10 @@ def mock_run_external_command() -> Generator[Mock, None, None]:
|
|||||||
"""Mock run_external_command for platformio_api."""
|
"""Mock run_external_command for platformio_api."""
|
||||||
with patch("esphome.platformio_api.run_external_command") as mock:
|
with patch("esphome.platformio_api.run_external_command") as mock:
|
||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_get_idedata() -> Generator[Mock, None, None]:
|
||||||
|
"""Mock get_idedata for platformio_api."""
|
||||||
|
with patch("esphome.platformio_api.get_idedata") as mock:
|
||||||
|
yield mock
|
||||||
|
@@ -12,6 +12,7 @@ from unittest.mock import MagicMock, Mock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
from pytest import CaptureFixture
|
from pytest import CaptureFixture
|
||||||
|
|
||||||
|
from esphome import platformio_api
|
||||||
from esphome.__main__ import (
|
from esphome.__main__ import (
|
||||||
Purpose,
|
Purpose,
|
||||||
choose_upload_log_host,
|
choose_upload_log_host,
|
||||||
@@ -28,7 +29,9 @@ from esphome.__main__ import (
|
|||||||
mqtt_get_ip,
|
mqtt_get_ip,
|
||||||
show_logs,
|
show_logs,
|
||||||
upload_program,
|
upload_program,
|
||||||
|
upload_using_esptool,
|
||||||
)
|
)
|
||||||
|
from esphome.components.esp32.const import KEY_ESP32, KEY_VARIANT, VARIANT_ESP32
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_API,
|
CONF_API,
|
||||||
CONF_BROKER,
|
CONF_BROKER,
|
||||||
@@ -220,6 +223,14 @@ def mock_run_external_process() -> Generator[Mock]:
|
|||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_run_external_command() -> Generator[Mock]:
|
||||||
|
"""Mock run_external_command for testing."""
|
||||||
|
with patch("esphome.__main__.run_external_command") as mock:
|
||||||
|
mock.return_value = 0 # Default to success
|
||||||
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
def test_choose_upload_log_host_with_string_default() -> None:
|
def test_choose_upload_log_host_with_string_default() -> None:
|
||||||
"""Test with a single string default device."""
|
"""Test with a single string default device."""
|
||||||
setup_core()
|
setup_core()
|
||||||
@@ -818,6 +829,122 @@ def test_upload_program_serial_esp8266_with_file(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_upload_using_esptool_path_conversion(
|
||||||
|
tmp_path: Path,
|
||||||
|
mock_run_external_command: Mock,
|
||||||
|
mock_get_idedata: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test upload_using_esptool properly converts Path objects to strings for esptool.
|
||||||
|
|
||||||
|
This test ensures that img.path (Path object) is converted to string before
|
||||||
|
passing to esptool, preventing AttributeError.
|
||||||
|
"""
|
||||||
|
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test")
|
||||||
|
|
||||||
|
# Set up ESP32-specific data required by get_esp32_variant()
|
||||||
|
CORE.data[KEY_ESP32] = {KEY_VARIANT: VARIANT_ESP32}
|
||||||
|
|
||||||
|
# Create mock IDEData with Path objects
|
||||||
|
mock_idedata = MagicMock(spec=platformio_api.IDEData)
|
||||||
|
mock_idedata.firmware_bin_path = tmp_path / "firmware.bin"
|
||||||
|
mock_idedata.extra_flash_images = [
|
||||||
|
platformio_api.FlashImage(path=tmp_path / "bootloader.bin", offset="0x1000"),
|
||||||
|
platformio_api.FlashImage(path=tmp_path / "partitions.bin", offset="0x8000"),
|
||||||
|
]
|
||||||
|
|
||||||
|
mock_get_idedata.return_value = mock_idedata
|
||||||
|
|
||||||
|
# Create the actual firmware files so they exist
|
||||||
|
(tmp_path / "firmware.bin").touch()
|
||||||
|
(tmp_path / "bootloader.bin").touch()
|
||||||
|
(tmp_path / "partitions.bin").touch()
|
||||||
|
|
||||||
|
config = {CONF_ESPHOME: {"platformio_options": {}}}
|
||||||
|
|
||||||
|
# Call upload_using_esptool without custom file argument
|
||||||
|
result = upload_using_esptool(config, "/dev/ttyUSB0", None, None)
|
||||||
|
|
||||||
|
assert result == 0
|
||||||
|
|
||||||
|
# Verify that run_external_command was called
|
||||||
|
assert mock_run_external_command.call_count == 1
|
||||||
|
|
||||||
|
# Get the actual call arguments
|
||||||
|
call_args = mock_run_external_command.call_args[0]
|
||||||
|
|
||||||
|
# The first argument should be esptool.main function,
|
||||||
|
# followed by the command arguments
|
||||||
|
assert len(call_args) > 1
|
||||||
|
|
||||||
|
# Find the indices of the flash image arguments
|
||||||
|
# They should come after "write-flash" and "-z"
|
||||||
|
cmd_list = list(call_args[1:]) # Skip the esptool.main function
|
||||||
|
|
||||||
|
# Verify all paths are strings, not Path objects
|
||||||
|
# The firmware and flash images should be at specific positions
|
||||||
|
write_flash_idx = cmd_list.index("write-flash")
|
||||||
|
|
||||||
|
# After write-flash we have: -z, --flash-size, detect, then offset/path pairs
|
||||||
|
# Check firmware at offset 0x10000 (ESP32)
|
||||||
|
firmware_offset_idx = write_flash_idx + 4
|
||||||
|
assert cmd_list[firmware_offset_idx] == "0x10000"
|
||||||
|
firmware_path = cmd_list[firmware_offset_idx + 1]
|
||||||
|
assert isinstance(firmware_path, str)
|
||||||
|
assert firmware_path.endswith("firmware.bin")
|
||||||
|
|
||||||
|
# Check bootloader
|
||||||
|
bootloader_offset_idx = firmware_offset_idx + 2
|
||||||
|
assert cmd_list[bootloader_offset_idx] == "0x1000"
|
||||||
|
bootloader_path = cmd_list[bootloader_offset_idx + 1]
|
||||||
|
assert isinstance(bootloader_path, str)
|
||||||
|
assert bootloader_path.endswith("bootloader.bin")
|
||||||
|
|
||||||
|
# Check partitions
|
||||||
|
partitions_offset_idx = bootloader_offset_idx + 2
|
||||||
|
assert cmd_list[partitions_offset_idx] == "0x8000"
|
||||||
|
partitions_path = cmd_list[partitions_offset_idx + 1]
|
||||||
|
assert isinstance(partitions_path, str)
|
||||||
|
assert partitions_path.endswith("partitions.bin")
|
||||||
|
|
||||||
|
|
||||||
|
def test_upload_using_esptool_with_file_path(
|
||||||
|
tmp_path: Path,
|
||||||
|
mock_run_external_command: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test upload_using_esptool with a custom file that's a Path object."""
|
||||||
|
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test")
|
||||||
|
|
||||||
|
# Create a test firmware file
|
||||||
|
firmware_file = tmp_path / "custom_firmware.bin"
|
||||||
|
firmware_file.touch()
|
||||||
|
|
||||||
|
config = {CONF_ESPHOME: {"platformio_options": {}}}
|
||||||
|
|
||||||
|
# Call with a Path object as the file argument (though usually it's a string)
|
||||||
|
result = upload_using_esptool(config, "/dev/ttyUSB0", str(firmware_file), None)
|
||||||
|
|
||||||
|
assert result == 0
|
||||||
|
|
||||||
|
# Verify that run_external_command was called
|
||||||
|
mock_run_external_command.assert_called_once()
|
||||||
|
|
||||||
|
# Get the actual call arguments
|
||||||
|
call_args = mock_run_external_command.call_args[0]
|
||||||
|
cmd_list = list(call_args[1:]) # Skip the esptool.main function
|
||||||
|
|
||||||
|
# Find the firmware path in the command
|
||||||
|
write_flash_idx = cmd_list.index("write-flash")
|
||||||
|
|
||||||
|
# For custom file, it should be at offset 0x0
|
||||||
|
firmware_offset_idx = write_flash_idx + 4
|
||||||
|
assert cmd_list[firmware_offset_idx] == "0x0"
|
||||||
|
firmware_path = cmd_list[firmware_offset_idx + 1]
|
||||||
|
|
||||||
|
# Verify it's a string, not a Path object
|
||||||
|
assert isinstance(firmware_path, str)
|
||||||
|
assert firmware_path.endswith("custom_firmware.bin")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"platform,device",
|
"platform,device",
|
||||||
[
|
[
|
||||||
|
Reference in New Issue
Block a user