diff options
Diffstat (limited to 'src/test/java')
5 files changed, 627 insertions, 85 deletions
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index 78a45e9..8cd867e 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -6,8 +6,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assumptions.assumeTrue; import eu.mulk.aendggner.aenderung.Aenderungsbefehl; +import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ersetzung; +import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Sammelbefehl; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturEinfuegung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.UnbekannterBefehl; +import eu.mulk.aendggner.aenderung.Aenderungsbefehl.VerweisenderBefehl; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortAnker; import eu.mulk.aendggner.aenderung.parse.AenderungsantragParser; import eu.mulk.aendggner.aenderung.parse.AenderungsgesetzParser; @@ -17,11 +20,14 @@ import eu.mulk.aendggner.aenderung.parse.SuperskriptModus; import eu.mulk.aendggner.aenderung.parse.TextBereiniger; import eu.mulk.aendggner.aenderung.parse.ZitatExtraktor; import eu.mulk.aendggner.anwendung.BefehlAnwender; +import eu.mulk.aendggner.anwendung.Grund; +import eu.mulk.aendggner.anwendung.Nachfassungsabgleich; import eu.mulk.aendggner.gesetz.Absatz; import eu.mulk.aendggner.gesetz.Gesetz; import eu.mulk.aendggner.gesetz.gii.GiiXmlLoader; import eu.mulk.aendggner.synopse.HtmlRenderer; import eu.mulk.aendggner.synopse.SynopseBuilder; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.LocalDate; @@ -259,16 +265,17 @@ class EndToEndTest { * auch eine Aufhebung als Räumende zählt, läuft sie vor der Umnummerierung, und § 108 Absatz 1 * trägt danach die lückenlose Folge der Nummern 1 bis 32. * - * <ol> - * <li>{@code 40. a) hh)} „Die bisherige Nummer 18 wird Nummer 29 und nach der Angabe ‚Absatz 1‘ - * werden die Wörter ‚oder Absatz 4‘ eingefügt.“ — Die Begleitklausel löst norm-weit auf - * (bewusst so, siehe {@code BefehlErkenner}) und trifft dort 21 Vorkommen. Gemeint ist die - * soeben umnummerierte Einheit. - * <li>{@code 43. b) aa)} „In der Überschrift werden die Wörter … gestrichen.“ — Ziel ist die - * Überschrift der <em>Nummer 1 der Anlage 8</em>. Das ist dieselbe offene Modellfrage wie - * bei Berlins Anlage: Die Nummern einer Anlage brauchen einen eigenen Rang zwischen Norm - * und Absatz. - * </ol> + * <p>Auch {@code 43. b) aa)} ist keiner mehr („In der Überschrift werden die Wörter … in den + * Fällen des § 69 und § 71 Absatz 1 … gestrichen.“). Ziel ist die Überschrift der <em>Nummer 1 + * der Anlage 8</em>, und es brauchte dafür keinen eigenen Rang zwischen Norm und Absatz: Im + * gii-XML ist die Überschrift einer Anlagen-Nummer die Kopfzeile ihres Aufzählungsblocks. Der + * Fehler saß darin, dass der Überschrift-Zweig die feinere Angabe schweigend verwarf und stets + * auf den Normtitel griff. + * + * <p>Ein Rest bleibt: {@code 40. a) hh)} „Die bisherige Nummer 18 wird Nummer 29 und nach der + * Angabe ‚Absatz 1‘ werden die Wörter ‚oder Absatz 4‘ eingefügt.“ — Die Begleitklausel löst + * norm-weit auf (bewusst so, siehe {@code BefehlErkenner}) und trifft dort 21 Vorkommen. Gemeint + * ist die soeben umnummerierte Einheit. */ @Test void gegGegenZeitrichtigenStamm() throws Exception { @@ -290,8 +297,8 @@ class EndToEndTest { .filter(a -> a.status() == BefehlAnwender.Status.MANUELL_PRUEFEN) .map(a -> a.befehl().provenienz().gliederungsPfad()) .toList(); - assertThat(manuellPfade).containsExactly("40. a) hh)", "43. b) aa)"); - assertThat(anwendung.anzahlAngewandt()).isEqualTo(117); + assertThat(manuellPfade).isEmpty(); + assertThat(anwendung.anzahlAngewandt()).isEqualTo(119); // Die Kaskade des § 108 Absatz 1 geht lückenlos auf: 32 Nummern, keine doppelt, keine fehlend. var nummern = @@ -309,6 +316,22 @@ class EndToEndTest { assertThat(nummern).hasSize(32).endsWith("32"); var neu = anwendung.neu(); + // Die Begleitklausel der Nummer 18/29 hat allein dort gegriffen — der Wortlaut gleicht der + // amtlichen Nachfassung (BJNR172810020.xml, § 108 Absatz 1 Nummer 29). + var paragraph108 = neu.norm("§ 108").orElseThrow().absaetze().get(0).text(); + assertThat(paragraph108) + .contains("29. entgegen § 96 Absatz 1 oder Absatz 4 eine Bestätigung nicht") + .contains("30. entgegen § 96 Absatz 5 Satz 2 eine Abrechnung nicht"); + + // Die Überschrift der Nummer 1 der Anlage 8 ist geändert — und nur sie: Der Titel der Anlage + // trägt seinen eigenen Wortlaut unversehrt, den Punkt 43. a) ihm gegeben hat. + var anlage8 = neu.norm("Anlage 8").orElseThrow(); + assertThat(anlage8.gesamtText()) + .contains("1. Wärmedämmung von Wärmeverteilungs- und Warmwasserleitungen sowie Armaturen") + .doesNotContain("sowie Armaturen in den Fällen des § 69 und § 71 Absatz 1"); + assertThat(anlage8.titel()) + .contains("Anforderungen an die Wärmedämmung von Rohrleitungen und Armaturen"); + // Der neue § 9a steht im Gesetz und in der Inhaltsübersicht. assertThat(neu.norm("§ 9a")).isPresent(); assertThat(neu.norm("Inhaltsübersicht").orElseThrow().gesamtText()) @@ -737,13 +760,13 @@ class EndToEndTest { assumeTrue(Files.exists(pfad), "Nachfassung " + nachfassung + " fehlt"); var soll = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(pfad); assertThat(soll.normen()).hasSameSizeAs(alt.normen()); - for (var normSoll : soll.normen()) { - var normIst = anwendung.neu().norm(normSoll.enbez()); - assertThat(normIst).as("Norm %s fehlt", normSoll.enbez()).isPresent(); - assertThat(normIst.orElseThrow().gesamtText().replaceAll("\\s+", " ").strip()) - .as("Norm %s", normSoll.enbez()) - .isEqualTo(normSoll.gesamtText().replaceAll("\\s+", " ").strip()); - } + + // Geprüft wird mit demselben Abgleich, den auch --nachfassung fährt: Werkzeug und Test messen + // an einem Maßstab, sonst geht der eine durch, wo der andere anschlüge. + var abgleich = Nachfassungsabgleich.vergleiche(soll, anwendung.neu()); + assertThat(abgleich.fehlende()).isEmpty(); + assertThat(abgleich.abweichungen()).as("Abweichungen gegen die amtliche Nachfassung").isEmpty(); + assertThat(abgleich.gehtAuf()).as(abgleich.kurzbericht()).isTrue(); } /** @@ -1055,17 +1078,10 @@ class EndToEndTest { var sollAsog = SAMPLEDATA.resolve("Berlin/ASOG-Bln-neu.txt"); assumeTrue(Files.exists(sollAsog), "ASOG-Nachfassung fehlt"); var soll = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(sollAsog); - var abweichend = new java.util.ArrayList<String>(); - for (var normSoll : soll.normen()) { - var normIst = anwendung.neu().norm(normSoll.enbez()).orElseThrow(); - if (!normIst - .gesamtText() - .replaceAll("\\s+", " ") - .strip() - .equals(normSoll.gesamtText().replaceAll("\\s+", " ").strip())) { - abweichend.add(normSoll.enbez()); - } - } + var abweichend = + Nachfassungsabgleich.vergleiche(soll, anwendung.neu()).abweichungen().stream() + .map(Nachfassungsabgleich.Abweichung::enbez) + .toList(); // Keine Abweichung mehr: Alle 171 Normen gleichen der amtlichen Nachfassung. // // § 67 trug die beiden zuvor benannten Abweichungen: Das Portal setzt in der neuen Fassung @@ -1148,17 +1164,10 @@ class EndToEndTest { var soll = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader() .load(SAMPLEDATA.resolve("BadenWuerttemberg/KomWO-BW-neu.txt")); - var abweichend = new java.util.ArrayList<String>(); - for (var normSoll : soll.normen()) { - var normIst = anwendung.neu().norm(normSoll.enbez()).orElseThrow(); - if (!normIst - .gesamtText() - .replaceAll("\\s+", " ") - .strip() - .equals(normSoll.gesamtText().replaceAll("\\s+", " ").strip())) { - abweichend.add(normSoll.enbez()); - } - } + var abweichend = + Nachfassungsabgleich.vergleiche(soll, anwendung.neu()).abweichungen().stream() + .map(Nachfassungsabgleich.Abweichung::enbez) + .toList(); // § 20: Der Befehl ersetzt das Wort „Name“ durch „der vollständige Familienname“; im Zieltext // steht davor bereits „der“, sodass es doppelt erscheint. Die amtliche Nachfassung räumt das // auf, der Befehlswortlaut tut es nicht — ÄndGgner wendet den Wortlaut an. @@ -1353,13 +1362,10 @@ class EndToEndTest { assumeTrue(Files.exists(soll), "Hessische Nachfassung fehlt"); var amtlich = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(soll); assertThat(amtlich.normen()).hasSize(52); - for (var normSoll : amtlich.normen()) { - var normIst = anwendung.neu().norm(normSoll.enbez()); - assertThat(normIst).as("Norm %s fehlt", normSoll.enbez()).isPresent(); - assertThat(normIst.orElseThrow().gesamtText().replaceAll("\\s+", " ").strip()) - .as("Norm %s", normSoll.enbez()) - .isEqualTo(normSoll.gesamtText().replaceAll("\\s+", " ").strip()); - } + var abgleich = Nachfassungsabgleich.vergleiche(amtlich, anwendung.neu()); + assertThat(abgleich.fehlende()).isEmpty(); + assertThat(abgleich.abweichungen()).isEmpty(); + assertThat(abgleich.gleich()).isEqualTo(52); } /** @@ -1527,8 +1533,8 @@ class EndToEndTest { Files.exists(alt) && Files.exists(entwurfPdf) && Files.exists(antragPdf), "BayJG-Beispieldaten fehlen"); - var ohne = Pipeline.erzeugeSynopse(alt, List.of(entwurfPdf), null, false); - var mit = Pipeline.erzeugeSynopse(alt, List.of(entwurfPdf, antragPdf), null, false); + var ohne = Pipeline.erzeugeSynopse(Pipeline.Auftrag.von(alt, List.of(entwurfPdf))); + var mit = Pipeline.erzeugeSynopse(Pipeline.Auftrag.von(alt, List.of(entwurfPdf, antragPdf))); assertThat(mit.anzahlAngewandt()).isEqualTo(ohne.anzahlAngewandt()).isEqualTo(154); assertThat(mit.anzahlManuell()).isEqualTo(ohne.anzahlManuell()).isEqualTo(0); @@ -1607,8 +1613,8 @@ class EndToEndTest { Files.exists(xml) && Files.exists(empfehlung) && Files.exists(entwurf), "GEG-Beispieldaten fehlen"); - var ausEmpfehlung = Pipeline.erzeugeSynopse(xml, List.of(empfehlung), null, false); - var ausEntwurf = Pipeline.erzeugeSynopse(xml, List.of(entwurf), null, false); + var ausEmpfehlung = Pipeline.erzeugeSynopse(Pipeline.Auftrag.von(xml, List.of(empfehlung))); + var ausEntwurf = Pipeline.erzeugeSynopse(Pipeline.Auftrag.von(xml, List.of(entwurf))); // 69 statt der früheren 68: Nennt der Rahmen dieselbe Gliederungseinheit wie der Befehl („… // Teil 2 wird wie folgt geändert: … die Angabe zur Überschrift von Teil 2 Abschnitt 4 …“), @@ -1635,7 +1641,7 @@ class EndToEndTest { var pdf = SAMPLEDATA.resolve("IfSG/1924334.pdf"); assumeTrue(Files.exists(xml) && Files.exists(pdf), "IfSG-Beispieldaten fehlen"); - var ergebnis = Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false); + var ergebnis = Pipeline.erzeugeSynopse(Pipeline.Auftrag.von(xml, List.of(pdf))); // 51 statt der früheren 47: Die Satzzählung folgt jetzt der amtlichen — eine eingerückte // Aufzählungsmarke beendet keinen Satz, und eine Ordnungszahl vor einem Gliederungswort („nach @@ -1655,7 +1661,7 @@ class EndToEndTest { var pdf = SAMPLEDATA.resolve("GEG/BT-Drs-21-7071_Beschlussempfehlung.pdf"); assumeTrue(Files.exists(xml) && Files.exists(pdf), "GEG-Beispieldaten fehlen"); - var ergebnis = Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false); + var ergebnis = Pipeline.erzeugeSynopse(Pipeline.Auftrag.von(xml, List.of(pdf))); assertThat(ergebnis.anzahlAngewandt()).isZero(); assertThat(ergebnis.html()).contains("keine Änderungsbefehle"); @@ -1682,7 +1688,7 @@ class EndToEndTest { assumeTrue(Files.exists(xml) && Files.exists(pdf), "UWG-Beispieldaten fehlen"); // Ohne Stichtag bleibt es beim vollen Bestand — aber nicht stillschweigend. - var ohne = Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false); + var ohne = Pipeline.erzeugeSynopse(Pipeline.Auftrag.von(xml, List.of(pdf))); assertThat(ohne.anzahlAngewandt()).isEqualTo(19); assertThat(ohne.html()) .contains("Das Änderungsgesetz tritt gestaffelt in Kraft") @@ -1690,7 +1696,9 @@ class EndToEndTest { .contains("<dt>Inkrafttreten</dt><dd>27. September 2026 (gestaffelt, siehe unten)</dd>"); // Am 19. Juni 2026 galt genau ein Befehl: der Buchstabe c der Nummer 2. - var frueh = Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false, LocalDate.of(2026, 6, 19)); + var frueh = + Pipeline.erzeugeSynopse( + Pipeline.Auftrag.von(xml, List.of(pdf)).mitStichtag(LocalDate.of(2026, 6, 19))); assertThat(frueh.anzahlAngewandt()).isEqualTo(1); assertThat(frueh.anzahlManuell()).isZero(); assertThat(frueh.html()) @@ -1698,12 +1706,16 @@ class EndToEndTest { .contains("Am Stichtag noch nicht in Kraft") .contains("Tritt erst am 27. September 2026 in Kraft"); // Der Tag davor ändert noch gar nichts. - var davor = Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false, LocalDate.of(2026, 6, 18)); + var davor = + Pipeline.erzeugeSynopse( + Pipeline.Auftrag.von(xml, List.of(pdf)).mitStichtag(LocalDate.of(2026, 6, 18))); assertThat(davor.anzahlAngewandt()).isZero(); // Am 27. September 2026 ist das Gesetz vollständig in Kraft; dann deckt sich die Fassung mit // der ungefilterten, und der Abschnitt „noch nicht in Kraft“ entfällt. - var spaet = Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false, LocalDate.of(2026, 9, 27)); + var spaet = + Pipeline.erzeugeSynopse( + Pipeline.Auftrag.von(xml, List.of(pdf)).mitStichtag(LocalDate.of(2026, 9, 27))); assertThat(spaet.anzahlAngewandt()).isEqualTo(19); assertThat(spaet.html()).doesNotContain("Am Stichtag noch nicht in Kraft"); } @@ -1711,8 +1723,8 @@ class EndToEndTest { /** * Dasselbe am großen Fall, und zugleich die Probe auf die Schritt-Ordnung: Im GEG bleibt am 1. * Januar 2024 allein der Befehl der Nummer 22 zurück. Er steht in keiner Umnummerierungs-Kaskade, - * sodass die übrigen 115 unverändert durchlaufen — die drei liegengebliebenen Befehle sind - * dieselben wie ohne Stichtag und werden von der Auswahl nicht vermehrt. + * sodass die übrigen 116 unverändert durchlaufen — die liegengebliebenen Befehle sind dieselben + * wie ohne Stichtag und werden von der Auswahl nicht vermehrt. */ @Test void gegStichtagLaesstDieUebrigenBefehleUnberuehrt() throws Exception { @@ -1720,21 +1732,23 @@ class EndToEndTest { var pdf = SAMPLEDATA.resolve("GEG/bgbl123s0280_regelungstext.pdf"); assumeTrue(Files.exists(xml) && Files.exists(pdf), "GEG-Beispieldaten fehlen"); - var vollstaendig = Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false); - assertThat(vollstaendig.anzahlAngewandt()).isEqualTo(117); - assertThat(vollstaendig.anzahlManuell()).isEqualTo(2); + var vollstaendig = Pipeline.erzeugeSynopse(Pipeline.Auftrag.von(xml, List.of(pdf))); + assertThat(vollstaendig.anzahlAngewandt()).isEqualTo(119); + assertThat(vollstaendig.anzahlManuell()).isZero(); var anfang2024 = - Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false, LocalDate.of(2024, 1, 1)); - assertThat(anfang2024.anzahlAngewandt()).isEqualTo(116); - assertThat(anfang2024.anzahlManuell()).isEqualTo(2); + Pipeline.erzeugeSynopse( + Pipeline.Auftrag.von(xml, List.of(pdf)).mitStichtag(LocalDate.of(2024, 1, 1))); + assertThat(anfang2024.anzahlAngewandt()).isEqualTo(118); + assertThat(anfang2024.anzahlManuell()).isZero(); assertThat(anfang2024.html()) .contains("Am Stichtag noch nicht in Kraft") .contains("Tritt erst am 1. Oktober 2024 in Kraft"); var oktober2024 = - Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false, LocalDate.of(2024, 10, 1)); - assertThat(oktober2024.anzahlAngewandt()).isEqualTo(117); + Pipeline.erzeugeSynopse( + Pipeline.Auftrag.von(xml, List.of(pdf)).mitStichtag(LocalDate.of(2024, 10, 1))); + assertThat(oktober2024.anzahlAngewandt()).isEqualTo(119); } /** @@ -1753,7 +1767,8 @@ class EndToEndTest { assumeTrue(Files.exists(xml) && Files.exists(pdf), "IfSG-Beispieldaten fehlen"); var novemberFassung = - Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false, LocalDate.of(2020, 11, 19)); + Pipeline.erzeugeSynopse( + Pipeline.Auftrag.von(xml, List.of(pdf)).mitStichtag(LocalDate.of(2020, 11, 19))); assertThat(novemberFassung.anzahlAngewandt()).isEqualTo(65); assertThat(novemberFassung.html()) .contains("Am Stichtag noch nicht in Kraft") @@ -1761,7 +1776,8 @@ class EndToEndTest { // Am 1. April 2021 kommen die zehn Befehle des Artikels 2 und der Doppelbuchstabe hinzu. var aprilFassung = - Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false, LocalDate.of(2021, 4, 1)); + Pipeline.erzeugeSynopse( + Pipeline.Auftrag.von(xml, List.of(pdf)).mitStichtag(LocalDate.of(2021, 4, 1))); assertThat(aprilFassung.anzahlAngewandt()).isEqualTo(75); assertThat(aprilFassung.html()).doesNotContain("Am Stichtag noch nicht in Kraft"); } @@ -1812,7 +1828,7 @@ class EndToEndTest { assertThat(grundregel.wortlaut()) .isEqualTo("Dieses Gesetz tritt am Tag nach der Verkündung in Kraft."); - var synopse = Pipeline.erzeugeSynopse(alt, List.of(pdf), null, false); + var synopse = Pipeline.erzeugeSynopse(Pipeline.Auftrag.von(alt, List.of(pdf))); assertThat(synopse.anzahlAngewandt()).isEqualTo(4); assertThat(synopse.html()).doesNotContain("tritt gestaffelt in Kraft"); } @@ -1866,19 +1882,98 @@ class EndToEndTest { .filter(b -> b.provenienz().originalText().contains("die Worte")) .toList(); assertThat(wortbefehle).hasSizeGreaterThanOrEqualTo(7); - assertThat(wortbefehle).filteredOn(b -> b instanceof UnbekannterBefehl).hasSize(1); - - // Drei benannte Idiom-Grenzen, alle im Wortlaut festgehalten: - // 5. a) aa) — Satzzeichen-Ersetzung im Verbund mit einer Halbsatz-Anfügung („wird der Punkt - // durch einen Strichpunkt ersetzt und folgender Halbsatz angefügt“). - // 7. a) aa) — „In der Einleitung“ als Ziel, also der Chapeau eines Satzes mit Nummern. - // 12. — die Verweisung auf einen anderen Punkt desselben Artikels („Die Inhaltsübersicht wird - // entsprechend der vorstehenden Nummer 8 Buchst. a geändert“). - var unerkannt = - parseErgebnis.befehle().stream() - .filter(b -> b instanceof UnbekannterBefehl) - .map(b -> b.provenienz().gliederungsPfad()) - .toList(); - assertThat(unerkannt).containsExactly("5. a) aa)", "7. a) aa)", "12."); + assertThat(wortbefehle).noneMatch(b -> b instanceof UnbekannterBefehl); + + // Der Strichpunkt-Verbund („wird der Punkt durch einen Strichpunkt ersetzt und folgender + // Halbsatz angefügt“) und der bezugspunktlose Chapeau („In der Einleitung“) werden gelesen; + // dass beide gleichwohl nicht greifen, liegt an der Quelle und nicht am Werkzeug: Der Stamm + // ist hier die Nachfassung, ihr Wortlaut trägt die Änderung bereits. + assertThat(befehlAn(parseErgebnis, "5. a) aa)")).isInstanceOf(Sammelbefehl.class); + assertThat(befehlAn(parseErgebnis, "7. a) aa)")).isInstanceOf(Ersetzung.class); + + // Kein Befehl des Heftes bleibt mehr ungelesen. + assertThat(parseErgebnis.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl); + + // Die Verweisung auf einen anderen Punkt desselben Artikels wird gelesen, aber bewusst nicht + // ausgeführt. Der Unterschied ist kein kosmetischer: Die Synopse sagt fortan, dass das + // Erzeugnis hier die Grenze zieht, und nicht, dass die Vorlage unverständlich sei. + var verweisung = befehlAn(parseErgebnis, "12."); + assertThat(verweisung).isInstanceOf(VerweisenderBefehl.class); + assertThat(((VerweisenderBefehl) verweisung).verweis()).isEqualTo("Nummer 8 Buchst. a"); + + var anwendung = BefehlAnwender.anwenden(gesetz, List.of(verweisung)); + assertThat(anwendung.protokoll().get(0).grund()).isEqualTo(Grund.NICHT_UNTERSTUETZT); + assertThat(anwendung.protokoll().get(0).begruendung()) + .contains("verweist auf „Nummer 8 Buchst. a“"); + } + + private static Aenderungsbefehl befehlAn( + AenderungsgesetzParser.ParseErgebnis ergebnis, String gliederungsPfad) { + return ergebnis.befehle().stream() + .filter(b -> gliederungsPfad.equals(b.provenienz().gliederungsPfad())) + .findFirst() + .orElseThrow(); + } + + /** + * Der Abgleich mit der amtlichen Nachfassung ist fortan eine Leistung des Erzeugnisses und nicht + * bloß eine des Testcodes: {@code --nachfassung} nimmt dieselben Eingaben an wie das Stammgesetz + * und stellt das Ergebnis normweise dagegen. Berlin, Artikel 1: 171 von 171. + */ + @Test + void nachfassungWirdNormweiseAbgeglichen() throws Exception { + var alt = SAMPLEDATA.resolve("Berlin/ASOG-Bln-alt.txt"); + var neu = SAMPLEDATA.resolve("Berlin/ASOG-Bln-neu.txt"); + var pdf = SAMPLEDATA.resolve("Berlin/GVBl-2026-17_ASOG-LAF-AendG.pdf"); + assumeTrue( + Files.exists(alt) && Files.exists(neu) && Files.exists(pdf), "ASOG-Beispieldaten fehlen"); + + var ergebnis = + Pipeline.erzeugeSynopse( + Pipeline.Auftrag.von(alt, List.of(pdf)) + .mitArtikel("1") + .mitNachfassung(Quelle.lies(neu))); + + var abgleich = ergebnis.abgleich(); + assertThat(abgleich).isNotNull(); + assertThat(abgleich.gehtAuf()).as(abgleich.kurzbericht()).isTrue(); + assertThat(abgleich.gleich()).isEqualTo(171); + assertThat(abgleich.geprueft()).isEqualTo(171); + // Und er steht in der Synopse, wo ihn auch findet, wer keine Befehlszeile liest. + assertThat(ergebnis.html()) + .contains("Abgleich mit der amtlichen Nachfassung") + .contains("171 von 171 Normen gleich"); + } + + /** + * Die Kette steht und fällt damit, dass die ausgegebene Fassung wieder eingelesen dasselbe Gesetz + * ergibt. Geprüft wird das hier am <em>Bundesrecht</em> — der Rundlauf-Test des Textausgebers + * deckt nur die Klartext-Stammfassungen ab, und gerade der Weg vom gii-XML in den kanonischen + * Klartext ist der, den die Kette braucht: Wer ein zweites Heft auf das Ergebnis des ersten + * anwenden will, hat kein XML mehr, sondern nur diesen Text. + */ + @Test + void dieAusgegebeneFassungLaesstSichWiederEinlesen() throws Exception { + var xml = SAMPLEDATA.resolve("UWG/BJNR141400004.xml"); + var pdf = SAMPLEDATA.resolve("UWG/bgbl126s0043_regelungstext.pdf"); + assumeTrue(Files.exists(xml) && Files.exists(pdf), "UWG-Beispieldaten fehlen"); + + var erst = Pipeline.erzeugeSynopse(Pipeline.Auftrag.von(xml, List.of(pdf))); + assertThat(erst.anzahlAngewandt()).isEqualTo(19); + + // Der zweite Lauf hält dasselbe Ergebnis gegen den Text, den der erste geschrieben hat. Geht + // der Abgleich auf, so trägt der Text die Fassung vollständig — und damit die Kette. + var zweit = + Pipeline.erzeugeSynopse( + Pipeline.Auftrag.von(xml, List.of(pdf)) + .mitNachfassung( + new Quelle("UWG-neu.txt", erst.neufassung().getBytes(StandardCharsets.UTF_8)))); + + var abgleich = zweit.abgleich(); + assertThat(abgleich).isNotNull(); + assertThat(abgleich.fehlende()).isEmpty(); + assertThat(abgleich.ueberzaehlige()).isEmpty(); + assertThat(abgleich.abweichungen()).isEmpty(); + assertThat(abgleich.gehtAuf()).as(abgleich.kurzbericht()).isTrue(); } } diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java index 7742a3a..327c8db 100644 --- a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java @@ -15,6 +15,7 @@ import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Streichung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturEinfuegung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturErsetzung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Umnummerierung; +import eu.mulk.aendggner.aenderung.Aenderungsbefehl.VerweisenderBefehl; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WoerterEinfuegung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortAnker; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortlautZuAbsatz; @@ -1473,4 +1474,74 @@ class BefehlErkennerTest { assertThat(BefehlErkenner.erkenne(text, Stelle.LEER, zitate, PROV)) .containsInstanceOf(Ersetzung.class); } + + /** + * Rheinland-Pfalz: „In Satz 2 wird der Punkt durch einen Strichpunkt ersetzt und folgender + * Halbsatz angefügt: „…““. Zwei Neuerungen in einem Satz — der Strichpunkt als deutsche Nebenform + * des Semikolons und der Halbsatz als Ebene der Anfügung. Die Verbundmechanik selbst bestand + * bereits. + */ + @Test + void erkenntStrichpunktErsetzungMitHalbsatzAnfuegung() { + var befehl = + erkenne( + "In Satz 2 wird der Punkt durch einen Strichpunkt ersetzt und folgender Halbsatz" + + " angefügt: „die Bearbeitungszeit der Abschlussarbeit zählt zu den" + + " Fachstudien.“", + new Stelle(List.of(new Stelle.Paragraph("8"), new Stelle.AbsatzNr("1")))); + + assertThat(befehl).containsInstanceOf(Sammelbefehl.class); + var teile = ((Sammelbefehl) befehl.orElseThrow()).teilbefehle(); + assertThat(teile).hasSize(2); + + assertThat(teile.get(0)).isInstanceOf(Ersetzung.class); + var ersetzung = (Ersetzung) teile.get(0); + assertThat(ersetzung.alt()).isEqualTo("."); + assertThat(ersetzung.neu()).isEqualTo(";"); + assertThat(ersetzung.stelle().anzeigeText()).isEqualTo("§ 8 Absatz 1 Satz 2"); + + assertThat(teile.get(1)).isInstanceOf(Anfuegung.class); + var anfuegung = (Anfuegung) teile.get(1); + assertThat(anfuegung.ebene()).isEqualTo(Ebene.HALBSATZ); + assertThat(anfuegung.text()).contains("die Bearbeitungszeit der Abschlussarbeit"); + } + + /** Der Strichpunkt gilt auch als Subjekt („Der Strichpunkt wird durch ein Komma ersetzt“). */ + @Test + void erkenntDenStrichpunktAlsSubjekt() { + var befehl = erkenne("Der Strichpunkt am Ende wird durch ein Komma ersetzt.", PARAGRAPH_5); + assertThat(befehl).containsInstanceOf(Ersetzung.class); + var ersetzung = (Ersetzung) befehl.orElseThrow(); + assertThat(ersetzung.alt()).isEqualTo(";"); + assertThat(ersetzung.neu()).isEqualTo(","); + } + + /** + * Die Verweisung auf einen anderen Punkt desselben Artikels wird gelesen, aber nicht ausgeführt. + * Erkannt zu werden ist gleichwohl mehr als gar nichts: Die Rüge sagt dann, dass das Erzeugnis + * die Grenze zieht, und nicht, dass die Vorlage unverständlich sei. + */ + @Test + void erkenntDieVerweisungAufEinenAnderenPunkt() { + var befehl = + erkenne( + "Die Inhaltsübersicht wird entsprechend der vorstehenden Nummer 8 Buchst. a geändert.", + Stelle.LEER); + + assertThat(befehl).containsInstanceOf(VerweisenderBefehl.class); + var verweisung = (VerweisenderBefehl) befehl.orElseThrow(); + assertThat(verweisung.verweis()).isEqualTo("Nummer 8 Buchst. a"); + assertThat(verweisung.stelle().betrifftInhaltsuebersicht()).isTrue(); + } + + /** Ein bloß adverbiales „entsprechend“ wird davon nicht mitgerissen. */ + @Test + void adverbialesEntsprechendIstKeineVerweisung() { + var befehl = + erkenne( + "In § 5 werden die Wörter „entsprechend der Anlage“ durch die Wörter „nach der" + + " Anlage“ ersetzt.", + Stelle.LEER); + assertThat(befehl).containsInstanceOf(Ersetzung.class); + } } diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java index ee7e5c7..13e4442 100644 --- a/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java @@ -138,4 +138,18 @@ class StellenParserTest { StellenParser.parse("Absatz 1 in der Angabe vor Nummer 1").orElseThrow().anzeigeText()) .isEqualTo("Absatz 1"); } + + /** + * „In der Einleitung“ (Rheinland-Pfalz) ist ein Chapeau-Lokator wie „im Satzteil vor Nummer 1“: + * Er nennt keinen Bezugspunkt und trägt deshalb keine eigene Komponente — die Operation läuft auf + * der Stelle, die der Rahmen bezeichnet. + */ + @Test + void einleitungIstEinChapeauLokator() { + assertThat(StellenParser.istNurChapeau("In der Einleitung")).isTrue(); + assertThat(StellenParser.istNurChapeau("Im Eingangssatz")).isTrue(); + assertThat(StellenParser.istNurChapeau("in dem Einleitungssatz")).isTrue(); + // Ein Wort, das bloß so anfängt, ist keiner. + assertThat(StellenParser.istNurChapeau("In der Einleitungsformel")).isFalse(); + } } diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java index 629ec49..39c71cc 100644 --- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java +++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java @@ -4,6 +4,7 @@ package eu.mulk.aendggner.anwendung; import static org.assertj.core.api.Assertions.assertThat; +import eu.mulk.aendggner.aenderung.Aenderungsbefehl; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Anfuegung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Aufhebung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.BereichsUmnummerierung; @@ -1666,4 +1667,296 @@ class BefehlAnwenderTest { assertThat(ergebnis.anzahlManuell()).isEqualTo(1); assertThat(ergebnis.protokoll().get(0).begruendung()).contains("nicht so viele Einheiten"); } + + /** + * „In der Überschrift der Nummer 1 …“ meint die Kopfzeile jenes Aufzählungsblocks, nicht den + * Titel der Norm. Zuvor verwarf der Überschrift-Zweig die Nummer schweigend und griff auf den + * Normtitel — stand der Zieltext zufällig dort, so änderte er die falsche Einheit. + */ + @Test + void ueberschriftEinerNummerIstDieKopfzeileIhresBlocks() { + var anlage = + new Norm( + "Anlage 8", + "Anforderungen an die Wärmedämmung in den Fällen des § 69", + null, + List.of( + new Absatz( + null, + "1. Wärmedämmung von Leitungen in den Fällen des § 69\n" + + " a) Leitungen sind in den Fällen des § 69 zu dämmen.\n" + + "2. Sonstiges\n" + + " a) Im Übrigen gilt nichts.")), + false); + var gesetz = new Gesetz("TestG", "Testgesetz", "TestG", List.of(anlage)); + + var befehl = + new Streichung( + stelle( + new Stelle.Gliederungseinheit("Anlage", "8"), + new Stelle.NummerNr("1"), + new Stelle.Ueberschrift()), + " in den Fällen des § 69", + PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz, List.of(befehl)); + assertThat(ergebnis.protokoll()).allMatch(a -> a.status() == Status.ANGEWANDT); + + var text = ergebnis.neu().norm("Anlage 8").orElseThrow(); + // Die Kopfzeile ist gestutzt … + assertThat(text.absaetze().get(0).text()).startsWith("1. Wärmedämmung von Leitungen\n"); + // … der Buchstabe a) darunter und der Titel der Anlage dagegen unberührt: Beide führen + // denselben Wortlaut, und eine norm-weite Streichung hätte sie mitgenommen. + assertThat(text.absaetze().get(0).text()) + .contains("a) Leitungen sind in den Fällen des § 69 zu dämmen."); + assertThat(text.titel()).isEqualTo("Anforderungen an die Wärmedämmung in den Fällen des § 69"); + } + + /** + * Ein Aufzählungsglied aus einer einzigen Zeile hat keine Überschrift, sondern nur Text. Dort + * wird nichts angewandt, sondern gerügt: Andernfalls träfe die Operation den ganzen Wortlaut und + * gäbe vor, eine Überschrift geändert zu haben. + */ + @Test + void einzeiligesAufzaehlungsgliedTraegtKeineUeberschrift() { + var anlage = + new Norm( + "Anlage 8", + "Anforderungen", + null, + List.of(new Absatz(null, "1. Kurz und bündig\n2. Ebenso kurz")), + false); + var gesetz = new Gesetz("TestG", "Testgesetz", "TestG", List.of(anlage)); + + var befehl = + new Streichung( + stelle( + new Stelle.Gliederungseinheit("Anlage", "8"), + new Stelle.NummerNr("1"), + new Stelle.Ueberschrift()), + " und bündig", + PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz, List.of(befehl)); + assertThat(ergebnis.protokoll()).hasSize(1); + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.MANUELL_PRUEFEN); + assertThat(ergebnis.protokoll().get(0).grund()).isEqualTo(Grund.STELLE_NICHT_AUFLOESBAR); + assertThat(ergebnis.protokoll().get(0).begruendung()) + .contains("trägt keine eigene Überschrift"); + assertThat(ergebnis.neu().norm("Anlage 8").orElseThrow().absaetze().get(0).text()) + .isEqualTo("1. Kurz und bündig\n2. Ebenso kurz"); + } + + /** + * Die Neufassung der Überschrift einer Aufzählungseinheit bleibt eine bewusst gezogene Grenze: + * Deren Kopfzeile trägt zugleich die Aufzählungsmarke, die eine Neufassung nicht mitliefert. + * Gerügt wird sie deshalb — nicht auf den Normtitel umgebogen. + */ + @Test + void neufassungDerUeberschriftEinerNummerWirdGeruegt() { + var anlage = + new Norm( + "Anlage 8", + "Anforderungen", + null, + List.of(new Absatz(null, "1. Wärmedämmung\n a) Näheres.")), + false); + + var gesetz = new Gesetz("TestG", "Testgesetz", "TestG", List.of(anlage)); + + var befehl = + new Neufassung( + stelle( + new Stelle.Gliederungseinheit("Anlage", "8"), + new Stelle.NummerNr("1"), + new Stelle.Ueberschrift()), + "Wärmedämmung und Kältedämmung", + PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz, List.of(befehl)); + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.MANUELL_PRUEFEN); + assertThat(ergebnis.protokoll().get(0).grund()).isEqualTo(Grund.NICHT_UNTERSTUETZT); + assertThat(ergebnis.neu().norm("Anlage 8").orElseThrow().titel()).isEqualTo("Anforderungen"); + } + + private static Gesetz bussgeldkatalog() { + return new Gesetz( + "TestG", + "Testgesetz", + "TestG", + List.of( + new Norm( + "§ 108", + "Bußgeldvorschriften", + null, + List.of( + new Absatz( + "1", + "17. entgegen § 90 Absatz 1 etwas tut,\n" + + "18. entgegen § 96 Absatz 1 etwas unterlässt,\n" + + "19. entgegen § 97 Absatz 1 etwas anderes tut,")), + false))); + } + + private static Sammelbefehl verbund(Aenderungsbefehl... teile) { + return new Sammelbefehl(List.of(teile)); + } + + /** + * „Die bisherige Nummer 18 wird Nummer 29 und nach der Angabe ‚Absatz 1‘ werden die Wörter ‚oder + * Absatz 4‘ eingefügt.“ — Der Anker steht in jeder Nummer; norm-weit ist er mehrdeutig. Erst dann + * entscheidet die soeben umnummerierte Einheit. + */ + @Test + void mehrdeutigeBegleitklauselFaelltAufDieUmnummerierteEinheitZurueck() { + var rahmen = stelle(new Stelle.Paragraph("108"), new Stelle.AbsatzNr("1")); + var befehl = + verbund( + new Umnummerierung( + rahmen.plus(stelle(new Stelle.NummerNr("18"))), + stelle(new Stelle.NummerNr("29")), + PROV), + new WoerterEinfuegung( + rahmen, new WortAnker.NachWoertern("Absatz 1"), "oder Absatz 4", PROV)); + + var ergebnis = BefehlAnwender.anwenden(bussgeldkatalog(), List.of(befehl)); + assertThat(ergebnis.protokoll()).allMatch(a -> a.status() == Status.ANGEWANDT); + + var text = ergebnis.neu().norm("§ 108").orElseThrow().absaetze().get(0).text(); + // Allein die umnummerierte Nummer hat die Wörter bekommen. + assertThat(text).contains("29. entgegen § 96 Absatz 1 oder Absatz 4 etwas unterlässt,"); + assertThat(text).contains("17. entgegen § 90 Absatz 1 etwas tut,"); + assertThat(text).contains("19. entgegen § 97 Absatz 1 etwas anderes tut,"); + } + + /** + * Der Rückfall greift nur bei Mehrdeutigkeit, nie bei fehlendem Zieltext: Wer seinen Anker + * nirgends findet, meint etwas anderes — ihn in die umnummerierte Einheit zu zwingen wäre eine + * Falschanwendung, keine Verfeinerung. + */ + @Test + void fehlenderZieltextFaelltNichtZurueck() { + var rahmen = stelle(new Stelle.Paragraph("108"), new Stelle.AbsatzNr("1")); + var befehl = + verbund( + new Umnummerierung( + rahmen.plus(stelle(new Stelle.NummerNr("18"))), + stelle(new Stelle.NummerNr("29")), + PROV), + new WoerterEinfuegung( + rahmen, new WortAnker.NachWoertern("Paragraph 1"), "oder Absatz 4", PROV)); + + var ergebnis = BefehlAnwender.anwenden(bussgeldkatalog(), List.of(befehl)); + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.MANUELL_PRUEFEN); + assertThat(ergebnis.neu().norm("§ 108").orElseThrow().absaetze().get(0).text()) + .doesNotContain("oder Absatz 4"); + } + + /** + * Bleibt der Anker auch in der umnummerierten Einheit mehrdeutig, so bleibt der Befehl liegen — + * und mit der Begründung des <em>ersten</em> Versuchs: Die Mehrdeutigkeit ist der wahre Befund. + */ + @Test + void mehrdeutigBleibtMehrdeutig() { + var gesetz = + new Gesetz( + "TestG", + "Testgesetz", + "TestG", + List.of( + new Norm( + "§ 108", + null, + null, + List.of( + new Absatz( + "1", + "17. entgegen § 90 Absatz 1 etwas tut,\n" + + "18. entgegen § 96 Absatz 1 und § 97 Absatz 1 etwas unterlässt,")), + false))); + var rahmen = stelle(new Stelle.Paragraph("108"), new Stelle.AbsatzNr("1")); + var befehl = + verbund( + new Umnummerierung( + rahmen.plus(stelle(new Stelle.NummerNr("18"))), + stelle(new Stelle.NummerNr("29")), + PROV), + new WoerterEinfuegung( + rahmen, new WortAnker.NachWoertern("Absatz 1"), "oder Absatz 4", PROV)); + + var ergebnis = BefehlAnwender.anwenden(gesetz, List.of(befehl)); + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.MANUELL_PRUEFEN); + assertThat(ergebnis.protokoll().get(0).grund()).isEqualTo(Grund.MEHRDEUTIG); + assertThat(ergebnis.protokoll().get(0).begruendung()).contains("mehrdeutig"); + assertThat(ergebnis.neu().norm("§ 108").orElseThrow().absaetze().get(0).text()) + .doesNotContain("oder Absatz 4"); + } + + /** + * Ein Halbsatz setzt den Satz hinter dem Strichpunkt fort; er beginnt keinen neuen. Angefügt wird + * er deshalb wie ein Satz — aber nur hinter einem Strichpunkt. Endet der Zieltext auf einen + * Punkt, so wäre das Angefügte gerade kein Halbsatz, und der Befehl bleibt liegen. + */ + @Test + void halbsatzWirdHinterDemStrichpunktAngefuegt() { + var gesetz = + new Gesetz( + "TestG", + "Testgesetz", + "TestG", + List.of( + new Norm( + "§ 8", + null, + null, + List.of(new Absatz("1", "Erster Satz. Der zweite Satz endet hier;")), + false))); + + var befehl = + new Anfuegung( + stelle(new Stelle.Paragraph("8"), new Stelle.AbsatzNr("1")), + Ebene.HALBSATZ, + null, + "die Bearbeitungszeit zählt zu den Fachstudien.", + PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz, List.of(befehl)); + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT); + assertThat(ergebnis.neu().norm("§ 8").orElseThrow().absaetze().get(0).text()) + .isEqualTo( + "Erster Satz. Der zweite Satz endet hier; die Bearbeitungszeit zählt zu den" + + " Fachstudien."); + } + + @Test + void halbsatzOhneStrichpunktBleibtLiegen() { + var gesetz = + new Gesetz( + "TestG", + "Testgesetz", + "TestG", + List.of( + new Norm( + "§ 8", + null, + null, + List.of(new Absatz("1", "Erster Satz. Der zweite Satz endet hier.")), + false))); + + var befehl = + new Anfuegung( + stelle(new Stelle.Paragraph("8"), new Stelle.AbsatzNr("1")), + Ebene.HALBSATZ, + null, + "die Bearbeitungszeit zählt zu den Fachstudien.", + PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz, List.of(befehl)); + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.MANUELL_PRUEFEN); + assertThat(ergebnis.protokoll().get(0).grund()).isEqualTo(Grund.BESTAND_WIDERSPRICHT); + assertThat(ergebnis.protokoll().get(0).begruendung()) + .contains("setzt einen Strichpunkt voraus"); + assertThat(ergebnis.neu().norm("§ 8").orElseThrow().absaetze().get(0).text()) + .isEqualTo("Erster Satz. Der zweite Satz endet hier."); + } } diff --git a/src/test/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextAusgeberTest.java b/src/test/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextAusgeberTest.java new file mode 100644 index 0000000..e620a03 --- /dev/null +++ b/src/test/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextAusgeberTest.java @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2026 Matthias Andreas Benkard <code@mail.matthias.benkard.de> +// SPDX-License-Identifier: AGPL-3.0-or-later +package eu.mulk.aendggner.gesetz.land; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import eu.mulk.aendggner.gesetz.Gesetz; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Der Beleg des Textausgebers ist der Rundlauf, nicht der Augenschein: Was er schreibt, muss der + * Lader wieder zu demselben Gesetz lesen. Geprüft wird das an sämtlichen Klartext-Stammfassungen + * des Beispielkorpus — dreizehn Länder, jede Eigenheit des Formats mindestens einmal (Superskripte + * und „Art.“-Sigel in Bayern, titellose Paragraphen und ausgeschriebene Ordinale in Hessen, die + * Nummern einer Anlage als eigene Normen in Berlin, Zwischentitel, Fußnoten, Inhaltsübersicht). + */ +class LandesRechtTextAusgeberTest { + + private static final Path SAMPLEDATA = Path.of("src/test/resources/sampledata"); + + static Stream<Path> klartextFassungen() throws IOException { + if (!Files.isDirectory(SAMPLEDATA)) { + return Stream.of(); + } + try (var pfade = Files.walk(SAMPLEDATA)) { + return pfade + .filter(p -> p.getFileName().toString().endsWith(".txt")) + .sorted() + .toList() + .stream(); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("klartextFassungen") + void rundlaufErhaeltDasGesetz(Path datei) throws IOException { + assumeTrue(Files.exists(datei), "Beispieldaten fehlen"); + var gelesen = new LandesRechtLoader().load(datei); + + var geschrieben = LandesRechtTextAusgeber.ausgeben(gelesen); + var wiederGelesen = LandesRechtTextParser.parse(geschrieben); + + assertThat(wiederGelesen.langue()).isEqualTo(gelesen.langue()); + assertThat(wiederGelesen.jurabk()).isEqualTo(gelesen.jurabk()); + assertThat(wiederGelesen.kurzue()).isEqualTo(gelesen.kurzue()); + assertThat(bezeichnungen(wiederGelesen)).isEqualTo(bezeichnungen(gelesen)); + assertThat(wiederGelesen.gliederungen()).isEqualTo(gelesen.gliederungen()); + + for (var soll : gelesen.normen()) { + var ist = wiederGelesen.norm(soll.enbez()).orElseThrow(); + assertThat(ist.titel()).as("Titel von %s", soll.enbez()).isEqualTo(soll.titel()); + assertThat(ist.weggefallen()) + .as("Wegfall von %s", soll.enbez()) + .isEqualTo(soll.weggefallen()); + assertThat(ist.absaetze()).as("Absätze von %s", soll.enbez()).isEqualTo(soll.absaetze()); + } + } + + private static List<String> bezeichnungen(Gesetz gesetz) { + return gesetz.normen().stream().map(n -> n.enbez()).toList(); + } +} |
