init
This commit is contained in:
187
tests/test_bausteine.py
Normal file
187
tests/test_bausteine.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""Deterministische Bausteine: textkit, jsonx, auto_loop, QA-Note."""
|
||||
|
||||
import auto_loop
|
||||
import jsonx
|
||||
import qa
|
||||
import textkit
|
||||
|
||||
|
||||
def test_finde_zitat_whitespace_tolerant():
|
||||
text = "Der Automat\nbesteht aus Zuständen."
|
||||
span = textkit.finde_zitat(text, "Der Automat besteht aus Zuständen.")
|
||||
assert span is not None
|
||||
start, ende = span
|
||||
assert text[start:ende].split() == ["Der", "Automat", "besteht", "aus", "Zuständen."]
|
||||
|
||||
|
||||
def test_finde_zitat_case_fallback_und_fehlschlag():
|
||||
assert textkit.finde_zitat("DER AUTOMAT LÄUFT", "der Automat läuft") is not None
|
||||
assert textkit.finde_zitat("völlig anderer Text", "der Automat läuft") is None
|
||||
|
||||
|
||||
def test_finde_zitat_locker_interpunktion():
|
||||
# PDF-Extrakte variieren Interpunktion/Dashes/Quotes — Wortlaut gleich → Treffer
|
||||
text = "Es gilt: „Ein Automat – mit Zuständen – akzeptiert reguläre Sprachen."
|
||||
zitat = 'Ein Automat, mit Zuständen, akzeptiert reguläre Sprachen'
|
||||
span = textkit.finde_zitat(text, zitat)
|
||||
assert span is not None
|
||||
assert "Automat" in text[span[0]:span[1]]
|
||||
# Paraphrase (andere Wörter) bleibt draußen
|
||||
assert textkit.finde_zitat(text, "Ein DFA mit Zuständen akzeptiert alle Sprachen") is None
|
||||
# zu kurze Zitate matchen locker nicht (Eindeutigkeit)
|
||||
assert textkit.finde_zitat("abc def", "ab, c") is None
|
||||
|
||||
|
||||
def test_finde_zitat_dekomponierte_umlaute():
|
||||
"""Lektion 82: pdftotext liefert dekomponierte Umlaute (a+U+0308), das LLM-Zitat
|
||||
präkomponierte (ä) — die Locker-Stufe faltet beide auf den Basisbuchstaben.
|
||||
Vorher scheiterte fast jedes deutsche Zitat (aak: 59/96 Atome ohne Anker)."""
|
||||
import unicodedata
|
||||
text = unicodedata.normalize("NFD", "Satz 6.25. 3-SAT ist NP-vollständig. Beweis folgt später.")
|
||||
span = textkit.finde_zitat(text, "Satz 6.25. 3-SAT ist NP-vollständig.")
|
||||
assert span is not None
|
||||
assert text[span[0]:span[1]].startswith("Satz 6.25")
|
||||
# umgekehrt: präkomponierter Text, dekomponiertes Zitat
|
||||
assert textkit.finde_zitat(
|
||||
"Die Erfüllbarkeit boolescher Ausdrücke ist zentral.",
|
||||
unicodedata.normalize("NFD", "Erfüllbarkeit boolescher Ausdrücke ist zentral")) is not None
|
||||
|
||||
|
||||
def test_snapshot_schreiben_normalisiert_nfc():
|
||||
"""Korpus-Snapshots werden am Import NFC-normalisiert — die exakte Zitat-Stufe
|
||||
darf nicht an unsichtbar anderen Bytes scheitern."""
|
||||
import unicodedata
|
||||
import korpus
|
||||
roh = unicodedata.normalize("NFD", "NP-vollständig und erfüllbar")
|
||||
assert not unicodedata.is_normalized("NFC", roh)
|
||||
pfad, _h = korpus._snapshot_schreiben("nfc-test", roh)
|
||||
inhalt = open(pfad, encoding="utf-8").read()
|
||||
assert unicodedata.is_normalized("NFC", inhalt)
|
||||
assert inhalt == unicodedata.normalize("NFC", roh)
|
||||
|
||||
|
||||
def test_snapshot_schreiben_ersetzt_kontrollzeichen():
|
||||
"""PDF-Schriften mappen Sonderglyphen (ε) auf Steuerbytes — der Reader echot sie
|
||||
als Müll und das Zitat wird unmatchbar. Import ersetzt sie durch Leerzeichen;
|
||||
\\n und \\t bleiben (Layout)."""
|
||||
import korpus
|
||||
pfad, _h = korpus._snapshot_schreiben("ctrl-test", "Algorithmus (A\x0f )\nZeile\tzwei\x07!")
|
||||
inhalt = open(pfad, encoding="utf-8").read()
|
||||
assert inhalt == "Algorithmus (A )\nZeile\tzwei !"
|
||||
|
||||
|
||||
def test_finde_zitat_fuzzy_listing_zeilennummern():
|
||||
"""Stufe 4 (Lektion 83): pdftotext streut Listing-Zeilennummern in den Text,
|
||||
das Reader-Zitat lässt sie weg — fuzzy mit harter Distanzschranke matcht trotzdem."""
|
||||
text = ("Algorithmus ListScheduling(L=(J1 , . . . , Jn ),m)\n 1 for i=1 to m do\n"
|
||||
" 2 Ei = 0; Bi = ∅;\n 3 od\n 4 for j=1 to n do\n"
|
||||
" 5 wähle i mit Ei minimal;\n 6 od\n")
|
||||
zitat = ("Algorithmus ListScheduling(L=(J1 , . . . , Jn ),m) for i=1 to m do Ei = 0; "
|
||||
"Bi = ∅; od for j=1 to n do wähle i mit Ei minimal; od")
|
||||
span = textkit.finde_zitat(text, zitat)
|
||||
assert span is not None
|
||||
assert text[span[0]:span[1]].startswith("Algorithmus ListScheduling")
|
||||
|
||||
|
||||
def test_finde_zitat_fuzzy_rekonstruierte_glyphe():
|
||||
"""PDF verlor die ε-Glyphe („(A )"), der Reader zitiert „(Aε)" — 1 Zeichen
|
||||
Differenz auf langem Zitat → Treffer."""
|
||||
text = ("Man nennt eine solche Familie von Algorithmen (A ) ein vollständiges "
|
||||
"polynomielles Approximationsschema (FPTAS).")
|
||||
zitat = ("Man nennt eine solche Familie von Algorithmen (Aε) ein vollständiges "
|
||||
"polynomielles Approximationsschema (FPTAS).")
|
||||
assert textkit.finde_zitat(text, zitat) is not None
|
||||
|
||||
|
||||
def test_finde_zitat_fuzzy_mehrdeutig_bleibt_draussen():
|
||||
"""Eindeutigkeits-Guard: zwei fast identische Definitionen (SubSetSum/Partition-
|
||||
Muster) → kein Anker statt Falsch-Anker."""
|
||||
a = "Gegeben: n ganze Zahlen c1 , . . . , cn und eine Zahl K. Entscheide: Gibt es eine Teilmenge S?"
|
||||
b = "Gegeben: n ganze Zahlen c1 , . . . , cn und eine Zahl X. Entscheide: Gibt es eine Teilmenge T?"
|
||||
zitat = "Gegeben: n ganze Zahlen c1 , . . . , cn und eine Zahl Q. Entscheide: Gibt es eine Teilmenge U?"
|
||||
assert textkit.finde_zitat(a + "\n\nDazwischen steht anderer Text.\n\n" + b, zitat) is None
|
||||
|
||||
|
||||
def test_finde_zitat_fuzzy_grenzen():
|
||||
"""Paraphrase über der Distanzschranke bleibt draußen; Kurz-Zitate nie fuzzy."""
|
||||
text = "Der List-Scheduling-Algorithmus verteilt Jobs der Reihe nach auf die am wenigsten belastete Maschine."
|
||||
assert textkit.finde_zitat(text, "List Scheduling weist jeden Job der aktuell "
|
||||
"günstigsten Maschine in Reihenfolge zu und stoppt") is None
|
||||
assert textkit.finde_zitat("kurzer Text über ähm Dinge", "kurzer Test über ähm Dinge") is None
|
||||
|
||||
|
||||
def test_snapshot_schreiben_ftfy_mojibake():
|
||||
"""ftfy am Import: Mojibake und Ligaturen werden repariert (Lektion 83)."""
|
||||
import importlib.util
|
||||
import pytest
|
||||
if importlib.util.find_spec("ftfy") is None:
|
||||
pytest.skip("ftfy nicht installiert — NFC-Fallback deckt test_snapshot_schreiben_normalisiert_nfc")
|
||||
import korpus
|
||||
pfad, _h = korpus._snapshot_schreiben("ftfy-test", "effiziente NP-vollständige Suffixe")
|
||||
inhalt = open(pfad, encoding="utf-8").read()
|
||||
assert "vollständige" in inhalt # Mojibake ä → ä
|
||||
assert "Suffixe" in inhalt # Ligatur ffi → ffi
|
||||
|
||||
|
||||
def test_abschnitte_und_ueberlappung():
|
||||
text = ("Absatz eins.\n\n" * 50) + ("Absatz zwei.\n\n" * 50)
|
||||
teile = textkit.abschnitte(text, max_chars=300)
|
||||
assert all(len(t) <= 300 for _, t in teile)
|
||||
assert "".join(t for _, t in teile) == text
|
||||
assert textkit.ueberlappung((0, 10), (5, 15)) == 0.5
|
||||
assert textkit.ueberlappung((0, 10), (20, 30)) == 0.0
|
||||
|
||||
|
||||
def test_negations_guard():
|
||||
assert textkit.negations_menge("L ist nicht regulär") != textkit.negations_menge("L ist regulär")
|
||||
|
||||
|
||||
def test_jsonx_fences_und_prosa():
|
||||
assert jsonx.parse('```json\n{"a": 1}\n```') == {"a": 1}
|
||||
assert jsonx.parse('Hier das Ergebnis: [{"b": "x — y"}] Danke!') == [{"b": "x — y"}]
|
||||
assert jsonx.parse("kein json") is None
|
||||
assert jsonx.parse('{"s": "mit \\"quote\\" und }"}') == {"s": 'mit "quote" und }'}
|
||||
|
||||
|
||||
async def test_auto_loop_bedingungen():
|
||||
note, grund = await auto_loop.auto_repair_loop("t", 10.0, None)
|
||||
assert grund == "fertig"
|
||||
|
||||
async def stillstand():
|
||||
return 8.0, False
|
||||
note, grund = await auto_loop.auto_repair_loop("t", 8.0, stillstand)
|
||||
assert grund == "stillstand"
|
||||
|
||||
zaehler = {"n": 0}
|
||||
|
||||
async def livelock():
|
||||
zaehler["n"] += 1
|
||||
return 8.0, True # bewegt, aber nie besser → hartes Limit greift
|
||||
note, grund = await auto_loop.auto_repair_loop("t", 7.0, livelock, max_iter=4)
|
||||
assert grund == "limit" and zaehler["n"] == 4
|
||||
|
||||
|
||||
def test_qa_note_formel():
|
||||
assert qa.note([], 10) == 10.0
|
||||
n = qa.note([{"art": "marker_fehlend", "item": "1", "detail": ""}], 10)
|
||||
assert n < 10.0
|
||||
viele = [{"art": "atom_ohne_anker", "item": str(i), "detail": ""} for i in range(50)]
|
||||
assert 0.0 <= qa.note(viele, 10) <= 9.9
|
||||
|
||||
|
||||
def test_det_auftraege_blockquote_und_artefakt():
|
||||
import db
|
||||
import guide
|
||||
from conftest import topic_anlegen
|
||||
topic = topic_anlegen("detcheck")
|
||||
ziel = db.insert("lernziele", topic=topic, text="Kann X", status="aktiv")
|
||||
b_id = db.insert("bausteine", topic=topic, ziel_id=ziel, titel="B", ord=0, status="neu")
|
||||
a = db.insert("atome", topic=topic, titel="A", typ="begriff", definition="d",
|
||||
status="neu", baustein_id=b_id, braucht=db.j([]))
|
||||
lang = (f"<!-- atom: {a} | A | E -->\n" + "Fließtext. " * 45
|
||||
+ "\n> wörtliches Rohzitat aus der Quelle\nEs gilt u 6= v."
|
||||
+ "\n\nDas Blank-Symbol $[. ist speziell und liegt in $[ \\in \\Gamma$.")
|
||||
auftraege = guide._det_auftraege(topic, {"id": b_id, "ord": 0}, lang)
|
||||
text = " ".join(auftraege)
|
||||
assert "Blockquote" in text and "6=" in text
|
||||
assert "ungerade Anzahl $-Zeichen" in text
|
||||
Reference in New Issue
Block a user