Files

230 lines
7.4 KiB
Python

"""Tests for the font component's prefetch extraction."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from esphome import external_files
from esphome.components import font
import esphome.config_validation as cv
from esphome.external_files import RemoteFile
def _gspec(family: str, weight: int = 400, italic: bool = False) -> dict:
return {"family": family, "weight": weight, "italic": italic}
def test_extract_gfonts_shorthand_defaults(setup_core: Path) -> None:
spec = font._extract_remote_font("gfonts://Roboto")
assert spec is not None
assert spec[font.CONF_FAMILY] == "Roboto"
assert spec[font.CONF_WEIGHT] == 400
assert spec[font.CONF_ITALIC] is False
def test_extract_gfonts_shorthand_weight_variants(setup_core: Path) -> None:
assert font._extract_remote_font("gfonts://Roboto@bold")[font.CONF_WEIGHT] == 700
assert font._extract_remote_font("gfonts://Roboto@500")[font.CONF_WEIGHT] == 500
def test_extract_gfonts_normalizes_quoted_italic(setup_core: Path) -> None:
"""Boolean spellings the schema accepts are accepted by the extractor."""
spec = font._extract_remote_font(
{"type": "gfonts", "family": "Roboto", "italic": "true"}
)
assert spec is not None
assert spec[font.CONF_ITALIC] is True
assert (
font._extract_remote_font(
{"type": "gfonts", "family": "Roboto", "italic": "maybe"}
)
is None
)
def test_extract_typed_gfonts_dict(setup_core: Path) -> None:
spec = font._extract_remote_font(
{"type": "gfonts", "family": "Roboto", "weight": "medium", "italic": True}
)
assert spec is not None
assert spec[font.CONF_WEIGHT] == 500
assert spec[font.CONF_ITALIC] is True
def test_extract_web_font(setup_core: Path) -> None:
url = "https://example.com/font.ttf"
for value in (url, {"type": "web", "url": url}):
spec = font._extract_remote_font(value)
assert spec is not None
assert spec[font.CONF_URL] == url
def test_extract_skips_local_and_garbage(setup_core: Path) -> None:
assert font._extract_remote_font("fonts/local.ttf") is None
assert font._extract_remote_font({"type": "local", "path": "x.ttf"}) is None
assert (
font._extract_remote_font({"type": "gfonts", "family": "R", "weight": "no"})
is None
)
assert font._extract_remote_font(42) is None
def test_prefetch_yields_css_for_stale_gfont(setup_core: Path) -> None:
entries = [
{"file": "gfonts://Roboto"},
{"file": "fonts/local.ttf"},
{
"file": "https://example.com/font.ttf",
"extras": [{"file": "gfonts://Monocraft"}],
},
]
batches = list(font.PREFETCH_FILES(entries))
urls = [file.url for file in batches[0]]
assert font._gfonts_css_url(_gspec("Roboto")) in urls
assert font._gfonts_css_url(_gspec("Monocraft")) in urls
assert "https://example.com/font.ttf" in urls
assert len(batches[0]) == 3
def test_prefetch_skips_recent_ttf(setup_core: Path) -> None:
path = font._gfonts_ttf_path(_gspec("Roboto"))
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(b"cached ttf")
batches = list(font.PREFETCH_FILES([{"file": "gfonts://Roboto"}]))
assert batches == [[], []]
def test_stage2_parses_cached_css(setup_core: Path) -> None:
css_path = font._gfonts_css_path(_gspec("Roboto"))
css_path.parent.mkdir(parents=True, exist_ok=True)
css_path.write_text(
"src: url(https://fonts.gstatic.com/roboto.ttf) format('truetype');"
)
# Stage two only trusts CSS confirmed fetched this run.
external_files._run_data().fresh_paths.add(css_path)
batches = list(font.PREFETCH_FILES([{"file": "gfonts://Roboto"}]))
assert batches[1] == [
RemoteFile(
"https://fonts.gstatic.com/roboto.ttf",
font._gfonts_ttf_path(_gspec("Roboto")),
)
]
def test_stage2_skips_missing_css(setup_core: Path) -> None:
batches = list(font.PREFETCH_FILES([{"file": "gfonts://NoCss"}]))
assert batches[1] == []
def test_prefetch_handles_bare_mapping_extras(setup_core: Path) -> None:
"""A bare-mapping extras value (valid raw config) is scanned."""
entries = [
{
"file": "fonts/local.ttf",
"extras": {"file": "gfonts://Roboto", "glyphs": "ABC"},
}
]
batches = list(font.PREFETCH_FILES(entries))
assert [file.url for file in batches[0]] == [font._gfonts_css_url(_gspec("Roboto"))]
def test_unparseable_gfonts_css_is_evicted(setup_core: Path) -> None:
"""A CSS body that fails to parse is removed from the cache."""
spec = {
"family": "Roboto",
"weight": 400,
"italic": False,
"refresh": font._REFRESH_VALIDATOR("0s"),
}
css_path = font._gfonts_css_path(spec)
with (
patch(
"esphome.components.font.external_files.download_content",
return_value=b"no truetype url here",
),
patch(
"esphome.components.font.external_files.is_fresh_this_run",
return_value=True,
),
pytest.raises(cv.Invalid, match="please report this"),
):
font.download_gfont(spec)
assert not css_path.exists()
with (
patch(
"esphome.components.font.external_files.download_content",
return_value=b"\xff\xfe\x00\x01binary",
),
patch(
"esphome.components.font.external_files.is_fresh_this_run",
return_value=True,
),
pytest.raises(cv.Invalid, match="not a text document"),
):
font.download_gfont(spec)
assert not css_path.exists()
def test_unrevalidated_gfonts_css_uses_cached_font(setup_core: Path) -> None:
"""A CSS body that could not be revalidated is not parsed for a ttf
URL; the cached font is used instead."""
spec = {
"family": "Roboto",
"weight": 400,
"italic": False,
"refresh": font._REFRESH_VALIDATOR("0s"),
}
ttf_path = font._gfonts_ttf_path(spec)
ttf_path.parent.mkdir(parents=True, exist_ok=True)
ttf_path.write_bytes(b"cached ttf")
cache = MagicMock()
with (
patch.object(font, "FONT_CACHE", cache),
patch(
"esphome.components.font.external_files.download_content",
return_value=b"stale css",
),
):
assert font.download_gfont(spec) is spec
cache.__setitem__.assert_called_once_with(spec, ttf_path)
def test_unrevalidated_gfonts_css_without_cached_font_errors(
setup_core: Path,
) -> None:
"""No verified CSS and no cached font is a clear error."""
spec = {
"family": "Roboto",
"weight": 500,
"italic": False,
"refresh": font._REFRESH_VALIDATOR("0s"),
}
with (
patch(
"esphome.components.font.external_files.download_content",
return_value=b"stale css",
),
pytest.raises(cv.Invalid, match="no cached font"),
):
font.download_gfont(spec)
def test_stage2_skips_css_not_fetched_this_run(setup_core: Path) -> None:
"""A leftover CSS from an earlier run is not trusted for stage two."""
css_path = font._gfonts_css_path(_gspec("Roboto"))
css_path.parent.mkdir(parents=True, exist_ok=True)
css_path.write_text(
"src: url(https://fonts.gstatic.com/rotated.ttf) format('truetype');"
)
batches = list(font.PREFETCH_FILES([{"file": "gfonts://Roboto"}]))
assert batches[1] == []