"""Arch news. Upstream uses the news feed to announce upgrades that will not complete on their own, so a tool that applies updates for you should not let those pass unnoticed. The feed is RSS 2.0 and parsed with the standard library; the interesting parts are the judgement calls, not the parsing. """ from __future__ import annotations import sys import tempfile import time from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from updater import news # noqa: E402 FEED = """ Arch Linux: Recent news updates Mkinitcpio requires manual intervention for LUKS https://archlinux.org/news/mkinitcpio/ <p>You must run <code>mkinitcpio -P</code> before rebooting.</p> David Runge Tue, 22 Sep 2026 09:09:27 +0000 tag:archlinux.org,2026-09-22:/news/mkinitcpio/ A routine package note https://archlinux.org/news/routine/ <p>Nothing to do here.</p> Someone Mon, 01 Sep 2026 09:00:00 +0000 tag:archlinux.org,2026-09-01:/news/routine/ Missing date https://archlinux.org/news/undated/ no date at all x """ def parsed(): return news._parse(FEED) # -------------------------------------------------------------------------- # parsing # -------------------------------------------------------------------------- def test_parses_titles_links_dates_and_authors(): items = parsed() assert len(items) == 3, items newest = items[0] assert newest.title.startswith("Mkinitcpio") assert newest.link.endswith("/news/mkinitcpio/") assert newest.author == "David Runge", newest.author # the namespaced must be found whichever prefix it uses assert newest.published > 0 def test_html_is_stripped_from_the_summary(): """The description is markup; printing it raw would be a wall of tags.""" item = parsed()[0] assert "<" not in item.summary and ">" not in item.summary assert "mkinitcpio -P" in item.summary def test_items_are_newest_first(): stamps = [i.published for i in parsed()] assert stamps == sorted(stamps, reverse=True), stamps def test_an_item_without_a_date_is_kept_not_dropped(): """Losing an item is worse than showing one with an odd timestamp. A stamp of zero is never greater than a positive cutoff, so an undated item was invisible in `recent()` for good -- an Arch news item nobody would ever see. It inherits the feed's newest stamp instead. """ items = parsed() undated = [i for i in items if i.title == "Missing date"] assert len(undated) == 1 assert undated[0].published > 0 # and it survives the filter that used to hide it shown = news.recent(items, since=1.0) assert "Missing date" in [i.title for i in shown] def test_a_broken_feed_is_not_an_exception(): for bad in ("", "not xml at all", "", "x"): assert news._parse(bad) == [], bad def test_an_item_without_a_title_is_skipped(): feed = ( "xreal" ) got = news._parse(feed) assert [i.title for i in got] == ["real"] # -------------------------------------------------------------------------- # what needs a human # -------------------------------------------------------------------------- def test_manual_intervention_is_recognised(): item = parsed()[0] assert item.needs_action, item.title def test_a_routine_item_is_not_flagged(): routine = next(i for i in parsed() if i.title == "A routine package note") assert not routine.needs_action def test_the_phrases_that_signal_required_action(): for phrase in ( "requires manual intervention", "manual action needed", "before upgrading, do this", "you will need to reboot", ): item = news.NewsItem(title=phrase, link="", published=time.time()) assert item.needs_action, phrase # -------------------------------------------------------------------------- # recency # -------------------------------------------------------------------------- def test_recent_uses_the_recorded_timestamp(): now = time.time() items = [ news.NewsItem("new", "", now - 60), news.NewsItem("old", "", now - 100000), ] assert [i.title for i in news.recent(items, since=now - 3600)] == ["new"] def test_recent_falls_back_to_a_window_when_nothing_is_recorded(): now = time.time() items = [ news.NewsItem("fresh", "", now - 3600), news.NewsItem("ancient", "", now - 400 * 24 * 3600), ] got = news.recent(items, since=0.0) assert [i.title for i in got] == ["fresh"] # -------------------------------------------------------------------------- # presentation # -------------------------------------------------------------------------- def test_the_summary_says_how_many_need_a_decision(): items = parsed() line = news.summary_line(items) assert "needing a decision" in line, line assert news.summary_line([]) == "nothing new" def test_the_explanation_leads_with_what_needs_action(): items = parsed() text = "\n".join(news.explain(items)) assert "READ BEFORE UPDATING" in text, text assert "ALSO RECENT" in text, text # the item that needs a decision comes before the routine one assert text.index("Mkinitcpio") < text.index("A routine package note"), text assert "archlinux.org" in text, "the link should be there to follow" def test_explaining_nothing_is_reassuring_not_empty(): lines = news.explain([]) assert lines and "nothing new" in lines[0] # -------------------------------------------------------------------------- # the real feed, when it is reachable # -------------------------------------------------------------------------- def test_fetching_the_real_feed_never_raises(): with tempfile.TemporaryDirectory() as td: import os os.environ["XDG_CACHE_HOME"] = td items, error = news.fetch_news(timeout=15) assert isinstance(items, list) if error: assert isinstance(error, str) and error else: assert items, "no error but no items either" for item in items: assert item.title assert item.link.startswith("https://") def main() -> int: tests = [(n, f) for n, f in sorted(globals().items()) if n.startswith("test_") and callable(f)] failed = 0 for name, fn in tests: try: fn() except Exception as exc: # noqa: BLE001 failed += 1 print(f"FAIL {name}\n {type(exc).__name__}: {exc}") else: print(f"ok {name}") print(f"\n{len(tests) - failed}/{len(tests)} passed") return 1 if failed else 0 if __name__ == "__main__": raise SystemExit(main())