Merge remote-tracking branch 'origin/rp2040-upload-improvements' into integration

This commit is contained in:
J. Nick Koston
2026-03-09 17:49:47 -10:00
92 changed files with 1542 additions and 769 deletions
+12
View File
@@ -841,6 +841,18 @@ class TestEsphomeCore:
assert "WiFi" in target.platformio_libraries
def test_testing_ensure_platform_registered__sets_count(self, target):
"""Test testing_ensure_platform_registered sets count to 1 for new platform."""
assert target.platform_counts["sensor"] == 0
target.testing_ensure_platform_registered("sensor")
assert target.platform_counts["sensor"] == 1
def test_testing_ensure_platform_registered__does_not_overwrite(self, target):
"""Test testing_ensure_platform_registered preserves existing count."""
target.platform_counts["sensor"] = 3
target.testing_ensure_platform_registered("sensor")
assert target.platform_counts["sensor"] == 3
def test_add_library__extracts_short_name_from_path(self, target):
"""Test add_library extracts short name from library paths like owner/lib."""
target.data[const.KEY_CORE] = {
+69 -5
View File
@@ -76,6 +76,7 @@ from esphome.const import (
PLATFORM_RP2040,
)
from esphome.core import CORE, EsphomeError
from esphome.util import BootselResult
def strip_ansi_codes(text: str) -> str:
@@ -875,7 +876,7 @@ def test_choose_upload_log_host_no_defaults_with_rp2040_bootsel(
patch(
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
),
patch("esphome.__main__.detect_rp2040_bootsel", return_value=1),
patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(1)),
):
result = choose_upload_log_host(
default=None,
@@ -898,7 +899,7 @@ def test_choose_upload_log_host_rp2040_no_device_shows_bootsel_help() -> None:
patch(
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
),
patch("esphome.__main__.detect_rp2040_bootsel", return_value=0),
patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(0)),
pytest.raises(EsphomeError, match="BOOTSEL"),
):
choose_upload_log_host(
@@ -923,7 +924,7 @@ def test_choose_upload_log_host_rp2040_bootsel_tip_with_ota(
patch(
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
),
patch("esphome.__main__.detect_rp2040_bootsel", return_value=0),
patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(0)),
patch(
"esphome.__main__.choose_prompt",
return_value="192.168.1.100",
@@ -952,7 +953,7 @@ def test_choose_upload_log_host_rp2040_bootsel_tip_with_serial_ports(
"esphome.__main__._find_picotool",
return_value=Path("/usr/bin/picotool"),
),
patch("esphome.__main__.detect_rp2040_bootsel", return_value=0),
patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(0)),
caplog.at_level(logging.INFO, logger="esphome.__main__"),
):
choose_upload_log_host(
@@ -963,6 +964,69 @@ def test_choose_upload_log_host_rp2040_bootsel_tip_with_serial_ports(
assert "BOOTSEL" in caplog.text
@pytest.mark.usefixtures("mock_no_serial_ports")
def test_choose_upload_log_host_rp2040_permission_error_no_options(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test permission warning shown when BOOTSEL device found but not accessible."""
setup_core(platform=PLATFORM_RP2040)
with (
patch(
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
),
patch(
"esphome.__main__.detect_rp2040_bootsel",
return_value=BootselResult(0, permission_error=True),
),
patch("esphome.__main__.sys.platform", "linux"),
pytest.raises(EsphomeError, match="BOOTSEL"),
caplog.at_level(logging.WARNING, logger="esphome.__main__"),
):
choose_upload_log_host(
default=None,
check_default=None,
purpose=Purpose.UPLOADING,
)
assert "USB permissions" in caplog.text
assert "udev" in caplog.text
@pytest.mark.usefixtures("mock_no_serial_ports")
def test_choose_upload_log_host_rp2040_permission_error_with_ota(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test permission warning shown with OTA fallback available."""
setup_core(
platform=PLATFORM_RP2040,
config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]},
address="192.168.1.100",
)
with (
patch(
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
),
patch(
"esphome.__main__.detect_rp2040_bootsel",
return_value=BootselResult(0, permission_error=True),
),
patch(
"esphome.__main__.choose_prompt",
return_value="192.168.1.100",
),
caplog.at_level(logging.WARNING, logger="esphome.__main__"),
):
choose_upload_log_host(
default=None,
check_default=None,
purpose=Purpose.UPLOADING,
)
assert "USB permissions" in caplog.text
def test_choose_upload_log_host_no_bootsel_for_non_rp2040(
mock_no_serial_ports: Mock,
) -> None:
@@ -1000,7 +1064,7 @@ def test_choose_upload_log_host_rp2040_serial_and_bootsel(
patch(
"esphome.__main__._find_picotool", return_value=Path("/usr/bin/picotool")
),
patch("esphome.__main__.detect_rp2040_bootsel", return_value=1),
patch("esphome.__main__.detect_rp2040_bootsel", return_value=BootselResult(1)),
):
choose_upload_log_host(
default=None,
+44 -10
View File
@@ -463,8 +463,9 @@ def test_detect_rp2040_bootsel_found() -> None:
mock_result = MagicMock()
mock_result.stdout = b"Device Information\n type: RP2040\n"
with patch("esphome.util.subprocess.run", return_value=mock_result):
count = util.detect_rp2040_bootsel("/usr/bin/picotool")
assert count == 1
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
assert result.device_count == 1
assert result.permission_error is False
def test_detect_rp2040_bootsel_multiple() -> None:
@@ -472,8 +473,9 @@ def test_detect_rp2040_bootsel_multiple() -> None:
mock_result = MagicMock()
mock_result.stdout = b"type: RP2040\ntype: RP2350\n"
with patch("esphome.util.subprocess.run", return_value=mock_result):
count = util.detect_rp2040_bootsel("/usr/bin/picotool")
assert count == 2
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
assert result.device_count == 2
assert result.permission_error is False
def test_detect_rp2040_bootsel_none() -> None:
@@ -482,16 +484,47 @@ def test_detect_rp2040_bootsel_none() -> None:
mock_result.stdout = (
b"No accessible RP2040/RP2350 devices in BOOTSEL mode were found.\n"
)
mock_result.stderr = b""
with patch("esphome.util.subprocess.run", return_value=mock_result):
count = util.detect_rp2040_bootsel("/usr/bin/picotool")
assert count == 0
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
assert result.device_count == 0
assert result.permission_error is False
def test_detect_rp2040_bootsel_permission_error() -> None:
"""Test BOOTSEL detection with device found but not accessible."""
mock_result = MagicMock()
mock_result.stdout = (
b"No accessible RP-series devices in BOOTSEL mode were found.\n"
)
mock_result.stderr = (
b"RP2040 device at bus 5, address 24 appears to be in BOOTSEL mode, "
b"but picotool was unable to connect. "
b"Maybe try 'sudo' or check your permissions.\n"
)
with patch("esphome.util.subprocess.run", return_value=mock_result):
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
assert result.device_count == 0
assert result.permission_error is True
def test_detect_rp2040_bootsel_libusb_access_error() -> None:
"""Test BOOTSEL detection with LIBUSB_ERROR_ACCESS."""
mock_result = MagicMock()
mock_result.stdout = b""
mock_result.stderr = b"LIBUSB_ERROR_ACCESS\n"
with patch("esphome.util.subprocess.run", return_value=mock_result):
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
assert result.device_count == 0
assert result.permission_error is True
def test_detect_rp2040_bootsel_oserror() -> None:
"""Test BOOTSEL detection handles OSError."""
with patch("esphome.util.subprocess.run", side_effect=OSError("not found")):
count = util.detect_rp2040_bootsel("/usr/bin/picotool")
assert count == 0
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
assert result.device_count == 0
assert result.permission_error is False
def test_detect_rp2040_bootsel_timeout() -> None:
@@ -500,8 +533,9 @@ def test_detect_rp2040_bootsel_timeout() -> None:
"esphome.util.subprocess.run",
side_effect=subprocess.TimeoutExpired("picotool", 10),
):
count = util.detect_rp2040_bootsel("/usr/bin/picotool")
assert count == 0
result = util.detect_rp2040_bootsel("/usr/bin/picotool")
assert result.device_count == 0
assert result.permission_error is False
def _make_redirect(