Umsetzung von M1
This commit is contained in:
@@ -1,164 +1,30 @@
|
||||
package de.gecheckt.asv.adapter.in.cli;
|
||||
|
||||
import de.gecheckt.asv.domain.model.InputFile;
|
||||
import de.gecheckt.asv.adapter.out.parsing.DefaultInputFileParser;
|
||||
import de.gecheckt.asv.adapter.out.parsing.DefaultSegmentLineTokenizer;
|
||||
import de.gecheckt.asv.adapter.out.parsing.InputFileParseException;
|
||||
import de.gecheckt.asv.adapter.out.parsing.InputFileParser;
|
||||
import de.gecheckt.asv.adapter.out.parsing.SegmentLineTokenizer;
|
||||
import de.gecheckt.asv.application.DefaultInputFileValidator;
|
||||
import de.gecheckt.asv.application.InputFileValidator;
|
||||
import de.gecheckt.asv.application.field.DefaultFieldValidator;
|
||||
import de.gecheckt.asv.application.field.FieldValidator;
|
||||
import de.gecheckt.asv.application.model.ValidationResult;
|
||||
import de.gecheckt.asv.application.structure.DefaultStructureValidator;
|
||||
import de.gecheckt.asv.application.structure.StructureValidator;
|
||||
import de.gecheckt.asv.adapter.out.reporting.ValidationResultPrinter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Haupteinstiegspunkt für die ASV Format Validator CLI-Anwendung.
|
||||
*
|
||||
* Diese Anwendung validiert Dateien gegen ein segmentorientiertes Dateiformat.
|
||||
* Sie nimmt einen Dateipfad als Kommandozeilenargument entgegen, parst die Datei,
|
||||
* validiert sie und gibt die Ergebnisse auf der Konsole aus.
|
||||
* Ehemaliger Haupteinstiegspunkt des ASV-Format-Validators.
|
||||
*
|
||||
* <p><strong>Veraltet seit AP06.</strong> Die Verantwortlichkeiten wurden aufgeteilt:</p>
|
||||
* <ul>
|
||||
* <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 {
|
||||
|
||||
private static final int EXIT_CODE_SUCCESS = 0;
|
||||
private static final int EXIT_CODE_INVALID_ARGUMENTS = 1;
|
||||
private static final int EXIT_CODE_FILE_ERROR = 2;
|
||||
private static final int EXIT_CODE_VALIDATION_ERRORS = 3;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AsvValidatorApplication.class);
|
||||
|
||||
private final InputFileParser parser;
|
||||
private final InputFileValidator validator;
|
||||
private final ValidationResultPrinter printer;
|
||||
|
||||
|
||||
/**
|
||||
* Konstruktor für einen AsvValidatorApplication mit Standardkomponenten.
|
||||
*/
|
||||
public AsvValidatorApplication() {
|
||||
// Initialize all required components
|
||||
SegmentLineTokenizer tokenizer = new DefaultSegmentLineTokenizer();
|
||||
this.parser = new DefaultInputFileParser(tokenizer);
|
||||
|
||||
StructureValidator structureValidator = new DefaultStructureValidator();
|
||||
FieldValidator fieldValidator = new DefaultFieldValidator();
|
||||
this.validator = new DefaultInputFileValidator(structureValidator, fieldValidator);
|
||||
|
||||
this.printer = new ValidationResultPrinter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Konstruktor für einen AsvValidatorApplication mit den bereitgestellten Komponenten.
|
||||
* Dieser Konstruktor unterstützt Dependency Injection für bessere Testbarkeit.
|
||||
*
|
||||
* @param parser der Parser zum Parsen von Eingabedateien
|
||||
* @param validator der Validator zum Validieren geparster Dateien
|
||||
* @param printer der Printer zum Anzeigen von Validierungsergebnissen
|
||||
*/
|
||||
public AsvValidatorApplication(InputFileParser parser, InputFileValidator validator, ValidationResultPrinter printer) {
|
||||
this.parser = parser;
|
||||
this.validator = validator;
|
||||
this.printer = printer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Haupteinstiegspunkt für die Anwendung.
|
||||
*
|
||||
* @param args Kommandozeilenargumente - erwartet genau ein Argument: den Dateipfad
|
||||
* Nicht mehr verwenden. Nur noch als Kompatibilitätshülle vorhanden.
|
||||
*
|
||||
* @deprecated Verwende {@link de.gecheckt.asv.bootstrap.Main#main(String[])} stattdessen.
|
||||
*/
|
||||
@Deprecated(since = "AP06", forRemoval = true)
|
||||
public static void main(String[] args) {
|
||||
AsvValidatorApplication app = new AsvValidatorApplication();
|
||||
int exitCode = app.run(args);
|
||||
System.exit(exitCode);
|
||||
de.gecheckt.asv.bootstrap.Main.main(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt die Anwendung mit den bereitgestellten Argumenten aus.
|
||||
*
|
||||
* @param args Kommandozeilenargumente
|
||||
* @return Exit-Code (0 für Erfolg, ungleich 0 für Fehler)
|
||||
*/
|
||||
public int run(String[] args) {
|
||||
// Validate command line arguments
|
||||
if (args.length != 1) {
|
||||
printUsage();
|
||||
return EXIT_CODE_INVALID_ARGUMENTS;
|
||||
}
|
||||
|
||||
String filePath = args[0];
|
||||
|
||||
try {
|
||||
// Parse the file
|
||||
InputFile inputFile = parseFile(filePath);
|
||||
|
||||
// Validate the parsed file
|
||||
ValidationResult result = validator.validate(inputFile);
|
||||
|
||||
// Output results
|
||||
printer.printToConsole(result);
|
||||
|
||||
// Return appropriate exit code based on validation results
|
||||
return result.hasErrors() ? EXIT_CODE_VALIDATION_ERRORS : EXIT_CODE_SUCCESS;
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.error("Fehler beim Lesen der Datei: {}", e.getMessage(), e);
|
||||
System.err.println("Fehler beim Lesen der Datei: " + e.getMessage());
|
||||
return EXIT_CODE_FILE_ERROR;
|
||||
} catch (InputFileParseException e) {
|
||||
logger.error("Fehler beim Parsen der Datei: {}", e.getMessage(), e);
|
||||
System.err.println("Fehler beim Parsen der Datei: " + e.getMessage());
|
||||
return EXIT_CODE_FILE_ERROR;
|
||||
} catch (Exception e) {
|
||||
logger.error("Unerwarteter Fehler während der Validierung: {}", e.getMessage(), e);
|
||||
System.err.println("Unerwarteter Fehler während der Validierung: " + e.getMessage());
|
||||
return EXIT_CODE_FILE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parst eine Datei unter dem gegebenen Pfad.
|
||||
*
|
||||
* @param filePath Pfad zur zu parsenden Datei
|
||||
* @return geparstes InputFile-Objekt
|
||||
* @throws IOException wenn die Datei nicht gelesen werden kann
|
||||
* @throws InputFileParseException wenn die Datei nicht geparst werden kann
|
||||
*/
|
||||
private InputFile parseFile(String filePath) throws IOException, InputFileParseException {
|
||||
Path path = Paths.get(filePath);
|
||||
if (!Files.exists(path)) {
|
||||
throw new IOException("File does not exist: " + filePath);
|
||||
}
|
||||
|
||||
if (!Files.isRegularFile(path)) {
|
||||
throw new IOException("Path is not a regular file: " + filePath);
|
||||
}
|
||||
|
||||
if (!Files.isReadable(path)) {
|
||||
throw new IOException("File is not readable: " + filePath);
|
||||
}
|
||||
|
||||
String fileContent = Files.readString(path, StandardCharsets.UTF_8);
|
||||
return parser.parse(path.getFileName().toString(), fileContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Nutzungsinformationen auf der Konsole aus.
|
||||
*/
|
||||
private void printUsage() {
|
||||
System.out.println("ASV Format Validator");
|
||||
System.out.println("Verwendung: java -jar asv-format-validator.jar <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;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.core.LoggerContext;
|
||||
import org.apache.logging.log4j.core.appender.FileAppender;
|
||||
import org.apache.logging.log4j.core.config.Configuration;
|
||||
import org.apache.logging.log4j.core.config.LoggerConfig;
|
||||
import org.apache.logging.log4j.core.layout.PatternLayout;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Konfiguriert den Log4j2-Logging-Adapter programmatisch.
|
||||
* Erlaubt es, den Zielpfad der Log-Datei zur Laufzeit zu setzen.
|
||||
* Log4j2-Typen dürfen in diesem Paket direkt verwendet werden.
|
||||
*
|
||||
* <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 {
|
||||
|
||||
private static final String PATTERN =
|
||||
"%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n";
|
||||
|
||||
/**
|
||||
* Setzt den Zielpfad der Log-Datei für diesen Lauf.
|
||||
* Die tatsächliche dynamische Umleitung wird in AP07 implementiert.
|
||||
* Setzt den Zielpfad der Log-Datei für diesen Lauf und konfiguriert Log4j2
|
||||
* programmatisch um.
|
||||
*
|
||||
* @param logFile Pfad zur Log-Datei
|
||||
* <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) {
|
||||
// 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;
|
||||
|
||||
/**
|
||||
* Default implementation of FieldValidator that checks general field rules.
|
||||
*
|
||||
* Rules checked:
|
||||
* 1. Field.rawValue must not be empty
|
||||
* 2. Field.rawValue must not consist only of whitespaces
|
||||
* 3. Field positions within a segment should be consecutive without gaps, starting at 1
|
||||
* 4. If fieldName is set, it must not be empty or only whitespace
|
||||
* M3-Vorbau. In M1 bewusst nicht im produktiven Lauf verdrahtet.
|
||||
* Wird ab M3 wieder aktiviert und gegen die finalen Regelklassifikationen
|
||||
* (V1-V/T/N/K) aus fachliche-anforderungen.md bewertet.
|
||||
*
|
||||
* @see <a href="docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md">E-01</a>
|
||||
*
|
||||
* <p>Standardimplementierung des FieldValidator, die allgemeine Feldregeln prüft.</p>
|
||||
*
|
||||
* <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 {
|
||||
|
||||
|
||||
+30
-22
@@ -13,28 +13,36 @@ import de.gecheckt.asv.application.model.ValidationResult;
|
||||
import de.gecheckt.asv.application.model.ValidationSeverity;
|
||||
|
||||
/**
|
||||
* Standardimplementierung des StructureValidator, die allgemeine Strukturregeln prüft.
|
||||
*
|
||||
* Geprüfte Regeln:
|
||||
* 1. Die Eingabedatei muss mindestens eine Nachricht enthalten
|
||||
* 2. Jede Nachricht muss mindestens ein Segment enthalten
|
||||
* 3. Segmentnamen dürfen nicht leer sein
|
||||
* 4. Feldpositionen innerhalb eines Segments müssen eindeutig und positiv sein
|
||||
* 5. Segmentpositionen innerhalb einer Nachricht müssen eindeutig und positiv sein
|
||||
* 6. Nachrichtenpositionen innerhalb einer Eingabedatei müssen eindeutig und positiv sein
|
||||
* 7. UNH- und UNT-Referenznummern müssen innerhalb einer Nachricht übereinstimmen
|
||||
* 8. Die im UNT angegebene Segmentanzahl muss der tatsächlichen Anzahl der Segmente entsprechen
|
||||
* 9. Eine Nachricht muss mindestens ein UNH-Segment enthalten
|
||||
* 10. Eine Nachricht muss mindestens ein UNT-Segment enthalten
|
||||
* 11. UNH muss vor UNT stehen
|
||||
* 12. Der Nachrichtentyp in UNH/S009/0065 darf nur ASVREC oder ASVFEH sein
|
||||
* 13. Für Nachrichten vom Typ ASVREC müssen die Segmente IFA, REA und IVA vorhanden sein
|
||||
* 14. Für Nachrichten vom Typ ASVREC muss die Reihenfolge IFA vor REA vor IVA eingehalten werden
|
||||
* 15. Für ASVREC mit Rechnungskennzeichen "0" in REA müssen DGN und LEA vorhanden sein
|
||||
* 16. Für ASVREC mit Rechnungskennzeichen "1" in REA dürfen DGN und LEA nicht vorhanden sein
|
||||
* 17. Für ASVREC mit Rechnungskennzeichen "1" in REA muss der Rechnungsbetrag "0,00" sein
|
||||
* 18. Für ASVREC müssen IFA, REA und IVA jeweils genau einmal vorkommen
|
||||
* 19. Für ASVFEH-Nachrichten muss mindestens ein FHL-Segment vorhanden sein
|
||||
* M3-Vorbau. In M1 bewusst nicht im produktiven Lauf verdrahtet.
|
||||
* Wird ab M3 wieder aktiviert und gegen die finalen Regelklassifikationen
|
||||
* (V1-V/T/N/K) aus fachliche-anforderungen.md bewertet.
|
||||
*
|
||||
* @see <a href="docs/arbeitspakete/m1/E00-entscheidungsprotokoll.md">E-01</a>
|
||||
*
|
||||
* <p>Standardimplementierung des StructureValidator, die allgemeine Strukturregeln prüft.</p>
|
||||
*
|
||||
* <p>Geprüfte Regeln:</p>
|
||||
* <ol>
|
||||
* <li>Die Eingabedatei muss mindestens eine Nachricht enthalten</li>
|
||||
* <li>Jede Nachricht muss mindestens ein Segment enthalten</li>
|
||||
* <li>Segmentnamen dürfen nicht leer sein</li>
|
||||
* <li>Feldpositionen innerhalb eines Segments müssen eindeutig und positiv sein</li>
|
||||
* <li>Segmentpositionen innerhalb einer Nachricht müssen eindeutig und positiv sein</li>
|
||||
* <li>Nachrichtenpositionen innerhalb einer Eingabedatei müssen eindeutig und positiv sein</li>
|
||||
* <li>UNH- und UNT-Referenznummern müssen innerhalb einer Nachricht übereinstimmen</li>
|
||||
* <li>Die im UNT angegebene Segmentanzahl muss der tatsächlichen Anzahl der Segmente entsprechen</li>
|
||||
* <li>Eine Nachricht muss mindestens ein UNH-Segment enthalten</li>
|
||||
* <li>Eine Nachricht muss mindestens ein UNT-Segment enthalten</li>
|
||||
* <li>UNH muss vor UNT stehen</li>
|
||||
* <li>Der Nachrichtentyp in UNH/S009/0065 darf nur ASVREC oder ASVFEH sein</li>
|
||||
* <li>Für Nachrichten vom Typ ASVREC müssen die Segmente IFA, REA und IVA vorhanden sein</li>
|
||||
* <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 {
|
||||
|
||||
|
||||
@@ -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"?>
|
||||
<!--
|
||||
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">
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_ERR">
|
||||
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
|
||||
</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"/>
|
||||
</File>
|
||||
</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>
|
||||
Reference in New Issue
Block a user