diff --git a/tests/conftest.py b/tests/conftest.py index f5e3ec7..3b6d0a9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,11 @@ import os import tempfile +from io import BytesIO from pathlib import Path import pytest from inline_snapshot import Format, register_format +from PIL import Image, UnidentifiedImageError # Pin fontconfig (used by cairosvg) to the repo fonts so SVG rasterization in # snapshot tests resolves the same font files as the PIL pipeline, independent @@ -23,11 +25,46 @@ os.environ["FONTCONFIG_FILE"] = str(_FONTCONFIG_DIR / "fonts.conf") +class RenderedImage: + """A stored image compared by what it draws, not by how it was encoded. + + Two encoders can write the same picture as different bytes: PNG carries a + deflate stream, and the same pixels compress differently across zlib builds. + Comparing the files byte for byte therefore tests the encoder that happened + to be installed, and a snapshot suite that does it drifts out of date + without a single pixel having moved. + """ + + def __init__(self, data: bytes) -> None: + self._data = data + + @staticmethod + def _pixels(data: bytes): + with Image.open(BytesIO(data)) as image: + return image.size, image.convert("RGBA").tobytes() + + def __eq__(self, other: object) -> bool: + if isinstance(other, RenderedImage): + return self._pixels(self._data) == self._pixels(other._data) + if isinstance(other, bytes): + try: + return self._pixels(self._data) == self._pixels(other) + except UnidentifiedImageError: + return self._data == other + return NotImplemented + + def __hash__(self) -> int: + return hash(self._data) + + def __repr__(self) -> str: + return f"RenderedImage({len(self._data)} bytes)" + + class ImageFormat(Format): suffix = ".png" - def decode(self, path: Path) -> bytes: - return path.read_bytes() + def decode(self, path: Path) -> RenderedImage: + return RenderedImage(path.read_bytes()) def encode( self,