Umsetzung von M1
This commit is contained in:
@@ -3,3 +3,5 @@
|
|||||||
.settings/
|
.settings/
|
||||||
target/
|
target/
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
|
logs/
|
||||||
|
dependency-reduced-pom.xml
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
|
---
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
# AP05 – Befundmodell mit Spec-/Diagnose-Trennung
|
# 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
|
## 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.
|
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
|
## Voraussetzungen
|
||||||
|
|
||||||
- AP03 (Paketstruktur)
|
- AP03 (Paketstruktur vorhanden)
|
||||||
|
- AP04 (Logging-Adapter)
|
||||||
|
|
||||||
## Scope IN
|
## 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)
|
### `Severity` (Enum)
|
||||||
- `ERROR`
|
- `ERROR`
|
||||||
@@ -21,88 +31,108 @@ Folgende Typen im Paket `de.gecheckt.asv.domain.finding` (oder ähnliches Unterp
|
|||||||
|
|
||||||
### `FindingKind` (Enum)
|
### `FindingKind` (Enum)
|
||||||
- `SPEC` — Befund ist Teil des Spec-Urteils, beeinflusst `Verdict`
|
- `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)
|
### `FindingLayer` (Enum)
|
||||||
- `ARTIFACT` — äußeres Artefakt / Dateiebene
|
- `ARTIFACT` — äußeres Artefakt / Dateiebene
|
||||||
- `TECHNICAL_STRUCTURE` — Service-Segmente, KKS, Transport
|
- `TECHNICAL_STRUCTURE` — Service-Segmente, KKS, Transport
|
||||||
- `DOMAIN_MODEL` — kanonisches Fachmodell (ASVREC/ASVFEH)
|
- `DOMAIN_MODEL` — kanonisches Fachmodell (ASVREC/ASVFEH)
|
||||||
|
|
||||||
### `Finding` (Record)
|
### `Finding` (Record oder unveränderliche Klasse)
|
||||||
|
|
||||||
|
Alle Pflichtfelder laut `technik-und-architektur.md` §„Befundarten":
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public record Finding(
|
public record Finding(
|
||||||
FindingKind kind, // SPEC oder DIAGNOSTIC
|
FindingKind kind, // SPEC oder DIAGNOSTIC
|
||||||
Severity severity, // ERROR/WARNING/HINT
|
Severity severity, // ERROR/WARNING/HINT
|
||||||
FindingLayer layer, // ARTIFACT/TECHNICAL_STRUCTURE/DOMAIN_MODEL
|
FindingLayer layer, // ARTIFACT/TECHNICAL_STRUCTURE/DOMAIN_MODEL
|
||||||
String ruleId, // interne Regel-ID, optional null
|
String ruleId, // interne Regel-ID, nullable
|
||||||
String officialErrorCode, // offizieller Fehlercode, optional null
|
String officialErrorCode, // offizieller Spec-Fehlercode, nullable
|
||||||
String segmentType, // z.B. "UNB", optional
|
String segmentType, // z.B. "UNB", nullable
|
||||||
Integer segmentIndex, // optional
|
Integer segmentIndex, // nullable
|
||||||
String fieldId, // z.B. "UNB_0020", optional
|
String fieldId, // z.B. "UNB_0020", nullable
|
||||||
String rawValue, // Rohwert, optional
|
String rawValue, // Rohwert, nullable
|
||||||
Integer position, // Byte-/Zeichenposition, optional
|
Integer position, // Byte-/Zeichenposition, nullable
|
||||||
String messageReference, // UNH 0062 bei Nachrichtenbezug, optional
|
String messageReference, // UNH 0062 bei Nachrichtenbezug, nullable
|
||||||
String germanMessage // deutscher Befundtext
|
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)
|
### `Verdict` (Enum)
|
||||||
- `VALID` — keine Spec-ERROR-Befunde
|
- `VALID` — keine SPEC-ERROR-Befunde
|
||||||
- `INVALID` — mindestens ein Spec-ERROR-Befund
|
- `INVALID` — mindestens ein SPEC-ERROR-Befund
|
||||||
- `OPERATIONAL_ERROR` — Bedienfehler (Exit-Code 2)
|
- `OPERATIONAL_ERROR` — Bedienfehler (Exit-Code 2), wird in AP08 genutzt
|
||||||
|
|
||||||
### `ValidationReport` (Klasse)
|
### `ValidationReport` (unveränderliche Klasse)
|
||||||
- unveränderlich
|
|
||||||
- enthält:
|
|
||||||
- `List<Finding> findings` (alle Befunde, SPEC und DIAGNOSTIC gemischt, Reihenfolge erhalten)
|
|
||||||
- Methode `Verdict computeVerdict()` — berücksichtigt **nur** `kind == SPEC` und `severity == ERROR`
|
|
||||||
- Methode `List<Finding> specFindings()` — filtert auf `kind == SPEC`
|
|
||||||
- Methode `List<Finding> 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.
|
|
||||||
|
|
||||||
### Unit-Tests
|
```java
|
||||||
- `ValidationReport` ohne Befunde → `Verdict.VALID`
|
public final class ValidationReport {
|
||||||
- `ValidationReport` mit einem SPEC-ERROR → `Verdict.INVALID`
|
// Metadaten
|
||||||
- `ValidationReport` mit einem DIAGNOSTIC-ERROR → `Verdict.VALID` (dieser Test ist kritisch!)
|
String fileName;
|
||||||
- `ValidationReport` mit SPEC-WARNING → `Verdict.VALID` (nur ERROR zählt)
|
Instant timestamp;
|
||||||
- `specFindings()` / `diagnosticFindings()` filtern korrekt
|
// Befunde
|
||||||
- Unveränderlichkeit: `findings`-Liste ist nicht modifizierbar
|
List<Finding> findings; // unveränderlich
|
||||||
|
|
||||||
|
// Kern-Methoden
|
||||||
|
Verdict computeVerdict(); // NUR SPEC+ERROR zählt
|
||||||
|
boolean hasSpecErrors();
|
||||||
|
List<Finding> specFindings();
|
||||||
|
List<Finding> 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
|
## Scope OUT
|
||||||
|
|
||||||
- Integration des neuen Modells in den bestehenden Lauf (kommt in AP06)
|
- Integration des neuen Modells in den bestehenden Lauf (AP06)
|
||||||
- Löschen oder Umbenennen der alten `validation.model.ValidationResult`-Klasse (das ist Teil von AP09)
|
- Löschen oder Umbenennen der alten `ValidationResult`-Klasse (AP09)
|
||||||
- Berichtserzeugung (Text-Rendering), Bericht-Format, Konsolenausgabe (kommt in AP07)
|
- Berichtserzeugung, Textrendering, Konsolenausgabe (AP07)
|
||||||
- Architekturtest (kommt in AP10)
|
- Architekturtest (AP10)
|
||||||
|
|
||||||
## Schritte
|
## Schritte
|
||||||
|
|
||||||
1. Branch `m1/ap05-befundmodell`
|
1. Paket `de.gecheckt.asv.domain.finding` anlegen
|
||||||
2. Paket `de.gecheckt.asv.domain.finding` anlegen
|
2. Enums `Severity`, `FindingKind`, `FindingLayer` implementieren
|
||||||
3. Enums und Klassen implementieren
|
3. `Finding` implementieren
|
||||||
4. Unit-Tests schreiben, mindestens die sechs oben genannten
|
4. `Verdict` implementieren
|
||||||
5. `mvn clean verify` grün bekommen
|
5. `ValidationReport` implementieren
|
||||||
6. Commit `M1-AP05: Befundmodell mit Spec-/Diagnose-Trennung`
|
6. Unit-Tests schreiben — mindestens die sieben oben genannten
|
||||||
7. Abschlussbericht schreiben
|
7. `mvn clean verify` grün bekommen
|
||||||
|
8. Abschlussbericht schreiben
|
||||||
|
|
||||||
## Abnahmekriterien
|
## Abnahmekriterien
|
||||||
|
|
||||||
- Paket `domain.finding` enthält alle oben genannten Typen
|
- [ ] Paket `domain.finding` enthält alle genannten Typen
|
||||||
- **Der Test „DIAGNOSTIC-ERROR ergibt VALID" ist grün** und wird im Bericht explizit genannt
|
- [ ] **Test „DIAGNOSTIC-ERROR ergibt VALID" ist grün** und im Bericht explizit genannt
|
||||||
- `ValidationReport.findings` ist unveränderlich (Test vorhanden)
|
- [ ] `ValidationReport.findings` ist unveränderlich (Test vorhanden)
|
||||||
- alle Metadaten-Felder aus `technik-und-architektur.md` Abschnitt „Befundarten" sind im `Finding`-Typ vorhanden
|
- [ ] Alle Metadatenfelder aus `technik-und-architektur.md` §„Befundarten" sind im `Finding`-Typ vorhanden
|
||||||
- `mvn clean verify` ist grün
|
- [ ] `operationalError(...)` Factory-Methode existiert
|
||||||
- keine Änderung an `validation.model.ValidationResult` (Altmodell)
|
- [ ] Keine Änderung an `ValidationResult` (Altmodell)
|
||||||
- Abschlussbericht liegt vor
|
- [ ] `mvn clean verify` grün
|
||||||
|
- [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP05-bericht.md`
|
||||||
|
|
||||||
## Rest-Risiken und offene Punkte
|
## 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.
|
- Zwei parallele Ergebnistypen (`ValidationResult` alt, `ValidationReport` neu) sind bis AP09 Absicht.
|
||||||
- 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.
|
- Das Befundmodell ist bewusst eine **flache Liste** mit Metadaten, keine Hierarchie. Die hierarchische Berichtserzeugung kommt in M9.
|
||||||
|
|
||||||
## Bericht
|
## 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`.
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
|
---
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
# AP06 – Bootstrap und CLI-Adapter
|
# 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
|
## Ziel
|
||||||
|
|
||||||
Die bestehende `AsvValidatorApplication` wird in zwei klar getrennte Verantwortlichkeiten zerlegt:
|
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`.
|
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` oder ähnlich) — nimmt CLI-Argumente entgegen, ruft die Application-Schicht auf, übersetzt das Ergebnis in einen Exit-Code.
|
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
|
## Voraussetzungen
|
||||||
|
|
||||||
@@ -15,82 +24,146 @@ Zusätzlich werden die **Exit-Codes spec-konform** auf `0/1/2` umgestellt.
|
|||||||
|
|
||||||
## Scope IN
|
## Scope IN
|
||||||
|
|
||||||
### Bootstrap
|
### Bootstrap (`de.gecheckt.asv.bootstrap.Main`)
|
||||||
- Klasse `de.gecheckt.asv.bootstrap.Main` mit `public static void main(String[] args)`
|
|
||||||
- verdrahtet manuell:
|
- `public static void main(String[] args)`
|
||||||
- Logging-Konfigurator
|
- Verdrahtet manuell per Constructor Injection:
|
||||||
- CLI-Runner
|
- `LoggingConfigurator`
|
||||||
- (Application-Service — Platzhalter, wird in AP09 feingeschnitten)
|
- `CliRunner`
|
||||||
- ruft `CliRunner.run(args)` auf, gibt den zurückgegebenen Exit-Code an `System.exit` weiter
|
- 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)`
|
- Methode `int run(String[] args)`
|
||||||
- akzeptiert **genau ein Positionsargument**: den Pfad zur Eingabedatei
|
- Akzeptiert **genau ein Positionsargument**: Pfad zur Eingabedatei
|
||||||
- bei 0 oder ≥2 Argumenten → Exit-Code `2`, Minimalbericht-Vorbereitung (vollständige Minimalbericht-Logik kommt in AP08)
|
- 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 nicht existierender, nicht lesbarer oder nicht regulärer Eingabedatei → Exit-Code `2`
|
||||||
- bei erfolgreichem Lauf ohne Spec-Fehler → Exit-Code `0`
|
- Bei erfolgreichem Lauf ohne Spec-Fehler → Exit-Code `0`
|
||||||
- bei erfolgreichem Lauf mit Spec-Fehlern → Exit-Code `1`
|
- Bei erfolgreichem Lauf mit Spec-Fehlern → Exit-Code `1`
|
||||||
|
|
||||||
### Exit-Code-Konstanten
|
### Exit-Code-Konstanten
|
||||||
- Konstanten **nur noch in einer Klasse** (z.B. `ExitCode` im Paket `adapter.in.cli`)
|
|
||||||
- Werte:
|
Konstanten **nur noch in einer Klasse** (z.B. `ExitCode` im Paket `adapter.in.cli`):
|
||||||
- `0` = gültig, keine Fehler-Befunde
|
|
||||||
- `1` = ungültig, mindestens ein Spec-Fehler
|
```java
|
||||||
- `2` = Bedienfehler
|
public final class ExitCode {
|
||||||
- Die alten Konstanten (`EXIT_CODE_INVALID_ARGUMENTS=1`, `EXIT_CODE_FILE_ERROR=2`, `EXIT_CODE_VALIDATION_ERRORS=3`) werden **gelöscht**
|
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)
|
### 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
|
### 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
|
Beim Einlesen der Eingabedatei wird **ISO-8859-15** verwendet:
|
||||||
Files.readString(path, StandardCharsets.ISO_8859_1); // nicht ideal — besser Charset.forName("ISO-8859-15")
|
|
||||||
```
|
```java
|
||||||
Achtung: Java kennt `StandardCharsets.ISO_8859_1`, aber **nicht** `ISO_8859_15`. Daher `Charset.forName("ISO-8859-15")` verwenden.
|
// 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
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version><!-- aktuell stabil --></version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals><goal>shade</goal></goals>
|
||||||
|
<configuration>
|
||||||
|
<transformers>
|
||||||
|
<transformer implementation=
|
||||||
|
"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<mainClass>de.gecheckt.asv.bootstrap.Main</mainClass>
|
||||||
|
</transformer>
|
||||||
|
<!-- Log4j2 PluginCache zusammenführen -->
|
||||||
|
<transformer implementation=
|
||||||
|
"org.apache.maven.plugins.shade.resource.Log4j2PluginsCacheFileTransformer"/>
|
||||||
|
</transformers>
|
||||||
|
<filters>
|
||||||
|
<filter>
|
||||||
|
<artifact>*:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>META-INF/*.SF</exclude>
|
||||||
|
<exclude>META-INF/*.DSA</exclude>
|
||||||
|
<exclude>META-INF/*.RSA</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
</filters>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
```
|
||||||
|
|
||||||
|
`java -jar target/asv-format-validator-*.jar <datei>` 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
|
## Scope OUT
|
||||||
|
|
||||||
- Berichtdatei und Log-Datei im Eingabeverzeichnis (AP07)
|
- 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)
|
- Entkopplung der Altlogik (AP09)
|
||||||
- Architekturtest (AP10)
|
- Architekturtest (AP10)
|
||||||
|
|
||||||
## Schritte
|
## Schritte
|
||||||
|
|
||||||
1. Branch `m1/ap06-bootstrap-cli`
|
1. `de.gecheckt.asv.bootstrap.Main` anlegen
|
||||||
2. `de.gecheckt.asv.bootstrap.Main` anlegen mit `public static void main`
|
2. `CliRunner` in `adapter.in.cli` anlegen
|
||||||
3. `CliRunner` in `adapter.in.cli` anlegen
|
3. `ExitCode`-Konstantenklasse anlegen
|
||||||
4. `ExitCode`-Konstantenklasse anlegen
|
4. `AsvValidatorApplication` schrittweise entkernen: Code wandert nach `CliRunner` und `Main`
|
||||||
5. `AsvValidatorApplication` schrittweise entkernen: Code wandert nach `CliRunner` und `Main`
|
5. Einlese-Encoding auf `Charset.forName("ISO-8859-15")` umstellen
|
||||||
6. Einlese-Encoding auf `Charset.forName("ISO-8859-15")` umstellen
|
6. `maven-shade-plugin` in `pom.xml` einbinden, `maven-jar-plugin`-Platzhalter entfernen
|
||||||
7. `maven-jar-plugin` in `pom.xml` auf `de.gecheckt.asv.bootstrap.Main` setzen (Platzhalter aus AP02 konkretisieren)
|
7. `logs/` in `.gitignore` ergänzen
|
||||||
8. Alle Tests, die auf `AsvValidatorApplication` direkt zeigen, auf `CliRunner` umziehen
|
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 <test-datei>` prüfen
|
9. `mvn clean package` — erzeugtes JAR mit `java -jar target/asv-format-validator-*.jar <testdatei>` manuell prüfen
|
||||||
10. `mvn clean verify` grün bekommen
|
10. `mvn clean verify` grün bekommen
|
||||||
11. Commit `M1-AP06: Bootstrap, CLI-Adapter, Exit-Codes 0/1/2, ISO 8859-15`
|
11. Abschlussbericht schreiben
|
||||||
12. Abschlussbericht schreiben
|
|
||||||
|
|
||||||
## Abnahmekriterien
|
## Abnahmekriterien
|
||||||
|
|
||||||
- `de.gecheckt.asv.bootstrap.Main` existiert und ist `Main-Class` des JAR
|
- [ ] `de.gecheckt.asv.bootstrap.Main` existiert und ist `Main-Class` des Uber-JAR
|
||||||
- `CliRunner` ist der einzige Ort mit CLI-Argument-Parsing
|
- [ ] `CliRunner` ist der einzige Ort mit CLI-Argument-Parsing
|
||||||
- Exit-Codes `0`, `1`, `2` sind definiert und spec-konform eingesetzt
|
- [ ] 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 ohne Argument → Exit-Code `2`
|
||||||
- **Test:** Aufruf mit nicht existierender Datei → 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`)
|
- [ ] Test: Aufruf mit leerer, lesbarer Datei → Exit-Code `0`
|
||||||
- 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)
|
- [ ] Einlese-Encoding ist ISO-8859-15 (per Test belegt: Byte `0xA4` → `€`)
|
||||||
- ausführbares JAR unter `target/` ist manuell startbar
|
- [ ] `java -jar target/asv-format-validator-*.jar <datei>` startet ohne `-cp`
|
||||||
- `mvn clean verify` ist grün
|
- [ ] `logs/` in `.gitignore`
|
||||||
- Abschlussbericht liegt vor
|
- [ ] 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
|
## 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.
|
- Der Dummy-Pfad (Datei lesen, leerer Report) ist bewusst dünn. Echte Parser-/Validator-Einbindung kommt in M3+.
|
||||||
- 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.
|
- 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
|
## 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`.
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
|
---
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
# AP07 – Ausgabeartefakte: Berichtdatei und Log-Datei mit Suffix-Logik
|
# 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
|
## Ziel
|
||||||
|
|
||||||
Pro Lauf werden **zwei Ausgabedateien** im **Verzeichnis der Eingabedatei** erzeugt:
|
Pro Lauf werden **zwei Ausgabedateien** im **Verzeichnis der Eingabedatei** erzeugt:
|
||||||
- eine **Berichtdatei** `<basename>.txt`
|
- eine **Berichtdatei** `<basename>.txt`
|
||||||
- eine **Log-Datei** `<basename>.log`
|
- eine **Log-Datei** `<basename>.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
|
## Voraussetzungen
|
||||||
|
|
||||||
@@ -14,69 +23,96 @@ Beide in **UTF-8**. Zusätzlich wird der Bericht weiterhin in die **Konsole** ge
|
|||||||
|
|
||||||
## Scope IN
|
## Scope IN
|
||||||
|
|
||||||
### Berichtdatei
|
### `SuffixResolver` im Paket `adapter.out.filesystem`
|
||||||
- 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: `<basename-der-eingabedatei>.txt`, bei Konflikt `<basename>_v1.txt`, `<basename>_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.
|
|
||||||
|
|
||||||
### Log-Datei
|
```java
|
||||||
- Wiederverwendung des `LoggingConfigurator` aus AP04
|
public class SuffixResolver {
|
||||||
- Methode `configureLogFile(Path logFile)` wird im Bootstrap **vor** dem CLI-Runner aufgerufen
|
/**
|
||||||
- Log-Datei liegt im **selben Verzeichnis** wie die Eingabedatei
|
* Ermittelt den ersten freien Dateipfad für den gegebenen Basisnamen
|
||||||
- Dateiname: `<basename>.log`, Suffix-Logik analog zur Berichtdatei
|
* und die gegebene Extension im Zielverzeichnis.
|
||||||
- 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".
|
* Probiert: <baseName>.<ext>, dann <baseName>_v1.<ext>, <baseName>_v2.<ext>, ...
|
||||||
|
*/
|
||||||
|
public Path resolveNextFreePath(Path directory, String baseName, String extension) { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Suffix-Logik
|
- Suffix-Zählung ist **pro Extension unabhängig** — `.txt` und `.log` haben getrennte Zähler
|
||||||
- eigene kleine Utility-Klasse `SuffixResolver` im Paket `adapter.out.filesystem`
|
- Unit-Tests mindestens für: keine Datei vorhanden, `.txt` vorhanden, `.txt` + `_v1` vorhanden
|
||||||
- Methode `Path resolveNextFreePath(Path baseDirectory, String baseName, String extension)`:
|
|
||||||
- probiert `<baseName>.<ext>`, dann `<baseName>_v1.<ext>`, `<baseName>_v2.<ext>`, …
|
### `ReportFileWriter` im Paket `adapter.out.reporting`
|
||||||
- gibt den ersten freien Pfad zurück
|
|
||||||
- wird sowohl für die Berichtdatei als auch für die Log-Datei verwendet
|
- Eingabe: `ValidationReport` (AP05) + Eingabedatei-Pfad
|
||||||
- **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.
|
- 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
|
### 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
|
## Scope OUT
|
||||||
|
|
||||||
- hierarchische Berichtsgliederung (M9)
|
- Hierarchische Berichtsgliederung (M9)
|
||||||
- Einfärbung / ANSI-Codes in der Konsole
|
- ANSI-Farben / Einfärbung in der Konsole
|
||||||
- Log-Rotation
|
- Log-Rotation
|
||||||
- Minimalbericht bei Exit-Code 2 (AP08)
|
- Minimalbericht bei Exit-Code `2` (AP08)
|
||||||
|
|
||||||
## Schritte
|
## Schritte
|
||||||
|
|
||||||
1. Branch `m1/ap07-ausgabeartefakte`
|
1. `SuffixResolver` implementieren inkl. Unit-Tests
|
||||||
2. `SuffixResolver` implementieren inkl. Unit-Tests für: keine Datei vorhanden, `.txt` vorhanden, `.txt` + `_v1` vorhanden, mehrere Lücken
|
2. `ReportFileWriter` implementieren mit einfachem Zeilenformat für M1
|
||||||
3. `ReportFileWriter` implementieren mit einfachem Zeilenformat für M1
|
3. `LoggingConfigurator.configureLogFile(Path)` implementieren
|
||||||
4. `LoggingConfigurator.configureLogFile(Path)` implementieren (programmatische Log4j2-Reconfiguration)
|
4. Bootstrap erweitern: Log-Datei-Pfad bestimmen → `LoggingConfigurator` aufrufen
|
||||||
5. Bootstrap erweitern: vor CLI-Lauf → Log-Datei-Pfad bestimmen → `LoggingConfigurator` aufrufen
|
5. `CliRunner` erweitern: nach Lauf → Berichtdatei schreiben → Konsolenausgabe
|
||||||
6. CLI-Runner erweitert: nach Lauf → Berichtdatei schreiben → Konsolenausgabe erzeugen
|
6. End-to-End-Test: beide Ausgabedateien entstehen, Suffix-Logik funktioniert, UTF-8
|
||||||
7. End-to-End-Test mit Dummy-Eingabedatei: beide Ausgabedateien entstehen, Suffix-Logik funktioniert
|
7. `mvn clean verify` grün bekommen
|
||||||
8. `mvn clean verify` grün
|
8. Abschlussbericht schreiben
|
||||||
9. Commit `M1-AP07: Berichtdatei und Log-Datei im Eingabeverzeichnis mit Suffix-Logik`
|
|
||||||
10. Abschlussbericht schreiben
|
|
||||||
|
|
||||||
## Abnahmekriterien
|
## Abnahmekriterien
|
||||||
|
|
||||||
- nach einem Lauf mit Eingabedatei `foo/bar.auf` existieren `foo/bar.auf.txt` und `foo/bar.auf.log`
|
- [ ] Nach Lauf mit `foo/bar.auf` entstehen `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`
|
- [ ] Zweiter Lauf mit derselben Datei → `foo/bar.auf_v1.txt` und `foo/bar.auf_v1.log`
|
||||||
- dritter Lauf → `_v2` usw.
|
- [ ] Dritter Lauf → `_v2` usw.
|
||||||
- beide Ausgabedateien sind UTF-8
|
- [ ] Beide Ausgabedateien sind UTF-8
|
||||||
- Konsolenausgabe ist identisch zum Inhalt der Berichtdatei
|
- [ ] Konsolenausgabe ist identisch zum Inhalt der Berichtdatei
|
||||||
- `SuffixResolver` hat mindestens vier Unit-Tests
|
- [ ] `SuffixResolver` hat mindestens drei Unit-Tests
|
||||||
- `mvn clean verify` ist grün
|
- [ ] Log4j2-Typen sind außerhalb von `adapter.out.logging` und `bootstrap` nicht sichtbar
|
||||||
- Abschlussbericht liegt vor
|
- [ ] 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
|
## 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).
|
- 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.
|
|
||||||
- Das Bericht-Dateiformat ist in M1 absichtlich primitiv. In M9 wird es durch die finale hierarchische Struktur ersetzt.
|
- 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
|
## 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`.
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
|
---
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
# AP08 – Minimalbericht bei Bedienfehlern (Exit-Code 2)
|
# 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
|
## 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
|
## Voraussetzungen
|
||||||
|
|
||||||
@@ -10,71 +18,69 @@ Auch bei **Bedien- oder Zugriffsfehlern** (Exit-Code `2`) soll ein **Minimalberi
|
|||||||
|
|
||||||
## Scope IN
|
## Scope IN
|
||||||
|
|
||||||
### Bedienfehler-Fälle
|
### Bedienfehler-Fälle (alle müssen Exit-Code `2` + Minimalbericht ergeben)
|
||||||
Alle diese Fälle müssen in Exit-Code `2` mit Minimalbericht resultieren:
|
|
||||||
|
|
||||||
1. **Kein Argument übergeben** → Minimalbericht auf Konsole (Eingabeverzeichnis unbekannt, also keine Dateiausgabe möglich)
|
1. **Kein Argument** → nur Konsolenausgabe (Eingabeverzeichnis unbekannt)
|
||||||
2. **Mehr als ein Argument** → Minimalbericht auf Konsole
|
2. **Mehr als ein Argument** → nur Konsolenausgabe
|
||||||
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
|
3. **Eingabedatei existiert nicht** → Konsolenausgabe + Berichtdatei im übergeordneten Verzeichnis, sofern dieses schreibbar ist
|
||||||
4. **Eingabepfad ist kein regulärer Dateityp** (z.B. Verzeichnis) → Minimalbericht auf Konsole, keine Dateiausgabe
|
4. **Eingabepfad ist kein regulärer Dateityp** (z.B. Verzeichnis) → nur Konsolenausgabe
|
||||||
5. **Eingabedatei ist nicht lesbar** (Permissions) → Minimalbericht auf Konsole, Dateiausgabe wird versucht wenn Zielverzeichnis schreibbar ist, sonst nur Konsole
|
5. **Eingabedatei ist nicht lesbar** (Berechtigungen) → Konsolenausgabe + Berichtdatei sofern Zielverzeichnis schreibbar
|
||||||
|
|
||||||
### Minimalbericht-Inhalt
|
### Minimalbericht-Inhalt
|
||||||
Der Minimalbericht ist ein **`ValidationReport`** (aus AP05) mit:
|
|
||||||
- `fileName` = übergebener Pfad (oder Platzhalter `<kein Argument>` bzw. `<mehrere Argumente>`)
|
Ein `ValidationReport` (AP05) via `ValidationReport.operationalError(...)` mit:
|
||||||
|
- `fileName` = übergebener Pfad (oder `<kein Argument>` / `<mehrere Argumente>`)
|
||||||
- `timestamp` = jetzt
|
- `timestamp` = jetzt
|
||||||
- genau ein `Finding`:
|
- Genau ein `Finding`:
|
||||||
- `kind = SPEC`
|
- `kind = SPEC` → Verdict wird `OPERATIONAL_ERROR`
|
||||||
- `severity = ERROR`
|
- `severity = ERROR`
|
||||||
- `layer = ARTIFACT`
|
- `layer = ARTIFACT`
|
||||||
- `ruleId = "OPERATIONAL-<fallkennung>"` (z.B. `OPERATIONAL-MISSING-ARG`, `OPERATIONAL-FILE-NOT-FOUND`, `OPERATIONAL-NOT-READABLE`, `OPERATIONAL-NOT-REGULAR`, `OPERATIONAL-TOO-MANY-ARGS`)
|
- `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
|
- `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.
|
|
||||||
|
|
||||||
### Dateiausgabe bei Exit-Code 2
|
### Wichtige Regeln
|
||||||
- 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."
|
|
||||||
|
|
||||||
### Logging
|
- **Kein Stack-Trace für den Nutzer** — technische Details gehören ins Log, nicht in den Bericht
|
||||||
- der Minimalbericht wird zusätzlich **geloggt** (`logger.error(...)`) — damit in der Log-Datei (sofern erzeugt) dokumentiert ist, was schiefging
|
- Wenn Zielverzeichnis nicht schreibbar: **nur Konsolenausgabe**, kein Fehler-auf-Fehler
|
||||||
- bei Fall 1 und 2 (keine Dateipfadinformation) ist die Log-Datei nicht sinnvoll zu platzieren → Fallback auf `log4j2.xml`-Default aus AP04
|
- 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
|
## 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
|
- Internationalisierung
|
||||||
- Exit-Codes jenseits von `0/1/2`
|
- Exit-Codes jenseits von `0/1/2`
|
||||||
- Behandlung von `OutOfMemoryError`, `StackOverflowError` etc.
|
|
||||||
|
|
||||||
## Schritte
|
## Schritte
|
||||||
|
|
||||||
1. Branch `m1/ap08-minimalbericht`
|
1. `ValidationReport.operationalError(...)` prüfen — falls noch nicht vollständig in AP05 implementiert, hier ergänzen
|
||||||
2. Factory-Methode `ValidationReport.operationalError(...)` in AP05-Modell ergänzen (falls noch nicht vorhanden)
|
2. `CliRunner` um alle fünf Bedienfehler-Fälle erweitern
|
||||||
3. `CliRunner` um die fünf Bedienfehler-Fälle erweitern; pro Fall wird der passende Minimalbericht erzeugt
|
3. `ReportFileWriter`: im `OPERATIONAL_ERROR`-Fall weichere IO-Fehlerbehandlung (kein `RuntimeException`, stattdessen Konsolenhinweis)
|
||||||
4. `ReportFileWriter`: im OPERATIONAL-Fall weichere IO-Fehlerbehandlung (keine `RuntimeException`, stattdessen Konsolenhinweis)
|
4. Unit-Tests für alle fünf Fälle
|
||||||
5. Unit-Tests für alle fünf Fälle
|
5. `mvn clean verify` grün bekommen
|
||||||
6. End-to-End-Test: `java -jar ... /pfad/zu/nichtvorhandener/datei.auf` erzeugt auf Konsole einen Minimalbericht und Exit-Code `2`
|
6. Abschlussbericht schreiben
|
||||||
7. `mvn clean verify` grün
|
|
||||||
8. Commit `M1-AP08: Minimalbericht bei Exit-Code 2`
|
|
||||||
9. Abschlussbericht schreiben
|
|
||||||
|
|
||||||
## Abnahmekriterien
|
## Abnahmekriterien
|
||||||
|
|
||||||
- alle fünf Bedienfehler-Fälle erzeugen einen Minimalbericht (per Unit-Test belegt)
|
- [ ] Alle fünf Bedienfehler-Fälle erzeugen Exit-Code `2` (per Unit-Test belegt)
|
||||||
- Exit-Code in allen fünf Fällen ist `2`
|
- [ ] Fall „kein Argument" → **nur** Konsolenausgabe, keine Dateiausgabe
|
||||||
- Im Fall „Eingabedatei existiert nicht" wird der Minimalbericht in das übergeordnete Verzeichnis geschrieben, sofern dieses schreibbar ist
|
- [ ] Fall „Datei nicht vorhanden" → Berichtdatei im übergeordneten Verzeichnis, sofern schreibbar
|
||||||
- Im Fall „kein Argument" wird der Minimalbericht **nur** auf Konsole ausgegeben (keine Dateiausgabe)
|
- [ ] `Verdict.OPERATIONAL_ERROR` ist in mindestens einem Test verifiziert
|
||||||
- `Verdict.OPERATIONAL_ERROR` ist in mindestens einem Test verifiziert
|
- [ ] Kein Stack-Trace in STDERR (Negativ-Test vorhanden)
|
||||||
- `mvn clean verify` ist grün
|
- [ ] `mvn clean verify` grün
|
||||||
- Abschlussbericht liegt vor
|
- [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP08-bericht.md`
|
||||||
|
|
||||||
## Rest-Risiken und offene Punkte
|
## 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.
|
- 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.
|
||||||
- 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.
|
- 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
|
## 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`.
|
||||||
|
|||||||
@@ -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
|
## 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
|
## Voraussetzungen
|
||||||
|
|
||||||
- AP03 (Migration), AP05 (neues Befundmodell), AP06 (neuer Bootstrap/CLI)
|
- AP03 (Paketstruktur)
|
||||||
|
- AP05 (neues Befundmodell)
|
||||||
|
- AP06 (neuer Bootstrap/CLI)
|
||||||
|
|
||||||
## Scope IN
|
## Scope IN
|
||||||
|
|
||||||
### Einfrieren statt Löschen
|
### 1. Bootstrap-Verdrahtung anpassen
|
||||||
- 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.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Entkopplung vom Lauf
|
In `bootstrap.Main`: `DefaultInputFileValidator` erhält statt `DefaultStructureValidator` und `DefaultFieldValidator` jeweils eine **Null-Implementation** — eine leere Implementierung der jeweiligen Interfaces, die keine Befunde produziert.
|
||||||
- `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
|
|
||||||
|
|
||||||
### Saubere Kennzeichnung
|
Die Null-Implementierungen können als benannte Klassen in `bootstrap` oder `application` angelegt werden:
|
||||||
- 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.
|
```java
|
||||||
|
/** M1-Platzhalter. Ab M3 durch DefaultStructureValidator ersetzen. */
|
||||||
|
public final class NoOpStructureValidator implements StructureValidator {
|
||||||
|
@Override
|
||||||
|
public List<Finding> 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
|
## Scope OUT
|
||||||
|
|
||||||
- Weiterentwicklung der Preview-Klassen
|
- Paketumzug der Preview-Klassen (explizit **nicht** — Entscheidung E-01 Option b)
|
||||||
- Änderung der Preview-Tests (außer notwendige Import-Anpassungen durch den Package-Umzug)
|
- Inhaltliche Änderung an `DefaultStructureValidator` oder `DefaultFieldValidator`
|
||||||
- Integration der Preview-Klassen in die neue `domain.finding`-Struktur (das ist explizit M3+)
|
- Fachliche Neubewertung der 19 Preview-Regeln (das ist M3)
|
||||||
- Löschung von Preview-Klassen, auch wenn sie wie Duplikate wirken
|
- Löschen von Preview-Klassen
|
||||||
|
|
||||||
## Schritte
|
## Schritte
|
||||||
|
|
||||||
1. Branch `m1/ap09-preview-einfrieren`
|
1. `NoOpStructureValidator` und `NoOpFieldValidator` anlegen
|
||||||
2. Zielpaket wählen (`preview` empfohlen) und im Bericht begründen
|
2. `bootstrap.Main` umverdrahten: Preview-Validatoren durch NoOp-Implementierungen ersetzen
|
||||||
3. Alle Parser-Klassen verschieben
|
3. JavaDoc-Einfriermarker in `DefaultStructureValidator` und `DefaultFieldValidator` ergänzen
|
||||||
4. Alle Validator-Klassen verschieben
|
4. `DefaultStructureValidatorTestAdditional` löschen
|
||||||
5. `validation.model.ValidationResult` und `validation.model.*` mit verschieben
|
5. Grep-Nachweis ausführen und im Bericht dokumentieren
|
||||||
6. Tests entsprechend verschieben; Imports anpassen
|
6. Integrationstest: Lauf erzeugt keine ASVREC-/ASVFEH-Segmentbefunde
|
||||||
7. `CliRunner`/`Bootstrap` auf Preview-Imports prüfen — **darf keine haben**, sonst entkoppeln
|
7. `mvn clean verify` grün — alle bisherigen Tests der Preview-Klassen müssen weiterhin grün sein
|
||||||
8. `package-info.java` mit Warnhinweis in jedem Preview-Unterpaket anlegen
|
8. Abschlussbericht schreiben
|
||||||
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
|
|
||||||
|
|
||||||
## Abnahmekriterien
|
## Abnahmekriterien
|
||||||
|
|
||||||
- alle ursprünglich vorhandenen Parser- und Validator-Klassen liegen im Preview-Paket
|
- [ ] `NoOpStructureValidator` und `NoOpFieldValidator` existieren
|
||||||
- alle zugehörigen Tests laufen weiterhin grün
|
- [ ] `bootstrap.Main` verdrahtet keine Preview-Validatoren mehr
|
||||||
- `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)
|
- [ ] Grep auf `DefaultStructureValidator`/`DefaultFieldValidator` in `adapter` und `bootstrap` ist leer (Nachweis im Bericht)
|
||||||
- `package-info.java` mit Warnhinweis in jedem Preview-Unterpaket
|
- [ ] Einfriermarker-JavaDoc in beiden Preview-Klassen vorhanden
|
||||||
- README enthält Abschnitt „Preview-Code"
|
- [ ] `DefaultStructureValidatorTestAdditional` ist gelöscht
|
||||||
- keine Klasse wurde gelöscht (`git log --diff-filter=D` für diesen Commit zeigt nur Verschiebungen)
|
- [ ] Bestehende Tests der Preview-Klassen laufen weiterhin grün
|
||||||
- `mvn clean verify` ist grün
|
- [ ] Integrationstest: Lauf mit Testdatei erzeugt keine ASVREC-/ASVFEH-Segmentbefunde
|
||||||
- Abschlussbericht liegt vor
|
- [ ] `mvn clean verify` grün
|
||||||
|
- [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP09-bericht.md`
|
||||||
|
|
||||||
## Rest-Risiken und offene Punkte
|
## 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.
|
- Bei Wiederaufnahme in M3: jede der 19 Preview-Regeln ist neu gegen V1-V/T/N/K-Klassifikation zu bewerten. Explizit M3-Aufgabe.
|
||||||
- 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.
|
- 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
|
## 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`.
|
||||||
|
|||||||
@@ -1,26 +1,42 @@
|
|||||||
|
---
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
# AP10 – Architekturtest
|
# 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
|
## Ziel
|
||||||
|
|
||||||
Ein **automatisierter Architekturtest** stellt sicher, dass die in M1 etablierten Strukturregeln auch in Zukunft eingehalten werden. Insbesondere darf:
|
Automatisierte Architekturtests sichern die in M1 etablierten Strukturregeln dauerhaft ab. Zusätzlich wird das Build-Rauschen durch ERROR-Log-Zeilen in Negativ-Tests beseitigt.
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
- AP04 (Logging-Adapter), AP09 (Preview eingefroren)
|
- AP04 (Logging-Adapter etabliert)
|
||||||
|
- AP09 (Preview-Code eingefroren)
|
||||||
|
- Idealerweise alle AP05–AP09 abgeschlossen
|
||||||
|
|
||||||
## Scope IN
|
## Scope IN
|
||||||
|
|
||||||
### Technische Umsetzung
|
### 1. ArchUnit als Test-Dependency
|
||||||
- **ArchUnit** (`com.tngtech.archunit:archunit-junit5`) als Test-Dependency aufnehmen
|
|
||||||
- neue Test-Klasse `ArchitectureTest` im Paket `de.gecheckt.asv` im Testbereich
|
|
||||||
- vier Tests:
|
|
||||||
|
|
||||||
### Test 1: Log4j2-Sichtbarkeit
|
```xml
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.tngtech.archunit</groupId>
|
||||||
|
<artifactId>archunit-junit5</artifactId>
|
||||||
|
<version><!-- aktuell stabile Version --></version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Architekturtest-Klasse
|
||||||
|
|
||||||
|
Neue Testklasse `de.gecheckt.asv.ArchitectureTest` im Testbereich. Mindestens vier Regeln:
|
||||||
|
|
||||||
|
**Regel A — Log4j2-Sichtbarkeit:**
|
||||||
```java
|
```java
|
||||||
@ArchTest
|
@ArchTest
|
||||||
static final ArchRule log4j2_nur_in_logging_adapter_und_bootstrap =
|
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.");
|
.because("Log4j2 darf nur im Logging-Adapter und im Bootstrap sichtbar sein.");
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test 2: Domain ist frei
|
**Regel B — Domain-Reinheit:**
|
||||||
```java
|
```java
|
||||||
@ArchTest
|
@ArchTest
|
||||||
static final ArchRule domain_hat_keine_adapter_abhaengigkeit =
|
static final ArchRule domain_hat_keine_adapter_abhaengigkeit =
|
||||||
@@ -42,11 +58,10 @@ static final ArchRule domain_hat_keine_adapter_abhaengigkeit =
|
|||||||
.should().dependOnClassesThat()
|
.should().dependOnClassesThat()
|
||||||
.resideInAnyPackage(
|
.resideInAnyPackage(
|
||||||
"de.gecheckt.asv.adapter..",
|
"de.gecheckt.asv.adapter..",
|
||||||
"de.gecheckt.asv.bootstrap..",
|
"de.gecheckt.asv.bootstrap..");
|
||||||
"de.gecheckt.asv.preview..");
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test 3: Application ist frei von konkreten Adaptern
|
**Regel C — Application-Reinheit:**
|
||||||
```java
|
```java
|
||||||
@ArchTest
|
@ArchTest
|
||||||
static final ArchRule application_kennt_keine_adapter_implementierungen =
|
static final ArchRule application_kennt_keine_adapter_implementierungen =
|
||||||
@@ -58,53 +73,82 @@ static final ArchRule application_kennt_keine_adapter_implementierungen =
|
|||||||
"de.gecheckt.asv.bootstrap..");
|
"de.gecheckt.asv.bootstrap..");
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test 4: Preview wird nicht referenziert
|
**Regel D — Preview-Isolation:**
|
||||||
```java
|
```java
|
||||||
@ArchTest
|
@ArchTest
|
||||||
static final ArchRule preview_wird_nicht_aus_aktivem_code_referenziert =
|
static final ArchRule preview_wird_nicht_aus_aktivem_code_referenziert =
|
||||||
noClasses()
|
noClasses()
|
||||||
.that().resideInAnyPackage(
|
.that().resideInAnyPackage(
|
||||||
"de.gecheckt.asv.adapter..",
|
"de.gecheckt.asv.adapter..",
|
||||||
"de.gecheckt.asv.application..",
|
"de.gecheckt.asv.bootstrap..")
|
||||||
"de.gecheckt.asv.bootstrap..",
|
|
||||||
"de.gecheckt.asv.domain..")
|
|
||||||
.should().dependOnClassesThat()
|
.should().dependOnClassesThat()
|
||||||
.resideInAPackage("de.gecheckt.asv.preview..")
|
.haveSimpleNameContaining("DefaultStructureValidator")
|
||||||
.because("Preview-Code ist aus M1-Sicht eingefroren und wird erst ab M3 aktiv verwendet.");
|
.orShould().dependOnClassesThat()
|
||||||
|
.haveSimpleNameContaining("DefaultFieldValidator")
|
||||||
|
.because("Preview-Validatoren sind in M1 eingefroren und werden erst ab M3 aktiv verwendet.");
|
||||||
```
|
```
|
||||||
|
|
||||||
### Zusätzlich: Paketstruktur-Check
|
**Wichtig:** Wenn beim ersten Lauf Regeln rot sind, **müssen die Verstöße behoben werden** — die Regeln werden nicht abgeschwächt.
|
||||||
- Prüfen, dass die Soll-Pakete aus `technik-und-architektur.md` tatsächlich existieren (als ArchUnit-Regel oder einfacher Dateisystem-Test)
|
|
||||||
|
### 3. Test-Log-Konfiguration (E-02)
|
||||||
|
|
||||||
|
`src/test/resources/log4j2-test.xml` anlegen:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Configuration status="WARN">
|
||||||
|
<Appenders>
|
||||||
|
<Console name="Console" target="SYSTEM_ERR">
|
||||||
|
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
|
||||||
|
</Console>
|
||||||
|
</Appenders>
|
||||||
|
<Loggers>
|
||||||
|
<Root level="WARN">
|
||||||
|
<AppenderRef ref="Console"/>
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
</Configuration>
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
## Scope OUT
|
||||||
|
|
||||||
- komplexere Regeln wie „keine zyklischen Abhängigkeiten zwischen Paketen" — wäre schön, ist aber für M1 zu weitgehend
|
- Komplexe Regeln wie zyklische Abhängigkeiten — für M1 zu weitgehend
|
||||||
- Regeln zu Klassenbenennung
|
- Regeln zu Klassenbenennung oder `public`-Sichtbarkeit
|
||||||
- Regeln zu `public`-Sichtbarkeit
|
- Coverage- und Mutation-Schwellwerte (kommen erst in M9)
|
||||||
- Tests für Preview-internen Aufbau
|
- Neue Produktionsklassen
|
||||||
|
|
||||||
## Schritte
|
## Schritte
|
||||||
|
|
||||||
1. Branch `m1/ap10-architekturtest`
|
1. ArchUnit in `pom.xml` als Test-Dependency aufnehmen
|
||||||
2. ArchUnit in `pom.xml` als Test-Dependency aufnehmen
|
2. `ArchitectureTest`-Klasse mit den vier Regeln implementieren
|
||||||
3. `ArchitectureTest`-Klasse im Testbereich anlegen
|
3. `log4j2-test.xml` unter `src/test/resources/` anlegen
|
||||||
4. Die vier Regeln implementieren
|
4. `mvn clean verify` ausführen — alle vier ArchUnit-Regeln müssen grün sein
|
||||||
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!).
|
5. Falls Regeln rot: Verstöße identifizieren, beheben, erneut testen
|
||||||
6. Commit `M1-AP10: Architekturtest für Log4j2-Sichtbarkeit, Paketabhängigkeiten, Preview-Isolation`
|
6. Im Bericht dokumentieren ob beim ersten Lauf Regeln rot waren und wie behoben
|
||||||
7. Abschlussbericht schreiben
|
7. Abschlussbericht schreiben
|
||||||
|
|
||||||
## Abnahmekriterien
|
## Abnahmekriterien
|
||||||
|
|
||||||
- ArchUnit ist als Test-Dependency eingebunden
|
- [ ] ArchUnit als Test-Dependency in `pom.xml`
|
||||||
- vier Architektur-Regeln sind implementiert und grün
|
- [ ] Vier Architekturregeln A–D implementiert und grün
|
||||||
- `mvn clean verify` ist grün
|
- [ ] `log4j2-test.xml` unter `src/test/resources/` vorhanden
|
||||||
- Abschlussbericht liegt vor und dokumentiert, ob beim ersten Lauf Regeln rot waren und wenn ja, wie sie behoben wurden
|
- [ ] 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
|
## 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.
|
- 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.
|
||||||
- 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.
|
- 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
|
## 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`.
|
||||||
|
|||||||
@@ -1,92 +1,107 @@
|
|||||||
|
---
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
# AP11 – M1-Abnahme
|
# AP11 – M1-Abnahme
|
||||||
|
|
||||||
|
> **Meilenstein:** M1
|
||||||
|
> **Vorgänger:** AP01–AP10 alle ✅ erforderlich
|
||||||
|
> **Nachfolger:** M2
|
||||||
|
> **Grundlage:** `docs/specs/meilensteine.md` v3, M1-Abnahmekriterien
|
||||||
|
|
||||||
## Ziel
|
## 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
|
## Voraussetzungen
|
||||||
|
|
||||||
- AP01 bis AP10 abgeschlossen und grün
|
- AP01–AP10 abgeschlossen und grün
|
||||||
- alle AP-Berichte liegen in `docs/arbeitspakete/m1/berichte/` vor
|
- Alle AP-Berichte liegen in `docs/arbeitspakete/m1/berichte/` vor
|
||||||
|
|
||||||
## Scope IN
|
## Scope IN
|
||||||
|
|
||||||
### End-to-End-Lauf
|
### 1. Test-Artefakt anlegen
|
||||||
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
|
|
||||||
|
|
||||||
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
|
### 2. End-to-End-Läufe durchführen
|
||||||
Jeder Abnahmepunkt aus `docs/specs/meilensteine.md` v3 Abschnitt „Abnahme von M1" wird mit einem konkreten Nachweis verknüpft:
|
|
||||||
|
|
||||||
| 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 | ✅ |
|
| Anwendung als JAR unter Windows mit Java 21 startbar | Lauf 1 | |
|
||||||
| falsches oder fehlendes Argument → Exit-Code `2` mit Minimalbericht | Lauf 3, 4, 5 | ✅ |
|
| Fehlendes/falsches 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 | ✅ |
|
| Bericht- und Log-Datei im Eingabeverzeichnis mit korrekter Suffix-Logik | Lauf 1 + 2 | |
|
||||||
| Log4j2-Bindung ist außerhalb von Bootstrap und Logging-Adapter nicht sichtbar | Architekturtest AP10, Test 1 | ✅ |
|
| Log4j2-Bindung außerhalb Bootstrap/Logging-Adapter nicht sichtbar | Architekturtest AP10 Regel A | |
|
||||||
| Befundmodell unterscheidet Spec-Urteil und diagnostische Weiteranalyse | Unit-Test AP05 | ✅ |
|
| Befundmodell trennt Spec-Urteil und diagnostische Weiteranalyse | Unit-Test AP05 | |
|
||||||
| Build und Tests sind grün | `mvn clean verify` | ✅ |
|
| Build und Tests grün | `mvn clean verify` | |
|
||||||
|
|
||||||
### M1-Abschlussbericht
|
### 4. Konsolidierter 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: ..."
|
|
||||||
|
|
||||||
### Tagging
|
Datei: `docs/arbeitspakete/m1/berichte/M1-abschlussbericht.md`
|
||||||
- Git-Tag `m1-done` auf dem letzten AP11-Commit setzen
|
|
||||||
- Tag-Message: „Meilenstein 1 abgeschlossen, siehe 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
|
## 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
|
- Release-Builds, Signierung, Publizierung
|
||||||
- Externe Reviews (die kommen vom Rezensenten der Arbeitspakete, nicht aus diesem AP)
|
- Inhaltliche Berichtsvertiefung über M1-Minimum hinaus
|
||||||
|
|
||||||
## Schritte
|
## Schritte
|
||||||
|
|
||||||
1. Branch `m1/ap11-abnahme`
|
1. `test-artefakte/m1/minimal.txt` anlegen
|
||||||
2. Test-Artefakt `test-artefakte/m1/minimal.txt` anlegen (ISO-8859-15, 3–5 Zeilen Dummy-Inhalt)
|
2. `mvn clean package`
|
||||||
3. `mvn clean package` ausführen
|
3. Alle fünf Läufe durchführen und protokollieren
|
||||||
4. Die fünf Läufe durchführen und protokollieren
|
4. `mvn clean verify` ein letztes Mal
|
||||||
5. Konsolidierten M1-Abschlussbericht schreiben
|
5. Konsolidierten M1-Abschlussbericht schreiben
|
||||||
6. `mvn clean verify` ein letztes Mal laufen lassen
|
6. Git-Tag `m1-done` setzen
|
||||||
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"`
|
|
||||||
|
|
||||||
## Abnahmekriterien
|
## Abnahmekriterien
|
||||||
|
|
||||||
- `test-artefakte/m1/minimal.txt` existiert
|
- [ ] `test-artefakte/m1/minimal.txt` existiert
|
||||||
- alle fünf Läufe sind protokolliert
|
- [ ] Alle fünf Läufe sind protokolliert
|
||||||
- M1-Abschlussbericht existiert und enthält alle oben genannten Abschnitte
|
- [ ] `M1-abschlussbericht.md` existiert mit allen Pflichtabschnitten
|
||||||
- Meilenstein-Abnahmetabelle ist vollständig und jede Zeile hat einen konkreten Nachweis
|
- [ ] Meilenstein-Abnahmetabelle vollständig, jede Zeile mit konkretem Nachweis
|
||||||
- `mvn clean verify` ist grün
|
- [ ] Kein Exit-Code `3` mehr erreichbar
|
||||||
- Git-Tag `m1-done` ist gesetzt
|
- [ ] `mvn clean verify` grün
|
||||||
- der Freigabe-Vermerk am Ende des Abschlussberichts ist explizit
|
- [ ] Git-Tag `m1-done` gesetzt
|
||||||
|
- [ ] Freigabe-Vermerk ist explizit
|
||||||
|
- [ ] Abschlussbericht unter `docs/arbeitspakete/m1/berichte/AP11-bericht.md`
|
||||||
|
|
||||||
## Rest-Risiken und offene Punkte
|
## 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
|
## 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`.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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 `<mainClass>de.gecheckt.asv.bootstrap.Main</mainClass>` ([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 <datei>` 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<ValidationError>` 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.
|
||||||
@@ -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
|
||||||
@@ -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 ... <datei>` 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 <datei>` 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
|
||||||
@@ -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 (`<dateiname>.txt`) und eine Log-Datei (`<dateiname>.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 `<baseName>.<ext>`, dann `<baseName>_v1.<ext>`, `<baseName>_v2.<ext>` 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
|
||||||
@@ -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: `<kein Argument>` und `<mehrere Argumente>` 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 (`<kein Argument>`) funktionieren
|
||||||
|
- `buildReportContent(ValidationReport, Path)` delegiert jetzt an die neue private `buildReportContentWithFileName(ValidationReport, String)` — kein Duplizierungsrisiko
|
||||||
|
- Befundzeilen zeigen jetzt auch `Regel=<ruleId>`, 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 `<kein Argument>` und `<mehrere Argumente>`:** 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
|
||||||
@@ -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: * <p>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: * <p>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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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<VKNR>...`, verschlüsselt: `<IK>_<IK>_<E|T>ASV0<Zähler>`, 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"
|
||||||
|
```
|
||||||
@@ -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.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.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]
|
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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
================================================================
|
||||||
@@ -74,6 +74,14 @@
|
|||||||
<version>${mockito.version}</version>
|
<version>${mockito.version}</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- ArchUnit: Automatisierte Architekturtests (hexagonale Struktur, Log4j2-Isolation) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.tngtech.archunit</groupId>
|
||||||
|
<artifactId>archunit-junit5</artifactId>
|
||||||
|
<version>1.3.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
@@ -115,18 +123,46 @@
|
|||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
<!-- Ausführbares JAR; Main-Class-Klasse wird in AP06 angelegt -->
|
<!-- Uber-JAR via maven-shade-plugin (ersetzt maven-jar-plugin-Platzhalter aus AP02) -->
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-jar-plugin</artifactId>
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
<version>${maven-jar-plugin.version}</version>
|
<version>3.5.2</version>
|
||||||
|
<dependencies>
|
||||||
|
<!-- Stellt Log4j2PluginsCacheFileTransformer bereit (nicht im Shade-Plugin selbst enthalten) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.logging.log4j</groupId>
|
||||||
|
<artifactId>log4j-transform-maven-shade-plugin-extensions</artifactId>
|
||||||
|
<version>0.1.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
<configuration>
|
<configuration>
|
||||||
<archive>
|
<transformers>
|
||||||
<manifest>
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
<mainClass>de.gecheckt.asv.bootstrap.Main</mainClass>
|
<mainClass>de.gecheckt.asv.bootstrap.Main</mainClass>
|
||||||
</manifest>
|
</transformer>
|
||||||
</archive>
|
<!-- Log4j2-Plugin-Cache korrekt zusammenführen, sonst fehlen Plugins im Uber-JAR -->
|
||||||
|
<transformer implementation="org.apache.logging.log4j.maven.plugins.shade.transformer.Log4j2PluginCacheFileTransformer"/>
|
||||||
|
</transformers>
|
||||||
|
<filters>
|
||||||
|
<filter>
|
||||||
|
<artifact>*:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>META-INF/*.SF</exclude>
|
||||||
|
<exclude>META-INF/*.DSA</exclude>
|
||||||
|
<exclude>META-INF/*.RSA</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
</filters>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
<!-- JaCoCo: Coverage-Messung ohne Schwellwerte (Schwellen kommen in M9) -->
|
<!-- JaCoCo: Coverage-Messung ohne Schwellwerte (Schwellen kommen in M9) -->
|
||||||
|
|||||||
@@ -1,164 +1,30 @@
|
|||||||
package de.gecheckt.asv.adapter.in.cli;
|
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.
|
* Ehemaliger Haupteinstiegspunkt des ASV-Format-Validators.
|
||||||
*
|
*
|
||||||
* Diese Anwendung validiert Dateien gegen ein segmentorientiertes Dateiformat.
|
* <p><strong>Veraltet seit AP06.</strong> Die Verantwortlichkeiten wurden aufgeteilt:</p>
|
||||||
* Sie nimmt einen Dateipfad als Kommandozeilenargument entgegen, parst die Datei,
|
* <ul>
|
||||||
* validiert sie und gibt die Ergebnisse auf der Konsole aus.
|
* <li>Bootstrap und Constructor Injection → {@link de.gecheckt.asv.bootstrap.Main}</li>
|
||||||
|
* <li>CLI-Argument-Verarbeitung und Exit-Code → {@link CliRunner}</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Diese Klasse bleibt als leere Hülle erhalten, bis AP09 (Altlogik einfrieren) abgeschlossen
|
||||||
|
* ist. Sie darf nicht mehr direkt verwendet werden.</p>
|
||||||
|
*
|
||||||
|
* @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 {
|
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.
|
* Nicht mehr verwenden. Nur noch als Kompatibilitätshülle vorhanden.
|
||||||
*/
|
|
||||||
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
|
* @deprecated Verwende {@link de.gecheckt.asv.bootstrap.Main#main(String[])} stattdessen.
|
||||||
* @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
|
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(since = "AP06", forRemoval = true)
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
AsvValidatorApplication app = new AsvValidatorApplication();
|
de.gecheckt.asv.bootstrap.Main.main(args);
|
||||||
int exitCode = app.run(args);
|
|
||||||
System.exit(exitCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 <datei-pfad>");
|
|
||||||
System.out.println(" <datei-pfad> Pfad zur zu validierenden Datei");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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}.
|
||||||
|
*
|
||||||
|
* <p>Reihenfolge pro Lauf (AP07/AP08):</p>
|
||||||
|
* <ol>
|
||||||
|
* <li>Argument-Prüfung und Datei-Vorabprüfung</li>
|
||||||
|
* <li>Bei Bedienfehler: Minimalbericht erzeugen und ggf. Berichtdatei schreiben</li>
|
||||||
|
* <li>Log-Datei-Pfad via {@link SuffixResolver} bestimmen</li>
|
||||||
|
* <li>{@link LoggingConfigurator#configureLogFile(Path)} aufrufen</li>
|
||||||
|
* <li>Validierungslauf über {@link FileValidationService#validate(Path)}</li>
|
||||||
|
* <li>Berichtdatei über {@link ReportFileWriter#write(ValidationReport, Path)} schreiben</li>
|
||||||
|
* <li>Berichtinhalt auf der Konsole ausgeben</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* <p><strong>Bedienfehler-Fälle (AP08):</strong></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Kein Argument → nur Konsole (Verzeichnis unbekannt)</li>
|
||||||
|
* <li>Mehr als ein Argument → nur Konsole</li>
|
||||||
|
* <li>Eingabedatei existiert nicht → Konsole + Berichtdatei im übergeordneten Verzeichnis</li>
|
||||||
|
* <li>Pfad ist kein regulärer Dateityp → nur Konsole</li>
|
||||||
|
* <li>Datei nicht lesbar → Konsole + Berichtdatei im übergeordneten Verzeichnis</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
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 = "<kein Argument>";
|
||||||
|
private static final String PLACEHOLDER_MANY_ARGS = "<mehrere Argumente>";
|
||||||
|
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* <p>IO-Fehler beim Schreiben der Berichtdatei verhindern die Konsolenausgabe nicht.
|
||||||
|
* Das Ergebnis wird in jedem Fall auf die Konsole geschrieben.</p>
|
||||||
|
*
|
||||||
|
* @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 <datei-pfad>";
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* <p>Ist das Verzeichnis nicht vorhanden oder nicht schreibbar, wird nur eine
|
||||||
|
* Hinweiszeile auf STDERR ausgegeben — kein Fehler auf Fehler.</p>
|
||||||
|
*
|
||||||
|
* @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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package de.gecheckt.asv.adapter.in.cli;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normative Exit-Codes der ASV-Format-Validator-CLI.
|
||||||
|
*
|
||||||
|
* <p>Die drei zulässigen Exit-Codes sind gemäß Technischer Anlage ASV 1.09 und
|
||||||
|
* {@code docs/specs/technik-und-architektur.md} definiert:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #VALID} (0) — Datei ist spec-konform, keine SPEC-ERROR-Befunde</li>
|
||||||
|
* <li>{@link #INVALID} (1) — Datei enthält mindestens einen SPEC-ERROR-Befund</li>
|
||||||
|
* <li>{@link #OPERATIONAL_ERROR} (2) — Bedienfehler (fehlendes Argument, nicht lesbare Datei)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>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 <baseName>.<ext>}; bei jedem
|
||||||
|
* weiteren Lauf wird {@code <baseName>_v1.<ext>}, {@code <baseName>_v2.<ext>} usw. erzeugt,
|
||||||
|
* bis ein freier Pfad gefunden ist.</p>
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*/
|
||||||
|
public class SuffixResolver {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt den ersten freien Dateipfad für den gegebenen Basisnamen und die gegebene
|
||||||
|
* Extension im Zielverzeichnis.
|
||||||
|
*
|
||||||
|
* <p>Probiert in dieser Reihenfolge:</p>
|
||||||
|
* <ol>
|
||||||
|
* <li>{@code <baseName>.<extension>}</li>
|
||||||
|
* <li>{@code <baseName>_v1.<extension>}</li>
|
||||||
|
* <li>{@code <baseName>_v2.<extension>}</li>
|
||||||
|
* <li>… bis ein freier Pfad gefunden ist</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>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}.</p>
|
||||||
|
*
|
||||||
|
* @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: <baseName>.<ext>
|
||||||
|
Path candidate = directory.resolve(baseName + "." + extension);
|
||||||
|
if (!exists(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mit Suffix: <baseName>_v1.<ext>, <baseName>_v2.<ext>, ...
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,86 @@
|
|||||||
package de.gecheckt.asv.adapter.out.logging;
|
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.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Konfiguriert den Log4j2-Logging-Adapter programmatisch.
|
* 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.
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* <p>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).</p>
|
||||||
*/
|
*/
|
||||||
public class LoggingConfigurator {
|
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.
|
* Setzt den Zielpfad der Log-Datei für diesen Lauf und konfiguriert Log4j2
|
||||||
* Die tatsächliche dynamische Umleitung wird in AP07 implementiert.
|
* programmatisch um.
|
||||||
*
|
*
|
||||||
* @param logFile Pfad zur Log-Datei
|
* <p>Diese Methode muss <strong>vor</strong> 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.</p>
|
||||||
|
*
|
||||||
|
* <p>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}.</p>
|
||||||
|
*
|
||||||
|
* @param logFile Zielpfad der Log-Datei (nicht null)
|
||||||
|
* @throws IllegalArgumentException wenn {@code logFile} null ist
|
||||||
*/
|
*/
|
||||||
public void configureLogFile(Path logFile) {
|
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<String, LoggerConfig> 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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* <p>Dieses Objekt enthält keinerlei Log4j2-Typen — Logging erfolgt ausschließlich über
|
||||||
|
* die SLF4J-Fassade.</p>
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* <p>IO-Fehler führen <em>nicht</em> zu einer {@link RuntimeException}, sondern werden
|
||||||
|
* protokolliert. Das Ergebnis signalisiert den Fehler über {@link ReportWriteResult}.</p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>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 <kein Argument>} mit Sonderzeichen funktionieren.</p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>Diese Variante wird für Bedienfehler-Berichte verwendet, bei denen der Dateiname
|
||||||
|
* ein Platzhalter wie {@code <kein Argument>} sein kann, der kein gültiger Pfad ist.</p>
|
||||||
|
*
|
||||||
|
* @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<Finding> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}.
|
||||||
|
*
|
||||||
|
* <p>Liest die Eingabedatei mit dem normativen Eingabe-Encoding <strong>ISO-8859-15</strong>
|
||||||
|
* ein, zählt die gelesenen Bytes und gibt einen leeren {@link ValidationReport} zurück.
|
||||||
|
* <em>Keine echte Validierung wird in M1 durchgeführt.</em></p>
|
||||||
|
*
|
||||||
|
* <p>Diese Klasse wird in M3 durch eine echte Implementierung ersetzt, die den
|
||||||
|
* vollständigen Parser- und Validator-Pfad aktiviert.</p>
|
||||||
|
*/
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>Nimmt einen bereits vorab geprüften (existierenden, regulären, lesbaren) Dateipfad
|
||||||
|
* entgegen und gibt einen {@link ValidationReport} zurück.</p>
|
||||||
|
*
|
||||||
|
* <p>In M1 liefert die Standardimplementierung ({@code DummyFileValidationService}) einen
|
||||||
|
* leeren Bericht. Echte Parser- und Validator-Einbindung folgt ab M3.</p>
|
||||||
|
*/
|
||||||
|
public interface FileValidationService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validiert die Datei unter dem angegebenen Pfad.
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* @param inputFile Pfad zur zu validierenden Eingabedatei (nicht null)
|
||||||
|
* @return Validierungsbericht (nicht null)
|
||||||
|
*/
|
||||||
|
ValidationReport validate(Path inputFile);
|
||||||
|
}
|
||||||
@@ -10,13 +10,21 @@ import de.gecheckt.asv.application.model.ValidationResult;
|
|||||||
import de.gecheckt.asv.application.model.ValidationSeverity;
|
import de.gecheckt.asv.application.model.ValidationSeverity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default implementation of FieldValidator that checks general field rules.
|
* 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.
|
||||||
*
|
*
|
||||||
* Rules checked:
|
* @see <a href="docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md">E-01</a>
|
||||||
* 1. Field.rawValue must not be empty
|
*
|
||||||
* 2. Field.rawValue must not consist only of whitespaces
|
* <p>Standardimplementierung des FieldValidator, die allgemeine Feldregeln prüft.</p>
|
||||||
* 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
|
* <p>Geprüfte Regeln:</p>
|
||||||
|
* <ol>
|
||||||
|
* <li>Field.rawValue darf nicht leer sein</li>
|
||||||
|
* <li>Field.rawValue darf nicht nur aus Leerzeichen bestehen</li>
|
||||||
|
* <li>Feldpositionen innerhalb eines Segments sollten lückenlos aufeinanderfolgen, beginnend bei 1</li>
|
||||||
|
* <li>Wenn fieldName gesetzt ist, darf er nicht leer oder nur aus Leerzeichen bestehen</li>
|
||||||
|
* </ol>
|
||||||
*/
|
*/
|
||||||
public class DefaultFieldValidator implements FieldValidator {
|
public class DefaultFieldValidator implements FieldValidator {
|
||||||
|
|
||||||
|
|||||||
+29
-21
@@ -13,28 +13,36 @@ import de.gecheckt.asv.application.model.ValidationResult;
|
|||||||
import de.gecheckt.asv.application.model.ValidationSeverity;
|
import de.gecheckt.asv.application.model.ValidationSeverity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standardimplementierung des StructureValidator, die allgemeine Strukturregeln prüft.
|
* 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.
|
||||||
*
|
*
|
||||||
* Geprüfte Regeln:
|
* @see <a href="docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md">E-01</a>
|
||||||
* 1. Die Eingabedatei muss mindestens eine Nachricht enthalten
|
*
|
||||||
* 2. Jede Nachricht muss mindestens ein Segment enthalten
|
* <p>Standardimplementierung des StructureValidator, die allgemeine Strukturregeln prüft.</p>
|
||||||
* 3. Segmentnamen dürfen nicht leer sein
|
*
|
||||||
* 4. Feldpositionen innerhalb eines Segments müssen eindeutig und positiv sein
|
* <p>Geprüfte Regeln:</p>
|
||||||
* 5. Segmentpositionen innerhalb einer Nachricht müssen eindeutig und positiv sein
|
* <ol>
|
||||||
* 6. Nachrichtenpositionen innerhalb einer Eingabedatei müssen eindeutig und positiv sein
|
* <li>Die Eingabedatei muss mindestens eine Nachricht enthalten</li>
|
||||||
* 7. UNH- und UNT-Referenznummern müssen innerhalb einer Nachricht übereinstimmen
|
* <li>Jede Nachricht muss mindestens ein Segment enthalten</li>
|
||||||
* 8. Die im UNT angegebene Segmentanzahl muss der tatsächlichen Anzahl der Segmente entsprechen
|
* <li>Segmentnamen dürfen nicht leer sein</li>
|
||||||
* 9. Eine Nachricht muss mindestens ein UNH-Segment enthalten
|
* <li>Feldpositionen innerhalb eines Segments müssen eindeutig und positiv sein</li>
|
||||||
* 10. Eine Nachricht muss mindestens ein UNT-Segment enthalten
|
* <li>Segmentpositionen innerhalb einer Nachricht müssen eindeutig und positiv sein</li>
|
||||||
* 11. UNH muss vor UNT stehen
|
* <li>Nachrichtenpositionen innerhalb einer Eingabedatei müssen eindeutig und positiv sein</li>
|
||||||
* 12. Der Nachrichtentyp in UNH/S009/0065 darf nur ASVREC oder ASVFEH sein
|
* <li>UNH- und UNT-Referenznummern müssen innerhalb einer Nachricht übereinstimmen</li>
|
||||||
* 13. Für Nachrichten vom Typ ASVREC müssen die Segmente IFA, REA und IVA vorhanden sein
|
* <li>Die im UNT angegebene Segmentanzahl muss der tatsächlichen Anzahl der Segmente entsprechen</li>
|
||||||
* 14. Für Nachrichten vom Typ ASVREC muss die Reihenfolge IFA vor REA vor IVA eingehalten werden
|
* <li>Eine Nachricht muss mindestens ein UNH-Segment enthalten</li>
|
||||||
* 15. Für ASVREC mit Rechnungskennzeichen "0" in REA müssen DGN und LEA vorhanden sein
|
* <li>Eine Nachricht muss mindestens ein UNT-Segment enthalten</li>
|
||||||
* 16. Für ASVREC mit Rechnungskennzeichen "1" in REA dürfen DGN und LEA nicht vorhanden sein
|
* <li>UNH muss vor UNT stehen</li>
|
||||||
* 17. Für ASVREC mit Rechnungskennzeichen "1" in REA muss der Rechnungsbetrag "0,00" sein
|
* <li>Der Nachrichtentyp in UNH/S009/0065 darf nur ASVREC oder ASVFEH sein</li>
|
||||||
* 18. Für ASVREC müssen IFA, REA und IVA jeweils genau einmal vorkommen
|
* <li>Für Nachrichten vom Typ ASVREC müssen die Segmente IFA, REA und IVA vorhanden sein</li>
|
||||||
* 19. Für ASVFEH-Nachrichten muss mindestens ein FHL-Segment vorhanden sein
|
* <li>Für Nachrichten vom Typ ASVREC muss die Reihenfolge IFA vor REA vor IVA eingehalten werden</li>
|
||||||
|
* <li>Für ASVREC mit Rechnungskennzeichen "0" in REA müssen DGN und LEA vorhanden sein</li>
|
||||||
|
* <li>Für ASVREC mit Rechnungskennzeichen "1" in REA dürfen DGN und LEA nicht vorhanden sein</li>
|
||||||
|
* <li>Für ASVREC mit Rechnungskennzeichen "1" in REA muss der Rechnungsbetrag "0,00" sein</li>
|
||||||
|
* <li>Für ASVREC müssen IFA, REA und IVA jeweils genau einmal vorkommen</li>
|
||||||
|
* <li>Für ASVFEH-Nachrichten muss mindestens ein FHL-Segment vorhanden sein</li>
|
||||||
|
* </ol>
|
||||||
*/
|
*/
|
||||||
public class DefaultStructureValidator implements StructureValidator {
|
public class DefaultStructureValidator implements StructureValidator {
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>Verantwortlichkeiten:</p>
|
||||||
|
* <ol>
|
||||||
|
* <li>Manuelle Constructor Injection aller Anwendungskomponenten</li>
|
||||||
|
* <li>Logging-Konfiguration über {@link LoggingConfigurator} (Log-Datei im Eingabeverzeichnis)</li>
|
||||||
|
* <li>Delegation an {@link CliRunner#run(String[], ReportFileWriter)}</li>
|
||||||
|
* <li>Weiterreichen des Exit-Codes an {@link System#exit(int)}</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p><strong>Log4j2-Sichtbarkeit:</strong> Nur dieses Paket ({@code bootstrap}) und
|
||||||
|
* {@code adapter.out.logging} dürfen Log4j2-Typen direkt verwenden.</p>
|
||||||
|
*
|
||||||
|
* <p><strong>Reihenfolge vor dem Validierungslauf (AP07):</strong></p>
|
||||||
|
* <ol>
|
||||||
|
* <li>Eingabedatei-Pfad aus Argumenten bestimmen (in {@link CliRunner})</li>
|
||||||
|
* <li>Basisname und Zielverzeichnis ableiten</li>
|
||||||
|
* <li>{@link SuffixResolver} für {@code .log} aufrufen</li>
|
||||||
|
* <li>{@link LoggingConfigurator#configureLogFile(java.nio.file.Path)} aufrufen</li>
|
||||||
|
* <li>Validierungslauf starten</li>
|
||||||
|
* <li>{@link ReportFileWriter} schreibt Berichtdatei</li>
|
||||||
|
* <li>Konsolenausgabe (identisch zum Berichtinhalt)</li>
|
||||||
|
* </ol>
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>Erzeugt keinerlei Befunde — der Validator ist in M1 bewusst ohne fachliche Prüflogik
|
||||||
|
* verdrahtet, damit kein aktiver Lauf ASVREC-/ASVFEH-Feldbefunde liefert.</p>
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>Erzeugt keinerlei Befunde — der Validator ist in M1 bewusst ohne fachliche Prüflogik
|
||||||
|
* verdrahtet, damit kein aktiver Lauf ASVREC-/ASVFEH-Strukturbefunde liefert.</p>
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package de.gecheckt.asv.domain.finding;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einzelbefund eines Validierungslaufs.
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* <p>Unveränderlich (Record). Alle nicht-nullable Felder werden im Konstruktor
|
||||||
|
* auf {@code null} geprüft.</p>
|
||||||
|
*
|
||||||
|
* @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 <em>nicht</em> {@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package de.gecheckt.asv.domain.finding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Art eines Befunds: Spec-Urteil oder diagnostische Weiteranalyse.
|
||||||
|
*
|
||||||
|
* <p>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}.</p>
|
||||||
|
*/
|
||||||
|
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 <em>niemals</em>, auch nicht bei
|
||||||
|
* {@link Severity#ERROR}.
|
||||||
|
*/
|
||||||
|
DIAGNOSTIC
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package de.gecheckt.asv.domain.finding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schicht, auf die sich ein Befund bezieht.
|
||||||
|
*
|
||||||
|
* <p>Die Schichttrennung stellt sicher, dass technische Befunde nicht mit fachlichen Befunden
|
||||||
|
* vermischt werden und eine spätere GUI differenziert auf dieselben Daten zugreifen kann.</p>
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package de.gecheckt.asv.domain.finding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schweregrad eines Befunds.
|
||||||
|
*
|
||||||
|
* <p>Nur {@link #ERROR}-Befunde mit {@link FindingKind#SPEC} beeinflussen das Prüfurteil
|
||||||
|
* ({@link ValidationReport#computeVerdict()}). Warnungen und Hinweise verändern den
|
||||||
|
* Gültigkeitsstatus nicht.</p>
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>Ein {@code ValidationReport} fasst alle {@link Finding}-Befunde zusammen und berechnet
|
||||||
|
* daraus das Gesamturteil ({@link Verdict}). Die zentrale Invariante lautet:</p>
|
||||||
|
* <blockquote>
|
||||||
|
* {@link #computeVerdict()} berücksichtigt <em>ausschließlich</em> Befunde mit
|
||||||
|
* {@link FindingKind#SPEC} <em>und</em> {@link Severity#ERROR}.
|
||||||
|
* Ein {@link FindingKind#DIAGNOSTIC}-Befund mit {@link Severity#ERROR} setzt das Urteil
|
||||||
|
* <em>niemals</em> auf {@link Verdict#INVALID}.
|
||||||
|
* </blockquote>
|
||||||
|
*
|
||||||
|
* <p>Instanzen sind unveränderlich. Die Befundliste kann nach Erzeugung nicht mehr verändert
|
||||||
|
* werden.</p>
|
||||||
|
*/
|
||||||
|
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<Finding> 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<Finding> 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<Finding> 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}.
|
||||||
|
*
|
||||||
|
* <p>Typische Anwendungsfälle: fehlendes Pflichtargument, nicht lesbare Eingabedatei.</p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p><strong>Invariante:</strong> 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.</p>
|
||||||
|
*
|
||||||
|
* @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<Finding> 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<Finding> 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.
|
||||||
|
*
|
||||||
|
* <p>Die zurückgegebene Liste kann nicht verändert werden — Versuche werfen
|
||||||
|
* {@link UnsupportedOperationException}.</p>
|
||||||
|
*
|
||||||
|
* @return unveränderliche Befundliste (nicht null)
|
||||||
|
*/
|
||||||
|
public List<Finding> getFindings() {
|
||||||
|
return findings;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package de.gecheckt.asv.domain.finding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gesamturteil eines Validierungslaufs.
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* <p>Entsprechung zu Exit-Codes gemäß Architekturvorgabe:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #VALID} → Exit-Code 0</li>
|
||||||
|
* <li>{@link #INVALID} → Exit-Code 1</li>
|
||||||
|
* <li>{@link #OPERATIONAL_ERROR} → Exit-Code 2</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Log4j2-Konfiguration des ASV-Format-Validators.
|
||||||
|
|
||||||
|
HINWEIS (AP07): Der File-Appender hier ist ein FALLBACK-Default.
|
||||||
|
Er greift nur, wenn LoggingConfigurator.configureLogFile(Path) NICHT aufgerufen wurde
|
||||||
|
(z.B. in Unit-Tests). Bei produktiven Läufen wird der Dateipfad programmatisch gesetzt.
|
||||||
|
-->
|
||||||
<Configuration status="WARN">
|
<Configuration status="WARN">
|
||||||
<Appenders>
|
<Appenders>
|
||||||
<Console name="Console" target="SYSTEM_ERR">
|
<Console name="Console" target="SYSTEM_ERR">
|
||||||
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
|
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
|
||||||
</Console>
|
</Console>
|
||||||
<File name="File" fileName="logs/asv-format-validator.log" append="true">
|
<!-- Fallback: greift nur wenn configureLogFile(Path) nicht aufgerufen wurde -->
|
||||||
|
<File name="File" fileName="logs/asv-format-validator-fallback.log" append="true">
|
||||||
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
|
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
|
||||||
</File>
|
</File>
|
||||||
</Appenders>
|
</Appenders>
|
||||||
|
|||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>Sichert die in M1 etablierten Strukturregeln der hexagonalen Architektur dauerhaft ab.
|
||||||
|
* Jede Verletzung führt zu einem fehlschlagenden Build.</p>
|
||||||
|
*
|
||||||
|
* <p>Regeln:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>A – Log4j2-Typen dürfen nur im Logging-Adapter und im Bootstrap sichtbar sein</li>
|
||||||
|
* <li>B – Domain-Klassen dürfen keine Adapter- oder Bootstrap-Abhängigkeiten haben</li>
|
||||||
|
* <li>C – Application-Klassen dürfen keine Adapter- oder Bootstrap-Abhängigkeiten haben</li>
|
||||||
|
* <li>D – Preview-Validatoren werden in M1 nicht aus aktivem Adapter- oder Bootstrap-Code referenziert</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@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.");
|
||||||
|
}
|
||||||
-219
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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}.
|
||||||
|
*
|
||||||
|
* <p>Abgedeckte Abnahmekriterien aus AP08:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Fall 1: Kein Argument → Exit 2, nur Konsole (STDERR), kein Verzeichnis</li>
|
||||||
|
* <li>Fall 2: Mehr als ein Argument → Exit 2, nur Konsole</li>
|
||||||
|
* <li>Fall 3: Datei existiert nicht → Exit 2, Berichtdatei im übergeordneten Verzeichnis</li>
|
||||||
|
* <li>Fall 4: Pfad ist kein regulärer Dateityp → Exit 2, nur Konsole</li>
|
||||||
|
* <li>Fall 5: Datei nicht lesbar → Exit 2, Berichtdatei im übergeordneten Verzeichnis</li>
|
||||||
|
* <li>Verdict OPERATIONAL_ERROR wird korrekt gesetzt</li>
|
||||||
|
* <li>Kein Stack-Trace in STDERR</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*/
|
||||||
|
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(
|
||||||
|
"<kein Argument>", "OPERATIONAL-MISSING-ARG",
|
||||||
|
"Kein Dateipfad angegeben.");
|
||||||
|
|
||||||
|
assertEquals(Verdict.OPERATIONAL_ERROR, report.computeVerdict(),
|
||||||
|
"operationalError-Factory muss Verdict OPERATIONAL_ERROR liefern");
|
||||||
|
|
||||||
|
List<Finding> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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).
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}.
|
||||||
|
*
|
||||||
|
* <p>Abgedeckte Abnahmekriterien aus AP06/AP07:</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Aufruf ohne Argument → Exit-Code 2</li>
|
||||||
|
* <li>Aufruf mit ≥ 2 Argumenten → Exit-Code 2</li>
|
||||||
|
* <li>Aufruf mit nicht existierender Datei → Exit-Code 2</li>
|
||||||
|
* <li>Aufruf mit leerer, lesbarer Datei → Exit-Code 0</li>
|
||||||
|
* <li>Konsolenausgabe enthält Berichtinhalt</li>
|
||||||
|
* <li>Berichtdatei wird im Verzeichnis der Eingabedatei erzeugt</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>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).</p>
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}.
|
||||||
|
*
|
||||||
|
* <p>Schwerpunkt: Nachweis der korrekten ISO-8859-15-Dekodierung gemäß AP06-Abnahmekriterium
|
||||||
|
* „Byte {@code 0xA4} ergibt Euro-Zeichen €".</p>
|
||||||
|
*/
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* <p>Dieser Test ist der formale Nachweis für AP09-Abnahmekriterium
|
||||||
|
* „Integrationstest: Lauf mit Testdatei erzeugt keine ASVREC-/ASVFEH-Segmentbefunde".</p>
|
||||||
|
*/
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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."));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}.
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*/
|
||||||
|
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<Finding> specList = report.specFindings();
|
||||||
|
List<Finding> 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<Finding> 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Configuration status="WARN">
|
||||||
|
<Appenders>
|
||||||
|
<Console name="Console" target="SYSTEM_ERR">
|
||||||
|
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
|
||||||
|
</Console>
|
||||||
|
</Appenders>
|
||||||
|
<Loggers>
|
||||||
|
<Root level="WARN">
|
||||||
|
<AppenderRef ref="Console"/>
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
</Configuration>
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
================================================================
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
================================================================
|
||||||
Reference in New Issue
Block a user