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

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
from collections.abc import Generator
from dataclasses import dataclass
from pathlib import Path
import re
from typing import Any
from unittest.mock import MagicMock, Mock, patch
@@ -15,6 +16,7 @@ from esphome.__main__ import (
Purpose,
choose_upload_log_host,
command_rename,
command_update_all,
command_wizard,
get_port_type,
has_ip_address,
@@ -55,6 +57,17 @@ from esphome.const import (
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
class MockSerialPort:
"""Mock serial port for testing.
@@ -1545,3 +1558,234 @@ esp32:
captured = capfd.readouterr()
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