import unittest from contextlib import redirect_stdout from io import StringIO from pathlib import Path from radioeins_top100 import ( POINTS, IncompleteJuryListError, build_argument_parser, decide_incomplete_list, derive_output_dir, parse_jury_page, ) class RadioeinsTop100Tests(unittest.TestCase): def parse_fixture(self, name): fixture = Path(__file__).parent / "fixtures" / name return parse_jury_page( fixture.read_text(encoding="utf-8"), fallback_name="Fallback", source_url=f"https://example.test/{name}", ) def test_output_directory_is_derived_from_url_path(self): base = "https://www.radioeins.de/musik/top_100/2026" self.assertEqual(derive_output_dir(f"{base}/zeit/"), Path("output_zeit")) self.assertEqual( derive_output_dir(f"{base}/deutscher-hip-hop"), Path("output_deutscher-hip-hop"), ) def test_point_mapping(self): self.assertEqual( [POINTS[rank] for rank in range(1, 11)], [12, 10, 8, 7, 6, 5, 4, 3, 2, 1], ) def test_parse_existing_jury_fixture(self): votes = self.parse_fixture("aditya-sharma.html") self.assertEqual(len(votes), 10) self.assertEqual(votes[0].juror, "Aditya Sharma") self.assertEqual((votes[0].rank, votes[0].points), (1, 12)) self.assertEqual((votes[1].artist, votes[1].title), ("Samy Deluxe", "Weck Mich Auf")) self.assertEqual((votes[-1].rank, votes[-1].points), (10, 1)) def test_parse_historical_page_without_heading(self): votes = self.parse_fixture("andreas-loeffler.html") self.assertEqual(len(votes), 10) self.assertEqual(votes[0].juror, "Andreas Löffler") self.assertEqual((votes[0].artist, votes[0].title), ("Die Ärzte", "Schrei nach Liebe")) self.assertEqual((votes[-1].rank, votes[-1].points), (10, 1)) def test_parse_historical_page_with_html_spacing(self): votes = self.parse_fixture("andreas-ulrich.html") self.assertEqual(len(votes), 10) self.assertEqual(votes[5].title, "Pocahontas") self.assertEqual(votes[-1].artist, "Wir Sind Helden") def test_historical_title_preserves_additional_colons(self): fixture = Path(__file__).parent / "fixtures" / "andreas-loeffler.html" html = fixture.read_text(encoding="utf-8").replace( "Schrei nach Liebe", "Live: Schrei nach Liebe" ) votes = parse_jury_page(html, "Fallback", "https://example.test/list.html") self.assertEqual(votes[0].title, "Live: Schrei nach Liebe") def test_historical_page_accepts_spaced_hyphen_separator(self): votes = self.parse_fixture("aydo-abay.html") self.assertEqual(len(votes), 10) self.assertEqual(votes[7].artist, "Boxhmasters") self.assertEqual(votes[7].title, "Mogli") def test_historical_rank_without_dot(self): votes = self.parse_fixture("love-aditya-sharma.html") self.assertEqual((votes[4].rank, votes[4].artist, votes[4].title), (5, "Nick Drake", "Northern Sky")) def test_historical_period_separator(self): christiane = self.parse_fixture("love-christiane-falk.html") sven = self.parse_fixture("love-sven-erik-stephan.html") self.assertEqual((christiane[3].artist, christiane[3].title), ("Jeff Buckley", "Hallelujah")) self.assertEqual((sven[6].artist, sven[6].title), ("Cyndi Lauper", "Time After Time")) self.assertEqual(christiane[2].artist, "R.E.M.") def test_incomplete_historical_list_is_rejected(self): with self.assertRaises(IncompleteJuryListError) as caught: self.parse_fixture("love-dagobert.html") output = StringIO() with redirect_stdout(output): accepted = decide_incomplete_list( caught.exception, accept_incomplete=False, interactive=False, decision_helper=lambda _: self.fail("must not prompt"), ) self.assertFalse(accepted) self.assertIn("Dagobert", output.getvalue()) self.assertEqual(caught.exception.missing_ranks, [10]) def test_incomplete_historical_list_is_accepted_interactively(self): with self.assertRaises(IncompleteJuryListError) as caught: self.parse_fixture("love-dagobert.html") with redirect_stdout(StringIO()): accepted = decide_incomplete_list( caught.exception, accept_incomplete=False, interactive=True, decision_helper=lambda prompt: "yes", ) self.assertTrue(accepted) self.assertEqual( [(vote.rank, vote.points) for vote in caught.exception.votes], [(1, 12), (2, 10), (3, 8), (4, 7), (5, 6), (6, 5), (7, 4), (8, 3), (9, 2)], ) def test_accept_incomplete_option(self): args = build_argument_parser().parse_args(["--accept-incomplete"]) self.assertTrue(args.accept_incomplete) with self.assertRaises(IncompleteJuryListError) as caught: self.parse_fixture("love-dagobert.html") with redirect_stdout(StringIO()): accepted = decide_incomplete_list( caught.exception, accept_incomplete=args.accept_incomplete, interactive=False, decision_helper=lambda _: self.fail("must not prompt"), ) self.assertTrue(accepted) if __name__ == "__main__": unittest.main()