mirror of
https://github.com/esphome/esphome.git
synced 2025-09-12 16:22:22 +01:00
Merge remote-tracking branch 'upstream/dev' into zwave_proxy
This commit is contained in:
@@ -226,7 +226,9 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||
.replace(b"\n", b"")
|
||||
.decode("utf8", "backslashreplace")
|
||||
)
|
||||
time_str = datetime.now().time().strftime("[%H:%M:%S]")
|
||||
time_ = datetime.now()
|
||||
nanoseconds = time_.microsecond // 1000
|
||||
time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]"
|
||||
safe_print(parser.parse_line(line, time_str))
|
||||
|
||||
backtrace_state = platformio_api.process_stacktrace(
|
||||
|
@@ -62,9 +62,11 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
|
||||
time_ = datetime.now()
|
||||
message: bytes = msg.message
|
||||
text = message.decode("utf8", "backslashreplace")
|
||||
for parsed_msg in parse_log_message(
|
||||
text, f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]"
|
||||
):
|
||||
nanoseconds = time_.microsecond // 1000
|
||||
timestamp = (
|
||||
f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]"
|
||||
)
|
||||
for parsed_msg in parse_log_message(text, timestamp):
|
||||
print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg)
|
||||
|
||||
stop = await async_run(cli, on_log, name=name)
|
||||
|
@@ -353,6 +353,7 @@ SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
|
||||
# pioarduino versions that don't require a release number
|
||||
# List based on https://github.com/pioarduino/esp-idf/releases
|
||||
SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
|
||||
cv.Version(5, 5, 1),
|
||||
cv.Version(5, 5, 0),
|
||||
cv.Version(5, 4, 2),
|
||||
cv.Version(5, 4, 1),
|
||||
|
@@ -43,13 +43,6 @@ void BLEClientBase::setup() {
|
||||
void BLEClientBase::set_state(espbt::ClientState st) {
|
||||
ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st);
|
||||
ESPBTClient::set_state(st);
|
||||
|
||||
if (st == espbt::ClientState::READY_TO_CONNECT) {
|
||||
// Enable loop for state processing
|
||||
this->enable_loop();
|
||||
// Connect immediately instead of waiting for next loop
|
||||
this->connect();
|
||||
}
|
||||
}
|
||||
|
||||
void BLEClientBase::loop() {
|
||||
@@ -65,8 +58,8 @@ void BLEClientBase::loop() {
|
||||
}
|
||||
this->set_state(espbt::ClientState::IDLE);
|
||||
}
|
||||
// If its idle, we can disable the loop as set_state
|
||||
// will enable it again when we need to connect.
|
||||
// If idle, we can disable the loop as connect()
|
||||
// will enable it again when a connection is needed.
|
||||
else if (this->state_ == espbt::ClientState::IDLE) {
|
||||
this->disable_loop();
|
||||
}
|
||||
@@ -108,9 +101,20 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
|
||||
#endif
|
||||
|
||||
void BLEClientBase::connect() {
|
||||
// Prevent duplicate connection attempts
|
||||
if (this->state_ == espbt::ClientState::CONNECTING || this->state_ == espbt::ClientState::CONNECTED ||
|
||||
this->state_ == espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_,
|
||||
this->address_str_.c_str(), espbt::client_state_to_string(this->state_));
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_.c_str(),
|
||||
this->remote_addr_type_);
|
||||
this->paired_ = false;
|
||||
// Enable loop for state processing
|
||||
this->enable_loop();
|
||||
// Immediately transition to CONNECTING to prevent duplicate connection attempts
|
||||
this->set_state(espbt::ClientState::CONNECTING);
|
||||
|
||||
// Determine connection parameters based on connection type
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
||||
@@ -168,7 +172,7 @@ void BLEClientBase::unconditional_disconnect() {
|
||||
this->log_gattc_warning_("esp_ble_gattc_close", err);
|
||||
}
|
||||
|
||||
if (this->state_ == espbt::ClientState::READY_TO_CONNECT || this->state_ == espbt::ClientState::DISCOVERED) {
|
||||
if (this->state_ == espbt::ClientState::DISCOVERED) {
|
||||
this->set_address(0);
|
||||
this->set_state(espbt::ClientState::IDLE);
|
||||
} else {
|
||||
@@ -212,8 +216,6 @@ void BLEClientBase::handle_connection_result_(esp_err_t ret) {
|
||||
if (ret) {
|
||||
this->log_gattc_warning_("esp_ble_gattc_open", ret);
|
||||
this->set_state(espbt::ClientState::IDLE);
|
||||
} else {
|
||||
this->set_state(espbt::ClientState::CONNECTING);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -51,8 +51,6 @@ const char *client_state_to_string(ClientState state) {
|
||||
return "IDLE";
|
||||
case ClientState::DISCOVERED:
|
||||
return "DISCOVERED";
|
||||
case ClientState::READY_TO_CONNECT:
|
||||
return "READY_TO_CONNECT";
|
||||
case ClientState::CONNECTING:
|
||||
return "CONNECTING";
|
||||
case ClientState::CONNECTED:
|
||||
@@ -795,7 +793,7 @@ void ESP32BLETracker::try_promote_discovered_clients_() {
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
this->update_coex_preference_(true);
|
||||
#endif
|
||||
client->set_state(ClientState::READY_TO_CONNECT);
|
||||
client->connect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@@ -159,8 +159,6 @@ enum class ClientState : uint8_t {
|
||||
IDLE,
|
||||
// Device advertisement found.
|
||||
DISCOVERED,
|
||||
// Device is discovered and the scanner is stopped
|
||||
READY_TO_CONNECT,
|
||||
// Connection in progress.
|
||||
CONNECTING,
|
||||
// Initial connection established.
|
||||
@@ -313,7 +311,6 @@ class ESP32BLETracker : public Component,
|
||||
counts.discovered++;
|
||||
break;
|
||||
case ClientState::CONNECTING:
|
||||
case ClientState::READY_TO_CONNECT:
|
||||
counts.connecting++;
|
||||
break;
|
||||
default:
|
||||
|
@@ -1,13 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Generator
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import Mock
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from tornado.httpclient import AsyncHTTPClient, HTTPResponse
|
||||
from tornado.httpclient import AsyncHTTPClient, HTTPClientError, HTTPResponse
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.testing import bind_unused_port
|
||||
@@ -34,6 +37,66 @@ class DashboardTestHelper:
|
||||
return await future
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_async_run_system_command() -> Generator[MagicMock]:
|
||||
"""Fixture to mock async_run_system_command."""
|
||||
with patch("esphome.dashboard.web_server.async_run_system_command") as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_trash_storage_path(tmp_path: Path) -> Generator[MagicMock]:
|
||||
"""Fixture to mock trash_storage_path."""
|
||||
trash_dir = tmp_path / "trash"
|
||||
with patch(
|
||||
"esphome.dashboard.web_server.trash_storage_path", return_value=str(trash_dir)
|
||||
) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_archive_storage_path(tmp_path: Path) -> Generator[MagicMock]:
|
||||
"""Fixture to mock archive_storage_path."""
|
||||
archive_dir = tmp_path / "archive"
|
||||
with patch(
|
||||
"esphome.dashboard.web_server.archive_storage_path",
|
||||
return_value=str(archive_dir),
|
||||
) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_dashboard_settings() -> Generator[MagicMock]:
|
||||
"""Fixture to mock dashboard settings."""
|
||||
with patch("esphome.dashboard.web_server.settings") as mock_settings:
|
||||
# Set default auth settings to avoid authentication issues
|
||||
mock_settings.using_auth = False
|
||||
mock_settings.on_ha_addon = False
|
||||
yield mock_settings
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ext_storage_path(tmp_path: Path) -> Generator[MagicMock]:
|
||||
"""Fixture to mock ext_storage_path."""
|
||||
with patch("esphome.dashboard.web_server.ext_storage_path") as mock:
|
||||
mock.return_value = str(tmp_path / "storage.json")
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_storage_json() -> Generator[MagicMock]:
|
||||
"""Fixture to mock StorageJSON."""
|
||||
with patch("esphome.dashboard.web_server.StorageJSON") as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_idedata() -> Generator[MagicMock]:
|
||||
"""Fixture to mock platformio_api.IDEData."""
|
||||
with patch("esphome.dashboard.web_server.platformio_api.IDEData") as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest_asyncio.fixture()
|
||||
async def dashboard() -> DashboardTestHelper:
|
||||
sock, port = bind_unused_port()
|
||||
@@ -80,3 +143,439 @@ async def test_devices_page(dashboard: DashboardTestHelper) -> None:
|
||||
first_device = configured_devices[0]
|
||||
assert first_device["name"] == "pico"
|
||||
assert first_device["configuration"] == "pico.yaml"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wizard_handler_invalid_input(dashboard: DashboardTestHelper) -> None:
|
||||
"""Test the WizardRequestHandler.post method with invalid inputs."""
|
||||
# Test with missing name (should fail with 422)
|
||||
body_no_name = json.dumps(
|
||||
{
|
||||
"name": "", # Empty name
|
||||
"platform": "ESP32",
|
||||
"board": "esp32dev",
|
||||
}
|
||||
)
|
||||
with pytest.raises(HTTPClientError) as exc_info:
|
||||
await dashboard.fetch(
|
||||
"/wizard",
|
||||
method="POST",
|
||||
body=body_no_name,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
assert exc_info.value.code == 422
|
||||
|
||||
# Test with invalid wizard type (should fail with 422)
|
||||
body_invalid_type = json.dumps(
|
||||
{
|
||||
"name": "test_device",
|
||||
"type": "invalid_type",
|
||||
"platform": "ESP32",
|
||||
"board": "esp32dev",
|
||||
}
|
||||
)
|
||||
with pytest.raises(HTTPClientError) as exc_info:
|
||||
await dashboard.fetch(
|
||||
"/wizard",
|
||||
method="POST",
|
||||
body=body_invalid_type,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
assert exc_info.value.code == 422
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wizard_handler_conflict(dashboard: DashboardTestHelper) -> None:
|
||||
"""Test the WizardRequestHandler.post when config already exists."""
|
||||
# Try to create a wizard for existing pico.yaml (should conflict)
|
||||
body = json.dumps(
|
||||
{
|
||||
"name": "pico", # This already exists in fixtures
|
||||
"platform": "ESP32",
|
||||
"board": "esp32dev",
|
||||
}
|
||||
)
|
||||
with pytest.raises(HTTPClientError) as exc_info:
|
||||
await dashboard.fetch(
|
||||
"/wizard",
|
||||
method="POST",
|
||||
body=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
assert exc_info.value.code == 409
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_binary_handler_not_found(
|
||||
dashboard: DashboardTestHelper,
|
||||
) -> None:
|
||||
"""Test the DownloadBinaryRequestHandler.get with non-existent config."""
|
||||
with pytest.raises(HTTPClientError) as exc_info:
|
||||
await dashboard.fetch(
|
||||
"/download.bin?configuration=nonexistent.yaml",
|
||||
method="GET",
|
||||
)
|
||||
assert exc_info.value.code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("mock_ext_storage_path")
|
||||
async def test_download_binary_handler_no_file_param(
|
||||
dashboard: DashboardTestHelper,
|
||||
tmp_path: Path,
|
||||
mock_storage_json: MagicMock,
|
||||
) -> None:
|
||||
"""Test the DownloadBinaryRequestHandler.get without file parameter."""
|
||||
# Mock storage to exist, but still should fail without file param
|
||||
mock_storage = Mock()
|
||||
mock_storage.name = "test_device"
|
||||
mock_storage.firmware_bin_path = str(tmp_path / "firmware.bin")
|
||||
mock_storage_json.load.return_value = mock_storage
|
||||
|
||||
with pytest.raises(HTTPClientError) as exc_info:
|
||||
await dashboard.fetch(
|
||||
"/download.bin?configuration=pico.yaml",
|
||||
method="GET",
|
||||
)
|
||||
assert exc_info.value.code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("mock_ext_storage_path")
|
||||
async def test_download_binary_handler_with_file(
|
||||
dashboard: DashboardTestHelper,
|
||||
tmp_path: Path,
|
||||
mock_storage_json: MagicMock,
|
||||
) -> None:
|
||||
"""Test the DownloadBinaryRequestHandler.get with existing binary file."""
|
||||
# Create a fake binary file
|
||||
build_dir = tmp_path / ".esphome" / "build" / "test"
|
||||
build_dir.mkdir(parents=True)
|
||||
firmware_file = build_dir / "firmware.bin"
|
||||
firmware_file.write_bytes(b"fake firmware content")
|
||||
|
||||
# Mock storage JSON
|
||||
mock_storage = Mock()
|
||||
mock_storage.name = "test_device"
|
||||
mock_storage.firmware_bin_path = str(firmware_file)
|
||||
mock_storage_json.load.return_value = mock_storage
|
||||
|
||||
response = await dashboard.fetch(
|
||||
"/download.bin?configuration=test.yaml&file=firmware.bin",
|
||||
method="GET",
|
||||
)
|
||||
assert response.code == 200
|
||||
assert response.body == b"fake firmware content"
|
||||
assert response.headers["Content-Type"] == "application/octet-stream"
|
||||
assert "attachment" in response.headers["Content-Disposition"]
|
||||
assert "test_device-firmware.bin" in response.headers["Content-Disposition"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("mock_ext_storage_path")
|
||||
async def test_download_binary_handler_compressed(
|
||||
dashboard: DashboardTestHelper,
|
||||
tmp_path: Path,
|
||||
mock_storage_json: MagicMock,
|
||||
) -> None:
|
||||
"""Test the DownloadBinaryRequestHandler.get with compression."""
|
||||
# Create a fake binary file
|
||||
build_dir = tmp_path / ".esphome" / "build" / "test"
|
||||
build_dir.mkdir(parents=True)
|
||||
firmware_file = build_dir / "firmware.bin"
|
||||
original_content = b"fake firmware content for compression test"
|
||||
firmware_file.write_bytes(original_content)
|
||||
|
||||
# Mock storage JSON
|
||||
mock_storage = Mock()
|
||||
mock_storage.name = "test_device"
|
||||
mock_storage.firmware_bin_path = str(firmware_file)
|
||||
mock_storage_json.load.return_value = mock_storage
|
||||
|
||||
response = await dashboard.fetch(
|
||||
"/download.bin?configuration=test.yaml&file=firmware.bin&compressed=1",
|
||||
method="GET",
|
||||
)
|
||||
assert response.code == 200
|
||||
# Decompress and verify content
|
||||
decompressed = gzip.decompress(response.body)
|
||||
assert decompressed == original_content
|
||||
assert response.headers["Content-Type"] == "application/octet-stream"
|
||||
assert "firmware.bin.gz" in response.headers["Content-Disposition"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("mock_ext_storage_path")
|
||||
async def test_download_binary_handler_custom_download_name(
|
||||
dashboard: DashboardTestHelper,
|
||||
tmp_path: Path,
|
||||
mock_storage_json: MagicMock,
|
||||
) -> None:
|
||||
"""Test the DownloadBinaryRequestHandler.get with custom download name."""
|
||||
# Create a fake binary file
|
||||
build_dir = tmp_path / ".esphome" / "build" / "test"
|
||||
build_dir.mkdir(parents=True)
|
||||
firmware_file = build_dir / "firmware.bin"
|
||||
firmware_file.write_bytes(b"content")
|
||||
|
||||
# Mock storage JSON
|
||||
mock_storage = Mock()
|
||||
mock_storage.name = "test_device"
|
||||
mock_storage.firmware_bin_path = str(firmware_file)
|
||||
mock_storage_json.load.return_value = mock_storage
|
||||
|
||||
response = await dashboard.fetch(
|
||||
"/download.bin?configuration=test.yaml&file=firmware.bin&download=custom_name.bin",
|
||||
method="GET",
|
||||
)
|
||||
assert response.code == 200
|
||||
assert "custom_name.bin" in response.headers["Content-Disposition"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("mock_ext_storage_path")
|
||||
async def test_download_binary_handler_idedata_fallback(
|
||||
dashboard: DashboardTestHelper,
|
||||
tmp_path: Path,
|
||||
mock_async_run_system_command: MagicMock,
|
||||
mock_storage_json: MagicMock,
|
||||
mock_idedata: MagicMock,
|
||||
) -> None:
|
||||
"""Test the DownloadBinaryRequestHandler.get falling back to idedata for extra images."""
|
||||
# Create build directory but no bootloader file initially
|
||||
build_dir = tmp_path / ".esphome" / "build" / "test"
|
||||
build_dir.mkdir(parents=True)
|
||||
firmware_file = build_dir / "firmware.bin"
|
||||
firmware_file.write_bytes(b"firmware")
|
||||
|
||||
# Create bootloader file that idedata will find
|
||||
bootloader_file = tmp_path / "bootloader.bin"
|
||||
bootloader_file.write_bytes(b"bootloader content")
|
||||
|
||||
# Mock storage JSON
|
||||
mock_storage = Mock()
|
||||
mock_storage.name = "test_device"
|
||||
mock_storage.firmware_bin_path = str(firmware_file)
|
||||
mock_storage_json.load.return_value = mock_storage
|
||||
|
||||
# Mock idedata response
|
||||
mock_image = Mock()
|
||||
mock_image.path = str(bootloader_file)
|
||||
mock_idedata_instance = Mock()
|
||||
mock_idedata_instance.extra_flash_images = [mock_image]
|
||||
mock_idedata.return_value = mock_idedata_instance
|
||||
|
||||
# Mock async_run_system_command to return idedata JSON
|
||||
mock_async_run_system_command.return_value = (0, '{"extra_flash_images": []}', "")
|
||||
|
||||
response = await dashboard.fetch(
|
||||
"/download.bin?configuration=test.yaml&file=bootloader.bin",
|
||||
method="GET",
|
||||
)
|
||||
assert response.code == 200
|
||||
assert response.body == b"bootloader content"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_request_handler_post_invalid_file(
|
||||
dashboard: DashboardTestHelper,
|
||||
) -> None:
|
||||
"""Test the EditRequestHandler.post with non-yaml file."""
|
||||
with pytest.raises(HTTPClientError) as exc_info:
|
||||
await dashboard.fetch(
|
||||
"/edit?configuration=test.txt",
|
||||
method="POST",
|
||||
body=b"content",
|
||||
)
|
||||
assert exc_info.value.code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_request_handler_post_existing(
|
||||
dashboard: DashboardTestHelper,
|
||||
tmp_path: Path,
|
||||
mock_dashboard_settings: MagicMock,
|
||||
) -> None:
|
||||
"""Test the EditRequestHandler.post with existing yaml file."""
|
||||
# Create a temporary yaml file to edit (don't modify fixtures)
|
||||
test_file = tmp_path / "test_edit.yaml"
|
||||
test_file.write_text("esphome:\n name: original\n")
|
||||
|
||||
# Configure the mock settings
|
||||
mock_dashboard_settings.rel_path.return_value = str(test_file)
|
||||
mock_dashboard_settings.absolute_config_dir = test_file.parent
|
||||
|
||||
new_content = "esphome:\n name: modified\n"
|
||||
response = await dashboard.fetch(
|
||||
"/edit?configuration=test_edit.yaml",
|
||||
method="POST",
|
||||
body=new_content.encode(),
|
||||
)
|
||||
assert response.code == 200
|
||||
|
||||
# Verify the file was actually modified
|
||||
assert test_file.read_text() == new_content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unarchive_request_handler(
|
||||
dashboard: DashboardTestHelper,
|
||||
mock_archive_storage_path: MagicMock,
|
||||
mock_dashboard_settings: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test the UnArchiveRequestHandler.post method."""
|
||||
# Set up an archived file
|
||||
archive_dir = Path(mock_archive_storage_path.return_value)
|
||||
archive_dir.mkdir(parents=True, exist_ok=True)
|
||||
archived_file = archive_dir / "archived.yaml"
|
||||
archived_file.write_text("test content")
|
||||
|
||||
# Set up the destination path where the file should be moved
|
||||
config_dir = tmp_path / "config"
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
destination_file = config_dir / "archived.yaml"
|
||||
mock_dashboard_settings.rel_path.return_value = str(destination_file)
|
||||
|
||||
response = await dashboard.fetch(
|
||||
"/unarchive?configuration=archived.yaml",
|
||||
method="POST",
|
||||
body=b"",
|
||||
)
|
||||
assert response.code == 200
|
||||
|
||||
# Verify the file was actually moved from archive to config
|
||||
assert not archived_file.exists() # File should be gone from archive
|
||||
assert destination_file.exists() # File should now be in config
|
||||
assert destination_file.read_text() == "test content" # Content preserved
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_secret_keys_handler_no_file(dashboard: DashboardTestHelper) -> None:
|
||||
"""Test the SecretKeysRequestHandler.get when no secrets file exists."""
|
||||
# By default, there's no secrets file in the test fixtures
|
||||
with pytest.raises(HTTPClientError) as exc_info:
|
||||
await dashboard.fetch("/secret_keys", method="GET")
|
||||
assert exc_info.value.code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_secret_keys_handler_with_file(
|
||||
dashboard: DashboardTestHelper,
|
||||
tmp_path: Path,
|
||||
mock_dashboard_settings: MagicMock,
|
||||
) -> None:
|
||||
"""Test the SecretKeysRequestHandler.get when secrets file exists."""
|
||||
# Create a secrets file in temp directory
|
||||
secrets_file = tmp_path / "secrets.yaml"
|
||||
secrets_file.write_text(
|
||||
"wifi_ssid: TestNetwork\nwifi_password: TestPass123\napi_key: test_key\n"
|
||||
)
|
||||
|
||||
# Configure mock to return our temp secrets file
|
||||
# Since the file actually exists, os.path.isfile will return True naturally
|
||||
mock_dashboard_settings.rel_path.return_value = str(secrets_file)
|
||||
|
||||
response = await dashboard.fetch("/secret_keys", method="GET")
|
||||
assert response.code == 200
|
||||
data = json.loads(response.body.decode())
|
||||
assert "wifi_ssid" in data
|
||||
assert "wifi_password" in data
|
||||
assert "api_key" in data
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_json_config_handler(
|
||||
dashboard: DashboardTestHelper,
|
||||
mock_async_run_system_command: MagicMock,
|
||||
) -> None:
|
||||
"""Test the JsonConfigRequestHandler.get method."""
|
||||
# This will actually run the esphome config command on pico.yaml
|
||||
mock_output = json.dumps(
|
||||
{
|
||||
"esphome": {"name": "pico"},
|
||||
"esp32": {"board": "esp32dev"},
|
||||
}
|
||||
)
|
||||
mock_async_run_system_command.return_value = (0, mock_output, "")
|
||||
|
||||
response = await dashboard.fetch(
|
||||
"/json-config?configuration=pico.yaml", method="GET"
|
||||
)
|
||||
assert response.code == 200
|
||||
data = json.loads(response.body.decode())
|
||||
assert data["esphome"]["name"] == "pico"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_json_config_handler_invalid_config(
|
||||
dashboard: DashboardTestHelper,
|
||||
mock_async_run_system_command: MagicMock,
|
||||
) -> None:
|
||||
"""Test the JsonConfigRequestHandler.get with invalid config."""
|
||||
# Simulate esphome config command failure
|
||||
mock_async_run_system_command.return_value = (1, "", "Error: Invalid configuration")
|
||||
|
||||
with pytest.raises(HTTPClientError) as exc_info:
|
||||
await dashboard.fetch("/json-config?configuration=pico.yaml", method="GET")
|
||||
assert exc_info.value.code == 422
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_json_config_handler_not_found(dashboard: DashboardTestHelper) -> None:
|
||||
"""Test the JsonConfigRequestHandler.get with non-existent file."""
|
||||
with pytest.raises(HTTPClientError) as exc_info:
|
||||
await dashboard.fetch(
|
||||
"/json-config?configuration=nonexistent.yaml", method="GET"
|
||||
)
|
||||
assert exc_info.value.code == 404
|
||||
|
||||
|
||||
def test_start_web_server_with_address_port(
|
||||
tmp_path: Path,
|
||||
mock_trash_storage_path: MagicMock,
|
||||
mock_archive_storage_path: MagicMock,
|
||||
) -> None:
|
||||
"""Test the start_web_server function with address and port."""
|
||||
app = Mock()
|
||||
trash_dir = Path(mock_trash_storage_path.return_value)
|
||||
archive_dir = Path(mock_archive_storage_path.return_value)
|
||||
|
||||
# Create trash dir to test migration
|
||||
trash_dir.mkdir()
|
||||
(trash_dir / "old.yaml").write_text("old")
|
||||
|
||||
web_server.start_web_server(app, None, "127.0.0.1", 6052, str(tmp_path / "config"))
|
||||
|
||||
# The function calls app.listen directly for non-socket mode
|
||||
app.listen.assert_called_once_with(6052, "127.0.0.1")
|
||||
|
||||
# Verify trash was moved to archive
|
||||
assert not trash_dir.exists()
|
||||
assert archive_dir.exists()
|
||||
assert (archive_dir / "old.yaml").exists()
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="Unix sockets are not supported on Windows")
|
||||
@pytest.mark.usefixtures("mock_trash_storage_path", "mock_archive_storage_path")
|
||||
def test_start_web_server_with_unix_socket(tmp_path: Path) -> None:
|
||||
"""Test the start_web_server function with unix socket."""
|
||||
app = Mock()
|
||||
socket_path = tmp_path / "test.sock"
|
||||
|
||||
# Don't create trash_dir - it doesn't exist, so no migration needed
|
||||
with (
|
||||
patch("tornado.httpserver.HTTPServer") as mock_server_class,
|
||||
patch("tornado.netutil.bind_unix_socket") as mock_bind,
|
||||
):
|
||||
server = Mock()
|
||||
mock_server_class.return_value = server
|
||||
mock_bind.return_value = Mock()
|
||||
|
||||
web_server.start_web_server(
|
||||
app, str(socket_path), None, None, str(tmp_path / "config")
|
||||
)
|
||||
|
||||
mock_server_class.assert_called_once_with(app)
|
||||
mock_bind.assert_called_once_with(str(socket_path), mode=0o666)
|
||||
server.add_socket.assert_called_once()
|
||||
|
512
tests/unit_tests/test_main.py
Normal file
512
tests/unit_tests/test_main.py
Normal file
@@ -0,0 +1,512 @@
|
||||
"""Unit tests for esphome.__main__ module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.__main__ import choose_upload_log_host
|
||||
from esphome.const import CONF_BROKER, CONF_MQTT, CONF_USE_ADDRESS, CONF_WIFI
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockSerialPort:
|
||||
"""Mock serial port for testing.
|
||||
|
||||
Attributes:
|
||||
path (str): The device path of the mock serial port (e.g., '/dev/ttyUSB0').
|
||||
description (str): A human-readable description of the mock serial port.
|
||||
"""
|
||||
|
||||
path: str
|
||||
description: str
|
||||
|
||||
|
||||
def setup_core(
|
||||
config: dict[str, Any] | None = None, address: str | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Helper to set up CORE configuration with optional address.
|
||||
|
||||
Args:
|
||||
config (dict[str, Any] | None): The configuration dictionary to set for CORE. If None, an empty dict is used.
|
||||
address (str | None): Optional network address to set in the configuration. If provided, it is set under the wifi config.
|
||||
"""
|
||||
if config is None:
|
||||
config = {}
|
||||
|
||||
if address is not None:
|
||||
# Set address via wifi config (could also use ethernet)
|
||||
config[CONF_WIFI] = {CONF_USE_ADDRESS: address}
|
||||
|
||||
CORE.config = config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_no_serial_ports() -> Generator[Mock]:
|
||||
"""Mock get_serial_ports to return no ports."""
|
||||
with patch("esphome.__main__.get_serial_ports", return_value=[]) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_serial_ports() -> Generator[Mock]:
|
||||
"""Mock get_serial_ports to return test ports."""
|
||||
mock_ports = [
|
||||
MockSerialPort("/dev/ttyUSB0", "USB Serial"),
|
||||
MockSerialPort("/dev/ttyUSB1", "Another USB Serial"),
|
||||
]
|
||||
with patch("esphome.__main__.get_serial_ports", return_value=mock_ports) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_choose_prompt() -> Generator[Mock]:
|
||||
"""Mock choose_prompt to return default selection."""
|
||||
with patch("esphome.__main__.choose_prompt", return_value="/dev/ttyUSB0") as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_no_mqtt_logging() -> Generator[Mock]:
|
||||
"""Mock has_mqtt_logging to return False."""
|
||||
with patch("esphome.__main__.has_mqtt_logging", return_value=False) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_has_mqtt_logging() -> Generator[Mock]:
|
||||
"""Mock has_mqtt_logging to return True."""
|
||||
with patch("esphome.__main__.has_mqtt_logging", return_value=True) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
def test_choose_upload_log_host_with_string_default() -> None:
|
||||
"""Test with a single string default device."""
|
||||
result = choose_upload_log_host(
|
||||
default="192.168.1.100",
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
show_mqtt=False,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == ["192.168.1.100"]
|
||||
|
||||
|
||||
def test_choose_upload_log_host_with_list_default() -> None:
|
||||
"""Test with a list of default devices."""
|
||||
result = choose_upload_log_host(
|
||||
default=["192.168.1.100", "192.168.1.101"],
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
show_mqtt=False,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == ["192.168.1.100", "192.168.1.101"]
|
||||
|
||||
|
||||
def test_choose_upload_log_host_with_multiple_ip_addresses() -> None:
|
||||
"""Test with multiple IP addresses as defaults."""
|
||||
result = choose_upload_log_host(
|
||||
default=["1.2.3.4", "4.5.5.6"],
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
show_mqtt=False,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == ["1.2.3.4", "4.5.5.6"]
|
||||
|
||||
|
||||
def test_choose_upload_log_host_with_mixed_hostnames_and_ips() -> None:
|
||||
"""Test with a mix of hostnames and IP addresses."""
|
||||
result = choose_upload_log_host(
|
||||
default=["host.one", "host.one.local", "1.2.3.4"],
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
show_mqtt=False,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == ["host.one", "host.one.local", "1.2.3.4"]
|
||||
|
||||
|
||||
def test_choose_upload_log_host_with_ota_list() -> None:
|
||||
"""Test with OTA as the only item in the list."""
|
||||
setup_core(config={"ota": {}}, address="192.168.1.100")
|
||||
|
||||
result = choose_upload_log_host(
|
||||
default=["OTA"],
|
||||
check_default=None,
|
||||
show_ota=True,
|
||||
show_mqtt=False,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == ["192.168.1.100"]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_has_mqtt_logging")
|
||||
def test_choose_upload_log_host_with_ota_list_mqtt_fallback() -> None:
|
||||
"""Test with OTA list falling back to MQTT when no address."""
|
||||
setup_core()
|
||||
|
||||
result = choose_upload_log_host(
|
||||
default=["OTA"],
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
show_mqtt=True,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == ["MQTT"]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_no_serial_ports")
|
||||
def test_choose_upload_log_host_with_serial_device_no_ports(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test SERIAL device when no serial ports are found."""
|
||||
result = choose_upload_log_host(
|
||||
default="SERIAL",
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
show_mqtt=False,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == []
|
||||
assert "No serial ports found, skipping SERIAL device" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_serial_ports")
|
||||
def test_choose_upload_log_host_with_serial_device_with_ports(
|
||||
mock_choose_prompt: Mock,
|
||||
) -> None:
|
||||
"""Test SERIAL device when serial ports are available."""
|
||||
result = choose_upload_log_host(
|
||||
default="SERIAL",
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
show_mqtt=False,
|
||||
show_api=False,
|
||||
purpose="testing",
|
||||
)
|
||||
assert result == ["/dev/ttyUSB0"]
|
||||
mock_choose_prompt.assert_called_once_with(
|
||||
[
|
||||
("/dev/ttyUSB0 (USB Serial)", "/dev/ttyUSB0"),
|
||||
("/dev/ttyUSB1 (Another USB Serial)", "/dev/ttyUSB1"),
|
||||
],
|
||||
purpose="testing",
|
||||
)
|
||||
|
||||
|
||||
def test_choose_upload_log_host_with_ota_device_with_ota_config() -> None:
|
||||
"""Test OTA device when OTA is configured."""
|
||||
setup_core(config={"ota": {}}, address="192.168.1.100")
|
||||
|
||||
result = choose_upload_log_host(
|
||||
default="OTA",
|
||||
check_default=None,
|
||||
show_ota=True,
|
||||
show_mqtt=False,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == ["192.168.1.100"]
|
||||
|
||||
|
||||
def test_choose_upload_log_host_with_ota_device_with_api_config() -> None:
|
||||
"""Test OTA device when API is configured."""
|
||||
setup_core(config={"api": {}}, address="192.168.1.100")
|
||||
|
||||
result = choose_upload_log_host(
|
||||
default="OTA",
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
show_mqtt=False,
|
||||
show_api=True,
|
||||
)
|
||||
assert result == ["192.168.1.100"]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_has_mqtt_logging")
|
||||
def test_choose_upload_log_host_with_ota_device_fallback_to_mqtt() -> None:
|
||||
"""Test OTA device fallback to MQTT when no OTA/API config."""
|
||||
setup_core()
|
||||
|
||||
result = choose_upload_log_host(
|
||||
default="OTA",
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
show_mqtt=True,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == ["MQTT"]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_no_mqtt_logging")
|
||||
def test_choose_upload_log_host_with_ota_device_no_fallback() -> None:
|
||||
"""Test OTA device with no valid fallback options."""
|
||||
setup_core()
|
||||
|
||||
result = choose_upload_log_host(
|
||||
default="OTA",
|
||||
check_default=None,
|
||||
show_ota=True,
|
||||
show_mqtt=True,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == []
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_choose_prompt")
|
||||
def test_choose_upload_log_host_multiple_devices() -> None:
|
||||
"""Test with multiple devices including special identifiers."""
|
||||
setup_core(config={"ota": {}}, address="192.168.1.100")
|
||||
|
||||
mock_ports = [MockSerialPort("/dev/ttyUSB0", "USB Serial")]
|
||||
|
||||
with patch("esphome.__main__.get_serial_ports", return_value=mock_ports):
|
||||
result = choose_upload_log_host(
|
||||
default=["192.168.1.50", "OTA", "SERIAL"],
|
||||
check_default=None,
|
||||
show_ota=True,
|
||||
show_mqtt=False,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == ["192.168.1.50", "192.168.1.100", "/dev/ttyUSB0"]
|
||||
|
||||
|
||||
def test_choose_upload_log_host_no_defaults_with_serial_ports(
|
||||
mock_choose_prompt: Mock,
|
||||
) -> None:
|
||||
"""Test interactive mode with serial ports available."""
|
||||
mock_ports = [
|
||||
MockSerialPort("/dev/ttyUSB0", "USB Serial"),
|
||||
]
|
||||
|
||||
setup_core()
|
||||
|
||||
with patch("esphome.__main__.get_serial_ports", return_value=mock_ports):
|
||||
result = choose_upload_log_host(
|
||||
default=None,
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
show_mqtt=False,
|
||||
show_api=False,
|
||||
purpose="uploading",
|
||||
)
|
||||
assert result == ["/dev/ttyUSB0"]
|
||||
mock_choose_prompt.assert_called_once_with(
|
||||
[("/dev/ttyUSB0 (USB Serial)", "/dev/ttyUSB0")],
|
||||
purpose="uploading",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_no_serial_ports")
|
||||
def test_choose_upload_log_host_no_defaults_with_ota() -> None:
|
||||
"""Test interactive mode with OTA option."""
|
||||
setup_core(config={"ota": {}}, address="192.168.1.100")
|
||||
|
||||
with patch(
|
||||
"esphome.__main__.choose_prompt", return_value="192.168.1.100"
|
||||
) as mock_prompt:
|
||||
result = choose_upload_log_host(
|
||||
default=None,
|
||||
check_default=None,
|
||||
show_ota=True,
|
||||
show_mqtt=False,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == ["192.168.1.100"]
|
||||
mock_prompt.assert_called_once_with(
|
||||
[("Over The Air (192.168.1.100)", "192.168.1.100")],
|
||||
purpose=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_no_serial_ports")
|
||||
def test_choose_upload_log_host_no_defaults_with_api() -> None:
|
||||
"""Test interactive mode with API option."""
|
||||
setup_core(config={"api": {}}, address="192.168.1.100")
|
||||
|
||||
with patch(
|
||||
"esphome.__main__.choose_prompt", return_value="192.168.1.100"
|
||||
) as mock_prompt:
|
||||
result = choose_upload_log_host(
|
||||
default=None,
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
show_mqtt=False,
|
||||
show_api=True,
|
||||
)
|
||||
assert result == ["192.168.1.100"]
|
||||
mock_prompt.assert_called_once_with(
|
||||
[("Over The Air (192.168.1.100)", "192.168.1.100")],
|
||||
purpose=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_no_serial_ports", "mock_has_mqtt_logging")
|
||||
def test_choose_upload_log_host_no_defaults_with_mqtt() -> None:
|
||||
"""Test interactive mode with MQTT option."""
|
||||
setup_core(config={CONF_MQTT: {CONF_BROKER: "mqtt.local"}})
|
||||
|
||||
with patch("esphome.__main__.choose_prompt", return_value="MQTT") as mock_prompt:
|
||||
result = choose_upload_log_host(
|
||||
default=None,
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
show_mqtt=True,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == ["MQTT"]
|
||||
mock_prompt.assert_called_once_with(
|
||||
[("MQTT (mqtt.local)", "MQTT")],
|
||||
purpose=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_has_mqtt_logging")
|
||||
def test_choose_upload_log_host_no_defaults_with_all_options(
|
||||
mock_choose_prompt: Mock,
|
||||
) -> None:
|
||||
"""Test interactive mode with all options available."""
|
||||
setup_core(
|
||||
config={"ota": {}, "api": {}, CONF_MQTT: {CONF_BROKER: "mqtt.local"}},
|
||||
address="192.168.1.100",
|
||||
)
|
||||
|
||||
mock_ports = [MockSerialPort("/dev/ttyUSB0", "USB Serial")]
|
||||
|
||||
with patch("esphome.__main__.get_serial_ports", return_value=mock_ports):
|
||||
result = choose_upload_log_host(
|
||||
default=None,
|
||||
check_default=None,
|
||||
show_ota=True,
|
||||
show_mqtt=True,
|
||||
show_api=True,
|
||||
purpose="testing",
|
||||
)
|
||||
assert result == ["/dev/ttyUSB0"]
|
||||
|
||||
expected_options = [
|
||||
("/dev/ttyUSB0 (USB Serial)", "/dev/ttyUSB0"),
|
||||
("Over The Air (192.168.1.100)", "192.168.1.100"),
|
||||
("MQTT (mqtt.local)", "MQTT"),
|
||||
]
|
||||
mock_choose_prompt.assert_called_once_with(expected_options, purpose="testing")
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_no_serial_ports")
|
||||
def test_choose_upload_log_host_check_default_matches() -> None:
|
||||
"""Test when check_default matches an available option."""
|
||||
setup_core(config={"ota": {}}, address="192.168.1.100")
|
||||
|
||||
result = choose_upload_log_host(
|
||||
default=None,
|
||||
check_default="192.168.1.100",
|
||||
show_ota=True,
|
||||
show_mqtt=False,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == ["192.168.1.100"]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_no_serial_ports")
|
||||
def test_choose_upload_log_host_check_default_no_match() -> None:
|
||||
"""Test when check_default doesn't match any available option."""
|
||||
setup_core()
|
||||
|
||||
with patch(
|
||||
"esphome.__main__.choose_prompt", return_value="fallback"
|
||||
) as mock_prompt:
|
||||
result = choose_upload_log_host(
|
||||
default=None,
|
||||
check_default="192.168.1.100",
|
||||
show_ota=False,
|
||||
show_mqtt=False,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == ["fallback"]
|
||||
mock_prompt.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_no_serial_ports")
|
||||
def test_choose_upload_log_host_empty_defaults_list() -> None:
|
||||
"""Test with an empty list as default."""
|
||||
with patch("esphome.__main__.choose_prompt", return_value="chosen") as mock_prompt:
|
||||
result = choose_upload_log_host(
|
||||
default=[],
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
show_mqtt=False,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == ["chosen"]
|
||||
mock_prompt.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_no_serial_ports", "mock_no_mqtt_logging")
|
||||
def test_choose_upload_log_host_all_devices_unresolved(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test when all specified devices cannot be resolved."""
|
||||
setup_core()
|
||||
|
||||
result = choose_upload_log_host(
|
||||
default=["SERIAL", "OTA"],
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
show_mqtt=False,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == []
|
||||
assert (
|
||||
"All specified devices: ['SERIAL', 'OTA'] could not be resolved." in caplog.text
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_no_serial_ports", "mock_no_mqtt_logging")
|
||||
def test_choose_upload_log_host_mixed_resolved_unresolved() -> None:
|
||||
"""Test with a mix of resolved and unresolved devices."""
|
||||
setup_core()
|
||||
|
||||
result = choose_upload_log_host(
|
||||
default=["192.168.1.50", "SERIAL", "OTA"],
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
show_mqtt=False,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == ["192.168.1.50"]
|
||||
|
||||
|
||||
def test_choose_upload_log_host_ota_both_conditions() -> None:
|
||||
"""Test OTA device when both OTA and API are configured and enabled."""
|
||||
setup_core(config={"ota": {}, "api": {}}, address="192.168.1.100")
|
||||
|
||||
result = choose_upload_log_host(
|
||||
default="OTA",
|
||||
check_default=None,
|
||||
show_ota=True,
|
||||
show_mqtt=False,
|
||||
show_api=True,
|
||||
)
|
||||
assert result == ["192.168.1.100"]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_no_mqtt_logging")
|
||||
def test_choose_upload_log_host_no_address_with_ota_config() -> None:
|
||||
"""Test OTA device when OTA is configured but no address is set."""
|
||||
setup_core(config={"ota": {}})
|
||||
|
||||
result = choose_upload_log_host(
|
||||
default="OTA",
|
||||
check_default=None,
|
||||
show_ota=True,
|
||||
show_mqtt=False,
|
||||
show_api=False,
|
||||
)
|
||||
assert result == []
|
Reference in New Issue
Block a user