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:
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user