149 lines
5.6 KiB
Python
149 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Characterization tests for tools/migrate_pages_to_content.py (U1).
|
|
|
|
Runs against the real detail pages in the repo, so it guards both the extractor
|
|
logic and the assumption that every current blog/news/event page is parseable.
|
|
|
|
Run directly: python tools/tests/test_migrate_roundtrip.py
|
|
Or with pytest: pytest tools/tests/test_migrate_roundtrip.py
|
|
"""
|
|
import os
|
|
import sys
|
|
import unittest
|
|
|
|
TOOLS = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
ROOT = os.path.dirname(TOOLS)
|
|
sys.path.insert(0, TOOLS)
|
|
|
|
import yaml # noqa: E402
|
|
import migrate_pages_to_content as mig # noqa: E402
|
|
|
|
|
|
def _read(rel):
|
|
with open(os.path.join(ROOT, rel), encoding="utf-8") as f:
|
|
return f.read()
|
|
|
|
|
|
def _split_front_matter(text):
|
|
"""Return (metadata_dict, body_str) from front-matter file text."""
|
|
assert text.startswith("---\n"), "file must start with a front-matter fence"
|
|
_, front, body = text.split("---\n", 2)
|
|
return yaml.safe_load(front), body
|
|
|
|
|
|
class BlogExtraction(unittest.TestCase):
|
|
def setUp(self):
|
|
slug = "how-to-open-slovenian-bank-account"
|
|
html = _read(f"en/blog/{slug}/index.html")
|
|
self.rec, self.err = mig.parse_page(html, "blog", slug, "en")
|
|
|
|
def test_parses(self):
|
|
self.assertIsNone(self.err)
|
|
self.assertIsNotNone(self.rec)
|
|
|
|
def test_structured_fields(self):
|
|
self.assertEqual(self.rec["type"], "blog")
|
|
self.assertEqual(self.rec["source_folder"], "blog")
|
|
self.assertTrue(self.rec["title"])
|
|
self.assertIn("subtitle", self.rec)
|
|
self.assertEqual(self.rec["category"], "Student life")
|
|
self.assertEqual(self.rec["date"], "2026-08-01")
|
|
|
|
def test_hero(self):
|
|
hero = self.rec["hero"]
|
|
self.assertTrue(hero["src"].startswith("/images/"))
|
|
self.assertTrue(hero["src"].endswith(".webp"))
|
|
self.assertIn("alt", hero)
|
|
|
|
def test_body_preserves_rich_elements(self):
|
|
body = self.rec["_body"]
|
|
self.assertEqual(self.rec["body_format"], "html")
|
|
self.assertIn("guide-box", body) # aside survives
|
|
self.assertIn('id="why-useful"', body) # h2 anchors survive (scrollspy source)
|
|
self.assertTrue(body.lstrip().startswith('<div class="article-body">'))
|
|
|
|
|
|
class NewsExtraction(unittest.TestCase):
|
|
def setUp(self):
|
|
slug = "proof-of-means-updated-2026"
|
|
html = _read(f"en/news/{slug}/index.html")
|
|
self.rec, self.err = mig.parse_page(html, "news", slug, "en")
|
|
|
|
def test_parses(self):
|
|
self.assertIsNone(self.err)
|
|
|
|
def test_news_has_no_subtitle(self):
|
|
# News pages have no .article-subtitle; the field must be omitted, not empty.
|
|
self.assertNotIn("subtitle", self.rec)
|
|
self.assertEqual(self.rec["type"], "news")
|
|
|
|
def test_hero_present(self):
|
|
self.assertIn("hero", self.rec)
|
|
|
|
|
|
class EventExtraction(unittest.TestCase):
|
|
def setUp(self):
|
|
slug = "paint-and-wine-at-sunset"
|
|
html = _read(f"en/projects/{slug}/index.html")
|
|
self.rec, self.err = mig.parse_page(html, "projects", slug, "en")
|
|
|
|
def test_type_and_folder_mapping(self):
|
|
self.assertIsNone(self.err)
|
|
self.assertEqual(self.rec["type"], "event")
|
|
self.assertEqual(self.rec["source_folder"], "projects") # URL/date key unchanged
|
|
|
|
def test_gallery_and_lightbox_preserved(self):
|
|
body = self.rec["_body"]
|
|
self.assertIn("event-gallery", body) # sibling section after .article-body
|
|
self.assertIn("eg-thumb", body) # lightbox thumbnails
|
|
self.assertIn("data-full", body)
|
|
|
|
|
|
class CyrillicRoundTrip(unittest.TestCase):
|
|
def test_mk_serialises_and_parses_without_mojibake(self):
|
|
slug = "proof-of-means-updated-2026"
|
|
html = _read(f"mk/news/{slug}/index.html")
|
|
rec, err = mig.parse_page(html, "news", slug, "mk")
|
|
self.assertIsNone(err)
|
|
# Title is Macedonian Cyrillic.
|
|
self.assertTrue(any("Ѐ" <= ch <= "ӿ" for ch in rec["title"]))
|
|
text = mig.to_file_text(rec)
|
|
meta, body = _split_front_matter(text)
|
|
self.assertEqual(meta["title"], rec["title"]) # exact UTF-8 round-trip
|
|
self.assertEqual(meta["lang"], "mk")
|
|
self.assertTrue(body.strip())
|
|
|
|
|
|
class FrontMatterRoundTrip(unittest.TestCase):
|
|
def test_metadata_survives_serialise_parse(self):
|
|
slug = "how-to-open-slovenian-bank-account"
|
|
html = _read(f"en/blog/{slug}/index.html")
|
|
rec, _ = mig.parse_page(html, "blog", slug, "en")
|
|
meta, _body = _split_front_matter(mig.to_file_text(rec))
|
|
for key in ("type", "slug", "lang", "source_folder", "title", "date", "body_format"):
|
|
self.assertEqual(meta[key], rec[key], f"{key} changed across round-trip")
|
|
|
|
|
|
class EveryPageParses(unittest.TestCase):
|
|
def test_no_page_is_skipped(self):
|
|
"""Every current blog/news/event page must parse (a skip is a regression)."""
|
|
en_cache = {}
|
|
for lang, folder, slug, page in mig.iter_pages():
|
|
if lang == "en":
|
|
with open(page, encoding="utf-8") as f:
|
|
en_cache[(folder, slug)] = f.read()
|
|
failures = []
|
|
for lang, folder, slug, page in mig.iter_pages():
|
|
with open(page, encoding="utf-8") as f:
|
|
html = f.read()
|
|
rec, err = mig.parse_page(html, folder, slug, lang,
|
|
en_html=en_cache.get((folder, slug)))
|
|
if rec is None:
|
|
failures.append(f"{lang}/{folder}/{slug}: {err}")
|
|
self.assertEqual(failures, [], f"unparseable pages: {failures}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|