Erste Fassung: Claude-Werkzeuge aus dem Vault
.vault-tools (ohne trading-plattform-pruefung und watermarks-remover), .claude/agents, .claude/skills, .claude/workflows, .claude/settings.json. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
fa1c26b407
137 files changed
+43926
No files matched your search
@@ -0,0 +1,751 @@
|
||||
"""Fährt die Angriffsliste gegen waisen.py, je Fall auf einem Wegwerf-Vault.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-09-29
|
||||
Entwurf: ENTWURF-waisen.md im selben Ordner
|
||||
|
||||
Grundlinie zuerst: Liefert der unbeschädigte Wegwerf-Vault nicht genau die
|
||||
erwartete Waise, misst der Rest nichts, und der Treiber bricht ab.
|
||||
|
||||
Je Fall eine frische Kopie der Grundlinie, genau eine Änderung und waisen.py
|
||||
als eigener Prozess mit --liste oder --zahl. Drei Fehlerpfade laufen im
|
||||
Prozess, weil sie sich von außen nicht auslösen lassen. Bestanden heißt:
|
||||
erwarteter Exit-Code, genau die erwarteten Waisen, jede erwartete Textstelle.
|
||||
Bei Exit 2 darf keine Zahl in der Ausgabe stehen.
|
||||
|
||||
--rotlauf baut jeden Mutanten aus MUTANTEN in eine Kopie von waisen.py und
|
||||
fährt die genannten Fälle dagegen. Getötet heißt: jeder genannte Fall schlägt
|
||||
fehl. Ein Muster, das nicht genau einmal in waisen.py steht, ist ein Befund.
|
||||
|
||||
--echt fährt waisen.py über den Vault, in dem dieser Treiber liegt, und misst
|
||||
die Laufzeit.
|
||||
|
||||
Aufruf:
|
||||
python .vault-tools/linkcheck/faelle_waisen.py
|
||||
python .vault-tools/linkcheck/faelle_waisen.py --rotlauf
|
||||
python .vault-tools/linkcheck/faelle_waisen.py --echt
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional, Union
|
||||
|
||||
HIER = Path(__file__).resolve().parent
|
||||
WAISEN = HIER / "waisen.py"
|
||||
LINKCHECK = HIER / "linkcheck.py"
|
||||
ZEILE = "Waisen außerhalb der Ausnahmen: "
|
||||
ZIEL_S = 10.0
|
||||
APP = '{"userIgnoreFilters": ["_index/", "/(^|/)node_modules//"]}'
|
||||
KEINE = "# keine Muster\n"
|
||||
|
||||
|
||||
def schreibe(vault: Path, rel: str, inhalt) -> None:
|
||||
# Immer Bytes: write_text würde unter Windows jedes LF zu CRLF machen.
|
||||
pfad = vault / rel
|
||||
pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||
pfad.write_bytes(inhalt if isinstance(inhalt, bytes) else inhalt.encode("utf-8"))
|
||||
|
||||
|
||||
def neu(rel: str, inhalt):
|
||||
return lambda v: schreibe(v, rel, inhalt)
|
||||
|
||||
|
||||
def nichts(v: Path) -> None:
|
||||
return None
|
||||
|
||||
|
||||
def baue_grundlinie(v: Path) -> None:
|
||||
schreibe(v, ".obsidian/app.json", APP)
|
||||
schreibe(v, "A/start.md", "Siehe [[B/ziel]].\n")
|
||||
schreibe(v, "B/ziel.md", "# Ziel\n")
|
||||
schreibe(v, "C/allein.md", "# Allein\n")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Fall:
|
||||
name: str
|
||||
aendern: Callable
|
||||
# None heisst: Abbruch mit Exit 2 erwartet.
|
||||
waisen: Optional[list]
|
||||
texte: list = field(default_factory=list)
|
||||
# None heisst: die Ausnahmedatei fehlt.
|
||||
ausnahmen: Union[str, bytes, None] = KEINE
|
||||
zahl: bool = False
|
||||
ohne_linkcheck: bool = False
|
||||
ohne_pythonioencoding: bool = False
|
||||
vault_fehlt: bool = False
|
||||
# Textstellen, die in stderr stehen müssen; bei --zahl gehört stdout allein der Zahl.
|
||||
texte_stderr: list = field(default_factory=list)
|
||||
|
||||
|
||||
GRUNDLINIE = Fall("GRUNDLINIE", nichts, ["C/allein.md"])
|
||||
|
||||
FAELLE = [
|
||||
Fall("wikilink | voller Pfad", neu("A/neu.md", "[[C/allein]]\n"), []),
|
||||
Fall("wikilink | nur der Name", neu("A/neu.md", "[[allein]]\n"), []),
|
||||
Fall("wikilink | mit Endung .md", neu("A/neu.md", "[[C/allein.md]]\n"), []),
|
||||
Fall("wikilink | mit Alias", neu("A/neu.md", "[[C/allein|Text]]\n"), []),
|
||||
Fall("wikilink | mit Anker und Blockverweis",
|
||||
neu("A/neu.md", "[[C/allein#Teil]] und [[C/allein^abc]]\n"), []),
|
||||
Fall("wikilink | escaptes Pipe in einer Tabellenzelle",
|
||||
neu("A/neu.md", "| a | [[C/allein\\|x]] |\n"), []),
|
||||
Fall("wikilink | Einbettung", neu("A/neu.md", "![[C/allein]]\n"), []),
|
||||
Fall("wikilink | Gross- und Kleinschreibung", neu("A/neu.md", "[[c/ALLEIN]]\n"), []),
|
||||
Fall("wikilink | Pfadende statt vollem Pfad",
|
||||
lambda v: ((v / "C" / "allein.md").unlink(),
|
||||
schreibe(v, "Ober/Tief/allein.md", "# Allein\n"),
|
||||
schreibe(v, "A/neu.md", "[[Tief/allein]]\n")),
|
||||
[]),
|
||||
Fall("wikilink | relativ mit ../", neu("A/neu.md", "[[../C/allein]]\n"), []),
|
||||
Fall("verweis | Wikilink ins Leere", neu("A/neu.md", "[[gibtsnicht]]\n"),
|
||||
["A/neu.md", "C/allein.md"]),
|
||||
Fall("verweis | Wikilink auf einen Anhang",
|
||||
lambda v: (schreibe(v, "A/bild.png", b"\x89PNG"), schreibe(v, "A/neu.md", "![[bild.png]]\n")),
|
||||
["A/neu.md", "C/allein.md"]),
|
||||
Fall("verweis | Selbstreferenz ist keine Kante",
|
||||
neu("C/allein.md", "# Allein\n\n[[C/allein]]\n"), ["C/allein.md"]),
|
||||
Fall("verweis | Anker in derselben Notiz",
|
||||
neu("C/allein.md", "# Allein\n\n[[#Allein]]\n"), ["C/allein.md"]),
|
||||
Fall("frontmatter | Wikilink in Anfuehrungszeichen",
|
||||
neu("A/neu.md", "---\nverwandt: \"[[C/allein]]\"\n---\n\nText\n"), []),
|
||||
Fall("frontmatter | Wikilink in einer Blockliste",
|
||||
neu("A/neu.md", "---\nverwandt:\n - \"[[C/allein]]\"\n---\n"), []),
|
||||
Fall("frontmatter | ohne Schlusszeile zaehlt als Text",
|
||||
neu("A/neu.md", "---\nquelle: \"[x](../C/allein.md)\"\n"), []),
|
||||
Fall("frontmatter | Markdown-Link im Frontmatter zaehlt nicht",
|
||||
neu("A/neu.md", "---\nquelle: \"[x](../C/allein.md)\"\n---\n"),
|
||||
["A/neu.md", "C/allein.md"]),
|
||||
Fall("markdown | relativ mit ../", neu("A/neu.md", "[x](../C/allein.md)\n"), []),
|
||||
Fall("markdown | im selben Ordner", neu("C/neu.md", "[x](allein.md)\n"), []),
|
||||
Fall("markdown | Pfad ab der Wurzel ohne ./", neu("A/neu.md", "[x](C/allein.md)\n"), []),
|
||||
Fall("markdown | Pfad mit fuehrendem /", neu("A/neu.md", "[x](/C/allein.md)\n"), []),
|
||||
Fall("markdown | URL-kodiert mit Leerzeichen und Umlaut",
|
||||
lambda v: (schreibe(v, "C/mit leer ü.md", "# M\n"),
|
||||
schreibe(v, "A/neu.md", "[x](../C/mit%20leer%20%C3%BC.md)\n")),
|
||||
["C/allein.md"]),
|
||||
Fall("markdown | spitze Klammern mit Leerzeichen",
|
||||
lambda v: (schreibe(v, "C/mit leer.md", "# M\n"),
|
||||
schreibe(v, "A/neu.md", "[x](<../C/mit leer.md>)\n")),
|
||||
["C/allein.md"]),
|
||||
Fall("markdown | mit Titel", neu("A/neu.md", "[x](../C/allein.md \"Titel\")\n"), []),
|
||||
Fall("markdown | mit Anker", neu("A/neu.md", "[x](../C/allein.md#teil)\n"), []),
|
||||
Fall("markdown | ohne Endung", neu("A/neu.md", "[x](../C/allein)\n"), []),
|
||||
# Web-Adresse und Laufwerkspfad messen das Ergebnis, nicht den Schemafilter in
|
||||
# markdown_ziele(): Unter Windows trägt kein Dateiname einen Doppelpunkt, also findet
|
||||
# die Auflösung für ein Ziel mit Schema ohnehin keine Notiz. Deshalb kein Mutant dafür.
|
||||
Fall("markdown | Web-Adresse findet keine Notiz",
|
||||
neu("A/neu.md", "[x](https://example.invalid/C/allein.md)\n"), ["A/neu.md", "C/allein.md"]),
|
||||
Fall("markdown | Anhang zaehlt nicht",
|
||||
lambda v: (schreibe(v, "A/doku.pdf", b"%PDF"), schreibe(v, "A/neu.md", "[x](doku.pdf)\n")),
|
||||
["A/neu.md", "C/allein.md"]),
|
||||
Fall("markdown | Laufwerkspfad findet keine Notiz",
|
||||
neu("A/neu.md", "[x](D:/Vault/C/allein.md)\n"), ["A/neu.md", "C/allein.md"]),
|
||||
Fall("markdown | Anker allein zaehlt nicht",
|
||||
neu("C/allein.md", "# Allein\n\n[x](#allein)\n"), ["C/allein.md"]),
|
||||
Fall("code | Wikilink im Zaun", neu("A/neu.md", "```\n[[C/allein]]\n```\n"),
|
||||
["A/neu.md", "C/allein.md"]),
|
||||
Fall("code | Markdown-Link im Code-Span",
|
||||
neu("A/neu.md", "Der Link `[x](../C/allein.md)` nur erwaehnt.\n"), ["A/neu.md", "C/allein.md"]),
|
||||
Fall("code | Link direkt nach dem Zaun", neu("A/neu.md", "```\ncode\n```\n[[C/allein]]\n"), []),
|
||||
Fall("reihenfolge | Waise an erster und an letzter Stelle",
|
||||
lambda v: (schreibe(v, "0/erste.md", "# E\n"), schreibe(v, "zz/letzte.md", "# L\n")),
|
||||
["0/erste.md", "C/allein.md", "zz/letzte.md"]),
|
||||
Fall("reihenfolge | Link am Dateiende ohne Zeilenumbruch", neu("A/neu.md", "Text\n\n[[C/allein]]"), []),
|
||||
Fall("reihenfolge | Link vor einem Zaun ohne Ende",
|
||||
neu("A/neu.md", "[[C/allein]]\n```\n[[B/ziel]]\n"), []),
|
||||
Fall("ausblendung | Punktordner ist kein Knoten und keine Quelle",
|
||||
neu(".versteckt/x.md", "[[C/allein]]\n"), ["C/allein.md"]),
|
||||
Fall("ausblendung | Punktdatei ist kein Knoten", neu("A/.x.md", "[[C/allein]]\n"), ["C/allein.md"]),
|
||||
Fall("ausblendung | Praefixfilter an der Wurzel", neu("_index/x.md", "[[C/allein]]\n"), ["C/allein.md"]),
|
||||
Fall("ausblendung | Praefixfilter greift nicht in der Tiefe", neu("Tief/_index/x.md", "# X\n"),
|
||||
["C/allein.md", "Tief/_index/x.md"]),
|
||||
Fall("ausblendung | regulaerer Ausdruck in der Tiefe",
|
||||
neu("P/node_modules/pkg/README.md", "# R\n"), ["C/allein.md"]),
|
||||
Fall("ausblendung | Praefixfilter ohne Ruecksicht auf die Schreibung",
|
||||
lambda v: (schreibe(v, ".obsidian/app.json", '{"userIgnoreFilters": ["archiv/"]}'),
|
||||
schreibe(v, "Archiv/x.md", "# X\n")),
|
||||
["C/allein.md"]),
|
||||
Fall("ausblendung | Ausdruck ohne Ruecksicht auf die Schreibung",
|
||||
lambda v: (schreibe(v, ".obsidian/app.json", '{"userIgnoreFilters": ["/(^|/)TMP//"]}'),
|
||||
schreibe(v, "Q/tmp/y.md", "# Y\n")),
|
||||
["C/allein.md"]),
|
||||
Fall("ausblendung | Link auf eine ausgeblendete Notiz",
|
||||
lambda v: (schreibe(v, "_index/x.md", "# X\n"), schreibe(v, "C/allein.md", "[[_index/x]]\n")),
|
||||
["C/allein.md"]),
|
||||
Fall("ausblendung | andere Endung ist kein Knoten", neu("C/daten.txt", "[[B/ziel]]\n"), ["C/allein.md"]),
|
||||
Fall("ausblendung | Endung in Grossbuchstaben ist ein Knoten", neu("C/GROSS.MD", "# G\n"),
|
||||
["C/GROSS.MD", "C/allein.md"]),
|
||||
# Dateifilter: Der Ordner davor bleibt stehen, erst die Prüfung je Datei blendet aus.
|
||||
Fall("ausblendung | Dateifilter blendet eine Notiz aus",
|
||||
neu(".obsidian/app.json", '{"userIgnoreFilters": ["C/allein.md"]}'), []),
|
||||
Fall("ausblendung | per Dateifilter ausgeblendete Notiz ist keine Quelle",
|
||||
lambda v: (schreibe(v, ".obsidian/app.json", '{"userIgnoreFilters": ["D/quelle.md"]}'),
|
||||
schreibe(v, "D/quelle.md", "[[C/allein]]\n")),
|
||||
["C/allein.md"]),
|
||||
Fall("ausnahme | Muster trifft die Waise", nichts, [],
|
||||
["Ausgenommene Waisen: 1 über 1 Muster"], ausnahmen="C/**\n"),
|
||||
Fall("ausnahme | ausgenommene Notiz liefert Kanten", neu("D/quelle.md", "[[C/allein]]\n"), [],
|
||||
ausnahmen="D/**\n"),
|
||||
Fall("ausnahme | Muster ohne Treffer wird genannt", nichts, ["C/allein.md"],
|
||||
["Muster ohne Treffer: Gibtsnicht/**"], ausnahmen="Gibtsnicht/**\n"),
|
||||
Fall("ausnahme | Stern innerhalb eines Namens", nichts, [], ausnahmen="*/allein.md\n"),
|
||||
Fall("ausnahme | Stern ueberschreitet keinen Ordner", nichts, ["C/allein.md"],
|
||||
["Muster ohne Treffer: *.md"], ausnahmen="*.md\n"),
|
||||
Fall("ausnahme | Gross- und Kleinschreibung", nichts, [], ausnahmen="c/ALLEIN.md\n"),
|
||||
Fall("ausnahme | nur Kommentar und Leerzeile", nichts, ["C/allein.md"], ausnahmen="# Kommentar\n\n"),
|
||||
Fall("ausnahme | leere Datei", nichts, ["C/allein.md"], ausnahmen=b""),
|
||||
Fall("ausnahme | CRLF", nichts, [], ausnahmen=b"C/**\r\n"),
|
||||
Fall("ausnahme | BOM", nichts, [], ausnahmen=b"\xef\xbb\xbfC/**\n"),
|
||||
Fall("ausnahme | Muster in erster und letzter Zeile",
|
||||
neu("D/zweite.md", "# Z\n"), [], ausnahmen="C/**\n# Mitte\nD/**"),
|
||||
Fall("ausnahme | Backslash ist ungueltig", nichts, None, ["ungültiges Muster"],
|
||||
ausnahmen="C\\allein.md\n"),
|
||||
Fall("ausnahme | fuehrender Schraegstrich ist ungueltig", nichts, None, ["ungültiges Muster"],
|
||||
ausnahmen="/C/**\n"),
|
||||
Fall("ausnahme | Punktpunkt ist ungueltig", nichts, None, ["ungültiges Muster"],
|
||||
ausnahmen="C/../**\n"),
|
||||
Fall("ausnahme | Laufwerk ist ungueltig", nichts, None, ["ungültiges Muster"],
|
||||
ausnahmen="D:/C/**\n"),
|
||||
Fall("ausnahme | Punkt als Pfadteil ist ungueltig", nichts, None, ["ungültiges Muster"],
|
||||
ausnahmen="./C/**\n"),
|
||||
Fall("ausnahme | leerer Pfadteil ist ungueltig", nichts, None, ["ungültiges Muster"],
|
||||
ausnahmen="C//allein.md\n"),
|
||||
Fall("ausnahme | doppeltes Muster", nichts, None, ["steht doppelt"], ausnahmen="C/**\nc/**\n"),
|
||||
Fall("ausnahme | Datei fehlt", nichts, None, ["Ausnahmedatei fehlt"], ausnahmen=None),
|
||||
Fall("ausnahme | ungueltiges UTF-8", nichts, None, ["Ausnahmedatei nicht lesbar"],
|
||||
ausnahmen=b"C/\xff\n"),
|
||||
Fall("obsidian | Ordner .obsidian fehlt", lambda v: shutil.rmtree(v / ".obsidian"), None,
|
||||
["Ordner .obsidian fehlt"]),
|
||||
Fall("obsidian | app.json fehlt, kein Filter",
|
||||
lambda v: ((v / ".obsidian" / "app.json").unlink(), schreibe(v, "_index/x.md", "# X\n")),
|
||||
["C/allein.md", "_index/x.md"]),
|
||||
Fall("obsidian | userIgnoreFilters fehlt, kein Filter",
|
||||
lambda v: (schreibe(v, ".obsidian/app.json", '{"newLinkFormat": "absolute"}'),
|
||||
schreibe(v, "_index/x.md", "# X\n")),
|
||||
["C/allein.md", "_index/x.md"]),
|
||||
Fall("obsidian | leere Filterliste",
|
||||
lambda v: (schreibe(v, ".obsidian/app.json", '{"userIgnoreFilters": []}'),
|
||||
schreibe(v, "_index/x.md", "# X\n")),
|
||||
["C/allein.md", "_index/x.md"]),
|
||||
Fall("obsidian | app.json ist kein JSON", neu(".obsidian/app.json", "{kaputt"), None,
|
||||
[".obsidian/app.json nicht lesbar"]),
|
||||
Fall("obsidian | app.json ist eine Liste", neu(".obsidian/app.json", "[]"), None,
|
||||
["kein JSON-Objekt"]),
|
||||
Fall("obsidian | userIgnoreFilters ist null", neu(".obsidian/app.json", '{"userIgnoreFilters": null}'),
|
||||
None, ["keine Liste"]),
|
||||
Fall("obsidian | Eintrag ist eine Zahl", neu(".obsidian/app.json", '{"userIgnoreFilters": [5]}'),
|
||||
None, ["kein Text"]),
|
||||
Fall("obsidian | leere Eintraege werden uebersprungen",
|
||||
lambda v: (schreibe(v, ".obsidian/app.json", '{"userIgnoreFilters": ["", " "]}'),
|
||||
schreibe(v, "_index/x.md", "# X\n")),
|
||||
["C/allein.md", "_index/x.md"], ["Einträge in userIgnoreFilters: 0"]),
|
||||
Fall("obsidian | Eintrag mit Leerzeichen wird getrimmt",
|
||||
lambda v: (schreibe(v, ".obsidian/app.json", '{"userIgnoreFilters": [" _index/ "]}'),
|
||||
schreibe(v, "_index/x.md", "# X\n")),
|
||||
["C/allein.md"]),
|
||||
Fall("obsidian | zwei Schraegstriche sind kein Ausdruck",
|
||||
neu(".obsidian/app.json", '{"userIgnoreFilters": ["//"]}'),
|
||||
["C/allein.md"], ["Einträge in userIgnoreFilters: 1"]),
|
||||
Fall("obsidian | ungueltiger Ausdruck wird uebersprungen und genannt",
|
||||
lambda v: (schreibe(v, ".obsidian/app.json", '{"userIgnoreFilters": ["/(/", "_index/"]}'),
|
||||
schreibe(v, "_index/x.md", "# X\n")),
|
||||
["C/allein.md"],
|
||||
["Filter übersprungen, ungültiger regulärer Ausdruck: '/(/'", "Einträge in userIgnoreFilters: 1"]),
|
||||
Fall("obsidian | Filter an erster und letzter Stelle",
|
||||
lambda v: (schreibe(v, ".obsidian/app.json", '{"userIgnoreFilters": ["_index/", "x", "/(^|/)tmp//"]}'),
|
||||
schreibe(v, "_index/x.md", "# X\n"), schreibe(v, "Q/tmp/y.md", "# Y\n")),
|
||||
["C/allein.md"]),
|
||||
Fall("umgebung | linkcheck.py fehlt", nichts, None, ["linkcheck.py nicht importierbar"],
|
||||
ohne_linkcheck=True),
|
||||
Fall("umgebung | Vault ohne Notiz", lambda v: [shutil.rmtree(v / n) for n in ("A", "B", "C")],
|
||||
None, ["keine Notiz"]),
|
||||
Fall("umgebung | Vault-Ordner fehlt", nichts, None, ["Vault-Ordner fehlt"], vault_fehlt=True),
|
||||
Fall("leer | leere Datei ist eine Waise", neu("C/leer.md", b""), ["C/allein.md", "C/leer.md"]),
|
||||
Fall("kodierung | ungueltiges UTF-8 vor dem Link", neu("A/neu.md", b"\xff\xfe Text [[C/allein]]\n"), []),
|
||||
Fall("kodierung | CRLF im Frontmatter",
|
||||
neu("A/neu.md", b"---\r\nverwandt: \"[[C/allein]]\"\r\n---\r\nText\r\n"), []),
|
||||
Fall("kodierung | BOM vor dem Frontmatter",
|
||||
neu("A/neu.md", b"\xef\xbb\xbf---\nverwandt: \"[[C/allein]]\"\n---\n"), []),
|
||||
Fall("umgebung | ohne PYTHONIOENCODING mit Umlaut und Emoji im Namen",
|
||||
neu("C/Übersicht 🚀.md", "# U\n"), ["C/allein.md", "C/Übersicht 🚀.md"],
|
||||
ohne_pythonioencoding=True),
|
||||
# Mehrdeutig: Die richtige Wahl liegt jeweils alphabetisch hinten, sonst misst der Fall nichts.
|
||||
Fall("mehrdeutig | gleichnamige Notiz im selben Ordner gewinnt",
|
||||
lambda v: (schreibe(v, "Q/X/doppelt.md", "# D\n"), schreibe(v, "A/X/doppelt.md", "# D\n"),
|
||||
schreibe(v, "Q/X/verweis.md", "[[X/doppelt]]\n")),
|
||||
["A/X/doppelt.md", "C/allein.md"]),
|
||||
Fall("mehrdeutig | relativ zur Quelle vor dem Namen",
|
||||
lambda v: (schreibe(v, "Z/sub/n.md", "# N\n"), schreibe(v, "A/sub/n.md", "# N\n"),
|
||||
schreibe(v, "Z/verweis.md", "[[sub/n]]\n")),
|
||||
["A/sub/n.md", "C/allein.md"]),
|
||||
Fall("mehrdeutig | sonst der kuerzeste Pfad",
|
||||
lambda v: (schreibe(v, "A/B/doppelt.md", "# D\n"), schreibe(v, "Z/doppelt.md", "# D\n"),
|
||||
schreibe(v, "Q/verweis.md", "[[doppelt]]\n")),
|
||||
["A/B/doppelt.md", "C/allein.md"]),
|
||||
# Regeln aus getLinkpathDest (Obsidian 1.13.7). Wo es eine Wahl gibt, liegt die richtige alphabetisch hinten.
|
||||
Fall("mehrdeutig | Teilbaum der Quelle vor kuerzerem Pfad",
|
||||
lambda v: (schreibe(v, "A/doppelt.md", "# D\n"), schreibe(v, "Z/tief/doppelt.md", "# D\n"),
|
||||
schreibe(v, "Z/verweis.md", "[[doppelt]]\n")),
|
||||
["A/doppelt.md", "C/allein.md"]),
|
||||
Fall("mehrdeutig | Laenge vor Tiefe",
|
||||
lambda v: (schreibe(v, "Aaaaaaaaaaaa/d.md", "# D\n"), schreibe(v, "Z/B/d.md", "# D\n"),
|
||||
schreibe(v, "R/v.md", "[[d]]\n")),
|
||||
["Aaaaaaaaaaaa/d.md", "C/allein.md"]),
|
||||
Fall("wikilink | Pfadende ohne Ordnergrenze",
|
||||
lambda v: (schreibe(v, "Obertief/allein.md", "# O\n"), schreibe(v, "R/w.md", "[[tief/allein]]\n")),
|
||||
["C/allein.md"]),
|
||||
Fall("markdown | ./ vor gleichnamiger Notiz an der Wurzel",
|
||||
lambda v: (schreibe(v, "n.md", "# N\n"), schreibe(v, "Z/Q/n.md", "# N\n"),
|
||||
schreibe(v, "Z/Q/neu.md", "[x](./n.md)\n")),
|
||||
["C/allein.md", "n.md"]),
|
||||
Fall("markdown | ./../ wie ../",
|
||||
lambda v: (schreibe(v, "n.md", "# N\n"), schreibe(v, "Z/n.md", "# N\n"),
|
||||
schreibe(v, "Z/Q/neu.md", "[x](./../n.md)\n")),
|
||||
["C/allein.md", "n.md"]),
|
||||
Fall("markdown | fuehrender / sucht nur ab der Wurzel", neu("A/neu.md", "[x](/allein.md)\n"),
|
||||
["A/neu.md", "C/allein.md"]),
|
||||
# Kein Mutant: Das folgt aus der Schleife über ../, ohne eigene Zeile im Code.
|
||||
Fall("markdown | ../ ueber die Wurzel hinaus bleibt an der Wurzel",
|
||||
neu("neu.md", "[x](../C/allein.md)\n"), []),
|
||||
Fall("markdown | Backslash bleibt stehen", neu("A/neu.md", "[x](..\\C\\allein.md)\n"),
|
||||
["A/neu.md", "C/allein.md"]),
|
||||
Fall("zahl | nur die Zahl", nichts, ["C/allein.md"], zahl=True),
|
||||
Fall("zahl | null Waisen", neu("A/neu.md", "[[C/allein]]\n"), [], zahl=True),
|
||||
Fall("zahl | Abbruch ohne Zahl", lambda v: shutil.rmtree(v / ".obsidian"), None,
|
||||
["Waisen - ABGEBROCHEN"], zahl=True),
|
||||
Fall("zahl | uebersprungener Filter bleibt aus der Zahl",
|
||||
neu(".obsidian/app.json", '{"userIgnoreFilters": ["/(/"]}'), ["C/allein.md"], zahl=True,
|
||||
texte_stderr=["Filter übersprungen, ungültiger regulärer Ausdruck: '/(/'"]),
|
||||
]
|
||||
|
||||
ALLE = [GRUNDLINIE] + FAELLE
|
||||
|
||||
|
||||
def frische_kopie(basis: Path, arbeit: Path) -> Path:
|
||||
v = arbeit / "fall"
|
||||
if v.exists():
|
||||
shutil.rmtree(v)
|
||||
shutil.copytree(basis, v)
|
||||
return v
|
||||
|
||||
|
||||
def streng_utf8(roh: bytes) -> str:
|
||||
# Ein ungültiges Byte darf nicht still ersetzt werden, sonst besteht der
|
||||
# Umgebungsfall auch dann, wenn waisen.py cp1252 schreibt.
|
||||
try:
|
||||
return roh.decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
return f"UNGUELTIGES-UTF-8: {exc}"
|
||||
|
||||
|
||||
def fahre(skript: Path, argumente: list, umgebung) -> tuple:
|
||||
start = time.perf_counter()
|
||||
lauf = subprocess.run(
|
||||
[sys.executable, str(skript), *argumente],
|
||||
capture_output=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
timeout=300,
|
||||
env=umgebung,
|
||||
)
|
||||
dauer = time.perf_counter() - start
|
||||
return lauf.returncode, streng_utf8(lauf.stdout), lauf.stderr.decode("utf-8", errors="replace"), dauer
|
||||
|
||||
|
||||
def pruefe_ausgabe(fall: Fall, code: int, out: str) -> list:
|
||||
probleme = []
|
||||
erwartet_exit = 2 if fall.waisen is None else (1 if fall.waisen else 0)
|
||||
if code != erwartet_exit:
|
||||
probleme.append(f"Exit {code} statt {erwartet_exit}")
|
||||
for text in fall.texte:
|
||||
if text not in out:
|
||||
probleme.append(f"fehlt in der Ausgabe: {text}")
|
||||
zeilen = out.replace("\r\n", "\n").split("\n")
|
||||
if fall.waisen is None:
|
||||
if any(z.startswith(ZEILE) for z in zeilen) or out.strip().isdigit():
|
||||
probleme.append("Abbruch, aber eine Zahl steht in der Ausgabe")
|
||||
return probleme
|
||||
if fall.zahl:
|
||||
if out.strip() != str(len(fall.waisen)):
|
||||
probleme.append(f"--zahl gab {out.strip()!r} statt {len(fall.waisen)}")
|
||||
return probleme
|
||||
zahl = [z for z in zeilen if z.startswith(ZEILE)]
|
||||
if zahl != [f"{ZEILE}{len(fall.waisen)}"]:
|
||||
probleme.append(f"Zahlzeile {zahl!r} statt {ZEILE}{len(fall.waisen)}")
|
||||
gelistet = sorted(z[4:] for z in zeilen if z.startswith(" - "))
|
||||
if gelistet != sorted(fall.waisen):
|
||||
probleme.append(f"Waisen {gelistet!r} statt {sorted(fall.waisen)!r}")
|
||||
return probleme
|
||||
|
||||
|
||||
def lauf_fall(fall: Fall, basis: Path, arbeit: Path, skript: Path) -> tuple:
|
||||
v = frische_kopie(basis, arbeit)
|
||||
fall.aendern(v)
|
||||
ausnahmen = arbeit / "ausnahmen.txt"
|
||||
if ausnahmen.exists():
|
||||
ausnahmen.unlink()
|
||||
if fall.ausnahmen is not None:
|
||||
roh = fall.ausnahmen if isinstance(fall.ausnahmen, bytes) else fall.ausnahmen.encode("utf-8")
|
||||
ausnahmen.write_bytes(roh)
|
||||
ziel_skript = skript
|
||||
if fall.ohne_linkcheck:
|
||||
allein = arbeit / "ohne-linkcheck"
|
||||
if allein.exists():
|
||||
shutil.rmtree(allein)
|
||||
allein.mkdir()
|
||||
ziel_skript = allein / "waisen.py"
|
||||
shutil.copy2(skript, ziel_skript)
|
||||
vault = arbeit / "gibtsnicht" if fall.vault_fehlt else v
|
||||
argumente = ["--vault", str(vault), "--ausnahmen", str(ausnahmen), "--zahl" if fall.zahl else "--liste"]
|
||||
umgebung = None
|
||||
if fall.ohne_pythonioencoding:
|
||||
umgebung = dict(os.environ)
|
||||
umgebung.pop("PYTHONIOENCODING", None)
|
||||
code, out, err, dauer = fahre(ziel_skript, argumente, umgebung)
|
||||
probleme = pruefe_ausgabe(fall, code, out)
|
||||
for text in fall.texte_stderr:
|
||||
if text not in err:
|
||||
probleme.append(f"fehlt in stderr: {text}")
|
||||
kopf = f"exit={code} {dauer:.2f}s"
|
||||
if not probleme:
|
||||
return True, kopf
|
||||
text = kopf + "\n" + "\n".join(" " + p for p in probleme)
|
||||
return False, text + f"\n stdout: {out[:600]!r}\n stderr: {err[:600]!r}"
|
||||
|
||||
|
||||
def fahre_liste(faelle: list, skript: Path, arbeit: Path, ausgabe: bool) -> dict:
|
||||
basis = arbeit / "grundlinie"
|
||||
if not basis.exists():
|
||||
baue_grundlinie(basis)
|
||||
ergebnis = {}
|
||||
for fall in faelle:
|
||||
try:
|
||||
ok, text = lauf_fall(fall, basis, arbeit, skript)
|
||||
except Exception as exc:
|
||||
ok, text = False, f"ABSTURZ im Treiber: {type(exc).__name__}: {exc}"
|
||||
ergebnis[fall.name] = ok
|
||||
if ausgabe:
|
||||
print(f"{'BESTANDEN' if ok else 'FEHLER':10} {fall.name} {text}")
|
||||
if fall is GRUNDLINIE and not ok:
|
||||
break
|
||||
return ergebnis
|
||||
|
||||
|
||||
def lade_modul(name: str, pfad: Path):
|
||||
spec = importlib.util.spec_from_file_location(name, pfad)
|
||||
modul = importlib.util.module_from_spec(spec)
|
||||
# Vor exec_module eintragen: dataclasses sucht das Modul in sys.modules.
|
||||
sys.modules[name] = modul
|
||||
spec.loader.exec_module(modul)
|
||||
return modul
|
||||
|
||||
|
||||
def im_prozess(modul, v: Path, ausnahmen: Path) -> tuple:
|
||||
puffer = io.StringIO()
|
||||
with contextlib.redirect_stdout(puffer):
|
||||
code = modul.main(["--vault", str(v), "--ausnahmen", str(ausnahmen), "--liste"])
|
||||
return code, puffer.getvalue()
|
||||
|
||||
|
||||
def ausnahmen_leer(arbeit: Path) -> Path:
|
||||
datei = arbeit / "ausnahmen.txt"
|
||||
datei.write_bytes(KEINE.encode("utf-8"))
|
||||
return datei
|
||||
|
||||
|
||||
def ip_datei_gesperrt(v: Path, arbeit: Path) -> tuple:
|
||||
modul = lade_modul("waisen_gesperrt", WAISEN)
|
||||
original = modul.lies
|
||||
|
||||
def gesperrt(pfad):
|
||||
if Path(pfad).name == "allein.md":
|
||||
raise PermissionError("Testsperre")
|
||||
return original(pfad)
|
||||
|
||||
modul.lies = gesperrt
|
||||
code, out = im_prozess(modul, v, ausnahmen_leer(arbeit))
|
||||
ok = code == 2 and "1 Notiz(en) nicht lesbar (erste: C/allein.md)" in out and ZEILE not in out
|
||||
return ok, f"exit={code} {out[:400]!r}"
|
||||
|
||||
|
||||
class _WalkPatch:
|
||||
"""Ersetzt os.walk nur im Namensraum des geladenen Moduls (modul.os)."""
|
||||
|
||||
def __init__(self, echt_os, fehler_pfad: Path) -> None:
|
||||
self._echt = echt_os
|
||||
self._fehler_pfad = fehler_pfad
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._echt, name)
|
||||
|
||||
def walk(self, start, onerror=None, **kwargs):
|
||||
if onerror is not None:
|
||||
fehler = OSError(13, "Testsperre")
|
||||
fehler.filename = str(self._fehler_pfad)
|
||||
onerror(fehler)
|
||||
yield from self._echt.walk(start, onerror=onerror, **kwargs)
|
||||
|
||||
|
||||
def ip_ordner_gesperrt(v: Path, arbeit: Path) -> tuple:
|
||||
modul = lade_modul("waisen_ordner", WAISEN)
|
||||
modul.os = _WalkPatch(modul.os, v / "gesperrt")
|
||||
code, out = im_prozess(modul, v, ausnahmen_leer(arbeit))
|
||||
ok = code == 2 and "1 Ordner nicht lesbar" in out and ZEILE not in out
|
||||
return ok, f"exit={code} {out[:400]!r}"
|
||||
|
||||
|
||||
def ip_unerwartet(v: Path, arbeit: Path) -> tuple:
|
||||
modul = lade_modul("waisen_unerwartet", WAISEN)
|
||||
|
||||
def kaputt(notizen):
|
||||
raise ValueError("Testfehler")
|
||||
|
||||
modul.baue_index = kaputt
|
||||
code, out = im_prozess(modul, v, ausnahmen_leer(arbeit))
|
||||
ok = code == 2 and "ABGEBROCHEN: ValueError: Testfehler" in out and ZEILE not in out
|
||||
return ok, f"exit={code} {out[:400]!r}"
|
||||
|
||||
|
||||
IM_PROZESS = [
|
||||
("absturz | Notiz nicht lesbar", ip_datei_gesperrt),
|
||||
("absturz | Ordner nicht lesbar", ip_ordner_gesperrt),
|
||||
("absturz | unerwarteter Fehler", ip_unerwartet),
|
||||
]
|
||||
|
||||
# (Name, Zeile in waisen.py, Ersatzzeile, Fälle, die rot werden müssen)
|
||||
MUTANTEN = [
|
||||
("Code nicht ausgeblendet",
|
||||
" rumpf = strip_code(rumpf)",
|
||||
" rumpf = rumpf",
|
||||
["code | Wikilink im Zaun", "code | Markdown-Link im Code-Span"]),
|
||||
("Markdown-Links ignoriert",
|
||||
" ziele += markdown_ziele(rumpf)",
|
||||
" ziele += []",
|
||||
["markdown | relativ mit ../", "markdown | URL-kodiert mit Leerzeichen und Umlaut",
|
||||
"markdown | ohne Endung"]),
|
||||
("Frontmatter ignoriert",
|
||||
" ziele = wikilink_ziele(kopf)",
|
||||
" ziele = []",
|
||||
["frontmatter | Wikilink in Anfuehrungszeichen", "kodierung | CRLF im Frontmatter",
|
||||
"kodierung | BOM vor dem Frontmatter"]),
|
||||
("Ausnahmen nicht abgezogen",
|
||||
" waisen = [rel for rel in alle if not any(m.regex.match(rel) for m in muster)]",
|
||||
" waisen = list(alle)",
|
||||
["ausnahme | Muster trifft die Waise", "ausnahme | CRLF"]),
|
||||
("Selbstreferenz als Kante",
|
||||
" if ziel is None or ziel == rel:",
|
||||
" if ziel is None:",
|
||||
["verweis | Selbstreferenz ist keine Kante"]),
|
||||
("Punktordner mitgezaehlt",
|
||||
" if not name.startswith(\".\") and not ausgeblendet(praefix + name + \"/\", filter_liste)",
|
||||
" if not ausgeblendet(praefix + name + \"/\", filter_liste)",
|
||||
["ausblendung | Punktordner ist kein Knoten und keine Quelle"]),
|
||||
("fehlende Ausnahmedatei still",
|
||||
" raise Abbruch(f\"Ausnahmedatei fehlt: {datei}\")",
|
||||
" return []",
|
||||
["ausnahme | Datei fehlt"]),
|
||||
("keine URL-Dekodierung",
|
||||
" ziele.append(unquote(ziel))",
|
||||
" ziele.append(ziel)",
|
||||
["markdown | URL-kodiert mit Leerzeichen und Umlaut"]),
|
||||
("regulaerer Filter als Praefix",
|
||||
" liste.append(re.compile(text[1:-1], re.IGNORECASE))",
|
||||
" liste.append(re.compile(\"^\" + re.escape(text), re.IGNORECASE))",
|
||||
["ausblendung | regulaerer Ausdruck in der Tiefe"]),
|
||||
("Frontmatter ohne Schlusszeile als Kopf",
|
||||
" return \"\", text",
|
||||
" return text, \"\"",
|
||||
["frontmatter | ohne Schlusszeile zaehlt als Text"]),
|
||||
("Punkt als Pfadteil erlaubt",
|
||||
" or \"..\" in teile or \".\" in teile or \"\" in teile[:-1]):",
|
||||
" or \"..\" in teile or \"\" in teile[:-1]):",
|
||||
["ausnahme | Punkt als Pfadteil ist ungueltig"]),
|
||||
("leerer Pfadteil erlaubt",
|
||||
" or \"..\" in teile or \".\" in teile or \"\" in teile[:-1]):",
|
||||
" or \"..\" in teile or \".\" in teile):",
|
||||
["ausnahme | leerer Pfadteil ist ungueltig"]),
|
||||
("Dateifilter ignoriert",
|
||||
" if not ausgeblendet(praefix + name, filter_liste):",
|
||||
" if True:",
|
||||
["ausblendung | Dateifilter blendet eine Notiz aus",
|
||||
"ausblendung | per Dateifilter ausgeblendete Notiz ist keine Quelle"]),
|
||||
("Praefixfilter mit Schreibung",
|
||||
" liste.append(re.compile(\"^\" + re.escape(text), re.IGNORECASE))",
|
||||
" liste.append(re.compile(\"^\" + re.escape(text)))",
|
||||
["ausblendung | Praefixfilter ohne Ruecksicht auf die Schreibung"]),
|
||||
("Ausdruck mit Schreibung",
|
||||
" liste.append(re.compile(text[1:-1], re.IGNORECASE))",
|
||||
" liste.append(re.compile(text[1:-1]))",
|
||||
["ausblendung | Ausdruck ohne Ruecksicht auf die Schreibung"]),
|
||||
("Filtereintrag nicht getrimmt",
|
||||
" text = eintrag.strip()",
|
||||
" text = eintrag",
|
||||
["obsidian | Eintrag mit Leerzeichen wird getrimmt"]),
|
||||
("leerer Filtereintrag nicht uebersprungen",
|
||||
" if not text:",
|
||||
" if False:",
|
||||
["obsidian | leere Eintraege werden uebersprungen"]),
|
||||
("zwei Schraegstriche als Ausdruck",
|
||||
" if len(text) > 2 and text.startswith(\"/\") and text.endswith(\"/\"):",
|
||||
" if len(text) >= 2 and text.startswith(\"/\") and text.endswith(\"/\"):",
|
||||
["obsidian | zwei Schraegstriche sind kein Ausdruck"]),
|
||||
("ungueltiger Ausdruck bricht ab",
|
||||
" uebersprungen.append(f\"{eintrag!r} ({fehler})\")",
|
||||
" raise",
|
||||
["obsidian | ungueltiger Ausdruck wird uebersprungen und genannt",
|
||||
"zahl | uebersprungener Filter bleibt aus der Zahl"]),
|
||||
("uebersprungener Filter nicht genannt",
|
||||
" print(f\"Filter übersprungen, ungültiger regulärer Ausdruck: {text}\")",
|
||||
" pass",
|
||||
["obsidian | ungueltiger Ausdruck wird uebersprungen und genannt"]),
|
||||
("Meldung bei --zahl auf stdout",
|
||||
" print(f\"Filter übersprungen, ungültiger regulärer Ausdruck: {text}\", file=sys.stderr)",
|
||||
" print(f\"Filter übersprungen, ungültiger regulärer Ausdruck: {text}\")",
|
||||
["zahl | uebersprungener Filter bleibt aus der Zahl"]),
|
||||
("Meldung bei --zahl fehlt",
|
||||
" print(f\"Filter übersprungen, ungültiger regulärer Ausdruck: {text}\", file=sys.stderr)",
|
||||
" pass",
|
||||
["zahl | uebersprungener Filter bleibt aus der Zahl"]),
|
||||
("escaptes Pipe nicht entfernt",
|
||||
' ziel = treffer.group(1).strip().rstrip("\\\\").strip()',
|
||||
" ziel = treffer.group(1).strip()",
|
||||
["wikilink | escaptes Pipe in einer Tabellenzelle"]),
|
||||
("Backslash im Ziel umgewandelt",
|
||||
" ziel = ziel.strip()",
|
||||
' ziel = ziel.strip().replace("\\\\", "/")',
|
||||
["markdown | Backslash bleibt stehen"]),
|
||||
("./ nicht gegen den Ordner der Quelle",
|
||||
" pfad = ordner + pfad[2:]",
|
||||
" pfad = pfad[2:]",
|
||||
["markdown | ./ vor gleichnamiger Notiz an der Wurzel"]),
|
||||
("./../ nicht verkuerzt",
|
||||
" pfad = pfad[2:]",
|
||||
" pass",
|
||||
["markdown | ./../ wie ../"]),
|
||||
("fuehrender / sucht auch ueber den Namen",
|
||||
" if ziel.startswith(\"/\"):",
|
||||
" if False:",
|
||||
["markdown | fuehrender / sucht nur ab der Wurzel"]),
|
||||
("Pfadende mit Ordnergrenze",
|
||||
" passend = [rel for rel in kandidaten if rel.lower().endswith(pfad)]",
|
||||
" passend = [rel for rel in kandidaten if (\"/\" + rel.lower()).endswith(\"/\" + pfad)]",
|
||||
["wikilink | Pfadende ohne Ordnergrenze"]),
|
||||
("Teilbaum der Quelle ohne Vorrang",
|
||||
" treffer = nah + fern",
|
||||
" treffer = sorted(passend, key=len)",
|
||||
["mehrdeutig | gleichnamige Notiz im selben Ordner gewinnt",
|
||||
"mehrdeutig | relativ zur Quelle vor dem Namen",
|
||||
"mehrdeutig | Teilbaum der Quelle vor kuerzerem Pfad"]),
|
||||
("nur gleicher Ordner statt Teilbaum",
|
||||
" nah = sorted((rel for rel in passend if rel.lower().startswith(ordner)), key=len)",
|
||||
" nah = sorted((rel for rel in passend if _eltern(rel).lower() == ordner), key=len)",
|
||||
["mehrdeutig | Teilbaum der Quelle vor kuerzerem Pfad"]),
|
||||
("Tiefe vor Laenge",
|
||||
" fern = sorted((rel for rel in passend if not rel.lower().startswith(ordner)), key=len)",
|
||||
" fern = sorted((rel for rel in passend if not rel.lower().startswith(ordner)), key=lambda rel: (rel.count(\"/\"), len(rel)))",
|
||||
["mehrdeutig | Laenge vor Tiefe"]),
|
||||
("ohne Sortierung nach Laenge",
|
||||
" fern = sorted((rel for rel in passend if not rel.lower().startswith(ordner)), key=len)",
|
||||
" fern = [rel for rel in passend if not rel.lower().startswith(ordner)]",
|
||||
["mehrdeutig | Laenge vor Tiefe", "mehrdeutig | sonst der kuerzeste Pfad"]),
|
||||
]
|
||||
|
||||
|
||||
def rotlauf() -> int:
|
||||
quelle = WAISEN.read_text(encoding="utf-8")
|
||||
nach_name = {f.name: f for f in ALLE}
|
||||
alle_ok = True
|
||||
for name, aus, wird, rot_bei in MUTANTEN:
|
||||
anzahl = quelle.count(aus + "\n")
|
||||
unbekannt = [n for n in rot_bei if n not in nach_name]
|
||||
if anzahl != 1 or unbekannt:
|
||||
print(f"MUSTER {name}: steht {anzahl}-mal in waisen.py, unbekannte Faelle {unbekannt}")
|
||||
alle_ok = False
|
||||
continue
|
||||
arbeit = Path(tempfile.mkdtemp(prefix="waisen-rot-")).resolve()
|
||||
try:
|
||||
ordner = arbeit / "skript"
|
||||
ordner.mkdir()
|
||||
skript = ordner / "waisen.py"
|
||||
with open(skript, "w", encoding="utf-8", newline="\n") as datei:
|
||||
datei.write(quelle.replace(aus + "\n", wird + "\n"))
|
||||
shutil.copy2(LINKCHECK, ordner / "linkcheck.py")
|
||||
ergebnis = fahre_liste([nach_name[n] for n in rot_bei], skript, arbeit, ausgabe=False)
|
||||
finally:
|
||||
shutil.rmtree(arbeit, ignore_errors=True)
|
||||
gruen = [n for n in rot_bei if ergebnis.get(n)]
|
||||
if gruen:
|
||||
alle_ok = False
|
||||
print(f"UEBERLEBT {name}: gruen blieben {gruen}")
|
||||
else:
|
||||
print(f"GETOETET {name}")
|
||||
if alle_ok:
|
||||
print("Rotlauf: alle Mutanten getoetet.")
|
||||
return 0
|
||||
print("Rotlauf: mindestens ein Mutant nicht getoetet oder ein Muster falsch.")
|
||||
return 1
|
||||
|
||||
|
||||
def echter_lauf() -> int:
|
||||
vault = HIER.parent.parent
|
||||
code, out, err, dauer = fahre(WAISEN, ["--vault", str(vault)], None)
|
||||
print(out.rstrip())
|
||||
if err.strip():
|
||||
print("--- stderr ---")
|
||||
print(err.rstrip())
|
||||
print(f"exit={code} Laufzeit {dauer:.2f} s (Ziel: unter {ZIEL_S:.0f} s)")
|
||||
if code not in (0, 1):
|
||||
return 1
|
||||
if dauer >= ZIEL_S:
|
||||
print("LAUFZEIT UEBER ZIEL")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
for pfad in (WAISEN, LINKCHECK):
|
||||
if not pfad.is_file():
|
||||
print(f"ABBRUCH: {pfad} fehlt")
|
||||
return 2
|
||||
namen = [f.name for f in ALLE] + [n for n, _ in IM_PROZESS]
|
||||
if len(set(namen)) != len(namen):
|
||||
print("ABBRUCH: ein Fallname steht doppelt")
|
||||
return 2
|
||||
if "--echt" in sys.argv[1:]:
|
||||
return echter_lauf()
|
||||
if "--rotlauf" in sys.argv[1:]:
|
||||
return rotlauf()
|
||||
arbeit = Path(tempfile.mkdtemp(prefix="waisen-faelle-")).resolve()
|
||||
try:
|
||||
ergebnis = fahre_liste(ALLE, WAISEN, arbeit, ausgabe=True)
|
||||
if not ergebnis.get(GRUNDLINIE.name):
|
||||
print("ABBRUCH: Die Grundlinie ist nicht gruen. Der Rest misst nichts.")
|
||||
return 2
|
||||
for name, funktion in IM_PROZESS:
|
||||
try:
|
||||
ok, text = funktion(frische_kopie(arbeit / "grundlinie", arbeit), arbeit)
|
||||
except Exception as exc:
|
||||
ok, text = False, f"ABSTURZ im Treiber: {type(exc).__name__}: {exc}"
|
||||
ergebnis[name] = ok
|
||||
print(f"{'BESTANDEN' if ok else 'FEHLER':10} {name} (im Prozess) {text}")
|
||||
bestanden = sum(1 for ok in ergebnis.values() if ok)
|
||||
print(f"\n{bestanden} von {len(ergebnis)} Faellen bestanden.")
|
||||
return 0 if bestanden == len(ergebnis) else 1
|
||||
finally:
|
||||
shutil.rmtree(arbeit, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except AttributeError:
|
||||
pass
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user