update
This commit is contained in:
@@ -1,60 +0,0 @@
|
||||
import inhalt
|
||||
import quellcode
|
||||
|
||||
|
||||
def test_unformatierte_prompts_ohne_doppelklammern():
|
||||
"""Was nicht durch .format() geht, darf keine Escape-Klammern tragen —
|
||||
das Modell schrieb sie sonst wörtlich in die Antwort und das JSON war kaputt."""
|
||||
for name in ("ABDECKUNG_SYSTEM",):
|
||||
text = getattr(inhalt, name)
|
||||
assert "{{" not in text and "}}" not in text, name
|
||||
|
||||
|
||||
def test_formatierte_prompts_ueberstehen_format():
|
||||
punkt = {"art": "Bedienung", "datei": "a.vue", "zeile": 1,
|
||||
"umfeld": "x", "rumpf": ""}
|
||||
fertig = inhalt.ABDECKUNG.format(funktionen="—", **punkt)
|
||||
assert '{"abgedeckt": true' in fertig
|
||||
assert "{{" not in fertig
|
||||
|
||||
|
||||
def test_beleg_muss_woertlich_dastehen():
|
||||
quelle = {"a.py": "def eins():\n return 1\n"}
|
||||
assert quellcode.belegt(quelle, "a.py", "def eins():")
|
||||
assert quellcode.belegt(quelle, "a.py", " def eins(): ") # Einrückung egal
|
||||
assert quellcode.belegt(quelle, "a.py", "def eins():\nreturn 1")
|
||||
assert not quellcode.belegt(quelle, "a.py", "def zwei():")
|
||||
assert not quellcode.belegt(quelle, "b.py", "def eins():")
|
||||
assert not quellcode.belegt(quelle, "a.py", "")
|
||||
|
||||
|
||||
def test_interaktionen_sind_deterministisch():
|
||||
quelle = {
|
||||
"frontend/src/X.vue": (
|
||||
'<button @click="starte(\'ziel\')">a</button>\n'
|
||||
'<button @click="starte(\'regeln\')">b</button>\n'
|
||||
'<div @contextmenu="menue">c</div>\n'
|
||||
'<input @blur="fertig" @keydown.esc="abbrechen">\n'
|
||||
'<span @click="offen = false">zu</span>\n'
|
||||
),
|
||||
"backend/routes.py": '@router.get("/projekte")\n@router.post("/projekte")\n',
|
||||
"frontend/src/m.js": " { text: '+ Projekt erstellen', aktion: () => x() },\n",
|
||||
}
|
||||
schluessel = [p["schluessel"] for p in quellcode.interaktionen(quelle)]
|
||||
# Zwölf Aufrufe von starte() sind eine Fähigkeit, nicht zwölf
|
||||
assert schluessel.count("bedienen:starte") == 1
|
||||
assert "bedienen:menue" in schluessel
|
||||
# Routen: der einzige Weg an Parser, Dateiverwaltung und Prüflogik
|
||||
assert "route:get /projekte" in schluessel
|
||||
assert "route:post /projekte" in schluessel
|
||||
assert "menue:+ Projekt erstellen" in schluessel
|
||||
# Technische Aufhänger und Zuweisungen sind keine Fähigkeiten
|
||||
assert not any(s.startswith("bedienen:fertig") for s in schluessel)
|
||||
assert not any(s.startswith("bedienen:abbrechen") for s in schluessel)
|
||||
assert not any(s.startswith("bedienen:offen") for s in schluessel)
|
||||
# Zweiter Aufruf liefert exakt dasselbe
|
||||
assert schluessel == [p["schluessel"] for p in quellcode.interaktionen(quelle)]
|
||||
|
||||
|
||||
def test_tests_sind_kein_quelltext():
|
||||
assert not any("tests/" in p for p in quellcode.dateien())
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Das Gedächtnis der Inhaltsprüfungen: was gilt weiter, was wird neu gefragt.
|
||||
"""Das Gedächtnis der Stilprüfung: was gilt weiter, was wird neu gefragt.
|
||||
|
||||
Die Prüfungen 2 und 3 fragen ein Modell. Dieselbe Frage kippt bei Grenzfällen auch
|
||||
bei Temperatur 0 — vorher meldete Runde 1 einen Befund, Runde 2 keinen, Runde 3
|
||||
wieder einen. Hier steht, unter welchen Bedingungen ein Urteil stehen bleibt.
|
||||
Die Prüfung fragt ein Modell. Dieselbe Frage kippt bei Grenzfällen auch bei
|
||||
Temperatur 0 — vorher meldete Runde 1 einen Befund, Runde 2 keinen, Runde 3 wieder
|
||||
einen. Hier steht, unter welchen Bedingungen ein Urteil stehen bleibt.
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -11,7 +11,6 @@ import pytest
|
||||
|
||||
import agents
|
||||
import features
|
||||
import inhalt
|
||||
import ledger
|
||||
import projekte
|
||||
|
||||
@@ -33,9 +32,6 @@ def karte(projekt, titel, *funktionen):
|
||||
return features.anlegen(projekt, text)
|
||||
|
||||
|
||||
QUELLE = {"frontend/src/App.vue": '<button @click="starte">los</button>\n'}
|
||||
|
||||
|
||||
class Zaehler:
|
||||
"""Ersetzt das Modell und zählt mit, wie oft überhaupt gefragt wurde."""
|
||||
|
||||
@@ -82,431 +78,6 @@ def test_gedaechtnis_ist_kein_feature(projekt):
|
||||
assert [f["sektionen"]["titel"] for f in features.liste(projekt)] == ["Eins"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Prüfung 2
|
||||
|
||||
GEFUNDEN = json.dumps({"gefunden": True, "datei": "frontend/src/App.vue",
|
||||
"beleg": '<button @click="starte">los</button>'})
|
||||
|
||||
|
||||
async def test_beleg_wird_gemerkt_und_beim_zweiten_lauf_nicht_gefragt(
|
||||
projekt, monkeypatch):
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
modell = Zaehler(GEFUNDEN)
|
||||
monkeypatch.setattr(agents, "frage", modell)
|
||||
|
||||
alle = features.liste(projekt)
|
||||
text = "### frontend/src/App.vue\n" + QUELLE["frontend/src/App.vue"]
|
||||
assert await inhalt.pruefe_feature_im_code(projekt, alle, QUELLE, text) == []
|
||||
erste_runde = modell.rufe
|
||||
assert erste_runde >= len(inhalt.SUCHE_TEMPERATUREN)
|
||||
|
||||
assert await inhalt.pruefe_feature_im_code(projekt, alle, QUELLE, text) == []
|
||||
assert modell.rufe == erste_runde # keine einzige neue Anfrage
|
||||
|
||||
|
||||
async def test_beleg_verfaellt_wenn_die_zeile_verschwindet(projekt, monkeypatch):
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
modell = Zaehler(GEFUNDEN)
|
||||
monkeypatch.setattr(agents, "frage", modell)
|
||||
|
||||
alle = features.liste(projekt)
|
||||
text = "### frontend/src/App.vue\n" + QUELLE["frontend/src/App.vue"]
|
||||
await inhalt.pruefe_feature_im_code(projekt, alle, QUELLE, text)
|
||||
vorher = modell.rufe
|
||||
|
||||
anders = {"frontend/src/App.vue": "<div>nichts mehr</div>\n"}
|
||||
await inhalt.pruefe_feature_im_code(projekt, alle, anders, "### x\n<div>")
|
||||
assert modell.rufe > vorher # der Beleg trägt nicht mehr, also neu fragen
|
||||
|
||||
|
||||
async def test_geaenderte_funktion_wird_neu_gefragt(projekt, monkeypatch):
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
modell = Zaehler(GEFUNDEN)
|
||||
monkeypatch.setattr(agents, "frage", modell)
|
||||
|
||||
text = "### frontend/src/App.vue\n" + QUELLE["frontend/src/App.vue"]
|
||||
await inhalt.pruefe_feature_im_code(projekt, features.liste(projekt), QUELLE, text)
|
||||
vorher = modell.rufe
|
||||
|
||||
datei = projekte.wurzel(projekt) / f"{features.liste(projekt)[0]['id']}.md"
|
||||
datei.write_text(datei.read_text().replace("- Knopf", "- Ganz anderer Inhalt"))
|
||||
await inhalt.pruefe_feature_im_code(projekt, features.liste(projekt), QUELLE, text)
|
||||
assert modell.rufe > vorher
|
||||
|
||||
|
||||
async def test_fehlanzeige_gilt_nur_bei_unveraendertem_quelltext(projekt, monkeypatch):
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
modell = Zaehler(json.dumps({"gefunden": False, "grund": "gibt es nicht"}))
|
||||
monkeypatch.setattr(agents, "frage", modell)
|
||||
|
||||
alle = features.liste(projekt)
|
||||
text = "### frontend/src/App.vue\n" + QUELLE["frontend/src/App.vue"]
|
||||
erste = await inhalt.pruefe_feature_im_code(projekt, alle, QUELLE, text)
|
||||
assert len(erste) == 1
|
||||
vorher = modell.rufe
|
||||
|
||||
zweite = await inhalt.pruefe_feature_im_code(projekt, alle, QUELLE, text)
|
||||
assert [p["vorschlag"] for p in zweite] == [p["vorschlag"] for p in erste]
|
||||
assert modell.rufe == vorher
|
||||
|
||||
# Neuer Quelltext: eine Löschempfehlung muss frisch belegt werden
|
||||
mehr = {**QUELLE, "backend/neu.py": "def starte():\n pass\n"}
|
||||
await inhalt.pruefe_feature_im_code(projekt, alle, mehr, text)
|
||||
assert modell.rufe > vorher
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Dubletten
|
||||
|
||||
def punkt(schluessel="bedienen:starte", beleg='<button @click="starte">los</button>'):
|
||||
return {"schluessel": schluessel, "art": "Bedienung",
|
||||
"datei": "frontend/src/App.vue", "zeile": 1, "beleg": beleg,
|
||||
"umfeld": beleg, "rumpf": ""}
|
||||
|
||||
|
||||
async def test_gleicher_beleg_macht_zwei_funktionen_zu_kandidaten(projekt, monkeypatch):
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
karte(projekt, "Zwei", ("Lauf beginnen", "Knopf"))
|
||||
monkeypatch.setattr(agents, "frage", Zaehler(json.dumps({"gegenstand_gleich": True, "ergebnis_gleich": True})))
|
||||
|
||||
alle = features.liste(projekt)
|
||||
stand = ledger.lade(projekt)
|
||||
for f in alle:
|
||||
stand["belege"][ledger.schluessel(f["id"], f["sektionen"]["funktionen"][0]["text"])] = {
|
||||
"abdruck": "egal", "datei": "frontend/src/App.vue",
|
||||
"beleg": '<button @click="starte">los</button>'}
|
||||
gefunden = await inhalt.dubletten(alle, stand)
|
||||
assert len(gefunden) == 1
|
||||
# Gleich stark: der Titel entscheidet, damit der Vorschlag nicht springt
|
||||
assert gefunden[0]["titel"] == "Etwas starten"
|
||||
assert "Lauf beginnen" in gefunden[0]["grund"]
|
||||
|
||||
|
||||
async def test_uneiniges_urteil_ist_keine_dublette_und_wird_gemerkt(
|
||||
projekt, monkeypatch):
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
karte(projekt, "Zwei", ("Lauf beginnen", "Knopf"))
|
||||
|
||||
def wechselhaft(nachricht, ruf):
|
||||
return json.dumps({"gegenstand_gleich": True, "ergebnis_gleich": ruf == 1})
|
||||
|
||||
modell = Zaehler(wechselhaft)
|
||||
monkeypatch.setattr(agents, "frage", modell)
|
||||
|
||||
alle = features.liste(projekt)
|
||||
stand = ledger.lade(projekt)
|
||||
for f in alle:
|
||||
stand["belege"][ledger.schluessel(f["id"], f["sektionen"]["funktionen"][0]["text"])] = {
|
||||
"abdruck": "egal", "datei": "a.vue", "beleg": "x"}
|
||||
assert await inhalt.dubletten(alle, stand) == []
|
||||
vorher = modell.rufe
|
||||
assert await inhalt.dubletten(alle, stand) == []
|
||||
assert modell.rufe == vorher # Ablehnung wird gemerkt, nicht neu bezahlt
|
||||
|
||||
|
||||
def test_gegenstueck_faellt_nicht_ueber_den_titel():
|
||||
"""anlegen/entfernen und öffnen/schließen dürfen den Titelkanal nicht auslösen —
|
||||
sonst kostet jedes Gegensatzpaar drei Anfragen für nichts."""
|
||||
for a, b in (("Feature anlegen", "Feature entfernen"),
|
||||
("Panel öffnen", "Panel schließen")):
|
||||
assert inhalt._aehnlich(a, b) < inhalt.TITEL_KANAL
|
||||
|
||||
|
||||
ENTFERNEN = ["Vollmodus öffnen", "RMB auf die Funktion",
|
||||
'Klick auf "Funktion entfernen"', "Funktion entfernt"]
|
||||
|
||||
|
||||
def test_umschriebene_dublette_wird_kandidat(projekt):
|
||||
"""Anderer Titel, andere Karte, dieselbe Sache. Weder Titel noch Beleg fangen
|
||||
das — nur der Inhaltskanal."""
|
||||
karte(projekt, "Funktionen",
|
||||
("Funktion entfernen", "x-Icon", ENTFERNEN),
|
||||
("Funktion anlegen", "+-Icon", ["Vollmodus öffnen", "RMB auf die Karte",
|
||||
'Klick auf "Funktion erstellen"',
|
||||
"Funktion angelegt"]),
|
||||
("Testschritte anpassen", "Ein Schritt je Zeile",
|
||||
["Vollmodus öffnen", "LMB auf Testen", "Text ändern", "Schritte angepasst"]))
|
||||
karte(projekt, "Features", ("Funktion löschen", "x-Icon", ENTFERNEN))
|
||||
|
||||
alle = inhalt._funktionen(features.liste(projekt))
|
||||
paare = {frozenset((alle[a]["titel"], alle[b]["titel"])) for _, a, b, _ in
|
||||
inhalt._kandidaten(alle, {})}
|
||||
assert frozenset(("Funktion entfernen", "Funktion löschen")) in paare
|
||||
assert frozenset(("Funktion anlegen", "Funktion entfernen")) not in paare
|
||||
|
||||
|
||||
def test_floskeln_zaehlen_nicht_mit(projekt):
|
||||
"""Was fast überall steht, unterscheidet nichts und fliegt aus dem Vergleich."""
|
||||
schritte = ["Vollmodus öffnen", "Text ändern", "Angepasst"]
|
||||
karte(projekt, "Eins",
|
||||
*[(f"{feld} anpassen", "eine Regel", schritte)
|
||||
for feld in ("Titel", "Ziel", "Regeln", "Testen", "Badge")])
|
||||
kerne = inhalt._kerne(inhalt._funktionen(features.liste(projekt)))
|
||||
gemeinsam = set.intersection(*kerne.values())
|
||||
assert gemeinsam == set() # „vollmodus", „ändern", „angepasst" sind raus
|
||||
assert all(k for k in kerne.values()) # jede Funktion behält ihr eigenes Wort
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Prüfung 3
|
||||
|
||||
ABGEDECKT = json.dumps({"abgedeckt": True, "funktion": "Etwas starten"})
|
||||
FEHLT = json.dumps({"abgedeckt": False, "karte": "Eins", "titel": "Neu bedienen"})
|
||||
|
||||
|
||||
async def test_abdeckung_wird_gemerkt_solange_die_funktion_dasteht(
|
||||
projekt, monkeypatch):
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
modell = Zaehler(ABGEDECKT)
|
||||
monkeypatch.setattr(agents, "frage", modell)
|
||||
|
||||
stand = ledger.lade(projekt)
|
||||
alle = features.liste(projekt)
|
||||
assert await inhalt.code_im_feature(alle, [punkt()], stand) == []
|
||||
vorher = modell.rufe
|
||||
assert await inhalt.code_im_feature(alle, [punkt()], stand) == []
|
||||
assert modell.rufe == vorher
|
||||
|
||||
# Funktion weg: die Abdeckung trägt nicht mehr
|
||||
ohne = features.liste(projekt)
|
||||
ohne[0]["sektionen"]["funktionen"] = []
|
||||
await inhalt.code_im_feature(ohne, [punkt()], stand)
|
||||
assert modell.rufe > vorher
|
||||
|
||||
|
||||
async def test_geaenderte_codestelle_wird_neu_beurteilt(projekt, monkeypatch):
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
modell = Zaehler(ABGEDECKT)
|
||||
monkeypatch.setattr(agents, "frage", modell)
|
||||
|
||||
stand = ledger.lade(projekt)
|
||||
alle = features.liste(projekt)
|
||||
await inhalt.code_im_feature(alle, [punkt()], stand)
|
||||
vorher = modell.rufe
|
||||
await inhalt.code_im_feature(alle, [punkt(beleg="ganz anderer Code")], stand)
|
||||
assert modell.rufe > vorher
|
||||
|
||||
|
||||
async def test_fehlmeldung_bleibt_wortgleich_bis_die_funktion_da_ist(
|
||||
projekt, monkeypatch):
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
modell = Zaehler(FEHLT)
|
||||
monkeypatch.setattr(agents, "frage", modell)
|
||||
|
||||
stand = ledger.lade(projekt)
|
||||
alle = features.liste(projekt)
|
||||
erste = await inhalt.code_im_feature(alle, [punkt()], stand)
|
||||
assert [x["titel"] for x in erste] == ["Neu bedienen"]
|
||||
vorher = modell.rufe
|
||||
|
||||
zweite = await inhalt.code_im_feature(alle, [punkt()], stand)
|
||||
assert zweite == erste
|
||||
assert modell.rufe == vorher # kein Flackern, keine neue Anfrage
|
||||
|
||||
|
||||
async def test_angenommener_vorschlag_loest_die_fehlmeldung_ab(projekt, monkeypatch):
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
modell = Zaehler(FEHLT)
|
||||
monkeypatch.setattr(agents, "frage", modell)
|
||||
|
||||
stand = ledger.lade(projekt)
|
||||
await inhalt.code_im_feature(features.liste(projekt), [punkt()], stand)
|
||||
vorher = modell.rufe
|
||||
|
||||
# Der Nutzer nimmt den Vorschlag an — jetzt muss neu beurteilt werden
|
||||
modell.antwort = ABGEDECKT
|
||||
karte(projekt, "Zwei", ("Neu bedienen", "Knopf"))
|
||||
assert await inhalt.code_im_feature(features.liste(projekt), [punkt()], stand) == []
|
||||
assert modell.rufe > vorher
|
||||
|
||||
|
||||
async def test_unentschiedenes_urteil_wird_festgehalten(projekt, monkeypatch):
|
||||
"""Drei zu drei ist ein Münzwurf. Der darf einmal fallen, nicht jede Runde."""
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
|
||||
def haelfte(nachricht, ruf):
|
||||
return ABGEDECKT if ruf % 2 else FEHLT
|
||||
|
||||
modell = Zaehler(haelfte)
|
||||
monkeypatch.setattr(agents, "frage", modell)
|
||||
|
||||
stand = ledger.lade(projekt)
|
||||
alle = features.liste(projekt)
|
||||
assert await inhalt.code_im_feature(alle, [punkt()], stand) == []
|
||||
vorher = modell.rufe
|
||||
assert await inhalt.code_im_feature(alle, [punkt()], stand) == []
|
||||
assert modell.rufe == vorher
|
||||
|
||||
|
||||
async def test_verschwundene_stelle_raeumt_ihren_eintrag_weg(projekt, monkeypatch):
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
monkeypatch.setattr(agents, "frage", Zaehler(ABGEDECKT))
|
||||
|
||||
stand = ledger.lade(projekt)
|
||||
alle = features.liste(projekt)
|
||||
await inhalt.code_im_feature(alle, [punkt()], stand)
|
||||
assert stand["abdeckung"]
|
||||
await inhalt.code_im_feature(alle, [punkt(schluessel="bedienen:anderes")], stand)
|
||||
assert "bedienen:starte" not in stand["abdeckung"]
|
||||
|
||||
|
||||
def test_fehlende_stelle_landet_auch_auf_erfundener_karte(projekt):
|
||||
"""Sechs Stimmen sagen „fehlt", nennen aber Karten, die es nicht gibt. Vorher
|
||||
fiel der Befund lautlos weg."""
|
||||
karte(projekt, "Projekte", ("Projekt anlegen", "+-Icon"))
|
||||
karten = {f["sektionen"]["titel"]: f for f in features.liste(projekt)}
|
||||
urteile = [{"abgedeckt": False, "karte": "Datenverwaltung", "titel": "x"}] * 4
|
||||
assert inhalt._zielkarte(urteile, karten) is karten["Projekte"]
|
||||
|
||||
|
||||
def test_erster_aufloesbarer_kartenname_gewinnt(projekt):
|
||||
karte(projekt, "Projekte", ("Projekt anlegen", "+-Icon"))
|
||||
karte(projekt, "Darkmode", ("Darkmode umschalten", "Icon"))
|
||||
karten = {f["sektionen"]["titel"]: f for f in features.liste(projekt)}
|
||||
urteile = ([{"abgedeckt": False, "karte": "Export", "titel": "x"}] * 3
|
||||
+ [{"abgedeckt": False, "karte": "Darkmode", "titel": "x"}] * 2)
|
||||
assert inhalt._zielkarte(urteile, karten) is karten["Darkmode"]
|
||||
|
||||
|
||||
def test_ohne_karten_kein_ziel(projekt):
|
||||
assert inhalt._zielkarte([{"abgedeckt": False, "karte": "X"}], {}) is None
|
||||
|
||||
|
||||
def test_grund_trennt_dublette_von_fehlendem_code():
|
||||
"""Beides heißt streichen — aber aus verschiedenen Gründen, und der Grund
|
||||
entscheidet, ob man zustimmt."""
|
||||
assert inhalt._wegfall_grund([{"art": "dublette"}]) == "1 Funktion doppelt"
|
||||
assert inhalt._wegfall_grund([{"art": "ohne_code"}] * 2) \
|
||||
== "2 Funktionen ohne eigenen Code"
|
||||
assert inhalt._wegfall_grund([{"art": "dublette"}, {"art": "ohne_code"}]) \
|
||||
== "1 Funktion doppelt, 1 Funktion ohne eigenen Code"
|
||||
|
||||
|
||||
def test_halbe_datei_ueberschreibt_nicht_die_ganze(projekt, monkeypatch):
|
||||
"""Absturz beim Schreiben: die alte Datei muss stehen bleiben."""
|
||||
stand = ledger.lade(projekt)
|
||||
stand["belege"]["a"] = {"abdruck": "x", "datei": "a.py", "beleg": "z"}
|
||||
ledger.sichere(projekt, stand)
|
||||
|
||||
echt = ledger.json.dumps
|
||||
|
||||
def platzt(*a, **k):
|
||||
raise MemoryError("mittendrin")
|
||||
|
||||
monkeypatch.setattr(ledger.json, "dumps", platzt)
|
||||
with pytest.raises(MemoryError):
|
||||
ledger.sichere(projekt, {"belege": {"b": {}}})
|
||||
monkeypatch.setattr(ledger.json, "dumps", echt)
|
||||
assert ledger.lade(projekt)["belege"] == stand["belege"]
|
||||
|
||||
|
||||
async def test_ausgefallene_stimmen_merken_nichts(projekt, monkeypatch):
|
||||
"""Fällt der Anbieter aus, darf keine Löschempfehlung im Gedächtnis landen."""
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
|
||||
async def tot(system, nachricht, temperatur=0.0):
|
||||
raise RuntimeError("429")
|
||||
|
||||
monkeypatch.setattr(agents, "frage", tot)
|
||||
stand = ledger.lade(projekt)
|
||||
gemeldet = []
|
||||
assert await inhalt.feature_im_code(features.liste(projekt), QUELLE, "x", stand,
|
||||
melde=lambda t, n=0: gemeldet.append(n)) == []
|
||||
assert gemeldet == [1] # laut gescheitert, nicht still
|
||||
assert stand["ohne_code"] == {}
|
||||
assert stand["belege"] == {}
|
||||
|
||||
|
||||
async def test_ein_ausfall_rettet_die_anderen_urteile(projekt, monkeypatch):
|
||||
"""Eine kaputte Stelle darf die bezahlten Urteile der anderen nicht wegwerfen."""
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
|
||||
async def launisch(system, nachricht, temperatur=0.0):
|
||||
if "bedienen:kaputt" in nachricht:
|
||||
raise RuntimeError("429")
|
||||
return ABGEDECKT
|
||||
|
||||
monkeypatch.setattr(agents, "frage", launisch)
|
||||
stand = ledger.lade(projekt)
|
||||
gemeldet = []
|
||||
await inhalt.code_im_feature(
|
||||
features.liste(projekt),
|
||||
[punkt(), punkt(schluessel="bedienen:kaputt", beleg="bedienen:kaputt")],
|
||||
stand, melde=lambda t, n=0: gemeldet.append(n))
|
||||
assert gemeldet == [1]
|
||||
assert "bedienen:starte" in stand["abdeckung"]
|
||||
assert "bedienen:kaputt" not in stand["abdeckung"]
|
||||
|
||||
|
||||
async def test_projekt_ohne_karten_stuerzt_nicht_ab(projekt, monkeypatch):
|
||||
monkeypatch.setattr(agents, "frage", Zaehler(FEHLT))
|
||||
stand = ledger.lade(projekt)
|
||||
assert await inhalt.code_im_feature([], [punkt()], stand) == []
|
||||
assert await inhalt.dubletten([], stand) == []
|
||||
assert await inhalt.feature_im_code([], QUELLE, "x", stand) == []
|
||||
|
||||
|
||||
async def test_feststehendes_urteil_braucht_keine_vollen_stimmen(projekt, monkeypatch):
|
||||
"""Vier von sechs sagen „abgedeckt": die zwei fehlenden könnten die Schwelle
|
||||
nicht mehr reißen. Daraus einen Fehlschlag zu machen wäre Verschwendung."""
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
ausfaelle = {"n": 0}
|
||||
|
||||
async def zwei_platzen(system, nachricht, temperatur=0.0):
|
||||
ausfaelle["n"] += 1
|
||||
if ausfaelle["n"] <= 2:
|
||||
raise RuntimeError("429")
|
||||
return ABGEDECKT
|
||||
|
||||
monkeypatch.setattr(agents, "frage", zwei_platzen)
|
||||
stand = ledger.lade(projekt)
|
||||
gemeldet = []
|
||||
assert await inhalt.code_im_feature(features.liste(projekt), [punkt()], stand,
|
||||
melde=lambda t, n=0: gemeldet.append(n)) == []
|
||||
assert gemeldet == [] # kein Fehler: das Ergebnis stand fest
|
||||
assert "bedienen:starte" in stand["abdeckung"]
|
||||
|
||||
|
||||
async def test_offenes_urteil_bei_ausfall_wird_gemeldet(projekt, monkeypatch):
|
||||
"""Vier Stimmen sagen „fehlt", zwei fehlen — mit ihnen wäre die Schwelle
|
||||
erreicht. Das ist offen und muss laut scheitern, nicht still durchgehen."""
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
ausfaelle = {"n": 0}
|
||||
|
||||
async def zwei_platzen(system, nachricht, temperatur=0.0):
|
||||
ausfaelle["n"] += 1
|
||||
if ausfaelle["n"] <= 2:
|
||||
raise RuntimeError("429")
|
||||
return FEHLT
|
||||
|
||||
monkeypatch.setattr(agents, "frage", zwei_platzen)
|
||||
stand = ledger.lade(projekt)
|
||||
gemeldet = []
|
||||
assert await inhalt.code_im_feature(features.liste(projekt), [punkt()], stand,
|
||||
melde=lambda t, n=0: gemeldet.append(n)) == []
|
||||
assert gemeldet == [1]
|
||||
assert stand["abdeckung"] == {} and stand["ohne_karte"] == {}
|
||||
|
||||
|
||||
async def test_zwei_ueberlebende_stimmen_urteilen_nicht(projekt, monkeypatch):
|
||||
"""Rechnerisch könnten zwei Stimmen die Schwelle unerreichbar machen. Als
|
||||
dauerhaft gemerktes Urteil ist das zu wenig."""
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
ausfaelle = {"n": 0}
|
||||
|
||||
async def vier_platzen(system, nachricht, temperatur=0.0):
|
||||
ausfaelle["n"] += 1
|
||||
if ausfaelle["n"] <= 4:
|
||||
raise RuntimeError("429")
|
||||
return ABGEDECKT
|
||||
|
||||
monkeypatch.setattr(agents, "frage", vier_platzen)
|
||||
stand = ledger.lade(projekt)
|
||||
gemeldet = []
|
||||
await inhalt.code_im_feature(features.liste(projekt), [punkt()], stand,
|
||||
melde=lambda t, n=0: gemeldet.append(n))
|
||||
assert gemeldet == [1]
|
||||
assert stand["abdeckung"] == {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Vision
|
||||
|
||||
VISION = "<!-- titel -->\nVision\n<!-- art -->\nvision\n<!-- ziel -->\nSo soll es werden\n"
|
||||
@@ -530,3 +101,65 @@ def test_keine_pruefung_fasst_die_vision_an(projekt):
|
||||
assert len(features.liste(projekt)) == 2
|
||||
titel = [f["sektionen"]["titel"] for f in features.pruefbar(projekt)]
|
||||
assert titel == ["Eins"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Prüfung 1
|
||||
|
||||
async def test_stilurteil_wird_gemerkt_und_nicht_neu_gefragt(projekt, monkeypatch):
|
||||
"""176 Anfragen je Lauf brachten den Anbieter zum Drosseln. Unveränderter
|
||||
Ausschnitt bei unverändertem Katalog heißt: das Urteil von vorhin gilt."""
|
||||
import pruefung
|
||||
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
modell = Zaehler('{"felder": {}}')
|
||||
monkeypatch.setattr(agents, "frage", modell)
|
||||
|
||||
liste, arbeite, abschluss = await pruefung._stil_aufgaben(projekt)
|
||||
for a in liste:
|
||||
assert await arbeite(a, None) == []
|
||||
abschluss()
|
||||
erste = modell.rufe
|
||||
assert erste >= len(pruefung.TEMPERATUREN)
|
||||
|
||||
liste, arbeite, abschluss = await pruefung._stil_aufgaben(projekt)
|
||||
for a in liste:
|
||||
assert await arbeite(a, None) == []
|
||||
assert modell.rufe == erste # keine einzige neue Anfrage
|
||||
|
||||
|
||||
async def test_geaenderter_text_wird_neu_gefragt(projekt, monkeypatch):
|
||||
import pruefung
|
||||
|
||||
karte(projekt, "Eins", ("Etwas starten", "Knopf"))
|
||||
modell = Zaehler('{"felder": {}}')
|
||||
monkeypatch.setattr(agents, "frage", modell)
|
||||
|
||||
liste, arbeite, abschluss = await pruefung._stil_aufgaben(projekt)
|
||||
for a in liste:
|
||||
await arbeite(a, None)
|
||||
abschluss()
|
||||
vorher = modell.rufe
|
||||
|
||||
datei = projekte.wurzel(projekt) / f"{features.liste(projekt)[0]['id']}.md"
|
||||
datei.write_text(datei.read_text().replace("- Knopf", "- Ganz anderer Text"))
|
||||
liste, arbeite, _ = await pruefung._stil_aufgaben(projekt)
|
||||
for a in liste:
|
||||
await arbeite(a, None)
|
||||
assert modell.rufe > vorher
|
||||
|
||||
|
||||
async def test_gemessener_verstoss_kommt_nie_aus_dem_gedaechtnis(projekt, monkeypatch):
|
||||
"""Sonst bliebe eine neu hinzugekommene Messregel an alten Karten wirkungslos."""
|
||||
import pruefung
|
||||
|
||||
karte(projekt, "Eins", ("Etwas starten", "Auch Passwort prüfen."))
|
||||
modell = Zaehler('{"felder": {"regeln": {"neu": "- Passwort prüfen",'
|
||||
' "grund": "Füllwort", "regel": "x"}}}')
|
||||
monkeypatch.setattr(agents, "frage", modell)
|
||||
|
||||
for lauf in range(2):
|
||||
liste, arbeite, abschluss = await pruefung._stil_aufgaben(projekt)
|
||||
gefunden = [p for a in liste for p in await arbeite(a, None)]
|
||||
abschluss()
|
||||
assert [p["vorschlag"] for p in gefunden] == ["Passwort prüfen"], lauf
|
||||
assert modell.rufe > len(pruefung.TEMPERATUREN) # zweimal wirklich gefragt
|
||||
|
||||
@@ -33,8 +33,10 @@ def test_alle_sektionen():
|
||||
"ziel": "",
|
||||
"regeln": "- Passwort nie loggen\n- Sperre nach 5 Fehlversuchen",
|
||||
"testen": "- Formular öffnen\n- Daten eingeben",
|
||||
"stand": "gebaut",
|
||||
},
|
||||
{"text": "Abmeldung", "ziel": "", "regeln": "", "testen": ""},
|
||||
{"text": "Abmeldung", "ziel": "", "regeln": "", "testen": "",
|
||||
"stand": "gebaut"},
|
||||
]
|
||||
|
||||
|
||||
@@ -114,3 +116,22 @@ def test_marker_mit_leerraum():
|
||||
def test_titel_erste_nicht_leere_zeile():
|
||||
s = zerlege("<!-- titel -->\n\nErste\nZweite\n")
|
||||
assert s["titel"] == "Erste"
|
||||
|
||||
|
||||
def test_stand_offen_wird_gelesen():
|
||||
"""Bestellt, aber noch nicht gebaut — sonst klagt die Vorwärtsprüfung sie an."""
|
||||
s = zerlege("<!-- titel -->\nX\n<!-- funktion -->\nA\n<!-- stand -->\noffen\n"
|
||||
"<!-- funktion -->\nB\n")
|
||||
assert [(f["text"], f["stand"]) for f in s["funktionen"]] == [("A", "offen"),
|
||||
("B", "gebaut")]
|
||||
assert s["sonstiges"] == "" # der Marker landet nicht im Rest
|
||||
|
||||
|
||||
def test_unbekannter_stand_gilt_als_gebaut():
|
||||
s = zerlege("<!-- titel -->\nX\n<!-- funktion -->\nA\n<!-- stand -->\nirgendwas\n")
|
||||
assert s["funktionen"][0]["stand"] == "gebaut"
|
||||
|
||||
|
||||
def test_stand_ohne_funktion_faellt_in_sonstiges():
|
||||
s = zerlege("<!-- titel -->\nX\n<!-- stand -->\noffen\n")
|
||||
assert s["sonstiges"] == "offen"
|
||||
|
||||
201
backend/tests/test_planung.py
Normal file
201
backend/tests/test_planung.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""Die Planung: eine Arbeitskopie, die den Projektstand nicht anfasst.
|
||||
|
||||
Geprüft wird die eine Zusage — solange die Planung läuft, bleibt die Übersicht,
|
||||
was sie war. Und dass sie einen Neustart übersteht, weil sie auf der Platte liegt.
|
||||
"""
|
||||
|
||||
import features
|
||||
import parser
|
||||
import planung
|
||||
import projekte
|
||||
import pytest
|
||||
|
||||
|
||||
def karte(projekt: str, titel: str, *funktionen: str) -> str:
|
||||
roh = f"<!-- titel -->\n{titel}\n" + "".join(
|
||||
f"{'-' * 40}\n<!-- funktion -->\n{f}\n<!-- regeln -->\n- Etwas gilt\n"
|
||||
for f in funktionen)
|
||||
return features.anlegen(projekt, roh)["id"]
|
||||
|
||||
|
||||
def titel(projekt: str) -> list:
|
||||
return sorted(f["sektionen"]["titel"] for f in features.liste(projekt))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def projekt(datenordner):
|
||||
projekte.anlegen("P")
|
||||
karte("P", "Galerie", "Bild wählen")
|
||||
karte("P", "Shop", "Karte kaufen")
|
||||
return "P"
|
||||
|
||||
|
||||
def test_ohne_planung_arbeitet_alles_im_projektordner(projekt):
|
||||
assert not planung.laeuft(projekt)
|
||||
assert projekte.wurzel(projekt) == projekte.stand(projekt)
|
||||
assert planung.aenderungen(projekt) == {}
|
||||
|
||||
|
||||
def test_planung_leitet_das_schreiben_um(projekt):
|
||||
planung.starte(projekt)
|
||||
assert projekte.wurzel(projekt) != projekte.stand(projekt)
|
||||
|
||||
karte(projekt, "Neu", "Etwas tun")
|
||||
# Die Karte steht in der Kopie …
|
||||
assert "Neu" in titel(projekt)
|
||||
# … und nicht im Projektstand
|
||||
assert not (projekte.stand(projekt) / "neu.md").exists()
|
||||
|
||||
|
||||
def test_zweite_planung_wird_abgelehnt(projekt):
|
||||
planung.starte(projekt)
|
||||
with pytest.raises(ValueError):
|
||||
planung.starte(projekt)
|
||||
|
||||
|
||||
def test_planung_ueberlebt_einen_neustart(projekt):
|
||||
planung.starte(projekt)
|
||||
karte(projekt, "Neu", "Etwas tun")
|
||||
# Nichts im Speicher — der nächste Prozess sieht nur die Platte
|
||||
assert planung.laeuft(projekt)
|
||||
assert "Neu" in planung.aenderungen(projekt)
|
||||
|
||||
|
||||
def test_abbruch_laesst_den_projektstand_unberuehrt(projekt):
|
||||
vorher = titel(projekt)
|
||||
planung.starte(projekt)
|
||||
karte(projekt, "Neu", "Etwas tun")
|
||||
planung.brich_ab(projekt)
|
||||
assert not planung.laeuft(projekt)
|
||||
assert titel(projekt) == vorher
|
||||
|
||||
|
||||
def test_umsetzen_macht_die_kopie_zum_stand(projekt):
|
||||
planung.starte(projekt)
|
||||
karte(projekt, "Neu", "Etwas tun")
|
||||
planung.setze_um(projekt)
|
||||
assert not planung.laeuft(projekt)
|
||||
assert "Neu" in titel(projekt)
|
||||
assert not (projekte.stand(projekt) / projekte.PLANUNG).exists()
|
||||
|
||||
|
||||
def test_umsetzen_ohne_planung_ist_ein_fehler(projekt):
|
||||
with pytest.raises(ValueError):
|
||||
planung.setze_um(projekt)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Änderungen
|
||||
|
||||
def test_neue_karte_ist_neu(projekt):
|
||||
planung.starte(projekt)
|
||||
karte(projekt, "Neu", "Etwas tun")
|
||||
a = planung.aenderungen(projekt)
|
||||
assert a["Neu"]["art"] == "neu"
|
||||
assert a["Neu"]["funktionen"] == {"Etwas tun": "neu"}
|
||||
assert "Galerie" not in a
|
||||
|
||||
|
||||
def test_entfernte_karte_ist_weg(projekt):
|
||||
planung.starte(projekt)
|
||||
features.loeschen(projekt, "shop")
|
||||
a = planung.aenderungen(projekt)
|
||||
assert a["Shop"]["art"] == "weg"
|
||||
assert a["Shop"]["funktionen"] == {"Karte kaufen": "weg"}
|
||||
|
||||
|
||||
def test_neue_funktion_wird_markiert(projekt):
|
||||
planung.starte(projekt)
|
||||
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == "Galerie")
|
||||
features.speichern(projekt, f["id"],
|
||||
f["roh"] + f"{'-' * 40}\n<!-- funktion -->\nBild löschen\n")
|
||||
a = planung.aenderungen(projekt)
|
||||
assert a["Galerie"]["art"] == "geaendert"
|
||||
assert a["Galerie"]["funktionen"] == {"Bild löschen": "neu"}
|
||||
|
||||
|
||||
def test_geaenderte_regel_wird_markiert(projekt):
|
||||
planung.starte(projekt)
|
||||
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == "Galerie")
|
||||
features.speichern(projekt, f["id"], f["roh"].replace("Etwas gilt", "Anderes gilt"))
|
||||
assert planung.aenderungen(projekt)["Galerie"]["funktionen"] == {
|
||||
"Bild wählen": "geaendert"}
|
||||
|
||||
|
||||
def test_umbenannte_karte_ist_neu_und_weg(projekt):
|
||||
"""Verglichen wird über den Titel. Eine Umbenennung ist zwei Änderungen."""
|
||||
planung.starte(projekt)
|
||||
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == "Shop")
|
||||
features.speichern(projekt, f["id"], f["roh"].replace("Shop", "Laden", 1))
|
||||
a = planung.aenderungen(projekt)
|
||||
assert a["Shop"]["art"] == "weg" and a["Laden"]["art"] == "neu"
|
||||
|
||||
|
||||
def test_gleicher_inhalt_ist_keine_aenderung(projekt):
|
||||
planung.starte(projekt)
|
||||
f = features.liste(projekt)[0]
|
||||
features.speichern(projekt, f["id"], f["roh"])
|
||||
assert planung.aenderungen(projekt) == {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Zurücksetzen
|
||||
|
||||
def test_zuruecksetzen_loescht_eine_neue_karte(projekt):
|
||||
planung.starte(projekt)
|
||||
karte(projekt, "Neu", "Etwas tun")
|
||||
planung.zuruecksetzen(projekt, "Neu")
|
||||
assert planung.aenderungen(projekt) == {}
|
||||
assert "Neu" not in titel(projekt)
|
||||
|
||||
|
||||
def test_zuruecksetzen_holt_eine_geloeschte_karte_zurueck(projekt):
|
||||
planung.starte(projekt)
|
||||
features.loeschen(projekt, "shop")
|
||||
planung.zuruecksetzen(projekt, "Shop")
|
||||
assert planung.aenderungen(projekt) == {}
|
||||
assert "Shop" in titel(projekt)
|
||||
|
||||
|
||||
def test_zuruecksetzen_stellt_den_alten_inhalt_her(projekt):
|
||||
planung.starte(projekt)
|
||||
f = next(x for x in features.liste(projekt) if x["sektionen"]["titel"] == "Galerie")
|
||||
features.speichern(projekt, f["id"], f["roh"].replace("Etwas gilt", "Anderes gilt"))
|
||||
planung.zuruecksetzen(projekt, "Galerie")
|
||||
assert planung.aenderungen(projekt) == {}
|
||||
|
||||
|
||||
def test_zuruecksetzen_laesst_die_uebrigen_stehen(projekt):
|
||||
planung.starte(projekt)
|
||||
karte(projekt, "Eins", "A tun")
|
||||
karte(projekt, "Zwei", "B tun")
|
||||
planung.zuruecksetzen(projekt, "Eins")
|
||||
assert set(planung.aenderungen(projekt)) == {"Zwei"}
|
||||
|
||||
|
||||
def test_zuruecksetzen_ohne_planung_ist_ein_fehler(projekt):
|
||||
with pytest.raises(ValueError):
|
||||
planung.zuruecksetzen(projekt, "Galerie")
|
||||
|
||||
|
||||
def test_unbekannte_karte_zuruecksetzen_meldet_es(projekt):
|
||||
planung.starte(projekt)
|
||||
with pytest.raises(FileNotFoundError):
|
||||
planung.zuruecksetzen(projekt, "Gibt es nicht")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Nachbarschaft
|
||||
|
||||
def test_die_kopie_ist_kein_eigenes_projekt(projekt):
|
||||
planung.starte(projekt)
|
||||
assert [p["name"] for p in projekte.liste()] == ["P"]
|
||||
|
||||
|
||||
def test_gespraech_und_gedaechtnis_wandern_mit(projekt):
|
||||
"""Was während des Planens beurteilt wurde, gilt für den geplanten Stand."""
|
||||
(projekte.stand(projekt) / ".pruefung.json").write_text('{"stil": {}}')
|
||||
planung.starte(projekt)
|
||||
assert (projekte.wurzel(projekt) / ".pruefung.json").is_file()
|
||||
|
||||
|
||||
def test_die_kopie_kopiert_sich_nicht_selbst(projekt):
|
||||
planung.starte(projekt)
|
||||
assert not (projekte.wurzel(projekt) / projekte.PLANUNG).exists()
|
||||
@@ -197,8 +197,9 @@ async def test_herkunft_kommt_vom_code_nicht_vom_modell(datenordner, monkeypatch
|
||||
|
||||
async def luegt(system, inhalt, temperatur=0.0):
|
||||
# Das Modell behauptet eine fremde Funktion — das muss ignoriert werden
|
||||
# „Anmelden" ist selbst ein gültiger Titel — sonst weist die Messung ihn ab
|
||||
return ('{"felder": {"titel": {"regel": "Sprache: Deutsch, Präsens, keine Anrede",'
|
||||
' "neu": "Anders", "grund": "x"},'
|
||||
' "neu": "Einloggen", "grund": "x"},'
|
||||
' "funktion": {"neu": "Erfunden", "grund": "y"}}}')
|
||||
|
||||
monkeypatch.setattr(agents, "frage", luegt)
|
||||
|
||||
@@ -166,3 +166,96 @@ def test_gemessene_befunde_haengen_am_ausschnitt(datenordner):
|
||||
text = pruefung.inhalt(karte, befunde)
|
||||
assert "Gemessen, bitte neu schreiben:" in text
|
||||
assert "Mehr als 3 Wörter" in text
|
||||
|
||||
|
||||
def test_funktionstitel_braucht_ein_verb():
|
||||
"""Ohne Verb steht dort ein Bereich oder ein Zustand — nichts Testbares."""
|
||||
for titel in ("Galerie im Modal", "Preis aktualisiert", "In den Warenkorb",
|
||||
"Modal tastaturgesteuert"):
|
||||
assert _probleme(_pruefe("funktion", "titel", titel)) \
|
||||
== ["Kein Verb im Infinitiv am Ende"], titel
|
||||
|
||||
|
||||
def test_gute_funktionstitel_bleiben_unberuehrt():
|
||||
for titel in ("Feature anlegen", "Anzeigemodus umschalten", "Option wählen",
|
||||
"Nächsten Rückschritt anzeigen", "Karten prüfen", "Stand umschalten",
|
||||
"Projekt aufrufen", "Bildergalerie bedienen", "PDF hochladen"):
|
||||
assert _pruefe("funktion", "titel", titel) == [], titel
|
||||
|
||||
|
||||
def test_kartentitel_darf_ohne_verb_stehen():
|
||||
"""Eine Karte ist ein Gegenstand, keine Handlung."""
|
||||
assert _pruefe("feature", "titel", "Karten prüfen") == []
|
||||
assert _pruefe("feature", "titel", "Darkmode") == []
|
||||
|
||||
|
||||
ABLAUF = "- Vollmodus öffnen\n- LMB auf Regeln\n- Text ändern\n- Regeln angepasst"
|
||||
|
||||
|
||||
def test_ein_ablauf_geht_durch():
|
||||
assert _pruefe("funktion", "testen", ABLAUF) == []
|
||||
|
||||
|
||||
def test_zweiter_ablauf_wird_gemeldet():
|
||||
"""So entstand „Bildergalerie bedienen": blättern, Miniatur, Großansicht in einem."""
|
||||
zwei = ("- Zurück-Button klicken\n- Vorheriges Bild erscheint\n"
|
||||
"- Miniaturbild klicken\n- Zugehöriges Bild erscheint")
|
||||
b = _pruefe("funktion", "testen", zwei)
|
||||
assert _probleme(b) == ["Zweiter Ablauf in einer Funktion"]
|
||||
assert b[0]["original"] == "Miniaturbild klicken"
|
||||
|
||||
|
||||
def test_nominalstil_ist_eine_handlung():
|
||||
"""„Bestätigen im Modal" hat das Verb vorn — sonst gälte es als Ergebnis."""
|
||||
text = ("- RMB auf Featurecard\n- Klick auf \"Feature entfernen\"\n"
|
||||
"- Bestätigen im Modal\n- Feature entfernt")
|
||||
assert _pruefe("funktion", "testen", text) == []
|
||||
|
||||
|
||||
def test_mehrzahl_auf_en_ist_kein_verb():
|
||||
"""Deutsche Mehrzahl endet auch auf -en — nur Anfang und Ende zählen."""
|
||||
text = "- LMB auf Icon\n- Fehlerhafte Karten rot markiert"
|
||||
assert _pruefe("funktion", "testen", text) == []
|
||||
|
||||
|
||||
def test_zeilenfehler_haben_vorrang():
|
||||
"""Erst die Zeilen in Ordnung bringen; zwei Vorschläge je Feld wären nicht
|
||||
unabhängig anwendbar."""
|
||||
text = "- Auch klicken.\n- Ergebnis da\n- Weiter klicken"
|
||||
assert _probleme(_pruefe("funktion", "testen", text)) \
|
||||
== ["Satzzeichen am Zeilenende, Füllwort „auch“"]
|
||||
|
||||
|
||||
def test_funktion_ohne_jedes_feld_ist_leer():
|
||||
"""Ein bloßer Titel beschreibt nichts — der Prüfer stülpt ihn über alles."""
|
||||
b = struktur.befunde("funktion", {"titel": "Modal steuern", "ziel": "",
|
||||
"regeln": "", "testen": ""})
|
||||
assert _probleme(b) == ["Funktion ohne Ziel, Regeln und Testen"]
|
||||
|
||||
|
||||
def test_ein_einziges_feld_genuegt():
|
||||
for feld in ("ziel", "regeln", "testen"):
|
||||
felder = {"titel": "Modal steuern", "ziel": "", "regeln": "", "testen": ""}
|
||||
felder[feld] = "Etwas tun" if feld == "ziel" else "- Etwas tun"
|
||||
assert struktur.befunde("funktion", felder) == [], feld
|
||||
|
||||
|
||||
def test_einzelnes_feld_loest_nichts_aus():
|
||||
"""Beim Prüfen eines Feldes sagt das Fehlen der anderen nichts."""
|
||||
assert struktur.befunde("funktion", {"titel": "Modal steuern"}) == []
|
||||
|
||||
|
||||
def test_kartenebene_darf_leer_sein():
|
||||
assert struktur.befunde("feature", {"titel": "Darkmode", "badge": "kern",
|
||||
"ziel": "", "regeln": ""}) == []
|
||||
|
||||
|
||||
def test_platzhalter_ist_kein_inhalt():
|
||||
"""`<testschritt>` erfüllt jede Formregel und sagt trotzdem nichts."""
|
||||
assert struktur._umschreiben("funktion", "testen", "<testschritt>")
|
||||
assert struktur._umschreiben("funktion", "regeln", "Noch offen ...")
|
||||
assert struktur._umschreiben("funktion", "regeln", "TODO klären")
|
||||
|
||||
|
||||
def test_kleiner_als_ist_kein_platzhalter():
|
||||
assert not struktur._umschreiben("funktion", "regeln", "Wert 3 < 5")
|
||||
|
||||
Reference in New Issue
Block a user