aboutsummaryrefslogtreecommitdiff
path: root/src/test/java/eu
diff options
context:
space:
mode:
Diffstat (limited to 'src/test/java/eu')
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java111
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java100
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java61
-rw-r--r--src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java39
4 files changed, 307 insertions, 4 deletions
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index 57e745f..5254ff2 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -25,10 +25,9 @@ import org.junit.jupiter.api.Test;
*/
class EndToEndTest {
- private static final Path STAMMGESETZ =
- Path.of("src/main/resources/sampledata/IfSG/BJNR104510000.xml");
- private static final Path AENDERUNGSGESETZ =
- Path.of("src/main/resources/sampledata/IfSG/bgbl120s2397_78991.pdf");
+ private static final Path SAMPLEDATA = Path.of("src/main/resources/sampledata");
+ private static final Path STAMMGESETZ = SAMPLEDATA.resolve("IfSG/BJNR104510000.xml");
+ private static final Path AENDERUNGSGESETZ = SAMPLEDATA.resolve("IfSG/bgbl120s2397_78991.pdf");
@Test
void gesamtePipelineAufIfSgBeispiel() throws Exception {
@@ -69,4 +68,108 @@ class EndToEndTest {
assertThat(html).contains("Manuell prüfen");
assertThat(html).containsAnyOf("<del>", "<ins>");
}
+
+ /** Neues digitales BGBl-Format (recht.bund.de, ab 2023): 3. UWGÄndG 2026. */
+ @Test
+ void uwgNeuesBgblFormat() 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 gesetz = new GiiXmlLoader().load(xml);
+ var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
+
+ // Der Fontgrößen-Filter muss die Fußnotenblöcke entfernt haben, die im neuen Format sonst
+ // mitten im Gesetzestext landen.
+ assertThat(text).doesNotContain("Dieses Gesetz dient der Umsetzung der Richtlinie (EU)");
+ assertThat(text).contains("wird wie folgt geändert");
+
+ var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null);
+ assertThat(parseErgebnis.artikel()).containsExactly("1");
+ assertThat(parseErgebnis.befehle().size()).isGreaterThanOrEqualTo(10);
+ var typisiert =
+ parseErgebnis.befehle().stream().filter(b -> !(b instanceof UnbekannterBefehl)).count();
+ assertThat(typisiert).isGreaterThan(parseErgebnis.befehle().size() / 2);
+
+ var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
+ var synopse = SynopseBuilder.baue(gesetz, anwendung, parseErgebnis.warnungen(), false);
+ assertThat(HtmlRenderer.rendere(synopse, "E2E-Test")).contains("Neue Fassung");
+ }
+
+ /** Stresstest: GEG-Novelle 2023 („Heizungsgesetz“, 26 Seiten, >100 Befehle). */
+ @Test
+ void gegGrossesAenderungsgesetz() throws Exception {
+ var xml = SAMPLEDATA.resolve("GEG/BJNR172810020.xml");
+ var pdf = SAMPLEDATA.resolve("GEG/bgbl123s0280_regelungstext.pdf");
+ assumeTrue(Files.exists(xml) && Files.exists(pdf), "GEG-Beispieldaten fehlen");
+
+ var gesetz = new GiiXmlLoader().load(xml);
+ var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
+ var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null);
+
+ assertThat(parseErgebnis.artikel()).contains("1");
+ assertThat(parseErgebnis.befehle().size()).isGreaterThanOrEqualTo(100);
+
+ // Das XML ist bereits konsolidiert; entscheidend ist, dass die Anwendung sauber terminiert
+ // und jeden Befehl protokolliert.
+ var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
+ assertThat(anwendung.protokoll()).hasSameSizeAs(parseErgebnis.befehle());
+ var synopse = SynopseBuilder.baue(gesetz, anwendung, parseErgebnis.warnungen(), false);
+ assertThat(HtmlRenderer.rendere(synopse, "E2E-Test")).contains("Manuell prüfen");
+ }
+
+ /**
+ * Gesetzentwurf (Bundestags-Drucksache mit markerloser Silbentrennung). Das AGG-XML ist NICHT
+ * konsolidiert (Stand 2023, Entwurf von 2026) — hier entstehen echte Diffs.
+ */
+ @Test
+ void aggRegierungsentwurfMitEchtenDiffs() throws Exception {
+ var xml = SAMPLEDATA.resolve("AGG/BJNR189710006.xml");
+ var pdf = SAMPLEDATA.resolve("AGG/BT-Drs-21-6178_Regierungsentwurf.pdf");
+ assumeTrue(Files.exists(xml) && Files.exists(pdf), "AGG-Beispieldaten fehlen");
+
+ var gesetz = new GiiXmlLoader().load(xml);
+ var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
+
+ // Markerlose Silbentrennung („Schwel|lenwertes“) muss zusammengezogen sein.
+ assertThat(text).contains("Schwellenwertes");
+
+ var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null);
+ assertThat(parseErgebnis.artikel()).containsExactly("1");
+ // Der Begründungsteil darf keine Befehle erzeugen.
+ assertThat(parseErgebnis.befehle().size()).isBetween(15, 40);
+
+ var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
+ assertThat(anwendung.anzahlAngewandt()).isGreaterThan(10);
+
+ var synopse = SynopseBuilder.baue(gesetz, anwendung, parseErgebnis.warnungen(), false);
+ var html = HtmlRenderer.rendere(synopse, "E2E-Test");
+ assertThat(html).contains("<del>").contains("<ins>");
+ // Stichprobe aus Artikel 1 Nummer 1: „Alters“ → „Lebensalters“ in § 1.
+ assertThat(html).contains("<ins>Lebensalters</ins>");
+ }
+
+ /**
+ * Regierungsentwurf, dessen Artikel 1 ein Ablösegesetz ist; nur Artikel 2 ändert das ProdHaftG.
+ */
+ @Test
+ void prodHaftGRegierungsentwurf() throws Exception {
+ var xml = SAMPLEDATA.resolve("ProdHaftG/BJNR021980989.xml");
+ var pdf = SAMPLEDATA.resolve("ProdHaftG/RegE_Produkthaftungsrecht_2025-12-17.pdf");
+ assumeTrue(Files.exists(xml) && Files.exists(pdf), "ProdHaftG-Beispieldaten fehlen");
+
+ var gesetz = new GiiXmlLoader().load(xml);
+ var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
+ var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null);
+
+ // Artikel 1 (neues Produkthaftungsgesetz) betrifft das Stammgesetz nicht; Artikel 2 schon.
+ assertThat(parseErgebnis.artikel()).containsExactly("2");
+ assertThat(parseErgebnis.befehle()).hasSize(1);
+
+ var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
+ assertThat(anwendung.anzahlAngewandt()).isEqualTo(1);
+ var neueNorm = anwendung.neu().norm("§ 19").orElseThrow();
+ assertThat(neueNorm.titel()).isEqualTo("Außerkrafttreten");
+ assertThat(neueNorm.gesamtText()).contains("9. Dezember 2026 außer Kraft");
+ }
}
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 c8f9cb8..f0282c5 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
@@ -247,6 +247,106 @@ class BefehlErkennerTest {
}
@Test
+ void erkenntStrukturErsetzungEinesAbsatzes() {
+ // Neues BGBl-Format (3. UWGÄndG 2026, Artikel 1 Nummer 1).
+ var befehl =
+ erkenne(
+ "§ 2 Absatz 2 wird durch die folgenden Absätze 2 und 3 ersetzt: „(2) Im Sinne"
+ + " dieses Gesetzes ist … (3) Weiteres.“",
+ Stelle.LEER);
+
+ assertThat(befehl).containsInstanceOf(Aenderungsbefehl.StrukturErsetzung.class);
+ var ersetzung = (Aenderungsbefehl.StrukturErsetzung) befehl.orElseThrow();
+ assertThat(ersetzung.stelle().anzeigeText()).isEqualTo("§ 2 Absatz 2");
+ assertThat(ersetzung.ebene()).isEqualTo(Ebene.ABSATZ);
+ assertThat(ersetzung.text()).startsWith("(2) Im Sinne");
+ }
+
+ @Test
+ void erkenntStrukturErsetzungEinesSatzes() {
+ var befehl =
+ erkenne(
+ "Satz 3 wird durch die folgenden Sätze ersetzt: „Erster neuer Satz. Zweiter"
+ + " neuer Satz.“",
+ new Stelle(List.of(new Stelle.Paragraph("13"), new Stelle.AbsatzNr("3"))));
+
+ assertThat(befehl).containsInstanceOf(Aenderungsbefehl.StrukturErsetzung.class);
+ var ersetzung = (Aenderungsbefehl.StrukturErsetzung) befehl.orElseThrow();
+ assertThat(ersetzung.stelle().anzeigeText()).isEqualTo("§ 13 Absatz 3 Satz 3");
+ assertThat(ersetzung.ebene()).isEqualTo(Ebene.SATZ);
+ }
+
+ @Test
+ void erkenntUeberschriftErsetzungAlsNeufassung() {
+ // AGG-Regierungsentwurf 2026, Artikel 1 Nummer 4 Buchstabe a.
+ var befehl =
+ erkenne(
+ "Die Überschrift wird durch die folgende Überschrift ersetzt: „§ 10 Zulässige"
+ + " unterschiedliche Behandlung wegen des Lebensalters“.",
+ new Stelle(List.of(new Stelle.Paragraph("10"))));
+
+ assertThat(befehl).containsInstanceOf(Neufassung.class);
+ assertThat(befehl.orElseThrow().stelle().betrifftUeberschrift()).isTrue();
+ }
+
+ @Test
+ void erkenntParagraphErsetzungAlsNeufassung() {
+ // ProdHaftG-Regierungsentwurf 2025, Artikel 2.
+ var befehl =
+ erkenne(
+ "§ 19 wird durch den folgenden § 19 ersetzt: „§ 19 Außerkrafttreten Dieses"
+ + " Gesetz tritt am 9. Dezember 2026 außer Kraft.“",
+ Stelle.LEER);
+
+ assertThat(befehl).containsInstanceOf(Neufassung.class);
+ assertThat(befehl.orElseThrow().stelle().anzeigeText()).isEqualTo("§ 19");
+ }
+
+ @Test
+ void erkenntPluralEinfuegung() {
+ // AGG-Regierungsentwurf 2026: „die folgenden Absätze 6 und 7“.
+ var befehl =
+ erkenne(
+ "Nach Absatz 5 werden die folgenden Absätze 6 und 7 eingefügt: „(6) Neu."
+ + " (7) Auch neu.“",
+ new Stelle(List.of(new Stelle.Paragraph("27"))));
+
+ assertThat(befehl).containsInstanceOf(StrukturEinfuegung.class);
+ var einfuegung = (StrukturEinfuegung) befehl.orElseThrow();
+ assertThat(einfuegung.ebene()).isEqualTo(Ebene.ABSATZ);
+ assertThat(einfuegung.bezeichnung()).isNull();
+ }
+
+ @Test
+ void erkenntSatzzeichenErsetzungMitWoertern() {
+ // GEG-Novelle 2023: „durch ein Komma und die Wörter … ersetzt“.
+ var befehl =
+ erkenne(
+ "In Satz 2 wird der Punkt am Ende durch ein Komma und die Wörter „sowie neue"
+ + " Anforderungen“ ersetzt.",
+ new Stelle(List.of(new Stelle.Paragraph("47"), new Stelle.AbsatzNr("1"))));
+
+ assertThat(befehl).containsInstanceOf(Ersetzung.class);
+ var ersetzung = (Ersetzung) befehl.orElseThrow();
+ assertThat(ersetzung.alt()).isEqualTo(".");
+ assertThat(ersetzung.neu()).isEqualTo(", sowie neue Anforderungen");
+ assertThat(ersetzung.amEnde()).isTrue();
+ }
+
+ @Test
+ void erkenntAngabeEinfuegungImInhaltsuebersichtsKontext() {
+ // GEG-Novelle 2023: verschachtelt unter „Die Inhaltsübersicht wird wie folgt geändert:“.
+ var kontext = new Stelle(List.of(new Stelle.Inhaltsuebersicht()));
+ var befehl =
+ erkenne(
+ "Nach der Angabe zu § 9 wird folgende Angabe eingefügt: „§ 9a" + " Länderregelung“.",
+ kontext);
+
+ assertThat(befehl).containsInstanceOf(WoerterEinfuegung.class);
+ assertThat(befehl.orElseThrow().stelle().betrifftInhaltsuebersicht()).isTrue();
+ }
+
+ @Test
void faelltBeiBereichsbefehlenAufUnbekanntZurueck() {
// Bereichs- und Mehrfachbefehle sind in v1 bewusst nicht unterstützt.
assertThat(
diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
index d8c5a03..98dc1d3 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
@@ -46,4 +46,65 @@ class TextBereinigerTest {
assertThat(TextBereiniger.bereinige("Preisbildung und -\ngestaltung"))
.isEqualTo("Preisbildung und -\ngestaltung");
}
+
+ @Test
+ void entferntKopfzeilenDesNeuenBgblFormats() {
+ var roh =
+ "Erster Satz. \n"
+ + "Seite 2 von 5 Bundesgesetzblatt Jahrgang 2026 Teil I Nr. 43, ausgegeben zu"
+ + " Bonn am 19. Februar 2026 \n"
+ + "Zweiter Satz. ";
+
+ assertThat(TextBereiniger.bereinige(roh)).isEqualTo("Erster Satz.\nZweiter Satz.");
+ }
+
+ @Test
+ void entferntSeitenmarkerVonEntwuerfen() {
+ assertThat(TextBereiniger.bereinige("Erster Satz. \n - 10 - \nZweiter Satz. "))
+ .isEqualTo("Erster Satz.\nZweiter Satz.");
+ }
+
+ @Test
+ void entferntDrucksachenKopfzeilen() {
+ var roh =
+ "Erster Satz. \n"
+ + "Drucksache 21/6178 – 2 – Deutscher Bundestag – 21. Wahlperiode \n"
+ + "Deutscher Bundestag – 21. Wahlperiode – 3 – Drucksache 21/6178 \n"
+ + "Zweiter Satz. ";
+
+ assertThat(TextBereiniger.bereinige(roh)).isEqualTo("Erster Satz.\nZweiter Satz.");
+ }
+
+ @Test
+ void ziehtMarkerloseSilbentrennungZusammen() {
+ // Bundestags-Drucksachen: reguläre Umbrüche enden mit Leerzeichen, Trennungen nicht.
+ var roh = "unterhalb eines Schwel\nlenwertes von 50 \nWohnungen. ";
+
+ assertThat(TextBereiniger.bereinige(roh))
+ .isEqualTo("unterhalb eines Schwellenwertes von 50\nWohnungen.");
+ }
+
+ @Test
+ void ziehtOhneTrailingSpaceKonventionKeineMarkerlosenTrennungenZusammen() {
+ // Handgeschriebene Klartextdateien: kein Trailing-Space-Signal → kein Join.
+ var roh = "das zuletzt geändert worden\nist, wird wie folgt geändert:";
+
+ assertThat(TextBereiniger.bereinige(roh)).isEqualTo(roh);
+ }
+
+ @Test
+ void ziehtMarkerlosNichtVorKonjunktionenZusammen() {
+ var roh = "die Wirk\nund Hilfsstoffe sind wichtig. \nZweite Zeile endet mit Leerzeichen. ";
+
+ assertThat(TextBereiniger.bereinige(roh)).startsWith("die Wirk\nund Hilfsstoffe");
+ }
+
+ @Test
+ void repariertInvertierteZitatzeichenAnAbsatzmarkern() {
+ // BMJV-Vorlagen zeichnen das hängende „ nach dem Absatzmarker bzw. der Paragraphenangabe.
+ assertThat(TextBereiniger.bereinige("(1) „ Ungeachtet des § 8 gilt.“"))
+ .isEqualTo("„(1) Ungeachtet des § 8 gilt.“");
+ assertThat(TextBereiniger.bereinige("§ 19„\nAußerkrafttreten"))
+ .isEqualTo("„§ 19\nAußerkrafttreten");
+ }
}
diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
index 26557d1..beae519 100644
--- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
+++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
@@ -9,6 +9,7 @@ import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ersetzung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Neufassung;
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.UnbekannterBefehl;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WoerterEinfuegung;
@@ -309,6 +310,44 @@ class BefehlAnwenderTest {
}
@Test
+ void ersetztAbsatzDurchMehrereAbsaetze() {
+ var befehl =
+ new StrukturErsetzung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("1")),
+ Ebene.ABSATZ,
+ "(1) Zweck ist die Erprobung. (1a) Die Erprobung ist wichtig.",
+ PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl));
+
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT);
+ var norm = ergebnis.neu().norm("§ 1").orElseThrow();
+ assertThat(norm.absaetze()).hasSize(3);
+ assertThat(norm.absaetze().get(0).nummer()).isEqualTo("1");
+ assertThat(norm.absaetze().get(1).nummer()).isEqualTo("1a");
+ assertThat(norm.absaetze().get(1).text()).isEqualTo("Die Erprobung ist wichtig.");
+ assertThat(norm.absaetze().get(2).nummer()).isEqualTo("2");
+ }
+
+ @Test
+ void ersetztSatzDurchMehrereSaetze() {
+ var befehl =
+ new StrukturErsetzung(
+ stelle(new Stelle.Paragraph("2"), new Stelle.SatzNr("2")),
+ Ebene.SATZ,
+ "Die Prüfung erfolgt gewissenhaft. Sie wird dokumentiert.",
+ PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl));
+
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT);
+ assertThat(absatzText(ergebnis.neu(), "§ 2", 0))
+ .contains(
+ "Erprobung ist die Prüfung der Tauglichkeit. Die Prüfung erfolgt gewissenhaft."
+ + " Sie wird dokumentiert. Sie endet mit einem Bericht.");
+ }
+
+ @Test
void fuegtSatzNachSatzEin() {
var befehl =
new StrukturEinfuegung(