"""Tests for byte-based format and size filtering. Two layers are covered: * :mod:`tools.images` -- the stdlib-only detector and the stdlib-only builders for the two accepted formats, so fixtures are real images rather than magic strings; * the CLI search path -- candidates are filtered on the *downloaded bytes*, so a source that mislabels a file, claims the wrong size or serves an unreadable body produces the correct ``ok`` / ``no_format`` / ``no_size`` / ``access_error`` answer. Every CLI test injects a fake byte-fetch callable, so nothing here touches the network. """ from __future__ import annotations import json import struct from typing import Any, Mapping, Sequence import pytest from tools import envelope, example, images from tools.errors import SourceAccessError from tools.images import ImageFormatError, ImageInfo # --------------------------------------------------------------------------- # Real-format builders used as fixtures # --------------------------------------------------------------------------- def png_payload(width: int, height: int) -> bytes: """A real PNG built with :func:`tools.images.png_bytes`.""" return images.png_bytes(width, height) def ico_payload(width: int, height: int) -> bytes: """A real ICO (PNG-compressed entry) built with :func:`tools.images.ico_bytes`.""" return images.ico_bytes(width, height) def gif_payload(width: int = 100, height: int = 100) -> bytes: """A payload whose real format is GIF, i.e. one we must never accept.""" return b"GIF89a" + struct.pack(" bytes: """A payload whose real format is JPEG, i.e. another format we must not accept.""" return b"\xff\xd8\xff\xe0" + b"\x00" * 12 def svg_payload(width: int = 100, height: int = 100) -> bytes: """A real SVG document, the most common non-bitmap hit on a live source.""" return ( '\n' f'\n' ).encode("utf-8") def tiff_payload() -> bytes: """A payload whose real format is TIFF.""" return b"II*\x00" + b"\x00" * 16 def pdf_payload() -> bytes: """A payload whose real format is PDF, served by document-friendly sources.""" return b"%PDF-1.7\n%\xe2\xe3\xcf\xd3\n" # --------------------------------------------------------------------------- # tools.images: detection # --------------------------------------------------------------------------- def test_png_bytes_builds_a_png_with_the_requested_dimensions() -> None: payload = png_payload(13, 7) assert payload.startswith(images.PNG_SIGNATURE) assert images.detect_format(payload) == images.PNG assert images.inspect_image(payload) == ImageInfo(format="png", width=13, height=7) # The IHDR chunk really carries the dimensions, so the bytes are the source # of truth rather than a helper function agreeing with itself. assert payload[12:16] == b"IHDR" assert struct.unpack(">II", payload[16:24]) == (13, 7) assert images.read_png_dimensions(payload) == (13, 7) def test_ico_bytes_builds_a_directory_that_reports_the_requested_sizes() -> None: payload = ico_payload(48, 37) assert images.detect_format(payload) == images.ICO assert images.inspect_image(payload) == ImageInfo(format="ico", width=48, height=37) # One directory entry, and its data range lies inside the payload. reserved, kind, count = struct.unpack(" None: payload = ico_payload(256, 256) assert payload[6] == 0 and payload[7] == 0 assert images.read_ico_dimensions(payload) == (256, 256) def test_ico_dimensions_use_the_largest_directory_entry() -> None: payload = images.ico_bytes(16, 16, frames=[(16, 16), (32, 32), (256, 256)]) assert len(images.ico_entries(payload)) == 3 assert images.read_ico_dimensions(payload) == (256, 256) assert images.inspect_image(payload).format == "ico" @pytest.mark.parametrize( ("payload", "expected"), [ (png_payload(2, 2), images.PNG), (ico_payload(2, 2), images.ICO), (gif_payload(), images.GIF), (jpeg_payload(), images.JPEG), (b"BM" + b"\x00" * 20, images.BMP), (b"RIFF" + b"\x00\x00\x00\x00" + b"WEBP" + b"\x00" * 8, images.WEBP), (svg_payload(), images.SVG), (svg_payload().replace(b" None: assert images.detect_format(payload) == expected @pytest.mark.parametrize("payload", [b"", b"payload", b"not-an-image", b""]) def test_detect_format_returns_none_for_non_images(payload: bytes) -> None: assert images.detect_format(payload) is None @pytest.mark.parametrize("payload", [gif_payload(), jpeg_payload(), svg_payload(), tiff_payload(), pdf_payload()]) def test_inspect_image_rejects_formats_whose_size_is_not_readable(payload: bytes) -> None: with pytest.raises(ImageFormatError) as excinfo: images.inspect_image(payload) assert excinfo.value.code == "image_unreadable" # The format is named, so the CLI can report it as another format rather # than as an access problem. assert excinfo.value.details["format"] == images.detect_format(payload) @pytest.mark.parametrize( "payload", [ b"not-an-image", images.PNG_SIGNATURE, images.PNG_SIGNATURE + struct.pack(">I", 13) + b"IDAT" + b"\x00" * 13, b"\x00\x00\x01\x00" + struct.pack(" None: with pytest.raises(ImageFormatError): images.inspect_image(payload) def test_png_dimensions_reject_a_zero_sized_image() -> None: payload = images.PNG_SIGNATURE + struct.pack(">I", 13) + b"IHDR" + struct.pack(">IIBBBBB", 0, 0, 8, 6, 0, 0, 0) with pytest.raises(ImageFormatError): images.read_png_dimensions(payload) # --------------------------------------------------------------------------- # CLI search path: filtering on downloaded bytes # --------------------------------------------------------------------------- class StubCandidate: """Candidate whose bytes are supplied by a fake fetch callable.""" def __init__( self, title: str, url: str, *, payload: bytes | None = None, error: Exception | None = None, mime_type: str | None = None, width: int | None = None, height: int | None = None, ) -> None: self.source_id = "fake" self.title = title self.file_url = url self.page_url = url + ".html" # Deliberately misleading metadata: filtering must ignore all of this. self.mime_type = mime_type self.width = width self.height = height self._payload = payload self._error = error self.fetch_calls = 0 def fetch_bytes(self) -> bytes: self.fetch_calls += 1 if self._error is not None: raise self._error assert self._payload is not None return self._payload class StubRegistry: """Registry stand-in that returns a fixed candidate list.""" def __init__(self, candidates: Sequence[StubCandidate]) -> None: self.candidates = list(candidates) self.calls: list[tuple[str, str, int]] = [] def available_sources(self) -> Sequence[str]: return ["fake"] def search(self, source_id: str, topic: str, limit: int = 10) -> list[StubCandidate]: self.calls.append((source_id, topic, limit)) return self.candidates[:limit] def run_search( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], candidates: Sequence[StubCandidate], *args: str, ) -> tuple[int, dict[str, Any], StubRegistry]: registry = StubRegistry(candidates) monkeypatch.setattr(example, "default_registry", lambda: registry) code = example.main(list(args)) captured = capsys.readouterr() assert captured.err == "" lines = [line for line in captured.out.splitlines() if line.strip()] assert len(lines) == 1, captured.out document = json.loads(lines[0]) assert isinstance(document, Mapping) return code, document, registry BASE_ARGS = ("--topic", "red fox", "--format", "png", "--source", "fake") def test_png_candidate_is_kept_with_its_actual_dimensions( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: payload = png_payload(800, 600) candidate = StubCandidate( "fox", "https://example.test/fox.png", payload=payload, # The source claims something else entirely; the bytes win. mime_type="image/jpeg", width=12, height=12, ) code, document, registry = run_search( monkeypatch, capsys, [candidate], *BASE_ARGS, "--width", "640", "--height", "480" ) assert code == envelope.EXIT_OK assert document["status"] == envelope.STATUS_OK assert document["error"] is None item = document["items"][0] assert item["source"] == "fake" assert item["url"] == "https://example.test/fox.png" assert item["format"] == "png" assert (item["width"], item["height"]) == (800, 600) assert item["size_bytes"] == len(payload) assert candidate.fetch_calls == 1 # The bounded candidate window is requested from the source, not --count. assert registry.calls == [("fake", "red fox", example.candidate_cap(10))] def test_ico_request_with_only_png_payloads_yields_no_format( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: candidates = [ StubCandidate("fox", "https://example.test/a.png", payload=png_payload(400, 400), mime_type="image/x-icon"), StubCandidate("fox2", "https://example.test/b.png", payload=png_payload(500, 500), mime_type="image/ico"), ] code, document, _ = run_search( monkeypatch, capsys, candidates, "--topic", "red fox", "--format", "ico", "--source", "fake", "--width", "64", "--height", "64", ) assert code == envelope.EXIT_NO_FORMAT assert document["status"] == envelope.STATUS_NO_FORMAT assert document["items"] == [] assert document["error"]["code"] == "no_format_match" assert document["error"]["details"]["requested_format"] == "ico" assert document["error"]["details"]["unreadable_candidates"] == 0 assert document["error"]["details"]["format_matches"] == 0 def test_ico_payload_is_kept_for_an_ico_request( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: candidate = StubCandidate("fox", "https://example.test/fox.ico", payload=ico_payload(64, 48)) code, document, _ = run_search( monkeypatch, capsys, [candidate], "--topic", "red fox", "--format", "ico", "--source", "fake", "--width", "32", "--height", "32", ) assert code == envelope.EXIT_OK item = document["items"][0] assert item["format"] == "ico" assert (item["width"], item["height"]) == (64, 48) def test_all_too_small_payloads_yield_no_size( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: candidates = [ StubCandidate("small", "https://example.test/a.png", payload=png_payload(100, 100), width=4000, height=4000), StubCandidate("tall", "https://example.test/b.png", payload=png_payload(900, 100)), ] code, document, _ = run_search( monkeypatch, capsys, candidates, *BASE_ARGS, "--width", "200", "--height", "200" ) assert code == envelope.EXIT_NO_SIZE assert document["status"] == envelope.STATUS_NO_SIZE assert document["items"] == [] assert document["error"]["code"] == "no_size_match" assert document["error"]["details"]["format_matches"] == 2 assert document["error"]["details"]["unreadable_candidates"] == 0 def test_exactly_minimum_size_is_accepted( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: candidate = StubCandidate("edge", "https://example.test/edge.png", payload=png_payload(200, 200)) code, document, _ = run_search( monkeypatch, capsys, [candidate], *BASE_ARGS, "--width", "200", "--height", "200" ) assert code == envelope.EXIT_OK assert (document["items"][0]["width"], document["items"][0]["height"]) == (200, 200) def test_no_format_and_no_size_and_access_error_stay_distinct( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: format_code, format_document, _ = run_search( monkeypatch, capsys, [StubCandidate("gif", "https://example.test/a.png", payload=gif_payload())], *BASE_ARGS, ) size_code, size_document, _ = run_search( monkeypatch, capsys, [StubCandidate("small", "https://example.test/a.png", payload=png_payload(10, 10))], *BASE_ARGS, "--width", "100", "--height", "100", ) access_code, access_document, _ = run_search( monkeypatch, capsys, [StubCandidate("broken", "https://example.test/a.png", error=SourceAccessError("refused"))], *BASE_ARGS, ) assert format_document["status"] == envelope.STATUS_NO_FORMAT assert size_document["status"] == envelope.STATUS_NO_SIZE assert access_document["status"] == envelope.STATUS_ACCESS_ERROR assert len({format_code, size_code, access_code}) == 3 assert (format_code, size_code, access_code) == ( envelope.EXIT_NO_FORMAT, envelope.EXIT_NO_SIZE, envelope.EXIT_ACCESS_ERROR, ) def test_fetch_failure_yields_access_error( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: candidates = [ StubCandidate( "broken", "https://example.test/a.png", error=SourceAccessError("image source returned HTTP status 503", url="https://example.test/a.png"), ) ] code, document, _ = run_search(monkeypatch, capsys, candidates, *BASE_ARGS) assert code == envelope.EXIT_ACCESS_ERROR assert document["status"] == envelope.STATUS_ACCESS_ERROR assert document["items"] == [] assert document["error"]["code"] == "candidate_unreadable" assert document["error"]["details"]["failures"][0]["url"] == "https://example.test/a.png" def test_non_image_payload_is_no_format_not_access_error( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: """Bytes that are no image at all are still "not the requested format". A live source answers with HTML error pages, licence documents and vector files; none of them is a png, so the topic verdict is ``no_format`` and the caller must not see an access error (or a partial download) instead. """ candidate = StubCandidate( "junk", "https://example.test/a.png", payload=b"not-an-image", mime_type="image/png" ) code, document, _ = run_search(monkeypatch, capsys, [candidate], *BASE_ARGS) assert code == envelope.EXIT_NO_FORMAT assert document["status"] == envelope.STATUS_NO_FORMAT assert document["items"] == [] assert document["error"]["code"] == "no_format_match" details = document["error"]["details"] assert details["other_format_candidates"] == 1 assert details["observed_formats"] == {example.UNKNOWN_FORMAT: 1} assert details["unreadable_candidates"] == 0 def test_search_keeps_png_when_the_window_also_serves_other_formats( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: """The real source mixes formats; a working search stays ``ok``. Every non-png payload in the bounded window used to be classified as an unreadable candidate, which demoted a successful search to ``partial_download`` (exit 6). They are format mismatches now. """ candidates = [ StubCandidate("gif", "https://example.test/a.gif", payload=gif_payload(), mime_type="image/png"), StubCandidate("jpeg", "https://example.test/a.jpg", payload=jpeg_payload(), mime_type="image/png"), StubCandidate("svg", "https://example.test/a.svg", payload=svg_payload(), mime_type="image/png"), StubCandidate("tiff", "https://example.test/a.tiff", payload=tiff_payload(), mime_type="image/png"), StubCandidate("pdf", "https://example.test/a.pdf", payload=pdf_payload(), mime_type="image/png"), StubCandidate("page", "https://example.test/a.html", payload=b"rate limited"), StubCandidate("fox", "https://example.test/fox.png", payload=png_payload(800, 600)), ] code, document, _ = run_search( monkeypatch, capsys, candidates, *BASE_ARGS, "--width", "640", "--height", "480" ) assert code == envelope.EXIT_OK assert document["status"] == envelope.STATUS_OK assert document["error"] is None assert [item["url"] for item in document["items"]] == ["https://example.test/fox.png"] assert (document["items"][0]["width"], document["items"][0]["height"]) == (800, 600) def test_unfetchable_candidate_does_not_demote_a_usable_search( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: """A working search stays ``ok`` even when one candidate cannot be fetched.""" candidates = [ StubCandidate( "missing", "https://example.test/missing.png", error=SourceAccessError("image source returned HTTP status 503"), ), StubCandidate("fox", "https://example.test/fox.png", payload=png_payload(300, 200)), ] code, document, _ = run_search( monkeypatch, capsys, candidates, *BASE_ARGS, "--width", "100", "--height", "100" ) assert code == envelope.EXIT_OK assert document["status"] == envelope.STATUS_OK assert [item["url"] for item in document["items"]] == ["https://example.test/fox.png"] def test_ico_request_with_only_foreign_formats_yields_no_format_not_access_error( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: """REQ-007: ico may be unavailable, and that is ``no_format`` (exit 3).""" candidates = [ StubCandidate("gif", "https://example.test/a.gif", payload=gif_payload()), StubCandidate("jpeg", "https://example.test/a.jpg", payload=jpeg_payload()), StubCandidate("svg", "https://example.test/a.svg", payload=svg_payload()), StubCandidate("tiff", "https://example.test/a.tiff", payload=tiff_payload()), StubCandidate("page", "https://example.test/a.html", payload=b""), ] code, document, _ = run_search( monkeypatch, capsys, candidates, "--topic", "red fox", "--format", "ico", "--source", "fake", "--width", "64", "--height", "64", ) assert code == envelope.EXIT_NO_FORMAT assert document["status"] == envelope.STATUS_NO_FORMAT details = document["error"]["details"] assert details["requested_format"] == "ico" assert details["other_format_candidates"] == 5 assert details["observed_formats"] == { "gif": 1, "jpeg": 1, "svg": 1, "tiff": 1, example.UNKNOWN_FORMAT: 1, } assert details["unreadable_candidates"] == 0 def test_no_size_survives_other_formats_in_the_same_window( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: """REQ-008: format-matching but too-small images keep their own status.""" candidates = [ StubCandidate("gif", "https://example.test/a.gif", payload=gif_payload()), StubCandidate("small", "https://example.test/a.png", payload=png_payload(100, 100)), StubCandidate("svg", "https://example.test/a.svg", payload=svg_payload()), ] code, document, _ = run_search( monkeypatch, capsys, candidates, *BASE_ARGS, "--width", "200", "--height", "200" ) assert code == envelope.EXIT_NO_SIZE assert document["status"] == envelope.STATUS_NO_SIZE details = document["error"]["details"] assert details["format_matches"] == 1 assert details["other_format_candidates"] == 2 assert details["unreadable_candidates"] == 0 def test_corrupt_payload_of_the_requested_format_does_not_hide_a_valid_match( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: corrupt = images.PNG_SIGNATURE + struct.pack(">I", 13) + b"IDAT" + b"\x00" * 13 candidates = [ StubCandidate("broken", "https://example.test/broken.png", payload=corrupt), StubCandidate("fox", "https://example.test/fox.png", payload=png_payload(200, 200)), ] code, document, _ = run_search( monkeypatch, capsys, candidates, *BASE_ARGS, "--width", "100", "--height", "100" ) assert code == envelope.EXIT_OK assert document["status"] == envelope.STATUS_OK assert [item["url"] for item in document["items"]] == ["https://example.test/fox.png"] def test_empty_search_window_is_no_format_not_access_error( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: code, document, _ = run_search(monkeypatch, capsys, [], *BASE_ARGS) assert code == envelope.EXIT_NO_FORMAT assert document["status"] == envelope.STATUS_NO_FORMAT assert document["error"]["details"]["candidate_count"] == 0 assert document["error"]["details"]["unreadable_candidates"] == 0 def test_candidate_window_is_bounded_by_the_requested_count( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: candidates = [ StubCandidate(f"fox{n}", f"https://example.test/{n}.png", payload=png_payload(100, 100)) for n in range(40) ] code, document, registry = run_search( monkeypatch, capsys, candidates, *BASE_ARGS, "--count", "3" ) assert code == envelope.EXIT_OK assert len(document["items"]) == 3 assert registry.calls == [("fake", "red fox", example.candidate_cap(3))] assert example.candidate_cap(3) == 12 # Fetching stops as soon as enough usable images were found. assert sum(candidate.fetch_calls for candidate in candidates) == 3 def test_filtering_walks_past_rejected_candidates_until_the_count_is_met( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: candidates = [ StubCandidate("gif", "https://example.test/0.png", payload=gif_payload()), StubCandidate("tiny", "https://example.test/1.png", payload=png_payload(10, 10)), StubCandidate("ok1", "https://example.test/2.png", payload=png_payload(120, 120)), StubCandidate("ok2", "https://example.test/3.png", payload=png_payload(130, 130)), StubCandidate("never", "https://example.test/4.png", payload=png_payload(140, 140)), ] code, document, _ = run_search( monkeypatch, capsys, candidates, *BASE_ARGS, "--count", "2", "--width", "100", "--height", "100" ) assert code == envelope.EXIT_OK assert [item["url"] for item in document["items"]] == [ "https://example.test/2.png", "https://example.test/3.png", ] assert [(item["width"], item["height"]) for item in document["items"]] == [(120, 120), (130, 130)] assert candidates[-1].fetch_calls == 0