Files
10Backward/tests/test_matching.py
T

236 lines
7.7 KiB
Python

from __future__ import annotations
import logging
from datetime import date
from pathlib import Path
from typing import Callable
import pytest
from tenbackward.matching import (
MatchedPost,
find_anniversary_matches,
iter_anniversary_paths,
slugify_title,
)
SITE_URL = "https://chaospott.de"
_MATCHING_LOGGER = "tenbackward.matching"
def _write_post(root: Path, rel_path: str, *, body: str = "post") -> None:
full = root / rel_path
full.parent.mkdir(parents=True, exist_ok=True)
full.write_text(body, encoding="utf-8")
def _capture_matching_warnings(call: Callable[[], object]) -> list[logging.LogRecord]:
records: list[logging.LogRecord] = []
handler = logging.Handler()
handler.setLevel(logging.WARNING)
handler.emit = records.append # type: ignore[assignment]
logger = logging.getLogger(_MATCHING_LOGGER)
previous_level = logger.level
logger.setLevel(logging.WARNING)
logger.addHandler(handler)
try:
call()
finally:
logger.removeHandler(handler)
logger.setLevel(previous_level)
return records
def _event_names(records: list[logging.LogRecord]) -> list[str]:
return [getattr(record, "event", None) for record in records]
def test_match_uses_frontmatter_date_and_title(tmp_path: Path) -> None:
root = tmp_path / "_posts" / "blog"
_write_post(
root,
"2014/2014-08-04-foo.md",
body="---\ntitle: Foo\ndate: 2014-08-04\n---\nbody\n",
)
today = date(2024, 8, 4)
matches = find_anniversary_matches(root, SITE_URL, today=today)
assert len(matches) == 1
match = matches[0]
assert isinstance(match, MatchedPost)
assert match.path == "2014/2014-08-04-foo.md"
assert match.title == "Foo"
assert match.date == date(2014, 8, 4)
assert match.url == "https://chaospott.de/2014/08/04/foo/"
assert match.url.endswith(slugify_title(match.title) + "/")
def test_match_falls_back_to_filename_when_frontmatter_missing(tmp_path: Path) -> None:
root = tmp_path / "_posts" / "blog"
_write_post(root, "2015/2015-03-10-no-frontmatter.md", body="no frontmatter at all\n")
_write_post(
root, "2015/2015-03-11-broken-frontmatter.md", body="---\ndate: not-a-date\n: :\n"
)
today = date(2025, 3, 10)
matches: list[MatchedPost] = []
records = _capture_matching_warnings(
lambda: matches.extend(find_anniversary_matches(root, SITE_URL, today=today))
)
paths = {m.path for m in matches}
assert "2015/2015-03-10-no-frontmatter.md" in paths
assert "2015/2015-03-11-broken-frontmatter.md" not in paths
match = next(m for m in matches if m.path == "2015/2015-03-10-no-frontmatter.md")
assert match.title == "no-frontmatter"
assert match.date == date(2015, 3, 10)
assert match.url == "https://chaospott.de/2015/03/10/no-frontmatter/"
assert match.url.endswith(slugify_title(match.title) + "/")
def test_unpublished_post_is_skipped(tmp_path: Path) -> None:
root = tmp_path / "_posts" / "blog"
_write_post(
root,
"2014/2014-08-04-draft.md",
body="---\ntitle: Draft\ndate: 2014-08-04\npublished: false\n---\n",
)
today = date(2024, 8, 4)
matches: list[MatchedPost] = []
records = _capture_matching_warnings(
lambda: matches.extend(find_anniversary_matches(root, SITE_URL, today=today))
)
assert matches == []
assert _event_names(records) == ["post_unpublished"]
def test_iter_anniversary_paths_yields_only_path(tmp_path: Path) -> None:
root = tmp_path / "_posts" / "blog"
_write_post(
root,
"2014/2014-08-04-foo.md",
body="---\ntitle: Foo\ndate: 2014-08-04\n---\n",
)
paths = list(iter_anniversary_paths(root, SITE_URL, today=date(2024, 8, 4)))
assert paths == ["2014/2014-08-04-foo.md"]
@pytest.mark.parametrize(
"today, source_iso, expected_match",
[
# Normal exact match (Mar 1 source, Mar 1 today) -> match
(date(2025, 3, 1), "2015-03-01", True),
# Mar 1 source (2016, the day after Feb 29) in a non-leap current year -> no match
(date(2025, 3, 1), "2016-03-01", False),
# Feb 29 source in a non-leap current year, today is Mar 1: source year
# (2016) does not equal target_year (2015), so it does NOT match under
# the strict year-equality rule.
(date(2025, 3, 1), "2016-02-29", False),
# Feb 29 source (2016) in a leap current year (2028), today is Feb 29:
# target_year (2018) is not a leap year, but the Feb 29 -> Feb 29
# leap-year branch accepts any prior Feb 29 post.
(date(2028, 2, 29), "2016-02-29", True),
# Feb 29 source (2020) in a leap current year (2028), today is Feb 29:
# same branch, also matches.
(date(2028, 2, 29), "2020-02-29", True),
],
)
def test_leap_day_matching(
tmp_path: Path, today: date, source_iso: str, expected_match: bool
) -> None:
root = tmp_path / "_posts" / "blog"
target_year = today.year - 10
post_year = int(source_iso.split("-", 1)[0])
rel = f"{post_year}/{source_iso}-leap.md"
_write_post(
root,
rel,
body=f"---\ntitle: Leap\ndate: {source_iso}\n---\n",
)
matches = find_anniversary_matches(root, SITE_URL, today=today)
matched_paths = [m.path for m in matches]
if expected_match:
assert rel in matched_paths
else:
assert rel not in matched_paths
def test_leap_day_feb29_matches_with_canonical_url(tmp_path: Path) -> None:
root = tmp_path / "_posts" / "blog"
rel = "2016/2016-02-29-ten-years-before-2026.md"
_write_post(
root,
rel,
body=(
"---\n"
"title: Would Be Ten Years Ago Today In 2026 Mar 1\n"
"date: 2016-02-29 08:00:00\n"
"---\n"
"body\n"
),
)
today = date(2028, 2, 29)
matches = find_anniversary_matches(root, SITE_URL, today=today)
assert len(matches) == 1
match = matches[0]
assert match.path == rel
assert match.date == date(2016, 2, 29)
assert match.title == "Would Be Ten Years Ago Today In 2026 Mar 1"
assert match.url == "https://chaospott.de/2016/02/29/would-be-ten-years-ago-today-in-2026-mar-1/"
def test_match_uses_datetime_frontmatter_date(tmp_path: Path) -> None:
root = tmp_path / "_posts" / "blog"
_write_post(
root,
"2014/2014-08-04-with-time.md",
body="---\ntitle: WithTime\ndate: 2014-08-04T12:30:00\n---\n",
)
today = date(2024, 8, 4)
matches = find_anniversary_matches(root, SITE_URL, today=today)
assert len(matches) == 1
assert matches[0].date == date(2014, 8, 4)
def test_match_returns_results_in_deterministic_order(tmp_path: Path) -> None:
root = tmp_path / "_posts" / "blog"
for slug, day in [("alpha", 1), ("beta", 2), ("gamma", 3)]:
_write_post(
root,
f"2014/2014-08-0{day}-{slug}.md",
body=f"---\ntitle: {slug}\ndate: 2014-08-0{day}\n---\n",
)
today = date(2024, 8, 2)
matches = find_anniversary_matches(root, SITE_URL, today=today)
assert [m.path for m in matches] == ["2014/2014-08-02-beta.md"]
def test_missing_post_root_returns_empty(tmp_path: Path) -> None:
today = date(2024, 8, 4)
assert find_anniversary_matches(tmp_path / "does-not-exist", SITE_URL, today=today) == []
def test_invalid_filename_is_skipped(tmp_path: Path) -> None:
root = tmp_path / "_posts" / "blog"
_write_post(root, "2014/not-a-date.md", body="---\ntitle: Bad\n---\n")
today = date(2024, 8, 4)
matches: list[MatchedPost] = []
records = _capture_matching_warnings(
lambda: matches.extend(find_anniversary_matches(root, SITE_URL, today=today))
)
assert matches == []
assert "post_filename_invalid" in _event_names(records)