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

This commit is contained in:
J. Nick Koston
2025-09-21 11:00:49 -06:00
parent e3b64103cc
commit 7ea680a802
2 changed files with 249 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,234 @@ 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."""
# Create test YAML files
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
""")
# Set up CORE
setup_core(tmp_path=tmp_path)
# Mock successful updates
mock_run_external_process.return_value = 0
# Create args with the directory as configuration
args = MockArgs(configuration=[str(tmp_path)])
# Run command_update_all
result = command_update_all(args)
# Should succeed
assert result == 0
# Capture output
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."""
# Create test YAML files
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
""")
# Set up CORE
setup_core(tmp_path=tmp_path)
# Mock mixed results - first succeeds, second fails
mock_run_external_process.side_effect = [0, 1]
# Create args with the directory as configuration
args = MockArgs(configuration=[str(tmp_path)])
# Run command_update_all
result = command_update_all(args)
# Should return 1 (failure) since one device failed
assert result == 1
# Capture output
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)."""
# Set up CORE with empty directory
setup_core(tmp_path=tmp_path)
# Create args with the directory as configuration
args = MockArgs(configuration=[str(tmp_path)])
# Run command_update_all
result = command_update_all(args)
# Should succeed with no updates
assert result == 0
# Should not have called run_external_process
mock_run_external_process.assert_not_called()
# Capture output
captured = capfd.readouterr()
clean_output = strip_ansi_codes(captured.out)
# Should still show summary
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."""
# Create test YAML file
yaml_file = tmp_path / "single_device.yaml"
yaml_file.write_text("""
esphome:
name: single_device
esp32:
board: nodemcu-32s
""")
# Set up CORE
setup_core(tmp_path=tmp_path)
# Mock successful update
mock_run_external_process.return_value = 0
# Create args with single file as configuration
args = MockArgs(configuration=[str(yaml_file)])
# Run command_update_all
result = command_update_all(args)
# Should succeed
assert result == 0
# Capture output
captured = capfd.readouterr()
clean_output = strip_ansi_codes(captured.out)
# Check output
assert "single_device.yaml" in clean_output
assert "SUCCESS" in clean_output
# Verify run_external_process was called once
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."""
# Create a test YAML file with special characters in name
yaml_file = tmp_path / "test-device_123.yaml"
yaml_file.write_text("""
esphome:
name: test-device_123
esp32:
board: nodemcu-32s
""")
# Set up CORE
setup_core(tmp_path=tmp_path)
# Mock successful update
mock_run_external_process.return_value = 0
# Create args
args = MockArgs(configuration=[str(tmp_path)])
# Run command_update_all
result = command_update_all(args)
# Should succeed
assert result == 0
# Capture output
captured = capfd.readouterr()
clean_output = strip_ansi_codes(captured.out)
# The file path should appear in the output without causing TypeError
assert "test-device_123.yaml" in clean_output
# Check that output contains expected content
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