1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-21 20:52:20 +01:00

[core] Fix TypeError in update-all command after Path migration (#10821)

This commit is contained in:
J. Nick Koston
2025-09-21 11:07:27 -06:00
committed by GitHub
parent e3b64103cc
commit 4729bc87fa
2 changed files with 186 additions and 5 deletions

View File

@@ -772,7 +772,7 @@ def command_update_all(args: ArgsProtocol) -> int | None:
safe_print(f"{half_line}{middle_text}{half_line}") safe_print(f"{half_line}{middle_text}{half_line}")
for f in files: for f in files:
safe_print(f"Updating {color(AnsiFore.CYAN, f)}") safe_print(f"Updating {color(AnsiFore.CYAN, str(f))}")
safe_print("-" * twidth) safe_print("-" * twidth)
safe_print() safe_print()
if CORE.dashboard: if CORE.dashboard:
@@ -784,10 +784,10 @@ def command_update_all(args: ArgsProtocol) -> int | None:
"esphome", "run", f, "--no-logs", "--device", "OTA" "esphome", "run", f, "--no-logs", "--device", "OTA"
) )
if rc == 0: if rc == 0:
print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {f}") print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}")
success[f] = True success[f] = True
else: else:
print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {f}") print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {str(f)}")
success[f] = False success[f] = False
safe_print() safe_print()
@@ -798,9 +798,9 @@ def command_update_all(args: ArgsProtocol) -> int | None:
failed = 0 failed = 0
for f in files: for f in files:
if success[f]: if success[f]:
safe_print(f" - {f}: {color(AnsiFore.GREEN, 'SUCCESS')}") safe_print(f" - {str(f)}: {color(AnsiFore.GREEN, 'SUCCESS')}")
else: else:
safe_print(f" - {f}: {color(AnsiFore.BOLD_RED, 'FAILED')}") safe_print(f" - {str(f)}: {color(AnsiFore.BOLD_RED, 'FAILED')}")
failed += 1 failed += 1
return failed return failed

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
from collections.abc import Generator from collections.abc import Generator
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
import re
from typing import Any from typing import Any
from unittest.mock import MagicMock, Mock, patch from unittest.mock import MagicMock, Mock, patch
@@ -15,6 +16,7 @@ from esphome.__main__ import (
Purpose, Purpose,
choose_upload_log_host, choose_upload_log_host,
command_rename, command_rename,
command_update_all,
command_wizard, command_wizard,
get_port_type, get_port_type,
has_ip_address, has_ip_address,
@@ -55,6 +57,17 @@ from esphome.const import (
from esphome.core import CORE, EsphomeError from esphome.core import CORE, EsphomeError
def strip_ansi_codes(text: str) -> str:
"""Remove ANSI escape codes from text.
This helps make test assertions cleaner by removing color codes and other
terminal formatting that can make tests brittle.
"""
# Pattern to match ANSI escape sequences
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
return ansi_escape.sub("", text)
@dataclass @dataclass
class MockSerialPort: class MockSerialPort:
"""Mock serial port for testing. """Mock serial port for testing.
@@ -1545,3 +1558,171 @@ esp32:
captured = capfd.readouterr() captured = capfd.readouterr()
assert "Rename failed" in captured.out assert "Rename failed" in captured.out
def test_command_update_all_path_string_conversion(
tmp_path: Path,
mock_run_external_process: Mock,
capfd: CaptureFixture[str],
) -> None:
"""Test that command_update_all properly converts Path objects to strings in output."""
yaml1 = tmp_path / "device1.yaml"
yaml1.write_text("""
esphome:
name: device1
esp32:
board: nodemcu-32s
""")
yaml2 = tmp_path / "device2.yaml"
yaml2.write_text("""
esphome:
name: device2
esp8266:
board: nodemcuv2
""")
setup_core(tmp_path=tmp_path)
mock_run_external_process.return_value = 0
assert command_update_all(MockArgs(configuration=[str(tmp_path)])) == 0
captured = capfd.readouterr()
clean_output = strip_ansi_codes(captured.out)
# Check that Path objects were properly converted to strings
# The output should contain file paths without causing TypeError
assert "device1.yaml" in clean_output
assert "device2.yaml" in clean_output
assert "SUCCESS" in clean_output
assert "SUMMARY" in clean_output
# Verify run_external_process was called for each file
assert mock_run_external_process.call_count == 2
def test_command_update_all_with_failures(
tmp_path: Path,
mock_run_external_process: Mock,
capfd: CaptureFixture[str],
) -> None:
"""Test command_update_all handles mixed success/failure cases properly."""
yaml1 = tmp_path / "success_device.yaml"
yaml1.write_text("""
esphome:
name: success_device
esp32:
board: nodemcu-32s
""")
yaml2 = tmp_path / "failed_device.yaml"
yaml2.write_text("""
esphome:
name: failed_device
esp8266:
board: nodemcuv2
""")
setup_core(tmp_path=tmp_path)
# Mock mixed results - first succeeds, second fails
mock_run_external_process.side_effect = [0, 1]
# Should return 1 (failure) since one device failed
assert command_update_all(MockArgs(configuration=[str(tmp_path)])) == 1
captured = capfd.readouterr()
clean_output = strip_ansi_codes(captured.out)
# Check that both success and failure are properly displayed
assert "SUCCESS" in clean_output
assert "ERROR" in clean_output or "FAILED" in clean_output
assert "SUMMARY" in clean_output
# Files are processed in alphabetical order, so we need to check which one succeeded/failed
# The mock_run_external_process.side_effect = [0, 1] applies to files in alphabetical order
# So "failed_device.yaml" gets 0 (success) and "success_device.yaml" gets 1 (failure)
assert "failed_device.yaml: SUCCESS" in clean_output
assert "success_device.yaml: FAILED" in clean_output
def test_command_update_all_empty_directory(
tmp_path: Path,
mock_run_external_process: Mock,
capfd: CaptureFixture[str],
) -> None:
"""Test command_update_all with an empty directory (no YAML files)."""
setup_core(tmp_path=tmp_path)
assert command_update_all(MockArgs(configuration=[str(tmp_path)])) == 0
mock_run_external_process.assert_not_called()
captured = capfd.readouterr()
clean_output = strip_ansi_codes(captured.out)
assert "SUMMARY" in clean_output
def test_command_update_all_single_file(
tmp_path: Path,
mock_run_external_process: Mock,
capfd: CaptureFixture[str],
) -> None:
"""Test command_update_all with a single YAML file specified."""
yaml_file = tmp_path / "single_device.yaml"
yaml_file.write_text("""
esphome:
name: single_device
esp32:
board: nodemcu-32s
""")
setup_core(tmp_path=tmp_path)
mock_run_external_process.return_value = 0
assert command_update_all(MockArgs(configuration=[str(yaml_file)])) == 0
captured = capfd.readouterr()
clean_output = strip_ansi_codes(captured.out)
assert "single_device.yaml" in clean_output
assert "SUCCESS" in clean_output
mock_run_external_process.assert_called_once()
def test_command_update_all_path_formatting_in_color_calls(
tmp_path: Path,
mock_run_external_process: Mock,
capfd: CaptureFixture[str],
) -> None:
"""Test that Path objects are properly converted when passed to color() function."""
yaml_file = tmp_path / "test-device_123.yaml"
yaml_file.write_text("""
esphome:
name: test-device_123
esp32:
board: nodemcu-32s
""")
setup_core(tmp_path=tmp_path)
mock_run_external_process.return_value = 0
assert command_update_all(MockArgs(configuration=[str(tmp_path)])) == 0
captured = capfd.readouterr()
clean_output = strip_ansi_codes(captured.out)
assert "test-device_123.yaml" in clean_output
assert "Updating" in clean_output
assert "SUCCESS" in clean_output
assert "SUMMARY" in clean_output
# Should not have any Python error messages
assert "TypeError" not in clean_output
assert "can only concatenate str" not in clean_output