mirror of
https://github.com/esphome/esphome.git
synced 2026-01-11 12:40:50 -07:00
353 lines
12 KiB
Python
353 lines
12 KiB
Python
"""Tests for image configuration validation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from esphome import config_validation as cv
|
|
from esphome.components.image import (
|
|
CONF_TRANSPARENCY,
|
|
CONFIG_SCHEMA,
|
|
get_all_image_metadata,
|
|
get_image_metadata,
|
|
)
|
|
from esphome.const import CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE
|
|
from esphome.core import CORE
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("config", "error_match"),
|
|
[
|
|
pytest.param(
|
|
"a string",
|
|
"Badly formed image configuration, expected a list or a dictionary",
|
|
id="invalid_string_config",
|
|
),
|
|
pytest.param(
|
|
{"id": "image_id", "type": "rgb565"},
|
|
r"required key not provided @ data\['file'\]",
|
|
id="missing_file",
|
|
),
|
|
pytest.param(
|
|
{"file": "image.png", "type": "rgb565"},
|
|
r"required key not provided @ data\['id'\]",
|
|
id="missing_id",
|
|
),
|
|
pytest.param(
|
|
{"id": "mdi_id", "file": "mdi:weather-##", "type": "rgb565"},
|
|
"Could not parse mdi icon name",
|
|
id="invalid_mdi_icon",
|
|
),
|
|
pytest.param(
|
|
{
|
|
"id": "image_id",
|
|
"file": "image.png",
|
|
"type": "binary",
|
|
"transparency": "alpha_channel",
|
|
},
|
|
"Image format 'BINARY' cannot have transparency",
|
|
id="binary_with_transparency",
|
|
),
|
|
pytest.param(
|
|
{
|
|
"id": "image_id",
|
|
"file": "image.png",
|
|
"type": "rgb565",
|
|
"transparency": "chroma_key",
|
|
"invert_alpha": True,
|
|
},
|
|
"No alpha channel to invert",
|
|
id="invert_alpha_without_alpha_channel",
|
|
),
|
|
pytest.param(
|
|
{
|
|
"id": "image_id",
|
|
"file": "image.png",
|
|
"type": "binary",
|
|
"byte_order": "big_endian",
|
|
},
|
|
"Image format 'BINARY' does not support byte order configuration",
|
|
id="binary_with_byte_order",
|
|
),
|
|
pytest.param(
|
|
{"id": "image_id", "file": "bad.png", "type": "binary"},
|
|
"File can't be opened as image",
|
|
id="invalid_image_file",
|
|
),
|
|
pytest.param(
|
|
{"defaults": {}, "images": [{"id": "image_id", "file": "image.png"}]},
|
|
"Type is required either in the image config or in the defaults",
|
|
id="missing_type_in_defaults",
|
|
),
|
|
],
|
|
)
|
|
def test_image_configuration_errors(
|
|
config: Any,
|
|
error_match: str,
|
|
) -> None:
|
|
"""Test detection of invalid configuration."""
|
|
with pytest.raises(cv.Invalid, match=error_match):
|
|
CONFIG_SCHEMA(config)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"config",
|
|
[
|
|
pytest.param(
|
|
{
|
|
"id": "image_id",
|
|
"file": "image.png",
|
|
"type": "rgb565",
|
|
"transparency": "chroma_key",
|
|
"byte_order": "little_endian",
|
|
"dither": "FloydSteinberg",
|
|
"resize": "100x100",
|
|
"invert_alpha": False,
|
|
},
|
|
id="single_image_all_options",
|
|
),
|
|
pytest.param(
|
|
[
|
|
{
|
|
"id": "image_id",
|
|
"file": "image.png",
|
|
"type": "binary",
|
|
}
|
|
],
|
|
id="list_of_images",
|
|
),
|
|
pytest.param(
|
|
{
|
|
"defaults": {
|
|
"type": "rgb565",
|
|
"transparency": "chroma_key",
|
|
"byte_order": "little_endian",
|
|
"dither": "FloydSteinberg",
|
|
"resize": "100x100",
|
|
"invert_alpha": False,
|
|
},
|
|
"images": [
|
|
{
|
|
"id": "image_id",
|
|
"file": "image.png",
|
|
}
|
|
],
|
|
},
|
|
id="images_with_defaults",
|
|
),
|
|
pytest.param(
|
|
{
|
|
"rgb565": {
|
|
"alpha_channel": [
|
|
{
|
|
"id": "image_id",
|
|
"file": "image.png",
|
|
"transparency": "alpha_channel",
|
|
"byte_order": "little_endian",
|
|
"dither": "FloydSteinberg",
|
|
"resize": "100x100",
|
|
"invert_alpha": False,
|
|
}
|
|
]
|
|
},
|
|
"binary": [
|
|
{
|
|
"id": "image_id",
|
|
"file": "image.png",
|
|
"transparency": "opaque",
|
|
"dither": "FloydSteinberg",
|
|
"resize": "100x100",
|
|
"invert_alpha": False,
|
|
}
|
|
],
|
|
},
|
|
id="type_based_organization",
|
|
),
|
|
pytest.param(
|
|
{
|
|
"defaults": {
|
|
"type": "binary",
|
|
"transparency": "chroma_key",
|
|
"byte_order": "little_endian",
|
|
"dither": "FloydSteinberg",
|
|
"resize": "100x100",
|
|
"invert_alpha": False,
|
|
},
|
|
"rgb565": {
|
|
"alpha_channel": [
|
|
{
|
|
"id": "image_id",
|
|
"file": "image.png",
|
|
"transparency": "alpha_channel",
|
|
"dither": "none",
|
|
}
|
|
]
|
|
},
|
|
"binary": [
|
|
{
|
|
"id": "image_id",
|
|
"file": "image.png",
|
|
"transparency": "opaque",
|
|
}
|
|
],
|
|
},
|
|
id="type_based_with_defaults",
|
|
),
|
|
pytest.param(
|
|
{
|
|
"defaults": {
|
|
"type": "rgb565",
|
|
"transparency": "alpha_channel",
|
|
},
|
|
"binary": {
|
|
"opaque": [
|
|
{
|
|
"id": "image_id",
|
|
"file": "image.png",
|
|
}
|
|
],
|
|
},
|
|
},
|
|
id="binary_with_defaults",
|
|
),
|
|
],
|
|
)
|
|
def test_image_configuration_success(
|
|
config: dict[str, Any] | list[dict[str, Any]],
|
|
) -> None:
|
|
"""Test successful configuration validation."""
|
|
result = CONFIG_SCHEMA(config)
|
|
# All valid configurations should return a list of images
|
|
assert isinstance(result, list)
|
|
for key in (CONF_TYPE, CONF_ID, CONF_TRANSPARENCY, CONF_RAW_DATA_ID):
|
|
assert all(key in x for x in result), (
|
|
f"Missing key {key} in image configuration"
|
|
)
|
|
|
|
|
|
def test_image_generation(
|
|
generate_main: Callable[[str | Path], str],
|
|
component_config_path: Callable[[str], Path],
|
|
) -> None:
|
|
"""Test image generation configuration."""
|
|
|
|
main_cpp = generate_main(component_config_path("image_test.yaml"))
|
|
assert "uint8_t_id[] PROGMEM = {0x24, 0x21, 0x24, 0x21" in main_cpp
|
|
assert (
|
|
"cat_img = new image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);"
|
|
in main_cpp
|
|
)
|
|
|
|
|
|
def test_image_to_code_defines_and_core_data(
|
|
generate_main: Callable[[str | Path], str],
|
|
component_config_path: Callable[[str], Path],
|
|
) -> None:
|
|
"""Test that to_code() sets USE_IMAGE define and stores image metadata."""
|
|
# Generate the main cpp which will call to_code
|
|
generate_main(component_config_path("image_test.yaml"))
|
|
|
|
# Verify USE_IMAGE define was added
|
|
assert any(d.name == "USE_IMAGE" for d in CORE.defines), (
|
|
"USE_IMAGE define should be set when images are configured"
|
|
)
|
|
|
|
# Use the public API to get image metadata
|
|
# The test config has an image with id 'cat_img'
|
|
cat_img_metadata = get_image_metadata("cat_img")
|
|
|
|
assert cat_img_metadata is not None, (
|
|
"Image metadata should be retrievable via get_image_metadata()"
|
|
)
|
|
|
|
# Verify the metadata has the expected attributes
|
|
assert hasattr(cat_img_metadata, "width"), "Metadata should have width attribute"
|
|
assert hasattr(cat_img_metadata, "height"), "Metadata should have height attribute"
|
|
assert hasattr(cat_img_metadata, "image_type"), (
|
|
"Metadata should have image_type attribute"
|
|
)
|
|
assert hasattr(cat_img_metadata, "transparency"), (
|
|
"Metadata should have transparency attribute"
|
|
)
|
|
|
|
# Verify the values are correct (from the test image)
|
|
assert cat_img_metadata.width == 32, "Width should be 32"
|
|
assert cat_img_metadata.height == 24, "Height should be 24"
|
|
assert cat_img_metadata.image_type == "RGB565", "Type should be RGB565"
|
|
assert cat_img_metadata.transparency == "opaque", "Transparency should be opaque"
|
|
|
|
|
|
def test_image_to_code_multiple_images(
|
|
generate_main: Callable[[str | Path], str],
|
|
component_config_path: Callable[[str], Path],
|
|
) -> None:
|
|
"""Test that to_code() stores metadata for multiple images."""
|
|
generate_main(component_config_path("image_test.yaml"))
|
|
|
|
# Use the public API to get all image metadata
|
|
all_metadata = get_all_image_metadata()
|
|
|
|
assert isinstance(all_metadata, dict), (
|
|
"get_all_image_metadata() should return a dictionary"
|
|
)
|
|
|
|
# Verify that at least one image is present
|
|
assert len(all_metadata) > 0, "Should have at least one image metadata entry"
|
|
|
|
# Each image ID should map to an ImageMetaData object
|
|
for image_id, metadata in all_metadata.items():
|
|
assert isinstance(image_id, str), "Image IDs should be strings"
|
|
|
|
# Verify it's an ImageMetaData object with all required attributes
|
|
assert hasattr(metadata, "width"), (
|
|
f"Metadata for '{image_id}' should have width"
|
|
)
|
|
assert hasattr(metadata, "height"), (
|
|
f"Metadata for '{image_id}' should have height"
|
|
)
|
|
assert hasattr(metadata, "image_type"), (
|
|
f"Metadata for '{image_id}' should have image_type"
|
|
)
|
|
assert hasattr(metadata, "transparency"), (
|
|
f"Metadata for '{image_id}' should have transparency"
|
|
)
|
|
|
|
# Verify values are valid
|
|
assert isinstance(metadata.width, int), (
|
|
f"Width for '{image_id}' should be an integer"
|
|
)
|
|
assert isinstance(metadata.height, int), (
|
|
f"Height for '{image_id}' should be an integer"
|
|
)
|
|
assert isinstance(metadata.image_type, str), (
|
|
f"Type for '{image_id}' should be a string"
|
|
)
|
|
assert isinstance(metadata.transparency, str), (
|
|
f"Transparency for '{image_id}' should be a string"
|
|
)
|
|
assert metadata.width > 0, f"Width for '{image_id}' should be positive"
|
|
assert metadata.height > 0, f"Height for '{image_id}' should be positive"
|
|
|
|
|
|
def test_get_image_metadata_nonexistent() -> None:
|
|
"""Test that get_image_metadata returns None for non-existent image IDs."""
|
|
# This should return None when no images are configured or ID doesn't exist
|
|
metadata = get_image_metadata("nonexistent_image_id")
|
|
assert metadata is None, (
|
|
"get_image_metadata should return None for non-existent IDs"
|
|
)
|
|
|
|
|
|
def test_get_all_image_metadata_empty() -> None:
|
|
"""Test that get_all_image_metadata returns empty dict when no images configured."""
|
|
# When CORE hasn't been initialized with images, should return empty dict
|
|
all_metadata = get_all_image_metadata()
|
|
assert isinstance(all_metadata, dict), (
|
|
"get_all_image_metadata should always return a dict"
|
|
)
|
|
# Length could be 0 or more depending on what's in CORE at test time
|