Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain

# Conflicts:
#	esphome/components/esp8266/__init__.py
This commit is contained in:
J. Nick Koston
2026-09-03 23:48:38 +02:00
129 changed files with 2545 additions and 675 deletions
@@ -26,7 +26,7 @@ from ..types import SetCoreConfigCallable
(PlatformFramework.ESP32_IDF, None, True),
(PlatformFramework.RTL87XX_ARDUINO, FAMILY_RTL8720C, True),
(PlatformFramework.RTL87XX_ARDUINO, FAMILY_RTL8710B, False),
(PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7231N, True),
(PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7231N, False),
(PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7238, True),
(PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7231T, False),
(PlatformFramework.ESP8266_ARDUINO, None, False),
+101
View File
@@ -0,0 +1,101 @@
"""Tests for the sdl display schema, in particular the headless option."""
from __future__ import annotations
import pytest
from esphome import config_validation as cv
from esphome.components.sdl.display import (
CONF_SDL_ID,
CONFIG_SCHEMA,
headless_final_validate,
)
from esphome.config import Config
from esphome.const import PlatformFramework
from esphome.core import ID
from esphome.final_validate import full_config
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
@pytest.fixture(autouse=True)
def _host_platform(set_core_config: SetCoreConfigCallable) -> None:
set_core_config(PlatformFramework.HOST_NATIVE)
def _config(**extra: object) -> ConfigType:
config: ConfigType = {
"dimensions": {"width": 320, "height": 240},
# sdl2-config is not necessarily installed in the test environment
"sdl_options": "-lSDL2",
}
config.update(extra)
return config
def test_defaults_to_windowed() -> None:
"""A display without the option is not headless."""
assert CONFIG_SCHEMA(_config())["headless"] is False
def test_headless_accepted() -> None:
"""A headless display needs nothing beyond the dimensions."""
assert CONFIG_SCHEMA(_config(headless=True))["headless"] is True
def test_headless_rejects_window_options() -> None:
"""Window options are meaningless without a window."""
with pytest.raises(cv.Invalid, match="has no effect"):
CONFIG_SCHEMA(
_config(headless=True, window_options={"position": {"x": 0, "y": 0}})
)
def test_headless_rejects_snapshot_key() -> None:
"""A headless display has no keyboard, so the action is the only way in."""
with pytest.raises(cv.Invalid, match="snapshot.take"):
CONFIG_SCHEMA(_config(headless=True, snapshot_key="SDLK_F12"))
def test_snapshot_key_accepted_when_windowed() -> None:
"""The key is only valid alongside a window."""
config = CONFIG_SCHEMA(_config(snapshot_key="SDLK_F12"))
assert str(config["snapshot_key"]) == "SDLK_F12"
def _declare_sdl_display(headless: bool) -> ID:
"""Register a full_config with a single sdl display declaration and return a reference to it.
Mirrors what the real config pipeline leaves behind: a "display" domain entry plus a
declare_ids record id_declaration_match_schema uses to find it again.
"""
declared_id = ID("my_sdl", is_declaration=True)
fc = Config()
fc["display"] = [
{
"platform": "sdl",
"id": declared_id,
"headless": headless,
"dimensions": {"width": 320, "height": 240},
}
]
fc.declare_ids.append((declared_id, ["display", 0, "id"]))
full_config.set(fc)
return ID("my_sdl")
@pytest.mark.parametrize("platform", ["binary_sensor", "touchscreen"])
def test_headless_final_validate_rejects_headless_display(platform: str) -> None:
"""binary_sensor and touchscreen both need a window, so a headless display is rejected."""
sdl_ref = _declare_sdl_display(headless=True)
schema = headless_final_validate(platform)
with pytest.raises(cv.Invalid, match="needs a window"):
schema({CONF_SDL_ID: sdl_ref})
@pytest.mark.parametrize("platform", ["binary_sensor", "touchscreen"])
def test_headless_final_validate_accepts_windowed_display(platform: str) -> None:
"""The same platforms are accepted once the display has a window."""
sdl_ref = _declare_sdl_display(headless=False)
schema = headless_final_validate(platform)
schema({CONF_SDL_ID: sdl_ref}) # Should not raise.
+1 -1
View File
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO14
packages:
uart_38400: !include ../../test_build_components/common/uart_38400/esp32-idf.yaml
uart_38400_even: !include ../../test_build_components/common/uart_38400_even/esp32-idf.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO3
packages:
uart_38400: !include ../../test_build_components/common/uart_38400/esp8266-ard.yaml
uart_38400_even: !include ../../test_build_components/common/uart_38400_even/esp8266-ard.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO5
packages:
uart_38400: !include ../../test_build_components/common/uart_38400/rp2040-ard.yaml
uart_38400_even: !include ../../test_build_components/common/uart_38400_even/rp2040-ard.yaml
<<: !include common.yaml
@@ -1,4 +1,4 @@
packages:
uart_1200: !include ../../test_build_components/common/uart_1200/esp32-idf.yaml
uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp32-idf.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
uart_rx_pin: GPIO3
packages:
uart_1200: !include ../../test_build_components/common/uart_1200/esp8266-ard.yaml
uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp8266-ard.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO5
packages:
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO2
packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO5
packages:
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
uart_115200: !include ../../test_build_components/common/uart_115200/rp2040-ard.yaml
<<: !include common.yaml
@@ -2,7 +2,7 @@ remote_transmitter:
id: xmitr
pin: GPIO26
carrier_duty_percent: 50%
# non_blocking is bk7231n/bk7238-only; the CI board is a BK7252
# non_blocking is bk7238-only; the CI board is a BK7252, so this builds the bit-bang path
packages:
buttons: !include common-buttons.yaml
+27
View File
@@ -14,6 +14,15 @@ display:
position:
x: 100
y: 100
snapshot_key: SDLK_F12
- platform: sdl
id: headless_display
headless: true
show_test_card: true
dimensions:
width: 320
height: 240
- platform: sdl
id: second_display
@@ -46,3 +55,21 @@ binary_sensor:
sdl_id: sdl_sdl_display
id: key_enter
key: SDLK_RETURN
esphome:
# A name of your own is only good for one snapshot - a second one under the same name fails
# rather than writing over the first - so these run once rather than on a repeating interval.
on_boot:
- delay: 2s
- snapshot.take:
id: headless_display
filename: test_card.bmp
- snapshot.take:
id: headless_display
filename: !lambda 'return "shot.bmp";'
interval:
# A generated name has the time in it, so this one can repeat.
- interval: 10s
then:
- snapshot.take: sdl_sdl_display
+29
View File
@@ -0,0 +1,29 @@
# Config-only test for the headless and screenshot options. The combinations that must be
# rejected are covered by tests/component_tests/sdl/test_sdl.py; this file checks that the
# accepted forms validate together.
host:
mac_address: "62:23:45:AF:B3:DD"
display:
- platform: sdl
id: headless_display
headless: true
dimensions: 320x240
- platform: sdl
id: windowed_display
dimensions: 320x240
snapshot_key: SDLK_F12
binary_sensor:
- platform: sdl
sdl_id: windowed_display
id: key_up
key: SDLK_UP
interval:
- interval: 10s
then:
- snapshot.take:
id: headless_display
filename: periodic.bmp
+34
View File
@@ -0,0 +1,34 @@
display:
- platform: snapshot
id: snapshot_display
update_interval: 1s
show_test_card: true
# An odd width exercises the row padding in the BMP writer
dimensions:
width: 101
height: 64
- platform: snapshot
id: snapshot_rotated
rotation: 90
dimensions: 320x240
lambda: |-
it.filled_rectangle(0, 0, 40, 20, Color(0xFF, 0x80, 0x00));
esphome:
# A name of your own is only good for one snapshot - a second one under the same name fails
# rather than writing over the first - so these run once rather than on a repeating interval.
on_boot:
- delay: 2s
- snapshot.take:
id: snapshot_display
filename: test_card.bmp
- snapshot.take:
id: snapshot_rotated
filename: !lambda 'return "rotated.bmp";'
interval:
# A generated name has the time in it, so this one can repeat.
- interval: 10s
then:
- snapshot.take: snapshot_display
+5
View File
@@ -0,0 +1,5 @@
host:
mac_address: "62:23:45:AF:B3:DE"
packages:
snapshot: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO5
packages:
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
uart_1200_even_7bits: !include ../../test_build_components/common/uart_1200_even_7bits/esp32-idf.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO2
packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
uart_1200_even_7bits: !include ../../test_build_components/common/uart_1200_even_7bits/esp8266-ard.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO5
packages:
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
uart_1200_even_7bits: !include ../../test_build_components/common/uart_1200_even_7bits/rp2040-ard.yaml
<<: !include common.yaml
@@ -0,0 +1,14 @@
packages:
uart_9600_even_7bits: !include ../../test_build_components/common/uart_9600_even_7bits/esp32-idf.yaml
teleinfo:
id: test_teleinfo_standard
historical_mode: false
update_interval: 60s
sensor:
- platform: teleinfo
name: sinsts
tag_name: SINSTS
teleinfo_id: test_teleinfo_standard
unit_of_measurement: VA
+26
View File
@@ -0,0 +1,26 @@
"""Shared utilities for ESPHome integration tests - keeping output from failing tests."""
from __future__ import annotations
from pathlib import Path
#: Where a failing test leaves output for someone to look at afterwards. pytest's own
#: temporary folder is no use on a CI runner, which throws the whole workspace away when
#: the job ends; the workflow uploads this folder instead when a job fails.
ARTIFACT_DIR = Path(__file__).resolve().parents[2] / "test_artifacts"
def keep_artifact(name: str, data: bytes) -> Path:
"""Write ``data`` where it can still be read after the run, and return the path.
Args:
name: File name to write under the artifact folder.
data: Contents to write.
Returns:
The full path written.
"""
ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)
path = ARTIFACT_DIR / name
path.write_bytes(data)
return path
+161
View File
@@ -0,0 +1,161 @@
"""Shared utilities for ESPHome integration tests - reading BMP snapshots."""
from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from pathlib import Path
import struct
# Size of the smallest BMP header pair (file header plus BITMAPINFOHEADER).
_MIN_HEADER_SIZE = 54
# How long capture_when_drawn() keeps asking for a picture with something on it.
DRAW_TIMEOUT = 15.0
@dataclass(frozen=True)
class Bmp:
"""A decoded BMP image."""
width: int
height: int
bits: int
#: Pixel data with the per row padding stripped, so it depends only on the image itself.
pixels: bytes
class NotABmpError(Exception):
"""The data is not a BMP at all, as opposed to a BMP that is still being written."""
def parse_bmp(data: bytes) -> Bmp | None:
"""Decode a BMP, or return None if the data is not a complete image yet.
Raises:
NotABmpError: If the data cannot become a valid BMP however much more is appended.
"""
# Writes go to the file in order, so a short read is always a prefix of what will be there.
# Anything wrong in a prefix we have already read is wrong for good, and worth saying now
# rather than reporting as a timeout later.
if len(data) >= 2 and data[:2] != b"BM":
raise NotABmpError(f"expected a BMP, got {data[:2]!r}")
if len(data) < _MIN_HEADER_SIZE:
return None
file_size = struct.unpack_from("<I", data, 2)[0]
offset = struct.unpack_from("<I", data, 10)[0]
width, height = struct.unpack_from("<ii", data, 18)
bits = struct.unpack_from("<H", data, 28)[0]
rows = abs(height)
row_size = ((width * bits + 31) // 32) * 4
if width <= 0 or rows == 0 or bits == 0 or offset < _MIN_HEADER_SIZE:
raise NotABmpError(
f"BMP header makes no sense: {width}x{height}, {bits} bits, "
f"pixels at offset {offset}"
)
if file_size < offset + row_size * rows:
raise NotABmpError(
f"BMP header claims {file_size} bytes, too few for {width}x{rows} "
f"at {bits} bits"
)
if len(data) < file_size:
return None
used = width * bits // 8
pixels = b"".join(
data[offset + row * row_size : offset + row * row_size + used]
for row in range(rows)
)
return Bmp(width=width, height=rows, bits=bits, pixels=pixels)
async def wait_for_bmp(path: Path, timeout: float = 5.0) -> Bmp:
"""Wait for a complete BMP file to appear at ``path`` and return it.
The file is created before any of its contents are written, so waiting for it to exist is
not enough - a read that wins the race sees a truncated image. Keep reading until the
headers say the whole image is there.
Args:
path: The file to wait for.
timeout: Maximum time to wait in seconds.
Returns:
The decoded image.
Raises:
AssertionError: If no complete image is readable within ``timeout``.
NotABmpError: If what was written is not a BMP. This is reported as soon as it is
seen, so a device that writes the wrong thing is named for what it did rather
than waiting out the timeout.
"""
loop = asyncio.get_running_loop()
deadline = loop.time() + timeout
while True:
try:
data = path.read_bytes()
except FileNotFoundError:
data = b""
if (image := parse_bmp(data)) is not None:
return image
if loop.time() >= deadline:
break
await asyncio.sleep(0.05)
if not data:
raise AssertionError(f"no snapshot appeared at {path} within {timeout}s")
raise AssertionError(
f"{path} was still incomplete after {timeout}s ({len(data)} bytes)"
)
def is_blank(image: Bmp) -> bool:
"""True if every pixel of the image is the same colour.
Whole pixels are counted rather than byte values: a plain background is usually made of more
than one distinct byte, so counting bytes would find several of them in a blank screen.
"""
return len({image.pixels[i : i + 3] for i in range(0, len(image.pixels), 3)}) <= 1
async def capture_when_drawn(
take: Callable[[str], Awaitable[None]],
directory: Path,
prefix: str = "drawn",
timeout: float = DRAW_TIMEOUT,
) -> tuple[Bmp, Path]:
"""Ask for snapshots until one has something drawn on it, and return it and where it went.
A display holds one flat colour until it first draws, which is one update interval after it
starts - long enough that a test connecting over the API can easily get in first. Capturing
once and hoping would compare a blank screen against whatever the test expects, reporting a
drawing fault where the real trouble was timing.
Args:
take: Asks the device for a snapshot under the name it is given.
directory: Where the device writes them.
prefix: Start of the names asked for. Each attempt needs its own, because a snapshot never
writes over a file that is already there.
timeout: How long to keep asking.
Returns:
The first image that is not one flat colour, and the path it was read from.
Raises:
AssertionError: If nothing had been drawn within ``timeout``.
"""
loop = asyncio.get_running_loop()
deadline = loop.time() + timeout
attempt = 0
while True:
attempt += 1
path = directory / f"{prefix}-{attempt}.bmp"
await take(path.name)
image = await wait_for_bmp(path)
if not is_blank(image):
return image, path
if loop.time() >= deadline:
raise AssertionError(
f"the screen was still a single flat colour after {timeout}s and "
f"{attempt} captures - nothing was drawn"
)
await asyncio.sleep(0.5)
@@ -0,0 +1,53 @@
esphome:
name: lvgl-headless-render-test
host:
api:
actions:
# The name comes from the test so it can capture more than once: a snapshot never writes over
# a file that is already there, so a fixed name could only ever be captured once.
- action: take_screenshot
variables:
name: string
then:
- snapshot.take:
id: lvgl_display
filename: !lambda return name;
logger:
level: DEBUG
display:
# A display with no screen, so what LVGL draws depends on LVGL alone - nothing about the machine
# running the test, and no graphics library outside this repository, can move the result.
- platform: snapshot
id: lvgl_display
auto_clear_enabled: false
dimensions:
width: 300
height: 300
# The widgets are spelled out here rather than left to the built in "Hello World" screen, which
# LVGL builds when nothing is configured: that screen contains a spinner, and an animation cannot
# produce the same picture twice.
#
# Everything that affects the rendered pixels is set explicitly, so the expected hash in the test
# depends only on the drawing code and the built in font. In particular the background comes from a
# full screen object rather than from the theme, so adjusting a theme default does not break this.
lvgl:
displays: lvgl_display
default_font: montserrat_14
widgets:
- obj:
width: 100%
height: 100%
bg_color: 0x000080
bg_opa: cover
border_width: 0
radius: 0
pad_all: 0
widgets:
- label:
align: center
text: "Hello World!"
text_color: 0xFFFFFF
@@ -0,0 +1,29 @@
esphome:
name: sdl-headless-screenshot-test
host:
api:
actions:
# The name comes from the test so it can capture more than once while it waits for the first
# frame: a snapshot never writes over a file that is already there.
- action: take_screenshot
variables:
name: string
then:
- snapshot.take:
id: sdl_display
filename: !lambda return name;
logger:
level: DEBUG
display:
- platform: sdl
id: sdl_display
headless: true
show_test_card: true
update_interval: 100ms
# An odd width exercises the row padding in the BMP writer
dimensions:
width: 101
height: 64
@@ -0,0 +1,28 @@
esphome:
name: snapshot-display-test
host:
api:
actions:
# The name comes from the test so it can ask for several in a row and check what each one
# does with it.
- action: take_snapshot
variables:
name: string
then:
- snapshot.take:
id: snapshot_display
filename: !lambda return name;
logger:
level: DEBUG
display:
- platform: snapshot
id: snapshot_display
show_test_card: true
update_interval: 100ms
# An odd width exercises the row padding in the BMP writer
dimensions:
width: 101
height: 64
+141 -140
View File
@@ -1,142 +1,143 @@
{
"tests/integration/test_action_concurrent_reentry.py": 45.23,
"tests/integration/test_addressable_light_transition.py": 74.47,
"tests/integration/test_alarm_control_panel_state_transitions.py": 74.1,
"tests/integration/test_api_action_metadata.py": 62.1,
"tests/integration/test_api_action_responses.py": 71.08,
"tests/integration/test_api_action_timeout.py": 21.64,
"tests/integration/test_api_conditional_memory.py": 13.72,
"tests/integration/test_api_custom_services.py": 24.16,
"tests/integration/test_api_get_time_response_timezone.py": 23.48,
"tests/integration/test_api_homeassistant.py": 37.87,
"tests/integration/test_api_homeassistant_action_no_subscriber.py": 14.38,
"tests/integration/test_api_list_entities_backpressure.py": 26.85,
"tests/integration/test_api_message_size_batching.py": 33.36,
"tests/integration/test_api_reboot_timeout.py": 13.63,
"tests/integration/test_api_string_lambda.py": 25.04,
"tests/integration/test_api_vv_logging.py": 16.6,
"tests/integration/test_api_zero_psk_provisioning.py": 43.14,
"tests/integration/test_areas_and_devices.py": 25.98,
"tests/integration/test_automation_wait_actions.py": 21.91,
"tests/integration/test_automations.py": 42.43,
"tests/integration/test_batch_delay_zero_rapid_transitions.py": 16.65,
"tests/integration/test_binary_sensor_autorepeat_filter.py": 28.67,
"tests/integration/test_binary_sensor_invalidate_state.py": 23.69,
"tests/integration/test_blocking_warning_log_time_not_charged_to_next_operation.py": 22.99,
"tests/integration/test_build_info.py": 24.96,
"tests/integration/test_camera_mock.py": 14.47,
"tests/integration/test_climate_control_action.py": 31.07,
"tests/integration/test_climate_custom_modes.py": 28.59,
"tests/integration/test_continuation_actions.py": 14.96,
"tests/integration/test_cover_control_action.py": 26.14,
"tests/integration/test_crc8_helper.py": 10.92,
"tests/integration/test_device_id_in_state.py": 64.97,
"tests/integration/test_duplicate_entities.py": 30.81,
"tests/integration/test_entity_icon.py": 32.85,
"tests/integration/test_fan_turn_on_action.py": 24.91,
"tests/integration/test_fnv1_hash_object_id.py": 12.54,
"tests/integration/test_fnv1a_hash.py": 21.8,
"tests/integration/test_gpio_expander_cache.py": 5.2,
"tests/integration/test_host_logger_thread_safety.py": 21.7,
"tests/integration/test_host_mode_basic.py": 13.62,
"tests/integration/test_host_mode_batch_delay.py": 14.56,
"tests/integration/test_host_mode_climate_basic_state.py": 30.95,
"tests/integration/test_host_mode_climate_control.py": 29.06,
"tests/integration/test_host_mode_empty_string_options.py": 27.22,
"tests/integration/test_host_mode_entity_fields.py": 30.95,
"tests/integration/test_host_mode_fan_preset.py": 14.44,
"tests/integration/test_host_mode_many_entities.py": 54.13,
"tests/integration/test_host_mode_many_entities_multiple_connections.py": 32.17,
"tests/integration/test_host_mode_noise_encryption.py": 42.77,
"tests/integration/test_host_mode_reconnect.py": 4.06,
"tests/integration/test_host_mode_sensor.py": 13.47,
"tests/integration/test_host_ota.py": 21.4,
"tests/integration/test_host_preferences.py": 25.43,
"tests/integration/test_host_preferences_suspend_resume.py": 19.2,
"tests/integration/test_improv_serial_uart.py": 31.52,
"tests/integration/test_large_message_batching.py": 15.64,
"tests/integration/test_legacy_area.py": 22.63,
"tests/integration/test_legacy_climate_compat.py": 26.13,
"tests/integration/test_legacy_fan_compat.py": 24.05,
"tests/integration/test_light_automations.py": 30.86,
"tests/integration/test_light_binary_effect_off_phase.py": 23.19,
"tests/integration/test_light_calls.py": 32.35,
"tests/integration/test_light_constant_brightness.py": 29.89,
"tests/integration/test_light_control_action.py": 29.06,
"tests/integration/test_light_dim_relative_action.py": 29.61,
"tests/integration/test_light_effect_zero_brightness.py": 18.68,
"tests/integration/test_light_initial_state.py": 24.49,
"tests/integration/test_light_toggle_action.py": 26.46,
"tests/integration/test_lock_automations.py": 23.28,
"tests/integration/test_logger_buffered_recursion_guard.py": 24.29,
"tests/integration/test_loop_disable_enable.py": 45.28,
"tests/integration/test_loop_interval_decoupling.py": 28.35,
"tests/integration/test_loop_interval_default_not_pulled_forward.py": 21.97,
"tests/integration/test_micros_to_millis.py": 20.79,
"tests/integration/test_multi_click_trigger.py": 26.2,
"tests/integration/test_multi_device_preferences.py": 16.87,
"tests/integration/test_noise_encryption_key_protection.py": 77.05,
"tests/integration/test_object_id_api_verification.py": 73.51,
"tests/integration/test_object_id_friendly_name_no_mac_suffix.py": 62.33,
"tests/integration/test_object_id_no_friendly_name.py": 43.47,
"tests/integration/test_online_image_auto_detects_image_bmp_mime.py": 32.21,
"tests/integration/test_online_image_auto_detects_redirected_image_bmp_mime.py": 56.86,
"tests/integration/test_online_image_bmp.py": 50.9,
"tests/integration/test_oversized_payloads.py": 53.2,
"tests/integration/test_preference_key_stability.py": 26.09,
"tests/integration/test_runtime_stats.py": 18.34,
"tests/integration/test_safe_mode_loop_runs.py": 10.07,
"tests/integration/test_scheduler_blocking_warning.py": 40.91,
"tests/integration/test_scheduler_bulk_cleanup.py": 23.14,
"tests/integration/test_scheduler_defer_cancel.py": 24.54,
"tests/integration/test_scheduler_defer_cancel_regular.py": 13.48,
"tests/integration/test_scheduler_defer_fifo_simple.py": 26.86,
"tests/integration/test_scheduler_defer_stress.py": 27.23,
"tests/integration/test_scheduler_heap_stress.py": 24.02,
"tests/integration/test_scheduler_internal_id_no_collision.py": 24.57,
"tests/integration/test_scheduler_interval_reschedule.py": 13.12,
"tests/integration/test_scheduler_interval_zero_coerced.py": 22.91,
"tests/integration/test_scheduler_null_name.py": 23.46,
"tests/integration/test_scheduler_numeric_id_test.py": 24.54,
"tests/integration/test_scheduler_pool.py": 25.0,
"tests/integration/test_scheduler_rapid_cancellation.py": 14.68,
"tests/integration/test_scheduler_recursive_timeout.py": 25.35,
"tests/integration/test_scheduler_removed_item_race.py": 26.19,
"tests/integration/test_scheduler_self_keyed.py": 23.43,
"tests/integration/test_scheduler_simultaneous_callbacks.py": 22.16,
"tests/integration/test_scheduler_string_test.py": 15.22,
"tests/integration/test_script_array_params.py": 14.67,
"tests/integration/test_script_delay_params.py": 15.65,
"tests/integration/test_script_queued.py": 24.93,
"tests/integration/test_script_queued_idle_loop.py": 5.04,
"tests/integration/test_script_wait_on_boot.py": 13.08,
"tests/integration/test_select_stringref_trigger.py": 29.6,
"tests/integration/test_sensor_filters_delta.py": 28.01,
"tests/integration/test_sensor_filters_ring_buffer.py": 25.04,
"tests/integration/test_sensor_filters_sliding_window.py": 71.5,
"tests/integration/test_sensor_filters_value_list.py": 16.94,
"tests/integration/test_sensor_timeout_filter.py": 29.48,
"tests/integration/test_socket_wake_gate_tcp.py": 20.36,
"tests/integration/test_status_flags.py": 37.42,
"tests/integration/test_strftime_to.py": 22.61,
"tests/integration/test_syslog.py": 16.34,
"tests/integration/test_template_alarm_control_panel_many_sensors.py": 29.81,
"tests/integration/test_template_text_save.py": 25.43,
"tests/integration/test_text_command.py": 23.34,
"tests/integration/test_text_sensor_raw_state.py": 69.57,
"tests/integration/test_uart_mock_ld2410.py": 37.95,
"tests/integration/test_uart_mock_ld2412.py": 93.22,
"tests/integration/test_uart_mock_ld2420.py": 43.24,
"tests/integration/test_uart_mock_ld2450.py": 31.75,
"tests/integration/test_uart_mock_modbus.py": 667.4,
"tests/integration/test_udp.py": 9.38,
"tests/integration/test_use_address_runtime.py": 37.05,
"tests/integration/test_valve_control_action.py": 24.47,
"tests/integration/test_varint_five_byte_device_id.py": 25.03,
"tests/integration/test_wait_until_mid_loop_timing.py": 23.73,
"tests/integration/test_wait_until_on_boot.py": 9.16,
"tests/integration/test_wait_until_ordering.py": 13.3,
"tests/integration/test_wait_until_reentrant_restart.py": 25.23,
"tests/integration/test_wake_loop_forces_phase_b.py": 23.34,
"tests/integration/test_water_heater_template.py": 17.67
"tests/integration/test_action_concurrent_reentry.py": 57.91,
"tests/integration/test_addressable_light_transition.py": 21.25,
"tests/integration/test_alarm_control_panel_state_transitions.py": 70.71,
"tests/integration/test_api_action_metadata.py": 66.6,
"tests/integration/test_api_action_responses.py": 36.1,
"tests/integration/test_api_action_timeout.py": 68.86,
"tests/integration/test_api_conditional_memory.py": 15.48,
"tests/integration/test_api_custom_services.py": 18.77,
"tests/integration/test_api_get_time_response_timezone.py": 21.08,
"tests/integration/test_api_homeassistant.py": 65.59,
"tests/integration/test_api_homeassistant_action_no_subscriber.py": 18.44,
"tests/integration/test_api_homeassistant_binary_sensor_initial_state.py": 15.05,
"tests/integration/test_api_list_entities_backpressure.py": 13.88,
"tests/integration/test_api_message_size_batching.py": 29.98,
"tests/integration/test_api_reboot_timeout.py": 16.05,
"tests/integration/test_api_string_lambda.py": 15.31,
"tests/integration/test_api_vv_logging.py": 19.28,
"tests/integration/test_api_zero_psk_provisioning.py": 31.5,
"tests/integration/test_areas_and_devices.py": 24.95,
"tests/integration/test_automation_wait_actions.py": 20.92,
"tests/integration/test_automations.py": 35.19,
"tests/integration/test_batch_delay_zero_rapid_transitions.py": 17.99,
"tests/integration/test_binary_sensor_autorepeat_filter.py": 20.39,
"tests/integration/test_binary_sensor_invalidate_state.py": 18.41,
"tests/integration/test_blocking_warning_log_time_not_charged_to_next_operation.py": 24.69,
"tests/integration/test_build_info.py": 18.7,
"tests/integration/test_camera_mock.py": 16.23,
"tests/integration/test_climate_control_action.py": 21.14,
"tests/integration/test_climate_custom_modes.py": 20.74,
"tests/integration/test_continuation_actions.py": 16.81,
"tests/integration/test_cover_control_action.py": 20.34,
"tests/integration/test_crc8_helper.py": 9.36,
"tests/integration/test_device_id_in_state.py": 44.67,
"tests/integration/test_duplicate_entities.py": 23.58,
"tests/integration/test_entity_icon.py": 34.35,
"tests/integration/test_fan_turn_on_action.py": 24.23,
"tests/integration/test_fnv1_hash_object_id.py": 16.21,
"tests/integration/test_fnv1a_hash.py": 13.38,
"tests/integration/test_gpio_expander_cache.py": 13.06,
"tests/integration/test_host_logger_thread_safety.py": 23.66,
"tests/integration/test_host_mode_basic.py": 8.01,
"tests/integration/test_host_mode_batch_delay.py": 21.0,
"tests/integration/test_host_mode_climate_basic_state.py": 22.14,
"tests/integration/test_host_mode_climate_control.py": 19.39,
"tests/integration/test_host_mode_empty_string_options.py": 21.76,
"tests/integration/test_host_mode_entity_fields.py": 29.61,
"tests/integration/test_host_mode_fan_preset.py": 20.01,
"tests/integration/test_host_mode_many_entities.py": 39.08,
"tests/integration/test_host_mode_many_entities_multiple_connections.py": 23.92,
"tests/integration/test_host_mode_noise_encryption.py": 42.42,
"tests/integration/test_host_mode_reconnect.py": 3.41,
"tests/integration/test_host_mode_sensor.py": 22.96,
"tests/integration/test_host_ota.py": 29.5,
"tests/integration/test_host_preferences.py": 16.06,
"tests/integration/test_host_preferences_suspend_resume.py": 18.71,
"tests/integration/test_improv_serial_uart.py": 20.22,
"tests/integration/test_large_message_batching.py": 26.56,
"tests/integration/test_legacy_area.py": 22.72,
"tests/integration/test_legacy_climate_compat.py": 14.13,
"tests/integration/test_legacy_fan_compat.py": 14.33,
"tests/integration/test_light_automations.py": 18.81,
"tests/integration/test_light_binary_effect_off_phase.py": 8.38,
"tests/integration/test_light_calls.py": 21.88,
"tests/integration/test_light_constant_brightness.py": 59.45,
"tests/integration/test_light_control_action.py": 31.91,
"tests/integration/test_light_dim_relative_action.py": 14.43,
"tests/integration/test_light_effect_zero_brightness.py": 25.05,
"tests/integration/test_light_initial_state.py": 18.97,
"tests/integration/test_light_toggle_action.py": 17.44,
"tests/integration/test_lock_automations.py": 18.9,
"tests/integration/test_logger_buffered_recursion_guard.py": 18.2,
"tests/integration/test_loop_disable_enable.py": 63.35,
"tests/integration/test_loop_interval_decoupling.py": 17.7,
"tests/integration/test_loop_interval_default_not_pulled_forward.py": 21.56,
"tests/integration/test_micros_to_millis.py": 15.89,
"tests/integration/test_multi_click_trigger.py": 17.23,
"tests/integration/test_multi_device_preferences.py": 19.4,
"tests/integration/test_noise_encryption_key_protection.py": 72.59,
"tests/integration/test_object_id_api_verification.py": 19.22,
"tests/integration/test_object_id_friendly_name_no_mac_suffix.py": 16.77,
"tests/integration/test_object_id_no_friendly_name.py": 45.8,
"tests/integration/test_online_image_auto_detects_image_bmp_mime.py": 86.73,
"tests/integration/test_online_image_auto_detects_redirected_image_bmp_mime.py": 40.4,
"tests/integration/test_online_image_bmp.py": 37.24,
"tests/integration/test_oversized_payloads.py": 55.75,
"tests/integration/test_preference_key_stability.py": 25.49,
"tests/integration/test_runtime_stats.py": 29.81,
"tests/integration/test_safe_mode_loop_runs.py": 6.26,
"tests/integration/test_scheduler_blocking_warning.py": 37.98,
"tests/integration/test_scheduler_bulk_cleanup.py": 18.67,
"tests/integration/test_scheduler_defer_cancel.py": 18.46,
"tests/integration/test_scheduler_defer_cancel_regular.py": 16.34,
"tests/integration/test_scheduler_defer_fifo_simple.py": 18.26,
"tests/integration/test_scheduler_defer_stress.py": 17.74,
"tests/integration/test_scheduler_heap_stress.py": 3.89,
"tests/integration/test_scheduler_internal_id_no_collision.py": 20.01,
"tests/integration/test_scheduler_interval_reschedule.py": 16.29,
"tests/integration/test_scheduler_interval_zero_coerced.py": 16.09,
"tests/integration/test_scheduler_null_name.py": 14.69,
"tests/integration/test_scheduler_numeric_id_test.py": 17.08,
"tests/integration/test_scheduler_pool.py": 19.88,
"tests/integration/test_scheduler_rapid_cancellation.py": 4.42,
"tests/integration/test_scheduler_recursive_timeout.py": 4.3,
"tests/integration/test_scheduler_removed_item_race.py": 15.49,
"tests/integration/test_scheduler_self_keyed.py": 25.77,
"tests/integration/test_scheduler_simultaneous_callbacks.py": 14.84,
"tests/integration/test_scheduler_string_test.py": 15.42,
"tests/integration/test_script_array_params.py": 12.73,
"tests/integration/test_script_delay_params.py": 12.69,
"tests/integration/test_script_queued.py": 20.38,
"tests/integration/test_script_queued_idle_loop.py": 25.06,
"tests/integration/test_script_wait_on_boot.py": 15.67,
"tests/integration/test_select_stringref_trigger.py": 19.48,
"tests/integration/test_sensor_filters_delta.py": 27.62,
"tests/integration/test_sensor_filters_ring_buffer.py": 20.27,
"tests/integration/test_sensor_filters_sliding_window.py": 56.28,
"tests/integration/test_sensor_filters_value_list.py": 20.6,
"tests/integration/test_sensor_timeout_filter.py": 22.21,
"tests/integration/test_socket_wake_gate_tcp.py": 16.37,
"tests/integration/test_status_flags.py": 29.68,
"tests/integration/test_strftime_to.py": 17.42,
"tests/integration/test_syslog.py": 18.39,
"tests/integration/test_template_alarm_control_panel_many_sensors.py": 25.61,
"tests/integration/test_template_text_save.py": 19.16,
"tests/integration/test_text_command.py": 16.43,
"tests/integration/test_text_sensor_raw_state.py": 17.19,
"tests/integration/test_uart_mock_ld2410.py": 37.0,
"tests/integration/test_uart_mock_ld2412.py": 40.82,
"tests/integration/test_uart_mock_ld2420.py": 32.7,
"tests/integration/test_uart_mock_ld2450.py": 32.84,
"tests/integration/test_uart_mock_modbus.py": 548.87,
"tests/integration/test_udp.py": 16.67,
"tests/integration/test_use_address_runtime.py": 27.26,
"tests/integration/test_valve_control_action.py": 24.58,
"tests/integration/test_varint_five_byte_device_id.py": 22.5,
"tests/integration/test_wait_until_mid_loop_timing.py": 22.05,
"tests/integration/test_wait_until_on_boot.py": 10.37,
"tests/integration/test_wait_until_ordering.py": 18.23,
"tests/integration/test_wait_until_reentrant_restart.py": 19.35,
"tests/integration/test_wake_loop_forces_phase_b.py": 17.83,
"tests/integration/test_water_heater_template.py": 25.7
}
@@ -0,0 +1,83 @@
"""Integration test that checks what LVGL actually draws, using a display with no screen.
The rendered screen is compared against a hash rather than a checked in reference image, so the
repository does not have to carry a binary file. If a change to the drawing code or to the bundled
LVGL alters the output, this test fails and prints the hash it saw; update EXPECTED_SHA256 once the
new image has been looked at and found to be correct.
The picture is drawn and encoded entirely by code in this repository, so nothing installed on the
machine running the test takes part in the result.
"""
from __future__ import annotations
import hashlib
from pathlib import Path
import pytest
from .artifact_utils import keep_artifact
from .bmp_utils import capture_when_drawn
from .types import APIClientConnectedFactory, RunCompiledFunction
WIDTH = 300
HEIGHT = 300
# sha256 of the pixel data of a 300x300 screen showing "Hello World!" centred in white on a dark
# blue background, drawn with the built in montserrat_14 font. To regenerate, run this test and
# take the hash it reports.
EXPECTED_SHA256 = "a995b002dd1d183c47514da15ab9a60a3e7d788c2e24386a02fddd48655092ed"
# Bundled LVGL version (esphome/components/lvgl/__init__.py, LVGL_VERSION) the hash above was
# generated against. A version bump can shift anti-aliasing enough to change the hash even though
# nothing is actually wrong -- if this test fails, check that first before regenerating the hash.
EXPECTED_LVGL_VERSION = "9.5.0"
@pytest.mark.asyncio
async def test_lvgl_headless_render(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""LVGL draws the expected screen on a 300x300 display with no screen behind it."""
snapshot_dir = tmp_path / "snapshots"
monkeypatch.setenv("ESPHOME_SNAPSHOT_DIR", str(snapshot_dir))
async with run_compiled(yaml_config), api_client_connected() as client:
_, services = await client.list_entities_services()
service = next(s for s in services if s.name == "take_screenshot")
async def take(name: str) -> None:
await client.execute_service(service, {"name": name})
# The background is not the whole picture: LVGL must have drawn on it. Waiting for that
# rather than for a fixed time keeps a slow first frame from being reported as a hash
# mismatch, which would look like a drawing regression.
image, capture = await capture_when_drawn(take, snapshot_dir, prefix="render")
assert (image.width, image.height, image.bits) == (WIDTH, HEIGHT, 24)
digest = hashlib.sha256(image.pixels).hexdigest()
if digest != EXPECTED_SHA256:
# Kept outside the temporary folder so CI can upload it; see artifact_utils.
kept = keep_artifact(
"lvgl_headless_render_actual.bmp", capture.read_bytes()
)
from esphome.components.lvgl import LVGL_VERSION
version_hint = ""
if LVGL_VERSION != EXPECTED_LVGL_VERSION:
version_hint = (
f"the bundled LVGL version changed ({EXPECTED_LVGL_VERSION} -> "
f"{LVGL_VERSION}), which is the likely cause\n"
)
pytest.fail(
f"rendered screen does not match the expected hash\n"
f"{version_hint}"
f" expected: {EXPECTED_SHA256}\n"
f" actual: {digest}\n"
f"the image that was rendered has been kept at {kept}\n"
f"on CI it is in the integration-test-artifacts upload for this job"
)
@@ -0,0 +1,49 @@
"""Integration test for headless SDL rendering and snapshot capture.
How a file is named and written is the same for every display that can take a snapshot and is
covered by test_snapshot_display; what is tested here is that SDL renders and can be read back
with no display server present.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from .bmp_utils import capture_when_drawn
from .types import APIClientConnectedFactory, RunCompiledFunction
WIDTH = 101
HEIGHT = 64
@pytest.mark.asyncio
async def test_sdl_headless_screenshot(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A headless SDL display renders with no display server and can be captured."""
snapshot_dir = tmp_path / "snapshots"
# The device reads this when it writes a file; the subprocess inherits our environment, so it
# must be set before the binary is launched.
monkeypatch.setenv("ESPHOME_SNAPSHOT_DIR", str(snapshot_dir))
# Make sure the run really is headless even when the test machine has a display.
monkeypatch.delenv("DISPLAY", raising=False)
monkeypatch.delenv("WAYLAND_DISPLAY", raising=False)
async with run_compiled(yaml_config), api_client_connected() as client:
_, services = await client.list_entities_services()
service = next(s for s in services if s.name == "take_screenshot")
async def take(name: str) -> None:
await client.execute_service(service, {"name": name})
# The test card is drawn in several colours, so once it is on the screen the picture is
# not one flat shade. Capturing until that is true waits out the first update rather than
# racing it.
image, _ = await capture_when_drawn(take, snapshot_dir)
assert (image.width, image.height, image.bits) == (WIDTH, HEIGHT, 24)
@@ -0,0 +1,78 @@
"""Integration test for the snapshot display and the file writing shared with other displays."""
from __future__ import annotations
import asyncio
from pathlib import Path
from aioesphomeapi import LogLevel
import pytest
from .bmp_utils import capture_when_drawn, wait_for_bmp
from .types import APIClientConnectedFactory, RunCompiledFunction
WIDTH = 101
HEIGHT = 64
# Part of the message the writer logs when it will not write over a file that is already there.
REFUSAL_MESSAGE = b"not overwriting"
@pytest.mark.asyncio
async def test_snapshot_display(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A display with no screen draws into memory and writes what it drew to a file."""
snapshot_dir = tmp_path / "snapshots"
# The device reads this when it writes a file; the subprocess inherits our environment, so it
# must be set before the binary is launched.
monkeypatch.setenv("ESPHOME_SNAPSHOT_DIR", str(snapshot_dir))
async with run_compiled(yaml_config), api_client_connected() as client:
_, services = await client.list_entities_services()
service = next(s for s in services if s.name == "take_snapshot")
async def take(name: str) -> None:
await client.execute_service(service, {"name": name})
# The test card is drawn in several colours, so once it is on the screen the picture is
# not one flat shade. Capturing until that is true waits out the first update rather than
# racing it.
image, capture = await capture_when_drawn(take, snapshot_dir)
assert (image.width, image.height, image.bits) == (WIDTH, HEIGHT, 24)
# An extension is only added when there is not one already, whatever its case.
await take("UPPER.BMP")
await wait_for_bmp(snapshot_dir / "UPPER.BMP")
# A name that tries to lead somewhere else is cut back to one harmless name in the
# snapshot directory.
await take("../escape")
await wait_for_bmp(snapshot_dir / ".._escape.bmp")
# A second capture under a name already used must fail rather than write over the first.
# Wait for the device to report the refusal: on its own, an unchanged file cannot tell a
# refusal apart from a request the device has not got to yet, so a regression that wrote
# over the file could still pass on a busy machine.
refused = asyncio.Event()
def on_log(msg) -> None:
if REFUSAL_MESSAGE in msg.message:
refused.set()
client.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_DEBUG)
before = capture.read_bytes()
await take(capture.name)
await asyncio.wait_for(refused.wait(), timeout=10.0)
assert capture.read_bytes() == before
# Nothing beyond what was asked for, leaving out however many captures it took to wait
# for the first frame.
written = sorted(
p.name for p in snapshot_dir.iterdir() if not p.name.startswith("drawn-")
)
assert written == [".._escape.bmp", "UPPER.BMP"]
@@ -0,0 +1,14 @@
# Common UART configuration for ESP32 Arduino tests - 1200 baud, EVEN parity, 7 data bits
substitutions:
tx_pin: GPIO17
rx_pin: GPIO16
uart:
- id: uart_bus
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: 1200
parity: EVEN
data_bits: 7
stop_bits: 1
@@ -0,0 +1,14 @@
# Common UART configuration for ESP32-C3 Arduino tests - 1200 baud, EVEN parity, 7 data bits
substitutions:
tx_pin: GPIO20
rx_pin: GPIO21
uart:
- id: uart_bus
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: 1200
parity: EVEN
data_bits: 7
stop_bits: 1
@@ -0,0 +1,14 @@
# Common UART configuration for ESP32-C3 IDF tests - 1200 baud, EVEN parity, 7 data bits
substitutions:
tx_pin: GPIO20
rx_pin: GPIO21
uart:
- id: uart_bus
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: 1200
parity: EVEN
data_bits: 7
stop_bits: 1
@@ -0,0 +1,14 @@
# Common UART configuration for ESP32 IDF tests - 1200 baud, EVEN parity, 7 data bits
substitutions:
tx_pin: GPIO17
rx_pin: GPIO16
uart:
- id: uart_bus
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: 1200
parity: EVEN
data_bits: 7
stop_bits: 1
@@ -0,0 +1,14 @@
# Common UART configuration for ESP8266 Arduino tests - 1200 baud, EVEN parity, 7 data bits
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
uart:
- id: uart_bus
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: 1200
parity: EVEN
data_bits: 7
stop_bits: 1
@@ -0,0 +1,14 @@
# Common UART configuration for RP2040 Arduino tests - 1200 baud, EVEN parity, 7 data bits
substitutions:
tx_pin: GPIO0
rx_pin: GPIO1
uart:
- id: uart_bus
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: 1200
parity: EVEN
data_bits: 7
stop_bits: 1
@@ -0,0 +1,12 @@
# Common UART configuration for ESP32 Arduino tests - 38400 baud, EVEN parity
substitutions:
tx_pin: GPIO17
rx_pin: GPIO16
uart:
- id: uart_bus
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: 38400
parity: EVEN
@@ -0,0 +1,12 @@
# Common UART configuration for ESP32-C3 Arduino tests - 38400 baud, EVEN parity
substitutions:
tx_pin: GPIO20
rx_pin: GPIO21
uart:
- id: uart_bus
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: 38400
parity: EVEN
@@ -0,0 +1,12 @@
# Common UART configuration for ESP32-C3 IDF tests - 38400 baud, EVEN parity
substitutions:
tx_pin: GPIO20
rx_pin: GPIO21
uart:
- id: uart_bus
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: 38400
parity: EVEN
@@ -0,0 +1,12 @@
# Common UART configuration for ESP32 IDF tests - 38400 baud, EVEN parity
substitutions:
tx_pin: GPIO17
rx_pin: GPIO16
uart:
- id: uart_bus
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: 38400
parity: EVEN
@@ -0,0 +1,12 @@
# Common UART configuration for ESP8266 Arduino tests - 38400 baud, EVEN parity
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
uart:
- id: uart_bus
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: 38400
parity: EVEN
@@ -0,0 +1,12 @@
# Common UART configuration for RP2040 Arduino tests - 38400 baud, EVEN parity
substitutions:
tx_pin: GPIO0
rx_pin: GPIO1
uart:
- id: uart_bus
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
baud_rate: 38400
parity: EVEN
@@ -1,11 +1,7 @@
"""Tests for the per-board linker-script rule."""
import pytest
from esphome.components.esp8266 import _choose_ld_script
from esphome.components.esp8266.boards import BOARDS, board_ld_script
import esphome.config_validation as cv
from esphome.core import EsphomeError
def test_d1_wroom_02_keeps_its_shipped_layout() -> None:
@@ -21,13 +17,6 @@ def test_default_boards_use_the_flash_size_layout() -> None:
def test_choose_ld_script_paths() -> None:
"""Old cores get the size default, overriding boards hard-error there
(a substituted layout would wipe flash-backed state), modern cores
honor the override."""
assert _choose_ld_script("nodemcuv2", cv.Version(2, 3, 0)) is None
assert _choose_ld_script("nodemcuv2", cv.Version(2, 4, 2)) == "eagle.flash.4m.ld"
assert _choose_ld_script("d1_wroom_02", cv.Version(2, 7, 4)) == (
"eagle.flash.2m64.ld"
)
with pytest.raises(EsphomeError, match="cannot honor"):
_choose_ld_script("d1_wroom_02", cv.Version(2, 4, 2))
"""Default boards get the size layout, overriding boards keep theirs."""
assert _choose_ld_script("nodemcuv2") == "eagle.flash.4m.ld"
assert _choose_ld_script("d1_wroom_02") == "eagle.flash.2m64.ld"
@@ -0,0 +1,23 @@
"""Tests for the Arduino framework version floor."""
import pytest
from esphome.components.esp8266 import _arduino_check_versions
import esphome.config_validation as cv
from esphome.const import CONF_PLATFORM_VERSION, CONF_VERSION
def test_versions_before_3_are_rejected() -> None:
with pytest.raises(cv.Invalid, match="no longer supported") as excinfo:
_arduino_check_versions({CONF_VERSION: "2.7.4"})
assert excinfo.value.path == [CONF_VERSION]
def test_supported_versions_pass() -> None:
value = _arduino_check_versions({CONF_VERSION: "3.0.2"})
assert value[CONF_VERSION] == "3.0.2"
assert "espressif8266@3.2.0" in value[CONF_PLATFORM_VERSION]
value = _arduino_check_versions({CONF_VERSION: "recommended"})
assert value[CONF_VERSION] == "3.1.2"
assert "espressif8266@4.2.1" in value[CONF_PLATFORM_VERSION]
@@ -63,8 +63,10 @@ def test_valid_config_passes() -> None:
def test_platformio_toolchain_skips_checks() -> None:
# 3.0.2 is pio-legal (>= the global 3.0.0 floor) but below the native
# toolchain's own 3.1.1 floor; the bogus board only the native path checks
CORE.toolchain = Toolchain.PLATFORMIO
config = _config(board="not_a_board", version="2.7.4")
config = _config(board="not_a_board", version="3.0.2")
assert _validate_native_toolchain(config) is config
+6 -11
View File
@@ -21,17 +21,12 @@ def _build_path(tmp_path: Path) -> None:
def test_framework_package_version() -> None:
assert framework.framework_package_version(cv.Version(3, 1, 2)) == "3.30102.0"
assert framework.framework_package_version(cv.Version(3, 2, 0)) == "3.30200.0"
# 2.6.3+ cores use the same package-major-3 encoding (PlatformIO path)
assert framework.framework_package_version(cv.Version(2, 7, 4)) == "3.20704.0"
# A future major bump needs its own encoding, not a doomed registry lookup
with pytest.raises(EsphomeError, match="not supported yet"):
framework.framework_package_version(cv.Version(4, 0, 0))
# The boundary matches the PlatformIO era guard; a 2.6.2 pre-release
# keeps this encoding
with pytest.raises(EsphomeError, match="older package encoding"):
framework.framework_package_version(cv.Version(2, 6, 2))
assert framework.framework_package_version(cv.Version(2, 6, 2, "b1")) == "3.20602.0"
assert framework.framework_package_version(cv.Version(2, 6, 3)) == "3.20603.0"
# Cores before 3.x cannot build ESPHome (C++20) and are rejected
with pytest.raises(EsphomeError, match="requires core 3"):
framework.framework_package_version(cv.Version(2, 7, 4))
def test_format_framework_arduino_version_pins_all_series() -> None:
@@ -39,10 +34,10 @@ def test_format_framework_arduino_version_pins_all_series() -> None:
era, including the 4.x rejection it now shares with the installer."""
from esphome.components.esp8266 import _format_framework_arduino_version as fmt
assert fmt(cv.Version(2, 4, 1)) == "~1.20401.0"
assert fmt(cv.Version(2, 6, 2)) == "~2.20602.0"
assert fmt(cv.Version(2, 7, 4)) == "~3.20704.0"
assert fmt(cv.Version(3, 1, 2)) == "~3.30102.0"
# Pre-3 cores are rejected with the version line anchored
with pytest.raises(cv.Invalid, match="requires core 3"):
fmt(cv.Version(2, 7, 4))
# Anchored to the framework version line, not a bare EsphomeError
with pytest.raises(cv.Invalid, match="not supported yet") as excinfo:
fmt(cv.Version(4, 0, 0))