Files
pi-weekly-newspaper/tests/test_cover.py

168 lines
5.5 KiB
Python
Raw Normal View History

import json
import os
from collections import Counter
from datetime import date
from unittest.mock import patch, MagicMock
from io import BytesIO
from PIL import Image as PILImage, ImageDraw, ImageFont
from src.cover import (
generate_programmatic_cover,
generate_ai_cover,
generate_cover,
_get_dominant_category,
_truncate_to_width,
_get_font,
CATEGORY_PROMPTS,
)
def test_programmatic_cover_is_portrait(tmp_path):
path = generate_programmatic_cover(
output_dir=str(tmp_path),
week_start=date(2026, 4, 6),
week_end=date(2026, 4, 12),
headlines=["Article One", "Article Two", "Article Three"],
categories=["Government", "Government", "Culture"],
)
assert os.path.exists(path)
img = PILImage.open(path)
assert img.format == "JPEG"
assert img.size == (480, 800)
def test_programmatic_cover_no_headlines(tmp_path):
path = generate_programmatic_cover(
output_dir=str(tmp_path),
week_start=date(2026, 4, 6),
week_end=date(2026, 4, 12),
headlines=[],
categories=[],
)
assert os.path.exists(path)
img = PILImage.open(path)
assert img.size == (480, 800)
def test_ai_cover_is_portrait(tmp_path):
fake_img = PILImage.new("RGB", (480, 800), color="blue")
buf = BytesIO()
fake_img.save(buf, format="JPEG")
fake_bytes = buf.getvalue()
mock_response = MagicMock()
mock_response.content = fake_bytes
mock_response.raise_for_status = MagicMock()
with patch("src.cover.requests.get", return_value=mock_response):
path = generate_ai_cover(
output_dir=str(tmp_path),
week_start=date(2026, 4, 6),
week_end=date(2026, 4, 12),
headlines=["Test Headline"],
categories=["Government"],
)
assert os.path.exists(path)
img = PILImage.open(path)
assert img.format == "JPEG"
assert img.size == (480, 800)
def test_ai_cover_falls_back_to_programmatic(tmp_path):
with patch("src.cover.requests.get", side_effect=Exception("API down")):
path = generate_ai_cover(
output_dir=str(tmp_path),
week_start=date(2026, 4, 6),
week_end=date(2026, 4, 12),
headlines=["Test"],
categories=["Government"],
)
assert os.path.exists(path)
img = PILImage.open(path)
assert img.format == "JPEG"
assert img.size == (480, 800)
def test_generate_cover_dispatches(tmp_path):
with patch("src.cover.generate_ai_cover") as mock_ai:
mock_ai.return_value = "/fake/ai.jpg"
result = generate_cover("ai", str(tmp_path), date(2026, 4, 6),
date(2026, 4, 12), ["A"], ["Government"])
assert result == "/fake/ai.jpg"
mock_ai.assert_called_once()
with patch("src.cover.generate_programmatic_cover") as mock_prog:
mock_prog.return_value = "/fake/text.jpg"
result = generate_cover("text", str(tmp_path), date(2026, 4, 6),
date(2026, 4, 12), ["A"], ["Government"])
assert result == "/fake/text.jpg"
mock_prog.assert_called_once()
def test_get_dominant_category():
assert _get_dominant_category(["Government", "Government", "Culture"]) == "Government"
assert _get_dominant_category(["Culture", "Government"]) == "Culture"
assert _get_dominant_category([]) == "Default"
def test_category_prompts_include_massachusetts():
for category, prompt in CATEGORY_PROMPTS.items():
assert "Massachusetts" in prompt, f"Prompt for {category} missing 'Massachusetts'"
def test_truncate_to_width_respects_pixel_budget():
font = _get_font(18)
short = "Short text"
result = _truncate_to_width(short, font, 400)
assert result == short, "Short text should not be truncated"
long = "A" * 200
result = _truncate_to_width(long, font, 400)
assert result.endswith("\u2026"), "Long text should end with ellipsis"
assert len(result) < len(long), "Truncated text should be shorter"
def test_truncate_to_width_fits_within_budget():
font = _get_font(18)
long = "This is a really long headline that should definitely be truncated to fit"
max_width = 448
result = _truncate_to_width(long, font, max_width)
dummy = PILImage.new("RGBA", (1, 1))
draw = ImageDraw.Draw(dummy)
bbox = draw.textbbox((0, 0), result, font=font)
rendered_width = bbox[2] - bbox[0]
assert rendered_width <= max_width, f"Rendered width {rendered_width} exceeds {max_width}"
def test_obituary_headlines_filtered():
"""Verify the filtering pattern used by publish/issues/scheduler call sites."""
class FakeArticle:
def __init__(self, title, categories):
self.title = title
self.categories = json.dumps(categories)
articles = [
FakeArticle("Town Meeting Approves Budget", ["Government"]),
FakeArticle("John Smith, 85, beloved teacher", ["Obituaries"]),
FakeArticle("Harbor Festival This Weekend", ["Culture"]),
FakeArticle("Jane Doe, 92, retired nurse", ["Obituaries"]),
FakeArticle("Panthers Win Championship", ["Sports"]),
]
headlines = [
a.title for a in articles
if "Obituaries" not in json.loads(a.categories)
]
assert len(headlines) == 3
assert "John Smith, 85, beloved teacher" not in headlines
assert "Jane Doe, 92, retired nurse" not in headlines
assert "Town Meeting Approves Budget" in headlines
assert "Harbor Festival This Weekend" in headlines
assert "Panthers Win Championship" in headlines