diff --git a/.gitignore b/.gitignore index dbdc852..35218b3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ .settings/ target/ .claude/settings.local.json +logs/ +dependency-reduced-pom.xml diff --git a/docs/arbeitspakete/m1/AP05-befundmodell.md b/docs/arbeitspakete/m1/AP05-befundmodell.md index 4a1ebe7..16c263a 100644 --- a/docs/arbeitspakete/m1/AP05-befundmodell.md +++ b/docs/arbeitspakete/m1/AP05-befundmodell.md @@ -1,18 +1,28 @@ +--- +model: sonnet +--- # AP05 – Befundmodell mit Spec-/Diagnose-Trennung +> **Meilenstein:** M1 +> **Vorgänger:** AP03, AP04 ✅ +> **Nachfolger:** AP06, AP07, AP09 +> **Grundlage:** `docs/specs/technik-und-architektur.md` v5, §§ „Ergebnis- und Befundmodell", „Befundarten", „Gültigkeitsentscheidung und Exit-Codes" +> **Entscheidungsprotokoll:** `docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md` + ## Ziel Im Paket `domain` ein **stabiles Befundmodell** einführen, das von Anfang an zwischen **Spec-Urteil** (verbindliches Prüfurteil gemäß Spezifikation) und **diagnostischer Weiteranalyse** (zusätzliche, das Spec-Urteil nicht verändernde Hinweise) unterscheidet. Dieses Modell ist das **Herzstück** für alle nachfolgenden Meilensteine. -Der bestehende Typ `de.gecheckt.asv.validation.model.ValidationResult` wird nicht einfach gelöscht, sondern **ersetzt** durch ein sauber geschnittenes Domain-Modell. Der alte Typ wird im Rahmen von AP09 eingefroren. +Der bestehende Typ `de.gecheckt.asv.validation.model.ValidationResult` wird nicht geändert — er wird in AP09 eingefroren. ## Voraussetzungen -- AP03 (Paketstruktur) +- AP03 (Paketstruktur vorhanden) +- AP04 (Logging-Adapter) ## Scope IN -Folgende Typen im Paket `de.gecheckt.asv.domain.finding` (oder ähnliches Unterpaket von `domain`): +Folgende Typen im Paket `de.gecheckt.asv.domain.finding`: ### `Severity` (Enum) - `ERROR` @@ -21,88 +31,108 @@ Folgende Typen im Paket `de.gecheckt.asv.domain.finding` (oder ähnliches Unterp ### `FindingKind` (Enum) - `SPEC` — Befund ist Teil des Spec-Urteils, beeinflusst `Verdict` -- `DIAGNOSTIC` — zusätzliche Weiteranalyse, beeinflusst `Verdict` **nicht** +- `DIAGNOSTIC` — zusätzliche Weiteranalyse, beeinflusst `Verdict` **niemals** ### `FindingLayer` (Enum) - `ARTIFACT` — äußeres Artefakt / Dateiebene - `TECHNICAL_STRUCTURE` — Service-Segmente, KKS, Transport - `DOMAIN_MODEL` — kanonisches Fachmodell (ASVREC/ASVFEH) -### `Finding` (Record) +### `Finding` (Record oder unveränderliche Klasse) + +Alle Pflichtfelder laut `technik-und-architektur.md` §„Befundarten": + ```java public record Finding( FindingKind kind, // SPEC oder DIAGNOSTIC Severity severity, // ERROR/WARNING/HINT FindingLayer layer, // ARTIFACT/TECHNICAL_STRUCTURE/DOMAIN_MODEL - String ruleId, // interne Regel-ID, optional null - String officialErrorCode, // offizieller Fehlercode, optional null - String segmentType, // z.B. "UNB", optional - Integer segmentIndex, // optional - String fieldId, // z.B. "UNB_0020", optional - String rawValue, // Rohwert, optional - Integer position, // Byte-/Zeichenposition, optional - String messageReference, // UNH 0062 bei Nachrichtenbezug, optional - String germanMessage // deutscher Befundtext + String ruleId, // interne Regel-ID, nullable + String officialErrorCode, // offizieller Spec-Fehlercode, nullable + String segmentType, // z.B. "UNB", nullable + Integer segmentIndex, // nullable + String fieldId, // z.B. "UNB_0020", nullable + String rawValue, // Rohwert, nullable + Integer position, // Byte-/Zeichenposition, nullable + String messageReference, // UNH 0062 bei Nachrichtenbezug, nullable + String germanMessage // deutscher Befundtext, nicht nullable ) {} ``` -Records mit vielen optionalen Feldern sind unschön — als Alternative ist eine reguläre Klasse mit Builder erlaubt, solange sie unveränderlich (`final`) ist und die gleichen Felder trägt. **Wichtig ist: unveränderlich und mit allen Metadaten aus `technik-und-architektur.md` Abschnitt „Befundarten".** + +Records mit vielen optionalen Feldern sind unschön — als Alternative ist eine reguläre unveränderliche Klasse mit Builder erlaubt, solange alle Felder vorhanden sind. ### `Verdict` (Enum) -- `VALID` — keine Spec-ERROR-Befunde -- `INVALID` — mindestens ein Spec-ERROR-Befund -- `OPERATIONAL_ERROR` — Bedienfehler (Exit-Code 2) +- `VALID` — keine SPEC-ERROR-Befunde +- `INVALID` — mindestens ein SPEC-ERROR-Befund +- `OPERATIONAL_ERROR` — Bedienfehler (Exit-Code 2), wird in AP08 genutzt -### `ValidationReport` (Klasse) -- unveränderlich -- enthält: - - `List findings` (alle Befunde, SPEC und DIAGNOSTIC gemischt, Reihenfolge erhalten) - - Methode `Verdict computeVerdict()` — berücksichtigt **nur** `kind == SPEC` und `severity == ERROR` - - Methode `List specFindings()` — filtert auf `kind == SPEC` - - Methode `List diagnosticFindings()` — filtert auf `kind == DIAGNOSTIC` - - Methode `boolean hasSpecErrors()` - - Metadaten: `String fileName`, `Instant timestamp` -- **invariant:** eine `DIAGNOSTIC`-Severity `ERROR` darf das Verdict **niemals** auf `INVALID` setzen. Dies ist per Unit-Test abzusichern. +### `ValidationReport` (unveränderliche Klasse) -### Unit-Tests -- `ValidationReport` ohne Befunde → `Verdict.VALID` -- `ValidationReport` mit einem SPEC-ERROR → `Verdict.INVALID` -- `ValidationReport` mit einem DIAGNOSTIC-ERROR → `Verdict.VALID` (dieser Test ist kritisch!) -- `ValidationReport` mit SPEC-WARNING → `Verdict.VALID` (nur ERROR zählt) -- `specFindings()` / `diagnosticFindings()` filtern korrekt -- Unveränderlichkeit: `findings`-Liste ist nicht modifizierbar +```java +public final class ValidationReport { + // Metadaten + String fileName; + Instant timestamp; + // Befunde + List findings; // unveränderlich + + // Kern-Methoden + Verdict computeVerdict(); // NUR SPEC+ERROR zählt + boolean hasSpecErrors(); + List specFindings(); + List diagnosticFindings(); + + // Factory für Bedienfehler (AP08) + static ValidationReport operationalError(String fileName, String ruleId, String message); +} +``` + +**Invariante:** `computeVerdict()` berücksichtigt **ausschließlich** Findings mit `kind == SPEC` und `severity == ERROR`. Ein `DIAGNOSTIC`-ERROR darf das Verdict **niemals** auf `INVALID` setzen. Dies ist per Unit-Test abzusichern. + +### Unit-Tests (Mindestanforderung) + +1. Leerer Report → `Verdict.VALID` +2. Ein SPEC-ERROR → `Verdict.INVALID` +3. **Ein DIAGNOSTIC-ERROR → `Verdict.VALID`** (dieser Test ist kritisch und muss explizit vorhanden sein) +4. SPEC-WARNING → `Verdict.VALID` (nur ERROR zählt) +5. `specFindings()` / `diagnosticFindings()` filtern korrekt +6. `findings`-Liste ist nicht von außen modifizierbar +7. `operationalError(...)` → `Verdict.OPERATIONAL_ERROR` ## Scope OUT -- Integration des neuen Modells in den bestehenden Lauf (kommt in AP06) -- Löschen oder Umbenennen der alten `validation.model.ValidationResult`-Klasse (das ist Teil von AP09) -- Berichtserzeugung (Text-Rendering), Bericht-Format, Konsolenausgabe (kommt in AP07) -- Architekturtest (kommt in AP10) +- Integration des neuen Modells in den bestehenden Lauf (AP06) +- Löschen oder Umbenennen der alten `ValidationResult`-Klasse (AP09) +- Berichtserzeugung, Textrendering, Konsolenausgabe (AP07) +- Architekturtest (AP10) ## Schritte -1. Branch `m1/ap05-befundmodell` -2. Paket `de.gecheckt.asv.domain.finding` anlegen -3. Enums und Klassen implementieren -4. Unit-Tests schreiben, mindestens die sechs oben genannten -5. `mvn clean verify` grün bekommen -6. Commit `M1-AP05: Befundmodell mit Spec-/Diagnose-Trennung` -7. Abschlussbericht schreiben +1. Paket `de.gecheckt.asv.domain.finding` anlegen +2. Enums `Severity`, `FindingKind`, `FindingLayer` implementieren +3. `Finding` implementieren +4. `Verdict` implementieren +5. `ValidationReport` implementieren +6. Unit-Tests schreiben — mindestens die sieben oben genannten +7. `mvn clean verify` grün bekommen +8. Abschlussbericht schreiben ## Abnahmekriterien -- Paket `domain.finding` enthält alle oben genannten Typen -- **Der Test „DIAGNOSTIC-ERROR ergibt VALID" ist grün** und wird im Bericht explizit genannt -- `ValidationReport.findings` ist unveränderlich (Test vorhanden) -- alle Metadaten-Felder aus `technik-und-architektur.md` Abschnitt „Befundarten" sind im `Finding`-Typ vorhanden -- `mvn clean verify` ist grün -- keine Änderung an `validation.model.ValidationResult` (Altmodell) -- Abschlussbericht liegt vor +- [ ] Paket `domain.finding` enthält alle genannten Typen +- [ ] **Test „DIAGNOSTIC-ERROR ergibt VALID" ist grün** und im Bericht explizit genannt +- [ ] `ValidationReport.findings` ist unveränderlich (Test vorhanden) +- [ ] Alle Metadatenfelder aus `technik-und-architektur.md` §„Befundarten" sind im `Finding`-Typ vorhanden +- [ ] `operationalError(...)` Factory-Methode existiert +- [ ] Keine Änderung an `ValidationResult` (Altmodell) +- [ ] `mvn clean verify` grün +- [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP05-bericht.md` ## Rest-Risiken und offene Punkte -- Wir haben jetzt zwei parallele Ergebnis-Typen: den alten `ValidationResult` und den neuen `ValidationReport`. Das ist **Absicht** bis AP09, wo die alte Logik sauber eingefroren wird. -- Das Befundmodell ist bewusst **keine** Hierarchie (Datei → Schicht → Nachricht → Befund), sondern eine flache Liste mit Metadaten. Die hierarchische Berichtserzeugung passiert später auf Basis dieser Metadaten in M9. Für M1 genügt die flache Struktur. +- Zwei parallele Ergebnistypen (`ValidationResult` alt, `ValidationReport` neu) sind bis AP09 Absicht. +- Das Befundmodell ist bewusst eine **flache Liste** mit Metadaten, keine Hierarchie. Die hierarchische Berichtserzeugung kommt in M9. ## Bericht -`docs/arbeitspakete/m1/berichte/AP05-bericht.md` nach `templates/ap-bericht.md`. +`docs/arbeitspakete/m1/berichte/AP05-bericht.md` nach `docs/arbeitspakete/m1/templates/ap-bericht.md`. diff --git a/docs/arbeitspakete/m1/AP06-bootstrap-cli.md b/docs/arbeitspakete/m1/AP06-bootstrap-cli.md index 354b072..d0c60d9 100644 --- a/docs/arbeitspakete/m1/AP06-bootstrap-cli.md +++ b/docs/arbeitspakete/m1/AP06-bootstrap-cli.md @@ -1,13 +1,22 @@ +--- +model: sonnet +--- # AP06 – Bootstrap und CLI-Adapter +> **Meilenstein:** M1 +> **Vorgänger:** AP05 ✅ erforderlich +> **Nachfolger:** AP07, AP09 +> **Grundlage:** `docs/specs/technik-und-architektur.md` v5, §§ „CLI-Zuschnitt", „Laufzeit- und Betriebsmodell", „Gültigkeitsentscheidung und Exit-Codes", „Zeichensätze" +> **Entscheidungsprotokoll:** `docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md` (E-05 fat JAR) + ## Ziel Die bestehende `AsvValidatorApplication` wird in zwei klar getrennte Verantwortlichkeiten zerlegt: -1. **Bootstrap** (`de.gecheckt.asv.bootstrap.Main`) — verdrahtet die Komponenten manuell per Constructor Injection und ist der einzige `public static void main`. -2. **CLI-Adapter** (`de.gecheckt.asv.adapter.in.cli.CliRunner` oder ähnlich) — nimmt CLI-Argumente entgegen, ruft die Application-Schicht auf, übersetzt das Ergebnis in einen Exit-Code. +1. **Bootstrap** (`de.gecheckt.asv.bootstrap.Main`) — verdrahtet alle Komponenten manuell per Constructor Injection, ist der einzige `public static void main` +2. **CLI-Adapter** (`de.gecheckt.asv.adapter.in.cli.CliRunner`) — nimmt CLI-Argumente entgegen, ruft Application-Schicht auf, übersetzt Ergebnis in Exit-Code -Zusätzlich werden die **Exit-Codes spec-konform** auf `0/1/2` umgestellt. +Zusätzlich werden Exit-Codes spec-konform auf `0/1/2` umgestellt, ISO-8859-15 als Eingabe-Encoding eingeführt, und das Uber-JAR via `maven-shade-plugin` gebaut. ## Voraussetzungen @@ -15,82 +24,146 @@ Zusätzlich werden die **Exit-Codes spec-konform** auf `0/1/2` umgestellt. ## Scope IN -### Bootstrap -- Klasse `de.gecheckt.asv.bootstrap.Main` mit `public static void main(String[] args)` -- verdrahtet manuell: - - Logging-Konfigurator - - CLI-Runner - - (Application-Service — Platzhalter, wird in AP09 feingeschnitten) -- ruft `CliRunner.run(args)` auf, gibt den zurückgegebenen Exit-Code an `System.exit` weiter +### Bootstrap (`de.gecheckt.asv.bootstrap.Main`) + +- `public static void main(String[] args)` +- Verdrahtet manuell per Constructor Injection: + - `LoggingConfigurator` + - `CliRunner` + - Application-Service (in M1 noch Dummy-Pfad — Datei lesen, leeren `ValidationReport` zurückgeben) +- Ruft `CliRunner.run(args)` auf und gibt den Exit-Code an `System.exit` weiter +- Log4j2-Typen dürfen **nur** hier und in `adapter.out.logging` sichtbar sein + +### CLI-Adapter (`de.gecheckt.asv.adapter.in.cli.CliRunner`) -### CLI-Adapter -- Klasse `CliRunner` im Paket `adapter.in.cli` - Methode `int run(String[] args)` -- akzeptiert **genau ein Positionsargument**: den Pfad zur Eingabedatei -- bei 0 oder ≥2 Argumenten → Exit-Code `2`, Minimalbericht-Vorbereitung (vollständige Minimalbericht-Logik kommt in AP08) -- bei nicht existierender, nicht lesbarer oder nicht regulärer Eingabedatei → Exit-Code `2` -- bei erfolgreichem Lauf ohne Spec-Fehler → Exit-Code `0` -- bei erfolgreichem Lauf mit Spec-Fehlern → Exit-Code `1` +- Akzeptiert **genau ein Positionsargument**: Pfad zur Eingabedatei +- Bei 0 oder ≥ 2 Argumenten → Exit-Code `2`, kurze deutsche Fehlermeldung auf STDERR (vollständiger Minimalbericht kommt in AP08) +- Bei nicht existierender, nicht lesbarer oder nicht regulärer Eingabedatei → Exit-Code `2` +- Bei erfolgreichem Lauf ohne Spec-Fehler → Exit-Code `0` +- Bei erfolgreichem Lauf mit Spec-Fehlern → Exit-Code `1` ### Exit-Code-Konstanten -- Konstanten **nur noch in einer Klasse** (z.B. `ExitCode` im Paket `adapter.in.cli`) -- Werte: - - `0` = gültig, keine Fehler-Befunde - - `1` = ungültig, mindestens ein Spec-Fehler - - `2` = Bedienfehler -- Die alten Konstanten (`EXIT_CODE_INVALID_ARGUMENTS=1`, `EXIT_CODE_FILE_ERROR=2`, `EXIT_CODE_VALIDATION_ERRORS=3`) werden **gelöscht** + +Konstanten **nur noch in einer Klasse** (z.B. `ExitCode` im Paket `adapter.in.cli`): + +```java +public final class ExitCode { + public static final int VALID = 0; + public static final int INVALID = 1; + public static final int OPERATIONAL_ERROR = 2; + private ExitCode() {} +} +``` + +Die alten Konstanten (`EXIT_CODE_INVALID_ARGUMENTS=1`, `EXIT_CODE_FILE_ERROR=2`, `EXIT_CODE_VALIDATION_ERRORS=3`) werden **gelöscht**. ### Verdrahtung mit Befundmodell (AP05) -- `CliRunner` gibt am Ende einen `ValidationReport` aus AP05 weiter oder erzeugt selbst einen Minimal-`ValidationReport` mit einem `Finding` des Kinds `SPEC`, Severity `ERROR`, Layer `ARTIFACT` im Bedienfehlerfall -- das eigentliche Einlesen und Verarbeiten der Datei kann für M1 noch ein **Dummy-Pfad** sein: die Datei wird gelesen, die Bytes gezählt, ein leerer `ValidationReport` mit `fileName` und `timestamp` zurückgegeben. Echte Validierung gehört nicht in M1. + +- `CliRunner` nutzt `ValidationReport.computeVerdict()` zur Exit-Code-Ableitung +- Im Bedienfehlerfall: `ValidationReport.operationalError(...)` → Exit-Code `2` +- Der eigentliche M1-Dummy-Pfad: Datei wird gelesen (ISO-8859-15), Bytes gezählt, leerer `ValidationReport` mit `fileName` und `timestamp` zurückgegeben. **Keine echte Validierung in M1.** ### Zeichensatz-Korrektur -- beim Einlesen der Eingabedatei wird **ISO 8859-15** verwendet, nicht UTF-8. Das ist eine harte Spec-Vorgabe aus `fachliche-anforderungen.md` §5.1 und muss ab jetzt dauerhaft so bleiben. - ```java - Files.readString(path, StandardCharsets.ISO_8859_1); // nicht ideal — besser Charset.forName("ISO-8859-15") - ``` - Achtung: Java kennt `StandardCharsets.ISO_8859_1`, aber **nicht** `ISO_8859_15`. Daher `Charset.forName("ISO-8859-15")` verwenden. + +Beim Einlesen der Eingabedatei wird **ISO-8859-15** verwendet: + +```java +// Korrekt: +Charset iso = Charset.forName("ISO-8859-15"); +// NICHT: StandardCharsets.UTF_8 +// NICHT: Charset.defaultCharset() +``` + +JDK-21-Verfügbarkeit per Test belegen: Eine Datei mit Byte `0xA4` ergibt beim Einlesen das Euro-Zeichen `€`, weil `0xA4` in ISO-8859-15 auf Euro-Zeichen liegt. + +### Uber-JAR via `maven-shade-plugin` + +`maven-jar-plugin`-Platzhalter aus AP02 ersetzen durch `maven-shade-plugin`: + +```xml + + org.apache.maven.plugins + maven-shade-plugin + + + + package + shade + + + + de.gecheckt.asv.bootstrap.Main + + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + +``` + +`java -jar target/asv-format-validator-*.jar ` muss ohne `-cp` funktionieren. + +### `.gitignore` — `logs/` ergänzen + +`logs/` zum `.gitignore` hinzufügen (gemäß E-04 aus Entscheidungsprotokoll). Der statische Dateipfad aus AP04 wird nach AP07 durch dynamische Logdatei ersetzt. ## Scope OUT - Berichtdatei und Log-Datei im Eingabeverzeichnis (AP07) -- vollständiger Minimalbericht bei Exit-Code `2` (AP08) +- Vollständiger Minimalbericht bei Exit-Code `2` (AP08) - Entkopplung der Altlogik (AP09) - Architekturtest (AP10) ## Schritte -1. Branch `m1/ap06-bootstrap-cli` -2. `de.gecheckt.asv.bootstrap.Main` anlegen mit `public static void main` -3. `CliRunner` in `adapter.in.cli` anlegen -4. `ExitCode`-Konstantenklasse anlegen -5. `AsvValidatorApplication` schrittweise entkernen: Code wandert nach `CliRunner` und `Main` -6. Einlese-Encoding auf `Charset.forName("ISO-8859-15")` umstellen -7. `maven-jar-plugin` in `pom.xml` auf `de.gecheckt.asv.bootstrap.Main` setzen (Platzhalter aus AP02 konkretisieren) -8. Alle Tests, die auf `AsvValidatorApplication` direkt zeigen, auf `CliRunner` umziehen -9. `mvn clean package` laufen lassen, das erzeugte JAR manuell mit `java -jar target/asv-format-validator-*.jar ` prüfen +1. `de.gecheckt.asv.bootstrap.Main` anlegen +2. `CliRunner` in `adapter.in.cli` anlegen +3. `ExitCode`-Konstantenklasse anlegen +4. `AsvValidatorApplication` schrittweise entkernen: Code wandert nach `CliRunner` und `Main` +5. Einlese-Encoding auf `Charset.forName("ISO-8859-15")` umstellen +6. `maven-shade-plugin` in `pom.xml` einbinden, `maven-jar-plugin`-Platzhalter entfernen +7. `logs/` in `.gitignore` ergänzen +8. Alle Tests die auf `AsvValidatorApplication` direkt zeigen auf `CliRunner` umziehen +9. `mvn clean package` — erzeugtes JAR mit `java -jar target/asv-format-validator-*.jar ` manuell prüfen 10. `mvn clean verify` grün bekommen -11. Commit `M1-AP06: Bootstrap, CLI-Adapter, Exit-Codes 0/1/2, ISO 8859-15` -12. Abschlussbericht schreiben +11. Abschlussbericht schreiben ## Abnahmekriterien -- `de.gecheckt.asv.bootstrap.Main` existiert und ist `Main-Class` des JAR -- `CliRunner` ist der einzige Ort mit CLI-Argument-Parsing -- Exit-Codes `0`, `1`, `2` sind definiert und spec-konform eingesetzt -- **Test:** Aufruf ohne Argument → Exit-Code `2` -- **Test:** Aufruf mit nicht existierender Datei → Exit-Code `2` -- **Test:** Aufruf mit leerer, lesbarer Datei → Exit-Code `0` (Dummy-Pfad, leerer `ValidationReport`) -- Einlese-Encoding ist ISO 8859-15 (per Test belegt: eine Datei mit Byte `0xA4` ergibt beim Einlesen `€`, weil `0xA4` in ISO 8859-15 auf Euro-Zeichen liegt) -- ausführbares JAR unter `target/` ist manuell startbar -- `mvn clean verify` ist grün -- Abschlussbericht liegt vor +- [ ] `de.gecheckt.asv.bootstrap.Main` existiert und ist `Main-Class` des Uber-JAR +- [ ] `CliRunner` ist der einzige Ort mit CLI-Argument-Parsing +- [ ] Exit-Codes `0`, `1`, `2` sind definiert und spec-konform eingesetzt, kein Exit-Code `3` mehr erreichbar +- [ ] Test: Aufruf ohne Argument → Exit-Code `2` +- [ ] Test: Aufruf mit nicht existierender Datei → Exit-Code `2` +- [ ] Test: Aufruf mit leerer, lesbarer Datei → Exit-Code `0` +- [ ] Einlese-Encoding ist ISO-8859-15 (per Test belegt: Byte `0xA4` → `€`) +- [ ] `java -jar target/asv-format-validator-*.jar ` startet ohne `-cp` +- [ ] `logs/` in `.gitignore` +- [ ] Keine Log4j2-Typen außerhalb von `bootstrap` und `adapter.out.logging` +- [ ] `mvn clean verify` grün +- [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP06-bericht.md` ## Rest-Risiken und offene Punkte -- Der Dummy-Pfad (Datei wird gelesen, leerer Report zurückgegeben) ist bewusst dünn. Die Einbindung der alten Parser-Logik passiert in AP09. -- Es ist verlockend, schon hier in AP06 die bestehende Parser-/Validator-Kette mit dem neuen Modell zu verknüpfen. **Nicht tun.** AP06 soll nur die äußere Hülle geradeziehen. +- Der Dummy-Pfad (Datei lesen, leerer Report) ist bewusst dünn. Echte Parser-/Validator-Einbindung kommt in M3+. +- Nicht versuchen, in AP06 schon die alte Parser-/Validator-Kette mit dem neuen Modell zu verknüpfen — das ist AP09-Scope. +- `maven-shade-plugin` mit Log4j2 benötigt den `Log4j2PluginsCacheFileTransformer`, sonst werden Log4j2-Plugins nicht korrekt geladen. ## Bericht -`docs/arbeitspakete/m1/berichte/AP06-bericht.md` nach `templates/ap-bericht.md`. +`docs/arbeitspakete/m1/berichte/AP06-bericht.md` nach `docs/arbeitspakete/m1/templates/ap-bericht.md`. diff --git a/docs/arbeitspakete/m1/AP07-ausgabeartefakte.md b/docs/arbeitspakete/m1/AP07-ausgabeartefakte.md index 9c0ec43..d8b09a9 100644 --- a/docs/arbeitspakete/m1/AP07-ausgabeartefakte.md +++ b/docs/arbeitspakete/m1/AP07-ausgabeartefakte.md @@ -1,12 +1,21 @@ +--- +model: sonnet +--- # AP07 – Ausgabeartefakte: Berichtdatei und Log-Datei mit Suffix-Logik +> **Meilenstein:** M1 +> **Vorgänger:** AP04, AP05, AP06 ✅ erforderlich +> **Nachfolger:** AP08, AP10 +> **Grundlage:** `docs/specs/technik-und-architektur.md` v5, §§ „Ausgabeartefakte", „Zeichensätze", „Logging und Berichtserzeugung" +> **Entscheidungsprotokoll:** `docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md` (E-04 logs/-Verzeichnis) + ## Ziel Pro Lauf werden **zwei Ausgabedateien** im **Verzeichnis der Eingabedatei** erzeugt: - eine **Berichtdatei** `.txt` - eine **Log-Datei** `.log` -Beide in **UTF-8**. Zusätzlich wird der Bericht weiterhin in die **Konsole** geschrieben. Wenn bereits gleichnamige Dateien existieren, werden neue mit laufendem Suffix `_v1`, `_v2`, … erzeugt, **pro Eingabedatei-Basisname**. +Beide in **UTF-8**. Der Bericht wird zusätzlich in die **Konsole** geschrieben. Bei bereits vorhandenen Dateien gleichen Namens werden neue mit laufendem Suffix `_v1`, `_v2`, … erzeugt. ## Voraussetzungen @@ -14,69 +23,96 @@ Beide in **UTF-8**. Zusätzlich wird der Bericht weiterhin in die **Konsole** ge ## Scope IN -### Berichtdatei -- Klasse `ReportFileWriter` oder ähnlich im Paket `adapter.out.reporting` -- Eingabe: `ValidationReport` (aus AP05) und Eingabedatei-Pfad -- Ausgabe: eine UTF-8-Textdatei im **selben Verzeichnis** wie die Eingabedatei -- Dateiname: `.txt`, bei Konflikt `_v1.txt`, `_v2.txt`, … -- Inhalt für M1: **einfach gehalten**. Pro `Finding` eine Zeile mit den wichtigsten Metadaten (Severity, Kind, Layer, Feld-ID, deutsche Nachricht). Kopfzeile mit Dateiname, Zeitstempel, Verdict. Die fein strukturierte hierarchische Darstellung kommt erst in M9. +### `SuffixResolver` im Paket `adapter.out.filesystem` -### Log-Datei -- Wiederverwendung des `LoggingConfigurator` aus AP04 -- Methode `configureLogFile(Path logFile)` wird im Bootstrap **vor** dem CLI-Runner aufgerufen -- Log-Datei liegt im **selben Verzeichnis** wie die Eingabedatei -- Dateiname: `.log`, Suffix-Logik analog zur Berichtdatei -- Log4j2 wird programmatisch umkonfiguriert: der File-Appender schreibt nach dem neuen Pfad. Die statische `log4j2.xml` aus AP04 ist nur der Fallback für „kein Eingabeargument". +```java +public class SuffixResolver { + /** + * Ermittelt den ersten freien Dateipfad für den gegebenen Basisnamen + * und die gegebene Extension im Zielverzeichnis. + * Probiert: ., dann _v1., _v2., ... + */ + public Path resolveNextFreePath(Path directory, String baseName, String extension) { ... } +} +``` -### Suffix-Logik -- eigene kleine Utility-Klasse `SuffixResolver` im Paket `adapter.out.filesystem` -- Methode `Path resolveNextFreePath(Path baseDirectory, String baseName, String extension)`: - - probiert `.`, dann `_v1.`, `_v2.`, … - - gibt den ersten freien Pfad zurück -- wird sowohl für die Berichtdatei als auch für die Log-Datei verwendet -- **Hinweis:** Die Zählung ist pro Basisname unabhängig. Wenn für `test.auf.txt` schon `test.auf_v1.txt` existiert, aber für `test.auf.log` noch keine `_v1`, kann das vorkommen — die Suffixe müssen **nicht synchron** sein. +- Suffix-Zählung ist **pro Extension unabhängig** — `.txt` und `.log` haben getrennte Zähler +- Unit-Tests mindestens für: keine Datei vorhanden, `.txt` vorhanden, `.txt` + `_v1` vorhanden + +### `ReportFileWriter` im Paket `adapter.out.reporting` + +- Eingabe: `ValidationReport` (AP05) + Eingabedatei-Pfad +- Ausgabe: UTF-8-Textdatei im Verzeichnis der Eingabedatei +- Dateiname via `SuffixResolver` +- **Berichtinhalt für M1** (absichtlich minimal, wird in M9 ausgebaut): + - Kopfzeile: Zeitstempel, Eingabedatei, Verdict + - Pro `Finding`: eine Zeile mit Severity, Kind, Layer, Feld-ID, deutscher Meldung + - Fußzeile: Hinweis auf bewusst nicht geprüfte Bereiche + +Alle Texte auf **Deutsch**, Encoding **UTF-8** explizit — kein Plattform-Default. + +### `LoggingConfigurator.configureLogFile(Path)` in `adapter.out.logging` + +- Methode wird im Bootstrap **vor** dem ersten fachlichen Log-Aufruf aufgerufen +- Konfiguriert Log4j2-File-Appender programmatisch auf den gewünschten Pfad +- Dateiname via `SuffixResolver` (analog zur Berichtdatei, eigene Zählung) +- Log4j2-Typen (`LoggerContext`, `Appender` etc.) bleiben **ausschließlich** in `adapter.out.logging` und `bootstrap` +- Statischer `logs/`-Pfad aus `log4j2.xml` (AP04) wird entfernt oder auf Fallback-Default gesetzt, der nur greift wenn `configureLogFile` nicht aufgerufen wurde (z.B. für Testläufe) +- **Fallback:** Falls programmatische Log4j2-Umkonfiguration sich als instabil erweist, ist eine System-Property-basierte Konfiguration (`-Dasv.log.file=...`) ein zulässiger Ausweg — Entscheidung im Bericht dokumentieren + +### Integration in `bootstrap.Main` + +Reihenfolge vor dem Validierungslauf: +1. Eingabedatei-Pfad bestimmen +2. Basisnamen und Zielverzeichnis ableiten +3. `SuffixResolver` für `.log` aufrufen +4. `LoggingConfigurator.configureLogFile(logPath)` aufrufen → ab jetzt gehen Logs in die Datei +5. Validierungslauf starten +6. `ReportFileWriter` schreiben +7. Konsolenausgabe (identisch zum Berichtinhalt) ### Konsolenausgabe -- bleibt erhalten, schreibt denselben Bericht-Text wie die Berichtdatei -- Konsolenausgabe erfolgt **nach** der Berichtdatei-Erstellung, damit ein IO-Fehler beim Schreiben der Berichtdatei nicht die Konsolenausgabe verhindert + +- Schreibt denselben Text wie die Berichtdatei +- Erfolgt **nach** der Berichtdatei-Erstellung, damit ein IO-Fehler beim Schreiben die Konsolenausgabe nicht verhindert ## Scope OUT -- hierarchische Berichtsgliederung (M9) -- Einfärbung / ANSI-Codes in der Konsole +- Hierarchische Berichtsgliederung (M9) +- ANSI-Farben / Einfärbung in der Konsole - Log-Rotation -- Minimalbericht bei Exit-Code 2 (AP08) +- Minimalbericht bei Exit-Code `2` (AP08) ## Schritte -1. Branch `m1/ap07-ausgabeartefakte` -2. `SuffixResolver` implementieren inkl. Unit-Tests für: keine Datei vorhanden, `.txt` vorhanden, `.txt` + `_v1` vorhanden, mehrere Lücken -3. `ReportFileWriter` implementieren mit einfachem Zeilenformat für M1 -4. `LoggingConfigurator.configureLogFile(Path)` implementieren (programmatische Log4j2-Reconfiguration) -5. Bootstrap erweitern: vor CLI-Lauf → Log-Datei-Pfad bestimmen → `LoggingConfigurator` aufrufen -6. CLI-Runner erweitert: nach Lauf → Berichtdatei schreiben → Konsolenausgabe erzeugen -7. End-to-End-Test mit Dummy-Eingabedatei: beide Ausgabedateien entstehen, Suffix-Logik funktioniert -8. `mvn clean verify` grün -9. Commit `M1-AP07: Berichtdatei und Log-Datei im Eingabeverzeichnis mit Suffix-Logik` -10. Abschlussbericht schreiben +1. `SuffixResolver` implementieren inkl. Unit-Tests +2. `ReportFileWriter` implementieren mit einfachem Zeilenformat für M1 +3. `LoggingConfigurator.configureLogFile(Path)` implementieren +4. Bootstrap erweitern: Log-Datei-Pfad bestimmen → `LoggingConfigurator` aufrufen +5. `CliRunner` erweitern: nach Lauf → Berichtdatei schreiben → Konsolenausgabe +6. End-to-End-Test: beide Ausgabedateien entstehen, Suffix-Logik funktioniert, UTF-8 +7. `mvn clean verify` grün bekommen +8. Abschlussbericht schreiben ## Abnahmekriterien -- nach einem Lauf mit Eingabedatei `foo/bar.auf` existieren `foo/bar.auf.txt` und `foo/bar.auf.log` -- zweiter Lauf mit derselben Eingabedatei erzeugt `foo/bar.auf_v1.txt` und `foo/bar.auf_v1.log` -- dritter Lauf → `_v2` usw. -- beide Ausgabedateien sind UTF-8 -- Konsolenausgabe ist identisch zum Inhalt der Berichtdatei -- `SuffixResolver` hat mindestens vier Unit-Tests -- `mvn clean verify` ist grün -- Abschlussbericht liegt vor +- [ ] Nach Lauf mit `foo/bar.auf` entstehen `foo/bar.auf.txt` und `foo/bar.auf.log` +- [ ] Zweiter Lauf mit derselben Datei → `foo/bar.auf_v1.txt` und `foo/bar.auf_v1.log` +- [ ] Dritter Lauf → `_v2` usw. +- [ ] Beide Ausgabedateien sind UTF-8 +- [ ] Konsolenausgabe ist identisch zum Inhalt der Berichtdatei +- [ ] `SuffixResolver` hat mindestens drei Unit-Tests +- [ ] Log4j2-Typen sind außerhalb von `adapter.out.logging` und `bootstrap` nicht sichtbar +- [ ] Statischer `logs/`-Pfad aus `log4j2.xml` entfernt oder auf Fallback-Default gesetzt +- [ ] `mvn clean verify` grün +- [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP07-bericht.md` ## Rest-Risiken und offene Punkte -- **Race Conditions** bei gleichzeitigen Läufen auf derselben Eingabedatei: laut `technik-und-architektur.md` bewusst nicht behandelt (kein Mehrbenutzerbetrieb in V1). -- Die programmatische Log4j2-Reconfiguration ist technisch nicht ganz ohne. Falls sie sich als zu instabil erweist, ist eine **sys-property-basierte** Konfiguration der Log-Datei (`-Dasv.log.file=...`) ein zulässiger Fallback. Entscheidung im Bericht dokumentieren. +- Race Conditions bei gleichzeitigen Läufen auf derselben Eingabedatei: laut `technik-und-architektur.md` bewusst nicht behandelt (kein Mehrbenutzerbetrieb in V1). - Das Bericht-Dateiformat ist in M1 absichtlich primitiv. In M9 wird es durch die finale hierarchische Struktur ersetzt. +- Fallback bei instabiler programmatischer Log4j2-Umkonfiguration im Bericht dokumentieren. ## Bericht -`docs/arbeitspakete/m1/berichte/AP07-bericht.md` nach `templates/ap-bericht.md`. +`docs/arbeitspakete/m1/berichte/AP07-bericht.md` nach `docs/arbeitspakete/m1/templates/ap-bericht.md`. diff --git a/docs/arbeitspakete/m1/AP08-minimalbericht.md b/docs/arbeitspakete/m1/AP08-minimalbericht.md index 753ece9..0ed5ea7 100644 --- a/docs/arbeitspakete/m1/AP08-minimalbericht.md +++ b/docs/arbeitspakete/m1/AP08-minimalbericht.md @@ -1,8 +1,16 @@ +--- +model: sonnet +--- # AP08 – Minimalbericht bei Bedienfehlern (Exit-Code 2) +> **Meilenstein:** M1 +> **Vorgänger:** AP06, AP07 ✅ erforderlich +> **Nachfolger:** AP10, AP11 +> **Grundlage:** `docs/specs/technik-und-architektur.md` v5, §§ „Gültigkeitsentscheidung und Exit-Codes", „Laufzeit- und Betriebsmodell" + ## Ziel -Auch bei **Bedien- oder Zugriffsfehlern** (Exit-Code `2`) soll ein **Minimalbericht** entstehen, der den Fehler nachvollziehbar beschreibt. Das verlangt `technik-und-architektur.md` ausdrücklich: „Auch bei Exit-Code 2 soll, soweit technisch möglich, ein Minimalbericht erzeugt werden, der den Bedien- oder Zugriffsfehler nachvollziehbar beschreibt." +Auch bei **Bedien- oder Zugriffsfehlern** (Exit-Code `2`) entsteht ein **Minimalbericht**, der den Fehler nachvollziehbar beschreibt. `technik-und-architektur.md` verlangt das ausdrücklich: „Auch bei Exit-Code 2 soll, soweit technisch möglich, ein Minimalbericht erzeugt werden." ## Voraussetzungen @@ -10,71 +18,69 @@ Auch bei **Bedien- oder Zugriffsfehlern** (Exit-Code `2`) soll ein **Minimalberi ## Scope IN -### Bedienfehler-Fälle -Alle diese Fälle müssen in Exit-Code `2` mit Minimalbericht resultieren: +### Bedienfehler-Fälle (alle müssen Exit-Code `2` + Minimalbericht ergeben) -1. **Kein Argument übergeben** → Minimalbericht auf Konsole (Eingabeverzeichnis unbekannt, also keine Dateiausgabe möglich) -2. **Mehr als ein Argument** → Minimalbericht auf Konsole -3. **Eingabedatei existiert nicht** → Minimalbericht auf Konsole und falls möglich in das übergeordnete Verzeichnis (dort wo die Datei hätte liegen sollen), sonst nur Konsole -4. **Eingabepfad ist kein regulärer Dateityp** (z.B. Verzeichnis) → Minimalbericht auf Konsole, keine Dateiausgabe -5. **Eingabedatei ist nicht lesbar** (Permissions) → Minimalbericht auf Konsole, Dateiausgabe wird versucht wenn Zielverzeichnis schreibbar ist, sonst nur Konsole +1. **Kein Argument** → nur Konsolenausgabe (Eingabeverzeichnis unbekannt) +2. **Mehr als ein Argument** → nur Konsolenausgabe +3. **Eingabedatei existiert nicht** → Konsolenausgabe + Berichtdatei im übergeordneten Verzeichnis, sofern dieses schreibbar ist +4. **Eingabepfad ist kein regulärer Dateityp** (z.B. Verzeichnis) → nur Konsolenausgabe +5. **Eingabedatei ist nicht lesbar** (Berechtigungen) → Konsolenausgabe + Berichtdatei sofern Zielverzeichnis schreibbar ### Minimalbericht-Inhalt -Der Minimalbericht ist ein **`ValidationReport`** (aus AP05) mit: -- `fileName` = übergebener Pfad (oder Platzhalter `` bzw. ``) + +Ein `ValidationReport` (AP05) via `ValidationReport.operationalError(...)` mit: +- `fileName` = übergebener Pfad (oder `` / ``) - `timestamp` = jetzt -- genau ein `Finding`: - - `kind = SPEC` +- Genau ein `Finding`: + - `kind = SPEC` → Verdict wird `OPERATIONAL_ERROR` - `severity = ERROR` - `layer = ARTIFACT` - - `ruleId = "OPERATIONAL-"` (z.B. `OPERATIONAL-MISSING-ARG`, `OPERATIONAL-FILE-NOT-FOUND`, `OPERATIONAL-NOT-READABLE`, `OPERATIONAL-NOT-REGULAR`, `OPERATIONAL-TOO-MANY-ARGS`) - - `germanMessage` = kurzer, verständlicher deutscher Text -- **wichtig:** das Verdict dieses Reports ist **nicht** `INVALID`, sondern `OPERATIONAL_ERROR`. Die `Verdict`-Ableitungslogik muss in AP05 bereits den Fall „nur OPERATIONAL-Findings" erkennen können — falls das in AP05 noch nicht vorgesehen wurde, muss `ValidationReport` hier minimal erweitert werden. -- Alternative, sauberere Modellierung: ein zusätzlicher Konstruktor oder Factory-Methode `ValidationReport.operationalError(String fileName, String ruleId, String message)`, der den Verdict-Status explizit auf `OPERATIONAL_ERROR` setzt. + - `ruleId` = z.B. `OPERATIONAL-MISSING-ARG`, `OPERATIONAL-FILE-NOT-FOUND`, `OPERATIONAL-NOT-READABLE`, `OPERATIONAL-NOT-REGULAR`, `OPERATIONAL-TOO-MANY-ARGS` + - `germanMessage` = kurzer verständlicher deutscher Text -### Dateiausgabe bei Exit-Code 2 -- wenn das Zielverzeichnis ermittelbar und schreibbar ist: Berichtdatei wird gemäß AP07-Logik erzeugt -- wenn nicht: **nur Konsolenausgabe**, keine Fehlermeldung („Bericht konnte nicht geschrieben werden"), weil das den Benutzer doppelt verunsichert -- eine kurze Hinweiszeile auf der Konsole ist okay: „Bericht konnte nicht in das Verzeichnis geschrieben werden, siehe Konsolenausgabe oben." +### Wichtige Regeln -### Logging -- der Minimalbericht wird zusätzlich **geloggt** (`logger.error(...)`) — damit in der Log-Datei (sofern erzeugt) dokumentiert ist, was schiefging -- bei Fall 1 und 2 (keine Dateipfadinformation) ist die Log-Datei nicht sinnvoll zu platzieren → Fallback auf `log4j2.xml`-Default aus AP04 +- **Kein Stack-Trace für den Nutzer** — technische Details gehören ins Log, nicht in den Bericht +- Wenn Zielverzeichnis nicht schreibbar: **nur Konsolenausgabe**, kein Fehler-auf-Fehler +- Eine kurze Hinweiszeile auf Konsole ist okay: „Bericht konnte nicht in das Verzeichnis geschrieben werden." +- Der Bedienfehler wird zusätzlich geloggt (`logger.error(...)`) + +### Tests + +- Unit-Test für alle fünf Fälle: Exit-Code `2` + korrekter `ruleId`-Wert +- Negativ-Test: kein Stack-Trace in der Ausgabe +- Test: Fall 3 (Datei nicht vorhanden) → Berichtdatei im übergeordneten Verzeichnis, wenn schreibbar ## Scope OUT -- Unterscheidung zwischen verschiedenen IO-Exceptions im Detail (`FileSystemException`, `AccessDeniedException`, …) — ein einheitlicher Fall „nicht lesbar" reicht +- Unterscheidung feingranularer IO-Exceptions (`AccessDeniedException`, `FileSystemException` etc.) — ein einheitlicher Fall „nicht lesbar" reicht - Internationalisierung - Exit-Codes jenseits von `0/1/2` -- Behandlung von `OutOfMemoryError`, `StackOverflowError` etc. ## Schritte -1. Branch `m1/ap08-minimalbericht` -2. Factory-Methode `ValidationReport.operationalError(...)` in AP05-Modell ergänzen (falls noch nicht vorhanden) -3. `CliRunner` um die fünf Bedienfehler-Fälle erweitern; pro Fall wird der passende Minimalbericht erzeugt -4. `ReportFileWriter`: im OPERATIONAL-Fall weichere IO-Fehlerbehandlung (keine `RuntimeException`, stattdessen Konsolenhinweis) -5. Unit-Tests für alle fünf Fälle -6. End-to-End-Test: `java -jar ... /pfad/zu/nichtvorhandener/datei.auf` erzeugt auf Konsole einen Minimalbericht und Exit-Code `2` -7. `mvn clean verify` grün -8. Commit `M1-AP08: Minimalbericht bei Exit-Code 2` -9. Abschlussbericht schreiben +1. `ValidationReport.operationalError(...)` prüfen — falls noch nicht vollständig in AP05 implementiert, hier ergänzen +2. `CliRunner` um alle fünf Bedienfehler-Fälle erweitern +3. `ReportFileWriter`: im `OPERATIONAL_ERROR`-Fall weichere IO-Fehlerbehandlung (kein `RuntimeException`, stattdessen Konsolenhinweis) +4. Unit-Tests für alle fünf Fälle +5. `mvn clean verify` grün bekommen +6. Abschlussbericht schreiben ## Abnahmekriterien -- alle fünf Bedienfehler-Fälle erzeugen einen Minimalbericht (per Unit-Test belegt) -- Exit-Code in allen fünf Fällen ist `2` -- Im Fall „Eingabedatei existiert nicht" wird der Minimalbericht in das übergeordnete Verzeichnis geschrieben, sofern dieses schreibbar ist -- Im Fall „kein Argument" wird der Minimalbericht **nur** auf Konsole ausgegeben (keine Dateiausgabe) -- `Verdict.OPERATIONAL_ERROR` ist in mindestens einem Test verifiziert -- `mvn clean verify` ist grün -- Abschlussbericht liegt vor +- [ ] Alle fünf Bedienfehler-Fälle erzeugen Exit-Code `2` (per Unit-Test belegt) +- [ ] Fall „kein Argument" → **nur** Konsolenausgabe, keine Dateiausgabe +- [ ] Fall „Datei nicht vorhanden" → Berichtdatei im übergeordneten Verzeichnis, sofern schreibbar +- [ ] `Verdict.OPERATIONAL_ERROR` ist in mindestens einem Test verifiziert +- [ ] Kein Stack-Trace in STDERR (Negativ-Test vorhanden) +- [ ] `mvn clean verify` grün +- [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP08-bericht.md` ## Rest-Risiken und offene Punkte -- Im Fall „Eingabedatei existiert nicht, Zielverzeichnis aber schon" ist der Bericht-Basisname der **angegebene** Dateiname (obwohl die Datei nicht existiert). Das ist okay — der Benutzer findet den Bericht dort, wo er die Datei erwartet hätte. -- Die Unterscheidung zwischen `OPERATIONAL_ERROR` und `INVALID` im Verdict ist wichtig für spätere Reporting-Logik. Falls sich herausstellt, dass `OPERATIONAL_ERROR` als separater Verdict-Wert zu Komplikationen führt, kann alternativ ein Boolean-Flag `isOperational` auf dem Report verwendet werden. Entscheidung im Bericht dokumentieren. +- Im Fall „Datei nicht vorhanden, Zielverzeichnis aber existiert" ist der Basisname der **angegebene** Dateiname — der Nutzer findet den Bericht dort, wo er die Datei erwartet hätte. +- Falls `Verdict.OPERATIONAL_ERROR` als separater Wert zu Komplikationen führt: Boolean-Flag `isOperational` auf dem Report ist zulässiger Ausweg — Entscheidung im Bericht dokumentieren. ## Bericht -`docs/arbeitspakete/m1/berichte/AP08-bericht.md` nach `templates/ap-bericht.md`. +`docs/arbeitspakete/m1/berichte/AP08-bericht.md` nach `docs/arbeitspakete/m1/templates/ap-bericht.md`. diff --git a/docs/arbeitspakete/m1/AP09-altlogik-einfrieren.md b/docs/arbeitspakete/m1/AP09-altlogik-einfrieren.md index 8f2bb09..bbbc78f 100644 --- a/docs/arbeitspakete/m1/AP09-altlogik-einfrieren.md +++ b/docs/arbeitspakete/m1/AP09-altlogik-einfrieren.md @@ -1,86 +1,121 @@ -# AP09 – Altlogik aus M1 entkoppeln (Parser/Validator einfrieren) +--- +model: sonnet +--- +# AP09 – Altlogik einfrieren (Preview-Code deaktivieren) + +> **Meilenstein:** M1 +> **Vorgänger:** AP03, AP05, AP06 ✅ erforderlich +> **Nachfolger:** AP10, AP11 +> **Grundlage:** AP00-ist-analyse.md §§ 7, 8 +> **Entscheidungsprotokoll:** `docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md` (E-01 Option b, E-03 leere Testklasse) ## Ziel -Die **bereits vorhandene Parser- und Validator-Logik** aus der früheren Implementierung (vor dieser M1-Planung) wird **nicht gelöscht**, aber sauber **entkoppelt** vom aktiven M1-Lauf. Sie wird als „Vorbau für M3 und folgende" explizit markiert und ist während M1 **nicht** Bestandteil der aktiven Verarbeitungskette. +Die bestehende Preview-Parser- und Validator-Logik (`DefaultStructureValidator`, `DefaultFieldValidator`, `DefaultInputFileValidator`) wird **nicht gelöscht und nicht verschoben**, sondern im Bootstrap **nicht mehr verdrahtet**. Ein M1-Lauf erzeugt keine fachlichen ASVREC-/ASVFEH-Befunde mehr. Die Klassen bleiben physisch an ihrem Ort als M3-Vorbau. -Hintergrund: Der ursprüngliche Stand im Repository enthält bereits `DefaultInputFileParser`, `DefaultSegmentLineTokenizer`, `DefaultStructureValidator`, `DefaultFieldValidator`, `DefaultInputFileValidator` und `validation.model.ValidationResult`. Das ist wertvoll und darf nicht verloren gehen — gehört aber fachlich in M3 (Parser), M5 (Feldregeln) und M6 (Beziehungen), nicht in M1. +## Hintergrund und Entscheidung + +Laut AP00-Ist-Analyse läuft `DefaultStructureValidator` (19 ASVREC/ASVFEH-Regeln) aktiv im produktiven Lauf mit — was dem M1-Ziel „noch keine ASV-Fachvalidierung" widerspricht. + +**Entscheidung E-01: Option (b)** — Klassen bleiben in ihren Paketen, werden aber im Bootstrap nicht mehr verdrahtet. Für M3 kann die Verdrahtung direkt wieder aktiviert werden. + +Kein Paketumzug, keine Umbenennung, kein `@Deprecated`. ## Voraussetzungen -- AP03 (Migration), AP05 (neues Befundmodell), AP06 (neuer Bootstrap/CLI) +- AP03 (Paketstruktur) +- AP05 (neues Befundmodell) +- AP06 (neuer Bootstrap/CLI) ## Scope IN -### Einfrieren statt Löschen -- Die bestehenden Klassen bleiben **vollständig erhalten**, inklusive Tests -- Sie werden in ein klar erkennbares Unterpaket verschoben, z.B.: - ``` - de.gecheckt.asv.legacy.parser - de.gecheckt.asv.legacy.validation - de.gecheckt.asv.legacy.model - ``` - oder alternativ: - ``` - de.gecheckt.asv.application.preview - ``` - Die Entscheidung zwischen `legacy` und `preview` wird im Bericht dokumentiert und begründet. Empfehlung: **`preview`**, weil „legacy" suggeriert, dass etwas alt und zu entsorgen ist — tatsächlich wird der Code in M3/M5/M6 weiterverwendet. -- Jedes Paket bekommt ein `package-info.java` mit deutlichem Hinweis: - ``` - Diese Klassen stammen aus einer früheren Implementierung und sind - für die Meilensteine M3 bis M6 vorgesehen. Sie sind in M1 nicht Teil - der aktiven Validierungskette. Änderungen an diesen Klassen während - M1 sind zu vermeiden. - ``` +### 1. Bootstrap-Verdrahtung anpassen -### Entkopplung vom Lauf -- `CliRunner` und `Bootstrap` dürfen die Preview-Klassen **nicht** aufrufen -- der aktive M1-Lauf verwendet ausschließlich den Dummy-Pfad aus AP06 (Datei einlesen, leeren `ValidationReport` erzeugen) -- die alten Tests der Preview-Klassen laufen **weiterhin grün mit**, damit der Code nicht verrottet -- `validation.model.ValidationResult` (alt) bleibt im Preview-Paket und wird **nicht** mit dem neuen `ValidationReport` aus AP05 verwechselt +In `bootstrap.Main`: `DefaultInputFileValidator` erhält statt `DefaultStructureValidator` und `DefaultFieldValidator` jeweils eine **Null-Implementation** — eine leere Implementierung der jeweiligen Interfaces, die keine Befunde produziert. -### Saubere Kennzeichnung -- In `README.md` des Repos (falls vorhanden, sonst anlegen) ein kurzer Abschnitt „Preview-Code" mit Verweis auf `docs/arbeitspakete/m1/AP09-altlogik-einfrieren.md` -- Kein `@Deprecated`! Deprecated würde bedeuten „wird entfernt" — das Gegenteil ist der Fall. +Die Null-Implementierungen können als benannte Klassen in `bootstrap` oder `application` angelegt werden: + +```java +/** M1-Platzhalter. Ab M3 durch DefaultStructureValidator ersetzen. */ +public final class NoOpStructureValidator implements StructureValidator { + @Override + public List validate(ValidationContext ctx) { + return List.of(); // bewusst leer in M1 + } +} +``` + +Analog für `NoOpFieldValidator`. + +### 2. Einfriermarker als JavaDoc-Kommentar + +`DefaultStructureValidator` und `DefaultFieldValidator` erhalten folgenden JavaDoc-Kommentar — keine andere Änderung: + +```java +/** + * M3-Vorbau. In M1 bewusst nicht im produktiven Lauf verdrahtet. + * Wird ab M3 wieder aktiviert und gegen die finalen Regelklassifikationen + * (V1-V/T/N/K) aus fachliche-anforderungen.md bewertet. + * + * @see docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md E-01 + */ +``` + +### 3. Abnahmetest + +Ein Integrationstest prüft, dass ein Lauf mit einer beliebigen Testdatei **keine** fachlichen Findings mit Bezug auf ASVREC-/ASVFEH-Segmentregeln erzeugt. + +### 4. Aufräumen + +- `DefaultStructureValidatorTestAdditional` (leere Testklasse ohne `@Test`-Methoden) löschen (gemäß E-03) +- Sicherstellen dass `logs/` in `.gitignore` steht (falls AP06 das noch nicht erledigt hat) +- Aktive Tests von `DefaultStructureValidator` und `DefaultFieldValidator` bleiben **erhalten und grün** — sie testen den M3-Vorbau + +### 5. Grep-Nachweis im Bericht + +```bash +grep -rn "DefaultStructureValidator\|DefaultFieldValidator" \ + src/main/java/de/gecheckt/asv/adapter \ + src/main/java/de/gecheckt/asv/bootstrap +``` + +Muss **leer** sein — der aktive Code darf diese Klassen nicht mehr referenzieren. ## Scope OUT -- Weiterentwicklung der Preview-Klassen -- Änderung der Preview-Tests (außer notwendige Import-Anpassungen durch den Package-Umzug) -- Integration der Preview-Klassen in die neue `domain.finding`-Struktur (das ist explizit M3+) -- Löschung von Preview-Klassen, auch wenn sie wie Duplikate wirken +- Paketumzug der Preview-Klassen (explizit **nicht** — Entscheidung E-01 Option b) +- Inhaltliche Änderung an `DefaultStructureValidator` oder `DefaultFieldValidator` +- Fachliche Neubewertung der 19 Preview-Regeln (das ist M3) +- Löschen von Preview-Klassen ## Schritte -1. Branch `m1/ap09-preview-einfrieren` -2. Zielpaket wählen (`preview` empfohlen) und im Bericht begründen -3. Alle Parser-Klassen verschieben -4. Alle Validator-Klassen verschieben -5. `validation.model.ValidationResult` und `validation.model.*` mit verschieben -6. Tests entsprechend verschieben; Imports anpassen -7. `CliRunner`/`Bootstrap` auf Preview-Imports prüfen — **darf keine haben**, sonst entkoppeln -8. `package-info.java` mit Warnhinweis in jedem Preview-Unterpaket anlegen -9. README-Abschnitt „Preview-Code" ergänzen -10. `mvn clean verify` grün bekommen (alle Tests der Preview-Klassen laufen weiter mit) -11. Commit `M1-AP09: Alt-Parser und Alt-Validator nach preview-Paket, vom M1-Lauf entkoppelt` -12. Abschlussbericht schreiben +1. `NoOpStructureValidator` und `NoOpFieldValidator` anlegen +2. `bootstrap.Main` umverdrahten: Preview-Validatoren durch NoOp-Implementierungen ersetzen +3. JavaDoc-Einfriermarker in `DefaultStructureValidator` und `DefaultFieldValidator` ergänzen +4. `DefaultStructureValidatorTestAdditional` löschen +5. Grep-Nachweis ausführen und im Bericht dokumentieren +6. Integrationstest: Lauf erzeugt keine ASVREC-/ASVFEH-Segmentbefunde +7. `mvn clean verify` grün — alle bisherigen Tests der Preview-Klassen müssen weiterhin grün sein +8. Abschlussbericht schreiben ## Abnahmekriterien -- alle ursprünglich vorhandenen Parser- und Validator-Klassen liegen im Preview-Paket -- alle zugehörigen Tests laufen weiterhin grün -- `grep -rn "de.gecheckt.asv.preview" src/main/java/de/gecheckt/asv/adapter src/main/java/de/gecheckt/asv/bootstrap src/main/java/de/gecheckt/asv/application` ist **leer** (keine Referenzen aus dem aktiven Code) -- `package-info.java` mit Warnhinweis in jedem Preview-Unterpaket -- README enthält Abschnitt „Preview-Code" -- keine Klasse wurde gelöscht (`git log --diff-filter=D` für diesen Commit zeigt nur Verschiebungen) -- `mvn clean verify` ist grün -- Abschlussbericht liegt vor +- [ ] `NoOpStructureValidator` und `NoOpFieldValidator` existieren +- [ ] `bootstrap.Main` verdrahtet keine Preview-Validatoren mehr +- [ ] Grep auf `DefaultStructureValidator`/`DefaultFieldValidator` in `adapter` und `bootstrap` ist leer (Nachweis im Bericht) +- [ ] Einfriermarker-JavaDoc in beiden Preview-Klassen vorhanden +- [ ] `DefaultStructureValidatorTestAdditional` ist gelöscht +- [ ] Bestehende Tests der Preview-Klassen laufen weiterhin grün +- [ ] Integrationstest: Lauf mit Testdatei erzeugt keine ASVREC-/ASVFEH-Segmentbefunde +- [ ] `mvn clean verify` grün +- [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP09-bericht.md` ## Rest-Risiken und offene Punkte -- Bei Wiederaufnahme in M3 wird zu klären sein, wie der Preview-Code an das neue Befundmodell angebunden wird. Das ist explizit M3-Aufgabe, nicht M1. -- Falls die Preview-Tests beim Package-Umzug brechen (wegen relativer Ressourcenpfade o.ä.), müssen sie einmalig angepasst werden. Das ist kein Scope-Verstoß, sondern Teil des Umzugs. +- Bei Wiederaufnahme in M3: jede der 19 Preview-Regeln ist neu gegen V1-V/T/N/K-Klassifikation zu bewerten. Explizit M3-Aufgabe. +- Falls Preview-Tests beim Einfrieren brechen (z.B. wegen Interface-Änderungen durch AP05): einmalige Anpassung der Testimports ist kein Scope-Verstoß, sondern Teil der Konsolidierung. ## Bericht -`docs/arbeitspakete/m1/berichte/AP09-bericht.md` nach `templates/ap-bericht.md`. +`docs/arbeitspakete/m1/berichte/AP09-bericht.md` nach `docs/arbeitspakete/m1/templates/ap-bericht.md`. diff --git a/docs/arbeitspakete/m1/AP10-architekturtest.md b/docs/arbeitspakete/m1/AP10-architekturtest.md index 433a516..a9ca08a 100644 --- a/docs/arbeitspakete/m1/AP10-architekturtest.md +++ b/docs/arbeitspakete/m1/AP10-architekturtest.md @@ -1,26 +1,42 @@ +--- +model: sonnet +--- # AP10 – Architekturtest +> **Meilenstein:** M1 +> **Vorgänger:** AP04, AP09 ✅ erforderlich (alle AP05–AP09 sollten abgeschlossen sein) +> **Nachfolger:** AP11 +> **Grundlage:** `docs/specs/technik-und-architektur.md` v5, §§ „Architekturprinzipien", „Test- und Qualitätsanforderungen" +> **Entscheidungsprotokoll:** `docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md` (E-02 Test-Log, E-03 leere Testklasse) + ## Ziel -Ein **automatisierter Architekturtest** stellt sicher, dass die in M1 etablierten Strukturregeln auch in Zukunft eingehalten werden. Insbesondere darf: - -1. die **Log4j2-Bindung** nur in `adapter.out.logging` und `bootstrap` sichtbar sein -2. das `domain`-Paket **nichts** aus Adaptern oder Infrastruktur importieren -3. das `application`-Paket **nichts** aus konkreten Adaptern importieren, sondern nur aus `domain` und eigenen Ports -4. **Preview-Code** nicht aus dem aktiven Code (Bootstrap, CLI-Adapter, Application) referenziert werden +Automatisierte Architekturtests sichern die in M1 etablierten Strukturregeln dauerhaft ab. Zusätzlich wird das Build-Rauschen durch ERROR-Log-Zeilen in Negativ-Tests beseitigt. ## Voraussetzungen -- AP04 (Logging-Adapter), AP09 (Preview eingefroren) +- AP04 (Logging-Adapter etabliert) +- AP09 (Preview-Code eingefroren) +- Idealerweise alle AP05–AP09 abgeschlossen ## Scope IN -### Technische Umsetzung -- **ArchUnit** (`com.tngtech.archunit:archunit-junit5`) als Test-Dependency aufnehmen -- neue Test-Klasse `ArchitectureTest` im Paket `de.gecheckt.asv` im Testbereich -- vier Tests: +### 1. ArchUnit als Test-Dependency -### Test 1: Log4j2-Sichtbarkeit +```xml + + com.tngtech.archunit + archunit-junit5 + + test + +``` + +### 2. Architekturtest-Klasse + +Neue Testklasse `de.gecheckt.asv.ArchitectureTest` im Testbereich. Mindestens vier Regeln: + +**Regel A — Log4j2-Sichtbarkeit:** ```java @ArchTest static final ArchRule log4j2_nur_in_logging_adapter_und_bootstrap = @@ -33,7 +49,7 @@ static final ArchRule log4j2_nur_in_logging_adapter_und_bootstrap = .because("Log4j2 darf nur im Logging-Adapter und im Bootstrap sichtbar sein."); ``` -### Test 2: Domain ist frei +**Regel B — Domain-Reinheit:** ```java @ArchTest static final ArchRule domain_hat_keine_adapter_abhaengigkeit = @@ -42,11 +58,10 @@ static final ArchRule domain_hat_keine_adapter_abhaengigkeit = .should().dependOnClassesThat() .resideInAnyPackage( "de.gecheckt.asv.adapter..", - "de.gecheckt.asv.bootstrap..", - "de.gecheckt.asv.preview.."); + "de.gecheckt.asv.bootstrap.."); ``` -### Test 3: Application ist frei von konkreten Adaptern +**Regel C — Application-Reinheit:** ```java @ArchTest static final ArchRule application_kennt_keine_adapter_implementierungen = @@ -58,53 +73,82 @@ static final ArchRule application_kennt_keine_adapter_implementierungen = "de.gecheckt.asv.bootstrap.."); ``` -### Test 4: Preview wird nicht referenziert +**Regel D — Preview-Isolation:** ```java @ArchTest static final ArchRule preview_wird_nicht_aus_aktivem_code_referenziert = noClasses() .that().resideInAnyPackage( "de.gecheckt.asv.adapter..", - "de.gecheckt.asv.application..", - "de.gecheckt.asv.bootstrap..", - "de.gecheckt.asv.domain..") + "de.gecheckt.asv.bootstrap..") .should().dependOnClassesThat() - .resideInAPackage("de.gecheckt.asv.preview..") - .because("Preview-Code ist aus M1-Sicht eingefroren und wird erst ab M3 aktiv verwendet."); + .haveSimpleNameContaining("DefaultStructureValidator") + .orShould().dependOnClassesThat() + .haveSimpleNameContaining("DefaultFieldValidator") + .because("Preview-Validatoren sind in M1 eingefroren und werden erst ab M3 aktiv verwendet."); ``` -### Zusätzlich: Paketstruktur-Check -- Prüfen, dass die Soll-Pakete aus `technik-und-architektur.md` tatsächlich existieren (als ArchUnit-Regel oder einfacher Dateisystem-Test) +**Wichtig:** Wenn beim ersten Lauf Regeln rot sind, **müssen die Verstöße behoben werden** — die Regeln werden nicht abgeschwächt. + +### 3. Test-Log-Konfiguration (E-02) + +`src/test/resources/log4j2-test.xml` anlegen: + +```xml + + + + + + + + + + + + + +``` + +Log4j2 bevorzugt `log4j2-test.xml` im Test-Classpath gegenüber `log4j2.xml`. Das unterdrückt die erwartete ERROR-Zeile aus dem CLI-Negativ-Test. + +### 4. Aufräumen + +- Prüfen ob `DefaultStructureValidatorTestAdditional` bereits in AP09 gelöscht wurde — falls nicht, hier löschen +- Sicherstellen dass keine weiteren leeren Testklassen im Projekt existieren ## Scope OUT -- komplexere Regeln wie „keine zyklischen Abhängigkeiten zwischen Paketen" — wäre schön, ist aber für M1 zu weitgehend -- Regeln zu Klassenbenennung -- Regeln zu `public`-Sichtbarkeit -- Tests für Preview-internen Aufbau +- Komplexe Regeln wie zyklische Abhängigkeiten — für M1 zu weitgehend +- Regeln zu Klassenbenennung oder `public`-Sichtbarkeit +- Coverage- und Mutation-Schwellwerte (kommen erst in M9) +- Neue Produktionsklassen ## Schritte -1. Branch `m1/ap10-architekturtest` -2. ArchUnit in `pom.xml` als Test-Dependency aufnehmen -3. `ArchitectureTest`-Klasse im Testbereich anlegen -4. Die vier Regeln implementieren -5. `mvn clean verify` laufen lassen — die Tests müssen **grün** sein. Falls rot: **das heißt, eine frühere M1-Phase hat die Regel verletzt**. Die Verletzung muss gefunden und behoben werden (kein Entschärfen der Regel!). -6. Commit `M1-AP10: Architekturtest für Log4j2-Sichtbarkeit, Paketabhängigkeiten, Preview-Isolation` +1. ArchUnit in `pom.xml` als Test-Dependency aufnehmen +2. `ArchitectureTest`-Klasse mit den vier Regeln implementieren +3. `log4j2-test.xml` unter `src/test/resources/` anlegen +4. `mvn clean verify` ausführen — alle vier ArchUnit-Regeln müssen grün sein +5. Falls Regeln rot: Verstöße identifizieren, beheben, erneut testen +6. Im Bericht dokumentieren ob beim ersten Lauf Regeln rot waren und wie behoben 7. Abschlussbericht schreiben ## Abnahmekriterien -- ArchUnit ist als Test-Dependency eingebunden -- vier Architektur-Regeln sind implementiert und grün -- `mvn clean verify` ist grün -- Abschlussbericht liegt vor und dokumentiert, ob beim ersten Lauf Regeln rot waren und wenn ja, wie sie behoben wurden +- [ ] ArchUnit als Test-Dependency in `pom.xml` +- [ ] Vier Architekturregeln A–D implementiert und grün +- [ ] `log4j2-test.xml` unter `src/test/resources/` vorhanden +- [ ] Keine leeren Testklassen im Projekt +- [ ] `mvn clean verify` grün, kein unerwartetes Log-Rauschen +- [ ] Bericht dokumentiert ob erste Ausführung Regeln rot hatte und wie behoben +- [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP10-bericht.md` ## Rest-Risiken und offene Punkte -- ArchUnit hat beim ersten Einsatz manchmal Überraschungen mit transitiven Abhängigkeiten (Regel greift auch auf Framework-Klassen, die nicht gemeint waren). In dem Fall: Regel präzisieren, **nicht** ausschalten. -- Die Regel „Log4j2-Sichtbarkeit" schließt auch den Bootstrap mit ein. Wenn der Bootstrap später (M8) Krypto-Typen referenziert, müssen analoge Regeln ergänzt werden — aber das ist M8-Thema. +- ArchUnit hat beim ersten Einsatz manchmal Überraschungen mit transitiven Abhängigkeiten (Regel greift auch auf Framework-Klassen). In dem Fall: Regel präzisieren, **nicht** ausschalten. +- Regel D (Preview-Isolation) ist auf Klassennamen-Basis formuliert, weil kein separates Paket existiert (Option b). Falls das zu fragil ist, kann alternativ auf Package-Ebene mit einem `preview`-Paket geprüft werden — aber nur wenn AP09 entsprechend umgebaut wurde. ## Bericht -`docs/arbeitspakete/m1/berichte/AP10-bericht.md` nach `templates/ap-bericht.md`. +`docs/arbeitspakete/m1/berichte/AP10-bericht.md` nach `docs/arbeitspakete/m1/templates/ap-bericht.md`. diff --git a/docs/arbeitspakete/m1/AP11-m1-abnahme.md b/docs/arbeitspakete/m1/AP11-m1-abnahme.md index 7a767ef..0de5b35 100644 --- a/docs/arbeitspakete/m1/AP11-m1-abnahme.md +++ b/docs/arbeitspakete/m1/AP11-m1-abnahme.md @@ -1,92 +1,107 @@ +--- +model: sonnet +--- # AP11 – M1-Abnahme +> **Meilenstein:** M1 +> **Vorgänger:** AP01–AP10 alle ✅ erforderlich +> **Nachfolger:** M2 +> **Grundlage:** `docs/specs/meilensteine.md` v3, M1-Abnahmekriterien + ## Ziel -Der letzte Schritt in M1: Alles wird gegen die Meilenstein-Abnahmekriterien aus `docs/specs/meilensteine.md` v3 **geprüft**, ein **End-to-End-Lauf** mit einer Minimal-Eingabedatei wird durchgeführt, und alle AP-Berichte werden in einem **konsolidierten M1-Abschlussbericht** zusammengeführt. +M1 wird formal abgenommen. Alle Abnahmekriterien aus `meilensteine.md` sind erfüllt und nachweisbar. Das Projekt ist bereit für M2. ## Voraussetzungen -- AP01 bis AP10 abgeschlossen und grün -- alle AP-Berichte liegen in `docs/arbeitspakete/m1/berichte/` vor +- AP01–AP10 abgeschlossen und grün +- Alle AP-Berichte liegen in `docs/arbeitspakete/m1/berichte/` vor ## Scope IN -### End-to-End-Lauf -1. **Minimaldatei erstellen**: eine einfache Dummy-Textdatei im ISO-8859-15-Encoding, z.B. `test-artefakte/m1/minimal.txt` mit ein paar Zeilen Inhalt. Keine echten ASV-Daten, kein gültiges EDIFACT — dies ist nur ein Lauftest, kein Fachtest. -2. **JAR bauen**: `mvn clean package` -3. **Lauf 1**: `java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar test-artefakte/m1/minimal.txt` - - **Erwartung:** Exit-Code `0` (Dummy-Pfad, leerer Report), Berichtdatei `minimal.txt.txt` und Log-Datei `minimal.txt.log` entstehen, Konsolenausgabe vorhanden -4. **Lauf 2**: identischer Aufruf - - **Erwartung:** `minimal.txt_v1.txt` und `minimal.txt_v1.log` entstehen -5. **Lauf 3**: `java -jar ... nicht-vorhanden.txt` - - **Erwartung:** Exit-Code `2`, Minimalbericht auf Konsole, gegebenenfalls Berichtdatei im übergeordneten Verzeichnis wenn schreibbar -6. **Lauf 4**: `java -jar ...` (ohne Argument) - - **Erwartung:** Exit-Code `2`, Minimalbericht **nur** auf Konsole -7. **Lauf 5**: `java -jar ... datei1.txt datei2.txt` - - **Erwartung:** Exit-Code `2`, Minimalbericht auf Konsole +### 1. Test-Artefakt anlegen -Alle fünf Läufe werden im M1-Abschlussbericht dokumentiert (Befehl, Exit-Code, relevante Ausgabe). +`test-artefakte/m1/minimal.txt` anlegen — eine einfache ISO-8859-15-kompatible Textdatei mit 3–5 Zeilen Dummy-Inhalt. Keine echten ASV-Daten, kein gültiges EDIFACT — reiner Lauftest. -### Meilenstein-Abnahmeprüfung -Jeder Abnahmepunkt aus `docs/specs/meilensteine.md` v3 Abschnitt „Abnahme von M1" wird mit einem konkreten Nachweis verknüpft: +### 2. End-to-End-Läufe durchführen -| M1-Abnahmekriterium | Nachweis | Status | +`mvn clean package` ausführen, dann alle fünf Läufe: + +| Lauf | Befehl | Erwarteter Exit-Code | Erwartetes Verhalten | +|---|---|---|---| +| 1 | `java -jar target/asv-format-validator-*.jar test-artefakte/m1/minimal.txt` | `0` | `.txt` und `.log` entstehen im Verzeichnis | +| 2 | identischer Aufruf wie Lauf 1 | `0` | `_v1.txt` und `_v1.log` entstehen | +| 3 | `java -jar ... nicht-vorhanden.txt` | `2` | Minimalbericht auf Konsole | +| 4 | `java -jar ...` (ohne Argument) | `2` | Minimalbericht nur auf Konsole | +| 5 | `java -jar ... datei1.txt datei2.txt` | `2` | Minimalbericht nur auf Konsole | + +Alle fünf Läufe werden im M1-Abschlussbericht dokumentiert (Befehl, tatsächlicher Exit-Code, relevante Ausgabe). + +### 3. Meilenstein-Abnahmeprüfung + +Jeden Punkt aus `docs/specs/meilensteine.md` v3 §„Abnahme von M1" mit konkretem Nachweis verknüpfen: + +| Kriterium | Nachweis | Status | |---|---|---| -| Anwendung ist als JAR unter Windows mit Java 21 startbar | Lauf 1, JAR-Pfad | ✅ | -| falsches oder fehlendes Argument → Exit-Code `2` mit Minimalbericht | Lauf 3, 4, 5 | ✅ | -| Bericht- und Log-Datei werden im Eingabeverzeichnis mit korrekter Suffix-Logik erzeugt | Lauf 1 + Lauf 2 | ✅ | -| Log4j2-Bindung ist außerhalb von Bootstrap und Logging-Adapter nicht sichtbar | Architekturtest AP10, Test 1 | ✅ | -| Befundmodell unterscheidet Spec-Urteil und diagnostische Weiteranalyse | Unit-Test AP05 | ✅ | -| Build und Tests sind grün | `mvn clean verify` | ✅ | +| Anwendung als JAR unter Windows mit Java 21 startbar | Lauf 1 | | +| Fehlendes/falsches Argument → Exit-Code `2` mit Minimalbericht | Lauf 3, 4, 5 | | +| Bericht- und Log-Datei im Eingabeverzeichnis mit korrekter Suffix-Logik | Lauf 1 + 2 | | +| Log4j2-Bindung außerhalb Bootstrap/Logging-Adapter nicht sichtbar | Architekturtest AP10 Regel A | | +| Befundmodell trennt Spec-Urteil und diagnostische Weiteranalyse | Unit-Test AP05 | | +| Build und Tests grün | `mvn clean verify` | | -### M1-Abschlussbericht -- Datei: `docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md` -- Inhalt: - - **Zusammenfassung**: Was ist M1 geworden, in zwei Absätzen - - **AP-Übersicht**: Tabelle mit allen 11 APs, Status, Commit-Hashes, Abschlussdatum - - **Meilenstein-Abnahmetabelle** (siehe oben) - - **End-to-End-Protokoll**: die fünf Läufe mit Befehl, Exit-Code, Zusammenfassung der Ausgabe - - **Quality-Metriken** (Coverage, Testanzahl — **keine harten Gates in M1**, nur Informationswert) - - **Rest-Risiken und übertragene Punkte** aus allen AP-Berichten konsolidiert - - **Empfehlungen für M2**: Was sollte M2 beachten? Was ist aus M1-Sicht offen geblieben? - - **Commit-Graph-Snapshot**: `git log --oneline --graph main` für den M1-Zeitraum -- Freigabe-Vermerk am Ende: „M1 ist abnahmebereit" oder „M1 ist mit folgenden Einschränkungen abnahmebereit: ..." +### 4. Konsolidierter M1-Abschlussbericht -### Tagging -- Git-Tag `m1-done` auf dem letzten AP11-Commit setzen -- Tag-Message: „Meilenstein 1 abgeschlossen, siehe docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md" +Datei: `docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md` + +Pflichtabschnitte: +- **Zusammenfassung** — Was ist M1 geworden (max. zwei Absätze) +- **AP-Übersicht** — Tabelle mit allen 11 APs, Status, letzter Commit +- **Meilenstein-Abnahmetabelle** (siehe oben, vollständig ausgefüllt) +- **End-to-End-Protokoll** — alle fünf Läufe mit Befehl, Exit-Code, Ausgabe-Zusammenfassung +- **Quality-Metriken** — Testanzahl, Coverage-Wert (informativ, keine Gate-Prüfung in M1) +- **Rest-Risiken** — konsolidiert aus allen AP-Berichten +- **Empfehlungen für M2** — was M2 beachten sollte +- **Freigabe-Vermerk** — „M1 ist abnahmebereit" oder „M1 ist mit folgenden Einschränkungen abnahmebereit: ..." + +### 5. Git-Tag setzen + +```bash +git tag -a m1-done -m "Meilenstein 1 abgeschlossen, siehe docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md" +``` ## Scope OUT -- Vorgriffe auf M2-Themen (Dateinamensschemata, globale Rahmenregeln, ISO-8859-15 über Dateinamen hinaus — außer dem Einlese-Encoding aus AP06, das bleibt) +- Vorgriffe auf M2-Themen - Release-Builds, Signierung, Publizierung -- Externe Reviews (die kommen vom Rezensenten der Arbeitspakete, nicht aus diesem AP) +- Inhaltliche Berichtsvertiefung über M1-Minimum hinaus ## Schritte -1. Branch `m1/ap11-abnahme` -2. Test-Artefakt `test-artefakte/m1/minimal.txt` anlegen (ISO-8859-15, 3–5 Zeilen Dummy-Inhalt) -3. `mvn clean package` ausführen -4. Die fünf Läufe durchführen und protokollieren +1. `test-artefakte/m1/minimal.txt` anlegen +2. `mvn clean package` +3. Alle fünf Läufe durchführen und protokollieren +4. `mvn clean verify` ein letztes Mal 5. Konsolidierten M1-Abschlussbericht schreiben -6. `mvn clean verify` ein letztes Mal laufen lassen -7. Commit `M1-AP11: M1-Abnahme, End-to-End-Protokoll, konsolidierter Abschlussbericht` -8. Git-Tag `m1-done` setzen: `git tag -a m1-done -m "Meilenstein 1 abgeschlossen"` +6. Git-Tag `m1-done` setzen ## Abnahmekriterien -- `test-artefakte/m1/minimal.txt` existiert -- alle fünf Läufe sind protokolliert -- M1-Abschlussbericht existiert und enthält alle oben genannten Abschnitte -- Meilenstein-Abnahmetabelle ist vollständig und jede Zeile hat einen konkreten Nachweis -- `mvn clean verify` ist grün -- Git-Tag `m1-done` ist gesetzt -- der Freigabe-Vermerk am Ende des Abschlussberichts ist explizit +- [ ] `test-artefakte/m1/minimal.txt` existiert +- [ ] Alle fünf Läufe sind protokolliert +- [ ] `M1-abschlussbericht.md` existiert mit allen Pflichtabschnitten +- [ ] Meilenstein-Abnahmetabelle vollständig, jede Zeile mit konkretem Nachweis +- [ ] Kein Exit-Code `3` mehr erreichbar +- [ ] `mvn clean verify` grün +- [ ] Git-Tag `m1-done` gesetzt +- [ ] Freigabe-Vermerk ist explizit +- [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP11-bericht.md` ## Rest-Risiken und offene Punkte -- Dieser AP ist ein reines Zusammenfassungs-AP. Wenn hier Abnahmekriterien nicht erfüllt sind, zeigt das, dass ein früheres AP unvollständig war. In dem Fall: **zurück zum betroffenen AP**, nachbessern, dann AP11 wiederholen. Keine Abkürzungen. +- Wenn hier Abnahmekriterien nicht erfüllt sind, zeigt das, dass ein früheres AP unvollständig war. In dem Fall: **zurück zum betroffenen AP**, nachbessern, dann AP11 wiederholen. Keine Abkürzungen. ## Bericht -`docs/arbeitspakete/m1/berichte/AP11-bericht.md` nach `templates/ap-bericht.md` **zusätzlich** zum konsolidierten `M1-abschlussbericht.md`. +`docs/arbeitspakete/m1/berichte/AP11-bericht.md` nach `docs/arbeitspakete/m1/templates/ap-bericht.md`. +**Zusätzlich** konsolidierter `docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md`. diff --git a/docs/arbeitspakete/m1/M1-orchestrierung-prompt.md b/docs/arbeitspakete/m1/M1-orchestrierung-prompt.md new file mode 100644 index 0000000..4516c91 --- /dev/null +++ b/docs/arbeitspakete/m1/M1-orchestrierung-prompt.md @@ -0,0 +1,57 @@ +# M1-Orchestrierung: AP05 bis AP11 + +Dieser Prompt wird mit `claude --model opusplan` gestartet. + +--- + +Lies vor dem Start vollständig und in dieser Reihenfolge: + +1. `CLAUDE.md` +2. `docs/specs/technik-und-architektur.md` (Überblick — nicht jede Zeile) +3. `docs/specs/meilensteine.md` (nur M1-Abschnitt) +4. `docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md` +5. `docs/arbeitspakete/m1/README.md` +6. Alle AP05- bis AP11-Dateien in `docs/arbeitspakete/m1/` + +--- + +Du bist der Lead-Orchestrator für Meilenstein M1. Deine Aufgabe ist es, +die Arbeitspakete AP05 bis AP11 sequenziell zu orchestrieren. + +Jede AP-Datei enthält `model: sonnet` im Frontmatter — nutze dieses Modell +für die Subagenten die die Implementierung übernehmen. + +## Vorgehen + +Für jedes Arbeitspaket in der unten angegebenen Reihenfolge: + +1. Lies das AP-Dokument vollständig +2. Starte einen Subagenten (Task-Tool) mit dem Auftrag, genau dieses AP + umzusetzen — Subagent-Modell: sonnet +3. Der Subagent folgt den Schritten im AP-Dokument und endet mit + `mvn clean verify` und dem Abschlussbericht +4. Du prüfst nach Abschluss des Subagenten: + - Ist `mvn clean verify` grün? + - Liegt der Abschlussbericht unter `docs/arbeitspakete/m1/berichte/`? + - Sind alle Abnahmekriterien des APs erfüllt? +5. Nur wenn alle drei Punkte erfüllt: weiter zum nächsten AP +6. Wenn ein AP fehlschlägt: analysiere die Ursache, starte einen + Korrektur-Subagenten, prüfe erneut + +## Reihenfolge + +AP05 → AP06 → AP09 → AP07 → AP08 → AP10 → AP11 + +## Harte Regeln (aus CLAUDE.md — gelten für dich und alle Subagenten) + +- Kein `git commit`, kein `git add`, kein `git push` +- Kein Schreiben außerhalb des jeweiligen AP-Scopes +- Abschlussbericht pro AP ist Pflicht +- `mvn clean verify` muss nach jedem AP grün sein + +## Abschluss + +Nach AP11: melde „M1-Orchestrierung abgeschlossen" mit einer Zusammenfassung +welche APs in wie vielen Versuchen abgeschlossen wurden. + +Fange jetzt an. diff --git a/docs/arbeitspakete/m1/berichte/AP00-ist-analyse.md b/docs/arbeitspakete/m1/berichte/AP00-ist-analyse.md new file mode 100644 index 0000000..1224379 --- /dev/null +++ b/docs/arbeitspakete/m1/berichte/AP00-ist-analyse.md @@ -0,0 +1,328 @@ +# AP00 — Ist-Analyse Meilenstein M1 + +> **Bezug:** M1 aus `docs/specs/meilensteine.md` v3, technischer Soll-Rahmen aus `docs/specs/technik-und-architektur.md` v5 +> **Bearbeiter:** Claude Code (claude-opus-4-7), Ist-Analyse auf Auftrag, keine Code-Änderungen +> **Datum:** 2026-04-20 +> **Grundlage:** HEAD auf `main` nach den Commits AP01–AP04 (letzter: `cd6e522 docs: Review-Korrekturen aus Peer-Review anwenden`) +> **Status:** Analyse abgeschlossen, reine Dokumentation + +--- + +## 1. Zusammenfassung + +Der aktuelle M1-Stand ist zu rund einem Drittel umgesetzt. Die **Build-Infrastruktur** (AP02), die **hexagonale Paketstruktur** (AP03) und die **SLF4J-Logging-Fassade** (AP04) sind fertig, `mvn clean verify` ist grün (147 Tests, 0 Fehler). Offen bleiben die fachlich tragenden Teile von M1: das **Befundmodell mit Spec-/Diagnose-Trennung** (AP05), das korrekte **Exit-Code-Modell `0/1/2`** (aktuell 4 Codes `0/1/2/3` mit vertauschter Semantik), der **CLI-/Bootstrap-Zuschnitt** (AP06), die **Ausgabeartefakte** (`_v1/_v2`-Suffixlogik, `.txt`/`.log`, AP07), der **Minimalbericht** bei Bedienfehlern (AP08), das **Einfrieren der Altlogik** (AP09) sowie der **Architekturtest** (AP10). Das Eingabeencoding ist aktuell hartkodiert `UTF-8` statt `ISO-8859-15`, das Hauptartefakt-JAR referenziert eine noch nicht existierende `bootstrap.Main`-Klasse, und der Preview-`DefaultStructureValidator` (19 ASVREC/ASVFEH-Regeln) läuft im produktiven Lauf mit und erzeugt fachliche Befunde, die in M1 noch nicht vorgesehen sind. + +--- + +## 2. Projektstruktur + +### 2.1 Verzeichnisbaum (Wurzel) + +``` +asv-format-validator/ +├── .editorconfig, .gitattributes, .gitignore +├── CLAUDE.md, README.md, Spec.docx +├── pom.xml +├── Apply-ReviewPatches.ps1 +├── docs/ +│ ├── arbeitspakete/m1/{APxx.md, berichte/, templates/} +│ └── specs/{fachliche-anforderungen.md, technik-und-architektur.md, meilensteine.md} +├── logs/ # wurde vom Log4j2-File-Appender im Vorlauf angelegt +├── src/ +│ ├── main/java/... +│ ├── main/resources/log4j2.xml +│ ├── test/java/... +│ └── test/resources/*.asv # Parser-Testartefakte +└── target/ # Maven Build-Output +``` + +### 2.2 Maven-Module + +Ein einziges Modul: `de.gecheckt:asv-format-validator:0.0.1-SNAPSHOT`. Keine Submodule, keine Multi-Module-Struktur — wie vom Soll (M1) gefordert (evolutionär, kein Modulschnitt). + +### 2.3 Java-Pakete (Ist-Stand) + +``` +de.gecheckt.asv +├── domain (package-info.java) +│ └── model +│ ├── Field (record) +│ ├── Segment (record) +│ ├── Message (record) +│ └── InputFile (record) +├── application (package-info.java) +│ ├── InputFileValidator (Interface) +│ ├── DefaultInputFileValidator (Orchestrator) +│ ├── model +│ │ ├── ValidationSeverity (enum INFO/WARNING/ERROR) +│ │ ├── ValidationError (record, 9 Felder) +│ │ └── ValidationResult (final class) +│ ├── field +│ │ ├── FieldValidator (Interface) +│ │ └── DefaultFieldValidator (Preview-Fachlogik) +│ └── structure +│ ├── StructureValidator (Interface) +│ └── DefaultStructureValidator (Preview, 19 ASVREC/ASVFEH-Regeln) +├── adapter +│ ├── in +│ │ └── cli (package-info.java) +│ │ └── AsvValidatorApplication (main, run, parseFile, printUsage) +│ └── out +│ ├── filesystem (package-info.java – leer, AP07-Vorbehalt) +│ ├── parsing (package-info.java) +│ │ ├── InputFileParser (Interface) +│ │ ├── DefaultInputFileParser (ein Message pro Datei) +│ │ ├── SegmentLineTokenizer (Interface) +│ │ ├── DefaultSegmentLineTokenizer (hartes '+') +│ │ └── InputFileParseException +│ ├── reporting (package-info.java) +│ │ └── ValidationResultPrinter (Konsole) +│ └── logging (package-info.java) +│ └── LoggingConfigurator (Stub, AP07-Vorbehalt) +└── bootstrap (package-info.java – leer, AP06-Vorbehalt) +``` + +Noch **nicht** angelegt: `adapter.out.crypto` (planmäßig erst in M8). + +### 2.4 Konfigurationsdateien + +| Datei | Zweck | Status | +|---|---|---| +| `pom.xml` | Maven-Build, Dependencies, Plugins | AP02-gehärtet (SLF4J, JaCoCo, PIT-Profil, Mockito-Agent, `maven-jar-plugin` mit Platzhalter-Main-Class) | +| `src/main/resources/log4j2.xml` | Log4j2-Konfiguration (Console→STDERR, File→`logs/asv-format-validator.log`) | AP04-gesetzt | +| `.editorconfig` | Encoding UTF-8, LF, 4 Spaces | AP02 | +| `.gitattributes` | LF-Policy | AP02 | +| `.classpath`, `.project`, `.settings/` | Eclipse-Projektfiles | Repo-Altbestand, unverändert | + +### 2.5 Testklassen + +21 Testklassen, 147 Tests gesamt (bestätigt durch `mvn clean verify`): + +- `domain/model/*` — 4 Testklassen, 38 Tests (Record-Konstruktoren, Null-Guards) +- `adapter/out/parsing/*` — 2 Testklassen, 11 Tests +- `adapter/out/reporting/*` — 1 Testklasse, 3 Tests +- `adapter/in/cli/*` — 2 Testklassen, 6 Tests (`AsvValidatorApplicationTest`, `…AdditionalTest`) +- `application/*` — 1 Testklasse, 5 Tests (Orchestrator) +- `application/field/*` — 1 Testklasse, 9 Tests +- `application/model/*` — 2 Testklassen, 5 Tests +- `application/structure/*` — 8 Testklassen, 70 Tests (Preview-Regelabdeckung) + +Eine Testklasse fällt auf: `DefaultStructureValidatorTestAdditional` enthält keine `@Test`-Methoden (seit AP01 dokumentiert, Aufräumen ist AP10 zugeordnet). + +--- + +## 3. M1-Abnahmekriterien — Status je Punkt + +Bewertung je Kriterium gegen den tatsächlichen Code (nicht gegen die bisherigen Berichte). + +### 3.1 „Anwendung ist als JAR unter Windows mit Java 21 startbar" + +**🔶 TEILWEISE.** + +Der `pom.xml` konfiguriert zwar `maven-jar-plugin` mit `de.gecheckt.asv.bootstrap.Main` ([pom.xml:126](pom.xml#L126)), doch diese Klasse existiert noch nicht (Paket `bootstrap` enthält ausschließlich `package-info.java`). Ein `mvn clean package` würde ein JAR mit ungültigem Manifest-Eintrag erzeugen; `java -jar asv-format-validator.jar ` schlägt mit `Error: Could not find or load main class de.gecheckt.asv.bootstrap.Main` fehl. Der aktuelle tatsächliche Einstiegspunkt ist [`de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication#main`](src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java:81) — der wird aber vom Manifest nicht referenziert. AP06 muss die `bootstrap.Main`-Klasse anlegen. + +### 3.2 „Eingabedatei wird technisch entgegengenommen; falsches oder fehlendes Argument führt zu Exit-Code `2` mit Minimalbericht" + +**❌ FEHLT** (semantisch falsch und ohne Minimalbericht). + +Kritische Abweichungen ([AsvValidatorApplication.java:36-39](src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java:36)): + +```java +private static final int EXIT_CODE_SUCCESS = 0; +private static final int EXIT_CODE_INVALID_ARGUMENTS = 1; // Soll: 2 +private static final int EXIT_CODE_FILE_ERROR = 2; // Soll: 2 (gleicher Wert, aber semantisch mit invalid args zu vereinen) +private static final int EXIT_CODE_VALIDATION_ERRORS = 3; // Soll: 1 +``` + +Konsequenzen gegen den Soll-Rahmen: +- Fehlendes Argument → Exit `1` (sollte `2`) — `AsvValidatorApplication.java:97` +- Validierungsfehler → Exit `3` (sollte `1`) — `AsvValidatorApplication.java:113` +- Es existiert **vier** Exit-Codes statt der in Spec und `technik-und-architektur.md` vorgeschriebenen **drei** (`0/1/2`). +- Ein **Minimalbericht** bei Bedienfehlern wird nicht erzeugt. Bei fehlenden Argumenten ruft `printUsage()` nur `System.out.println(…)` auf; bei nicht lesbarer Datei wird `System.err.println("Fehler beim …")` geschrieben. Beide Wege erzeugen weder Berichtdatei noch strukturierten Text. + +### 3.3 „Bericht- und Log-Datei werden im Eingabeverzeichnis mit korrekter Suffix-Logik erzeugt (`_v1`, `_v2`, …)" + +**❌ FEHLT** vollständig. + +- Keine Berichtdatei-Erzeugung im Code. `ValidationResultPrinter#printToConsole` schreibt ausschließlich auf `System.out` ([ValidationResultPrinter.java:22](src/main/java/de/gecheckt/asv/adapter/out/reporting/ValidationResultPrinter.java:22)). +- Die Log-Datei wird statisch nach `logs/asv-format-validator.log` relativ zum Arbeitsverzeichnis geschrieben ([log4j2.xml:7](src/main/resources/log4j2.xml:7)), nicht in das Verzeichnis der Eingabedatei. +- Keine Suffix-Logik `_v1/_v2/…` vorhanden (Grep `_v1` im Hauptcode: keine Treffer). +- `adapter.out.filesystem` ist leer. `LoggingConfigurator#configureLogFile(Path)` ist ein Stub (`// TODO: dynamische Log-Datei-Umleitung in AP07`). + +### 3.4 „Log4j2-Bindung ist außerhalb von Bootstrap und Logging-Adapter nicht sichtbar" + +**✅ ERFÜLLT.** + +Grep über `src/`: `org.apache.logging.log4j` erscheint in **keinem einzigen** Produktions- oder Testcode-Import (`Grep org.apache.logging.log4j` → keine Treffer). Die Log4j2-Abhängigkeit wird ausschließlich per Runtime-Binding (`log4j-slf4j2-impl`, Scope `runtime`) gebunden. `LoggingConfigurator` liegt korrekt im erlaubten Paket, importiert aktuell aber noch keine Log4j2-Typen (Stub-Zustand). Das in AP10 vorgesehene Architekturgate kann diese Regel formal sichern. + +### 3.5 „Befundmodell trennt Spec-Urteil und Diagnose strukturell" + +**❌ FEHLT** vollständig. + +Der bestehende `ValidationError`-Record ([ValidationError.java:20](src/main/java/de/gecheckt/asv/application/model/ValidationError.java:20)) hat neun Felder (`errorCode`, `description`, `severity`, `segmentName`, `segmentPosition`, `fieldName`, `fieldPosition`, `actualValue`, `expectedRule`). Keines davon trägt die Unterscheidung zwischen verbindlichem **Spec-Urteil** und zusätzlicher **diagnostischer Weiteranalyse** — beides wandert undifferenziert in eine gemeinsame `List` in `ValidationResult`. Zentrale Soll-Metadaten (Artefaktschicht, Prüfstufe, Prüfbereich, Rohwert, Position, Nachrichtenbezug, optionaler offizieller Fehlercode) fehlen ebenfalls. Das ist AP05-Scope. + +### 3.6 „Build und Tests sind grün" + +**✅ ERFÜLLT.** + +`mvn clean verify` → `BUILD SUCCESS`, `Tests run: 147, Failures: 0, Errors: 0, Skipped: 0` (lokal reproduziert am 2026-04-20). JaCoCo-Report wird erzeugt, keine Schwellwerte (kommt erst M9). Warnungen: nur die bekannte HotSpot-Sharing-Warnung aus AP02 (kosmetisch). Bei den CLI-Tests wird eine erwartete `ERROR`-Zeile „Fehler beim Lesen der Datei: File does not exist: /non/existent/file.txt" ausgegeben — Test-Nebeneffekt, nicht fehlerhaft. + +### 3.7 Ergebnis-Tabelle + +| Abnahmekriterium | Status | +|---|---| +| JAR unter Windows mit Java 21 startbar | 🔶 | +| Eingabedatei/Exit-Code 2/Minimalbericht | ❌ | +| Bericht/Log im Eingabeverzeichnis mit Suffix-Logik | ❌ | +| Log4j2-Bindung gekapselt | ✅ | +| Befundmodell Spec-/Diagnose-Trennung | ❌ | +| Build und Tests grün | ✅ | + +Zwei von sechs erfüllt, einer teilweise, drei fehlend. + +--- + +## 4. Soll-Ist-Vergleich Paketstruktur + +| Soll-Paket | Ist-Stand | Bewertung | +|---|---|---| +| `domain` | Unterpaket `domain.model` mit 4 Records (`Field`, `Segment`, `Message`, `InputFile`); `package-info.java` auf `domain`-Ebene vorhanden | **vorhanden, aber flach** — noch kein Befundmodell, noch keine fachliche Schichtung | +| `application` | Orchestrator `DefaultInputFileValidator` + `application.model.*` + `application.field.*` + `application.structure.*` | **vorhanden, mischt Orchestrierung und Preview-Fachregeln** — AP09 muss entscheiden, wie die Preview-Teile markiert/abgetrennt werden | +| `adapter.in.cli` | `AsvValidatorApplication` (main + run + parseFile + printUsage) | **vorhanden, aber überladen** — enthält Bootstrap-Wiring, Dateisystemzugriff, CLI-Parsing und Fehlerbehandlung in einer Klasse | +| `adapter.out.filesystem` | leer (nur `package-info.java`) | **angelegt, unbefüllt** — AP07-Vorbehalt | +| `adapter.out.parsing` | 5 Klassen (Parser/Tokenizer + Exception) | **vorhanden** — Preview-Parser mit hartem `+` als Separator | +| `adapter.out.crypto` | nicht angelegt | **fehlt planmäßig** — ist M8-Scope, in M1 nicht gefordert | +| `adapter.out.reporting` | `ValidationResultPrinter` | **vorhanden** — nur Konsolenformat, kein Dateioutput | +| `adapter.out.logging` | `LoggingConfigurator` (Stub) | **angelegt, unbefüllt** — dynamische Log-Datei-Umleitung ist AP07 | +| `bootstrap` | leer (nur `package-info.java`) | **angelegt, unbefüllt** — `Main` ist AP06-Scope | + +Fazit: Die Paketstruktur ist aus AP03 heraus formal vollständig. Die Inhalte sind asymmetrisch — das Parser-, Validator- und Modellpaket ist „voll" (z.T. mit Preview-Code), das Bootstrap- und das Filesystem-Paket sind leer. + +--- + +## 5. Architekturprinzipien — Befunde + +### 5.1 Keine Log4j2-Typen außerhalb `adapter.out.logging`/`bootstrap` + +**✅ eingehalten.** Grep `org.apache.logging.log4j` über `src/` liefert keine Treffer. Nach AP04 ist die einzig erlaubte Anlaufstelle `LoggingConfigurator` (der aktuell selbst keine Log4j2-Typen importiert, da er nur Stub ist). + +### 5.2 Keine Infrastrukturabhängigkeiten in `domain` oder `application` + +**⚠️ größtenteils eingehalten, mit einem Rest-Risiko.** + +- `domain.*` importiert nur `java.util.*` und `java.util.Optional` — **sauber**. +- `application.*` importiert keine CLI-, Filesystem-, Logging- oder Parser-Typen. Grep bestätigt: kein `java.nio.file`, kein `System.out/err`, kein `org.slf4j` in `application/`. +- **Rest-Risiko:** Die Klassen `application.structure.DefaultStructureValidator` (19 Regeln, u.a. zu `UNH`/`UNT`, `ASVREC`/`ASVFEH`, Rechnungskennzeichen, FHL) und `application.field.DefaultFieldValidator` enthalten bereits fachliche ASV-Regelkenntnis. Formal gehören solche Regeln ins Domänen- bzw. Regelpaket, nicht in die „Application"-Orchestrierungsschicht. Da AP09 vorsieht, diese Altlogik explizit als Vorbau einzufrieren und erst in M3+ wieder aufzunehmen, ist das derzeit akzeptierte Schulden — aber es verletzt den Soll-Zuschnitt leise (Fachregeln in `application/*` statt in einer eigenständigen Regelschicht). + +### 5.3 Manuelle Constructor Injection, kein DI-Framework + +**✅ eingehalten.** `AsvValidatorApplication` besitzt einen Default-Konstruktor, der alle Komponenten manuell per `new` verdrahtet ([AsvValidatorApplication.java:50-60](src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java:50)), und einen Test-Konstruktor mit Constructor Injection ([AsvValidatorApplication.java:70](src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java:70)). `DefaultInputFileValidator` nimmt `StructureValidator` und `FieldValidator` per Constructor entgegen ([DefaultInputFileValidator.java:33](src/main/java/de/gecheckt/asv/application/DefaultInputFileValidator.java:33)). Kein Spring, kein Quarkus, kein CDI, kein Guice in den Dependencies. + +Anmerkung: Die Wiring-Logik liegt derzeit im CLI-Adapter selbst; der Soll-Zuschnitt will sie in `bootstrap` sehen (AP06). + +### 5.4 Befundmodell unterscheidet Spec-Urteil und Diagnose + +**❌ nicht eingehalten.** Siehe 3.5. Das aktuelle `ValidationResult` kennt nur `ERROR`/`WARNING`/`INFO`, keine Dimension „spec-verbindlich vs. diagnostisch". + +### 5.5 Exit-Codes `0/1/2` korrekt verdrahtet + +**❌ nicht eingehalten.** Siehe 3.2. Ist-Stand ist `0/1/2/3` mit teilweise invertierter Semantik. Konkrete Stellen: `AsvValidatorApplication.java:36-39` (Konstanten), `:97` (falscher Code bei fehlendem Argument), `:113` (falscher Code bei Validierungsfehler). + +### 5.6 Weitere prinzipielle Befunde + +- **Eingabe-Encoding ISO-8859-15 (`technik-und-architektur.md` §„Zeichensätze"):** Das Ist liest mit `StandardCharsets.UTF_8` ([AsvValidatorApplication.java:152](src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java:152)). `java.nio.charset.StandardCharsets` bietet kein ISO-8859-15, man muss `Charset.forName("ISO-8859-15")` nutzen. Aktuelle Fehlkodierung verfälscht alle Umlaute (ä=0xE4 statt UTF-8-Doppelbyte) und jeden Euro-Sonderfall (`€`). +- **Schreiben statt Speichern der Bericht-/Log-Datei:** Die Bericht- und Log-Dateien müssten laut Soll in UTF-8 in das Verzeichnis der Eingabedatei geschrieben werden. Keine dieser Anforderungen ist umgesetzt. +- **EDIFACT-Konformität des Tokenizers:** `DefaultSegmentLineTokenizer` trennt starr an `+` ([Zeile 13](src/main/java/de/gecheckt/asv/adapter/out/parsing/DefaultSegmentLineTokenizer.java:13)). Das UNA-Segment und die Gruppenelementtrennung `:` werden nicht ausgewertet. Nicht M1-Scope (gehört zu M3), aber Rest-Risiko bei Tests und als Vorbau. +- **Parser-Vereinfachung:** `DefaultInputFileParser` erzeugt pro Datei genau eine `Message` ([Zeile 54](src/main/java/de/gecheckt/asv/adapter/out/parsing/DefaultInputFileParser.java:54)), unabhängig von UNH/UNT-Gruppen. Preview-Verhalten. + +--- + +## 6. Qualitätsstatus (Build/Test) + +> Hinweis zu den widersprüchlichen Vorgaben des Auftrags: Die Einleitung spricht von „machst kein mvn", Abschnitt 5 fordert jedoch explizit `mvn clean verify`, der abschließende Teil präzisiert auf „kein `mvn package`". Der Autor dieses Berichts interpretiert den spezifischen Auftrag in Abschnitt 5 als maßgeblich; `mvn clean verify` wurde einmalig ausgeführt. Kein `mvn package`, kein `git`, kein Schreiben außerhalb dieses Berichts. + +- **`mvn clean verify`:** ✅ `BUILD SUCCESS` (lokal, 2026-04-20). +- **Tests:** 147 gesamt, 0 Failures, 0 Errors, 0 Skipped. Alle Testklassen der hexagonalen Struktur laufen durch. +- **Compile-/Javac-Warnungen:** keine über die aus AP02 bekannten hinaus. +- **JVM-Warnungen zur Laufzeit:** + - `Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended` — kosmetisch, stammt vom JaCoCo-Agent, bekannt seit AP02. + - Eine erwartete `ERROR`-Log-Zeile aus dem CLI-Test (nicht-existente Datei) ist sichtbar, wird aber nicht als Testfehler gewertet. Sie wäre zum M1-Ende über ein Logback-Test-Filter oder eine angepasste Test-Log-Konfiguration in AP10 unterdrückbar — kein Blocker. +- **Mutation Testing:** In AP02 einmalig ausgeführt (249 Mutationen, 83 % getötet), Schwellwerte kommen erst M9. +- **Coverage:** JaCoCo-Report unter `target/site/jacoco/`; keine Schwellwerte aktiv. + +--- + +## 7. Preview-Code / Altlogik + +### 7.1 Identifikation + +Als **Preview-Code** der Sondierungsphase (README, Abschnitt „Preview-Code"; AP01 Klassifikation) gelten: + +| Klasse | Paket | M1-Bewertung | Verdrahtung im Lauf | +|---|---|---|---| +| `DefaultInputFileParser` | `adapter.out.parsing` | Behalten, für M3 überarbeiten (Trennzeichen, Multi-Message) | aktiv in `AsvValidatorApplication` | +| `DefaultSegmentLineTokenizer` | `adapter.out.parsing` | Behalten, für M3 überarbeiten (UNA) | aktiv | +| `InputFileParser`/`SegmentLineTokenizer`/`InputFileParseException` | `adapter.out.parsing` | Behalten als stabile Ports | aktiv | +| `DefaultFieldValidator` | `application.field` | **einzufrieren (AP09)** — keine M1-Weiterentwicklung | aktiv über `DefaultInputFileValidator` | +| `DefaultStructureValidator` (19 ASVREC/ASVFEH-Regeln) | `application.structure` | **einzufrieren (AP09)** — 19 fachliche Regeln, M3+-Vorbau | aktiv über `DefaultInputFileValidator` | +| `ValidationError`, `ValidationResult`, `ValidationSeverity` | `application.model` | umzubauen/abzulösen durch Befundmodell mit Spec-/Diagnose-Trennung (AP05) | aktiv als einzige Befundträger | +| `ValidationResultPrinter` | `adapter.out.reporting` | Behalten als Konsolen-Formatierer | aktiv | + +### 7.2 Aktiv oder eingefroren? + +Alle Preview-Klassen sind **aktiv** in den Lauf verdrahtet: Der Default-Konstruktor von `AsvValidatorApplication` baut parser + validator + printer komplett mit der Preview-Logik zusammen ([AsvValidatorApplication.java:52-59](src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java:52)). Der produktive Lauf führt deshalb heute schon fachliche ASVREC-/ASVFEH-Prüfungen aus — nominell M3+-Scope, real vorhandenes Verhalten. Ein formales „Einfrieren" (Kommentar-Marker, Paketumzug oder gezielte Deaktivierung) ist bis zum AP09 noch nicht erfolgt. + +### 7.3 Risiken des Preview-Codes für M1 + +1. **Fachliche Befunde in M1:** Solange `DefaultStructureValidator` aktiv bleibt, liefert ein M1-Lauf bereits ASVREC-/ASVFEH-Befunde. Das widerspricht der M1-Vorgabe „noch keine ASV-Fachvalidierung" (`meilensteine.md` §M1 Ziel, und AP-README „Keine Fachvalidierung in M1"). +2. **Exit-Code-Verwechslung:** Weil Fachbefunde schon produziert werden, schlagen sie über `hasErrors()` auf den (falschen) Exit-Code `3` durch. Ein M1-Lauf einer beliebigen Textdatei liefert deshalb heute realistisch `3`, nicht das geplante `0` (gültig mangels Regeln) oder das korrekte `1` (ungültig). +3. **Hartkodiertes `+` als Trennzeichen:** Jede Datei ohne ASV-typische EDIFACT-Struktur wird stumm „erfolgreich" geparst. Für M1-Akzeptanz mit einer Minimaldatei akzeptabel; für alles Reale gefährlich. +4. **UTF-8-Lesen:** Alle Preview-Tests laufen auf UTF-8-kodierten `*.asv`-Testressourcen. Beim Umstellen auf ISO-8859-15 werden vorhandene Testdaten zum Risiko — Umstellung muss zusammen mit Testressourcen-Anpassung erfolgen. +5. **`DefaultStructureValidatorTestAdditional` leer:** Eine Testklasse ohne `@Test`-Methoden bleibt seit AP01 bestehen und ist ein Restposten für AP10. +6. **Preview-Regeln als „V1-N"-Kandidaten:** Einige der 19 Regeln (z.B. strikte Reihenfolgeerzwingung der ASVREC-Segmente, FHL-Pflicht für ASVFEH) greifen zu rigide, wenn sie gegen die finalen Soll-Regelklassifikationen (`V1-V/T/N/K`) gestellt werden. Beim Wiederaufnehmen ab M3 ist jede Preview-Regel neu zu bewerten — das ist kein M1-Problem, aber ein M3-Erbe. + +--- + +## 8. Offene Punkte und Risiken + +### 8.1 Architekturrisiken + +- **R-ARCH-01 — Exit-Code-Semantik falsch und erweitert.** Der Ist-Code hat vier Codes mit vertauschter Bedeutung. Muss in AP06 korrigiert werden, damit AP11 abnehmbar ist. Bis dahin ist ein M1-Lauf nicht spec-konform. +- **R-ARCH-02 — Eingabe-Encoding hartkodiert UTF-8.** Widerspricht direkt dem Soll-Rahmen. `StandardCharsets` kennt kein ISO-8859-15; `Charset.forName("ISO-8859-15")` muss genutzt werden, JDK-Verfügbarkeit ist zu verifizieren. Umstellung ist in AP06 oder spätestens in AP07 zu verankern. +- **R-ARCH-03 — Keine Spec-/Diagnose-Trennung im Befundmodell.** Das aktuelle `ValidationError`/`ValidationResult` reicht für die Fortführung ab M2 nicht aus. Es muss in AP05 komplett neu geschnitten (oder stark erweitert) werden, inklusive aller Metadatenfelder aus `technik-und-architektur.md` § „Befundarten". Weil der Preview-`DefaultStructureValidator` dieses Modell produktiv befüllt, wird der Umbau nicht kostenlos — AP09 muss die Altlogik zuvor entkoppeln. +- **R-ARCH-04 — Ausgabeartefakte fehlen komplett.** Weder Berichtdatei noch Logdatei werden im Eingabeverzeichnis erzeugt; die Suffix-Logik existiert nicht. Das ist der größte Einzel-Arbeitsblock in M1 (AP07). +- **R-ARCH-05 — Preview-Code läuft produktiv mit und erzeugt Fachbefunde.** Siehe 7.3. AP09 muss klären: entkoppeln, per Default deaktivieren oder nur inert im Classpath behalten. + +### 8.2 Implementierungslücken (aus M1 noch offen) + +- **L-AP05** — Befundmodell mit Spec-/Diagnose-Trennung. +- **L-AP06** — `bootstrap.Main`-Klasse, sauberes CLI-Wiring, Exit-Codes `0/1/2`, ISO-8859-15. +- **L-AP07** — `adapter.out.filesystem` mit Dateioutput; `LoggingConfigurator` mit dynamischer Log-Datei-Umleitung; Suffix-Logik `_v1/_v2/…`. +- **L-AP08** — Minimalbericht bei Bedienfehlern (Exit-Code `2`). +- **L-AP09** — Altlogik einfrieren (oder deaktivieren), damit M1-Läufe keine unerwarteten Fachbefunde mehr erzeugen. +- **L-AP10** — Architekturtest (Paketabhängigkeiten, Log4j2-Sichtbarkeit, idealerweise auch Exit-Code-Semantik und Preview-Deaktivierung). +- **L-AP11** — End-to-End-Abnahme mit Minimaldatei; Berichtskonsolidierung. + +### 8.3 Unklarheiten / Entscheidungsbedarf + +- **E-01: Wie wird der Preview-`DefaultStructureValidator` in AP09 konkret eingefroren?** Drei Optionen: (a) in ein Sub-Paket `application.preview.structure` verschieben und den Orchestrator gegen eine Null-Implementation austauschen; (b) in Ort belassen, den Orchestrator im Bootstrap aber nicht mehr damit verdrahten; (c) weiterlaufen lassen, aber dessen Befunde im neuen Befundmodell als „Diagnose" klassifizieren. Jede Option hat andere Auswirkungen auf AP05 und AP10. +- **E-02: Wie wird der Test-Log-Rauschen (`ERROR`-Zeile im Negativ-Test) eliminiert?** Eigene Test-Log-Konfiguration oder bewusst stehen lassen? AP10 sollte hier eine Entscheidung zusichern. +- **E-03: Was passiert mit `DefaultStructureValidatorTestAdditional`?** Stummes Artefakt seit AP01 — in AP10 entfernen oder mit Tests füllen? +- **E-04: Soll der aktuelle Inhalt `logs/asv-format-validator.log` (Arbeitsverzeichnis) nach AP07 noch eine Rolle spielen?** Wenn die Logdatei künftig neben die Eingabedatei geschrieben wird, wird der aus AP04 konfigurierte statische Pfad überflüssig. Der `logs/`-Ordner im Repo-Root ist Resultat früherer Läufe und gehört vermutlich `.gitignore`'d. +- **E-05: JAR-Aufbau als „fat jar" oder als JAR mit Classpath?** `maven-jar-plugin` alleine erzeugt kein Uber-JAR — `java -jar asv-format-validator.jar` würde ohne `maven-shade-plugin` oder `Class-Path`-Manifest ohne Log4j2/SLF4J starten. AP06 muss dies entscheiden (die AP-Texte nennen nur `maven-jar-plugin`). + +--- + +## 9. Empfehlung: nächste sinnvolle Arbeitspakete für M1 + +Reihenfolge auf Basis der Abhängigkeiten in `docs/arbeitspakete/m1/README.md` und der oben sichtbaren Risiken: + +1. **AP05 — Befundmodell mit Spec-/Diagnose-Trennung.** Höchste Hebelwirkung: freigeschaltet sind danach AP06 (Exit-Codes bauen auf dem neuen Urteil auf), AP07 (Bericht rendert das neue Modell) und AP09 (Preview-Befunde lassen sich als Diagnose klassifizieren). Unmittelbar angreifbar, keine vorangehenden APs mehr offen. +2. **AP06 — Bootstrap und CLI.** Exit-Codes `0/1/2` korrigieren, `bootstrap.Main` anlegen, Wiring aus `AsvValidatorApplication` herauslösen, ISO-8859-15 einziehen. Nur mit AP05-Modell sinnvoll, weil `hasErrors()` des neuen Befundmodells den Urteils-Exit-Code speist. +3. **AP09 — Altlogik einfrieren.** Direkt nach AP06 sinnvoll, weil ab dann der Bootstrap sauber entscheiden kann, ob und wie die Preview-Validatoren verdrahtet werden. Ohne AP09 bringt AP05 keinen realen Effekt — `DefaultStructureValidator` produziert weiter Fachbefunde. +4. **AP07 — Ausgabeartefakte.** Nach AP06 sinnvoll, weil dann eine stabile Ausgangs-CLI vorliegt, in die die Dateilogik eingezogen werden kann. `LoggingConfigurator` wird hier Leben bekommen. +5. **AP08 — Minimalbericht.** Reine Veredelung der Bedienfehler-Pfade auf Basis von AP07. +6. **AP10 — Architekturtest.** Sobald AP04–AP09 stehen, kann ein ArchUnit-Scan die Log4j2-Sichtbarkeit, die Paketabhängigkeiten, die Exit-Codes (als Konstanten) und die Preview-Deaktivierung formell prüfen. +7. **AP11 — M1-Abnahme.** Abschlusslauf mit Minimaldatei, Konsolidierung aller Berichte. + +Wichtig: Jedes dieser APs bleibt ein **eigener Claude-Lauf**. Diese Ist-Analyse hier bündelt nur die Ausgangslage; sie darf in späteren APs zitiert, aber nicht als implizite Entscheidung missverstanden werden — insbesondere die Optionen zu E-01 bis E-05 sind weiterhin offen. diff --git a/docs/arbeitspakete/m1/berichte/AP05-bericht.md b/docs/arbeitspakete/m1/berichte/AP05-bericht.md new file mode 100644 index 0000000..8a15fcf --- /dev/null +++ b/docs/arbeitspakete/m1/berichte/AP05-bericht.md @@ -0,0 +1,101 @@ +# Abschlussbericht Arbeitspaket AP05 – Befundmodell mit Spec-/Diagnose-Trennung + +> **Bezug:** `docs/arbeitspakete/m1/AP05-befundmodell.md` +> **Bearbeiter:** Claude Code (claude-sonnet-4-6), Subagent-Lauf +> **Datum:** 2026-04-20 +> **Commit(s):** ausstehend (Mensch committet nach Sichtung) +> **Status:** ✅ abgeschlossen + +## 1. Zusammenfassung + +Das Paket `de.gecheckt.asv.domain.finding` wurde vollständig neu angelegt und enthält alle im AP05 geforderten Typen: die Enums `Severity`, `FindingKind`, `FindingLayer`, `Verdict`, den unveränderlichen Record `Finding` mit Builder sowie die unveränderliche Klasse `ValidationReport`. Die zentrale Invariante — dass DIAGNOSTIC-ERROR das Verdict niemals auf INVALID setzt — ist durch einen explizit markierten Unit-Test (`diagnosticErrorLiefertVALID`) abgesichert. `mvn clean verify` ist grün (164 Tests, 0 Fehler). + +## 2. Umgesetzte Änderungen + +Folgende Dateien wurden **neu angelegt** (keine bestehende Datei wurde verändert): + +**Produktionscode (`src/main/java/de/gecheckt/asv/domain/finding/`):** + +- `Severity.java` — Enum: ERROR, WARNING, HINT; JavaDoc erklärt Einfluss auf Verdict +- `FindingKind.java` — Enum: SPEC, DIAGNOSTIC; JavaDoc beschreibt Verdict-Invariante +- `FindingLayer.java` — Enum: ARTIFACT, TECHNICAL_STRUCTURE, DOMAIN_MODEL; JavaDoc erklärt Schichttrennung +- `Verdict.java` — Enum: VALID, INVALID, OPERATIONAL_ERROR; Exit-Code-Zuordnung in JavaDoc +- `Finding.java` — Record mit allen 12 Feldern gemäß AP05; Null-Prüfung im Kompaktkonstruktor für Pflichtfelder; zusätzlicher innerer `Builder` für komfortablere Erzeugung; Hilfsmethode `isSpecError()` +- `ValidationReport.java` — unveränderliche Klasse; `findings` intern per `List.copyOf()` gesichert; Methoden `computeVerdict()`, `hasSpecErrors()`, `specFindings()`, `diagnosticFindings()`, `getFindings()`, `getFileName()`, `getTimestamp()`; Factory `operationalError(fileName, ruleId, message)`; privater Konstruktor für Bedienfehler-Flag + +**Tests (`src/test/java/de/gecheckt/asv/domain/finding/`):** + +- `ValidationReportTest.java` — 11 Tests (die 7 Pflicht-Tests aus AP05 plus 4 ergänzende Absicherungen) +- `FindingTest.java` — 6 Tests für Record, Builder und Null-Absicherung + +**Keine bestehende Datei wurde verändert.** Insbesondere wurde `ValidationResult` (Altmodell) nicht angefasst. + +## 3. Scope-Treue + +| Scope-Punkt aus dem Arbeitspaket | Erfüllt? | Bemerkung | +|---|---|---| +| Paket `domain.finding` anlegen | ✅ | `de.gecheckt.asv.domain.finding` vollständig vorhanden | +| Enum `Severity` (ERROR/WARNING/HINT) | ✅ | Alle drei Werte implementiert | +| Enum `FindingKind` (SPEC/DIAGNOSTIC) | ✅ | Beide Werte mit präzisem JavaDoc | +| Enum `FindingLayer` (ARTIFACT/TECHNICAL_STRUCTURE/DOMAIN_MODEL) | ✅ | Alle drei Werte implementiert | +| `Finding` Record mit allen 12 Feldern | ✅ | Alle Felder vorhanden; `germanMessage` nicht nullable; übrige 11 nullable | +| `Verdict` Enum (VALID/INVALID/OPERATIONAL_ERROR) | ✅ | Alle drei Werte mit Exit-Code-Kommentar | +| `ValidationReport` unveränderliche Klasse | ✅ | `final`, findings per `List.copyOf()` gesichert | +| `computeVerdict()` nur SPEC+ERROR | ✅ | Invariante im Code und per Test abgesichert | +| `hasSpecErrors()` | ✅ | Implementiert | +| `specFindings()` / `diagnosticFindings()` | ✅ | Implementiert, per Test verifiziert | +| `operationalError(fileName, ruleId, message)` | ✅ | Factory-Methode vorhanden | +| Keine Änderung an `ValidationResult` | ✅ | Altmodell nicht angefasst | +| Integration in laufende Validierung (Scope OUT) | ✅ nicht gemacht | AP06 | +| Löschen/Umbenennen `ValidationResult` (Scope OUT) | ✅ nicht gemacht | AP09 | +| Berichtserzeugung/Textrendering (Scope OUT) | ✅ nicht gemacht | AP07 | +| Architekturtest (Scope OUT) | ✅ nicht gemacht | AP10 | + +**Wurde der Scope eingehalten?** Ja, vollständig. + +**Wurden Dinge außerhalb des Scopes gemacht?** Nein. Der `Builder` für `Finding` war im AP05 explizit als Alternative zum reinen Record erwähnt und fällt daher in den Scope. + +## 4. Abnahmekriterien + +| Abnahmekriterium aus dem Arbeitspaket | Erfüllt? | Nachweis | +|---|---|---| +| Paket `domain.finding` enthält alle genannten Typen | ✅ | 5 Dateien: `Severity`, `FindingKind`, `FindingLayer`, `Verdict`, `Finding`, `ValidationReport` | +| **Test „DIAGNOSTIC-ERROR ergibt VALID" ist grün** | ✅ | `ValidationReportTest#diagnosticErrorLiefertVALID()` — GRÜN; `@DisplayName("KRITISCH: Ein DIAGNOSTIC-ERROR-Befund liefert Verdict VALID (niemals INVALID)")` | +| `ValidationReport.findings` ist unveränderlich (Test vorhanden) | ✅ | `ValidationReportTest#findingsListeNichtModifizierbar()` testet: (1) Mutation der Eingabeliste nach Übergabe hat keinen Effekt; (2) `getFindings().add(...)` wirft `UnsupportedOperationException` | +| Alle Metadatenfelder aus technik-und-architektur.md §„Befundarten" im `Finding`-Typ | ✅ | Alle 12 Felder vorhanden: Artefaktschicht (`layer`), Prüfstufe/Art (`kind`), Segmenttyp, Segmentindex, Feld-ID, Rohwert, Position, Nachrichtenreferenz, offizieller Fehlercode, interne Regel-ID, Schweregrad, deutscher Befundtext | +| `operationalError(...)` Factory-Methode existiert | ✅ | Statische Methode in `ValidationReport`; Testnachweis: `ValidationReportTest#operationalErrorFactoryLiefertOPERATIONAL_ERROR()` | +| Keine Änderung an `ValidationResult` (Altmodell) | ✅ | Dateipfad `application/model/ValidationResult.java` wurde nicht geöffnet oder verändert | +| `mvn clean verify` grün | ✅ | 164 Tests, 0 Failures, 0 Errors, 0 Skipped | +| Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP05-bericht.md` | ✅ | Diese Datei | + +## 5. Build- und Teststatus + +- `mvn clean verify`: ✅ grün +- Anzahl Tests gesamt: **164** (davon **17 neu** in `domain.finding`: 11 in `ValidationReportTest`, 6 in `FindingTest`) +- Vorherige Testanzahl (vor AP05): 147 +- Coverage: JaCoCo läuft; das neue Paket `domain.finding` hat vollständige Abdeckung aller kritischen Pfade durch die 17 neuen Tests +- Warnungen beim Build: keine testbezogenen Warnungen; eine Java-Compiler-Warnung bezüglich Annotationsverarbeitung (`-proc:none` empfohlen) — diese war bereits vor AP05 vorhanden und ist kein AP05-Artefakt + +## 6. Rest-Risiken und offene Punkte + +- **Zwei parallele Ergebnistypen:** `ValidationResult` (Altmodell im Paket `application.model`) und `ValidationReport` (neues Befundmodell in `domain.finding`) koexistieren bewusst bis AP09. Dies ist im AP05 explizit als Absicht dokumentiert und kein Risiko dieses Arbeitspakets. +- **`operationalError`-Befund nutzt `FindingLayer.ARTIFACT`:** Der Bedienfehler-Befund wurde auf Schicht `ARTIFACT` abgelegt, da Bedienfehler typischerweise die Artefaktebene betreffen (nicht lesbare Datei, fehlender Pfad). Falls AP08 eine andere Schicht bevorzugt, ist das in der Factory anpassbar ohne API-Bruch. +- **`Finding.isSpecError()`** ist eine Hilfsmethode für interne Verwendung. Ob sie öffentlich bleiben soll oder auf `package-private` reduziert wird, kann in AP10 (Architekturtest) beurteilt werden. +- **Keine `package-info.java`** für das neue Paket `domain.finding` angelegt — dies war nicht im AP05-Scope, kann aber in einem Folge-AP ergänzt werden. + +## 7. Empfehlungen für Folge-Arbeitspakete + +- **AP06 (Bootstrap/CLI):** Der `ValidationReport` ist bereit zur Integration. Die Factory `operationalError(...)` deckt den Exit-Code-2-Pfad ab. `computeVerdict()` liefert direkt den Verdict für die Exit-Code-Entscheidung. +- **AP07 (Ausgabeartefakte):** Das `Finding`-Record ist flat und enthält alle Traceability-Informationen, die für einen hierarchischen Bericht benötigt werden. `specFindings()` und `diagnosticFindings()` erleichtern die Gliederung. +- **AP09 (Altlogik einfrieren):** `ValidationResult` und `ValidationReport` koexistieren sauber. AP09 kann `ValidationResult` einfrieren oder entfernen, ohne `ValidationReport` anzufassen. +- **AP10 (Architekturtest):** Das Paket `domain.finding` hat keine Infrastrukturabhängigkeiten (nur `java.time.Instant`, `java.util.*`). Der ArchUnit-Test sollte diese Eigenschaft formal absichern. + +## 8. Reviewer-Checkliste + +- [x] Alle im Arbeitspaket genannten Scope-IN-Punkte sind nachweislich umgesetzt +- [x] Keine Scope-OUT-Punkte wurden angefasst +- [x] Abnahmekriterien sind mit konkreten Nachweisen belegt (Tests, Dateipfade) +- [x] `mvn clean verify` ist grün (164 Tests, 0 Failures) +- [ ] Der Commit für dieses AP hat eine sprechende Message (`M1-AP05: ...`) — ausstehend, Mensch committet +- [x] Keine Regeln der Grunddokumente (Spec, Fachliche, Technik) wurden verletzt +- [x] Rest-Risiken sind ehrlich dokumentiert diff --git a/docs/arbeitspakete/m1/berichte/AP06-bericht.md b/docs/arbeitspakete/m1/berichte/AP06-bericht.md new file mode 100644 index 0000000..7250fa8 --- /dev/null +++ b/docs/arbeitspakete/m1/berichte/AP06-bericht.md @@ -0,0 +1,112 @@ +# Abschlussbericht Arbeitspaket AP06 – Bootstrap und CLI-Adapter + +> **Bezug:** `docs/arbeitspakete/m1/AP06-bootstrap-cli.md` +> **Bearbeiter:** Claude Code (claude-sonnet-4-6), Subagent-Lauf +> **Datum:** 2026-04-20 +> **Commit(s):** ausstehend (Mensch committet nach Sichtung) +> **Status:** ✅ abgeschlossen + +## 1. Zusammenfassung + +Die bisherige `AsvValidatorApplication` wurde in zwei klar getrennte Verantwortlichkeiten zerlegt: `de.gecheckt.asv.bootstrap.Main` übernimmt die manuelle Constructor Injection und den einzigen `main`-Einstiegspunkt, `de.gecheckt.asv.adapter.in.cli.CliRunner` übernimmt die CLI-Argument-Verarbeitung und die Exit-Code-Übersetzung. Exit-Codes wurden auf die normativen Werte 0/1/2 umgestellt, ISO-8859-15 als Eingabe-Encoding eingeführt, und das Uber-JAR wird nun über `maven-shade-plugin` erzeugt. `mvn clean verify` ist grün (168 Tests, 0 Fehler). + +## 2. Umgesetzte Änderungen + +**Neu angelegt:** + +- `src/main/java/de/gecheckt/asv/adapter/in/cli/ExitCode.java` — Normative Exit-Code-Konstanten (VALID=0, INVALID=1, OPERATIONAL_ERROR=2). Alte Konstanten aus `AsvValidatorApplication` entfernt. +- `src/main/java/de/gecheckt/asv/adapter/in/cli/CliRunner.java` — CLI-Adapter; einziger Ort mit Argument-Parsing; nutzt `ValidationReport.computeVerdict()` zur Exit-Code-Ableitung; enthält keine Log4j2-Typen (nur SLF4J). +- `src/main/java/de/gecheckt/asv/application/FileValidationService.java` — Anwendungsschnittstelle für die Dateivalidierung, mit `ValidationReport validate(Path)`. +- `src/main/java/de/gecheckt/asv/application/DummyFileValidationService.java` — M1-Platzhalter; liest Datei mit `Charset.forName("ISO-8859-15")`, zählt Bytes, gibt leeren `ValidationReport` zurück. Konstante `INPUT_CHARSET` paketöffentlich für Testbarkeit. +- `src/main/java/de/gecheckt/asv/bootstrap/Main.java` — Einziger `public static void main`; verdrahtet `LoggingConfigurator`, `DummyFileValidationService`, `CliRunner` per Constructor Injection; delegiert Exit-Code an `System.exit`. Log4j2-Typen nur über `LoggingConfigurator` (in `adapter.out.logging`) sichtbar. +- `src/test/java/de/gecheckt/asv/adapter/in/cli/CliRunnerTest.java` — 5 Tests: kein Argument → 2, zwei Argumente → 2, nicht existierende Datei → 2, leere lesbare Datei → 0, operationalError-Report → 2. +- `src/test/java/de/gecheckt/asv/application/DummyFileValidationServiceTest.java` — 4 Tests: leere Datei → VALID, Datei mit Inhalt → VALID, Byte 0xA4 → Euro-Zeichen €, INPUT_CHARSET-Name korrekt. + +**Geändert:** + +- `src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java` — Auf leere Hülle reduziert; `main` delegiert an `Main.main`. Mit `@Deprecated(forRemoval=true)` markiert. Wird in AP09 endgültig entfernt. +- `src/test/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplicationTest.java` — Auf leere Klasse reduziert; Tests nach `CliRunnerTest` migriert. +- `src/test/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplicationAdditionalTest.java` — Auf leere Klasse reduziert; Tests nach `CliRunnerTest` migriert. +- `pom.xml` — `maven-jar-plugin`-Platzhalter durch `maven-shade-plugin` 3.5.2 ersetzt; `log4j-transform-maven-shade-plugin-extensions` 0.1.0 als Plugin-Dependency ergänzt; `Log4j2PluginCacheFileTransformer` konfiguriert; META-INF-Signatur-Filter ergänzt. +- `.gitignore` — `logs/` und `dependency-reduced-pom.xml` (erzeugt durch shade-Plugin) hinzugefügt. + +## 3. Scope-Treue + +| Scope-Punkt aus dem Arbeitspaket | Erfüllt? | Bemerkung | +|---|---|---| +| `Main` mit `public static void main` und Constructor Injection | ✅ | `bootstrap.Main` verdrahtet alle Komponenten | +| Log4j2-Typen nur in `bootstrap` und `adapter.out.logging` | ✅ | `Main` hat keine direkten Log4j2-Importe; nur `LoggingConfigurator` (in `adapter.out.logging`) | +| `CliRunner.run(String[])` mit Argument-Prüfung | ✅ | Genau ein Positionsargument; 0 oder ≥2 → Exit 2 | +| Datei-Vorabprüfung (existent, regulär, lesbar) | ✅ | Alle drei Prüfungen implementiert | +| Exit-Code 0/1/2 spec-konform | ✅ | Kein Exit-Code 3 mehr erreichbar | +| `ExitCode`-Klasse mit VALID/INVALID/OPERATIONAL_ERROR | ✅ | Alte Konstanten gelöscht | +| Verdrahtung mit `ValidationReport.computeVerdict()` | ✅ | `CliRunner` nutzt den Report aus AP05 direkt | +| `operationalError(...)` für Bedienfehler → Exit 2 | ✅ | Im `CliRunner` nicht direkt verwendet; Verdict-Switch deckt den Fall ab | +| M1-Dummy-Pfad: ISO-8859-15 einlesen, leerer Report | ✅ | `DummyFileValidationService` implementiert genau das | +| `Charset.forName("ISO-8859-15")`, nicht UTF_8 | ✅ | Hardkodiert in `DummyFileValidationService.INPUT_CHARSET` | +| Uber-JAR via `maven-shade-plugin` | ✅ | `Log4j2PluginCacheFileTransformer` + Signatur-Filter konfiguriert | +| `java -jar ... ` ohne `-cp` | ✅ | Manuell verifiziert, Exit-Code 0 | +| `logs/` in `.gitignore` | ✅ | Eingetragen | +| `AsvValidatorApplication` entkern | ✅ | Auf Delegations-Hülle reduziert | +| Tests von `AsvValidatorApplication` nach `CliRunner` migrieren | ✅ | `CliRunnerTest` deckt alle relevanten Szenarien ab | +| Berichtdatei/Log-Datei im Eingabeverzeichnis (Scope OUT) | ✅ nicht gemacht | AP07 | +| Vollständiger Minimalbericht bei Exit 2 (Scope OUT) | ✅ nicht gemacht | AP08 | +| Altlogik-Entkopplung (Scope OUT) | ✅ nicht gemacht | AP09 | +| Architekturtest (Scope OUT) | ✅ nicht gemacht | AP10 | + +**Wurde der Scope eingehalten?** Ja, vollständig. + +**Wurden Dinge außerhalb des Scopes gemacht?** `dependency-reduced-pom.xml` in `.gitignore` eingetragen — dieses Artefakt wird automatisch vom shade-Plugin erzeugt und hatte keinen `.gitignore`-Eintrag. Dies ist eine direkte Konsequenz der Shade-Plugin-Einbindung und fällt in den Scope. + +## 4. Abnahmekriterien + +| Abnahmekriterium aus dem Arbeitspaket | Erfüllt? | Nachweis | +|---|---|---| +| `de.gecheckt.asv.bootstrap.Main` existiert und ist Main-Class des Uber-JAR | ✅ | `Main.java` angelegt; `mainClass` in shade-Plugin-Konfiguration gesetzt; JAR-Test erfolgreich | +| `CliRunner` ist einziger Ort mit CLI-Argument-Parsing | ✅ | `AsvValidatorApplication` enthält kein Argument-Parsing mehr | +| Exit-Codes 0, 1, 2 definiert und spec-konform, kein Exit-Code 3 | ✅ | `ExitCode.java`; `CliRunner`-Switch über `Verdict`; kein `return 3` mehr im Produktionscode | +| Test: Aufruf ohne Argument → Exit-Code 2 | ✅ | `CliRunnerTest#keineArgumente_liefernExitCode2()` — GRÜN | +| Test: Aufruf mit nicht existierender Datei → Exit-Code 2 | ✅ | `CliRunnerTest#nichtExistierendeDatei_liefertExitCode2()` — GRÜN | +| Test: Aufruf mit leerer, lesbarer Datei → Exit-Code 0 | ✅ | `CliRunnerTest#leereLesbareDatei_liefertExitCode0()` — GRÜN | +| Einlese-Encoding ist ISO-8859-15 (Byte 0xA4 → €) | ✅ | `DummyFileValidationServiceTest#byte0xA4_wirdAlsEuroZeichenDekodiert()` — GRÜN | +| `java -jar target/asv-format-validator-*.jar ` startet ohne `-cp` | ✅ | Manuell getestet: `/tmp/test-asv.txt` → Exit 0; kein Argument → Exit 2; nicht existierende Datei → Exit 2 | +| `logs/` in `.gitignore` | ✅ | `.gitignore` enthält `logs/` | +| Keine Log4j2-Typen außerhalb von `bootstrap` und `adapter.out.logging` | ✅ | `CliRunner`, `DummyFileValidationService`, `FileValidationService` importieren nur SLF4J/JDK; `Main` importiert keine Log4j2-Typen direkt | +| `mvn clean verify` grün | ✅ | 168 Tests, 0 Failures, 0 Errors, 0 Skipped | +| Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP06-bericht.md` | ✅ | Diese Datei | + +## 5. Build- und Teststatus + +- `mvn clean verify`: ✅ grün +- Anzahl Tests gesamt: **168** (davon **9 neu** in AP06: 5 in `CliRunnerTest`, 4 in `DummyFileValidationServiceTest`) +- Vorherige Testanzahl (vor AP06, nach AP05): 164 +- Coverage: JaCoCo läuft; neue Klassen vollständig durch Tests abgedeckt +- Warnungen beim Build: + - Shade-Plugin: überlappende META-INF-Ressourcen (LICENSE, NOTICE, DEPENDENCIES) aus Log4j2-JARs — harmlos, bekanntes Verhalten bei Fat-JAR-Erzeugung mit mehreren Apache-Projekten + - `sun.reflect.Reflection.getCallerClass is not supported` beim JAR-Test — Log4j2-interne Warnung, kein Fehler + - Compiler-Warnung zu `@Deprecated`-Annotationsverarbeitung — war bereits vor AP06 vorhanden + +## 6. Rest-Risiken und offene Punkte + +- **`AsvValidatorApplication` als Delegations-Hülle:** Die Klasse existiert noch mit einer `@Deprecated`-`main`-Methode. AP09 entfernt sie endgültig. Bis dahin könnten Tools, die `main`-Methoden suchen, beide Einstiegspunkte anzeigen. +- **Dummy-Pfad ohne echte Validierung:** `DummyFileValidationService` liest die Datei nur, validiert sie nicht. Jede Eingabedatei ergibt Exit-Code 0, solange sie lesbar ist. Echte Validierung kommt ab M3. +- **Shade-Warnung `sun.reflect.Reflection.getCallerClass`:** Log4j2 2.20.0 erzeugt diese Warnung im Shade-JAR-Betrieb. Betrifft nur die Startzeit-Performance, kein Fehler. Kann durch Log4j2-Upgrade auf 2.23+ behoben werden (nicht AP06-Scope). +- **`LoggingConfigurator.configureLogFile(Path)` wird in `Main` nicht aufgerufen:** Der Aufruf wurde weggelassen, da `configureLogFile` in M1 ein No-Op ist und `null` als Argument technisch korrekt, aber semantisch fragwürdig wäre. AP07 füllt diese Methode aus und wird `Main` entsprechend aktualisieren. +- **`dependency-reduced-pom.xml`:** Das shade-Plugin erzeugt diese Datei im Projekt-Root. Sie wurde in `.gitignore` eingetragen, sodass sie nicht versehentlich committet wird. + +## 7. Empfehlungen für Folge-Arbeitspakete + +- **AP07 (Ausgabeartefakte):** `Main` erwartet, dass `LoggingConfigurator.configureLogFile(Path)` mit dem korrekten Pfad aufgerufen wird. Der Pfad muss aus dem Eingabedatei-Pfad abgeleitet werden — AP07 soll `Main` entsprechend erweitern. +- **AP08 (Minimalbericht):** Bei Exit-Code 2 wird derzeit nur eine kurze STDERR-Meldung ausgegeben. AP08 soll einen vollständigen Minimalbericht erzeugen. `CliRunner` bietet dafür eine klare Erweiterungsstelle im Bedienfehler-Pfad. +- **AP09 (Altlogik einfrieren):** `AsvValidatorApplication` (deprecated), `AsvValidatorApplicationTest` und `AsvValidatorApplicationAdditionalTest` (beide leer) können in AP09 vollständig gelöscht werden. Der Altpfad (Parser → Validator → Printer) ist noch vorhanden und wird in AP09 eingefroren/entkoppelt. +- **AP10 (Architekturtest):** `CliRunner`, `Main` und `DummyFileValidationService` haben keine unerwünschten Infrastrukturabhängigkeiten. ArchUnit sollte sicherstellen, dass keine Log4j2-Typen außerhalb von `bootstrap` und `adapter.out.logging` importiert werden. + +## 8. Reviewer-Checkliste + +- [x] Alle im Arbeitspaket genannten Scope-IN-Punkte sind nachweislich umgesetzt +- [x] Keine Scope-OUT-Punkte wurden angefasst +- [x] Abnahmekriterien sind mit konkreten Nachweisen belegt (Tests, Dateipfade, JAR-Test) +- [x] `mvn clean verify` ist grün (168 Tests, 0 Failures) +- [ ] Der Commit für dieses AP hat eine sprechende Message (`M1-AP06: ...`) — ausstehend, Mensch committet +- [x] Keine Regeln der Grunddokumente (Spec, Fachliche, Technik) wurden verletzt +- [x] Rest-Risiken sind ehrlich dokumentiert diff --git a/docs/arbeitspakete/m1/berichte/AP07-bericht.md b/docs/arbeitspakete/m1/berichte/AP07-bericht.md new file mode 100644 index 0000000..13c6a63 --- /dev/null +++ b/docs/arbeitspakete/m1/berichte/AP07-bericht.md @@ -0,0 +1,110 @@ +# Abschlussbericht Arbeitspaket AP07 – Ausgabeartefakte: Berichtdatei und Log-Datei mit Suffix-Logik + +> **Bezug:** `docs/arbeitspakete/m1/AP07-ausgabeartefakte.md` +> **Bearbeiter:** Claude Code (claude-sonnet-4-6), Subagent-Lauf +> **Datum:** 2026-04-20 +> **Commit(s):** ausstehend (Mensch committet nach Sichtung) +> **Status:** ✅ abgeschlossen + +## 1. Zusammenfassung + +Pro Lauf werden nun zwei Ausgabedateien im Verzeichnis der Eingabedatei erzeugt: eine UTF-8-Berichtdatei (`.txt`) und eine Log-Datei (`.log`). Bei Folgeläufen greift die Suffix-Logik (`_v1`, `_v2`, …) unabhängig pro Extension. Die Konsolenausgabe ist identisch zum Berichtinhalt. `mvn clean verify` ist grün (202 Tests, 0 Fehler). + +## 2. Umgesetzte Änderungen + +**Neu angelegt:** + +- `src/main/java/de/gecheckt/asv/adapter/out/filesystem/SuffixResolver.java` — Ermittelt den ersten freien Dateipfad per Suffix-Logik. Probiert `.`, dann `_v1.`, `_v2.` usw.; Zählung pro Extension unabhängig. +- `src/main/java/de/gecheckt/asv/adapter/out/reporting/ReportFileWriter.java` — Schreibt den Validierungsbericht als UTF-8-Textdatei; nutzt `SuffixResolver`; Basisname = vollständiger Dateiname der Eingabedatei inkl. Extension; enthält `ReportWriteResult`-Record für Rückgabe von Inhalt, Pfad und ggf. IOException. +- `src/test/java/de/gecheckt/asv/adapter/out/filesystem/SuffixResolverTest.java` — 10 Unit-Tests: keine Datei, `.txt` vorhanden, `.txt` + `_v1` vorhanden, Extensions unabhängig, drei aufeinanderfolgende Läufe, Null-Guards. +- `src/test/java/de/gecheckt/asv/adapter/out/reporting/ReportFileWriterTest.java` — 10 Unit-Tests: Datei erzeugt, UTF-8-Encoding (Sonderzeichen äöü߀), Kopfzeile (Zeitstempel, Datei, Urteil), Befundzeile (Severity/Kind/Layer/Feld-ID/Meldung), Fußzeile (M1-Platzhalter-Hinweis), Suffix-Logik (zweiter Lauf → `_v1`), UNGÜLTIG-Urteil, Null-Guards. +- `src/test/java/de/gecheckt/asv/adapter/in/cli/CliRunnerOutputArtifactsTest.java` — 5 End-to-End-Integrationstests: Lauf 1 (`foo.auf.txt`), Lauf 2 (`foo.auf_v1.txt`), Lauf 3 (`foo.auf_v2.txt`), Suffix-Unabhängigkeit, UTF-8-Kodierung, Konsolenausgabe ≡ Berichtdatei. + +**Geändert:** + +- `src/main/java/de/gecheckt/asv/adapter/out/logging/LoggingConfigurator.java` — Implementiert `configureLogFile(Path)` mit programmatischer Log4j2-Umkonfiguration; erstellt einen neuen `FileAppender` ("DynamicFile") und hängt ihn an alle vorhandenen LoggerConfigs. Kapselt Log4j2-Typen vollständig in `adapter.out.logging`. +- `src/main/resources/log4j2.xml` — Statischer `logs/asv-format-validator.log` in `logs/asv-format-validator-fallback.log` umbenannt; Kommentar „FALLBACK-Default" ergänzt. Greift nur wenn `configureLogFile` nicht aufgerufen wurde (z.B. Unit-Tests ohne Log-Datei). +- `src/main/java/de/gecheckt/asv/bootstrap/Main.java` — Erzeugt nun `SuffixResolver`, `ReportFileWriter` und übergibt alle vier Adapter an `CliRunner` per Constructor Injection. +- `src/main/java/de/gecheckt/asv/adapter/in/cli/CliRunner.java` — Neuer 4-Parameter-Konstruktor; bestimmt Log-Datei-Pfad via `SuffixResolver` und ruft `configureLogFile` vor dem ersten fachlichen Log-Aufruf auf; ruft nach Validierungslauf `ReportFileWriter.write()` auf; gibt Berichtinhalt identisch auf stdout aus; IO-Fehler beim Datei-Schreiben blockiert Konsolenausgabe nicht. +- `src/test/java/de/gecheckt/asv/adapter/in/cli/CliRunnerTest.java` — Auf neuen 4-Parameter-Konstruktor umgestellt; `LoggingConfigurator` als Mockito-No-Op-Mock, um TempDir-Locking durch geöffnete Log4j2-Appender auf Windows zu vermeiden; 2 neue Tests (Konsolenausgabe, Berichtdatei-Erzeugung). + +## 3. Scope-Treue + +| Scope-Punkt aus dem Arbeitspaket | Erfüllt? | Bemerkung | +|---|---|---| +| `SuffixResolver` in `adapter.out.filesystem` | ✅ | Vollständig implementiert | +| Suffix-Zählung pro Extension unabhängig | ✅ | `.txt` und `.log` haben getrennte Zähler | +| Unit-Tests: keine Datei, `.txt` vorhanden, `.txt`+`_v1` vorhanden | ✅ | Alle drei Testfälle + weitere | +| `ReportFileWriter` in `adapter.out.reporting` | ✅ | Vollständig implementiert | +| Basisname = vollständiger Dateiname inkl. Extension | ✅ | `foo.auf` → `foo.auf.txt` | +| UTF-8 explizit (nicht Plattform-Default) | ✅ | `StandardCharsets.UTF_8` in `ReportFileWriter` | +| Kopfzeile: Zeitstempel (ISO), Eingabedatei, Verdict | ✅ | Alle drei Felder | +| Pro Finding: Severity, Kind, Layer, Feld-ID, deutsche Meldung | ✅ | Format `[SEV] [KIND] [LAYER] Feld=... – Meldung` | +| Fußzeile: Hinweis auf nicht geprüfte Bereiche | ✅ | M1-Platzhalter-Hinweis | +| `LoggingConfigurator.configureLogFile(Path)` | ✅ | Programmatische Umkonfiguration implementiert | +| Log4j2-Typen nur in `adapter.out.logging` und `bootstrap` | ✅ | Verifiziert per grep; CLI-Paket enthält keine Log4j2-Importe | +| Statischer `logs/`-Pfad entfernt/Fallback | ✅ | Auf `logs/asv-format-validator-fallback.log` umbenannt mit Kommentar | +| Integration in `bootstrap.Main` | ✅ | Reihenfolge korrekt: SuffixResolver → configureLogFile → Validierung → ReportFileWriter → Konsolenausgabe | +| Konsolenausgabe nach Berichtdatei | ✅ | IO-Fehler bei Datei blockiert Konsolenausgabe nicht | +| Hierarchische Berichtsgliederung (Scope OUT) | ✅ nicht gemacht | M9 | +| ANSI-Farben (Scope OUT) | ✅ nicht gemacht | — | +| Log-Rotation (Scope OUT) | ✅ nicht gemacht | — | +| Minimalbericht bei Exit 2 (Scope OUT) | ✅ nicht gemacht | AP08 | + +**Wurde der Scope eingehalten?** Ja, vollständig. + +**Wurden Dinge außerhalb des Scopes gemacht?** Nein. + +## 4. Abnahmekriterien + +| Abnahmekriterium aus dem Arbeitspaket | Erfüllt? | Nachweis | +|---|---|---| +| Nach Lauf mit `foo/bar.auf` entstehen `foo/bar.auf.txt` und `foo/bar.auf.log` | ✅ | E2E-Test (JAR): `test.auf` → `test.auf.txt` + `test.auf.log` bestätigt; `CliRunnerOutputArtifactsTest#lauf1_erzeugtBerichtdateiOhneSuffix()` | +| Zweiter Lauf → `_v1.txt` und `_v1.log` | ✅ | E2E-Test (JAR): `test.auf_v1.txt` + `test.auf_v1.log` bestätigt; `CliRunnerOutputArtifactsTest#lauf2_erzeugtBerichtdateiMitV1Suffix()` | +| Dritter Lauf → `_v2` | ✅ | E2E-Test (JAR): `test.auf_v2.txt` + `test.auf_v2.log` bestätigt; `CliRunnerOutputArtifactsTest#lauf3_erzeugtBerichtdateiMitV2Suffix()` | +| Beide Ausgaben UTF-8 | ✅ | `file test.auf.txt` → `Unicode text, UTF-8`; `ReportFileWriterTest#berichtdatei_istInUtf8()` mit `äöü߀`; `CliRunnerOutputArtifactsTest#berichtdatei_istInUtf8()` mit `GÜLTIG` | +| Konsolenausgabe identisch zum Berichtdatei-Inhalt | ✅ | `CliRunnerOutputArtifactsTest#konsolenausgabe_identischZumBerichtinhalt()` — direkter String-Vergleich | +| `SuffixResolver` hat ≥3 Unit-Tests | ✅ | 10 Tests in `SuffixResolverTest` | +| Log4j2-Typen nicht außerhalb `adapter.out.logging` und `bootstrap` | ✅ | Grep auf `import org.apache.logging.log4j` im `src/main/java/` ohne `adapter/out/logging` und `bootstrap` ergibt leer | +| Statischer `logs/`-Pfad aus `log4j2.xml` entfernt oder Fallback | ✅ | `log4j2.xml`: `logs/asv-format-validator-fallback.log`; Kommentar „FALLBACK-Default" | +| `mvn clean verify` grün | ✅ | 202 Tests, 0 Failures, 0 Errors | +| Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP07-bericht.md` | ✅ | Diese Datei | + +## 5. Build- und Teststatus + +- `mvn clean verify`: ✅ grün +- Anzahl Tests gesamt: **202** (davon **29 neu** in AP07: 10 `SuffixResolverTest`, 10 `ReportFileWriterTest`, 2 neue in `CliRunnerTest`, 5 in `CliRunnerOutputArtifactsTest`, 2 in `CliRunnerTest` erweitert) +- Vorherige Testanzahl (vor AP07, nach AP09): 173 +- Coverage: JaCoCo läuft; neue Klassen durch Tests abgedeckt +- Warnungen beim Build: identisch zu AP06/AP09 (Shade-Plugin META-INF-Überlappungen, `sun.reflect.Reflection.getCallerClass`-Warnung zur Laufzeit) — keine neuen Warnungen + +## 6. Rest-Risiken und offene Punkte + +- **TempDir-Locking auf Windows durch Log4j2-FileAppender:** Wenn der echte `LoggingConfigurator` in Tests aufgerufen wird, hält Log4j2 den File-Appender für die Log-Datei im TempDir offen. JUnit 5 kann das TempDir dann nach dem Test nicht löschen. Lösung in `CliRunnerTest` und `CliRunnerOutputArtifactsTest`: `LoggingConfigurator` als Mockito-No-Op-Mock. Die echte Umkonfiguration ist durch den manuellen E2E-Test mit dem Uber-JAR verifiziert. AP10 (Architekturtest) könnte dieses Verhalten formell absichern. + +- **Fallback-Entscheidung: Programmatische Log4j2-Umkonfiguration vs. System-Property:** Die programmatische Umkonfiguration über `LoggerContext`/`FileAppender.newBuilder()` ist stabil und funktioniert. Fallback (`-Dasv.log.file=...`) wurde nicht implementiert, da nicht nötig. Entscheidung: programmatisch, kein Fallback. + +- **Fallback-Datei `logs/asv-format-validator-fallback.log`:** Der Fallback-Appender in `log4j2.xml` schreibt bei Unit-Tests (ohne Eingabedatei-Pfad) in `logs/asv-format-validator-fallback.log`. Dieses Verzeichnis wird automatisch durch Log4j2 angelegt. Die `.gitignore`-Einträge `logs/` decken diese Datei ab. + +- **Race Conditions:** Gleichzeitige Läufe auf derselben Eingabedatei können zu Suffix-Konflikten führen. Laut `technik-und-architektur.md` bewusst nicht behandelt (kein Mehrbenutzerbetrieb in V1). + +- **Berichtformat absichtlich minimal:** Das M1-Format (Kopfzeile, Befundzeilen, Fußzeile) wird in M9 durch eine finale hierarchische Gliederung ersetzt. + +- **Konsolenausgabe-Encoding auf Windows:** Die Konsolenausgabe (`System.out.print`) zeigt auf Windows-Konsolen Mojibake für Umlaute (`G�LTIG`), weil Windows-Konsolen oft CP1252/OEM437 nutzen. Die Datei selbst ist korrekt UTF-8. Dies ist ein bekanntes Konsolen-Encoding-Problem unter Windows und kein Fehler des Validators. Empfehlung für AP11: Hinweis in der Benutzer-Dokumentation. + +## 7. Empfehlungen für Folge-Arbeitspakete + +- **AP08 (Minimalbericht bei Exit 2):** `CliRunner` gibt bei Bedienfehler bisher nur STDERR-Meldungen aus. AP08 kann hier anknüpfen und einen Minimalbericht erzeugen. Der `ReportFileWriter` ist dafür bereit. +- **AP10 (Architekturtest):** ArchUnit sollte sicherstellen, dass keine Log4j2-Typen außerhalb `adapter.out.logging` und `bootstrap` importiert werden. `SuffixResolver` und `ReportFileWriter` haben keine Infrastrukturabhängigkeiten — AP10 sollte diese Kapselung formell erzwingen. +- **AP11 (M1-Abnahme):** End-to-End-Test kann nun auf die Ausgabedateien prüfen: `exit 0`, `foo.auf.txt` existiert, enthält `GÜLTIG`, ist UTF-8; Zweiter Lauf erzeugt `foo.auf_v1.txt`. +- **M9 (Berichtformat ausbauen):** `ReportFileWriter.buildReportContent()` ist der Erweiterungspunkt. Die `ReportWriteResult`-Signatur kann unverändert bleiben. + +## 8. Reviewer-Checkliste + +- [x] Alle im Arbeitspaket genannten Scope-IN-Punkte sind nachweislich umgesetzt +- [x] Keine Scope-OUT-Punkte wurden angefasst +- [x] Abnahmekriterien sind mit konkreten Nachweisen belegt (Tests, E2E-JAR-Läufe, Dateipfade) +- [x] `mvn clean verify` ist grün (202 Tests, 0 Failures) +- [ ] Der Commit für dieses AP hat eine sprechende Message (`M1-AP07: ...`) — ausstehend, Mensch committet +- [x] Keine Regeln der Grunddokumente (Spec, Fachliche, Technik) wurden verletzt +- [x] Rest-Risiken sind ehrlich dokumentiert diff --git a/docs/arbeitspakete/m1/berichte/AP08-bericht.md b/docs/arbeitspakete/m1/berichte/AP08-bericht.md new file mode 100644 index 0000000..ca2e14a --- /dev/null +++ b/docs/arbeitspakete/m1/berichte/AP08-bericht.md @@ -0,0 +1,115 @@ +# Abschlussbericht Arbeitspaket AP08 – Minimalbericht bei Bedienfehlern (Exit-Code 2) + +> **Bezug:** `docs/arbeitspakete/m1/AP08-minimalbericht.md` +> **Bearbeiter:** Claude Code (claude-sonnet-4-6), Subagent-Lauf +> **Datum:** 2026-04-20 +> **Commit(s):** ausstehend (Mensch committet nach Sichtung) +> **Status:** ✅ abgeschlossen + +## 1. Zusammenfassung + +Alle fünf Bedienfehler-Fälle erzeugen nun einen `ValidationReport` mit `Verdict.OPERATIONAL_ERROR` und geben den Minimalbericht auf STDERR aus. Wo das übergeordnete Verzeichnis bekannt und schreibbar ist (Fälle 3 und 5), wird die Berichtdatei zusätzlich als `.txt`-Datei geschrieben. `mvn clean verify` ist grün (218 Tests, 0 Failures, 1 Skipped auf Windows). + +## 2. Umgesetzte Änderungen + +**Geändert:** + +- `src/main/java/de/gecheckt/asv/adapter/in/cli/CliRunner.java` + - Alle fünf Bedienfehler-Fälle erzeugen jetzt `ValidationReport.operationalError(...)` mit den definierten `ruleId`-Werten + - Zwei neue private Hilfsmethoden: `writeMinimalReportToConsoleOnly(report)` (STDERR-only) und `writeMinimalReportWithOptionalFile(report, directory, baseName)` (STDERR + optionale Datei) + - Fälle 1 (kein Arg), 2 (zu viele Args) und 4 (kein regulärer Dateityp) → nur Konsole + - Fälle 3 (Datei nicht gefunden) und 5 (Datei nicht lesbar) → Konsole + Berichtdatei im übergeordneten Verzeichnis, sofern schreibbar + - Alle Bedienfehler werden via `logger.error(...)` protokolliert + - Platzhalter-Konstanten: `` und `` für den `fileName`-Parameter + +- `src/main/java/de/gecheckt/asv/adapter/out/reporting/ReportFileWriter.java` + - Neue `public`-Methode `writeOperationalError(ValidationReport, Path, String)`: Schreibt Bedienfehler-Bericht direkt in ein angegebenes Verzeichnis; IO-Fehler lösen keine `RuntimeException` aus, sondern werden geloggt und als fehlgeschlagenes `ReportWriteResult` zurückgegeben + - Neue `public`-Methode `buildMinimalReportContent(ValidationReport)`: Erzeugt Berichtinhalt ohne `Path`-Parsing, damit Platzhalter mit Sonderzeichen (``) funktionieren + - `buildReportContent(ValidationReport, Path)` delegiert jetzt an die neue private `buildReportContentWithFileName(ValidationReport, String)` — kein Duplizierungsrisiko + - Befundzeilen zeigen jetzt auch `Regel=`, wenn eine `ruleId` gesetzt ist + +**Neu angelegt:** + +- `src/test/java/de/gecheckt/asv/adapter/in/cli/CliRunnerOperationalErrorTest.java` + - 16 Tests, 1 Skipped (Fall 5 auf Windows) + - Alle fünf Bedienfehler-Fälle getestet (Exit-Code 2) + - Korrekte `ruleId`-Werte für alle fünf Fälle verifiziert + - `Verdict.OPERATIONAL_ERROR` in dediziertem Test verifiziert + - Drei Negativ-Tests: kein Stack-Trace in STDERR + - Fall 3: Berichtdatei im übergeordneten Verzeichnis vorhanden und enthält `BEDIENFEHLER` + `OPERATIONAL-FILE-NOT-FOUND` + +## 3. Scope-Treue + +| Scope-Punkt aus dem Arbeitspaket | Erfüllt? | Bemerkung | +|---|---|---| +| Fall 1: Kein Argument → Exit 2 + nur Konsole | ✅ | `writeMinimalReportToConsoleOnly` | +| Fall 2: Mehr als ein Argument → Exit 2 + nur Konsole | ✅ | `writeMinimalReportToConsoleOnly` | +| Fall 3: Datei nicht gefunden → Exit 2 + Konsole + Datei | ✅ | `writeMinimalReportWithOptionalFile` | +| Fall 4: Kein regulärer Dateityp → Exit 2 + nur Konsole | ✅ | `writeMinimalReportToConsoleOnly` | +| Fall 5: Datei nicht lesbar → Exit 2 + Konsole + Datei | ✅ | Nur auf Unix; Windows-Test übersprungen | +| `ruleId`-Werte: OPERATIONAL-MISSING-ARG, OPERATIONAL-TOO-MANY-ARGS, OPERATIONAL-FILE-NOT-FOUND, OPERATIONAL-NOT-REGULAR, OPERATIONAL-NOT-READABLE | ✅ | Alle fünf implementiert und getestet | +| Kein Stack-Trace für den Nutzer | ✅ | Drei Negativ-Tests | +| `logger.error(...)` für Bedienfehler | ✅ | In jedem Fall vorhanden | +| `ReportFileWriter`: weiche IO-Fehlerbehandlung bei OPERATIONAL_ERROR | ✅ | `writeOperationalError` ohne RuntimeException | +| Konsole-Hinweis wenn Verzeichnis nicht schreibbar | ✅ | „Bericht konnte nicht in das Verzeichnis geschrieben werden." | +| Unit-Tests für alle fünf Fälle: Exit 2 + ruleId | ✅ | Alle fünf Fälle im neuen Test | +| Test Fall 3: Berichtdatei im übergeordneten Verzeichnis | ✅ | Test vorhanden und grün | +| `Verdict.OPERATIONAL_ERROR` in Test verifiziert | ✅ | `operationalErrorReport_verdictIstOPERATIONAL_ERROR` | +| `ValidationReport.operationalError(...)` vollständig (Layer ARTIFACT) | ✅ | War bereits in AP05 vollständig implementiert | +| Feingranulare IO-Exception-Unterscheidung (Scope OUT) | ✅ nicht gemacht | Einheitlich „nicht lesbar" | +| Internationalisierung (Scope OUT) | ✅ nicht gemacht | — | +| Exit-Codes jenseits 0/1/2 (Scope OUT) | ✅ nicht gemacht | — | + +**Wurde der Scope eingehalten?** Ja, vollständig. + +**Wurden Dinge außerhalb des Scopes gemacht?** +Die `buildReportContent`-Methode wurde intern auf `buildReportContentWithFileName` umgestellt und gibt jetzt auch `ruleId`-Werte aus. Das ist eine minimale Erweiterung des Berichtformats, aber direkt notwendig für den AP08-Abnahmetest (`fall3_dateiExistiertNicht_berichtdateiEnthaeltOpertionalError` prüft auf `OPERATIONAL-FILE-NOT-FOUND` im Berichtinhalt). Kein Code außerhalb des erlaubten Scope wurde berührt. + +## 4. Abnahmekriterien + +| Abnahmekriterium aus dem Arbeitspaket | Erfüllt? | Nachweis | +|---|---|---| +| Alle fünf Bedienfehler-Fälle erzeugen Exit-Code 2 (per Unit-Test) | ✅ | `CliRunnerOperationalErrorTest`: fall1…fall5, alle grün (fall5 Skipped auf Windows) | +| Fall „kein Argument" → nur Konsolenausgabe, keine Dateiausgabe | ✅ | `fall1_keinArgument_nurKonsole`: `countTxtFiles(tempDir) == 0` | +| Fall „Datei nicht vorhanden" → Berichtdatei im übergeordneten Verzeichnis | ✅ | `fall3_dateiExistiertNicht_berichtdateiImUebergeordnetenVerzeichnis` + `fall3_dateiExistiertNicht_berichtdateiEnthaeltOpertionalError` | +| `Verdict.OPERATIONAL_ERROR` in mindestens einem Test verifiziert | ✅ | `operationalErrorReport_verdictIstOPERATIONAL_ERROR` und `alleRuleIds_sindKorrektDefiniert` | +| Kein Stack-Trace in STDERR (Negativ-Test vorhanden) | ✅ | `keinArgument_keinStackTraceInStderr`, `dateiNichtGefunden_keinStackTraceInStderr`, `pfadIstVerzeichnis_keinStackTraceInStderr` | +| `mvn clean verify` grün | ✅ | 218 Tests, 0 Failures, 0 Errors, 1 Skipped | +| Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP08-bericht.md` | ✅ | Diese Datei | + +## 5. Build- und Teststatus + +- `mvn clean verify`: ✅ grün +- Anzahl Tests gesamt: **218** (davon **16 neu** in `CliRunnerOperationalErrorTest`) +- Vorherige Testanzahl (vor AP08, nach AP07/AP09): 202 +- 1 Test Skipped: `fall5_dateiNichtLesbar_exitCode2` — Windows-bedingt; `setReadable(false)` hat auf Windows keine Wirkung für den eigenen Prozess. Der Test enthält `assumeTrue(...)` zum expliziten Überspringen. +- Coverage: JaCoCo läuft; neue Methoden in `CliRunner` und `ReportFileWriter` sind durch die neuen Tests abgedeckt +- Warnungen beim Build: identisch zu AP06/AP07 (Shade-Plugin META-INF-Überlappungen) — keine neuen Warnungen + +## 6. Rest-Risiken und offene Punkte + +- **Fall 5 (Datei nicht lesbar) nur auf Unix testbar:** Auf Windows kann `setReadable(false)` eine Datei nicht für den eigenen Prozess unlesbar machen. Der Test wird explizit übersprungen. Die Implementierung in `CliRunner` ist korrekt und folgt demselben Muster wie Fall 3 (der auf beiden Plattformen vollständig getestet wird). Das Verhalten bei echten Berechtigungsfehlern auf Windows (z.B. NTFS-ACL) ist vom aktuellen Test nicht abgedeckt. + +- **`buildReportContent`-Refactoring:** Die Umstellung auf `buildReportContentWithFileName` ist intern und ändert das nach außen sichtbare Verhalten nicht. Bestehende Tests in `ReportFileWriterTest` wurden nicht gebrochen. Die neu hinzugefügte `ruleId`-Ausgabe (`Regel=...`) in der Befundzeile ist eine Erweiterung des M1-Formats — M9 wird das Format ohnehin final gestalten. + +- **Platzhalter `` und ``:** Diese Strings enthalten `<>`, die auf Windows als ungültige Pfadzeichen gelten. Durch den Wechsel auf `buildReportContentWithFileName` (kein `Path.of(...)`) ist das Problem gelöst. AP10 (Architekturtest) sollte sicherstellen, dass `ReportFileWriter` kein `Path.of(report.getFileName())` mehr aufruft. + +- **Konsolen-Encoding-Problem (Windows, bekannt aus AP07):** Die STDERR-Ausgabe des Minimalberichts enthält Umlaute (`BEDIENFEHLER`, `Fehler`). Auf Windows-Konsolen mit CP1252/OEM437 können diese als Mojibake erscheinen. Die Berichtdatei ist korrekt UTF-8. Bekanntes Windows-Konsolen-Problem, nicht AP08-Scope. + +## 7. Empfehlungen für Folge-Arbeitspakete + +- **AP10 (Architekturtest):** ArchUnit sollte sicherstellen, dass `ReportFileWriter` kein `Path.of(report.getFileName())` aufruft (wurde in AP08 explizit vermieden). Die drei Negativ-Stack-Trace-Tests könnten als Basis für eine formale „kein Exception-Stacktrace in STDERR"-Regel dienen. + +- **AP11 (M1-Abnahme):** End-to-End-Test kann die Bedienfehler-Szenarien prüfen: kein Arg → Exit 2, Berichtdatei nur wenn Verzeichnis bekannt, BEDIENFEHLER im Bericht. + +- **M9 (Berichtformat ausbauen):** Die neue `buildReportContentWithFileName`-Methode ist der Erweiterungspunkt für das finale Berichtformat. Die `ruleId`-Ausgabe (`Regel=...`) ist ein Vorgriff und kann in M9 in das finale Format integriert werden. + +## 8. Reviewer-Checkliste + +- [x] Alle im Arbeitspaket genannten Scope-IN-Punkte sind nachweislich umgesetzt +- [x] Keine Scope-OUT-Punkte wurden angefasst +- [x] Abnahmekriterien sind mit konkreten Nachweisen belegt (Tests, Dateipfade) +- [x] `mvn clean verify` ist grün (218 Tests, 0 Failures) +- [ ] Der Commit für dieses AP hat eine sprechende Message (`M1-AP08: ...`) — ausstehend, Mensch committet +- [x] Keine Regeln der Grunddokumente (Spec, Fachliche, Technik) wurden verletzt +- [x] Rest-Risiken sind ehrlich dokumentiert diff --git a/docs/arbeitspakete/m1/berichte/AP09-bericht.md b/docs/arbeitspakete/m1/berichte/AP09-bericht.md new file mode 100644 index 0000000..1381ded --- /dev/null +++ b/docs/arbeitspakete/m1/berichte/AP09-bericht.md @@ -0,0 +1,134 @@ +# Abschlussbericht Arbeitspaket AP09 – Altlogik einfrieren (Preview-Code deaktivieren) + +> **Bezug:** `docs/arbeitspakete/m1/AP09-altlogik-einfrieren.md` +> **Bearbeiter:** Claude Code (claude-sonnet-4-6), Subagent-Lauf +> **Datum:** 2026-04-20 +> **Commit(s):** ausstehend (Mensch committet nach Sichtung) +> **Status:** ✅ abgeschlossen + +## 1. Zusammenfassung + +Die Preview-Validatoren `DefaultStructureValidator` und `DefaultFieldValidator` wurden formell eingefroren: Beide erhalten den normativen JavaDoc-Einfriermarker, und zwei M1-Platzhalter-Implementierungen (`NoOpStructureValidator`, `NoOpFieldValidator`) wurden im Paket `de.gecheckt.asv.bootstrap` angelegt. Ein Integrationstest belegt, dass kein aktiver Lauf über `DefaultInputFileValidator` mit NoOp-Verdrahtung ASVREC-/ASVFEH-Segmentbefunde erzeugt. Der Grep-Nachweis bestätigt: keine funktionale Verdrahtung der Preview-Klassen in `adapter` oder `bootstrap`. `mvn clean verify` ist grün (173 Tests, 0 Fehler). + +## 2. Umgesetzte Änderungen + +**Neu angelegt:** + +- `src/main/java/de/gecheckt/asv/bootstrap/NoOpStructureValidator.java` — M1-Platzhalter; implementiert `StructureValidator`, gibt stets leeres `ValidationResult` zurück; JavaDoc auf Deutsch; null-Guard vorhanden. +- `src/main/java/de/gecheckt/asv/bootstrap/NoOpFieldValidator.java` — M1-Platzhalter; implementiert `FieldValidator`, gibt stets leeres `ValidationResult` zurück; JavaDoc auf Deutsch; null-Guard vorhanden. +- `src/test/java/de/gecheckt/asv/bootstrap/NoOpValidatorsIntegrationTest.java` — 5 Tests: ASVREC-Struktur → keine Befunde, ASVFEH-Struktur → keine Befunde, leere Eingabedatei → keine Befunde, null-Guard NoOpStructureValidator, null-Guard NoOpFieldValidator. + +**Geändert (nur JavaDoc, keine Logik):** + +- `src/main/java/de/gecheckt/asv/application/structure/DefaultStructureValidator.java` — Einfriermarker-JavaDoc ergänzt: „M3-Vorbau. In M1 bewusst nicht im produktiven Lauf verdrahtet. Wird ab M3 wieder aktiviert und gegen die finalen Regelklassifikationen (V1-V/T/N/K) aus fachliche-anforderungen.md bewertet. @see E-01". +- `src/main/java/de/gecheckt/asv/application/field/DefaultFieldValidator.java` — identischer Einfriermarker-JavaDoc ergänzt. + +**Gelöscht:** + +- `src/test/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplicationTest.java` — seit AP06 leere Hülle (keine `@Test`-Methoden), gemäß AP06-Bericht für AP09 vorgesehen. +- `src/test/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplicationAdditionalTest.java` — seit AP06 leere Hülle (keine `@Test`-Methoden), gemäß AP06-Bericht für AP09 vorgesehen. + +**Nicht geändert:** + +- `src/main/java/de/gecheckt/asv/bootstrap/Main.java` — `Main` verdrahtet bereits seit AP06 ausschließlich `DummyFileValidationService`; kein Preview-Validator war dort nach AP06 noch verdrahtet. Keine Änderung erforderlich. + +## 3. Scope-Treue + +| Scope-Punkt aus dem Arbeitspaket | Erfüllt? | Bemerkung | +|---|---|---| +| `NoOpStructureValidator` anlegen | ✅ | `bootstrap`-Paket, leeres `ValidationResult`, JavaDoc Deutsch | +| `NoOpFieldValidator` anlegen | ✅ | `bootstrap`-Paket, leeres `ValidationResult`, JavaDoc Deutsch | +| Bootstrap-Verdrahtung: Preview-Validatoren durch NoOps ersetzen | ✅ | `Main` nutzte nach AP06 schon keine Preview-Validatoren mehr; formale NoOps bereit für `DefaultInputFileValidator`-Pfad in M3 | +| Einfriermarker-JavaDoc in `DefaultStructureValidator` | ✅ | Nur JavaDoc ergänzt, keine Logikänderung | +| Einfriermarker-JavaDoc in `DefaultFieldValidator` | ✅ | Nur JavaDoc ergänzt, keine Logikänderung | +| `DefaultStructureValidatorTestAdditional` löschen | ⚠️ | Klasse enthält 3 aktive `@Test`-Methoden — sie ist NICHT leer (entgegen AP09-Aussage „leere Testklasse"); Löschen wäre ein Scope-Verstoß gegen „aktive Tests der Preview-Klassen bleiben grün". Bewusst nicht gelöscht. Siehe Abschnitt 6. | +| `logs/` in `.gitignore` | ✅ | Bereits durch AP06 eingetragen; keine Änderung erforderlich | +| Aktive Tests der Preview-Klassen bleiben grün | ✅ | 7 Testklassen, 70 Tests für `DefaultStructureValidator` und 9 Tests für `DefaultFieldValidator` weiterhin grün | +| Integrationstest: Lauf erzeugt keine ASVREC-/ASVFEH-Segmentbefunde | ✅ | `NoOpValidatorsIntegrationTest` mit 5 Tests | +| Grep-Nachweis leer (funktionale Verdrahtung) | ✅ | Nur JavaDoc-`@see`-Referenzen in NoOp-Klassen; keine Code-Verdrahtung. Nachweis in Abschnitt 4. | +| Paketumzug der Preview-Klassen (Scope OUT) | ✅ nicht gemacht | Klassen in ihren Originalpaketen belassen | +| Inhaltliche Änderung an `DefaultStructureValidator`/`DefaultFieldValidator` (Scope OUT) | ✅ nicht gemacht | Nur JavaDoc ergänzt | +| Fachliche Neubewertung der 19 Preview-Regeln (Scope OUT) | ✅ nicht gemacht | M3-Aufgabe | +| Löschen von Preview-Klassen (Scope OUT) | ✅ nicht gemacht | Beide Klassen physisch erhalten | + +**Wurde der Scope eingehalten?** Ja, mit einer begründeten Abweichung bei `DefaultStructureValidatorTestAdditional` (siehe Abschnitt 6). + +**Wurden Dinge außerhalb des Scopes gemacht?** Die leeren Hüllen `AsvValidatorApplicationTest` und `AsvValidatorApplicationAdditionalTest` wurden gelöscht. Dies ist im AP06-Bericht §7 explizit als Aufgabe für AP09 vorgesehen und fällt daher in den Scope. + +## 4. Abnahmekriterien + +| Abnahmekriterium aus dem Arbeitspaket | Erfüllt? | Nachweis | +|---|---|---| +| `NoOpStructureValidator` und `NoOpFieldValidator` existieren | ✅ | `bootstrap/NoOpStructureValidator.java`, `bootstrap/NoOpFieldValidator.java` | +| `bootstrap.Main` verdrahtet keine Preview-Validatoren mehr | ✅ | `Main.java` enthält keinen Import von `DefaultStructureValidator` oder `DefaultFieldValidator`; verdrahtet ausschließlich `DummyFileValidationService` | +| Grep auf `DefaultStructureValidator`/`DefaultFieldValidator` in `adapter` und `bootstrap` ist leer (Nachweis) | ✅ (mit Einschränkung) | Kommando und Ergebnis: siehe unten. Keine Code-Verdrahtung; nur JavaDoc-Referenzen in NoOp-Klassen. | +| Einfriermarker-JavaDoc in `DefaultStructureValidator` | ✅ | JavaDoc-Block Zeile 1–19 in `DefaultStructureValidator.java` | +| Einfriermarker-JavaDoc in `DefaultFieldValidator` | ✅ | JavaDoc-Block Zeile 1–19 in `DefaultFieldValidator.java` | +| `DefaultStructureValidatorTestAdditional` gelöscht | ⚠️ | Klasse enthält 3 aktive Tests — nicht gelöscht (Begründung in Abschnitt 6) | +| Bestehende Tests der Preview-Klassen laufen weiterhin grün | ✅ | 7 Testklassen für `DefaultStructureValidator` (70 Tests), 1 Testklasse für `DefaultFieldValidator` (9 Tests): alle grün | +| Integrationstest: Lauf mit Testdatei erzeugt keine ASVREC-/ASVFEH-Segmentbefunde | ✅ | `NoOpValidatorsIntegrationTest#asvrecStruktur_erzeugtKeineBefunde()` — GRÜN; `@DisplayName("KRITISCH: NoOp-Validatoren erzeugen keine Befunde für ASVREC-Struktur")` | +| `mvn clean verify` grün | ✅ | 173 Tests, 0 Failures, 0 Errors, 0 Skipped | +| Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP09-bericht.md` | ✅ | Diese Datei | + +### Grep-Nachweis + +Ausgeführtes Kommando: +``` +grep -rn "DefaultStructureValidator\|DefaultFieldValidator" \ + src/main/java/de/gecheckt/asv/adapter \ + src/main/java/de/gecheckt/asv/bootstrap +``` + +Ergebnis (vollständig): +``` +src/main/java/de/gecheckt/asv/bootstrap/NoOpFieldValidator.java:15: *

Ab M3 durch {@link de.gecheckt.asv.application.field.DefaultFieldValidator} +src/main/java/de/gecheckt/asv/bootstrap/NoOpFieldValidator.java:19: * @see de.gecheckt.asv.application.field.DefaultFieldValidator +src/main/java/de/gecheckt/asv/bootstrap/NoOpStructureValidator.java:15: *

Ab M3 durch {@link de.gecheckt.asv.application.structure.DefaultStructureValidator} +src/main/java/de/gecheckt/asv/bootstrap/NoOpStructureValidator.java:19: * @see de.gecheckt.asv.application.structure.DefaultStructureValidator +``` + +**Bewertung:** Alle vier Treffer sind ausschließlich JavaDoc-Kommentare (`{@link ...}` und `@see`) — keine Code-Verdrahtung, keine Instanziierung, kein `import`-Statement im Produktionspfad. Der aktive Code referenziert die Preview-Klassen nicht. Das Abnahmekriterium „aktiver Code darf diese Klassen nicht mehr referenzieren" ist erfüllt. + +## 5. Build- und Teststatus + +- `mvn clean verify`: ✅ grün +- Anzahl Tests gesamt: **173** (davon **5 neu** in `NoOpValidatorsIntegrationTest`) +- Vorherige Testanzahl (vor AP09, nach AP06): 168 +- Preview-Tests weiterhin grün: + - `DefaultStructureValidatorTest`: 22 Tests ✅ + - `DefaultStructureValidatorAsvfehFhlSegmentTest`: 8 Tests ✅ + - `DefaultStructureValidatorAsvrecRechnungsbetragTest`: 7 Tests ✅ + - `DefaultStructureValidatorAsvrecRechnungskennzeichenTest`: 12 Tests ✅ + - `DefaultStructureValidatorAsvrecSegmentCardinalityTest`: 9 Tests ✅ + - `DefaultStructureValidatorAsvrecSegmentOrderTest`: 5 Tests ✅ + - `DefaultStructureValidatorAsvrecSegmentsTest`: 7 Tests ✅ + - `DefaultStructureValidatorTestAdditional`: 3 Tests ✅ (bewusst nicht gelöscht, siehe Abschnitt 6) + - `DefaultFieldValidatorTest`: 9 Tests ✅ + - **Summe Preview-Tests: 82 Tests, alle grün** +- Warnungen beim Build: dieselben wie nach AP06 (Shade-Plugin META-INF-Überlappungen, `sun.reflect.Reflection.getCallerClass`, Annotationsverarbeitung) — keine neuen Warnungen. + +## 6. Rest-Risiken und offene Punkte + +- **`DefaultStructureValidatorTestAdditional` enthält aktive Tests:** AP09 bezeichnet diese Klasse als „leere Testklasse ohne `@Test`-Methoden" (gemäß AP00-Ist-Analyse). Zum Zeitpunkt des AP09-Laufs enthält sie jedoch 3 aktive `@Test`-Methoden (`validate_shouldNotReportErrorWhenMessageTypeIsASVREC`, `validate_shouldNotReportErrorWhenMessageTypeIsASVFEH`, `validate_shouldReportErrorWhenMessageTypeIsInvalid`). Löschen würde gegen das AP09-Kriterium „aktive Tests der Preview-Klassen bleiben grün" verstoßen. Entscheidung: nicht gelöscht. Empfehlung an Reviewer: Falls die Klasse in einem früheren AP manuell befüllt wurde und die Tests inhaltlich korrekt sind (was sie sind — sie testen STRUCTURE_012-Logik), kann die Klasse als reguläre Testklasse verbleiben. Falls sie tatsächlich gelöscht werden soll, muss ein separater Auftrag mit explizitem OK für den Testverlust erteilt werden. + +- **M3-Reaktivierung:** Bei der Wiederaufnahme in M3 ist jede der 19 Preview-Regeln in `DefaultStructureValidator` neu gegen V1-V/T/N/K-Klassifikation zu bewerten. Dies ist kein Risiko von AP09, aber ein kritisches M3-Erbe. + +- **`DefaultInputFileValidator` wird in M1 nicht im aktiven Lauf verwendet:** `Main.java` verdrahtet `DummyFileValidationService` direkt; `DefaultInputFileValidator` ist zwar funktional vorhanden und vollständig getestet, aber nicht in den Aufrufpfad eingebunden. In M3 muss `Main.java` auf einen echten `FileValidationService` mit `DefaultInputFileValidator` und den reaktivierten Preview-Validatoren umgestellt werden. + +- **`ValidationResult` und `ValidationSeverity` (Altmodell) koexistieren weiterhin:** Die Preview-Klassen und ihre Tests nutzen das Altmodell `application.model.*` (nicht `domain.finding.*`). Dieser Überstand ist bewusst — AP09 soll nur einfrieren, nicht umbauen. Die Ablösung des Altmodells ist M3-Scope. + +## 7. Empfehlungen für Folge-Arbeitspakete + +- **AP10 (Architekturtest):** Der Einfrierzustand ist formal durch den Grep-Nachweis und den Integrationstest belegt. Ein ArchUnit-Test könnte zusätzlich sicherstellen, dass `adapter` und `bootstrap` keine direkte Abhängigkeit auf `application.structure.DefaultStructureValidator` oder `application.field.DefaultFieldValidator` haben. Dies wäre ein starkes formales Gate gegen versehentliche Reaktivierung. +- **AP11 (M1-Abnahme):** Der M1-Lauf erzeugt keine Fachbefunde mehr. `DummyFileValidationService` liefert stets einen leeren `ValidationReport`. AP11 kann dies in einem End-to-End-Test mit einer Minimaldatei bestätigen (Exit-Code 0, kein Bericht mit Findings). +- **M3:** Beim Wiederaufnehmen die `NoOpStructureValidator`/`NoOpFieldValidator`-Klassen im `bootstrap`-Paket durch die echten Implementierungen ersetzen und `DefaultInputFileValidator` in `Main.java` verdrahten. `DefaultStructureValidatorTestAdditional` kann dann ggf. gelöscht oder in eine reguläre Testklasse umbenannt werden. + +## 8. Reviewer-Checkliste + +- [x] Alle im Arbeitspaket genannten Scope-IN-Punkte sind nachweislich umgesetzt (mit begründeter Abweichung bei `DefaultStructureValidatorTestAdditional`) +- [x] Keine Scope-OUT-Punkte wurden angefasst +- [x] Abnahmekriterien sind mit konkreten Nachweisen belegt (Grep-Nachweis, Testklassen, Dateipfade) +- [x] `mvn clean verify` ist grün (173 Tests, 0 Failures) +- [ ] Der Commit für dieses AP hat eine sprechende Message (`M1-AP09: ...`) — ausstehend, Mensch committet +- [x] Keine Regeln der Grunddokumente (Spec, Fachliche, Technik) wurden verletzt +- [x] Rest-Risiken sind ehrlich dokumentiert diff --git a/docs/arbeitspakete/m1/berichte/AP10-bericht.md b/docs/arbeitspakete/m1/berichte/AP10-bericht.md new file mode 100644 index 0000000..74d7663 --- /dev/null +++ b/docs/arbeitspakete/m1/berichte/AP10-bericht.md @@ -0,0 +1,84 @@ +# Abschlussbericht Arbeitspaket AP10 – Architekturtest + +> **Bezug:** `docs/arbeitspakete/m1/AP10-architekturtest.md` +> **Bearbeiter:** Claude Code (claude-sonnet-4-6), Subagent +> **Datum:** 2026-04-20 +> **Commit(s):** noch offen (Mensch committet nach Sichtung) +> **Status:** ✅ abgeschlossen + +## 1. Zusammenfassung + +ArchUnit 1.3.0 wurde als Test-Dependency aufgenommen und die Architekturtest-Klasse `de.gecheckt.asv.ArchitectureTest` mit vier Regeln (A–D) implementiert. Alle vier Regeln waren beim ersten Lauf **grün** — es wurden keine Verstöße gefunden. Zusätzlich wurde `src/test/resources/log4j2-test.xml` angelegt (E-02). + +## 2. Umgesetzte Änderungen + +- `pom.xml` — ArchUnit-Dependency `com.tngtech.archunit:archunit-junit5:1.3.0` (test scope) ergänzt +- `src/test/java/de/gecheckt/asv/ArchitectureTest.java` — neue Testklasse mit Regeln A–D (`@AnalyzeClasses`, `@ArchTest`) +- `src/test/resources/log4j2-test.xml` — Test-Log-Konfiguration angelegt (Root level WARN, Console/SYSTEM_ERR); Log4j2 bevorzugt diese Datei im Test-Classpath gegenüber `log4j2.xml` + +## 3. Scope-Treue + +| Scope-Punkt aus dem Arbeitspaket | Erfüllt? | Bemerkung | +|---|---|---| +| ArchUnit als Test-Dependency | ✅ | `archunit-junit5:1.3.0`, test scope | +| Vier Regeln A–D implementiert | ✅ | `ArchitectureTest.java` | +| `log4j2-test.xml` anlegen (E-02) | ✅ | Root level WARN, SYSTEM_ERR | +| Leere Testklassen prüfen und löschen | ✅ | Keine leeren Testklassen gefunden; `DefaultStructureValidatorTestAdditional` hat 4 aktive `@Test`-Methoden (bewusst behalten) | +| Keine neuen Produktionsklassen | ✅ | Kein einziger neuer `.java`-Produktionscode | +| Zyklische Abhängigkeits-Regeln (Scope OUT) | ✅ | Nicht umgesetzt | +| Coverage-/Mutation-Schwellen (Scope Out) | ✅ | Nicht umgesetzt | + +**Wurde der Scope eingehalten?** Ja, vollständig. + +**Wurden Dinge außerhalb des Scopes gemacht?** Nein. + +## 4. Abnahmekriterien + +| Abnahmekriterium aus dem Arbeitspaket | Erfüllt? | Nachweis | +|---|---|---| +| ArchUnit als Test-Dependency in `pom.xml` | ✅ | `archunit-junit5:1.3.0`, test scope, `pom.xml` | +| Vier Architekturregeln A–D implementiert und grün | ✅ | `Tests run: 4, Failures: 0, Errors: 0` in `de.gecheckt.asv.ArchitectureTest` | +| `log4j2-test.xml` unter `src/test/resources/` | ✅ | `src/test/resources/log4j2-test.xml` | +| Keine leeren Testklassen | ✅ | Alle Testklassen haben mindestens einen `@Test` | +| `mvn clean verify` grün, kein unerwartetes Log-Rauschen | ✅ | `BUILD SUCCESS`, 222 Tests, 0 Failures | +| Bericht dokumentiert erste Ausführung (rot/grün) | ✅ | Siehe Abschnitt 5 unten | +| Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP10-bericht.md` | ✅ | Diese Datei | + +## 5. Build- und Teststatus + +- `mvn clean verify`: ✅ grün (`BUILD SUCCESS`) +- Anzahl Tests: 222 gesamt (davon 1 Skipped — Windows-bedingter `fall5_dateiNichtLesbar_exitCode2` aus AP08); 4 neu in `ArchitectureTest` +- ArchUnit-Tests beim ersten Lauf: **alle 4 Regeln sofort grün** — keine Verstöße +- Log-Rauschen: Im Maven-Konsolenoutput erscheinen ERROR/WARN-Zeilen aus `CliRunnerOperationalErrorTest` (z.B. `Bedienfehler: Kein Argument übergeben`). Diese stammen vom SLF4J/Log4j2-Logger innerhalb des zu testenden `CliRunner.run()`-Aufrufs. Sie sind fachlich korrekt und erwünscht. Die `log4j2-test.xml` (Root=WARN) unterdrückt INFO/DEBUG-Rauschen; ERROR-Zeilen aus Negativ-Tests bleiben sichtbar — das ist das dokumentierte Verhalten aus E-02. Der Build ist sauber. +- Warnungen beim Build: die üblichen maven-shade-plugin-Überlappungswarnungen (unveränderter Stand seit AP02) + +### Erster Lauf der 4 Regeln + +Alle vier ArchUnit-Regeln waren beim ersten Lauf sofort grün. Das bestätigt: +- **Regel A (Log4j2-Sichtbarkeit):** `CliRunner` verwendet nur SLF4J (`org.slf4j`), kein direktes Log4j2. `LoggingConfigurator` liegt korrekt in `adapter.out.logging`. `Main` in `bootstrap` verwendet keine Log4j2-Typen direkt. +- **Regel B (Domain-Reinheit):** Keine Domain-Klasse referenziert Adapter oder Bootstrap. +- **Regel C (Application-Reinheit):** Keine Application-Klasse referenziert Adapter oder Bootstrap. (Die Testklassen wurden von `@AnalyzeClasses(..., importOptions = ImportOption.DoNotIncludeTests.class)` ausgeschlossen, sodass `DefaultStructureValidatorTestAdditional` mit seinen Adapter-Importen keine Rolle spielt.) +- **Regel D (Preview-Isolation):** Die `@see`- und `@link`-JavaDoc-Referenzen auf `DefaultStructureValidator` und `DefaultFieldValidator` in `NoOpStructureValidator` und `NoOpFieldValidator` erzeugen **keine** Bytecode-Abhängigkeiten. ArchUnit analysiert Bytecode, nicht JavaDoc — deshalb kein Verstoß. + +## 6. Rest-Risiken und offene Punkte + +- **Transitiv-Risiko ArchUnit:** ArchUnit analysiert nur direkten Bytecode. Transitive Abhängigkeiten über Reflection oder dynamisches Laden werden nicht erkannt. Für M1 ausreichend. +- **Regel D auf Namensbasis:** Regel D prüft über `haveSimpleNameContaining("DefaultStructureValidator")`. Falls ab M3 neue Klassen mit ähnlichem Namen entstehen, könnte die Regel unbeabsichtigt greifen. Bei M3-Aktivierung überprüfen und ggf. auf Paketnamen-Ebene umstellen. +- **Log-Rauschen in Tests:** Die ERROR/WARN-Zeilen aus `CliRunnerOperationalErrorTest` sind fachlich korrekt (sie testen Bedienfehler-Verhalten), erscheinen aber im Maven-Konsolenoutput. Das ist ein bekanntes Muster bei Tests, die Log4j2-konfigurierte Logger verwenden. Eine vollständige Unterdrückung würde auch echte Fehler verschlucken und ist bewusst nicht angestrebt. +- **SLF4J-Versionsdiskrepanz:** Das Projekt verwendet SLF4J 2.0.7, ArchUnit 1.3.0 zieht SLF4J 2.0.12 als transitive Dependency. Maven wählt 2.0.7 (nächste deklarierte gewinnt). Kein funktionales Problem, aber mittelfristig wäre eine Version-Angleichung empfehlenswert. + +## 7. Empfehlungen für Folge-Arbeitspakete + +- **AP11 (M1-Abnahme):** Die vier Architekturregeln können als Abnahmekriterium im Bericht gelistet werden — Nachweis: `ArchitectureTest` grün in CI. +- **Ab M3:** Regel D anpassen oder durch eine Paket-basierte Regel ersetzen, wenn `DefaultStructureValidator` und `DefaultFieldValidator` aktiv verdrahtet werden. Dabei sicherstellen, dass kein Adapter direkt auf die Implementierung, sondern nur auf die Interfaces (`StructureValidator`, `FieldValidator`) zugreift. +- **SLF4J-Version:** Bei nächster Dependency-Pflege auf SLF4J 2.0.12 anheben. + +## 8. Reviewer-Checkliste + +- [x] Alle im Arbeitspaket genannten Scope-IN-Punkte sind nachweislich umgesetzt +- [x] Keine Scope-OUT-Punkte wurden angefasst +- [x] Abnahmekriterien sind mit konkreten Nachweisen belegt (Tests, Dateipfade) +- [x] `mvn clean verify` ist grün (BUILD SUCCESS, 222 Tests, 0 Failures) +- [ ] Der Commit für dieses AP hat eine sprechende Message (`M1-AP10: ...`) +- [x] Keine Regeln der Grunddokumente (Spec, Fachliche, Technik) wurden verletzt +- [x] Rest-Risiken sind ehrlich dokumentiert diff --git a/docs/arbeitspakete/m1/berichte/AP11-bericht.md b/docs/arbeitspakete/m1/berichte/AP11-bericht.md new file mode 100644 index 0000000..333005b --- /dev/null +++ b/docs/arbeitspakete/m1/berichte/AP11-bericht.md @@ -0,0 +1,151 @@ +# Abschlussbericht Arbeitspaket AP11 – M1-Abnahme + +> **Bezug:** `docs/arbeitspakete/m1/AP11-m1-abnahme.md` +> **Bearbeiter:** Claude Code (claude-sonnet-4-6), Subagent-Lauf +> **Datum:** 2026-04-20 +> **Commit(s):** ausstehend (Mensch committet nach Sichtung) +> **Status:** ✅ abgeschlossen + +## 1. Zusammenfassung + +M1 wurde formal abgenommen. Alle sechs Abnahmekriterien aus `meilensteine.md` §„Abnahme von M1" sind erfüllt und nachweisbar dokumentiert. Das Test-Artefakt `test-artefakte/m1/minimal.txt` wurde angelegt, alle fünf End-to-End-Läufe durchgeführt und protokolliert, und der konsolidierte M1-Abschlussbericht unter `docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md` erstellt. `mvn clean verify` ist grün (222 Tests, 0 Failures, 1 Skipped). + +## 2. Umgesetzte Änderungen + +- `test-artefakte/m1/minimal.txt` — neu angelegt; ISO-8859-15-kompatible Dummy-Textdatei, 5 Zeilen, keine echten ASV-Daten, kein gültiges EDIFACT. +- `docs/arbeitspakete/m1/berichte/AP11-bericht.md` — dieser Bericht. +- `docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md` — konsolidierter M1-Abschlussbericht mit allen Pflichtabschnitten. + +Keine Produktionscode-Änderungen. Keine Test-Änderungen. Kein `git tag` gesetzt (CLAUDE.md-Regel, kein commit/tag durch Subagenten). + +## 3. Scope-Treue + +| Scope-Punkt aus dem Arbeitspaket | Erfüllt? | Bemerkung | +|---|---|---| +| `test-artefakte/m1/minimal.txt` anlegen | ✅ | ISO-8859-15-kompatibel, 5 Zeilen, kein echtes EDIFACT | +| `mvn clean package` ausführen | ✅ | BUILD SUCCESS, JAR `target/asv-format-validator-0.0.1-SNAPSHOT.jar` | +| Alle 5 End-to-End-Läufe mit Exit-Code-Nachweis | ✅ | Alle 5 Läufe protokolliert (§ End-to-End) | +| Meilenstein-Abnahmetabelle vollständig ausgefüllt | ✅ | Im M1-Abschlussbericht | +| Konsolidierter M1-Abschlussbericht mit allen Pflichtabschnitten | ✅ | `M1-abschlussbericht.md` | +| AP11-Bericht nach Vorlage | ✅ | Dieser Bericht | +| Git-Tag `m1-done` NICHT setzen (CLAUDE.md-Regel) | ✅ | Im Bericht dokumentiert | +| Vorgriffe auf M2 (Scope OUT) | ✅ nicht gemacht | — | +| Release-Builds, Signierung (Scope OUT) | ✅ nicht gemacht | — | + +**Wurde der Scope eingehalten?** Ja, vollständig. + +**Wurden Dinge außerhalb des Scopes gemacht?** Nein. + +## 4. Abnahmekriterien + +| Abnahmekriterium aus dem Arbeitspaket | Erfüllt? | Nachweis | +|---|---|---| +| `test-artefakte/m1/minimal.txt` existiert | ✅ | `test-artefakte/m1/minimal.txt` angelegt | +| Alle fünf Läufe sind protokolliert | ✅ | Abschnitt 5 unten; vollständig im M1-Abschlussbericht | +| `M1-abschlussbericht.md` existiert mit allen Pflichtabschnitten | ✅ | `docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md` | +| Meilenstein-Abnahmetabelle vollständig, jede Zeile mit Nachweis | ✅ | M1-Abschlussbericht §„Meilenstein-Abnahmetabelle" | +| Kein Exit-Code 3 mehr erreichbar | ✅ | CliRunner-Switch über `Verdict`; keine `return 3`-Stelle im Produktionscode (AP06-Nachweis) | +| `mvn clean verify` grün | ✅ | BUILD SUCCESS, 222 Tests, 0 Failures, 1 Skipped | +| Git-Tag `m1-done` — nicht gesetzt | ✅ | Dokumentiert: Tag wird vom Entwickler nach finaler Sichtung gesetzt | +| Freigabe-Vermerk ist explizit | ✅ | M1-Abschlussbericht §„Freigabe-Vermerk" | +| Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP11-bericht.md` | ✅ | Dieser Bericht | + +## 5. End-to-End-Protokoll + +Alle Läufe mit JAR `target/asv-format-validator-0.0.1-SNAPSHOT.jar` vom Projekt-Root aus. + +### Lauf 1 — Eingabedatei vorhanden (erster Lauf) + +``` +java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar test-artefakte/m1/minimal.txt +``` + +- **Exit-Code:** `0` ✅ +- **Ausgabe (stdout):** Prüfbericht mit `Urteil: GÜLTIG`, `Keine Befunde.`, M1-Platzhalter-Hinweis +- **Erzeugte Dateien:** `test-artefakte/m1/minimal.txt.txt`, `test-artefakte/m1/minimal.txt.log` + +### Lauf 2 — identischer Aufruf (Suffix-Logik) + +``` +java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar test-artefakte/m1/minimal.txt +``` + +- **Exit-Code:** `0` ✅ +- **Ausgabe (stdout):** identisch zu Lauf 1 +- **Erzeugte Dateien:** `test-artefakte/m1/minimal.txt_v1.txt`, `test-artefakte/m1/minimal.txt_v1.log` + +Nach Lauf 2 vorhandene Dateien im Verzeichnis: `minimal.txt`, `minimal.txt.txt`, `minimal.txt.log`, `minimal.txt_v1.txt`, `minimal.txt_v1.log` ✅ + +### Lauf 3 — nicht existierende Datei + +``` +java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar nicht-vorhanden.txt +``` + +- **Exit-Code:** `2` ✅ +- **Ausgabe (stderr):** `Bedienfehler: Datei nicht gefunden: nicht-vorhanden.txt` +- **Ausgabe (stdout):** Prüfbericht mit `Urteil: BEDIENFEHLER`, `Regel=OPERATIONAL-FILE-NOT-FOUND` +- **Berichtdatei:** `nicht-vorhanden.txt.txt` im aktuellen Verzeichnis (übergeordnetes Verzeichnis bekannt) + +### Lauf 4 — kein Argument + +``` +java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar +``` + +- **Exit-Code:** `2` ✅ +- **Ausgabe (stderr):** `Bedienfehler: Kein Argument übergeben.` +- **Ausgabe (stdout):** Prüfbericht mit `Urteil: BEDIENFEHLER`, `Regel=OPERATIONAL-MISSING-ARG` +- **Keine Berichtdatei** (kein Verzeichnis bekannt) + +### Lauf 5 — zu viele Argumente + +``` +java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar datei1.txt datei2.txt +``` + +- **Exit-Code:** `2` ✅ +- **Ausgabe (stderr):** `Bedienfehler: Zu viele Argumente (2).` +- **Ausgabe (stdout):** Prüfbericht mit `Urteil: BEDIENFEHLER`, `Regel=OPERATIONAL-TOO-MANY-ARGS` +- **Keine Berichtdatei** (kein Verzeichnis bekannt) + +## 6. Build- und Teststatus + +- `mvn clean verify`: ✅ grün (`BUILD SUCCESS`) +- Anzahl Tests: **222** (davon 0 neu in AP11 — kein Produktionscode geändert) +- Fehler / Skipped: 0 Failures / 1 Skipped (Windows-bedingt: `fall5_dateiNichtLesbar_exitCode2` aus AP08) +- Coverage (JaCoCo, informativ): **87 % Line Coverage** (704 / 806 Zeilen), keine Schwellwerte aktiv (M9-Scope) +- Warnungen: Shade-Plugin META-INF-Überlappungen (unverändert seit AP02); `sun.reflect.Reflection.getCallerClass` (Log4j2-interne Warnung beim JAR-Start) + +## 7. Rest-Risiken und offene Punkte + +- **Git-Tag `m1-done` nicht gesetzt:** Tag wird vom Entwickler nach finaler Sichtung manuell gesetzt (CLAUDE.md §„Harte Regeln: kein git commit/add/push durch Subagenten"). +- **Konsolenausgabe-Encoding auf Windows:** Die stdout-Ausgabe erscheint auf Windows-Konsolen mit CP1252/OEM437 mit Mojibake (`G?LTIG` statt `GÜLTIG`). Die Berichtdatei selbst ist korrekt UTF-8. Bekanntes Windows-Konsolen-Problem. Empfehlung: In M9-Dokumentation erwähnen (`chcp 65001` als Workaround). +- **`nicht-vorhanden.txt.txt`** entsteht im Projekt-Root durch Lauf 3. Dies ist korrekt (übergeordnetes Verzeichnis = Projekt-Root war bekannt). Datei kann nach Sichtung gelöscht werden. +- **`logs/asv-format-validator-fallback.log`:** Log4j2-Fallback-Datei aus `log4j2.xml`, entsteht bei Läufen ohne Eingabedatei-Pfad (Lauf 4, 5). Durch `.gitignore`-Eintrag `logs/` nicht versioniert. +- **Konsolidierte Rest-Risiken aus AP01–AP10** sind im M1-Abschlussbericht §„Rest-Risiken" vollständig dokumentiert. + +## 8. Empfehlungen für Folge-Arbeitspakete + +Siehe M1-Abschlussbericht §„Empfehlungen für M2". Wesentliche Punkte: +- M2 baut auf dem ISO-8859-15-Encoding auf, das in AP06 eingeführt wurde. +- Dateinamensschemata und globale Rahmenregeln kommen in M2. +- Architekturtest (ArchUnit) ist aktiv — bei M2-Klassen in `adapter` oder `bootstrap` sicherstellen, dass keine Log4j2-Typen direkt importiert werden. + +## 9. Git-Tag-Vermerk + +**Tag `m1-done` wurde NICHT gesetzt.** Gemäß CLAUDE.md §„Harte Regeln": Kein `git commit`, `git add` oder `git push` durch den Subagenten. Der Entwickler setzt den Tag nach finaler Sichtung manuell: + +```bash +git tag -a m1-done -m "Meilenstein 1 abgeschlossen, siehe docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md" +``` + +## 10. Reviewer-Checkliste + +- [x] Alle im Arbeitspaket genannten Scope-IN-Punkte sind nachweislich umgesetzt +- [x] Keine Scope-OUT-Punkte wurden angefasst +- [x] Abnahmekriterien sind mit konkreten Nachweisen belegt (Tests, Exit-Codes, Dateipfade) +- [x] `mvn clean verify` ist grün (222 Tests, BUILD SUCCESS) +- [ ] Der Commit für dieses AP hat eine sprechende Message (`M1-AP11: M1-Abnahme abgeschlossen`) — ausstehend, Mensch committet +- [x] Keine Regeln der Grunddokumente (Spec, Fachliche, Technik) wurden verletzt +- [x] Rest-Risiken sind ehrlich dokumentiert diff --git a/docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md b/docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md new file mode 100644 index 0000000..fbfa4dd --- /dev/null +++ b/docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md @@ -0,0 +1,193 @@ +# M1-Abschlussbericht – Projektfundament, Logging und Ergebnismodell + +> **Meilenstein:** M1 +> **Grundlage:** `docs/specs/meilensteine.md` v3 +> **Bearbeiter:** Claude Code (claude-sonnet-4-6), Subagenten-Reihe AP01–AP11 +> **Datum:** 2026-04-20 +> **Commit(s):** ausstehend — Mensch committet und taggt nach finaler Sichtung +> **Status:** ✅ abnahmebereit + +--- + +## 1. Zusammenfassung + +Meilenstein M1 stellt den tragfähigen technischen Sockel des ASV-Format-Validators bereit. Es wurden in elf aufeinander aufbauenden Arbeitspaketen das Maven-Projekt gehärtet, die hexagonale Paketstruktur etabliert, die SLF4J/Log4j2-Fassade eingeführt, das Befundmodell mit Spec-/Diagnose-Trennung aufgebaut, CLI-Bootstrap und Exit-Codes normiert, Ausgabeartefakte mit Suffix-Logik implementiert, Ministralbericht für Bedienfehler ergänzt, Preview-Altlogik eingefroren sowie ein vollständiger ArchUnit-Architekturtest hinterlegt. + +M1 enthält bewusst **keine** EDIFACT-Fachvalidierung. Der `DummyFileValidationService` liest jede Eingabedatei mit ISO-8859-15, gibt einen leeren Validierungsbericht zurück und liefert Exit-Code 0 (GÜLTIG). Die fachliche Tiefe beginnt ab M2 (Artefaktschicht, Dateinamen) und M3 (EDIFACT-Serviceebene). Die Preview-Validatoren (`DefaultStructureValidator`, `DefaultFieldValidator`) sind eingefroren und durch No-Op-Platzhalter ersetzt — sie werden ab M3 reaktiviert und gegen die finalen V1-Regelklassifikationen bewertet. + +--- + +## 2. AP-Übersicht + +| AP | Titel | Status | Commit | +|---|---|---|---| +| AP01 | Ist-Stand-Inventar und Delta-Analyse | ✅ | nicht committet — Entwickler committet final | +| AP02 | Build-Infrastruktur härten (SLF4J, JaCoCo, PIT, Shade-Plugin, Mockito-Agent) | ✅ | `d0aac6a` / `61935df` | +| AP03 | Hexagonale Paketstruktur anlegen und Ist-Code migrieren | ✅ | `bd45de8` | +| AP04 | Logging-Adapter: SLF4J-Fassade etabliert | ✅ | `a1a48e9` | +| AP05 | Befundmodell mit Spec-/Diagnose-Trennung | ✅ | ausstehend — Entwickler committet final | +| AP06 | Bootstrap und CLI-Adapter (Exit-Codes 0/1/2, ISO-8859-15, Shade-JAR) | ✅ | ausstehend | +| AP07 | Ausgabeartefakte: Berichtdatei + Log-Datei mit Suffix-Logik | ✅ | ausstehend | +| AP08 | Minimalbericht bei Bedienfehlern (Exit-Code 2) | ✅ | ausstehend | +| AP09 | Altlogik einfrieren (Preview-Validatoren deaktivieren) | ✅ | ausstehend | +| AP10 | Architekturtest (ArchUnit, 4 Regeln) | ✅ | ausstehend | +| AP11 | M1-Abnahme (dieser Bericht) | ✅ | ausstehend | + +Hinweis: AP02–AP04 haben bereits committete Hashes aus dem Git-Log (`cd6e522`, `a1a48e9`, `bd45de8`, `d0aac6a`, `61935df`). AP05–AP11 wurden in einem späteren Subagenten-Lauf bearbeitet; die Commits werden vom Entwickler nach finaler Sichtung gesetzt. + +--- + +## 3. Meilenstein-Abnahmetabelle + +| Kriterium | Nachweis | Status | +|---|---|---| +| Anwendung ist als JAR unter Windows mit Java 21 startbar | Lauf 1: `java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar test-artefakte/m1/minimal.txt` → Exit 0 | ✅ | +| Falsches oder fehlendes Argument → Exit-Code 2 mit Minimalbericht | Lauf 3 (Datei nicht vorhanden → Exit 2), Lauf 4 (kein Arg → Exit 2), Lauf 5 (2 Args → Exit 2) | ✅ | +| Bericht- und Log-Datei im Eingabeverzeichnis mit korrekter Suffix-Logik | Lauf 1: `minimal.txt.txt` + `minimal.txt.log`; Lauf 2: `minimal.txt_v1.txt` + `minimal.txt_v1.log` | ✅ | +| Log4j2-Bindung ist außerhalb von Bootstrap und Logging-Adapter nicht sichtbar | ArchUnit-Test AP10 Regel A: `keinLog4j2AusserInErlaubtenPaketen` → GRÜN; Grep auf `import org.apache.logging.log4j` außerhalb `adapter.out.logging` und `bootstrap` → leer | ✅ | +| Befundmodell trennt Spec-Urteil und diagnostische Weiteranalyse strukturell | Unit-Test AP05: `ValidationReportTest#diagnosticErrorLiefertVALID()` → GRÜN; `FindingKind.SPEC` vs. `FindingKind.DIAGNOSTIC`; `computeVerdict()` ignoriert DIAGNOSTIC | ✅ | +| Build und Tests sind grün | `mvn clean verify` → BUILD SUCCESS, 222 Tests, 0 Failures, 1 Skipped (Windows-bedingt) | ✅ | + +--- + +## 4. End-to-End-Protokoll + +JAR: `target/asv-format-validator-0.0.1-SNAPSHOT.jar` +Ausführungsverzeichnis: Projekt-Root `D:\Dev\Projects\asv-format-validator` + +### Lauf 1 — Eingabedatei vorhanden, erster Lauf + +``` +java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar test-artefakte/m1/minimal.txt +``` + +| Merkmal | Wert | +|---|---| +| Exit-Code | `0` (GÜLTIG) | +| stdout | Prüfbericht: `Urteil: GÜLTIG`, `Keine Befunde.`, M1-Platzhalter-Hinweis | +| erzeugte Dateien | `test-artefakte/m1/minimal.txt.txt` (UTF-8, ~430 Byte), `test-artefakte/m1/minimal.txt.log` | + +### Lauf 2 — identischer Aufruf (Suffix-Logik) + +``` +java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar test-artefakte/m1/minimal.txt +``` + +| Merkmal | Wert | +|---|---| +| Exit-Code | `0` (GÜLTIG) | +| stdout | identisch zu Lauf 1 | +| erzeugte Dateien | `test-artefakte/m1/minimal.txt_v1.txt`, `test-artefakte/m1/minimal.txt_v1.log` | + +**Dateien nach Lauf 2:** `minimal.txt`, `minimal.txt.txt`, `minimal.txt.log`, `minimal.txt_v1.txt`, `minimal.txt_v1.log` ✅ + +### Lauf 3 — nicht existierende Datei + +``` +java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar nicht-vorhanden.txt +``` + +| Merkmal | Wert | +|---|---| +| Exit-Code | `2` (BEDIENFEHLER) | +| stderr | `Bedienfehler: Datei nicht gefunden: nicht-vorhanden.txt` | +| stdout | Prüfbericht: `Urteil: BEDIENFEHLER`, `Regel=OPERATIONAL-FILE-NOT-FOUND` | +| Berichtdatei | `nicht-vorhanden.txt.txt` im Projekt-Root (übergeordnetes Verzeichnis bekannt) | + +### Lauf 4 — kein Argument + +``` +java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar +``` + +| Merkmal | Wert | +|---|---| +| Exit-Code | `2` (BEDIENFEHLER) | +| stderr | `Bedienfehler: Kein Argument übergeben.` | +| stdout | Prüfbericht: `Urteil: BEDIENFEHLER`, `Regel=OPERATIONAL-MISSING-ARG` | +| Berichtdatei | keine (kein Eingabeverzeichnis bekannt) | + +### Lauf 5 — zu viele Argumente + +``` +java -jar target/asv-format-validator-0.0.1-SNAPSHOT.jar datei1.txt datei2.txt +``` + +| Merkmal | Wert | +|---|---| +| Exit-Code | `2` (BEDIENFEHLER) | +| stderr | `Bedienfehler: Zu viele Argumente (2).` | +| stdout | Prüfbericht: `Urteil: BEDIENFEHLER`, `Regel=OPERATIONAL-TOO-MANY-ARGS` | +| Berichtdatei | keine (kein Eingabeverzeichnis bekannt) | + +--- + +## 5. Quality-Metriken + +> Alle Metriken informativ. Quality-Gates mit Schwellwerten gelten erst ab M9. + +| Metrik | Wert | +|---|---| +| Testanzahl gesamt | 222 | +| Failures | 0 | +| Errors | 0 | +| Skipped | 1 (Windows-bedingt: `fall5_dateiNichtLesbar_exitCode2` aus AP08; `setReadable(false)` ohne Wirkung für eigenen Prozess) | +| Line Coverage (JaCoCo, gesamt) | **87 %** (704 / 806 Zeilen) | +| Instruction Coverage (JaCoCo) | informativ, kein Gate | +| PIT Mutation-Score (einmalig AP02) | 83 % (249 Mutationen, 207 getötet) — Stand vor AP05–AP10 | +| Build-Dauer (`mvn clean verify`) | ~16 s | + +--- + +## 6. Rest-Risiken (konsolidiert aus AP01–AP10) + +| Risiko | Quelle | Auswirkung | M2/M3-Handlungsbedarf | +|---|---|---|---| +| `DefaultSegmentLineTokenizer` trennt starr an `+`; UNA-Segment nicht ausgewertet | AP01, AP00 | Falscher Parse für nicht-EDIFACT-Dateien ab M3 | M3: Tokenizer gegen UNA-bewusstes Pendant ersetzen | +| `DefaultInputFileParser` erzeugt immer genau eine Message pro Datei | AP01 | Nur Preview-Parser; nicht real für mehrere UNH/UNT-Paare | M3: Parser auf Nachrichtenerkennung erweitern | +| `DefaultStructureValidatorTestAdditional` hat 3 aktive Tests — nicht aus AP09 gelöscht | AP09, AP10 | Minimales Rauschen; Tests sind inhaltlich korrekt (STRUCTURE_012-Logik) | M3: bei Reaktivierung der Preview-Validatoren entscheiden | +| Preview-Regeln (`DefaultStructureValidator`) noch nicht gegen V1-V/T/N/K bewertet | AP09 | Einige der 19 Regeln könnten bei M3-Aktivierung zu rigide sein | M3: jede Regel neu klassifizieren | +| `ValidationResult`/`ValidationSeverity` (Altmodell in `application.model`) koexistieren mit neuem `domain.finding` | AP05, AP09 | Zwei parallele Ergebnistypen; Preview-Code nutzt Altmodell | M3: Altmodell ablösen oder vollständig migrieren | +| `LoggingConfigurator.configureLogFile(Path)` erzeugt Windows-TempDir-Lock in Tests | AP07 | Lösung durch Mockito-Mock in `CliRunnerTest` / `CliRunnerOutputArtifactsTest`; echte Konfiguration durch JAR-Test bestätigt | AP10 (erledigt): Mock ist Workaround; bei M9-Testausbau beachten | +| Konsolenausgabe-Encoding auf Windows (Mojibake bei Umlauten) | AP07, AP08 | Berichtdatei korrekt UTF-8; nur sichtbares Darstellungsproblem in `cmd.exe`/`powershell.exe` | M9: Dokumentation (`chcp 65001`-Hinweis) | +| SLF4J-Versionsdiskrepanz: Projekt 2.0.7, ArchUnit zieht 2.0.12 | AP10 | Kein funktionales Problem; Maven wählt 2.0.7 | M9 oder früher: Versions-Angleichung bei Dependency-Pflege | +| `sun.reflect.Reflection.getCallerClass`-Warnung im JAR-Betrieb | AP06 | Log4j2-interne Performance-Warnung; kein Fehler | M9: Log4j2-Upgrade auf 2.23+ optional | +| Shade-Plugin erzeugt Überlappungswarnungen bei META-INF-Ressourcen | AP06 | Kosmetisch; kein Fehler; bekanntes Fat-JAR-Verhalten | bleibt bis M9; kein Handlungsbedarf | +| Fall 5 (Datei nicht lesbar) nur auf Unix testbar | AP08 | Windows: `setReadable(false)` ohne Wirkung für eigenen Prozess; Test explizit übersprungen | M9: NTFS-ACL-basierter Testansatz falls benötigt | + +--- + +## 7. Empfehlungen für M2 + +- **ISO-8859-15 ist etabliert** (AP06): M2 kann darauf aufbauen. `DummyFileValidationService.INPUT_CHARSET = Charset.forName("ISO-8859-15")` ist paketöffentlich für Testbarkeit. +- **Dateinamensschemata** (unverschlüsselt: `B...`, verschlüsselt: `__ASV0`, Auftragsdatei: `.AUF`) kommen in M2 als harte Prüfregeln. +- **`adapter.out.filesystem`** ist angelegt aber leer. M2 füllt es mit dem Dateizugriffs-Adapter (read-only, ISO-8859-15, Artefaktklassifikation). +- **ArchUnit-Regeln A–D** sind aktiv. Bei neuen M2-Klassen in `adapter` oder `bootstrap` sicherstellen, dass keine Log4j2-Direktimporte vorkommen (Regel A) und dass `domain`/`application` keine Adapter-Abhängigkeiten bekommen (Regeln B/C). +- **Regel D** (Preview-Isolation) referenziert `DefaultStructureValidator` und `DefaultFieldValidator` namentlich. Ab M3, wenn diese Klassen reaktiviert werden, Regel D auf Paket-Basis umstellen. +- **`ValidationResult`/`ValidationReport` Koexistenz**: M2 sollte ausschließlich `ValidationReport` (neues Befundmodell) für neue Befunde verwenden. Die Ablösung des Altmodells ist M3-Scope. +- **Exit-Code 3 nicht mehr erreichbar** (AP06-Nachweis). M2 muss diesen Zustand bewahren. +- **Partnerdatei-Ableitung** (deterministisch, nicht heuristisch) ist M2-Scope — AP11 liefert keine Vorarbeit dafür. +- **Übermittlungszähler-Prüfung** (001–999, keine quartalsübergreifende Sequenz in V1) kommt in M2. + +--- + +## 8. Freigabe-Vermerk + +**M1 ist abnahmebereit.** + +Alle sechs Abnahmekriterien aus `meilensteine.md` v3 §„Abnahme von M1" sind erfüllt: + +1. Anwendung als JAR unter Windows mit Java 21 startbar — ✅ (Lauf 1, Exit-Code 0) +2. Falsches/fehlendes Argument → Exit-Code 2 mit Minimalbericht — ✅ (Läufe 3, 4, 5) +3. Bericht- und Log-Datei im Eingabeverzeichnis mit korrekter Suffix-Logik — ✅ (Läufe 1+2) +4. Log4j2-Bindung außerhalb Bootstrap/Logging-Adapter nicht sichtbar — ✅ (ArchUnit Regel A) +5. Befundmodell trennt Spec-Urteil und Diagnose strukturell — ✅ (`diagnosticErrorLiefertVALID` GRÜN) +6. Build und Tests grün — ✅ (222 Tests, 0 Failures) + +Der einzige Skipped-Test (`fall5_dateiNichtLesbar_exitCode2`) ist Windows-plattformbedingt und enthält ein explizites `assumeTrue(...)`. Er ist kein Blocker. + +**Git-Tag `m1-done` wurde NICHT gesetzt.** Gemäß CLAUDE.md §„Harte Regeln" setzt kein Subagent `git commit`, `git add` oder `git push`. Der Entwickler setzt den Tag nach finaler Sichtung der Berichte manuell: + +```bash +git tag -a m1-done -m "Meilenstein 1 abgeschlossen, siehe docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md" +``` diff --git a/logs/asv-format-validator.log b/logs/asv-format-validator.log index 07ef409..893e14a 100644 --- a/logs/asv-format-validator.log +++ b/logs/asv-format-validator.log @@ -146,3 +146,523 @@ java.io.IOException: File does not exist: /non/existent/file.txt at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) [surefire-booter-3.0.0.jar:3.0.0] at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) [surefire-booter-3.0.0.jar:3.0.0] at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) [surefire-booter-3.0.0.jar:3.0.0] +2026-04-20 08:05:59 [main] ERROR de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication - Fehler beim Lesen der Datei: File does not exist: /non/existent/file.txt +java.io.IOException: File does not exist: /non/existent/file.txt + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.parseFile(AsvValidatorApplication.java:141) ~[classes/:?] + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.run(AsvValidatorApplication.java:104) ~[classes/:?] + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplicationTest.testRunWithNonExistentFileShouldReturnFileErrorExitCode(AsvValidatorApplicationTest.java:94) ~[test-classes/:?] + at jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[?:?] + at java.lang.reflect.Method.invoke(Method.java:580) ~[?:?] + at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727) ~[junit-platform-commons-1.9.2.jar:1.9.2] + at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.apache.maven.surefire.junitplatform.LazyLauncher.execute(LazyLauncher.java:50) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:184) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:148) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:122) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) [surefire-booter-3.0.0.jar:3.0.0] +2026-04-20 08:06:18 [main] ERROR de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication - Fehler beim Lesen der Datei: File does not exist: /non/existent/file.txt +java.io.IOException: File does not exist: /non/existent/file.txt + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.parseFile(AsvValidatorApplication.java:141) ~[classes/:?] + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.run(AsvValidatorApplication.java:104) ~[classes/:?] + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplicationTest.testRunWithNonExistentFileShouldReturnFileErrorExitCode(AsvValidatorApplicationTest.java:94) ~[test-classes/:?] + at jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[?:?] + at java.lang.reflect.Method.invoke(Method.java:580) ~[?:?] + at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727) ~[junit-platform-commons-1.9.2.jar:1.9.2] + at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.apache.maven.surefire.junitplatform.LazyLauncher.execute(LazyLauncher.java:50) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:184) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:148) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:122) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) [surefire-booter-3.0.0.jar:3.0.0] +2026-04-20 08:51:45 [main] ERROR de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication - Fehler beim Lesen der Datei: File does not exist: /non/existent/file.txt +java.io.IOException: File does not exist: /non/existent/file.txt + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.parseFile(AsvValidatorApplication.java:141) ~[classes/:?] + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.run(AsvValidatorApplication.java:104) ~[classes/:?] + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplicationTest.testRunWithNonExistentFileShouldReturnFileErrorExitCode(AsvValidatorApplicationTest.java:94) ~[test-classes/:?] + at jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[?:?] + at java.lang.reflect.Method.invoke(Method.java:580) ~[?:?] + at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727) ~[junit-platform-commons-1.9.2.jar:1.9.2] + at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.apache.maven.surefire.junitplatform.LazyLauncher.execute(LazyLauncher.java:50) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:184) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:148) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:122) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) [surefire-booter-3.0.0.jar:3.0.0] +2026-04-20 08:53:25 [main] ERROR de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication - Fehler beim Lesen der Datei: File does not exist: /non/existent/file.txt +java.io.IOException: File does not exist: /non/existent/file.txt + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.parseFile(AsvValidatorApplication.java:141) ~[classes/:?] + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.run(AsvValidatorApplication.java:104) ~[classes/:?] + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplicationTest.testRunWithNonExistentFileShouldReturnFileErrorExitCode(AsvValidatorApplicationTest.java:94) ~[test-classes/:?] + at jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[?:?] + at java.lang.reflect.Method.invoke(Method.java:580) ~[?:?] + at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727) ~[junit-platform-commons-1.9.2.jar:1.9.2] + at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.apache.maven.surefire.junitplatform.LazyLauncher.execute(LazyLauncher.java:50) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:184) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:148) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:122) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) [surefire-booter-3.0.0.jar:3.0.0] +2026-04-20 08:53:43 [main] ERROR de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication - Fehler beim Lesen der Datei: File does not exist: /non/existent/file.txt +java.io.IOException: File does not exist: /non/existent/file.txt + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.parseFile(AsvValidatorApplication.java:141) ~[classes/:?] + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.run(AsvValidatorApplication.java:104) ~[classes/:?] + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplicationTest.testRunWithNonExistentFileShouldReturnFileErrorExitCode(AsvValidatorApplicationTest.java:94) ~[test-classes/:?] + at jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[?:?] + at java.lang.reflect.Method.invoke(Method.java:580) ~[?:?] + at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727) ~[junit-platform-commons-1.9.2.jar:1.9.2] + at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.apache.maven.surefire.junitplatform.LazyLauncher.execute(LazyLauncher.java:50) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:184) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:148) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:122) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) [surefire-booter-3.0.0.jar:3.0.0] +2026-04-20 08:53:58 [main] ERROR de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication - Fehler beim Lesen der Datei: File does not exist: /non/existent/file.txt +java.io.IOException: File does not exist: /non/existent/file.txt + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.parseFile(AsvValidatorApplication.java:141) ~[classes/:?] + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplication.run(AsvValidatorApplication.java:104) ~[classes/:?] + at de.gecheckt.asv.adapter.in.cli.AsvValidatorApplicationTest.testRunWithNonExistentFileShouldReturnFileErrorExitCode(AsvValidatorApplicationTest.java:94) ~[test-classes/:?] + at jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[?:?] + at java.lang.reflect.Method.invoke(Method.java:580) ~[?:?] + at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727) ~[junit-platform-commons-1.9.2.jar:1.9.2] + at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at java.util.ArrayList.forEach(ArrayList.java:1596) ~[?:?] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55) ~[junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) [junit-platform-launcher-1.9.2.jar:1.9.2] + at org.apache.maven.surefire.junitplatform.LazyLauncher.execute(LazyLauncher.java:50) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:184) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:148) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:122) [surefire-junit-platform-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507) [surefire-booter-3.0.0.jar:3.0.0] + at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495) [surefire-booter-3.0.0.jar:3.0.0] +2026-04-20 08:58:27 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID +2026-04-20 08:58:27 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0 +2026-04-20 08:58:27 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2 +2026-04-20 08:58:27 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR +2026-04-20 08:58:27 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt +2026-04-20 08:58:27 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID +2026-04-20 08:58:27 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15) +2026-04-20 08:58:27 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15) +2026-04-20 08:59:00 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID +2026-04-20 08:59:00 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0 +2026-04-20 08:59:00 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2 +2026-04-20 08:59:00 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR +2026-04-20 08:59:00 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt +2026-04-20 08:59:00 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID +2026-04-20 08:59:00 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15) +2026-04-20 08:59:00 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15) +2026-04-20 08:59:35 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID +2026-04-20 08:59:35 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0 +2026-04-20 08:59:35 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2 +2026-04-20 08:59:35 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR +2026-04-20 08:59:35 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt +2026-04-20 08:59:35 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID +2026-04-20 08:59:35 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15) +2026-04-20 08:59:35 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15) +2026-04-20 08:59:51 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'test-asv.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15) +2026-04-20 08:59:51 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: test-asv.txt, Urteil: VALID +2026-04-20 08:59:55 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0 +2026-04-20 08:59:59 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: C:/Program Files/Git/nicht/vorhanden.txt +2026-04-20 09:00:16 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID +2026-04-20 09:00:16 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0 +2026-04-20 09:00:16 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2 +2026-04-20 09:00:16 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR +2026-04-20 09:00:16 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt +2026-04-20 09:00:16 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID +2026-04-20 09:00:16 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15) +2026-04-20 09:00:16 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15) +2026-04-20 09:00:46 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID +2026-04-20 09:00:46 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0 +2026-04-20 09:00:46 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2 +2026-04-20 09:00:46 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR +2026-04-20 09:00:46 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt +2026-04-20 09:00:46 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID +2026-04-20 09:00:46 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15) +2026-04-20 09:00:46 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15) +2026-04-20 09:02:24 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID +2026-04-20 09:02:24 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0 +2026-04-20 09:02:24 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2 +2026-04-20 09:02:24 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR +2026-04-20 09:02:24 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt +2026-04-20 09:02:24 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID +2026-04-20 09:02:24 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15) +2026-04-20 09:02:24 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15) +2026-04-20 09:03:04 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID +2026-04-20 09:03:04 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0 +2026-04-20 09:03:04 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2 +2026-04-20 09:03:04 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR +2026-04-20 09:03:04 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt +2026-04-20 09:03:04 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID +2026-04-20 09:03:04 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15) +2026-04-20 09:03:04 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15) +2026-04-20 09:07:10 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID +2026-04-20 09:07:10 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0 +2026-04-20 09:07:10 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2 +2026-04-20 09:07:10 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR +2026-04-20 09:07:10 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt +2026-04-20 09:07:10 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID +2026-04-20 09:07:10 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15) +2026-04-20 09:07:10 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15) +2026-04-20 09:09:15 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: leer.txt, Urteil: VALID +2026-04-20 09:09:15 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 0 +2026-04-20 09:09:15 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Ungültige Argumentanzahl: 2 +2026-04-20 09:09:15 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: bedien.txt, Urteil: OPERATIONAL_ERROR +2026-04-20 09:09:15 [main] WARN de.gecheckt.asv.adapter.in.cli.CliRunner - Datei nicht gefunden: /nicht/vorhanden/datei.txt +2026-04-20 09:09:15 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: fehlerhaft.txt, Urteil: INVALID +2026-04-20 09:09:15 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'inhalt.txt' gelesen (11 Bytes, 11 Zeichen, Encoding: ISO-8859-15) +2026-04-20 09:09:15 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'leer.txt' gelesen (0 Bytes, 0 Zeichen, Encoding: ISO-8859-15) diff --git a/nicht-vorhanden.txt.txt b/nicht-vorhanden.txt.txt new file mode 100644 index 0000000..bfb43a8 --- /dev/null +++ b/nicht-vorhanden.txt.txt @@ -0,0 +1,14 @@ +================================================================ +ASV-Format-Validator – Prüfbericht +================================================================ +Zeitstempel : 2026-04-20T07:41:22.3183814Z +Eingabedatei: nicht-vorhanden.txt +Urteil : BEDIENFEHLER +---------------------------------------------------------------- +Befunde (1): + [ERROR] [SPEC] [ARTIFACT] Regel=OPERATIONAL-FILE-NOT-FOUND – Fehler: Datei nicht gefunden: nicht-vorhanden.txt +---------------------------------------------------------------- +Hinweis: Dieser Bericht wurde mit dem M1-Platzhalter-Validator +erzeugt. Viele Prüfbereiche (Fachmodell, Inhalt, Referenzdaten) +werden erst ab M3 aktiv geprüft. +================================================================ diff --git a/pom.xml b/pom.xml index e03e591..de80624 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,14 @@ ${mockito.version} test + + + + com.tngtech.archunit + archunit-junit5 + 1.3.0 + test + @@ -115,18 +123,46 @@ - + org.apache.maven.plugins - maven-jar-plugin - ${maven-jar-plugin.version} - - - - de.gecheckt.asv.bootstrap.Main - - - + maven-shade-plugin + 3.5.2 + + + + org.apache.logging.log4j + log4j-transform-maven-shade-plugin-extensions + 0.1.0 + + + + + package + + shade + + + + + de.gecheckt.asv.bootstrap.Main + + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + diff --git a/src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java b/src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java index 1c32789..6bd32ab 100644 --- a/src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java +++ b/src/main/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplication.java @@ -1,164 +1,30 @@ package de.gecheckt.asv.adapter.in.cli; -import de.gecheckt.asv.domain.model.InputFile; -import de.gecheckt.asv.adapter.out.parsing.DefaultInputFileParser; -import de.gecheckt.asv.adapter.out.parsing.DefaultSegmentLineTokenizer; -import de.gecheckt.asv.adapter.out.parsing.InputFileParseException; -import de.gecheckt.asv.adapter.out.parsing.InputFileParser; -import de.gecheckt.asv.adapter.out.parsing.SegmentLineTokenizer; -import de.gecheckt.asv.application.DefaultInputFileValidator; -import de.gecheckt.asv.application.InputFileValidator; -import de.gecheckt.asv.application.field.DefaultFieldValidator; -import de.gecheckt.asv.application.field.FieldValidator; -import de.gecheckt.asv.application.model.ValidationResult; -import de.gecheckt.asv.application.structure.DefaultStructureValidator; -import de.gecheckt.asv.application.structure.StructureValidator; -import de.gecheckt.asv.adapter.out.reporting.ValidationResultPrinter; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** - * Haupteinstiegspunkt für die ASV Format Validator CLI-Anwendung. - * - * Diese Anwendung validiert Dateien gegen ein segmentorientiertes Dateiformat. - * Sie nimmt einen Dateipfad als Kommandozeilenargument entgegen, parst die Datei, - * validiert sie und gibt die Ergebnisse auf der Konsole aus. + * Ehemaliger Haupteinstiegspunkt des ASV-Format-Validators. + * + *

Veraltet seit AP06. Die Verantwortlichkeiten wurden aufgeteilt:

+ *
    + *
  • Bootstrap und Constructor Injection → {@link de.gecheckt.asv.bootstrap.Main}
  • + *
  • CLI-Argument-Verarbeitung und Exit-Code → {@link CliRunner}
  • + *
+ * + *

Diese Klasse bleibt als leere Hülle erhalten, bis AP09 (Altlogik einfrieren) abgeschlossen + * ist. Sie darf nicht mehr direkt verwendet werden.

+ * + * @deprecated Ersetzt durch {@link de.gecheckt.asv.bootstrap.Main} und {@link CliRunner} (AP06). + * Wird in AP09 endgültig entfernt. */ +@Deprecated(since = "AP06", forRemoval = true) public class AsvValidatorApplication { - - private static final int EXIT_CODE_SUCCESS = 0; - private static final int EXIT_CODE_INVALID_ARGUMENTS = 1; - private static final int EXIT_CODE_FILE_ERROR = 2; - private static final int EXIT_CODE_VALIDATION_ERRORS = 3; - - private static final Logger logger = LoggerFactory.getLogger(AsvValidatorApplication.class); - - private final InputFileParser parser; - private final InputFileValidator validator; - private final ValidationResultPrinter printer; - + /** - * Konstruktor für einen AsvValidatorApplication mit Standardkomponenten. - */ - public AsvValidatorApplication() { - // Initialize all required components - SegmentLineTokenizer tokenizer = new DefaultSegmentLineTokenizer(); - this.parser = new DefaultInputFileParser(tokenizer); - - StructureValidator structureValidator = new DefaultStructureValidator(); - FieldValidator fieldValidator = new DefaultFieldValidator(); - this.validator = new DefaultInputFileValidator(structureValidator, fieldValidator); - - this.printer = new ValidationResultPrinter(); - } - - /** - * Konstruktor für einen AsvValidatorApplication mit den bereitgestellten Komponenten. - * Dieser Konstruktor unterstützt Dependency Injection für bessere Testbarkeit. - * - * @param parser der Parser zum Parsen von Eingabedateien - * @param validator der Validator zum Validieren geparster Dateien - * @param printer der Printer zum Anzeigen von Validierungsergebnissen - */ - public AsvValidatorApplication(InputFileParser parser, InputFileValidator validator, ValidationResultPrinter printer) { - this.parser = parser; - this.validator = validator; - this.printer = printer; - } - - /** - * Haupteinstiegspunkt für die Anwendung. - * - * @param args Kommandozeilenargumente - erwartet genau ein Argument: den Dateipfad + * Nicht mehr verwenden. Nur noch als Kompatibilitätshülle vorhanden. + * + * @deprecated Verwende {@link de.gecheckt.asv.bootstrap.Main#main(String[])} stattdessen. */ + @Deprecated(since = "AP06", forRemoval = true) public static void main(String[] args) { - AsvValidatorApplication app = new AsvValidatorApplication(); - int exitCode = app.run(args); - System.exit(exitCode); + de.gecheckt.asv.bootstrap.Main.main(args); } - - /** - * Führt die Anwendung mit den bereitgestellten Argumenten aus. - * - * @param args Kommandozeilenargumente - * @return Exit-Code (0 für Erfolg, ungleich 0 für Fehler) - */ - public int run(String[] args) { - // Validate command line arguments - if (args.length != 1) { - printUsage(); - return EXIT_CODE_INVALID_ARGUMENTS; - } - - String filePath = args[0]; - - try { - // Parse the file - InputFile inputFile = parseFile(filePath); - - // Validate the parsed file - ValidationResult result = validator.validate(inputFile); - - // Output results - printer.printToConsole(result); - - // Return appropriate exit code based on validation results - return result.hasErrors() ? EXIT_CODE_VALIDATION_ERRORS : EXIT_CODE_SUCCESS; - - } catch (IOException e) { - logger.error("Fehler beim Lesen der Datei: {}", e.getMessage(), e); - System.err.println("Fehler beim Lesen der Datei: " + e.getMessage()); - return EXIT_CODE_FILE_ERROR; - } catch (InputFileParseException e) { - logger.error("Fehler beim Parsen der Datei: {}", e.getMessage(), e); - System.err.println("Fehler beim Parsen der Datei: " + e.getMessage()); - return EXIT_CODE_FILE_ERROR; - } catch (Exception e) { - logger.error("Unerwarteter Fehler während der Validierung: {}", e.getMessage(), e); - System.err.println("Unerwarteter Fehler während der Validierung: " + e.getMessage()); - return EXIT_CODE_FILE_ERROR; - } - } - - /** - * Parst eine Datei unter dem gegebenen Pfad. - * - * @param filePath Pfad zur zu parsenden Datei - * @return geparstes InputFile-Objekt - * @throws IOException wenn die Datei nicht gelesen werden kann - * @throws InputFileParseException wenn die Datei nicht geparst werden kann - */ - private InputFile parseFile(String filePath) throws IOException, InputFileParseException { - Path path = Paths.get(filePath); - if (!Files.exists(path)) { - throw new IOException("File does not exist: " + filePath); - } - - if (!Files.isRegularFile(path)) { - throw new IOException("Path is not a regular file: " + filePath); - } - - if (!Files.isReadable(path)) { - throw new IOException("File is not readable: " + filePath); - } - - String fileContent = Files.readString(path, StandardCharsets.UTF_8); - return parser.parse(path.getFileName().toString(), fileContent); - } - - /** - * Gibt Nutzungsinformationen auf der Konsole aus. - */ - private void printUsage() { - System.out.println("ASV Format Validator"); - System.out.println("Verwendung: java -jar asv-format-validator.jar "); - System.out.println(" Pfad zur zu validierenden Datei"); - } -} \ No newline at end of file +} diff --git a/src/main/java/de/gecheckt/asv/adapter/in/cli/CliRunner.java b/src/main/java/de/gecheckt/asv/adapter/in/cli/CliRunner.java new file mode 100644 index 0000000..4c67a90 --- /dev/null +++ b/src/main/java/de/gecheckt/asv/adapter/in/cli/CliRunner.java @@ -0,0 +1,262 @@ +package de.gecheckt.asv.adapter.in.cli; + +import de.gecheckt.asv.adapter.out.filesystem.SuffixResolver; +import de.gecheckt.asv.adapter.out.logging.LoggingConfigurator; +import de.gecheckt.asv.adapter.out.reporting.ReportFileWriter; +import de.gecheckt.asv.application.FileValidationService; +import de.gecheckt.asv.domain.finding.ValidationReport; +import de.gecheckt.asv.domain.finding.Verdict; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; + +/** + * Eingehender CLI-Adapter. Nimmt Kommandozeilenargumente entgegen, prüft die + * Eingabedatei auf Existenz und Lesbarkeit, konfiguriert die Log-Datei, delegiert + * die Validierung an den {@link FileValidationService}, schreibt die Berichtdatei und + * gibt das Ergebnis auf der Konsole aus. Übersetzt das Ergebnis in einen numerischen + * Exit-Code gemäß {@link ExitCode}. + * + *

Reihenfolge pro Lauf (AP07/AP08):

+ *
    + *
  1. Argument-Prüfung und Datei-Vorabprüfung
  2. + *
  3. Bei Bedienfehler: Minimalbericht erzeugen und ggf. Berichtdatei schreiben
  4. + *
  5. Log-Datei-Pfad via {@link SuffixResolver} bestimmen
  6. + *
  7. {@link LoggingConfigurator#configureLogFile(Path)} aufrufen
  8. + *
  9. Validierungslauf über {@link FileValidationService#validate(Path)}
  10. + *
  11. Berichtdatei über {@link ReportFileWriter#write(ValidationReport, Path)} schreiben
  12. + *
  13. Berichtinhalt auf der Konsole ausgeben
  14. + *
+ * + *

Dieser Adapter enthält keinerlei Log4j2-Typen. Logging erfolgt ausschließlich + * über die SLF4J-Fassade. Die Log4j2-Umkonfiguration delegiert er an + * {@link LoggingConfigurator}, der im {@code adapter.out.logging}-Paket liegt.

+ * + *

Bedienfehler-Fälle (AP08):

+ *
    + *
  • Kein Argument → nur Konsole (Verzeichnis unbekannt)
  • + *
  • Mehr als ein Argument → nur Konsole
  • + *
  • Eingabedatei existiert nicht → Konsole + Berichtdatei im übergeordneten Verzeichnis
  • + *
  • Pfad ist kein regulärer Dateityp → nur Konsole
  • + *
  • Datei nicht lesbar → Konsole + Berichtdatei im übergeordneten Verzeichnis
  • + *
+ */ +public class CliRunner { + + private static final Logger log = LoggerFactory.getLogger(CliRunner.class); + + /** Platzhalter-Dateiname für Fälle ohne auflösbaren Dateinamen. */ + private static final String PLACEHOLDER_NO_ARG = ""; + private static final String PLACEHOLDER_MANY_ARGS = ""; + + private final FileValidationService validationService; + private final LoggingConfigurator loggingConfigurator; + private final SuffixResolver suffixResolver; + private final ReportFileWriter reportFileWriter; + + /** + * Erzeugt einen neuen {@code CliRunner} mit allen benötigten Adaptern. + * + * @param validationService Dienst, der die Dateivalidierung übernimmt (nicht null) + * @param loggingConfigurator Konfiguriert den Log-Datei-Pfad (nicht null) + * @param suffixResolver Ermittelt den freien Log-Datei-Pfad (nicht null) + * @param reportFileWriter Schreibt die Berichtdatei (nicht null) + */ + public CliRunner(FileValidationService validationService, + LoggingConfigurator loggingConfigurator, + SuffixResolver suffixResolver, + ReportFileWriter reportFileWriter) { + this.validationService = Objects.requireNonNull(validationService, + "validationService darf nicht null sein"); + this.loggingConfigurator = Objects.requireNonNull(loggingConfigurator, + "loggingConfigurator darf nicht null sein"); + this.suffixResolver = Objects.requireNonNull(suffixResolver, + "suffixResolver darf nicht null sein"); + this.reportFileWriter = Objects.requireNonNull(reportFileWriter, + "reportFileWriter darf nicht null sein"); + } + + /** + * Führt den CLI-Lauf mit den übergebenen Argumenten durch. + * + *

Genau ein Positionsargument wird erwartet: der Pfad zur Eingabedatei. + * Bei 0 oder ≥ 2 Argumenten wird Exit-Code {@link ExitCode#OPERATIONAL_ERROR} (2) + * zurückgegeben und ein Minimalbericht auf STDERR und (wo möglich) als Datei ausgegeben.

+ * + *

IO-Fehler beim Schreiben der Berichtdatei verhindern die Konsolenausgabe nicht. + * Das Ergebnis wird in jedem Fall auf die Konsole geschrieben.

+ * + * @param args Kommandozeilenargumente + * @return Exit-Code: {@link ExitCode#VALID} (0), {@link ExitCode#INVALID} (1) + * oder {@link ExitCode#OPERATIONAL_ERROR} (2) + */ + public int run(String[] args) { + // --- Fall 1: Kein Argument --- + if (args.length == 0) { + String msg = "Fehler: Kein Dateipfad angegeben. Verwendung: java -jar asv-format-validator.jar "; + log.error("Bedienfehler: Kein Argument übergeben."); + ValidationReport report = ValidationReport.operationalError( + PLACEHOLDER_NO_ARG, "OPERATIONAL-MISSING-ARG", msg); + // Kein Verzeichnis bekannt → nur Konsole + writeMinimalReportToConsoleOnly(report); + return ExitCode.OPERATIONAL_ERROR; + } + + // --- Fall 2: Mehr als ein Argument --- + if (args.length > 1) { + String msg = "Fehler: Zu viele Argumente. Es wird genau ein Dateipfad erwartet."; + log.error("Bedienfehler: Zu viele Argumente ({}). ", args.length); + ValidationReport report = ValidationReport.operationalError( + PLACEHOLDER_MANY_ARGS, "OPERATIONAL-TOO-MANY-ARGS", msg); + // Kein eindeutiges Verzeichnis → nur Konsole + writeMinimalReportToConsoleOnly(report); + return ExitCode.OPERATIONAL_ERROR; + } + + String filePath = args[0]; + + // Pfad parsen + Path path; + try { + path = Paths.get(filePath); + } catch (InvalidPathException e) { + String msg = "Fehler: Ungültiger Dateipfad: " + filePath; + log.error("Bedienfehler: Ungültiger Dateipfad: {}", filePath); + ValidationReport report = ValidationReport.operationalError( + filePath, "OPERATIONAL-FILE-NOT-FOUND", msg); + // Ungültiger Pfad → kein Verzeichnis ableitbar → nur Konsole + writeMinimalReportToConsoleOnly(report); + return ExitCode.OPERATIONAL_ERROR; + } + + // --- Fall 3: Datei existiert nicht --- + if (!Files.exists(path)) { + String msg = "Fehler: Datei nicht gefunden: " + filePath; + log.error("Bedienfehler: Datei nicht gefunden: {}", filePath); + String fileBaseName = path.getFileName() != null ? path.getFileName().toString() : "bedienfehler"; + ValidationReport report = ValidationReport.operationalError( + fileBaseName, "OPERATIONAL-FILE-NOT-FOUND", msg); + // Übergeordnetes Verzeichnis ableiten und Bericht schreiben, sofern schreibbar + Path parent = path.toAbsolutePath().getParent(); + writeMinimalReportWithOptionalFile(report, parent, fileBaseName); + return ExitCode.OPERATIONAL_ERROR; + } + + // --- Fall 4: Kein regulärer Dateityp --- + if (!Files.isRegularFile(path)) { + String msg = "Fehler: Pfad ist keine reguläre Datei (z.B. Verzeichnis): " + filePath; + log.error("Bedienfehler: Pfad ist keine reguläre Datei: {}", filePath); + String fileBaseName = path.getFileName() != null ? path.getFileName().toString() : filePath; + ValidationReport report = ValidationReport.operationalError( + fileBaseName, "OPERATIONAL-NOT-REGULAR", msg); + // Kein Minimalbericht als Datei (nur Konsole), da unklar ob Verz. schreibbar + writeMinimalReportToConsoleOnly(report); + return ExitCode.OPERATIONAL_ERROR; + } + + // --- Fall 5: Datei nicht lesbar --- + if (!Files.isReadable(path)) { + String msg = "Fehler: Datei ist nicht lesbar (fehlende Leseberechtigung): " + filePath; + log.error("Bedienfehler: Datei nicht lesbar: {}", filePath); + String fileBaseName = path.getFileName() != null ? path.getFileName().toString() : filePath; + ValidationReport report = ValidationReport.operationalError( + fileBaseName, "OPERATIONAL-NOT-READABLE", msg); + // Übergeordnetes Verzeichnis ableiten und Bericht schreiben, sofern schreibbar + Path parent = path.toAbsolutePath().getParent(); + writeMinimalReportWithOptionalFile(report, parent, fileBaseName); + return ExitCode.OPERATIONAL_ERROR; + } + + // --- Normaler Validierungslauf --- + + // Log-Datei bestimmen und Logging umkonfigurieren (AP07) + String baseName = path.getFileName().toString(); + Path directory = path.toAbsolutePath().getParent(); + if (directory == null) { + directory = Path.of("."); + } + Path logPath = suffixResolver.resolveNextFreePath(directory, baseName, "log"); + loggingConfigurator.configureLogFile(logPath); + + log.info("ASV-Format-Validator gestartet. Eingabedatei: {}", path.toAbsolutePath()); + + // Validierung delegieren + ValidationReport report = validationService.validate(path); + Verdict verdict = report.computeVerdict(); + + log.info("Validierung abgeschlossen. Datei: {}, Urteil: {}", path.getFileName(), verdict); + + // Berichtdatei schreiben (AP07) + ReportFileWriter.ReportWriteResult writeResult = reportFileWriter.write(report, path); + + if (!writeResult.isSuccess()) { + log.error("Berichtdatei konnte nicht geschrieben werden: {}", + writeResult.writeException() != null + ? writeResult.writeException().getMessage() : "unbekannter Fehler"); + } + + // Konsolenausgabe (immer, auch bei Schreibfehler der Datei) + System.out.print(writeResult.reportContent()); + + return switch (verdict) { + case VALID -> ExitCode.VALID; + case INVALID -> ExitCode.INVALID; + case OPERATIONAL_ERROR -> ExitCode.OPERATIONAL_ERROR; + }; + } + + /** + * Gibt den Minimalbericht ausschließlich auf der Konsole (STDERR) aus. + * Wird verwendet, wenn kein Zielverzeichnis bekannt oder sinnvoll ableitbar ist. + * + * @param report der Bedienfehler-Bericht (nicht null) + */ + private void writeMinimalReportToConsoleOnly(ValidationReport report) { + String content = reportFileWriter.buildMinimalReportContent(report); + System.err.print(content); + } + + /** + * Gibt den Minimalbericht auf der Konsole (STDERR) aus und versucht zusätzlich, + * ihn als Datei in das angegebene Verzeichnis zu schreiben. + * + *

Ist das Verzeichnis nicht vorhanden oder nicht schreibbar, wird nur eine + * Hinweiszeile auf STDERR ausgegeben — kein Fehler auf Fehler.

+ * + * @param report der Bedienfehler-Bericht (nicht null) + * @param directory das Zielverzeichnis; kann {@code null} sein + * @param baseName Basisname für die Berichtdatei + */ + private void writeMinimalReportWithOptionalFile(ValidationReport report, + Path directory, + String baseName) { + String content = reportFileWriter.buildMinimalReportContent(report); + System.err.print(content); + + if (directory == null || !Files.isDirectory(directory) || !Files.isWritable(directory)) { + System.err.println("Bericht konnte nicht in das Verzeichnis geschrieben werden."); + log.warn("Bedienfehler-Bericht konnte nicht als Datei geschrieben werden: " + + "Verzeichnis nicht vorhanden oder nicht schreibbar: {}", directory); + return; + } + + ReportFileWriter.ReportWriteResult result = + reportFileWriter.writeOperationalError(report, directory, baseName); + + if (result.isSuccess()) { + log.info("Bedienfehler-Bericht geschrieben: {}", result.reportPath()); + System.err.println("Bericht geschrieben: " + result.reportPath()); + } else { + System.err.println("Bericht konnte nicht in das Verzeichnis geschrieben werden."); + log.warn("Bedienfehler-Bericht konnte nicht geschrieben werden: {}", + result.writeException() != null + ? result.writeException().getMessage() : "unbekannter Fehler"); + } + } +} diff --git a/src/main/java/de/gecheckt/asv/adapter/in/cli/ExitCode.java b/src/main/java/de/gecheckt/asv/adapter/in/cli/ExitCode.java new file mode 100644 index 0000000..7f87da0 --- /dev/null +++ b/src/main/java/de/gecheckt/asv/adapter/in/cli/ExitCode.java @@ -0,0 +1,32 @@ +package de.gecheckt.asv.adapter.in.cli; + +/** + * Normative Exit-Codes der ASV-Format-Validator-CLI. + * + *

Die drei zulässigen Exit-Codes sind gemäß Technischer Anlage ASV 1.09 und + * {@code docs/specs/technik-und-architektur.md} definiert:

+ *
    + *
  • {@link #VALID} (0) — Datei ist spec-konform, keine SPEC-ERROR-Befunde
  • + *
  • {@link #INVALID} (1) — Datei enthält mindestens einen SPEC-ERROR-Befund
  • + *
  • {@link #OPERATIONAL_ERROR} (2) — Bedienfehler (fehlendes Argument, nicht lesbare Datei)
  • + *
+ * + *

Exit-Code 3 existiert nicht mehr. Die früheren Konstanten + * {@code EXIT_CODE_INVALID_ARGUMENTS}, {@code EXIT_CODE_FILE_ERROR} und + * {@code EXIT_CODE_VALIDATION_ERRORS} wurden mit AP06 entfernt.

+ */ +public final class ExitCode { + + /** Datei ist gültig (keine SPEC-ERROR-Befunde). */ + public static final int VALID = 0; + + /** Datei enthält mindestens einen SPEC-ERROR-Befund. */ + public static final int INVALID = 1; + + /** Bedienfehler: falsches Argument, nicht lesbare Datei o. Ä. */ + public static final int OPERATIONAL_ERROR = 2; + + private ExitCode() { + // Nicht instanziierbar + } +} diff --git a/src/main/java/de/gecheckt/asv/adapter/out/filesystem/SuffixResolver.java b/src/main/java/de/gecheckt/asv/adapter/out/filesystem/SuffixResolver.java new file mode 100644 index 0000000..d32b513 --- /dev/null +++ b/src/main/java/de/gecheckt/asv/adapter/out/filesystem/SuffixResolver.java @@ -0,0 +1,87 @@ +package de.gecheckt.asv.adapter.out.filesystem; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Ermittelt den nächsten freien Dateipfad im Zielverzeichnis anhand von Basisname und Extension. + * + *

Die Suffix-Logik arbeitet extension-unabhängig: Für {@code .txt} und {@code .log} werden + * separate Zähler geführt. Beim ersten Lauf entsteht {@code .}; bei jedem + * weiteren Lauf wird {@code _v1.}, {@code _v2.} usw. erzeugt, + * bis ein freier Pfad gefunden ist.

+ * + *

Dieses Objekt ist zustandslos. Alle Methoden können nebenläufig auf verschiedenen + * Eingabedatei-Pfaden verwendet werden; Race Conditions bei gleichzeitigen Läufen auf + * derselben Eingabedatei sind in V1 bewusst nicht behandelt.

+ */ +public class SuffixResolver { + + /** + * Ermittelt den ersten freien Dateipfad für den gegebenen Basisnamen und die gegebene + * Extension im Zielverzeichnis. + * + *

Probiert in dieser Reihenfolge:

+ *
    + *
  1. {@code .}
  2. + *
  3. {@code _v1.}
  4. + *
  5. {@code _v2.}
  6. + *
  7. … bis ein freier Pfad gefunden ist
  8. + *
+ * + *

Die Zählung ist pro Extension unabhängig: Eine vorhandene {@code foo.auf.txt} hat + * keinen Einfluss auf die Zählung für {@code foo.auf.log}.

+ * + * @param directory das Zielverzeichnis (muss existieren) + * @param baseName Basisname ohne Extension (z.B. {@code "foo.auf"}) + * @param extension Extension ohne führenden Punkt (z.B. {@code "txt"}) + * @return der erste freie Pfad (existiert noch nicht im Dateisystem) + * @throws IllegalArgumentException wenn {@code directory}, {@code baseName} oder + * {@code extension} null oder leer sind + * @throws UncheckedIOException wenn der Dateisystem-Zugriff fehlschlägt + */ + public Path resolveNextFreePath(Path directory, String baseName, String extension) { + if (directory == null) { + throw new IllegalArgumentException("directory darf nicht null sein"); + } + if (baseName == null || baseName.isBlank()) { + throw new IllegalArgumentException("baseName darf nicht null oder leer sein"); + } + if (extension == null || extension.isBlank()) { + throw new IllegalArgumentException("extension darf nicht null oder leer sein"); + } + + // Kandidat ohne Suffix: . + Path candidate = directory.resolve(baseName + "." + extension); + if (!exists(candidate)) { + return candidate; + } + + // Mit Suffix: _v1., _v2., ... + int counter = 1; + while (true) { + candidate = directory.resolve(baseName + "_v" + counter + "." + extension); + if (!exists(candidate)) { + return candidate; + } + counter++; + } + } + + /** + * Prüft, ob der Pfad im Dateisystem existiert. + * + * @param path der zu prüfende Pfad + * @return {@code true} wenn die Datei existiert + */ + private boolean exists(Path path) { + try { + return Files.exists(path); + } catch (SecurityException e) { + throw new UncheckedIOException( + new IOException("Dateisystem-Zugriff verweigert: " + path, e)); + } + } +} diff --git a/src/main/java/de/gecheckt/asv/adapter/out/logging/LoggingConfigurator.java b/src/main/java/de/gecheckt/asv/adapter/out/logging/LoggingConfigurator.java index 1dd5bba..c4c4c6b 100644 --- a/src/main/java/de/gecheckt/asv/adapter/out/logging/LoggingConfigurator.java +++ b/src/main/java/de/gecheckt/asv/adapter/out/logging/LoggingConfigurator.java @@ -1,21 +1,86 @@ package de.gecheckt.asv.adapter.out.logging; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.FileAppender; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.layout.PatternLayout; + import java.nio.file.Path; +import java.util.Map; +import java.util.Objects; /** * Konfiguriert den Log4j2-Logging-Adapter programmatisch. - * Erlaubt es, den Zielpfad der Log-Datei zur Laufzeit zu setzen. - * Log4j2-Typen dürfen in diesem Paket direkt verwendet werden. + * + *

Erlaubt es, den Zielpfad der Log-Datei zur Laufzeit zu setzen, bevor der erste + * fachliche Log-Aufruf erfolgt. Log4j2-Typen dürfen ausschließlich in diesem Paket + * ({@code adapter.out.logging}) und in {@code bootstrap} sichtbar sein.

+ * + *

Implementierungsansatz: programmatische Umkonfiguration über die Log4j2 Configurator-API. + * Ein neuer {@link FileAppender} wird erzeugt und dem Root-Logger sowie dem + * {@code de.gecheckt.asv}-Logger hinzugefügt. Der statische Fallback-Appender aus + * {@code log4j2.xml} (Datei {@code logs/asv-format-validator.log}) bleibt als Fallback + * bestehen, wenn diese Methode nicht aufgerufen wird (z.B. in reinen Unit-Tests).

*/ public class LoggingConfigurator { + private static final String PATTERN = + "%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"; + /** - * Setzt den Zielpfad der Log-Datei für diesen Lauf. - * Die tatsächliche dynamische Umleitung wird in AP07 implementiert. + * Setzt den Zielpfad der Log-Datei für diesen Lauf und konfiguriert Log4j2 + * programmatisch um. * - * @param logFile Pfad zur Log-Datei + *

Diese Methode muss vor dem ersten fachlichen Log-Aufruf + * aufgerufen werden. Nach dem Aufruf gehen alle Log-Nachrichten sowohl in die + * Konsole (STDERR, über den bestehenden Console-Appender) als auch in die + * angegebene Datei.

+ * + *

Schlägt die Umkonfiguration fehl (z.B. wegen eines SecurityManagers oder + * inkompatiblem Log4j2-Zustand), wird ein Fallback-Warnung auf STDERR ausgegeben; + * der laufende Betrieb wird nicht unterbrochen — Logs gehen dann nur in den + * Fallback-Appender aus {@code log4j2.xml}.

+ * + * @param logFile Zielpfad der Log-Datei (nicht null) + * @throws IllegalArgumentException wenn {@code logFile} null ist */ public void configureLogFile(Path logFile) { - // TODO: dynamische Log-Datei-Umleitung in AP07 + Objects.requireNonNull(logFile, "logFile darf nicht null sein"); + + try { + LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + Configuration config = ctx.getConfiguration(); + + PatternLayout layout = PatternLayout.newBuilder() + .withPattern(PATTERN) + .withConfiguration(config) // withConfiguration ist in PatternLayout.Builder nicht deprecated + .build(); + + FileAppender fileAppender = FileAppender.newBuilder() + .withFileName(logFile.toAbsolutePath().toString()) + .withAppend(false) + .setName("DynamicFile") + .setLayout(layout) + .setConfiguration(config) + .build(); + + fileAppender.start(); + config.addAppender(fileAppender); + + // Appender dem de.gecheckt.asv-Logger hinzufügen (falls vorhanden) + Map loggers = config.getLoggers(); + for (LoggerConfig loggerConfig : loggers.values()) { + loggerConfig.addAppender(fileAppender, null, null); + } + + ctx.updateLoggers(); + + } catch (Exception e) { + System.err.println("[LoggingConfigurator] Warnung: Programmatische Log4j2-Umkonfiguration" + + " fehlgeschlagen. Logs gehen nur in den Fallback-Appender. Ursache: " + + e.getMessage()); + } } } diff --git a/src/main/java/de/gecheckt/asv/adapter/out/reporting/ReportFileWriter.java b/src/main/java/de/gecheckt/asv/adapter/out/reporting/ReportFileWriter.java new file mode 100644 index 0000000..70b5594 --- /dev/null +++ b/src/main/java/de/gecheckt/asv/adapter/out/reporting/ReportFileWriter.java @@ -0,0 +1,258 @@ +package de.gecheckt.asv.adapter.out.reporting; + +import de.gecheckt.asv.adapter.out.filesystem.SuffixResolver; +import de.gecheckt.asv.domain.finding.Finding; +import de.gecheckt.asv.domain.finding.ValidationReport; +import de.gecheckt.asv.domain.finding.Verdict; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Objects; + +/** + * Schreibt den Validierungsbericht in eine UTF-8-Textdatei im Verzeichnis der Eingabedatei. + * + *

Der Dateiname wird über {@link SuffixResolver} bestimmt: Basisname ist der vollständige + * Dateiname der Eingabedatei inklusive Extension (z.B. {@code foo.auf}); die Ausgabedatei + * heißt dann {@code foo.auf.txt}, beim zweiten Lauf {@code foo.auf_v1.txt} usw.

+ * + *

Der Bericht ist für M1 absichtlich minimal strukturiert und wird in M9 durch die + * finale hierarchische Gliederung ersetzt. Alle Texte auf Deutsch, Encoding explizit UTF-8.

+ * + *

Dieses Objekt enthält keinerlei Log4j2-Typen — Logging erfolgt ausschließlich über + * die SLF4J-Fassade.

+ */ +public class ReportFileWriter { + + private static final Logger log = LoggerFactory.getLogger(ReportFileWriter.class); + + private static final DateTimeFormatter TIMESTAMP_FORMATTER = + DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneOffset.UTC); + + private final SuffixResolver suffixResolver; + + /** + * Erzeugt einen neuen {@code ReportFileWriter} mit dem angegebenen {@link SuffixResolver}. + * + * @param suffixResolver Resolver für den nächsten freien Dateipfad (nicht null) + */ + public ReportFileWriter(SuffixResolver suffixResolver) { + this.suffixResolver = Objects.requireNonNull(suffixResolver, + "suffixResolver darf nicht null sein"); + } + + /** + * Schreibt den Validierungsbericht als UTF-8-Textdatei in das Verzeichnis der Eingabedatei. + * + *

Gibt den Inhalt der Berichtdatei als String zurück, damit der Aufrufer ihn + * identisch auf der Konsole ausgeben kann. Ist das Schreiben der Datei nicht möglich, + * wird die Ausnahme protokolliert und die Methode gibt einen Fehlerbericht-String zurück — + * der Aufrufer kann die Konsolenausgabe trotzdem ausgeben.

+ * + * @param report der Validierungsbericht (nicht null) + * @param inputFilePath Pfad zur Eingabedatei; bestimmt Verzeichnis und Basisnamen (nicht null) + * @return Pfad zur erzeugten Berichtdatei; {@code null} wenn die Datei nicht geschrieben werden konnte + * @throws IllegalArgumentException wenn {@code report} oder {@code inputFilePath} null sind + */ + public ReportWriteResult write(ValidationReport report, Path inputFilePath) { + if (report == null) { + throw new IllegalArgumentException("report darf nicht null sein"); + } + if (inputFilePath == null) { + throw new IllegalArgumentException("inputFilePath darf nicht null sein"); + } + + // Basisname ist der vollständige Dateiname der Eingabedatei inkl. Extension + String baseName = inputFilePath.getFileName().toString(); + Path directory = inputFilePath.toAbsolutePath().getParent(); + if (directory == null) { + directory = Path.of("."); + } + + // Berichtinhalt aufbauen + String content = buildReportContent(report, inputFilePath); + + // Zieldatei bestimmen + Path reportPath = suffixResolver.resolveNextFreePath(directory, baseName, "txt"); + + // Schreiben + try (Writer writer = Files.newBufferedWriter(reportPath, StandardCharsets.UTF_8)) { + writer.write(content); + log.info("Berichtdatei geschrieben: {}", reportPath); + } catch (IOException e) { + log.error("Fehler beim Schreiben der Berichtdatei {}: {}", reportPath, e.getMessage()); + return new ReportWriteResult(content, null, e); + } + + return new ReportWriteResult(content, reportPath, null); + } + + /** + * Schreibt einen Bedienfehler-Bericht als UTF-8-Textdatei in das angegebene Verzeichnis. + * + *

Im Gegensatz zu {@link #write(ValidationReport, Path)} ist diese Methode für den + * Fall gedacht, dass keine Eingabedatei existiert (z.B. Datei nicht gefunden). Sie nimmt + * das Verzeichnis und den Basisnamen direkt entgegen.

+ * + *

IO-Fehler führen nicht zu einer {@link RuntimeException}, sondern werden + * protokolliert. Das Ergebnis signalisiert den Fehler über {@link ReportWriteResult}.

+ * + * @param report der Bedienfehler-Bericht (nicht null) + * @param directory das Zielverzeichnis (nicht null, muss existieren und schreibbar sein) + * @param baseName Basisname für die Berichtdatei (nicht null, nicht leer) + * @return Schreibergebnis; {@link ReportWriteResult#isSuccess()} gibt an, ob erfolgreich + */ + public ReportWriteResult writeOperationalError(ValidationReport report, + Path directory, + String baseName) { + Objects.requireNonNull(report, "report darf nicht null sein"); + Objects.requireNonNull(directory, "directory darf nicht null sein"); + if (baseName == null || baseName.isBlank()) { + throw new IllegalArgumentException("baseName darf nicht null oder leer sein"); + } + + String content = buildMinimalReportContent(report); + Path reportPath = suffixResolver.resolveNextFreePath(directory, baseName, "txt"); + + try (java.io.Writer writer = java.nio.file.Files.newBufferedWriter( + reportPath, java.nio.charset.StandardCharsets.UTF_8)) { + writer.write(content); + log.info("Bedienfehler-Bericht geschrieben: {}", reportPath); + } catch (IOException e) { + log.error("Fehler beim Schreiben des Bedienfehler-Berichts {}: {}", reportPath, e.getMessage()); + return new ReportWriteResult(content, null, e); + } + + return new ReportWriteResult(content, reportPath, null); + } + + /** + * Erstellt den Berichtinhalt für einen Bedienfehler-Bericht als formatierten String. + * + *

Wird von {@link de.gecheckt.asv.adapter.in.cli.CliRunner} direkt verwendet, wenn + * der Inhalt für die Konsolenausgabe benötigt wird, ohne eine Datei zu schreiben. + * Im Unterschied zu {@link #buildReportContent(ValidationReport, Path)} wird der + * Dateiname aus dem Bericht direkt als String verwendet — kein {@link Path}-Parsing, + * sodass auch Platzhalter wie {@code } mit Sonderzeichen funktionieren.

+ * + * @param report der Bedienfehler-Bericht (nicht null) + * @return der vollständige Berichtinhalt als String + */ + public String buildMinimalReportContent(ValidationReport report) { + Objects.requireNonNull(report, "report darf nicht null sein"); + return buildReportContentWithFileName(report, report.getFileName()); + } + + /** + * Erstellt den Berichtinhalt als formatierten String (delegiert an die String-Variante). + * + * @param report der Validierungsbericht + * @param inputFilePath Pfad zur Eingabedatei + * @return der vollständige Berichtinhalt + */ + String buildReportContent(ValidationReport report, Path inputFilePath) { + return buildReportContentWithFileName(report, inputFilePath.toAbsolutePath().toString()); + } + + /** + * Erstellt den Berichtinhalt als formatierten String mit dem Dateinamen als String. + * + *

Diese Variante wird für Bedienfehler-Berichte verwendet, bei denen der Dateiname + * ein Platzhalter wie {@code } sein kann, der kein gültiger Pfad ist.

+ * + * @param report der Validierungsbericht + * @param fileNameDisplay der anzuzeigende Dateiname (als String, nicht als Pfad) + * @return der vollständige Berichtinhalt + */ + private String buildReportContentWithFileName(ValidationReport report, String fileNameDisplay) { + StringBuilder sb = new StringBuilder(); + Verdict verdict = report.computeVerdict(); + + // Kopfzeile + sb.append("================================================================\n"); + sb.append("ASV-Format-Validator – Prüfbericht\n"); + sb.append("================================================================\n"); + sb.append("Zeitstempel : ").append(TIMESTAMP_FORMATTER.format(report.getTimestamp())).append("\n"); + sb.append("Eingabedatei: ").append(fileNameDisplay).append("\n"); + sb.append("Urteil : ").append(verdictText(verdict)).append("\n"); + sb.append("----------------------------------------------------------------\n"); + + // Befunde + List findings = report.getFindings(); + if (findings.isEmpty()) { + sb.append("Keine Befunde.\n"); + } else { + sb.append("Befunde (").append(findings.size()).append("):\n"); + for (Finding f : findings) { + sb.append(" [") + .append(f.severity()) + .append("] [") + .append(f.kind()) + .append("] [") + .append(f.layer()) + .append("]"); + if (f.ruleId() != null) { + sb.append(" Regel=").append(f.ruleId()); + } + if (f.fieldId() != null) { + sb.append(" Feld=").append(f.fieldId()); + } + sb.append(" – ").append(f.germanMessage()).append("\n"); + } + } + + // Fußzeile + sb.append("----------------------------------------------------------------\n"); + sb.append("Hinweis: Dieser Bericht wurde mit dem M1-Platzhalter-Validator\n"); + sb.append("erzeugt. Viele Prüfbereiche (Fachmodell, Inhalt, Referenzdaten)\n"); + sb.append("werden erst ab M3 aktiv geprüft.\n"); + sb.append("================================================================\n"); + + return sb.toString(); + } + + /** + * Gibt den deutschen Urteil-Text für das übergebene Verdict zurück. + * + * @param verdict das Prüfurteil + * @return deutschsprachige Urteilsbezeichnung + */ + private static String verdictText(Verdict verdict) { + return switch (verdict) { + case VALID -> "GÜLTIG"; + case INVALID -> "UNGÜLTIG"; + case OPERATIONAL_ERROR -> "BEDIENFEHLER"; + }; + } + + /** + * Ergebnis eines Schreibvorgangs. + * + * @param reportContent der erzeugte Berichtinhalt als String (niemals null) + * @param reportPath Pfad zur erzeugten Datei; {@code null} bei Schreibfehler + * @param writeException aufgetretene Ausnahme beim Schreiben; {@code null} bei Erfolg + */ + public record ReportWriteResult( + String reportContent, + Path reportPath, + IOException writeException + ) { + /** + * Gibt an, ob die Datei erfolgreich geschrieben wurde. + * + * @return {@code true} wenn {@link #reportPath()} nicht null ist + */ + public boolean isSuccess() { + return reportPath != null; + } + } +} diff --git a/src/main/java/de/gecheckt/asv/application/DummyFileValidationService.java b/src/main/java/de/gecheckt/asv/application/DummyFileValidationService.java new file mode 100644 index 0000000..3098c7e --- /dev/null +++ b/src/main/java/de/gecheckt/asv/application/DummyFileValidationService.java @@ -0,0 +1,54 @@ +package de.gecheckt.asv.application; + +import de.gecheckt.asv.domain.finding.ValidationReport; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.List; + +/** + * M1-Platzhalter-Implementierung des {@link FileValidationService}. + * + *

Liest die Eingabedatei mit dem normativen Eingabe-Encoding ISO-8859-15 + * ein, zählt die gelesenen Bytes und gibt einen leeren {@link ValidationReport} zurück. + * Keine echte Validierung wird in M1 durchgeführt.

+ * + *

Diese Klasse wird in M3 durch eine echte Implementierung ersetzt, die den + * vollständigen Parser- und Validator-Pfad aktiviert.

+ */ +public class DummyFileValidationService implements FileValidationService { + + /** Normatives Eingabe-Encoding gemäß Technischer Anlage ASV 1.09. */ + static final Charset INPUT_CHARSET = Charset.forName("ISO-8859-15"); + + private static final Logger log = LoggerFactory.getLogger(DummyFileValidationService.class); + + /** + * Liest die Datei mit ISO-8859-15 ein, zählt die Bytes und gibt einen leeren + * Validierungsbericht zurück. + * + * @param inputFile Pfad zur Eingabedatei (nicht null, vorab als existent/lesbar geprüft) + * @return leerer {@link ValidationReport} mit Dateiname und aktuellem Zeitstempel + */ + @Override + public ValidationReport validate(Path inputFile) { + String fileName = inputFile.getFileName().toString(); + + try { + byte[] rawBytes = Files.readAllBytes(inputFile); + String content = new String(rawBytes, INPUT_CHARSET); + log.info("M1-Dummy: Datei '{}' gelesen ({} Bytes, {} Zeichen, Encoding: ISO-8859-15)", + fileName, rawBytes.length, content.length()); + } catch (IOException e) { + log.warn("M1-Dummy: Lesefehler bei '{}': {}", fileName, e.getMessage()); + } + + return new ValidationReport(fileName, Instant.now(), List.of()); + } +} diff --git a/src/main/java/de/gecheckt/asv/application/FileValidationService.java b/src/main/java/de/gecheckt/asv/application/FileValidationService.java new file mode 100644 index 0000000..028a5b7 --- /dev/null +++ b/src/main/java/de/gecheckt/asv/application/FileValidationService.java @@ -0,0 +1,29 @@ +package de.gecheckt.asv.application; + +import de.gecheckt.asv.domain.finding.ValidationReport; + +import java.nio.file.Path; + +/** + * Anwendungsschnittstelle für die Dateivalidierung. + * + *

Nimmt einen bereits vorab geprüften (existierenden, regulären, lesbaren) Dateipfad + * entgegen und gibt einen {@link ValidationReport} zurück.

+ * + *

In M1 liefert die Standardimplementierung ({@code DummyFileValidationService}) einen + * leeren Bericht. Echte Parser- und Validator-Einbindung folgt ab M3.

+ */ +public interface FileValidationService { + + /** + * Validiert die Datei unter dem angegebenen Pfad. + * + *

Der Pfad wird als vorab geprüft betrachtet (existent, reguläre Datei, lesbar). + * Die Implementierung muss stets einen nicht-{@code null}-{@link ValidationReport} + * zurückgeben.

+ * + * @param inputFile Pfad zur zu validierenden Eingabedatei (nicht null) + * @return Validierungsbericht (nicht null) + */ + ValidationReport validate(Path inputFile); +} diff --git a/src/main/java/de/gecheckt/asv/application/field/DefaultFieldValidator.java b/src/main/java/de/gecheckt/asv/application/field/DefaultFieldValidator.java index 18042de..3b25430 100644 --- a/src/main/java/de/gecheckt/asv/application/field/DefaultFieldValidator.java +++ b/src/main/java/de/gecheckt/asv/application/field/DefaultFieldValidator.java @@ -10,13 +10,21 @@ import de.gecheckt.asv.application.model.ValidationResult; import de.gecheckt.asv.application.model.ValidationSeverity; /** - * Default implementation of FieldValidator that checks general field rules. - * - * Rules checked: - * 1. Field.rawValue must not be empty - * 2. Field.rawValue must not consist only of whitespaces - * 3. Field positions within a segment should be consecutive without gaps, starting at 1 - * 4. If fieldName is set, it must not be empty or only whitespace + * M3-Vorbau. In M1 bewusst nicht im produktiven Lauf verdrahtet. + * Wird ab M3 wieder aktiviert und gegen die finalen Regelklassifikationen + * (V1-V/T/N/K) aus fachliche-anforderungen.md bewertet. + * + * @see E-01 + * + *

Standardimplementierung des FieldValidator, die allgemeine Feldregeln prüft.

+ * + *

Geprüfte Regeln:

+ *
    + *
  1. Field.rawValue darf nicht leer sein
  2. + *
  3. Field.rawValue darf nicht nur aus Leerzeichen bestehen
  4. + *
  5. Feldpositionen innerhalb eines Segments sollten lückenlos aufeinanderfolgen, beginnend bei 1
  6. + *
  7. Wenn fieldName gesetzt ist, darf er nicht leer oder nur aus Leerzeichen bestehen
  8. + *
*/ public class DefaultFieldValidator implements FieldValidator { diff --git a/src/main/java/de/gecheckt/asv/application/structure/DefaultStructureValidator.java b/src/main/java/de/gecheckt/asv/application/structure/DefaultStructureValidator.java index 2768551..28ada54 100644 --- a/src/main/java/de/gecheckt/asv/application/structure/DefaultStructureValidator.java +++ b/src/main/java/de/gecheckt/asv/application/structure/DefaultStructureValidator.java @@ -13,28 +13,36 @@ import de.gecheckt.asv.application.model.ValidationResult; import de.gecheckt.asv.application.model.ValidationSeverity; /** - * Standardimplementierung des StructureValidator, die allgemeine Strukturregeln prüft. - * - * Geprüfte Regeln: - * 1. Die Eingabedatei muss mindestens eine Nachricht enthalten - * 2. Jede Nachricht muss mindestens ein Segment enthalten - * 3. Segmentnamen dürfen nicht leer sein - * 4. Feldpositionen innerhalb eines Segments müssen eindeutig und positiv sein - * 5. Segmentpositionen innerhalb einer Nachricht müssen eindeutig und positiv sein - * 6. Nachrichtenpositionen innerhalb einer Eingabedatei müssen eindeutig und positiv sein - * 7. UNH- und UNT-Referenznummern müssen innerhalb einer Nachricht übereinstimmen - * 8. Die im UNT angegebene Segmentanzahl muss der tatsächlichen Anzahl der Segmente entsprechen - * 9. Eine Nachricht muss mindestens ein UNH-Segment enthalten - * 10. Eine Nachricht muss mindestens ein UNT-Segment enthalten - * 11. UNH muss vor UNT stehen - * 12. Der Nachrichtentyp in UNH/S009/0065 darf nur ASVREC oder ASVFEH sein - * 13. Für Nachrichten vom Typ ASVREC müssen die Segmente IFA, REA und IVA vorhanden sein - * 14. Für Nachrichten vom Typ ASVREC muss die Reihenfolge IFA vor REA vor IVA eingehalten werden - * 15. Für ASVREC mit Rechnungskennzeichen "0" in REA müssen DGN und LEA vorhanden sein - * 16. Für ASVREC mit Rechnungskennzeichen "1" in REA dürfen DGN und LEA nicht vorhanden sein - * 17. Für ASVREC mit Rechnungskennzeichen "1" in REA muss der Rechnungsbetrag "0,00" sein - * 18. Für ASVREC müssen IFA, REA und IVA jeweils genau einmal vorkommen - * 19. Für ASVFEH-Nachrichten muss mindestens ein FHL-Segment vorhanden sein + * M3-Vorbau. In M1 bewusst nicht im produktiven Lauf verdrahtet. + * Wird ab M3 wieder aktiviert und gegen die finalen Regelklassifikationen + * (V1-V/T/N/K) aus fachliche-anforderungen.md bewertet. + * + * @see E-01 + * + *

Standardimplementierung des StructureValidator, die allgemeine Strukturregeln prüft.

+ * + *

Geprüfte Regeln:

+ *
    + *
  1. Die Eingabedatei muss mindestens eine Nachricht enthalten
  2. + *
  3. Jede Nachricht muss mindestens ein Segment enthalten
  4. + *
  5. Segmentnamen dürfen nicht leer sein
  6. + *
  7. Feldpositionen innerhalb eines Segments müssen eindeutig und positiv sein
  8. + *
  9. Segmentpositionen innerhalb einer Nachricht müssen eindeutig und positiv sein
  10. + *
  11. Nachrichtenpositionen innerhalb einer Eingabedatei müssen eindeutig und positiv sein
  12. + *
  13. UNH- und UNT-Referenznummern müssen innerhalb einer Nachricht übereinstimmen
  14. + *
  15. Die im UNT angegebene Segmentanzahl muss der tatsächlichen Anzahl der Segmente entsprechen
  16. + *
  17. Eine Nachricht muss mindestens ein UNH-Segment enthalten
  18. + *
  19. Eine Nachricht muss mindestens ein UNT-Segment enthalten
  20. + *
  21. UNH muss vor UNT stehen
  22. + *
  23. Der Nachrichtentyp in UNH/S009/0065 darf nur ASVREC oder ASVFEH sein
  24. + *
  25. Für Nachrichten vom Typ ASVREC müssen die Segmente IFA, REA und IVA vorhanden sein
  26. + *
  27. Für Nachrichten vom Typ ASVREC muss die Reihenfolge IFA vor REA vor IVA eingehalten werden
  28. + *
  29. Für ASVREC mit Rechnungskennzeichen "0" in REA müssen DGN und LEA vorhanden sein
  30. + *
  31. Für ASVREC mit Rechnungskennzeichen "1" in REA dürfen DGN und LEA nicht vorhanden sein
  32. + *
  33. Für ASVREC mit Rechnungskennzeichen "1" in REA muss der Rechnungsbetrag "0,00" sein
  34. + *
  35. Für ASVREC müssen IFA, REA und IVA jeweils genau einmal vorkommen
  36. + *
  37. Für ASVFEH-Nachrichten muss mindestens ein FHL-Segment vorhanden sein
  38. + *
*/ public class DefaultStructureValidator implements StructureValidator { diff --git a/src/main/java/de/gecheckt/asv/bootstrap/Main.java b/src/main/java/de/gecheckt/asv/bootstrap/Main.java new file mode 100644 index 0000000..007c03f --- /dev/null +++ b/src/main/java/de/gecheckt/asv/bootstrap/Main.java @@ -0,0 +1,64 @@ +package de.gecheckt.asv.bootstrap; + +import de.gecheckt.asv.adapter.in.cli.CliRunner; +import de.gecheckt.asv.adapter.out.filesystem.SuffixResolver; +import de.gecheckt.asv.adapter.out.logging.LoggingConfigurator; +import de.gecheckt.asv.adapter.out.reporting.ReportFileWriter; +import de.gecheckt.asv.application.DummyFileValidationService; +import de.gecheckt.asv.application.FileValidationService; + +/** + * Einziger {@code public static void main}-Einstiegspunkt des ASV-Format-Validators. + * + *

Verantwortlichkeiten:

+ *
    + *
  1. Manuelle Constructor Injection aller Anwendungskomponenten
  2. + *
  3. Logging-Konfiguration über {@link LoggingConfigurator} (Log-Datei im Eingabeverzeichnis)
  4. + *
  5. Delegation an {@link CliRunner#run(String[], ReportFileWriter)}
  6. + *
  7. Weiterreichen des Exit-Codes an {@link System#exit(int)}
  8. + *
+ * + *

Log4j2-Sichtbarkeit: Nur dieses Paket ({@code bootstrap}) und + * {@code adapter.out.logging} dürfen Log4j2-Typen direkt verwenden.

+ * + *

Reihenfolge vor dem Validierungslauf (AP07):

+ *
    + *
  1. Eingabedatei-Pfad aus Argumenten bestimmen (in {@link CliRunner})
  2. + *
  3. Basisname und Zielverzeichnis ableiten
  4. + *
  5. {@link SuffixResolver} für {@code .log} aufrufen
  6. + *
  7. {@link LoggingConfigurator#configureLogFile(java.nio.file.Path)} aufrufen
  8. + *
  9. Validierungslauf starten
  10. + *
  11. {@link ReportFileWriter} schreibt Berichtdatei
  12. + *
  13. Konsolenausgabe (identisch zum Berichtinhalt)
  14. + *
+ */ +public final class Main { + + private Main() { + // Nicht instanziierbar + } + + /** + * Startpunkt der CLI-Anwendung. + * + * @param args Kommandozeilenargumente; erwartet genau einen Dateipfad + */ + public static void main(String[] args) { + // Infrastruktur-Objekte (zustandslos) + LoggingConfigurator loggingConfigurator = new LoggingConfigurator(); + SuffixResolver suffixResolver = new SuffixResolver(); + ReportFileWriter reportFileWriter = new ReportFileWriter(suffixResolver); + + // Manuelle Constructor Injection + FileValidationService validationService = new DummyFileValidationService(); + CliRunner cliRunner = new CliRunner( + validationService, + loggingConfigurator, + suffixResolver, + reportFileWriter); + + // Ausführen und Exit-Code weiterreichen + int exitCode = cliRunner.run(args); + System.exit(exitCode); + } +} diff --git a/src/main/java/de/gecheckt/asv/bootstrap/NoOpFieldValidator.java b/src/main/java/de/gecheckt/asv/bootstrap/NoOpFieldValidator.java new file mode 100644 index 0000000..ff872a4 --- /dev/null +++ b/src/main/java/de/gecheckt/asv/bootstrap/NoOpFieldValidator.java @@ -0,0 +1,37 @@ +package de.gecheckt.asv.bootstrap; + +import java.util.List; + +import de.gecheckt.asv.application.field.FieldValidator; +import de.gecheckt.asv.application.model.ValidationResult; +import de.gecheckt.asv.domain.model.InputFile; + +/** + * M1-Platzhalter für den Feldvalidator. + * + *

Erzeugt keinerlei Befunde — der Validator ist in M1 bewusst ohne fachliche Prüflogik + * verdrahtet, damit kein aktiver Lauf ASVREC-/ASVFEH-Feldbefunde liefert.

+ * + *

Ab M3 durch {@link de.gecheckt.asv.application.field.DefaultFieldValidator} + * ersetzen, sobald die Regelklassifikationen (V1-V/T/N/K) aus + * {@code docs/specs/fachliche-anforderungen.md} abgestimmt sind.

+ * + * @see de.gecheckt.asv.application.field.DefaultFieldValidator + */ +public final class NoOpFieldValidator implements FieldValidator { + + /** + * Gibt stets ein leeres Validierungsergebnis zurück. + * + * @param inputFile die Eingabedatei (wird nicht ausgewertet) + * @return leeres {@link ValidationResult} ohne Fehler + * @throws IllegalArgumentException wenn inputFile null ist + */ + @Override + public ValidationResult validate(InputFile inputFile) { + if (inputFile == null) { + throw new IllegalArgumentException("inputFile darf nicht null sein"); + } + return new ValidationResult(List.of()); // bewusst leer in M1 + } +} diff --git a/src/main/java/de/gecheckt/asv/bootstrap/NoOpStructureValidator.java b/src/main/java/de/gecheckt/asv/bootstrap/NoOpStructureValidator.java new file mode 100644 index 0000000..f3251b7 --- /dev/null +++ b/src/main/java/de/gecheckt/asv/bootstrap/NoOpStructureValidator.java @@ -0,0 +1,37 @@ +package de.gecheckt.asv.bootstrap; + +import java.util.List; + +import de.gecheckt.asv.application.model.ValidationResult; +import de.gecheckt.asv.application.structure.StructureValidator; +import de.gecheckt.asv.domain.model.InputFile; + +/** + * M1-Platzhalter für den Strukturvalidator. + * + *

Erzeugt keinerlei Befunde — der Validator ist in M1 bewusst ohne fachliche Prüflogik + * verdrahtet, damit kein aktiver Lauf ASVREC-/ASVFEH-Strukturbefunde liefert.

+ * + *

Ab M3 durch {@link de.gecheckt.asv.application.structure.DefaultStructureValidator} + * ersetzen, sobald die Regelklassifikationen (V1-V/T/N/K) aus + * {@code docs/specs/fachliche-anforderungen.md} abgestimmt sind.

+ * + * @see de.gecheckt.asv.application.structure.DefaultStructureValidator + */ +public final class NoOpStructureValidator implements StructureValidator { + + /** + * Gibt stets ein leeres Validierungsergebnis zurück. + * + * @param inputFile die Eingabedatei (wird nicht ausgewertet) + * @return leeres {@link ValidationResult} ohne Fehler + * @throws IllegalArgumentException wenn inputFile null ist + */ + @Override + public ValidationResult validate(InputFile inputFile) { + if (inputFile == null) { + throw new IllegalArgumentException("inputFile darf nicht null sein"); + } + return new ValidationResult(List.of()); // bewusst leer in M1 + } +} diff --git a/src/main/java/de/gecheckt/asv/domain/finding/Finding.java b/src/main/java/de/gecheckt/asv/domain/finding/Finding.java new file mode 100644 index 0000000..c7f3fb0 --- /dev/null +++ b/src/main/java/de/gecheckt/asv/domain/finding/Finding.java @@ -0,0 +1,173 @@ +package de.gecheckt.asv.domain.finding; + +import java.util.Objects; + +/** + * Einzelbefund eines Validierungslaufs. + * + *

Ein Befund trägt alle Meta-Informationen, die für Berichtserzeugung und spätere + * GUI-Darstellung benötigt werden. Nullable-Felder sind explizit so gekennzeichnet — + * sie sind optional, weil nicht jeder Befund auf ein konkretes Segment oder Feld + * zurückführbar ist.

+ * + *

Unveränderlich (Record). Alle nicht-nullable Felder werden im Konstruktor + * auf {@code null} geprüft.

+ * + * @param kind Befundart: {@link FindingKind#SPEC} oder {@link FindingKind#DIAGNOSTIC} + * @param severity Schweregrad: ERROR, WARNING oder HINT + * @param layer Schicht, auf die sich der Befund bezieht + * @param ruleId interne Regel-ID; kann {@code null} sein + * @param officialErrorCode offizieller Spec-Fehlercode gemäß Spezifikation Abschnitt 7; + * kann {@code null} sein, wenn kein direkter Fehlercode zugeordnet ist + * @param segmentType Segmentbezeichnung, z.B. {@code "UNB"}; kann {@code null} sein + * @param segmentIndex Null-basierter Index des Segments in der Datei; kann {@code null} sein + * @param fieldId Feld-ID, z.B. {@code "UNB_0020"}; kann {@code null} sein + * @param rawValue Rohwert des betroffenen Felds oder Segments; kann {@code null} sein + * @param position Byte- oder Zeichenposition in der Eingabedatei; kann {@code null} sein + * @param messageReference UNH 0062-Referenz bei Nachrichtenbezug; kann {@code null} sein + * @param germanMessage deutschsprachiger Befundtext; darf nicht {@code null} sein + */ +public record Finding( + FindingKind kind, + Severity severity, + FindingLayer layer, + String ruleId, + String officialErrorCode, + String segmentType, + Integer segmentIndex, + String fieldId, + String rawValue, + Integer position, + String messageReference, + String germanMessage +) { + + /** + * Kompaktkonstruktor mit Null-Prüfung für alle Pflichtfelder. + */ + public Finding { + Objects.requireNonNull(kind, "kind darf nicht null sein"); + Objects.requireNonNull(severity, "severity darf nicht null sein"); + Objects.requireNonNull(layer, "layer darf nicht null sein"); + Objects.requireNonNull(germanMessage, "germanMessage darf nicht null sein"); + } + + // --------------------------------------------------------------------------- + // Hilfsmethoden + // --------------------------------------------------------------------------- + + /** + * Gibt zurück, ob es sich um einen SPEC-ERROR-Befund handelt. + * Nur solche Befunde beeinflussen das Gesamturteil. + * + * @return {@code true} genau dann, wenn {@code kind == SPEC && severity == ERROR} + */ + public boolean isSpecError() { + return kind == FindingKind.SPEC && severity == Severity.ERROR; + } + + // --------------------------------------------------------------------------- + // Builder + // --------------------------------------------------------------------------- + + /** + * Erzeugt einen neuen Builder für {@link Finding}. + * + * @param kind Befundart (Pflichtfeld) + * @param severity Schweregrad (Pflichtfeld) + * @param layer Schicht (Pflichtfeld) + * @param germanMessage Befundtext auf Deutsch (Pflichtfeld) + * @return neuer Builder + */ + public static Builder builder(FindingKind kind, Severity severity, FindingLayer layer, + String germanMessage) { + return new Builder(kind, severity, layer, germanMessage); + } + + /** + * Builder für {@link Finding}. Alle optionalen Felder werden mit {@code null} initialisiert. + */ + public static final class Builder { + + private final FindingKind kind; + private final Severity severity; + private final FindingLayer layer; + private final String germanMessage; + + private String ruleId; + private String officialErrorCode; + private String segmentType; + private Integer segmentIndex; + private String fieldId; + private String rawValue; + private Integer position; + private String messageReference; + + private Builder(FindingKind kind, Severity severity, FindingLayer layer, + String germanMessage) { + this.kind = kind; + this.severity = severity; + this.layer = layer; + this.germanMessage = germanMessage; + } + + /** Setzt die interne Regel-ID. */ + public Builder ruleId(String ruleId) { + this.ruleId = ruleId; + return this; + } + + /** Setzt den offiziellen Spec-Fehlercode. */ + public Builder officialErrorCode(String officialErrorCode) { + this.officialErrorCode = officialErrorCode; + return this; + } + + /** Setzt den Segmenttyp (z.B. {@code "UNB"}). */ + public Builder segmentType(String segmentType) { + this.segmentType = segmentType; + return this; + } + + /** Setzt den Segmentindex. */ + public Builder segmentIndex(Integer segmentIndex) { + this.segmentIndex = segmentIndex; + return this; + } + + /** Setzt die Feld-ID (z.B. {@code "UNB_0020"}). */ + public Builder fieldId(String fieldId) { + this.fieldId = fieldId; + return this; + } + + /** Setzt den Rohwert. */ + public Builder rawValue(String rawValue) { + this.rawValue = rawValue; + return this; + } + + /** Setzt die Position (Byte-/Zeichenposition). */ + public Builder position(Integer position) { + this.position = position; + return this; + } + + /** Setzt die Nachrichtenreferenz (UNH 0062). */ + public Builder messageReference(String messageReference) { + this.messageReference = messageReference; + return this; + } + + /** + * Baut das {@link Finding}-Objekt. + * + * @return neuer, unveränderlicher {@link Finding}-Befund + */ + public Finding build() { + return new Finding(kind, severity, layer, ruleId, officialErrorCode, + segmentType, segmentIndex, fieldId, rawValue, position, + messageReference, germanMessage); + } + } +} diff --git a/src/main/java/de/gecheckt/asv/domain/finding/FindingKind.java b/src/main/java/de/gecheckt/asv/domain/finding/FindingKind.java new file mode 100644 index 0000000..1bf92a1 --- /dev/null +++ b/src/main/java/de/gecheckt/asv/domain/finding/FindingKind.java @@ -0,0 +1,25 @@ +package de.gecheckt.asv.domain.finding; + +/** + * Art eines Befunds: Spec-Urteil oder diagnostische Weiteranalyse. + * + *

Die Unterscheidung ist architektonisch zentral: Nur {@link #SPEC}-Befunde dürfen das + * Gesamturteil ({@link Verdict}) beeinflussen. {@link #DIAGNOSTIC}-Befunde liefern zusätzliche + * technische Informationen, ohne das Spec-Urteil zu verändern — auch nicht bei + * {@link Severity#ERROR}.

+ */ +public enum FindingKind { + + /** + * Befund ist Teil des normativen Spec-Urteils. + * Ein {@link Severity#ERROR}-Befund dieser Art setzt das Urteil auf {@link Verdict#INVALID}. + */ + SPEC, + + /** + * Diagnostischer Befund zur Weiteranalyse. + * Dieser Befund beeinflusst das Spec-Urteil niemals, auch nicht bei + * {@link Severity#ERROR}. + */ + DIAGNOSTIC +} diff --git a/src/main/java/de/gecheckt/asv/domain/finding/FindingLayer.java b/src/main/java/de/gecheckt/asv/domain/finding/FindingLayer.java new file mode 100644 index 0000000..f0a7eea --- /dev/null +++ b/src/main/java/de/gecheckt/asv/domain/finding/FindingLayer.java @@ -0,0 +1,28 @@ +package de.gecheckt.asv.domain.finding; + +/** + * Schicht, auf die sich ein Befund bezieht. + * + *

Die Schichttrennung stellt sicher, dass technische Befunde nicht mit fachlichen Befunden + * vermischt werden und eine spätere GUI differenziert auf dieselben Daten zugreifen kann.

+ */ +public enum FindingLayer { + + /** + * Äußeres Artefakt — Datei auf Dateisystemebene (Dateiname, Dateityp, + * PKCS#7-/Auftragsdatei-/Nutzdatei-Schicht). + */ + ARTIFACT, + + /** + * Technische Struktur — Service-Segmente (UNA, UNB, UNH, UNT, UNZ), + * KKS-Auftragssatz, Datei-/Transportebene, Nachrichtenhüllen. + */ + TECHNICAL_STRUCTURE, + + /** + * Kanonisches Fachmodell — fachlich-technische Repräsentation der ASV-Nachrichten + * (ASVREC, ASVFEH und Storno-Ausprägungen). + */ + DOMAIN_MODEL +} diff --git a/src/main/java/de/gecheckt/asv/domain/finding/Severity.java b/src/main/java/de/gecheckt/asv/domain/finding/Severity.java new file mode 100644 index 0000000..99f9bee --- /dev/null +++ b/src/main/java/de/gecheckt/asv/domain/finding/Severity.java @@ -0,0 +1,20 @@ +package de.gecheckt.asv.domain.finding; + +/** + * Schweregrad eines Befunds. + * + *

Nur {@link #ERROR}-Befunde mit {@link FindingKind#SPEC} beeinflussen das Prüfurteil + * ({@link ValidationReport#computeVerdict()}). Warnungen und Hinweise verändern den + * Gültigkeitsstatus nicht.

+ */ +public enum Severity { + + /** Fehler — bei Spec-Befunden führt dies zu {@link Verdict#INVALID}. */ + ERROR, + + /** Warnung — beeinflusst das Spec-Urteil nicht. */ + WARNING, + + /** Hinweis — informativ, kein Einfluss auf das Spec-Urteil. */ + HINT +} diff --git a/src/main/java/de/gecheckt/asv/domain/finding/ValidationReport.java b/src/main/java/de/gecheckt/asv/domain/finding/ValidationReport.java new file mode 100644 index 0000000..87a260d --- /dev/null +++ b/src/main/java/de/gecheckt/asv/domain/finding/ValidationReport.java @@ -0,0 +1,188 @@ +package de.gecheckt.asv.domain.finding; + +import java.time.Instant; +import java.util.List; +import java.util.Objects; + +/** + * Gesamtergebnis eines Validierungslaufs. + * + *

Ein {@code ValidationReport} fasst alle {@link Finding}-Befunde zusammen und berechnet + * daraus das Gesamturteil ({@link Verdict}). Die zentrale Invariante lautet:

+ *
+ * {@link #computeVerdict()} berücksichtigt ausschließlich Befunde mit + * {@link FindingKind#SPEC} und {@link Severity#ERROR}. + * Ein {@link FindingKind#DIAGNOSTIC}-Befund mit {@link Severity#ERROR} setzt das Urteil + * niemals auf {@link Verdict#INVALID}. + *
+ * + *

Instanzen sind unveränderlich. Die Befundliste kann nach Erzeugung nicht mehr verändert + * werden.

+ */ +public final class ValidationReport { + + /** Name der validierten Eingabedatei. */ + private final String fileName; + + /** Zeitpunkt der Berichterstellung (UTC). */ + private final Instant timestamp; + + /** + * Unveränderliche Liste aller Befunde. Die Liste wird intern über + * {@link List#copyOf(java.util.Collection)} gesichert, sodass externe Referenzen + * sie nicht verändern können. + */ + private final List findings; + + /** + * Gibt an, ob es sich um einen Bedienfehler-Bericht handelt. In diesem Fall ist + * {@link #computeVerdict()} immer {@link Verdict#OPERATIONAL_ERROR}. + */ + private final boolean operationalError; + + // --------------------------------------------------------------------------- + // Konstruktoren + // --------------------------------------------------------------------------- + + /** + * Erzeugt einen normalen Validierungsbericht. + * + * @param fileName Dateiname der validierten Eingabedatei (nicht null) + * @param timestamp Zeitstempel der Berichterstellung (nicht null) + * @param findings Liste der Befunde (nicht null, darf leer sein) + */ + public ValidationReport(String fileName, Instant timestamp, List findings) { + this.fileName = Objects.requireNonNull(fileName, "fileName darf nicht null sein"); + this.timestamp = Objects.requireNonNull(timestamp, "timestamp darf nicht null sein"); + Objects.requireNonNull(findings, "findings darf nicht null sein"); + this.findings = List.copyOf(findings); + this.operationalError = false; + } + + /** + * Privater Konstruktor für den Bedienfehler-Fall. + */ + private ValidationReport(String fileName, Instant timestamp, List findings, + boolean operationalError) { + this.fileName = Objects.requireNonNull(fileName, "fileName darf nicht null sein"); + this.timestamp = Objects.requireNonNull(timestamp, "timestamp darf nicht null sein"); + Objects.requireNonNull(findings, "findings darf nicht null sein"); + this.findings = List.copyOf(findings); + this.operationalError = operationalError; + } + + // --------------------------------------------------------------------------- + // Factory-Methoden + // --------------------------------------------------------------------------- + + /** + * Erzeugt einen Bedienfehler-Bericht. Das Urteil ist immer {@link Verdict#OPERATIONAL_ERROR}. + * + *

Typische Anwendungsfälle: fehlendes Pflichtargument, nicht lesbare Eingabedatei.

+ * + * @param fileName Dateiname oder Platzhalter (nicht null) + * @param ruleId interne Regel-ID des auslösenden Prüfschritts (kann null sein) + * @param message deutschsprachige Fehlerbeschreibung (nicht null) + * @return neuer {@code ValidationReport} mit {@link Verdict#OPERATIONAL_ERROR} + */ + public static ValidationReport operationalError(String fileName, String ruleId, + String message) { + Objects.requireNonNull(fileName, "fileName darf nicht null sein"); + Objects.requireNonNull(message, "message darf nicht null sein"); + + Finding errorFinding = Finding.builder( + FindingKind.SPEC, Severity.ERROR, FindingLayer.ARTIFACT, message) + .ruleId(ruleId) + .build(); + + return new ValidationReport(fileName, Instant.now(), List.of(errorFinding), true); + } + + // --------------------------------------------------------------------------- + // Kern-Methoden + // --------------------------------------------------------------------------- + + /** + * Berechnet das Gesamturteil des Validierungslaufs. + * + *

Invariante: Nur Befunde mit {@link FindingKind#SPEC} und + * {@link Severity#ERROR} führen zu {@link Verdict#INVALID}. Diagnostische Befunde — + * auch solche mit {@link Severity#ERROR} — beeinflussen das Urteil niemals.

+ * + * @return {@link Verdict#OPERATIONAL_ERROR} bei Bedienfehler-Bericht, + * {@link Verdict#INVALID} bei mindestens einem SPEC-ERROR-Befund, + * {@link Verdict#VALID} sonst + */ + public Verdict computeVerdict() { + if (operationalError) { + return Verdict.OPERATIONAL_ERROR; + } + return hasSpecErrors() ? Verdict.INVALID : Verdict.VALID; + } + + /** + * Gibt zurück, ob mindestens ein SPEC-ERROR-Befund vorhanden ist. + * + * @return {@code true} genau dann, wenn {@code findings} mindestens einen Befund mit + * {@code kind == SPEC && severity == ERROR} enthält + */ + public boolean hasSpecErrors() { + return findings.stream().anyMatch(Finding::isSpecError); + } + + /** + * Gibt alle Befunde mit {@link FindingKind#SPEC} zurück. + * + * @return unveränderliche Liste aller Spec-Befunde (niemals null) + */ + public List specFindings() { + return findings.stream() + .filter(f -> f.kind() == FindingKind.SPEC) + .toList(); + } + + /** + * Gibt alle Befunde mit {@link FindingKind#DIAGNOSTIC} zurück. + * + * @return unveränderliche Liste aller Diagnose-Befunde (niemals null) + */ + public List diagnosticFindings() { + return findings.stream() + .filter(f -> f.kind() == FindingKind.DIAGNOSTIC) + .toList(); + } + + // --------------------------------------------------------------------------- + // Getter + // --------------------------------------------------------------------------- + + /** + * Gibt den Dateinamen der validierten Eingabedatei zurück. + * + * @return Dateiname (nicht null) + */ + public String getFileName() { + return fileName; + } + + /** + * Gibt den Zeitstempel der Berichterstellung zurück. + * + * @return Zeitstempel (nicht null, UTC) + */ + public Instant getTimestamp() { + return timestamp; + } + + /** + * Gibt die unveränderliche Liste aller Befunde zurück. + * + *

Die zurückgegebene Liste kann nicht verändert werden — Versuche werfen + * {@link UnsupportedOperationException}.

+ * + * @return unveränderliche Befundliste (nicht null) + */ + public List getFindings() { + return findings; + } +} diff --git a/src/main/java/de/gecheckt/asv/domain/finding/Verdict.java b/src/main/java/de/gecheckt/asv/domain/finding/Verdict.java new file mode 100644 index 0000000..10e5675 --- /dev/null +++ b/src/main/java/de/gecheckt/asv/domain/finding/Verdict.java @@ -0,0 +1,30 @@ +package de.gecheckt.asv.domain.finding; + +/** + * Gesamturteil eines Validierungslaufs. + * + *

Das Urteil wird durch {@link ValidationReport#computeVerdict()} berechnet und basiert + * ausschließlich auf {@link FindingKind#SPEC}-Befunden mit {@link Severity#ERROR}. + * Diagnostische Befunde beeinflussen das Urteil niemals.

+ * + *

Entsprechung zu Exit-Codes gemäß Architekturvorgabe:

+ *
    + *
  • {@link #VALID} → Exit-Code 0
  • + *
  • {@link #INVALID} → Exit-Code 1
  • + *
  • {@link #OPERATIONAL_ERROR} → Exit-Code 2
  • + *
+ */ +public enum Verdict { + + /** Gültig — keine SPEC-ERROR-Befunde vorhanden. Exit-Code 0. */ + VALID, + + /** Ungültig — mindestens ein SPEC-ERROR-Befund vorhanden. Exit-Code 1. */ + INVALID, + + /** + * Bedienfehler — z.B. fehlendes Argument oder nicht lesbare Eingabedatei. + * Exit-Code 2. Wird über {@link ValidationReport#operationalError} erzeugt. + */ + OPERATIONAL_ERROR +} diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index 9712838..02c2aed 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -1,10 +1,18 @@ + - + + diff --git a/src/test/java/de/gecheckt/asv/ArchitectureTest.java b/src/test/java/de/gecheckt/asv/ArchitectureTest.java new file mode 100644 index 0000000..59b596f --- /dev/null +++ b/src/test/java/de/gecheckt/asv/ArchitectureTest.java @@ -0,0 +1,93 @@ +package de.gecheckt.asv; + +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; + +/** + * Automatisierte Architekturtests für den ASV-Format-Validator. + * + *

Sichert die in M1 etablierten Strukturregeln der hexagonalen Architektur dauerhaft ab. + * Jede Verletzung führt zu einem fehlschlagenden Build.

+ * + *

Regeln:

+ *
    + *
  • A – Log4j2-Typen dürfen nur im Logging-Adapter und im Bootstrap sichtbar sein
  • + *
  • B – Domain-Klassen dürfen keine Adapter- oder Bootstrap-Abhängigkeiten haben
  • + *
  • C – Application-Klassen dürfen keine Adapter- oder Bootstrap-Abhängigkeiten haben
  • + *
  • D – Preview-Validatoren werden in M1 nicht aus aktivem Adapter- oder Bootstrap-Code referenziert
  • + *
+ */ +@AnalyzeClasses(packages = "de.gecheckt.asv", importOptions = ImportOption.DoNotIncludeTests.class) +class ArchitectureTest { + + /** + * Regel A — Log4j2-Sichtbarkeit. + * + * Log4j2-Typen ({@code org.apache.logging.log4j.*}) dürfen nur im Logging-Adapter + * ({@code adapter.out.logging}) und im Bootstrap sichtbar sein. Alle anderen Pakete + * verwenden ausschließlich die SLF4J-Fassade. + */ + @ArchTest + static final ArchRule log4j2_nur_in_logging_adapter_und_bootstrap = + noClasses() + .that().resideOutsideOfPackages( + "de.gecheckt.asv.adapter.out.logging..", + "de.gecheckt.asv.bootstrap..") + .should().dependOnClassesThat() + .resideInAPackage("org.apache.logging.log4j..") + .because("Log4j2 darf nur im Logging-Adapter und im Bootstrap sichtbar sein."); + + /** + * Regel B — Domain-Reinheit. + * + * Domain-Klassen kennen keine Adapter-Implementierungen und kein Bootstrap. + * Nur die Domain selbst sowie die SLF4J-API und Java-Standardklassen dürfen + * aus dem Domain-Paket referenziert werden. + */ + @ArchTest + static final ArchRule domain_hat_keine_adapter_abhaengigkeit = + noClasses() + .that().resideInAPackage("de.gecheckt.asv.domain..") + .should().dependOnClassesThat() + .resideInAnyPackage( + "de.gecheckt.asv.adapter..", + "de.gecheckt.asv.bootstrap.."); + + /** + * Regel C — Application-Reinheit. + * + * Application-Klassen (Services, Ports) kennen keine Adapter-Implementierungen + * und kein Bootstrap. Abhängigkeiten gehen nur in Richtung Domain. + */ + @ArchTest + static final ArchRule application_kennt_keine_adapter_implementierungen = + noClasses() + .that().resideInAPackage("de.gecheckt.asv.application..") + .should().dependOnClassesThat() + .resideInAnyPackage( + "de.gecheckt.asv.adapter..", + "de.gecheckt.asv.bootstrap.."); + + /** + * Regel D — Preview-Isolation. + * + * Die in M1 eingefrorenen Preview-Validatoren ({@code DefaultStructureValidator} und + * {@code DefaultFieldValidator}) dürfen aus aktivem Adapter- und Bootstrap-Code nicht + * direkt referenziert werden. Sie werden erst ab M3 wieder aktiv eingesetzt. + */ + @ArchTest + static final ArchRule preview_wird_nicht_aus_aktivem_code_referenziert = + noClasses() + .that().resideInAnyPackage( + "de.gecheckt.asv.adapter..", + "de.gecheckt.asv.bootstrap..") + .should().dependOnClassesThat() + .haveSimpleNameContaining("DefaultStructureValidator") + .orShould().dependOnClassesThat() + .haveSimpleNameContaining("DefaultFieldValidator") + .because("Preview-Validatoren sind in M1 eingefroren und werden erst ab M3 aktiv verwendet."); +} diff --git a/src/test/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplicationAdditionalTest.java b/src/test/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplicationAdditionalTest.java deleted file mode 100644 index 0432638..0000000 --- a/src/test/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplicationAdditionalTest.java +++ /dev/null @@ -1,219 +0,0 @@ -package de.gecheckt.asv.adapter.in.cli; - -import de.gecheckt.asv.domain.model.Field; -import de.gecheckt.asv.domain.model.InputFile; -import de.gecheckt.asv.domain.model.Message; -import de.gecheckt.asv.domain.model.Segment; -import de.gecheckt.asv.adapter.out.parsing.InputFileParseException; -import de.gecheckt.asv.adapter.out.parsing.InputFileParser; -import de.gecheckt.asv.application.InputFileValidator; -import de.gecheckt.asv.application.model.ValidationError; -import de.gecheckt.asv.application.model.ValidationResult; -import de.gecheckt.asv.application.model.ValidationSeverity; -import de.gecheckt.asv.adapter.out.reporting.ValidationResultPrinter; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.PrintStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.*; - -/** - * Zusätzliche Unittests für AsvValidatorApplication. - */ -class AsvValidatorApplicationAdditionalTest { - - @TempDir - Path tempDir; - - @Test - void testRunWithValidFileShouldReturnSuccessExitCode() throws InputFileParseException, IOException { - // Given - InputFileParser parser = mock(InputFileParser.class); - InputFileValidator validator = mock(InputFileValidator.class); - ValidationResultPrinter printer = mock(ValidationResultPrinter.class); - - AsvValidatorApplication app = new AsvValidatorApplication(parser, validator, printer); - - // Create a test file - Path testFile = tempDir.resolve("valid-file.txt"); - String validContent = "HDR|TestHeader\n" + - "DTL|TestData|MoreData\n" + - "TRL|3"; - Files.writeString(testFile, validContent); - - String[] args = {testFile.toString()}; - - // Create real objects instead of mocks for final classes - Field hdrField1 = new Field(1, "HDR"); - Field hdrField2 = new Field(2, "TestHeader"); - Segment hdrSegment = new Segment("HDR", 1, List.of(hdrField1, hdrField2)); - - Field dtlField1 = new Field(1, "DTL"); - Field dtlField2 = new Field(2, "TestData"); - Field dtlField3 = new Field(3, "MoreData"); - Segment dtlSegment = new Segment("DTL", 2, List.of(dtlField1, dtlField2, dtlField3)); - - Field trlField1 = new Field(1, "TRL"); - Field trlField2 = new Field(2, "3"); - Segment trlSegment = new Segment("TRL", 3, List.of(trlField1, trlField2)); - - Message message = new Message(1, List.of(hdrSegment, dtlSegment, trlSegment)); - InputFile inputFile = new InputFile("valid-file.txt", List.of(message)); - - // Mock the parser and validator behavior - when(parser.parse(anyString(), anyString())).thenReturn(inputFile); - - // Create a real ValidationResult with no errors - ValidationResult validationResult = new ValidationResult(List.of()); - - when(validator.validate(inputFile)).thenReturn(validationResult); - - // Capture System.out - ByteArrayOutputStream outContent = new ByteArrayOutputStream(); - PrintStream originalOut = System.out; - System.setOut(new PrintStream(outContent)); - - try { - // When - int exitCode = app.run(args); - - // Then - assertEquals(0, exitCode); - // Verify that the printer was called - verify(printer).printToConsole(validationResult); - } finally { - // Restore System.out - System.setOut(originalOut); - } - } - - @Test - void testRunWithInvalidFileShouldReturnValidationErrorsExitCode() throws InputFileParseException, IOException { - // Given - InputFileParser parser = mock(InputFileParser.class); - InputFileValidator validator = mock(InputFileValidator.class); - ValidationResultPrinter printer = mock(ValidationResultPrinter.class); - - AsvValidatorApplication app = new AsvValidatorApplication(parser, validator, printer); - - // Create an invalid test file (missing required segments) - Path testFile = tempDir.resolve("invalid-file.txt"); - String invalidContent = "DTL|TestData|MoreData\n" + // Missing HDR - "DTL|MoreData|EvenMoreData\n"; // Missing TRL - Files.writeString(testFile, invalidContent); - - String[] args = {testFile.toString()}; - - // Create real objects instead of mocks for final classes - Field dtlField1 = new Field(1, "DTL"); - Field dtlField2 = new Field(2, "TestData"); - Field dtlField3 = new Field(3, "MoreData"); - Segment dtlSegment1 = new Segment("DTL", 1, List.of(dtlField1, dtlField2, dtlField3)); - - Field dtlField4 = new Field(1, "DTL"); - Field dtlField5 = new Field(2, "MoreData"); - Field dtlField6 = new Field(3, "EvenMoreData"); - Segment dtlSegment2 = new Segment("DTL", 2, List.of(dtlField4, dtlField5, dtlField6)); - - Message message = new Message(1, List.of(dtlSegment1, dtlSegment2)); - InputFile inputFile = new InputFile("invalid-file.txt", List.of(message)); - - // Mock the parser and validator behavior - when(parser.parse(anyString(), anyString())).thenReturn(inputFile); - - // Create a real ValidationResult with errors - ValidationError error = new ValidationError( - "MISSING_SEGMENT", - "Required segment HDR is missing", - ValidationSeverity.ERROR, - "HDR", - 1, - "HDR", - 1, - null, - "HDR segment is required" - ); - ValidationResult validationResult = new ValidationResult(List.of(error)); - - when(validator.validate(inputFile)).thenReturn(validationResult); - - // Capture System.out - ByteArrayOutputStream outContent = new ByteArrayOutputStream(); - PrintStream originalOut = System.out; - System.setOut(new PrintStream(outContent)); - - try { - // When - int exitCode = app.run(args); - - // Then - assertEquals(3, exitCode); // Validation errors exit code - // Verify that the printer was called - verify(printer).printToConsole(validationResult); - } finally { - // Restore System.out - System.setOut(originalOut); - } - } - - /** - * Spezialisierter Test für den Fall, dass ein technisch lesbares/parstabares Dokument - * Validierungsfehler enthält und der CLI Exit-Code 3 zurückgibt. - * - * Dieser Test konzentriert sich explizit auf: - * 1. Parser liefert ein InputFile - * 2. Validator liefert ein ValidationResult mit mindestens einem ERROR - * 3. CLI gibt daraufhin Exit-Code 3 zurück - */ - @Test - void testParserReturnsInputFileAndValidatorReturnsErrorsShouldReturnExitCodeThree() throws InputFileParseException, IOException { - // Given - InputFileParser parser = mock(InputFileParser.class); - InputFileValidator validator = mock(InputFileValidator.class); - ValidationResultPrinter printer = mock(ValidationResultPrinter.class); - - AsvValidatorApplication app = new AsvValidatorApplication(parser, validator, printer); - - // Create a dummy test file (content doesn't matter since we're mocking the parser) - Path testFile = tempDir.resolve("dummy-file.txt"); - Files.writeString(testFile, "dummy content"); - - String[] args = {testFile.toString()}; - - // Mock: Parser liefert ein gültiges InputFile - InputFile inputFile = mock(InputFile.class); - when(parser.parse(anyString(), anyString())).thenReturn(inputFile); - - // Mock: Validator liefert ein ValidationResult mit mindestens einem ERROR - ValidationError validationError = new ValidationError( - "TEST_ERROR_CODE", - "Test error message", - ValidationSeverity.ERROR, // Wichtig: ValidationSeverity.ERROR - "TEST_SEGMENT", - 1, - "TEST_FIELD", - 1, - null, - "Test error description" - ); - ValidationResult validationResultWithErrors = new ValidationResult(List.of(validationError)); - when(validator.validate(inputFile)).thenReturn(validationResultWithErrors); - - // When - int exitCode = app.run(args); - - // Then - // Prüfe explizit, dass der Exit-Code 3 ist - assertEquals(3, exitCode, "CLI should return exit code 3 when validation errors occur"); - - // Verify that the printer was called with the validation result - verify(printer).printToConsole(validationResultWithErrors); - } -} \ No newline at end of file diff --git a/src/test/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplicationTest.java b/src/test/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplicationTest.java deleted file mode 100644 index ac2b724..0000000 --- a/src/test/java/de/gecheckt/asv/adapter/in/cli/AsvValidatorApplicationTest.java +++ /dev/null @@ -1,104 +0,0 @@ -package de.gecheckt.asv.adapter.in.cli; - -import de.gecheckt.asv.adapter.out.parsing.InputFileParser; -import de.gecheckt.asv.application.InputFileValidator; -import de.gecheckt.asv.adapter.out.reporting.ValidationResultPrinter; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; -import java.nio.file.Path; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.*; - -/** - * Unittests für AsvValidatorApplication. - */ -class AsvValidatorApplicationTest { - - @TempDir - Path tempDir; - - @Test - void testRunWithNoArgumentsShouldPrintUsageAndReturnInvalidArgumentsExitCode() { - // Given - InputFileParser parser = mock(InputFileParser.class); - InputFileValidator validator = mock(InputFileValidator.class); - ValidationResultPrinter printer = mock(ValidationResultPrinter.class); - AsvValidatorApplication app = new AsvValidatorApplication(parser, validator, printer); - String[] args = {}; - - // Capture System.out - ByteArrayOutputStream outContent = new ByteArrayOutputStream(); - PrintStream originalOut = System.out; - System.setOut(new PrintStream(outContent)); - - try { - // When - int exitCode = app.run(args); - - // Then - assertEquals(1, exitCode); - assertEquals(true, outContent.toString().contains("Verwendung:"), "Output should contain usage information"); - } finally { - // Restore System.out - System.setOut(originalOut); - } - } - - @Test - void testRunWithTooManyArgumentsShouldPrintUsageAndReturnInvalidArgumentsExitCode() { - // Given - InputFileParser parser = mock(InputFileParser.class); - InputFileValidator validator = mock(InputFileValidator.class); - ValidationResultPrinter printer = mock(ValidationResultPrinter.class); - AsvValidatorApplication app = new AsvValidatorApplication(parser, validator, printer); - String[] args = {"file1.txt", "file2.txt"}; - - // Capture System.out - ByteArrayOutputStream outContent = new ByteArrayOutputStream(); - PrintStream originalOut = System.out; - System.setOut(new PrintStream(outContent)); - - try { - // When - int exitCode = app.run(args); - - // Then - assertEquals(1, exitCode); - assertEquals(true, outContent.toString().contains("Verwendung:"), "Output should contain usage information"); - } finally { - // Restore System.out - System.setOut(originalOut); - } - } - - @Test - void testRunWithNonExistentFileShouldReturnFileErrorExitCode() { - // Given - InputFileParser parser = mock(InputFileParser.class); - InputFileValidator validator = mock(InputFileValidator.class); - ValidationResultPrinter printer = mock(ValidationResultPrinter.class); - AsvValidatorApplication app = new AsvValidatorApplication(parser, validator, printer); - String[] args = {"/non/existent/file.txt"}; - - // Capture System.err - ByteArrayOutputStream errContent = new ByteArrayOutputStream(); - PrintStream originalErr = System.err; - System.setErr(new PrintStream(errContent)); - - try { - // When - int exitCode = app.run(args); - - // Then - assertEquals(2, exitCode); - assertEquals(true, errContent.toString().contains("File does not exist"), "Error output should contain file not found message"); - } finally { - // Restore System.err - System.setErr(originalErr); - } - } -} \ No newline at end of file diff --git a/src/test/java/de/gecheckt/asv/adapter/in/cli/CliRunnerOperationalErrorTest.java b/src/test/java/de/gecheckt/asv/adapter/in/cli/CliRunnerOperationalErrorTest.java new file mode 100644 index 0000000..c1eea62 --- /dev/null +++ b/src/test/java/de/gecheckt/asv/adapter/in/cli/CliRunnerOperationalErrorTest.java @@ -0,0 +1,403 @@ +package de.gecheckt.asv.adapter.in.cli; + +import de.gecheckt.asv.adapter.out.filesystem.SuffixResolver; +import de.gecheckt.asv.adapter.out.logging.LoggingConfigurator; +import de.gecheckt.asv.adapter.out.reporting.ReportFileWriter; +import de.gecheckt.asv.application.FileValidationService; +import de.gecheckt.asv.domain.finding.Finding; +import de.gecheckt.asv.domain.finding.ValidationReport; +import de.gecheckt.asv.domain.finding.Verdict; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Unit-Tests für die Bedienfehler-Behandlung (Exit-Code 2, Minimalbericht) in {@link CliRunner}. + * + *

Abgedeckte Abnahmekriterien aus AP08:

+ *
    + *
  • Fall 1: Kein Argument → Exit 2, nur Konsole (STDERR), kein Verzeichnis
  • + *
  • Fall 2: Mehr als ein Argument → Exit 2, nur Konsole
  • + *
  • Fall 3: Datei existiert nicht → Exit 2, Berichtdatei im übergeordneten Verzeichnis
  • + *
  • Fall 4: Pfad ist kein regulärer Dateityp → Exit 2, nur Konsole
  • + *
  • Fall 5: Datei nicht lesbar → Exit 2, Berichtdatei im übergeordneten Verzeichnis
  • + *
  • Verdict OPERATIONAL_ERROR wird korrekt gesetzt
  • + *
  • Kein Stack-Trace in STDERR
  • + *
+ * + *

Hinweis: Der {@link LoggingConfigurator} wird in allen Tests als No-Op-Mock eingesetzt, + * um Windows-seitiges Dateisperr-Verhalten durch geöffnete Log4j2-Appender zu vermeiden.

+ */ +class CliRunnerOperationalErrorTest { + + @TempDir + Path tempDir; + + private PrintStream originalStderr; + private ByteArrayOutputStream stderrBuf; + private PrintStream originalStdout; + private ByteArrayOutputStream stdoutBuf; + + @BeforeEach + void captureStreams() { + originalStderr = System.err; + stderrBuf = new ByteArrayOutputStream(); + System.setErr(new PrintStream(stderrBuf, true, StandardCharsets.UTF_8)); + + originalStdout = System.out; + stdoutBuf = new ByteArrayOutputStream(); + System.setOut(new PrintStream(stdoutBuf, true, StandardCharsets.UTF_8)); + } + + @AfterEach + void restoreStreams() { + System.setErr(originalStderr); + System.setOut(originalStdout); + } + + /** Gibt den bisher auf STDERR geschriebenen Text zurück. */ + private String stderr() { + return stderrBuf.toString(StandardCharsets.UTF_8); + } + + /** Gibt den bisher auf STDOUT geschriebenen Text zurück. */ + private String stdout() { + return stdoutBuf.toString(StandardCharsets.UTF_8); + } + + /** + * Erzeugt einen {@link CliRunner} mit echten Adaptern und No-Op-LoggingConfigurator. + * + * @param service der zu verwendende {@link FileValidationService} + */ + private CliRunner runnerWith(FileValidationService service) { + LoggingConfigurator noOpLogging = mock(LoggingConfigurator.class); + doNothing().when(noOpLogging).configureLogFile(any(Path.class)); + SuffixResolver sr = new SuffixResolver(); + return new CliRunner(service, noOpLogging, sr, new ReportFileWriter(sr)); + } + + // ----------------------------------------------------------------------- + // Fall 1: Kein Argument + // ----------------------------------------------------------------------- + + @Test + @DisplayName("Fall 1: Kein Argument → Exit 2, ruleId OPERATIONAL-MISSING-ARG") + void fall1_keinArgument_exitCode2_undRuleId() { + FileValidationService service = mock(FileValidationService.class); + CliRunner runner = runnerWith(service); + + int exitCode = runner.run(new String[]{}); + + assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode, + "Kein Argument muss Exit-Code 2 liefern"); + verifyNoInteractions(service); + } + + @Test + @DisplayName("Fall 1: Kein Argument → STDERR enthält Fehlermeldung, keine Berichtdatei") + void fall1_keinArgument_nurKonsole() { + FileValidationService service = mock(FileValidationService.class); + CliRunner runner = runnerWith(service); + + runner.run(new String[]{}); + + String err = stderr(); + assertFalse(err.isBlank(), "STDERR muss eine Fehlermeldung enthalten"); + // STDOUT (normale Berichtdatei) bleibt leer — nur STDERR + assertTrue(stdout().isBlank(), "STDOUT darf bei kein-Argument-Fehler keine Ausgabe enthalten"); + // Keine Berichtdatei im TempDir + long txtFiles = countTxtFiles(tempDir); + assertEquals(0, txtFiles, "Bei 'kein Argument' darf keine Berichtdatei entstehen"); + } + + @Test + @DisplayName("Fall 1: Kein Argument → Minimalbericht im STDERR enthält BEDIENFEHLER") + void fall1_keinArgument_minimalberichtEnthaeltBedienfehler() { + FileValidationService service = mock(FileValidationService.class); + CliRunner runner = runnerWith(service); + + runner.run(new String[]{}); + + assertTrue(stderr().contains("BEDIENFEHLER"), + "Der Minimalbericht muss das Urteil BEDIENFEHLER enthalten"); + } + + // ----------------------------------------------------------------------- + // Fall 2: Mehr als ein Argument + // ----------------------------------------------------------------------- + + @Test + @DisplayName("Fall 2: Mehr als ein Argument → Exit 2, ruleId OPERATIONAL-TOO-MANY-ARGS") + void fall2_zuVieleArgumente_exitCode2() { + FileValidationService service = mock(FileValidationService.class); + CliRunner runner = runnerWith(service); + + int exitCode = runner.run(new String[]{"datei1.auf", "datei2.auf"}); + + assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode, + "Zu viele Argumente müssen Exit-Code 2 liefern"); + verifyNoInteractions(service); + } + + @Test + @DisplayName("Fall 2: Mehr als ein Argument → nur STDERR, keine Datei") + void fall2_zuVieleArgumente_nurKonsole() { + FileValidationService service = mock(FileValidationService.class); + CliRunner runner = runnerWith(service); + + runner.run(new String[]{"datei1.auf", "datei2.auf", "datei3.auf"}); + + assertFalse(stderr().isBlank(), "STDERR muss Fehlermeldung enthalten"); + assertTrue(stdout().isBlank(), "STDOUT darf keine Ausgabe enthalten"); + assertEquals(0, countTxtFiles(tempDir), "Keine Berichtdatei bei zu vielen Argumenten"); + } + + // ----------------------------------------------------------------------- + // Fall 3: Datei existiert nicht + // ----------------------------------------------------------------------- + + @Test + @DisplayName("Fall 3: Datei existiert nicht → Exit 2, ruleId OPERATIONAL-FILE-NOT-FOUND") + void fall3_dateiExistiertNicht_exitCode2() { + Path nichtVorhanden = tempDir.resolve("nicht-vorhanden.auf"); + FileValidationService service = mock(FileValidationService.class); + CliRunner runner = runnerWith(service); + + int exitCode = runner.run(new String[]{nichtVorhanden.toString()}); + + assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode, + "Nicht vorhandene Datei muss Exit-Code 2 liefern"); + verifyNoInteractions(service); + } + + @Test + @DisplayName("Fall 3: Datei existiert nicht → Berichtdatei im übergeordneten Verzeichnis") + void fall3_dateiExistiertNicht_berichtdateiImUebergeordnetenVerzeichnis() { + Path nichtVorhanden = tempDir.resolve("nicht-vorhanden.auf"); + FileValidationService service = mock(FileValidationService.class); + CliRunner runner = runnerWith(service); + + runner.run(new String[]{nichtVorhanden.toString()}); + + // Im tempDir (übergeordnetes Verzeichnis der nicht-vorhandenen Datei) soll eine .txt entstehen + Path erwarteterBericht = tempDir.resolve("nicht-vorhanden.auf.txt"); + assertTrue(Files.exists(erwarteterBericht), + "Berichtdatei soll im übergeordneten Verzeichnis liegen: " + erwarteterBericht); + } + + @Test + @DisplayName("Fall 3: Datei existiert nicht → Berichtdatei enthält BEDIENFEHLER-Urteil") + void fall3_dateiExistiertNicht_berichtdateiEnthaeltOpertionalError() throws IOException { + Path nichtVorhanden = tempDir.resolve("fehlt.auf"); + FileValidationService service = mock(FileValidationService.class); + CliRunner runner = runnerWith(service); + + runner.run(new String[]{nichtVorhanden.toString()}); + + Path bericht = tempDir.resolve("fehlt.auf.txt"); + assertTrue(Files.exists(bericht), "Berichtdatei muss existieren"); + String inhalt = Files.readString(bericht, StandardCharsets.UTF_8); + assertTrue(inhalt.contains("BEDIENFEHLER"), + "Bericht muss BEDIENFEHLER-Urteil enthalten"); + assertTrue(inhalt.contains("OPERATIONAL-FILE-NOT-FOUND"), + "Bericht muss ruleId OPERATIONAL-FILE-NOT-FOUND enthalten"); + } + + // ----------------------------------------------------------------------- + // Fall 4: Pfad ist kein regulärer Dateityp (Verzeichnis) + // ----------------------------------------------------------------------- + + @Test + @DisplayName("Fall 4: Pfad ist ein Verzeichnis → Exit 2, ruleId OPERATIONAL-NOT-REGULAR") + void fall4_pfadIstVerzeichnis_exitCode2() { + FileValidationService service = mock(FileValidationService.class); + CliRunner runner = runnerWith(service); + + // tempDir selbst ist ein Verzeichnis + int exitCode = runner.run(new String[]{tempDir.toString()}); + + assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode, + "Verzeichnis als Eingabe muss Exit-Code 2 liefern"); + verifyNoInteractions(service); + } + + @Test + @DisplayName("Fall 4: Pfad ist ein Verzeichnis → nur STDERR, keine Berichtdatei") + void fall4_pfadIstVerzeichnis_nurKonsole() { + FileValidationService service = mock(FileValidationService.class); + CliRunner runner = runnerWith(service); + + runner.run(new String[]{tempDir.toString()}); + + assertFalse(stderr().isBlank(), "STDERR muss Fehlermeldung enthalten"); + assertTrue(stdout().isBlank(), "STDOUT darf keine Ausgabe enthalten"); + } + + // ----------------------------------------------------------------------- + // Fall 5: Datei nicht lesbar — nur auf Nicht-Windows testbar + // Hinweis: Auf Windows gibt es keine zuverlässige Möglichkeit, eine Datei + // per setReadable(false) für den eigenen Prozess unlesbar zu machen. + // Dieser Test wird daher nur auf Unix-ähnlichen Systemen ausgeführt. + // ----------------------------------------------------------------------- + + @Test + @DisplayName("Fall 5: Datei nicht lesbar → Exit 2, ruleId OPERATIONAL-NOT-READABLE") + void fall5_dateiNichtLesbar_exitCode2() throws IOException { + // Nur auf Unix-ähnlichen Systemen ausführen (Windows ignoriert setReadable) + org.junit.jupiter.api.Assumptions.assumeTrue( + !System.getProperty("os.name", "").toLowerCase().contains("windows"), + "Test wird auf Windows übersprungen (setReadable nicht zuverlässig)"); + + Path nichtLesbar = tempDir.resolve("gesperrt.auf"); + Files.writeString(nichtLesbar, "Inhalt"); + boolean ok = nichtLesbar.toFile().setReadable(false); + org.junit.jupiter.api.Assumptions.assumeTrue(ok, + "setReadable(false) nicht anwendbar — Test wird übersprungen"); + + try { + FileValidationService service = mock(FileValidationService.class); + CliRunner runner = runnerWith(service); + + int exitCode = runner.run(new String[]{nichtLesbar.toString()}); + + assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode, + "Nicht lesbare Datei muss Exit-Code 2 liefern"); + verifyNoInteractions(service); + } finally { + nichtLesbar.toFile().setReadable(true); + } + } + + // ----------------------------------------------------------------------- + // Verdict OPERATIONAL_ERROR verifizieren + // ----------------------------------------------------------------------- + + @Test + @DisplayName("operationalError-Report: Verdict ist OPERATIONAL_ERROR") + void operationalErrorReport_verdictIstOPERATIONAL_ERROR() { + ValidationReport report = ValidationReport.operationalError( + "", "OPERATIONAL-MISSING-ARG", + "Kein Dateipfad angegeben."); + + assertEquals(Verdict.OPERATIONAL_ERROR, report.computeVerdict(), + "operationalError-Factory muss Verdict OPERATIONAL_ERROR liefern"); + + List findings = report.getFindings(); + assertFalse(findings.isEmpty(), "Mindestens ein Finding erwartet"); + + Finding finding = findings.get(0); + assertEquals("OPERATIONAL-MISSING-ARG", finding.ruleId(), + "ruleId muss OPERATIONAL-MISSING-ARG sein"); + assertNotNull(finding.germanMessage(), "germanMessage darf nicht null sein"); + } + + @Test + @DisplayName("Alle ruleIds der 5 Bedienfehler-Fälle sind korrekt definiert") + void alleRuleIds_sindKorrektDefiniert() { + String[] ruleIds = { + "OPERATIONAL-MISSING-ARG", + "OPERATIONAL-TOO-MANY-ARGS", + "OPERATIONAL-FILE-NOT-FOUND", + "OPERATIONAL-NOT-REGULAR", + "OPERATIONAL-NOT-READABLE" + }; + + for (String ruleId : ruleIds) { + ValidationReport report = ValidationReport.operationalError( + "test.auf", ruleId, "Testmeldung für " + ruleId); + + assertEquals(Verdict.OPERATIONAL_ERROR, report.computeVerdict(), + "Report mit ruleId " + ruleId + " muss OPERATIONAL_ERROR liefern"); + + assertEquals(ruleId, report.getFindings().get(0).ruleId(), + "ruleId muss korrekt gesetzt sein: " + ruleId); + } + } + + // ----------------------------------------------------------------------- + // Negativ-Test: Kein Stack-Trace in STDERR + // ----------------------------------------------------------------------- + + @Test + @DisplayName("Kein Stack-Trace in STDERR bei Bedienfehler 'Kein Argument'") + void keinArgument_keinStackTraceInStderr() { + FileValidationService service = mock(FileValidationService.class); + CliRunner runner = runnerWith(service); + + runner.run(new String[]{}); + + String err = stderr(); + // Stack-Traces enthalten "at " gefolgt von Java-Paketnamen + assertFalse(err.contains("\tat "), + "STDERR darf keinen Stack-Trace enthalten (kein '\\tat '). Gefunden: " + err); + assertFalse(err.contains("Exception"), + "STDERR darf keinen Exception-Klassennamen enthalten. Gefunden: " + err); + } + + @Test + @DisplayName("Kein Stack-Trace in STDERR bei Bedienfehler 'Datei nicht gefunden'") + void dateiNichtGefunden_keinStackTraceInStderr() { + Path nichtVorhanden = tempDir.resolve("nicht-da.auf"); + FileValidationService service = mock(FileValidationService.class); + CliRunner runner = runnerWith(service); + + runner.run(new String[]{nichtVorhanden.toString()}); + + String err = stderr(); + assertFalse(err.contains("\tat "), + "STDERR darf keinen Stack-Trace enthalten. Gefunden: " + err); + assertFalse(err.contains("Exception"), + "STDERR darf keinen Exception-Klassennamen enthalten. Gefunden: " + err); + } + + @Test + @DisplayName("Kein Stack-Trace in STDERR bei Bedienfehler 'Pfad ist Verzeichnis'") + void pfadIstVerzeichnis_keinStackTraceInStderr() { + FileValidationService service = mock(FileValidationService.class); + CliRunner runner = runnerWith(service); + + runner.run(new String[]{tempDir.toString()}); + + String err = stderr(); + assertFalse(err.contains("\tat "), + "STDERR darf keinen Stack-Trace enthalten. Gefunden: " + err); + assertFalse(err.contains("Exception"), + "STDERR darf keinen Exception-Klassennamen enthalten. Gefunden: " + err); + } + + // ----------------------------------------------------------------------- + // Hilfsmethoden + // ----------------------------------------------------------------------- + + /** Zählt .txt-Dateien direkt im angegebenen Verzeichnis. */ + private long countTxtFiles(Path dir) { + try { + return Files.list(dir) + .filter(p -> p.toString().endsWith(".txt")) + .count(); + } catch (IOException e) { + return 0; + } + } +} diff --git a/src/test/java/de/gecheckt/asv/adapter/in/cli/CliRunnerOutputArtifactsTest.java b/src/test/java/de/gecheckt/asv/adapter/in/cli/CliRunnerOutputArtifactsTest.java new file mode 100644 index 0000000..4b62dc1 --- /dev/null +++ b/src/test/java/de/gecheckt/asv/adapter/in/cli/CliRunnerOutputArtifactsTest.java @@ -0,0 +1,195 @@ +package de.gecheckt.asv.adapter.in.cli; + +import de.gecheckt.asv.adapter.out.filesystem.SuffixResolver; +import de.gecheckt.asv.adapter.out.logging.LoggingConfigurator; +import de.gecheckt.asv.adapter.out.reporting.ReportFileWriter; +import de.gecheckt.asv.application.DummyFileValidationService; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; + +/** + * End-to-End-Integrationstests für die Ausgabeartefakte (AP07). + * + *

Prüft, dass nach einem Lauf beide Ausgabedateien (Berichtdatei {@code .txt} + * und Log-Datei {@code .log}) im Verzeichnis der Eingabedatei entstehen, dass die + * Suffix-Logik bei Folgeläufen greift und dass beide Dateien UTF-8 kodiert sind.

+ * + *

Hinweis: Der {@link LoggingConfigurator} wird als No-Op-Mock eingesetzt, um das + * TempDir-Locking durch geöffnete Log4j2-FileAppender auf Windows zu vermeiden. + * Ein separater manueller End-to-End-Test mit dem Uber-JAR belegt, dass die echte + * Log-Datei-Umkonfiguration funktioniert.

+ */ +class CliRunnerOutputArtifactsTest { + + /** + * Erzeugt einen CliRunner mit DummyFileValidationService und No-Op-LoggingConfigurator. + */ + private CliRunner buildRunner() { + LoggingConfigurator noOpLogging = mock(LoggingConfigurator.class); + doNothing().when(noOpLogging).configureLogFile(any(Path.class)); + SuffixResolver suffixResolver = new SuffixResolver(); + ReportFileWriter reportFileWriter = new ReportFileWriter(suffixResolver); + return new CliRunner( + new DummyFileValidationService(), + noOpLogging, + suffixResolver, + reportFileWriter); + } + + @Test + @DisplayName("Lauf 1: foo.auf → foo.auf.txt wird erzeugt") + void lauf1_erzeugtBerichtdateiOhneSuffix(@TempDir Path tempDir) throws IOException { + Path inputFile = tempDir.resolve("foo.auf"); + Files.createFile(inputFile); + + suppressStdout(); + try { + buildRunner().run(new String[]{inputFile.toString()}); + } finally { + restoreStdout(); + } + + Path expectedReport = tempDir.resolve("foo.auf.txt"); + assertTrue(Files.exists(expectedReport), + "Nach Lauf 1 soll foo.auf.txt existieren"); + } + + @Test + @DisplayName("Lauf 2: foo.auf → foo.auf_v1.txt bei vorhandener foo.auf.txt") + void lauf2_erzeugtBerichtdateiMitV1Suffix(@TempDir Path tempDir) throws IOException { + Path inputFile = tempDir.resolve("foo.auf"); + Files.createFile(inputFile); + + suppressStdout(); + try { + // Lauf 1 + buildRunner().run(new String[]{inputFile.toString()}); + // Lauf 2 + buildRunner().run(new String[]{inputFile.toString()}); + } finally { + restoreStdout(); + } + + assertTrue(Files.exists(tempDir.resolve("foo.auf.txt")), + "Lauf 1 soll foo.auf.txt erzeugt haben"); + assertTrue(Files.exists(tempDir.resolve("foo.auf_v1.txt")), + "Lauf 2 soll foo.auf_v1.txt erzeugt haben"); + } + + @Test + @DisplayName("Lauf 3: foo.auf → foo.auf_v2.txt bei vorhandenen foo.auf.txt und foo.auf_v1.txt") + void lauf3_erzeugtBerichtdateiMitV2Suffix(@TempDir Path tempDir) throws IOException { + Path inputFile = tempDir.resolve("foo.auf"); + Files.createFile(inputFile); + + suppressStdout(); + try { + buildRunner().run(new String[]{inputFile.toString()}); + buildRunner().run(new String[]{inputFile.toString()}); + buildRunner().run(new String[]{inputFile.toString()}); + } finally { + restoreStdout(); + } + + assertTrue(Files.exists(tempDir.resolve("foo.auf_v2.txt")), + "Lauf 3 soll foo.auf_v2.txt erzeugt haben"); + } + + @Test + @DisplayName("Suffix-Zählung für .txt und .log ist unabhängig") + void suffixZaehlung_istProExtensionUnabhaengig(@TempDir Path tempDir) throws IOException { + Path inputFile = tempDir.resolve("bar.auf"); + Files.createFile(inputFile); + // .txt voranlegen (simuliert bereits vorhandenen Bericht ohne Log) + Files.createFile(tempDir.resolve("bar.auf.txt")); + + suppressStdout(); + try { + buildRunner().run(new String[]{inputFile.toString()}); + } finally { + restoreStdout(); + } + + // .txt bereits vorhanden → _v1.txt erzeugt + assertTrue(Files.exists(tempDir.resolve("bar.auf_v1.txt")), + "Vorhandene bar.auf.txt soll zu bar.auf_v1.txt führen"); + // .log war nicht vorhanden → kein Suffix (wird vom No-Op-Logger nicht erzeugt; + // der SuffixResolver würde bar.auf.log zurückgeben, wenn configureLogFile aufgerufen wird) + } + + @Test + @DisplayName("Berichtdatei ist in UTF-8 kodiert (enthält Umlaute korrekt)") + void berichtdatei_istInUtf8(@TempDir Path tempDir) throws IOException { + Path inputFile = tempDir.resolve("encoding.auf"); + Files.createFile(inputFile); + + suppressStdout(); + try { + buildRunner().run(new String[]{inputFile.toString()}); + } finally { + restoreStdout(); + } + + Path reportFile = tempDir.resolve("encoding.auf.txt"); + assertTrue(Files.exists(reportFile)); + + byte[] bytes = Files.readAllBytes(reportFile); + String content = new String(bytes, StandardCharsets.UTF_8); + // Das Urteil "GÜLTIG" enthält das Umlaut Ü — wenn UTF-8 korrekt, dann lesbar + assertTrue(content.contains("GÜLTIG"), + "UTF-8-Datei soll 'GÜLTIG' mit Umlaut enthalten"); + } + + @Test + @DisplayName("Konsolenausgabe ist identisch zum Berichtdatei-Inhalt") + void konsolenausgabe_identischZumBerichtinhalt(@TempDir Path tempDir) throws IOException { + Path inputFile = tempDir.resolve("console.auf"); + Files.createFile(inputFile); + + PrintStream originalOut = System.out; + java.io.ByteArrayOutputStream outBuf = new java.io.ByteArrayOutputStream(); + System.setOut(new PrintStream(outBuf, true, StandardCharsets.UTF_8)); + + try { + buildRunner().run(new String[]{inputFile.toString()}); + } finally { + System.setOut(originalOut); + } + + Path reportFile = tempDir.resolve("console.auf.txt"); + String fileContent = Files.readString(reportFile, StandardCharsets.UTF_8); + String consoleContent = outBuf.toString(StandardCharsets.UTF_8); + + assertEquals(fileContent, consoleContent, + "Konsolenausgabe soll identisch zum Berichtdatei-Inhalt sein"); + } + + // ----------------------------------------------------------------------- + // Hilfsmethoden für stdout-Unterdrückung + // ----------------------------------------------------------------------- + + private PrintStream originalOut; + + private void suppressStdout() { + originalOut = System.out; + System.setOut(new PrintStream(new java.io.ByteArrayOutputStream())); + } + + private void restoreStdout() { + System.setOut(originalOut); + } +} diff --git a/src/test/java/de/gecheckt/asv/adapter/in/cli/CliRunnerTest.java b/src/test/java/de/gecheckt/asv/adapter/in/cli/CliRunnerTest.java new file mode 100644 index 0000000..cfb979b --- /dev/null +++ b/src/test/java/de/gecheckt/asv/adapter/in/cli/CliRunnerTest.java @@ -0,0 +1,248 @@ +package de.gecheckt.asv.adapter.in.cli; + +import de.gecheckt.asv.adapter.out.filesystem.SuffixResolver; +import de.gecheckt.asv.adapter.out.logging.LoggingConfigurator; +import de.gecheckt.asv.adapter.out.reporting.ReportFileWriter; +import de.gecheckt.asv.application.FileValidationService; +import de.gecheckt.asv.domain.finding.Finding; +import de.gecheckt.asv.domain.finding.FindingKind; +import de.gecheckt.asv.domain.finding.FindingLayer; +import de.gecheckt.asv.domain.finding.Severity; +import de.gecheckt.asv.domain.finding.ValidationReport; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +/** + * Unit-Tests für {@link CliRunner}. + * + *

Abgedeckte Abnahmekriterien aus AP06/AP07:

+ *
    + *
  • Aufruf ohne Argument → Exit-Code 2
  • + *
  • Aufruf mit ≥ 2 Argumenten → Exit-Code 2
  • + *
  • Aufruf mit nicht existierender Datei → Exit-Code 2
  • + *
  • Aufruf mit leerer, lesbarer Datei → Exit-Code 0
  • + *
  • Konsolenausgabe enthält Berichtinhalt
  • + *
  • Berichtdatei wird im Verzeichnis der Eingabedatei erzeugt
  • + *
+ * + *

Hinweis: Der {@link LoggingConfigurator} wird in diesen Tests als Mockito-Mock + * verwendet (No-Op für {@code configureLogFile}), um zu verhindern, dass ein echter + * Log4j2-File-Appender im TempDir geöffnet bleibt und die TempDir-Bereinigung durch + * JUnit blockiert (Windows-spezifisches Dateisperrproblem).

+ */ +class CliRunnerTest { + + @TempDir + Path tempDir; + + // ----------------------------------------------------------------------- + // Hilfsmethode: CliRunner mit No-Op-LoggingConfigurator erzeugen + // ----------------------------------------------------------------------- + + /** + * Erzeugt einen {@link CliRunner} mit echten Adaptern (SuffixResolver, ReportFileWriter) + * und einem Mockito-Mock für LoggingConfigurator (No-Op für configureLogFile). + */ + private CliRunner runnerWith(FileValidationService service) { + LoggingConfigurator noOpLogging = mock(LoggingConfigurator.class); + doNothing().when(noOpLogging).configureLogFile(any(Path.class)); + return new CliRunner( + service, + noOpLogging, + new SuffixResolver(), + new ReportFileWriter(new SuffixResolver())); + } + + // ----------------------------------------------------------------------- + // Argument-Validierung + // ----------------------------------------------------------------------- + + @Test + @DisplayName("Kein Argument → Exit-Code 2 (OPERATIONAL_ERROR)") + void keineArgumente_liefernExitCode2() { + FileValidationService service = mock(FileValidationService.class); + CliRunner runner = runnerWith(service); + + ByteArrayOutputStream err = captureStderr(); + int exitCode = runner.run(new String[]{}); + restoreStderr(); + + assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode); + assertTrue(err.toString().contains("Fehler"), + "STDERR soll eine deutsche Fehlermeldung enthalten"); + verifyNoInteractions(service); + } + + @Test + @DisplayName("Zwei Argumente → Exit-Code 2 (OPERATIONAL_ERROR)") + void zweiArgumente_liefernExitCode2() { + FileValidationService service = mock(FileValidationService.class); + CliRunner runner = runnerWith(service); + + ByteArrayOutputStream err = captureStderr(); + int exitCode = runner.run(new String[]{"datei1.txt", "datei2.txt"}); + restoreStderr(); + + assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode); + verifyNoInteractions(service); + } + + // ----------------------------------------------------------------------- + // Datei-Vorabprüfung + // ----------------------------------------------------------------------- + + @Test + @DisplayName("Nicht existierende Datei → Exit-Code 2 (OPERATIONAL_ERROR)") + void nichtExistierendeDatei_liefertExitCode2() { + FileValidationService service = mock(FileValidationService.class); + CliRunner runner = runnerWith(service); + + ByteArrayOutputStream err = captureStderr(); + int exitCode = runner.run(new String[]{"/nicht/vorhanden/datei.txt"}); + restoreStderr(); + + assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode); + assertTrue(err.toString().contains("Fehler"), + "STDERR soll eine deutsche Fehlermeldung enthalten"); + verifyNoInteractions(service); + } + + // ----------------------------------------------------------------------- + // Erfolgreicher Lauf + // ----------------------------------------------------------------------- + + @Test + @DisplayName("Leere, lesbare Datei ohne Spec-Fehler → Exit-Code 0 (VALID)") + void leereLesbareDatei_liefertExitCode0() throws IOException { + Path testFile = tempDir.resolve("leer.auf"); + Files.createFile(testFile); + + ValidationReport emptyReport = new ValidationReport("leer.auf", Instant.now(), List.of()); + FileValidationService service = mock(FileValidationService.class); + when(service.validate(any(Path.class))).thenReturn(emptyReport); + + CliRunner runner = runnerWith(service); + int exitCode = runner.run(new String[]{testFile.toString()}); + + assertEquals(ExitCode.VALID, exitCode); + verify(service).validate(any(Path.class)); + } + + @Test + @DisplayName("Datei mit SPEC-ERROR-Befund → Exit-Code 1 (INVALID)") + void dateimitSpecFehler_liefertExitCode1() throws IOException { + Path testFile = tempDir.resolve("fehlerhaft.auf"); + Files.writeString(testFile, "irgendein Inhalt"); + + Finding specError = Finding.builder( + FindingKind.SPEC, Severity.ERROR, FindingLayer.ARTIFACT, "Testfehler").build(); + ValidationReport reportWithError = new ValidationReport( + "fehlerhaft.auf", Instant.now(), List.of(specError)); + FileValidationService service = mock(FileValidationService.class); + when(service.validate(any(Path.class))).thenReturn(reportWithError); + + CliRunner runner = runnerWith(service); + int exitCode = runner.run(new String[]{testFile.toString()}); + + assertEquals(ExitCode.INVALID, exitCode); + } + + @Test + @DisplayName("ValidationReport mit operationalError → Exit-Code 2 (OPERATIONAL_ERROR)") + void operationalErrorReport_liefertExitCode2() throws IOException { + Path testFile = tempDir.resolve("bedien.auf"); + Files.writeString(testFile, "Inhalt"); + + ValidationReport errReport = ValidationReport.operationalError( + "bedien.auf", "CLI-001", "Bedienfehler"); + FileValidationService service = mock(FileValidationService.class); + when(service.validate(any(Path.class))).thenReturn(errReport); + + CliRunner runner = runnerWith(service); + int exitCode = runner.run(new String[]{testFile.toString()}); + + assertEquals(ExitCode.OPERATIONAL_ERROR, exitCode); + } + + @Test + @DisplayName("Konsolenausgabe (stdout) enthält Berichtinhalt mit GÜLTIG-Urteil") + void konsolenausgabe_enthaeltBerichtinhalt() throws IOException { + Path testFile = tempDir.resolve("konsole.auf"); + Files.createFile(testFile); + + ValidationReport emptyReport = new ValidationReport("konsole.auf", Instant.now(), List.of()); + FileValidationService service = mock(FileValidationService.class); + when(service.validate(any(Path.class))).thenReturn(emptyReport); + + PrintStream originalOut = System.out; + ByteArrayOutputStream outBuf = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outBuf)); + + try { + CliRunner runner = runnerWith(service); + runner.run(new String[]{testFile.toString()}); + } finally { + System.setOut(originalOut); + } + + String output = outBuf.toString(); + assertTrue(output.contains("GÜLTIG"), + "Konsolenausgabe soll 'GÜLTIG' enthalten"); + assertTrue(output.contains("ASV-Format-Validator"), + "Konsolenausgabe soll Berichtkopf enthalten"); + } + + @Test + @DisplayName("Berichtdatei wird nach Lauf im Verzeichnis der Eingabedatei erzeugt") + void berichtdatei_wirdNachLaufErzeugt() throws IOException { + Path testFile = tempDir.resolve("bericht.auf"); + Files.createFile(testFile); + + ValidationReport emptyReport = new ValidationReport("bericht.auf", Instant.now(), List.of()); + FileValidationService service = mock(FileValidationService.class); + when(service.validate(any(Path.class))).thenReturn(emptyReport); + + CliRunner runner = runnerWith(service); + runner.run(new String[]{testFile.toString()}); + + Path expectedReport = tempDir.resolve("bericht.auf.txt"); + assertTrue(Files.exists(expectedReport), + "Berichtdatei soll existieren: " + expectedReport); + } + + // ----------------------------------------------------------------------- + // Hilfsmethoden + // ----------------------------------------------------------------------- + + private PrintStream originalStderr; + + private ByteArrayOutputStream captureStderr() { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + originalStderr = System.err; + System.setErr(new PrintStream(buf)); + return buf; + } + + private void restoreStderr() { + System.setErr(originalStderr); + } +} diff --git a/src/test/java/de/gecheckt/asv/adapter/out/filesystem/SuffixResolverTest.java b/src/test/java/de/gecheckt/asv/adapter/out/filesystem/SuffixResolverTest.java new file mode 100644 index 0000000..d3f5e3d --- /dev/null +++ b/src/test/java/de/gecheckt/asv/adapter/out/filesystem/SuffixResolverTest.java @@ -0,0 +1,130 @@ +package de.gecheckt.asv.adapter.out.filesystem; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Unit-Tests für {@link SuffixResolver}. + */ +class SuffixResolverTest { + + private final SuffixResolver resolver = new SuffixResolver(); + + // ------------------------------------------------------------------------- + // Normalfälle + // ------------------------------------------------------------------------- + + @Test + @DisplayName("Keine Datei vorhanden → Basisname ohne Suffix") + void keineDateiVorhanden_gibtBasisnameOhneSuffix(@TempDir Path tempDir) { + Path result = resolver.resolveNextFreePath(tempDir, "foo.auf", "txt"); + + assertEquals(tempDir.resolve("foo.auf.txt"), result); + assertFalse(Files.exists(result), "Ergebnis darf noch nicht existieren"); + } + + @Test + @DisplayName("Basisname.txt vorhanden → _v1.txt") + void txtVorhanden_gibtV1Suffix(@TempDir Path tempDir) throws IOException { + Files.createFile(tempDir.resolve("foo.auf.txt")); + + Path result = resolver.resolveNextFreePath(tempDir, "foo.auf", "txt"); + + assertEquals(tempDir.resolve("foo.auf_v1.txt"), result); + assertFalse(Files.exists(result), "Ergebnis darf noch nicht existieren"); + } + + @Test + @DisplayName("Basisname.txt und _v1.txt vorhanden → _v2.txt") + void txtUndV1Vorhanden_gibtV2Suffix(@TempDir Path tempDir) throws IOException { + Files.createFile(tempDir.resolve("foo.auf.txt")); + Files.createFile(tempDir.resolve("foo.auf_v1.txt")); + + Path result = resolver.resolveNextFreePath(tempDir, "foo.auf", "txt"); + + assertEquals(tempDir.resolve("foo.auf_v2.txt"), result); + assertFalse(Files.exists(result), "Ergebnis darf noch nicht existieren"); + } + + @Test + @DisplayName("Suffix-Zählung ist pro Extension unabhängig: .txt zählt nicht für .log") + void txtZaehltNichtFuerLog(@TempDir Path tempDir) throws IOException { + // .txt vorhanden → für txt wäre _v1.txt nötig + Files.createFile(tempDir.resolve("foo.auf.txt")); + Files.createFile(tempDir.resolve("foo.auf_v1.txt")); + + // .log ist unberührt → kein Suffix nötig + Path logResult = resolver.resolveNextFreePath(tempDir, "foo.auf", "log"); + + assertEquals(tempDir.resolve("foo.auf.log"), logResult); + assertFalse(Files.exists(logResult), "Ergebnis darf noch nicht existieren"); + } + + @Test + @DisplayName("Drei aufeinanderfolgende Läufe erzeugen korrekte Suffixfolge") + void dreiLaeufe_erzeugenKorrekteSuffixfolge(@TempDir Path tempDir) throws IOException { + // Lauf 1 → kein Suffix + Path first = resolver.resolveNextFreePath(tempDir, "bar.auf", "txt"); + assertEquals(tempDir.resolve("bar.auf.txt"), first); + Files.createFile(first); + + // Lauf 2 → _v1 + Path second = resolver.resolveNextFreePath(tempDir, "bar.auf", "txt"); + assertEquals(tempDir.resolve("bar.auf_v1.txt"), second); + Files.createFile(second); + + // Lauf 3 → _v2 + Path third = resolver.resolveNextFreePath(tempDir, "bar.auf", "txt"); + assertEquals(tempDir.resolve("bar.auf_v2.txt"), third); + } + + @Test + @DisplayName("Basisname mit Punkt (z.B. 'foo.auf') wird korrekt behandelt") + void baseName_mitPunkt_wirdKorrektBehandelt(@TempDir Path tempDir) { + Path result = resolver.resolveNextFreePath(tempDir, "foo.auf", "txt"); + + // Dateiname muss sein: foo.auf.txt (Punkt aus baseName + Punkt + ext) + assertEquals("foo.auf.txt", result.getFileName().toString()); + } + + // ------------------------------------------------------------------------- + // Fehlerfälle + // ------------------------------------------------------------------------- + + @Test + @DisplayName("Null directory → IllegalArgumentException") + void nullDirectory_wirft_IllegalArgumentException() { + assertThrows(IllegalArgumentException.class, + () -> resolver.resolveNextFreePath(null, "foo", "txt")); + } + + @Test + @DisplayName("Null baseName → IllegalArgumentException") + void nullBaseName_wirft_IllegalArgumentException(@TempDir Path tempDir) { + assertThrows(IllegalArgumentException.class, + () -> resolver.resolveNextFreePath(tempDir, null, "txt")); + } + + @Test + @DisplayName("Leerer baseName → IllegalArgumentException") + void leererBaseName_wirft_IllegalArgumentException(@TempDir Path tempDir) { + assertThrows(IllegalArgumentException.class, + () -> resolver.resolveNextFreePath(tempDir, " ", "txt")); + } + + @Test + @DisplayName("Null extension → IllegalArgumentException") + void nullExtension_wirft_IllegalArgumentException(@TempDir Path tempDir) { + assertThrows(IllegalArgumentException.class, + () -> resolver.resolveNextFreePath(tempDir, "foo", null)); + } +} diff --git a/src/test/java/de/gecheckt/asv/adapter/out/reporting/ReportFileWriterTest.java b/src/test/java/de/gecheckt/asv/adapter/out/reporting/ReportFileWriterTest.java new file mode 100644 index 0000000..1dc33ae --- /dev/null +++ b/src/test/java/de/gecheckt/asv/adapter/out/reporting/ReportFileWriterTest.java @@ -0,0 +1,206 @@ +package de.gecheckt.asv.adapter.out.reporting; + +import de.gecheckt.asv.adapter.out.filesystem.SuffixResolver; +import de.gecheckt.asv.domain.finding.Finding; +import de.gecheckt.asv.domain.finding.FindingKind; +import de.gecheckt.asv.domain.finding.FindingLayer; +import de.gecheckt.asv.domain.finding.Severity; +import de.gecheckt.asv.domain.finding.ValidationReport; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit-Tests für {@link ReportFileWriter}. + */ +class ReportFileWriterTest { + + private final SuffixResolver suffixResolver = new SuffixResolver(); + private final ReportFileWriter writer = new ReportFileWriter(suffixResolver); + + // ------------------------------------------------------------------------- + // Grundfunktion: Datei wird erzeugt + // ------------------------------------------------------------------------- + + @Test + @DisplayName("Leerer Report → Berichtdatei wird im Verzeichnis der Eingabedatei erzeugt") + void leererReport_erzeugtBerichtdateiImEingabeverzeichnis(@TempDir Path tempDir) throws IOException { + Path inputFile = tempDir.resolve("test.auf"); + Files.createFile(inputFile); + + ValidationReport report = new ValidationReport("test.auf", Instant.now(), List.of()); + ReportFileWriter.ReportWriteResult result = writer.write(report, inputFile); + + assertTrue(result.isSuccess(), "Schreibvorgang soll erfolgreich sein"); + assertEquals(tempDir.resolve("test.auf.txt"), result.reportPath()); + assertTrue(Files.exists(result.reportPath()), "Berichtdatei soll existieren"); + } + + @Test + @DisplayName("Berichtdatei ist in UTF-8 kodiert (Sonderzeichen äöü߀)") + void berichtdatei_istInUtf8(@TempDir Path tempDir) throws IOException { + Path inputFile = tempDir.resolve("test.auf"); + Files.createFile(inputFile); + + Finding finding = Finding.builder(FindingKind.SPEC, Severity.ERROR, + FindingLayer.ARTIFACT, "Ungültiges Feld — Sonderzeichen: äöü߀").build(); + ValidationReport report = new ValidationReport("test.auf", Instant.now(), List.of(finding)); + + ReportFileWriter.ReportWriteResult result = writer.write(report, inputFile); + + assertTrue(result.isSuccess()); + byte[] bytes = Files.readAllBytes(result.reportPath()); + String content = new String(bytes, StandardCharsets.UTF_8); + assertTrue(content.contains("äöü߀"), + "UTF-8-dekodierter Inhalt soll Sonderzeichen enthalten"); + } + + @Test + @DisplayName("Kopfzeile enthält Zeitstempel, Eingabedatei und Urteil GÜLTIG") + void kopfzeile_enthaeltZeitstempelEingabedateiUrteil(@TempDir Path tempDir) throws IOException { + Path inputFile = tempDir.resolve("bar.auf"); + Files.createFile(inputFile); + + Instant now = Instant.parse("2026-04-20T10:30:00Z"); + ValidationReport report = new ValidationReport("bar.auf", now, List.of()); + + ReportFileWriter.ReportWriteResult result = writer.write(report, inputFile); + String content = result.reportContent(); + + assertTrue(content.contains("2026-04-20T10:30:00Z"), + "Kopfzeile soll ISO-Zeitstempel enthalten"); + assertTrue(content.contains("bar.auf"), + "Kopfzeile soll Dateinamen enthalten"); + assertTrue(content.contains("GÜLTIG"), + "Kopfzeile soll Urteil 'GÜLTIG' enthalten"); + } + + @Test + @DisplayName("Pro Finding wird eine Zeile mit Severity, Kind, Layer und Meldung ausgegeben") + void proFinding_wirdEineZeileAusgegeben(@TempDir Path tempDir) throws IOException { + Path inputFile = tempDir.resolve("foo.auf"); + Files.createFile(inputFile); + + Finding finding = Finding.builder(FindingKind.SPEC, Severity.ERROR, + FindingLayer.TECHNICAL_STRUCTURE, "Pflichtfeld fehlt") + .fieldId("UNB_0020") + .build(); + ValidationReport report = new ValidationReport("foo.auf", Instant.now(), List.of(finding)); + + ReportFileWriter.ReportWriteResult result = writer.write(report, inputFile); + String content = result.reportContent(); + + assertTrue(content.contains("[ERROR]"), "Zeile soll Severity enthalten"); + assertTrue(content.contains("[SPEC]"), "Zeile soll Kind enthalten"); + assertTrue(content.contains("[TECHNICAL_STRUCTURE]"), "Zeile soll Layer enthalten"); + assertTrue(content.contains("UNB_0020"), "Zeile soll Feld-ID enthalten"); + assertTrue(content.contains("Pflichtfeld fehlt"), "Zeile soll deutsche Meldung enthalten"); + } + + @Test + @DisplayName("Fußzeile enthält Hinweis auf M1-Platzhalter-Validator") + void fuszeile_enthaeltHinweisAufM1Platzhalter(@TempDir Path tempDir) throws IOException { + Path inputFile = tempDir.resolve("foo.auf"); + Files.createFile(inputFile); + + ValidationReport report = new ValidationReport("foo.auf", Instant.now(), List.of()); + ReportFileWriter.ReportWriteResult result = writer.write(report, inputFile); + + assertTrue(result.reportContent().contains("M1-Platzhalter"), + "Fußzeile soll Hinweis auf M1-Platzhalter enthalten"); + } + + @Test + @DisplayName("Zweiter Lauf → Suffix _v1.txt") + void zweiterLauf_gibtV1Suffix(@TempDir Path tempDir) throws IOException { + Path inputFile = tempDir.resolve("foo.auf"); + Files.createFile(inputFile); + + ValidationReport report = new ValidationReport("foo.auf", Instant.now(), List.of()); + + // Erster Lauf + ReportFileWriter.ReportWriteResult first = writer.write(report, inputFile); + assertEquals(tempDir.resolve("foo.auf.txt"), first.reportPath(), + "Erster Lauf soll keinen Suffix erzeugen"); + + // Zweiter Lauf + ValidationReport report2 = new ValidationReport("foo.auf", Instant.now(), List.of()); + ReportFileWriter.ReportWriteResult second = writer.write(report2, inputFile); + assertEquals(tempDir.resolve("foo.auf_v1.txt"), second.reportPath(), + "Zweiter Lauf soll _v1-Suffix erzeugen"); + } + + @Test + @DisplayName("UNGÜLTIG-Urteil erscheint im Bericht") + void ungueltigUrteil_erscheintImBericht(@TempDir Path tempDir) throws IOException { + Path inputFile = tempDir.resolve("invalid.auf"); + Files.createFile(inputFile); + + Finding specError = Finding.builder(FindingKind.SPEC, Severity.ERROR, + FindingLayer.ARTIFACT, "Kritischer Fehler").build(); + ValidationReport report = new ValidationReport( + "invalid.auf", Instant.now(), List.of(specError)); + + ReportFileWriter.ReportWriteResult result = writer.write(report, inputFile); + assertTrue(result.reportContent().contains("UNGÜLTIG"), + "Bericht soll 'UNGÜLTIG' bei SPEC-ERROR enthalten"); + } + + @Test + @DisplayName("Keine Befunde → Zeile 'Keine Befunde' erscheint im Bericht") + void keineBefunde_zeigtPlatzhaltertext(@TempDir Path tempDir) throws IOException { + Path inputFile = tempDir.resolve("leer.auf"); + Files.createFile(inputFile); + + ValidationReport report = new ValidationReport("leer.auf", Instant.now(), List.of()); + ReportFileWriter.ReportWriteResult result = writer.write(report, inputFile); + + assertTrue(result.reportContent().contains("Keine Befunde"), + "Bericht soll 'Keine Befunde' bei leerem Report enthalten"); + } + + // ------------------------------------------------------------------------- + // Fehlerfälle + // ------------------------------------------------------------------------- + + @Test + @DisplayName("Null report → IllegalArgumentException") + void nullReport_wirft_IllegalArgumentException(@TempDir Path tempDir) { + Path inputFile = tempDir.resolve("foo.auf"); + assertThrows(IllegalArgumentException.class, + () -> writer.write(null, inputFile)); + } + + @Test + @DisplayName("Null inputFilePath → IllegalArgumentException") + void nullInputFilePath_wirft_IllegalArgumentException() { + ValidationReport report = new ValidationReport("foo.auf", Instant.now(), List.of()); + assertThrows(IllegalArgumentException.class, + () -> writer.write(report, null)); + } + + @Test + @DisplayName("ReportWriteResult.isSuccess gibt true zurück wenn reportPath gesetzt") + void reportWriteResult_isSuccess_true_wenn_Pfad_gesetzt() { + ReportFileWriter.ReportWriteResult result = + new ReportFileWriter.ReportWriteResult("inhalt", Path.of("foo.txt"), null); + + assertTrue(result.isSuccess()); + assertNotNull(result.reportPath()); + assertNull(result.writeException()); + } +} diff --git a/src/test/java/de/gecheckt/asv/application/DummyFileValidationServiceTest.java b/src/test/java/de/gecheckt/asv/application/DummyFileValidationServiceTest.java new file mode 100644 index 0000000..117d447 --- /dev/null +++ b/src/test/java/de/gecheckt/asv/application/DummyFileValidationServiceTest.java @@ -0,0 +1,75 @@ +package de.gecheckt.asv.application; + +import de.gecheckt.asv.domain.finding.ValidationReport; +import de.gecheckt.asv.domain.finding.Verdict; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Tests für {@link DummyFileValidationService}. + * + *

Schwerpunkt: Nachweis der korrekten ISO-8859-15-Dekodierung gemäß AP06-Abnahmekriterium + * „Byte {@code 0xA4} ergibt Euro-Zeichen €".

+ */ +class DummyFileValidationServiceTest { + + @TempDir + Path tempDir; + + @Test + @DisplayName("Leere Datei liefert leeren ValidationReport mit Verdict VALID") + void leereDatei_liefertLeerenReport() throws IOException { + Path leereDatei = tempDir.resolve("leer.txt"); + Files.createFile(leereDatei); + + DummyFileValidationService service = new DummyFileValidationService(); + ValidationReport report = service.validate(leereDatei); + + assertNotNull(report); + assertEquals(Verdict.VALID, report.computeVerdict()); + assertEquals("leer.txt", report.getFileName()); + assertNotNull(report.getTimestamp()); + } + + @Test + @DisplayName("Datei mit Inhalt liefert ValidationReport mit Verdict VALID (kein Spec-Fehler in M1)") + void dateiMitInhalt_liefertVALID() throws IOException { + Path datei = tempDir.resolve("inhalt.txt"); + Files.writeString(datei, "Test-Inhalt"); + + DummyFileValidationService service = new DummyFileValidationService(); + ValidationReport report = service.validate(datei); + + assertEquals(Verdict.VALID, report.computeVerdict()); + } + + @Test + @DisplayName("Byte 0xA4 in ISO-8859-15 wird als Euro-Zeichen € dekodiert") + void byte0xA4_wirdAlsEuroZeichenDekodiert() throws IOException { + // Byte 0xA4 ist in ISO-8859-15 dem Euro-Zeichen € zugewiesen. + // (In ISO-8859-1 wäre 0xA4 das Währungszeichen ¤ — daher dieser Test.) + byte[] inhalt = new byte[]{0x54, 0x65, 0x73, 0x74, (byte) 0xA4}; // "Test" + 0xA4 + Path datei = tempDir.resolve("euro.bin"); + Files.write(datei, inhalt); + + // Dekodierung mit dem in DummyFileValidationService verwendeten Charset + String dekodiert = new String(inhalt, DummyFileValidationService.INPUT_CHARSET); + + assertEquals("Test€", dekodiert, + "Byte 0xA4 muss in ISO-8859-15 als Euro-Zeichen € dekodiert werden"); + } + + @Test + @DisplayName("INPUT_CHARSET ist ISO-8859-15 (nicht UTF-8, nicht Plattform-Default)") + void inputCharset_istISO8859_15() { + assertEquals("ISO-8859-15", DummyFileValidationService.INPUT_CHARSET.name()); + } +} diff --git a/src/test/java/de/gecheckt/asv/bootstrap/NoOpValidatorsIntegrationTest.java b/src/test/java/de/gecheckt/asv/bootstrap/NoOpValidatorsIntegrationTest.java new file mode 100644 index 0000000..a573749 --- /dev/null +++ b/src/test/java/de/gecheckt/asv/bootstrap/NoOpValidatorsIntegrationTest.java @@ -0,0 +1,117 @@ +package de.gecheckt.asv.bootstrap; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import de.gecheckt.asv.application.DefaultInputFileValidator; +import de.gecheckt.asv.application.model.ValidationResult; +import de.gecheckt.asv.domain.model.Field; +import de.gecheckt.asv.domain.model.InputFile; +import de.gecheckt.asv.domain.model.Message; +import de.gecheckt.asv.domain.model.Segment; + +/** + * Integrationstest für den M1-Einfrierzustand der Preview-Validatoren. + * + *

Belegt, dass ein Lauf mit {@link NoOpStructureValidator} und {@link NoOpFieldValidator} + * keinerlei ASVREC-/ASVFEH-Segmentbefunde erzeugt — unabhängig davon, ob die Eingabedatei + * ASV-Strukturmerkmale enthält oder nicht.

+ * + *

Dieser Test ist der formale Nachweis für AP09-Abnahmekriterium + * „Integrationstest: Lauf mit Testdatei erzeugt keine ASVREC-/ASVFEH-Segmentbefunde".

+ */ +class NoOpValidatorsIntegrationTest { + + /** + * Erstellt einen {@link DefaultInputFileValidator} mit den M1-NoOp-Implementierungen. + */ + private DefaultInputFileValidator buildValidator() { + return new DefaultInputFileValidator( + new NoOpStructureValidator(), + new NoOpFieldValidator() + ); + } + + @Test + @DisplayName("KRITISCH: NoOp-Validatoren erzeugen keine Befunde für ASVREC-Struktur") + void asvrecStruktur_erzeugtKeineBefunde() { + // Given: eine vollständige ASVREC-Nachricht (die DefaultStructureValidator prüfen würde) + Segment unh = new Segment("UNH", 1, List.of(new Field(1, "12345"), new Field(2, "ASVREC:D:03B:UN:EAN008"))); + Segment ifa = new Segment("IFA", 2, List.of(new Field(1, "IFA-Wert"))); + Segment rea = new Segment("REA", 3, List.of(new Field(1, "100,00"), new Field(2, "X"), new Field(3, "Y"), new Field(4, "0"))); + Segment dgn = new Segment("DGN", 4, List.of(new Field(1, "DGN-Wert"))); + Segment lea = new Segment("LEA", 5, List.of(new Field(1, "LEA-Wert"))); + Segment iva = new Segment("IVA", 6, List.of(new Field(1, "IVA-Wert"))); + Segment unt = new Segment("UNT", 7, List.of(new Field(1, "7"), new Field(2, "12345"))); + Message message = new Message(1, List.of(unh, ifa, rea, dgn, lea, iva, unt)); + InputFile inputFile = new InputFile("testdatei.asv", List.of(message)); + + // When + ValidationResult result = buildValidator().validate(inputFile); + + // Then: keinerlei Befunde — NoOp-Validatoren produzieren nie Findings + assertTrue(result.getAllErrors().isEmpty(), + "NoOp-Validatoren dürfen keinerlei Befunde erzeugen, gefunden: " + result.getAllErrors()); + assertFalse(result.hasErrors(), "Keine Fehler erwartet"); + assertFalse(result.hasWarnings(), "Keine Warnungen erwartet"); + } + + @Test + @DisplayName("NoOp-Validatoren erzeugen keine Befunde für ASVFEH-Struktur") + void asvfehStruktur_erzeugtKeineBefunde() { + // Given: eine ASVFEH-Nachricht mit FHL-Segment + Segment unh = new Segment("UNH", 1, List.of(new Field(1, "99999"), new Field(2, "ASVFEH:D:03B:UN:EAN008"))); + Segment fhl = new Segment("FHL", 2, List.of(new Field(1, "Fehler-Hinweis"))); + Segment unt = new Segment("UNT", 3, List.of(new Field(1, "3"), new Field(2, "99999"))); + Message message = new Message(1, List.of(unh, fhl, unt)); + InputFile inputFile = new InputFile("testdatei-feh.asv", List.of(message)); + + // When + ValidationResult result = buildValidator().validate(inputFile); + + // Then: keinerlei Befunde + assertTrue(result.getAllErrors().isEmpty(), + "NoOp-Validatoren dürfen keinerlei Befunde erzeugen, gefunden: " + result.getAllErrors()); + } + + @Test + @DisplayName("NoOp-Validatoren erzeugen keine Befunde für leere Eingabedatei") + void leereEingabedatei_erzeugtKeineBefunde() { + // Given: eine Eingabedatei ohne Nachrichten + InputFile inputFile = new InputFile("leer.asv", List.of()); + + // When + ValidationResult result = buildValidator().validate(inputFile); + + // Then: keinerlei Befunde — auch STRUCTURE_001 (fehlende Nachricht) wird nicht gemeldet + assertTrue(result.getAllErrors().isEmpty(), + "NoOp-Validatoren dürfen auch für leere Eingabedatei keine Befunde erzeugen"); + } + + @Test + @DisplayName("NoOpStructureValidator wirft IllegalArgumentException bei null-Eingabe") + void noOpStructureValidator_wirftExceptionBeiNull() { + NoOpStructureValidator validator = new NoOpStructureValidator(); + org.junit.jupiter.api.Assertions.assertThrows( + IllegalArgumentException.class, + () -> validator.validate(null), + "Null-Eingabe muss IllegalArgumentException auslösen" + ); + } + + @Test + @DisplayName("NoOpFieldValidator wirft IllegalArgumentException bei null-Eingabe") + void noOpFieldValidator_wirftExceptionBeiNull() { + NoOpFieldValidator validator = new NoOpFieldValidator(); + org.junit.jupiter.api.Assertions.assertThrows( + IllegalArgumentException.class, + () -> validator.validate(null), + "Null-Eingabe muss IllegalArgumentException auslösen" + ); + } +} diff --git a/src/test/java/de/gecheckt/asv/domain/finding/FindingTest.java b/src/test/java/de/gecheckt/asv/domain/finding/FindingTest.java new file mode 100644 index 0000000..289b8eb --- /dev/null +++ b/src/test/java/de/gecheckt/asv/domain/finding/FindingTest.java @@ -0,0 +1,124 @@ +package de.gecheckt.asv.domain.finding; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit-Tests für {@link Finding}. + */ +class FindingTest { + + @Test + @DisplayName("Finding-Record mit allen Feldern ist korrekt befüllbar") + void findingRecordAllFields() { + Finding f = new Finding( + FindingKind.SPEC, + Severity.ERROR, + FindingLayer.TECHNICAL_STRUCTURE, + "RULE-001", + "1A001", + "UNB", + 0, + "UNB_0020", + "FALSCH", + 42, + "00001", + "Referenznummer stimmt nicht überein." + ); + + assertEquals(FindingKind.SPEC, f.kind()); + assertEquals(Severity.ERROR, f.severity()); + assertEquals(FindingLayer.TECHNICAL_STRUCTURE, f.layer()); + assertEquals("RULE-001", f.ruleId()); + assertEquals("1A001", f.officialErrorCode()); + assertEquals("UNB", f.segmentType()); + assertEquals(0, f.segmentIndex()); + assertEquals("UNB_0020", f.fieldId()); + assertEquals("FALSCH", f.rawValue()); + assertEquals(42, f.position()); + assertEquals("00001", f.messageReference()); + assertEquals("Referenznummer stimmt nicht überein.", f.germanMessage()); + } + + @Test + @DisplayName("Finding-Builder befüllt Pflichtfelder korrekt, optionale Felder null") + void findingBuilderMindestfelder() { + Finding f = Finding.builder( + FindingKind.DIAGNOSTIC, Severity.HINT, FindingLayer.ARTIFACT, + "Hinweis auf mögliche Anomalie.") + .build(); + + assertEquals(FindingKind.DIAGNOSTIC, f.kind()); + assertEquals(Severity.HINT, f.severity()); + assertEquals(FindingLayer.ARTIFACT, f.layer()); + assertEquals("Hinweis auf mögliche Anomalie.", f.germanMessage()); + + assertNull(f.ruleId()); + assertNull(f.officialErrorCode()); + assertNull(f.segmentType()); + assertNull(f.segmentIndex()); + assertNull(f.fieldId()); + assertNull(f.rawValue()); + assertNull(f.position()); + assertNull(f.messageReference()); + } + + @Test + @DisplayName("Finding-Builder befüllt optionale Felder korrekt") + void findingBuilderOptionalFelder() { + Finding f = Finding.builder( + FindingKind.SPEC, Severity.WARNING, FindingLayer.DOMAIN_MODEL, + "Segment fehlt.") + .ruleId("RULE-042") + .officialErrorCode("2A001") + .segmentType("REA") + .segmentIndex(5) + .fieldId("REA_0010") + .rawValue("") + .position(1024) + .messageReference("MSG00001") + .build(); + + assertEquals("RULE-042", f.ruleId()); + assertEquals("2A001", f.officialErrorCode()); + assertEquals("REA", f.segmentType()); + assertEquals(5, f.segmentIndex()); + assertEquals("REA_0010", f.fieldId()); + assertEquals("", f.rawValue()); + assertEquals(1024, f.position()); + assertEquals("MSG00001", f.messageReference()); + } + + @Test + @DisplayName("isSpecError() gibt true nur bei SPEC+ERROR") + void isSpecErrorNurBeiSpecUndError() { + Finding specError = Finding.builder(FindingKind.SPEC, Severity.ERROR, + FindingLayer.ARTIFACT, "Fehler.").build(); + Finding specWarning = Finding.builder(FindingKind.SPEC, Severity.WARNING, + FindingLayer.ARTIFACT, "Warnung.").build(); + Finding diagError = Finding.builder(FindingKind.DIAGNOSTIC, Severity.ERROR, + FindingLayer.ARTIFACT, "Diagnose-Fehler.").build(); + + assertTrue(specError.isSpecError(), "SPEC+ERROR muss isSpecError() = true ergeben."); + assertFalse(specWarning.isSpecError(), "SPEC+WARNING muss isSpecError() = false ergeben."); + assertFalse(diagError.isSpecError(), "DIAGNOSTIC+ERROR muss isSpecError() = false ergeben."); + } + + @Test + @DisplayName("Finding-Konstruktor wirft NullPointerException bei null-germanMessage") + void konstruktorWirftNPEBeiNullGermanMessage() { + assertThrows(NullPointerException.class, + () -> new Finding(FindingKind.SPEC, Severity.ERROR, FindingLayer.ARTIFACT, + null, null, null, null, null, null, null, null, null)); + } + + @Test + @DisplayName("Finding-Konstruktor wirft NullPointerException bei null-kind") + void konstruktorWirftNPEBeiNullKind() { + assertThrows(NullPointerException.class, + () -> new Finding(null, Severity.ERROR, FindingLayer.ARTIFACT, + null, null, null, null, null, null, null, null, "Meldung.")); + } +} diff --git a/src/test/java/de/gecheckt/asv/domain/finding/ValidationReportTest.java b/src/test/java/de/gecheckt/asv/domain/finding/ValidationReportTest.java new file mode 100644 index 0000000..146aa9d --- /dev/null +++ b/src/test/java/de/gecheckt/asv/domain/finding/ValidationReportTest.java @@ -0,0 +1,212 @@ +package de.gecheckt.asv.domain.finding; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit-Tests für {@link ValidationReport}. + * + *

Die kritische Invariante lautet: Nur SPEC-ERROR-Befunde führen zu {@link Verdict#INVALID}. + * Diagnostische Befunde — auch solche mit {@link Severity#ERROR} — dürfen das Urteil + * niemals auf {@link Verdict#INVALID} setzen.

+ */ +class ValidationReportTest { + + // --------------------------------------------------------------------------- + // Hilfsmethoden + // --------------------------------------------------------------------------- + + private static Finding specError() { + return Finding.builder(FindingKind.SPEC, Severity.ERROR, FindingLayer.ARTIFACT, + "Pflichtfeld fehlt.").build(); + } + + private static Finding specWarning() { + return Finding.builder(FindingKind.SPEC, Severity.WARNING, FindingLayer.ARTIFACT, + "Dateiname weicht vom Schema ab.").build(); + } + + private static Finding diagnosticError() { + return Finding.builder(FindingKind.DIAGNOSTIC, Severity.ERROR, FindingLayer.TECHNICAL_STRUCTURE, + "Diagnostischer Fehler — beeinflusst das Urteil nicht.").build(); + } + + private static Finding diagnosticWarning() { + return Finding.builder(FindingKind.DIAGNOSTIC, Severity.WARNING, FindingLayer.DOMAIN_MODEL, + "Diagnostische Warnung.").build(); + } + + private static ValidationReport emptyReport() { + return new ValidationReport("testdatei.txt", Instant.now(), List.of()); + } + + // --------------------------------------------------------------------------- + // Test 1: Leerer Report → VALID + // --------------------------------------------------------------------------- + + @Test + @DisplayName("Leerer Report liefert Verdict VALID") + void leeremReportLiefertVALID() { + ValidationReport report = emptyReport(); + + assertEquals(Verdict.VALID, report.computeVerdict(), + "Ein leerer Report ohne Befunde muss VALID liefern."); + } + + // --------------------------------------------------------------------------- + // Test 2: Ein SPEC-ERROR → INVALID + // --------------------------------------------------------------------------- + + @Test + @DisplayName("Ein SPEC-ERROR-Befund liefert Verdict INVALID") + void specErrorLiefertINVALID() { + ValidationReport report = new ValidationReport( + "testdatei.txt", Instant.now(), List.of(specError())); + + assertEquals(Verdict.INVALID, report.computeVerdict(), + "Ein SPEC-ERROR-Befund muss zu INVALID führen."); + } + + // --------------------------------------------------------------------------- + // Test 3 (kritisch): Ein DIAGNOSTIC-ERROR → VALID + // --------------------------------------------------------------------------- + + @Test + @DisplayName("KRITISCH: Ein DIAGNOSTIC-ERROR-Befund liefert Verdict VALID (niemals INVALID)") + void diagnosticErrorLiefertVALID() { + ValidationReport report = new ValidationReport( + "testdatei.txt", Instant.now(), List.of(diagnosticError())); + + assertEquals(Verdict.VALID, report.computeVerdict(), + "Ein DIAGNOSTIC-ERROR-Befund darf das Urteil NIEMALS auf INVALID setzen. " + + "Nur SPEC+ERROR zählt für das Gesamturteil."); + } + + // --------------------------------------------------------------------------- + // Test 4: SPEC-WARNING → VALID + // --------------------------------------------------------------------------- + + @Test + @DisplayName("Ein SPEC-WARNING-Befund liefert Verdict VALID") + void specWarningLiefertVALID() { + ValidationReport report = new ValidationReport( + "testdatei.txt", Instant.now(), List.of(specWarning())); + + assertEquals(Verdict.VALID, report.computeVerdict(), + "Nur SPEC+ERROR macht eine Datei ungültig — Warnungen nicht."); + } + + // --------------------------------------------------------------------------- + // Test 5: specFindings() / diagnosticFindings() filtern korrekt + // --------------------------------------------------------------------------- + + @Test + @DisplayName("specFindings() und diagnosticFindings() filtern korrekt") + void findingsFilterungKorrekt() { + Finding spec1 = specError(); + Finding spec2 = specWarning(); + Finding diag1 = diagnosticError(); + Finding diag2 = diagnosticWarning(); + + ValidationReport report = new ValidationReport( + "testdatei.txt", Instant.now(), List.of(spec1, diag1, spec2, diag2)); + + List specList = report.specFindings(); + List diagList = report.diagnosticFindings(); + + assertEquals(2, specList.size(), "specFindings() muss genau 2 SPEC-Befunde zurückliefern."); + assertEquals(2, diagList.size(), "diagnosticFindings() muss genau 2 DIAGNOSTIC-Befunde zurückliefern."); + + assertTrue(specList.stream().allMatch(f -> f.kind() == FindingKind.SPEC), + "specFindings() darf nur SPEC-Befunde enthalten."); + assertTrue(diagList.stream().allMatch(f -> f.kind() == FindingKind.DIAGNOSTIC), + "diagnosticFindings() darf nur DIAGNOSTIC-Befunde enthalten."); + } + + // --------------------------------------------------------------------------- + // Test 6: findings-Liste ist nicht von außen modifizierbar + // --------------------------------------------------------------------------- + + @Test + @DisplayName("Die findings-Liste ist von außen nicht modifizierbar") + void findingsListeNichtModifizierbar() { + List mutableList = new ArrayList<>(); + mutableList.add(specError()); + ValidationReport report = new ValidationReport("testdatei.txt", Instant.now(), mutableList); + + // Versuch 1: Originalliste nach Übergabe verändern + mutableList.add(diagnosticError()); + assertEquals(1, report.getFindings().size(), + "Änderungen an der Eingabeliste dürfen den Report nicht beeinflussen."); + + // Versuch 2: Zurückgegebene Liste direkt verändern + assertThrows(UnsupportedOperationException.class, + () -> report.getFindings().add(specWarning()), + "getFindings() muss eine unveränderliche Liste zurückliefern."); + } + + // --------------------------------------------------------------------------- + // Test 7: operationalError(...) → OPERATIONAL_ERROR + // --------------------------------------------------------------------------- + + @Test + @DisplayName("operationalError-Factory liefert Verdict OPERATIONAL_ERROR") + void operationalErrorFactoryLiefertOPERATIONAL_ERROR() { + ValidationReport report = ValidationReport.operationalError( + "nichtvorhanden.txt", "SYS-001", "Eingabedatei nicht lesbar."); + + assertEquals(Verdict.OPERATIONAL_ERROR, report.computeVerdict(), + "operationalError() muss immer OPERATIONAL_ERROR liefern."); + assertFalse(report.getFindings().isEmpty(), + "Der Bedienfehler-Bericht muss mindestens einen Befund enthalten."); + assertEquals("nichtvorhanden.txt", report.getFileName()); + } + + // --------------------------------------------------------------------------- + // Zusätzliche Absicherung: DIAGNOSTIC-ERROR gemeinsam mit SPEC-ERROR + // --------------------------------------------------------------------------- + + @Test + @DisplayName("SPEC-ERROR + DIAGNOSTIC-ERROR: Verdict ist INVALID (wegen SPEC-ERROR)") + void specErrorUndDiagnosticError_liefertINVALID() { + ValidationReport report = new ValidationReport( + "testdatei.txt", Instant.now(), List.of(specError(), diagnosticError())); + + assertEquals(Verdict.INVALID, report.computeVerdict(), + "Wenn sowohl SPEC-ERROR als auch DIAGNOSTIC-ERROR vorliegen, muss INVALID gelten " + + "— aber nur wegen des SPEC-ERROR."); + } + + // --------------------------------------------------------------------------- + // Zusätzliche Absicherung: hasSpecErrors() + // --------------------------------------------------------------------------- + + @Test + @DisplayName("hasSpecErrors() gibt false zurück bei leerem Report") + void hasSpecErrors_leerReport() { + assertFalse(emptyReport().hasSpecErrors()); + } + + @Test + @DisplayName("hasSpecErrors() gibt true zurück bei SPEC-ERROR") + void hasSpecErrors_mitSpecError() { + ValidationReport report = new ValidationReport( + "testdatei.txt", Instant.now(), List.of(specError())); + assertTrue(report.hasSpecErrors()); + } + + @Test + @DisplayName("hasSpecErrors() gibt false zurück bei nur DIAGNOSTIC-ERROR") + void hasSpecErrors_nurDiagnosticError() { + ValidationReport report = new ValidationReport( + "testdatei.txt", Instant.now(), List.of(diagnosticError())); + assertFalse(report.hasSpecErrors(), + "DIAGNOSTIC-ERROR darf hasSpecErrors() nicht auf true setzen."); + } +} diff --git a/src/test/resources/log4j2-test.xml b/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..04be617 --- /dev/null +++ b/src/test/resources/log4j2-test.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/test-artefakte/m1/minimal.txt b/test-artefakte/m1/minimal.txt new file mode 100644 index 0000000..f5f99bf --- /dev/null +++ b/test-artefakte/m1/minimal.txt @@ -0,0 +1,5 @@ +ASV-Format-Validator Testdatei M1 +Minimale Eingabedatei fuer End-to-End-Abnahme +Kein gueltiges EDIFACT, keine echten ASV-Daten +Zeichensatz: ISO-8859-15 kompatibel (keine Sonderzeichen) +Lauf 1 erwartet Exit-Code 0 (GUELTIG mangels Pruefregel) diff --git a/test-artefakte/m1/minimal.txt.log b/test-artefakte/m1/minimal.txt.log new file mode 100644 index 0000000..d755d68 --- /dev/null +++ b/test-artefakte/m1/minimal.txt.log @@ -0,0 +1,4 @@ +2026-04-20 09:41:04 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - ASV-Format-Validator gestartet. Eingabedatei: D:\Dev\Projects\asv-format-validator\test-artefakte\m1\minimal.txt +2026-04-20 09:41:04 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'minimal.txt' gelesen (242 Bytes, 242 Zeichen, Encoding: ISO-8859-15) +2026-04-20 09:41:04 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: minimal.txt, Urteil: VALID +2026-04-20 09:41:04 [main] INFO de.gecheckt.asv.adapter.out.reporting.ReportFileWriter - Berichtdatei geschrieben: D:\Dev\Projects\asv-format-validator\test-artefakte\m1\minimal.txt.txt diff --git a/test-artefakte/m1/minimal.txt.txt b/test-artefakte/m1/minimal.txt.txt new file mode 100644 index 0000000..45e0429 --- /dev/null +++ b/test-artefakte/m1/minimal.txt.txt @@ -0,0 +1,13 @@ +================================================================ +ASV-Format-Validator – Prüfbericht +================================================================ +Zeitstempel : 2026-04-20T07:41:04.8214296Z +Eingabedatei: D:\Dev\Projects\asv-format-validator\test-artefakte\m1\minimal.txt +Urteil : GÜLTIG +---------------------------------------------------------------- +Keine Befunde. +---------------------------------------------------------------- +Hinweis: Dieser Bericht wurde mit dem M1-Platzhalter-Validator +erzeugt. Viele Prüfbereiche (Fachmodell, Inhalt, Referenzdaten) +werden erst ab M3 aktiv geprüft. +================================================================ diff --git a/test-artefakte/m1/minimal.txt_v1.log b/test-artefakte/m1/minimal.txt_v1.log new file mode 100644 index 0000000..99a7a28 --- /dev/null +++ b/test-artefakte/m1/minimal.txt_v1.log @@ -0,0 +1,4 @@ +2026-04-20 09:41:09 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - ASV-Format-Validator gestartet. Eingabedatei: D:\Dev\Projects\asv-format-validator\test-artefakte\m1\minimal.txt +2026-04-20 09:41:09 [main] INFO de.gecheckt.asv.application.DummyFileValidationService - M1-Dummy: Datei 'minimal.txt' gelesen (242 Bytes, 242 Zeichen, Encoding: ISO-8859-15) +2026-04-20 09:41:09 [main] INFO de.gecheckt.asv.adapter.in.cli.CliRunner - Validierung abgeschlossen. Datei: minimal.txt, Urteil: VALID +2026-04-20 09:41:09 [main] INFO de.gecheckt.asv.adapter.out.reporting.ReportFileWriter - Berichtdatei geschrieben: D:\Dev\Projects\asv-format-validator\test-artefakte\m1\minimal.txt_v1.txt diff --git a/test-artefakte/m1/minimal.txt_v1.txt b/test-artefakte/m1/minimal.txt_v1.txt new file mode 100644 index 0000000..4b14daf --- /dev/null +++ b/test-artefakte/m1/minimal.txt_v1.txt @@ -0,0 +1,13 @@ +================================================================ +ASV-Format-Validator – Prüfbericht +================================================================ +Zeitstempel : 2026-04-20T07:41:09.3312576Z +Eingabedatei: D:\Dev\Projects\asv-format-validator\test-artefakte\m1\minimal.txt +Urteil : GÜLTIG +---------------------------------------------------------------- +Keine Befunde. +---------------------------------------------------------------- +Hinweis: Dieser Bericht wurde mit dem M1-Platzhalter-Validator +erzeugt. Viele Prüfbereiche (Fachmodell, Inhalt, Referenzdaten) +werden erst ab M3 aktiv geprüft. +================================================================