#!/usr/bin/env python3 """Testfaelle fuer die Revisionserkennung. python3 -m unittest test_revisionen -v Ohne Datenbank. Die Beispiele stammen saemtlich aus dem Bestand vom 4.-7.9.2026; erfundene Faelle pruefen hier nichts, weil das Modul eine Behauptung ueber echte Redaktionssprache aufstellt. """ import datetime as dt import unittest import normalisierung as n import revisionen as r def fassung(item, minute, titel, teaser="", url="u"): return r.Fassung(item, "zeit", dt.datetime(2026, 9, 7, 8, 0) + dt.timedelta(minutes=minute), dt.datetime(2026, 9, 7, 8, 0), titel, teaser, url) class Pluskasten(unittest.TestCase): """Fuer Revisionen muss der +++-Kasten stehen bleiben.""" TICKER = "++ Liveticker zur Sachsen-Anhalt-Wahl: Wahllokale haben geoeffnet ++" def test_dublettenweg_leert_den_titel(self): self.assertEqual(n.normalisiere(self.TICKER), "") def test_revisionsweg_behaelt_ihn(self): self.assertIn("wahllokale", n.normalisiere(self.TICKER, pluskasten=False)) class Ressortteilung(unittest.TestCase): def test_kopf_und_rest(self): kopf, rest = n.ressort_teilen( "Landtagswahl: AfD-Triumph in Sachsen-Anhalt erschwert die Regierungsbildung") self.assertEqual(kopf, "Landtagswahl") self.assertEqual(rest, "AfD-Triumph in Sachsen-Anhalt erschwert die Regierungsbildung") def test_kein_kopf_wenn_der_rest_zu_kurz_ist(self): # Die Vorsicht aus dubletten.py: bliebe hinter dem Doppelpunkt kein # Satz stehen, war es keine Ressortmarke. self.assertEqual(n.ressort_teilen("Landtagswahl: AfD-Triumph")[0], None) def test_kein_kopf_bei_zitat(self): # "Habeck: Wir haben uns geirrt" - der Teil vor dem Doppelpunkt # traegt die Aussage. ressort_teilen darf ihn nicht abschneiden. self.assertEqual(n.ressort_teilen("Habeck: Wir haben uns geirrt")[0], None) def test_abtrennen_nutzt_teilung(self): t = "Fussball: FIFA-Praesident sagt Besuch in Berlin ab" self.assertEqual(n._ressort_abtrennen(t), n.ressort_teilen(t)[1]) class Klassifikation(unittest.TestCase): def urteil(self, alt, neu): return r.klassifiziere(alt, neu) def test_unveraendert(self): self.assertEqual(self.urteil("Gleicher Titel", "Gleicher Titel"), "unveraendert") def test_formal_gedankenstrich(self): self.assertEqual(self.urteil( "Berliner Senat zahlt nicht - sensible Daten jetzt im Darknet", "Berliner Senat zahlt nicht – sensible Daten jetzt im Darknet"), "formal") def test_formal_apostroph(self): self.assertEqual(self.urteil("Zeigen, wie’s geht in der Bergdiele", "Zeigen, wie's geht in der Bergdiele"), "formal") def test_tippfehler(self): self.assertEqual(self.urteil( "Waldesaster in Sachsen-Anhalt: Warken sieht Reformkurs bestaetigt", "Wahldesaster in Sachsen-Anhalt: Warken sieht Reformkurs bestaetigt"), "tippfehler") def test_tippfehler_nicht_bei_wortkuerzung(self): # "ARD-Sondersendung" -> "ARD-Sendung" ist eine Kuerzung, kein # Tippfehler. Die Laengengrenze muss das trennen. self.assertNotEqual(self.urteil( "Livestream: ARD-Sondersendung zur Wahl in Sachsen-Anhalt", "Livestream: ARD-Sendung zur Wahl in Sachsen-Anhalt"), "tippfehler") def test_kopfwechsel(self): self.assertEqual(self.urteil( "Sabotage an Umspannwerken: Polizei fahndet nach mutmasslichem Taeter", "Stromnetz-Sabotage: Polizei fahndet nach mutmasslichem Taeter"), "kopfwechsel") def test_kopf_entfernt_ist_kopfwechsel(self): self.assertEqual(self.urteil( "Fussball-Bundesliga: Bayer Leverkusen zerlegt zahnloses Union Berlin", "Bayer Leverkusen zerlegt zahnloses Union Berlin"), "kopfwechsel") def test_erweiterung(self): self.assertEqual(self.urteil( "Natalie Portman ist zum dritten Mal Mutter geworden", "Natalie Portman ist mit 45 zum dritten Mal Mutter geworden"), "erweiterung") def test_kuerzung(self): self.assertEqual(self.urteil( "Top 10: Maehroboter ohne Begrenzungskabel im Test - Dreame vor Mammotion", "Top 10: Maehroboter ohne Begrenzungskabel"), "kuerzung") def test_umformulierung(self): self.assertEqual(self.urteil( "Landtagswahl: AfD-Triumph in Sachsen-Anhalt - Schwierige Regierungsbildung", "Landtagswahl: AfD-Triumph in Sachsen-Anhalt - Regierungsbildung schwierig"), "umformulierung") def test_neufassung(self): self.assertEqual(self.urteil( "Explosionen nahe iranischer Oelinsel Kharg gemeldet", "Iranische Revolutionsgarden haben drei Oeltanker in der Strasse" " von Hormus angegriffen"), "neufassung") def test_leere_fassung_bleibt_unbestimmt(self): self.assertEqual(self.urteil("...", "Ein richtiger Titel mit Woertern"), "unbestimmt") def test_richtung_zaehlt(self): a = "Polizei fahndet nach Taeter" b = "Polizei fahndet mit Fotos nach Taeter" self.assertEqual(self.urteil(a, b), "erweiterung") self.assertEqual(self.urteil(b, a), "kuerzung") class Wortdiff(unittest.TestCase): def test_hinzu_und_entfernt(self): hinzu, entfernt = r.wortdiff("Polizei fahndet nach Taeter", "Polizei sucht mit Fotos nach Taeter") self.assertEqual(hinzu, ["sucht", "mit", "fotos"]) self.assertEqual(entfernt, ["fahndet"]) def test_aehnlichkeit_gleich_ist_eins(self): self.assertEqual(r.aehnlichkeit("Ein Titel", "Ein Titel"), 1.0) def test_aehnlichkeit_faellt_bei_austausch(self): self.assertLess( r.aehnlichkeit("Explosionen nahe iranischer Oelinsel gemeldet", "Revolutionsgarden greifen Oeltanker in Hormus an"), r.aehnlichkeit("Explosionen nahe iranischer Oelinsel gemeldet", "Explosionen nahe iranischer Oelinsel bestaetigt")) class Tickermarken(unittest.TestCase): def test_erkannte_marken(self): for t in ("++ Liveticker zur Wahl: Stand ++", "Newsblog zum Ukraine-Krieg: Angriffe dauern an", "Liveblog zur Wahl in Sachsen-Anhalt: Klingbeil aeussert sich", "Livestream: ARD-Sondersendung zur Wahl", "Live: Heute im Livestream: Sachsen-Anhalt hat gewaehlt"): self.assertTrue(r.ticker_markiert(t), t) def test_liverpool_ist_kein_ticker(self): self.assertFalse(r.ticker_markiert( "Liverpool gelingt bei Ipswich der erste Saisonsieg")) def test_ressortkasten_ist_kein_ticker(self): # handelsblatt setzt "+++ USA +++:" als Ressortmarke. self.assertFalse(r.ticker_markiert( "+++ USA +++: Briefwahl in den USA beginnt")) class Rubrik(unittest.TestCase): """kopf() sieht nur nach, ob die Rubrik dieselbe blieb - ohne die Vorsicht von ressort_teilen(), die kurze Tickerstaende verschluckt.""" def test_kurzer_stand_behaelt_seinen_kopf(self): self.assertEqual(r.kopf("Liveblog zur Wahl: Wahllokale sind geoeffnet"), r.kopf("Liveblog zur Wahl: Erste Hochrechnung sieht AfD vorn")) def test_ohne_doppelpunkt_kein_kopf(self): self.assertIsNone(r.kopf("Hertha und Magdeburg liefern sich ein wildes Spiel")) def test_kopf_ohne_wortgrenze(self): # ressort_teilen() liesse den fuenfwortigen Kopf fallen, teilen() nicht. a = "CDU-Desaster in Sachsen-Anhalt: Jetzt wird es richtig chaotisch" b = "CDU-Desaster bei Wahl in Sachsen-Anhalt: Jetzt wird es richtig chaotisch" self.assertEqual(r.klassifiziere(a, b), "kopfwechsel") def test_rest_wird_mitgeteilt(self): self.assertEqual(r.teilen("Verkehr: Unfall auf der A3"), ("verkehr", "unfall auf der a3")) def test_gewechselte_rubrik(self): self.assertNotEqual(r.kopf("Sabotage an Umspannwerken: Polizei fahndet"), r.kopf("Stromnetz-Sabotage: Polizei fahndet")) class Zahlen(unittest.TestCase): def test_geaenderte_zahl(self): self.assertNotEqual(r.zahlen("54,4 Prozent am Nachmittag"), r.zahlen("65,3 Prozent am Nachmittag")) def test_umgestellte_zahl_bleibt_gleich(self): self.assertEqual(r.zahlen("2 zu 1 fuer Hertha"), r.zahlen("1 zu 2 fuer Hertha")) class Nutzlast(unittest.TestCase): def test_unveraendertes_item(self): l = r.nutzlast([fassung(1, 0, "Ein Titel, der so bleibt")]) self.assertEqual(l["fassungen"], 1) self.assertEqual(l["titel_aenderungen"], 0) self.assertEqual(l["art"], "unveraendert") self.assertEqual(l["hinzu"], []) self.assertEqual(l["aehnlichkeit"], 1.0) def test_erste_gegen_letzte(self): l = r.nutzlast([ fassung(2, 0, "Polizei fahndet nach Taeter"), fassung(2, 15, "Polizei fahndet mit Fotos nach Taeter"), fassung(2, 45, "Polizei fahndet mit Fotos und Video nach Taeter"), ]) self.assertEqual(l["fassungen"], 3) self.assertEqual(l["titel_aenderungen"], 2) self.assertEqual(l["arten"], ["erweiterung", "erweiterung"]) self.assertEqual(l["art"], "erweiterung") self.assertEqual(l["spanne_min"], 45) def test_zurueckgenommen(self): # Der faz-Fall aus dem Bestand: "dessen" eingefuegt und wieder # gestrichen. Erste und letzte Fassung sind gleich, geaendert wurde # trotzdem. l = r.nutzlast([ fassung(3, 0, "Siegmund dankt Elon Musk fuer Unterstuetzung"), fassung(3, 90, "Siegmund dankt Elon Musk fuer dessen Unterstuetzung"), fassung(3, 120, "Siegmund dankt Elon Musk fuer Unterstuetzung"), ]) self.assertEqual(l["art"], "zurueckgenommen") self.assertEqual(l["titel_aenderungen"], 2) def test_kopf_stabil(self): stabil = r.nutzlast([ fassung(4, 0, "Liveblog zur Wahl: Wahllokale sind geoeffnet"), fassung(4, 60, "Liveblog zur Wahl: Erste Hochrechnung sieht AfD vorn"), ]) self.assertTrue(stabil["kopf_stabil"]) self.assertTrue(stabil["ticker_markiert"]) gewechselt = r.nutzlast([ fassung(5, 0, "Sabotage an Umspannwerken: Polizei fahndet nach Taeter"), fassung(5, 60, "Stromnetz-Sabotage: Polizei fahndet nach Taeter"), ]) self.assertFalse(gewechselt["kopf_stabil"]) def test_teaser_getrennt_gezaehlt(self): l = r.nutzlast([ fassung(6, 0, "Gleicher Titel bleibt stehen", "Alter Teaser mit Text"), fassung(6, 30, "Gleicher Titel bleibt stehen", "Neuer Teaser mit Text"), ]) self.assertEqual(l["titel_aenderungen"], 0) self.assertEqual(l["teaser_aenderungen"], 1) self.assertIsNotNone(l["teaser_art"]) self.assertEqual(l["art"], "unveraendert") def test_wortlisten_gekappt(self): lang = " ".join(f"wort{i}" for i in range(40)) l = r.nutzlast([fassung(7, 0, "Ein kurzer Titel steht hier"), fassung(7, 10, lang)]) self.assertLessEqual(len(l["hinzu"]), r.WOERTER_MAX) self.assertTrue(l["gekappt"]) def test_keine_zeichenkette_ueber_hundert(self): # Spiegelt den Constraint codings_kein_volltext. lang = "Ein sehr langer Titel " * 12 l = r.nutzlast([fassung(8, 0, lang), fassung(8, 10, lang + " Nachtrag")]) for wert in _zeichenketten(l): self.assertLessEqual(len(wert), 100, wert) def _zeichenketten(wert): if isinstance(wert, str): yield wert elif isinstance(wert, dict): for v in wert.values(): yield from _zeichenketten(v) elif isinstance(wert, list): for v in wert: yield from _zeichenketten(v) if __name__ == "__main__": unittest.main()