199 lines
6.3 KiB
Python
199 lines
6.3 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,
|
|
)
|
|
|
|
|
|
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/"
|
|
|
|
|
|
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/"
|
|
|
|
|
|
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),
|
|
],
|
|
)
|
|
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
|
|
rel = f"{target_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_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) |