M4 Nachbearbeitung Anwendungskern testseitig geschärft
This commit is contained in:
@@ -356,6 +356,365 @@ class DocumentProcessingCoordinatorTest {
|
|||||||
"Attempt number must be taken from the repository");
|
"Attempt number must be taken from the repository");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Rückgabewert von process() und processDeferredOutcome()
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_newDocument_preCheckPassed_returnsTrue() {
|
||||||
|
// Prüft, dass process() bei erfolgreichem Persistieren true zurückgibt
|
||||||
|
recordRepo.setLookupResult(new DocumentUnknown());
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckPassed(
|
||||||
|
candidate, new PdfExtractionSuccess("text", new PdfPageCount(1)));
|
||||||
|
|
||||||
|
boolean result = processor.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
assertTrue(result, "process() muss bei erfolgreichem Persistieren true zurückgeben");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_newDocument_persistenceFailure_returnsFalse() {
|
||||||
|
// Prüft, dass process() bei Persistenzfehler false zurückgibt (nicht true)
|
||||||
|
recordRepo.setLookupResult(new DocumentUnknown());
|
||||||
|
unitOfWorkPort.failOnExecute = true;
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckPassed(
|
||||||
|
candidate, new PdfExtractionSuccess("text", new PdfPageCount(1)));
|
||||||
|
|
||||||
|
boolean result = processor.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
assertFalse(result, "process() muss bei Persistenzfehler false zurückgeben");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_knownDocument_persistenceFailure_returnsFalse() {
|
||||||
|
// Prüft, dass process() bei Persistenzfehler im known-Pfad false zurückgibt
|
||||||
|
DocumentRecord existingRecord = buildRecord(ProcessingStatus.FAILED_RETRYABLE, new FailureCounters(0, 1));
|
||||||
|
recordRepo.setLookupResult(new DocumentKnownProcessable(existingRecord));
|
||||||
|
unitOfWorkPort.failOnExecute = true;
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckPassed(
|
||||||
|
candidate, new PdfExtractionSuccess("text", new PdfPageCount(1)));
|
||||||
|
|
||||||
|
boolean result = processor.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
assertFalse(result, "process() muss bei Persistenzfehler im known-Pfad false zurückgeben");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_knownDocument_preCheckPassed_returnsTrue() {
|
||||||
|
// Prüft, dass process() bei erfolgreichem Persistieren im known-Pfad true zurückgibt
|
||||||
|
DocumentRecord existingRecord = buildRecord(ProcessingStatus.FAILED_RETRYABLE, new FailureCounters(0, 1));
|
||||||
|
recordRepo.setLookupResult(new DocumentKnownProcessable(existingRecord));
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckPassed(
|
||||||
|
candidate, new PdfExtractionSuccess("text", new PdfPageCount(1)));
|
||||||
|
|
||||||
|
boolean result = processor.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
assertTrue(result, "process() muss bei erfolgreichem Persistieren im known-Pfad true zurückgeben");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_terminalSuccess_skipPersistenceSuccess_returnsTrue() {
|
||||||
|
// Prüft, dass process() beim Skip-Pfad (SUCCESS) true zurückgibt
|
||||||
|
DocumentRecord existingRecord = buildRecord(ProcessingStatus.SUCCESS, FailureCounters.zero());
|
||||||
|
recordRepo.setLookupResult(new DocumentTerminalSuccess(existingRecord));
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckPassed(
|
||||||
|
candidate, new PdfExtractionSuccess("text", new PdfPageCount(1)));
|
||||||
|
|
||||||
|
boolean result = processor.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
assertTrue(result, "process() muss beim erfolgreichen Skip-Persistieren true zurückgeben");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_terminalFinalFailure_skipPersistenceSuccess_returnsTrue() {
|
||||||
|
// Prüft, dass process() beim Skip-Pfad (FAILED_FINAL) true zurückgibt
|
||||||
|
DocumentRecord existingRecord = buildRecord(ProcessingStatus.FAILED_FINAL, new FailureCounters(2, 0));
|
||||||
|
recordRepo.setLookupResult(new DocumentTerminalFinalFailure(existingRecord));
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckFailed(
|
||||||
|
candidate, PreCheckFailureReason.NO_USABLE_TEXT);
|
||||||
|
|
||||||
|
boolean result = processor.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
assertTrue(result, "process() muss beim erfolgreichen Skip-Persistieren true zurückgeben");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_terminalSuccess_skipPersistenceFailure_returnsFalse() {
|
||||||
|
// Prüft, dass process() false zurückgibt, wenn das Skip-Persistieren fehlschlägt
|
||||||
|
DocumentRecord existingRecord = buildRecord(ProcessingStatus.SUCCESS, FailureCounters.zero());
|
||||||
|
recordRepo.setLookupResult(new DocumentTerminalSuccess(existingRecord));
|
||||||
|
unitOfWorkPort.failOnExecute = true;
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckPassed(
|
||||||
|
candidate, new PdfExtractionSuccess("text", new PdfPageCount(1)));
|
||||||
|
|
||||||
|
boolean result = processor.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
assertFalse(result, "process() muss false zurückgeben, wenn Skip-Persistierung fehlschlägt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_persistenceLookupFailure_returnsFalse() {
|
||||||
|
// Prüft, dass process() bei Lookup-Fehler false zurückgibt
|
||||||
|
recordRepo.setLookupResult(new PersistenceLookupTechnicalFailure("DB nicht verfügbar", null));
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckPassed(
|
||||||
|
candidate, new PdfExtractionSuccess("text", new PdfPageCount(1)));
|
||||||
|
|
||||||
|
boolean result = processor.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
assertFalse(result, "process() muss bei Lookup-Fehler false zurückgeben");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Fehlermeldungsinhalt (buildFailureMessage)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_newDocument_firstContentError_failureMessageContainsContentErrorCount() {
|
||||||
|
// Prüft, dass die Fehlermeldung die Fehleranzahl enthält (nicht leer ist)
|
||||||
|
recordRepo.setLookupResult(new DocumentUnknown());
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckFailed(
|
||||||
|
candidate, PreCheckFailureReason.NO_USABLE_TEXT);
|
||||||
|
|
||||||
|
processor.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
ProcessingAttempt attempt = attemptRepo.savedAttempts.get(0);
|
||||||
|
assertNotNull(attempt.failureMessage(), "Fehlermeldung darf nicht null sein bei FAILED_RETRYABLE");
|
||||||
|
assertFalse(attempt.failureMessage().isBlank(),
|
||||||
|
"Fehlermeldung darf nicht leer sein bei FAILED_RETRYABLE");
|
||||||
|
assertTrue(attempt.failureMessage().contains("ContentErrors=1"),
|
||||||
|
"Fehlermeldung muss den Inhaltsfehler-Zähler enthalten: " + attempt.failureMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_knownDocument_secondContentError_failureMessageContainsFinalStatus() {
|
||||||
|
// Prüft, dass die Fehlermeldung bei FAILED_FINAL den Endzustand enthält
|
||||||
|
DocumentRecord existingRecord = buildRecord(ProcessingStatus.FAILED_RETRYABLE, new FailureCounters(1, 0));
|
||||||
|
recordRepo.setLookupResult(new DocumentKnownProcessable(existingRecord));
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckFailed(
|
||||||
|
candidate, PreCheckFailureReason.PAGE_LIMIT_EXCEEDED);
|
||||||
|
|
||||||
|
processor.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
ProcessingAttempt attempt = attemptRepo.savedAttempts.get(0);
|
||||||
|
assertNotNull(attempt.failureMessage(), "Fehlermeldung darf nicht null sein bei FAILED_FINAL");
|
||||||
|
assertFalse(attempt.failureMessage().isBlank(),
|
||||||
|
"Fehlermeldung darf nicht leer sein bei FAILED_FINAL");
|
||||||
|
assertTrue(attempt.failureMessage().contains("ContentErrors=2"),
|
||||||
|
"Fehlermeldung muss den aktualisierten Inhaltsfehler-Zähler enthalten: " + attempt.failureMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_newDocument_technicalError_failureMessageContainsTransientErrorCount() {
|
||||||
|
// Prüft, dass die Fehlermeldung bei transientem Fehler den Transient-Zähler enthält
|
||||||
|
recordRepo.setLookupResult(new DocumentUnknown());
|
||||||
|
DocumentProcessingOutcome outcome = new TechnicalDocumentError(candidate, "Timeout", null);
|
||||||
|
|
||||||
|
processor.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
ProcessingAttempt attempt = attemptRepo.savedAttempts.get(0);
|
||||||
|
assertNotNull(attempt.failureMessage());
|
||||||
|
assertTrue(attempt.failureMessage().contains("TransientErrors=1"),
|
||||||
|
"Fehlermeldung muss den Transient-Fehler-Zähler enthalten: " + attempt.failureMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_newDocument_preCheckPassed_failureClassAndMessageAreNull() {
|
||||||
|
// Prüft, dass bei Erfolg failureClass und failureMessage null sind
|
||||||
|
recordRepo.setLookupResult(new DocumentUnknown());
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckPassed(
|
||||||
|
candidate, new PdfExtractionSuccess("text", new PdfPageCount(1)));
|
||||||
|
|
||||||
|
processor.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
ProcessingAttempt attempt = attemptRepo.savedAttempts.get(0);
|
||||||
|
assertNull(attempt.failureClass(), "Bei Erfolg muss failureClass null sein");
|
||||||
|
assertNull(attempt.failureMessage(), "Bei Erfolg muss failureMessage null sein");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Zeitstempel-Korrektheit in buildUpdatedDocumentRecord (negierte Bedingung)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_knownDocument_preCheckPassed_lastSuccessInstantSetAndLastFailureInstantFromPreviousRecord() {
|
||||||
|
// Prüft, dass bei SUCCESS am known-Dokument lastSuccessInstant gesetzt
|
||||||
|
// und lastFailureInstant aus dem Vorgänger-Datensatz übernommen wird
|
||||||
|
Instant previousFailureInstant = Instant.parse("2025-01-15T10:00:00Z");
|
||||||
|
DocumentRecord existingRecord = new DocumentRecord(
|
||||||
|
fingerprint,
|
||||||
|
new SourceDocumentLocator("/tmp/test.pdf"),
|
||||||
|
"test.pdf",
|
||||||
|
ProcessingStatus.FAILED_RETRYABLE,
|
||||||
|
new FailureCounters(0, 1),
|
||||||
|
previousFailureInstant, // lastFailureInstant vorhanden
|
||||||
|
null, // noch kein Erfolgszeitpunkt
|
||||||
|
Instant.now(),
|
||||||
|
Instant.now()
|
||||||
|
);
|
||||||
|
recordRepo.setLookupResult(new DocumentKnownProcessable(existingRecord));
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckPassed(
|
||||||
|
candidate, new PdfExtractionSuccess("text", new PdfPageCount(1)));
|
||||||
|
|
||||||
|
processor.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
DocumentRecord updated = recordRepo.updatedRecords.get(0);
|
||||||
|
assertNotNull(updated.lastSuccessInstant(),
|
||||||
|
"lastSuccessInstant muss nach erfolgreichem Verarbeiten gesetzt sein");
|
||||||
|
assertEquals(previousFailureInstant, updated.lastFailureInstant(),
|
||||||
|
"lastFailureInstant muss bei SUCCESS den Vorgänger-Wert beibehalten");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_knownDocument_contentError_lastFailureInstantSetAndLastSuccessInstantFromPreviousRecord() {
|
||||||
|
// Prüft, dass bei FAILURE am known-Dokument lastFailureInstant gesetzt
|
||||||
|
// und lastSuccessInstant aus dem Vorgänger-Datensatz übernommen wird (negierte Bedingung abdecken)
|
||||||
|
Instant previousSuccessInstant = Instant.parse("2025-01-10T08:00:00Z");
|
||||||
|
DocumentRecord existingRecord = new DocumentRecord(
|
||||||
|
fingerprint,
|
||||||
|
new SourceDocumentLocator("/tmp/test.pdf"),
|
||||||
|
"test.pdf",
|
||||||
|
ProcessingStatus.FAILED_RETRYABLE,
|
||||||
|
new FailureCounters(0, 0),
|
||||||
|
null, // noch keine Fehlzeit
|
||||||
|
previousSuccessInstant, // vorheriger Erfolg vorhanden
|
||||||
|
Instant.now(),
|
||||||
|
Instant.now()
|
||||||
|
);
|
||||||
|
recordRepo.setLookupResult(new DocumentKnownProcessable(existingRecord));
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckFailed(
|
||||||
|
candidate, PreCheckFailureReason.NO_USABLE_TEXT);
|
||||||
|
|
||||||
|
processor.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
DocumentRecord updated = recordRepo.updatedRecords.get(0);
|
||||||
|
assertNotNull(updated.lastFailureInstant(),
|
||||||
|
"lastFailureInstant muss nach Inhaltsfehler gesetzt sein");
|
||||||
|
assertEquals(previousSuccessInstant, updated.lastSuccessInstant(),
|
||||||
|
"lastSuccessInstant muss bei Fehler den Vorgänger-Wert beibehalten");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Logger-Delegation (VoidMethodCallMutator abdecken mit CapturingLogger)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_persistenceLookupFailure_logsErrorMessage() {
|
||||||
|
// Prüft, dass bei Lookup-Fehler ein Fehler-Log-Eintrag erzeugt wird
|
||||||
|
CapturingProcessingLogger capturingLogger = new CapturingProcessingLogger();
|
||||||
|
DocumentProcessingCoordinator coordinatorWithCapturingLogger =
|
||||||
|
new DocumentProcessingCoordinator(recordRepo, attemptRepo, unitOfWorkPort, capturingLogger);
|
||||||
|
recordRepo.setLookupResult(new PersistenceLookupTechnicalFailure("Datenbank nicht erreichbar", null));
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckPassed(
|
||||||
|
candidate, new PdfExtractionSuccess("text", new PdfPageCount(1)));
|
||||||
|
|
||||||
|
coordinatorWithCapturingLogger.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
assertTrue(capturingLogger.errorCallCount > 0,
|
||||||
|
"Bei Lookup-Fehler muss ein Fehler geloggt werden");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_terminalSuccess_skipPath_logsInfoMessage() {
|
||||||
|
// Prüft, dass beim Überspringen eines bereits erfolgreich verarbeiteten Dokuments geloggt wird
|
||||||
|
CapturingProcessingLogger capturingLogger = new CapturingProcessingLogger();
|
||||||
|
DocumentProcessingCoordinator coordinatorWithCapturingLogger =
|
||||||
|
new DocumentProcessingCoordinator(recordRepo, attemptRepo, unitOfWorkPort, capturingLogger);
|
||||||
|
DocumentRecord existingRecord = buildRecord(ProcessingStatus.SUCCESS, FailureCounters.zero());
|
||||||
|
recordRepo.setLookupResult(new DocumentTerminalSuccess(existingRecord));
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckPassed(
|
||||||
|
candidate, new PdfExtractionSuccess("text", new PdfPageCount(1)));
|
||||||
|
|
||||||
|
coordinatorWithCapturingLogger.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
assertTrue(capturingLogger.infoCallCount > 0,
|
||||||
|
"Beim Überspringen eines erfolgreich verarbeiteten Dokuments muss eine Info geloggt werden");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_terminalFinalFailure_skipPath_logsInfoMessage() {
|
||||||
|
// Prüft, dass beim Überspringen eines final fehlgeschlagenen Dokuments geloggt wird
|
||||||
|
CapturingProcessingLogger capturingLogger = new CapturingProcessingLogger();
|
||||||
|
DocumentProcessingCoordinator coordinatorWithCapturingLogger =
|
||||||
|
new DocumentProcessingCoordinator(recordRepo, attemptRepo, unitOfWorkPort, capturingLogger);
|
||||||
|
DocumentRecord existingRecord = buildRecord(ProcessingStatus.FAILED_FINAL, new FailureCounters(2, 0));
|
||||||
|
recordRepo.setLookupResult(new DocumentTerminalFinalFailure(existingRecord));
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckFailed(
|
||||||
|
candidate, PreCheckFailureReason.NO_USABLE_TEXT);
|
||||||
|
|
||||||
|
coordinatorWithCapturingLogger.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
assertTrue(capturingLogger.infoCallCount > 0,
|
||||||
|
"Beim Überspringen eines final fehlgeschlagenen Dokuments muss eine Info geloggt werden");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_newDocument_preCheckPassed_logsInfo() {
|
||||||
|
// Prüft, dass nach erfolgreichem Persistieren einer neuen Datei geloggt wird
|
||||||
|
CapturingProcessingLogger capturingLogger = new CapturingProcessingLogger();
|
||||||
|
DocumentProcessingCoordinator coordinatorWithCapturingLogger =
|
||||||
|
new DocumentProcessingCoordinator(recordRepo, attemptRepo, unitOfWorkPort, capturingLogger);
|
||||||
|
recordRepo.setLookupResult(new DocumentUnknown());
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckPassed(
|
||||||
|
candidate, new PdfExtractionSuccess("text", new PdfPageCount(1)));
|
||||||
|
|
||||||
|
coordinatorWithCapturingLogger.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
assertTrue(capturingLogger.infoCallCount > 0,
|
||||||
|
"Nach erfolgreichem Verarbeiten eines neuen Dokuments muss eine Info geloggt werden");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_newDocument_persistenceFailure_logsError() {
|
||||||
|
// Prüft, dass bei Persistenzfehler ein Fehler-Log-Eintrag erzeugt wird
|
||||||
|
CapturingProcessingLogger capturingLogger = new CapturingProcessingLogger();
|
||||||
|
DocumentProcessingCoordinator coordinatorWithCapturingLogger =
|
||||||
|
new DocumentProcessingCoordinator(recordRepo, attemptRepo, unitOfWorkPort, capturingLogger);
|
||||||
|
recordRepo.setLookupResult(new DocumentUnknown());
|
||||||
|
unitOfWorkPort.failOnExecute = true;
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckPassed(
|
||||||
|
candidate, new PdfExtractionSuccess("text", new PdfPageCount(1)));
|
||||||
|
|
||||||
|
coordinatorWithCapturingLogger.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
assertTrue(capturingLogger.errorCallCount > 0,
|
||||||
|
"Bei Persistenzfehler muss ein Fehler geloggt werden");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_terminalSuccess_successfulSkip_logsDebug() {
|
||||||
|
// Prüft, dass nach erfolgreichem Skip-Persistieren ein Debug-Log erzeugt wird (persistSkipAttempt L301)
|
||||||
|
CapturingProcessingLogger capturingLogger = new CapturingProcessingLogger();
|
||||||
|
DocumentProcessingCoordinator coordinatorWithCapturingLogger =
|
||||||
|
new DocumentProcessingCoordinator(recordRepo, attemptRepo, unitOfWorkPort, capturingLogger);
|
||||||
|
DocumentRecord existingRecord = buildRecord(ProcessingStatus.SUCCESS, FailureCounters.zero());
|
||||||
|
recordRepo.setLookupResult(new DocumentTerminalSuccess(existingRecord));
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckPassed(
|
||||||
|
candidate, new PdfExtractionSuccess("text", new PdfPageCount(1)));
|
||||||
|
|
||||||
|
coordinatorWithCapturingLogger.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
assertTrue(capturingLogger.debugCallCount > 0,
|
||||||
|
"Nach erfolgreichem Skip-Persistieren muss ein Debug-Eintrag geloggt werden");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_terminalSuccess_skipPersistenceFailure_logsError() {
|
||||||
|
// Prüft, dass bei Persistenzfehler im Skip-Pfad ein Fehler geloggt wird (persistSkipAttempt L306)
|
||||||
|
CapturingProcessingLogger capturingLogger = new CapturingProcessingLogger();
|
||||||
|
DocumentProcessingCoordinator coordinatorWithCapturingLogger =
|
||||||
|
new DocumentProcessingCoordinator(recordRepo, attemptRepo, unitOfWorkPort, capturingLogger);
|
||||||
|
DocumentRecord existingRecord = buildRecord(ProcessingStatus.SUCCESS, FailureCounters.zero());
|
||||||
|
recordRepo.setLookupResult(new DocumentTerminalSuccess(existingRecord));
|
||||||
|
unitOfWorkPort.failOnExecute = true;
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckPassed(
|
||||||
|
candidate, new PdfExtractionSuccess("text", new PdfPageCount(1)));
|
||||||
|
|
||||||
|
coordinatorWithCapturingLogger.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
assertTrue(capturingLogger.errorCallCount > 0,
|
||||||
|
"Bei Persistenzfehler im Skip-Pfad muss ein Fehler geloggt werden");
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -490,4 +849,32 @@ class DocumentProcessingCoordinatorTest {
|
|||||||
// No-op
|
// No-op
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Zählt Logger-Aufrufe je Level, um VoidMethodCallMutator-Mutationen zu erkennen. */
|
||||||
|
private static class CapturingProcessingLogger implements ProcessingLogger {
|
||||||
|
int infoCallCount = 0;
|
||||||
|
int debugCallCount = 0;
|
||||||
|
int warnCallCount = 0;
|
||||||
|
int errorCallCount = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void info(String message, Object... args) {
|
||||||
|
infoCallCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void debug(String message, Object... args) {
|
||||||
|
debugCallCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void warn(String message, Object... args) {
|
||||||
|
warnCallCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void error(String message, Object... args) {
|
||||||
|
errorCallCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -516,6 +516,221 @@ class BatchRunProcessingUseCaseTest {
|
|||||||
assertFalse(outcome.isSuccess(), "Cannot be SUCCESS when persistence failed for any document");
|
assertFalse(outcome.isSuccess(), "Cannot be SUCCESS when persistence failed for any document");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Logger-Delegation (VoidMethodCallMutator abdecken)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void execute_fingerprintFailure_logsWarning() throws Exception {
|
||||||
|
// Prüft, dass bei Fingerprint-Fehler eine Warnung geloggt wird (handleFingerprintError)
|
||||||
|
CapturingProcessingLogger capturingLogger = new CapturingProcessingLogger();
|
||||||
|
RuntimeConfiguration config = buildConfig(tempDir);
|
||||||
|
|
||||||
|
SourceDocumentCandidate candidate = makeCandidate("unreadable.pdf");
|
||||||
|
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
|
||||||
|
|
||||||
|
FingerprintPort alwaysFailingFingerprintPort = c ->
|
||||||
|
new FingerprintTechnicalError("Datei nicht lesbar", null);
|
||||||
|
|
||||||
|
DefaultBatchRunProcessingUseCase useCase = new DefaultBatchRunProcessingUseCase(
|
||||||
|
config, new MockRunLockPort(), candidatesPort, new NoOpExtractionPort(),
|
||||||
|
alwaysFailingFingerprintPort, new NoOpDocumentProcessingCoordinator(),
|
||||||
|
capturingLogger);
|
||||||
|
|
||||||
|
useCase.execute(new BatchRunContext(new RunId("fp-warn"), Instant.now()));
|
||||||
|
|
||||||
|
assertTrue(capturingLogger.warnCallCount > 0,
|
||||||
|
"Bei Fingerprint-Fehler muss eine Warnung geloggt werden");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void execute_sourceAccessException_logsError() throws Exception {
|
||||||
|
// Prüft, dass bei Quellordner-Zugriffsfehler ein Fehler geloggt wird
|
||||||
|
CapturingProcessingLogger capturingLogger = new CapturingProcessingLogger();
|
||||||
|
RuntimeConfiguration config = buildConfig(tempDir);
|
||||||
|
|
||||||
|
SourceDocumentCandidatesPort failingPort = () -> {
|
||||||
|
throw new SourceDocumentAccessException("Quellordner nicht lesbar");
|
||||||
|
};
|
||||||
|
|
||||||
|
DefaultBatchRunProcessingUseCase useCase = new DefaultBatchRunProcessingUseCase(
|
||||||
|
config, new MockRunLockPort(), failingPort, new NoOpExtractionPort(),
|
||||||
|
new AlwaysSuccessFingerprintPort(), new NoOpDocumentProcessingCoordinator(),
|
||||||
|
capturingLogger);
|
||||||
|
|
||||||
|
useCase.execute(new BatchRunContext(new RunId("source-err"), Instant.now()));
|
||||||
|
|
||||||
|
assertTrue(capturingLogger.errorCallCount > 0,
|
||||||
|
"Bei Quellordner-Zugriffsfehler muss ein Fehler geloggt werden");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void execute_withPersistenceFailure_logsWarning() throws Exception {
|
||||||
|
// Prüft, dass nach Batch-Lauf mit Persistenzfehler eine Warnung geloggt wird
|
||||||
|
CapturingProcessingLogger capturingLogger = new CapturingProcessingLogger();
|
||||||
|
RuntimeConfiguration config = buildConfig(tempDir);
|
||||||
|
|
||||||
|
SourceDocumentCandidate candidate = makeCandidate("doc.pdf");
|
||||||
|
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
|
||||||
|
FixedExtractionPort extractionPort = new FixedExtractionPort(
|
||||||
|
new PdfExtractionSuccess("text", new PdfPageCount(1)));
|
||||||
|
|
||||||
|
// Coordinator der immer Persistenzfehler zurückgibt
|
||||||
|
DocumentProcessingCoordinator failingCoordinator = new DocumentProcessingCoordinator(
|
||||||
|
new NoOpDocumentRecordRepository(), new NoOpProcessingAttemptRepository(),
|
||||||
|
new NoOpUnitOfWorkPort(), new NoOpProcessingLogger()) {
|
||||||
|
@Override
|
||||||
|
public boolean processDeferredOutcome(
|
||||||
|
de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate c,
|
||||||
|
de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint fp,
|
||||||
|
de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext ctx,
|
||||||
|
java.time.Instant start,
|
||||||
|
java.util.function.Function<de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate, de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome> exec) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
DefaultBatchRunProcessingUseCase useCase = new DefaultBatchRunProcessingUseCase(
|
||||||
|
config, new MockRunLockPort(), candidatesPort, extractionPort,
|
||||||
|
new AlwaysSuccessFingerprintPort(), failingCoordinator, capturingLogger);
|
||||||
|
|
||||||
|
useCase.execute(new BatchRunContext(new RunId("persist-warn"), Instant.now()));
|
||||||
|
|
||||||
|
assertTrue(capturingLogger.warnCallCount > 0,
|
||||||
|
"Nach Batch-Lauf mit Persistenzfehler muss eine Warnung geloggt werden");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void execute_batchStart_logsInfo() throws Exception {
|
||||||
|
// Prüft, dass beim Batch-Start mindestens die erwarteten Info-Einträge geloggt werden.
|
||||||
|
// Erwartete info()-Aufrufe ohne Kandidaten: execute L130 + execute L145 + processCandidates L178 + L190 = 4
|
||||||
|
CapturingProcessingLogger capturingLogger = new CapturingProcessingLogger();
|
||||||
|
RuntimeConfiguration config = buildConfig(tempDir);
|
||||||
|
|
||||||
|
DefaultBatchRunProcessingUseCase useCase = new DefaultBatchRunProcessingUseCase(
|
||||||
|
config, new MockRunLockPort(), new EmptyCandidatesPort(), new NoOpExtractionPort(),
|
||||||
|
new AlwaysSuccessFingerprintPort(), new NoOpDocumentProcessingCoordinator(),
|
||||||
|
capturingLogger);
|
||||||
|
|
||||||
|
useCase.execute(new BatchRunContext(new RunId("start-log"), Instant.now()));
|
||||||
|
|
||||||
|
// Ohne Kandidaten: L130 + L145 + L178 + L190 = 4 info()-Aufrufe
|
||||||
|
assertTrue(capturingLogger.infoCallCount >= 4,
|
||||||
|
"Batch-Start muss mindestens 4 Info-Einträge loggen (L130/L145/L178/L190), war: "
|
||||||
|
+ capturingLogger.infoCallCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void execute_lockUnavailable_logsWarning() throws Exception {
|
||||||
|
// Prüft, dass bei nicht verfügbarem Lock eine Warnung geloggt wird
|
||||||
|
CapturingProcessingLogger capturingLogger = new CapturingProcessingLogger();
|
||||||
|
RuntimeConfiguration config = buildConfig(tempDir);
|
||||||
|
|
||||||
|
CountingRunLockPort lockPort = new CountingRunLockPort(true);
|
||||||
|
DefaultBatchRunProcessingUseCase useCase = new DefaultBatchRunProcessingUseCase(
|
||||||
|
config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort(),
|
||||||
|
new AlwaysSuccessFingerprintPort(), new NoOpDocumentProcessingCoordinator(),
|
||||||
|
capturingLogger);
|
||||||
|
|
||||||
|
useCase.execute(new BatchRunContext(new RunId("lock-warn"), Instant.now()));
|
||||||
|
|
||||||
|
assertTrue(capturingLogger.warnCallCount > 0,
|
||||||
|
"Bei nicht verfügbarem Lock muss eine Warnung geloggt werden");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void execute_extractionSuccess_logsPreCheckPassedInfoAndDebug() throws Exception {
|
||||||
|
// Prüft, dass bei erfolgreich verarbeiteter Datei debug() durch logExtractionResult
|
||||||
|
// und info() durch logProcessingOutcome aufgerufen wird.
|
||||||
|
// Erwartete debug()-Aufrufe für einen Kandidaten (success-Pfad):
|
||||||
|
// L138 (lock acquired) + L249 (processCandidate) + L293 (fingerprint) + L337 (logExtractionResult) + L213 (lock released) = 5
|
||||||
|
// Ohne logExtractionResult-Aufruf: 4
|
||||||
|
// Erwartete info()-Aufrufe für einen Kandidaten (success-Pfad):
|
||||||
|
// L130 (initiiert) + L145 (gestartet) + L178 (Kandidaten gefunden) + L365 (PreCheckPassed) + L190 (abgeschlossen) = 5
|
||||||
|
// Ohne logProcessingOutcome-Aufruf: 4
|
||||||
|
CapturingProcessingLogger capturingLogger = new CapturingProcessingLogger();
|
||||||
|
RuntimeConfiguration config = buildConfig(tempDir);
|
||||||
|
|
||||||
|
SourceDocumentCandidate candidate = makeCandidate("invoice.pdf");
|
||||||
|
PdfExtractionSuccess success = new PdfExtractionSuccess("Rechnungstext", new PdfPageCount(1));
|
||||||
|
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
|
||||||
|
FixedExtractionPort extractionPort = new FixedExtractionPort(success);
|
||||||
|
TrackingDocumentProcessingCoordinator processor = new TrackingDocumentProcessingCoordinator();
|
||||||
|
|
||||||
|
DefaultBatchRunProcessingUseCase useCase = new DefaultBatchRunProcessingUseCase(
|
||||||
|
config, new MockRunLockPort(), candidatesPort, extractionPort,
|
||||||
|
new AlwaysSuccessFingerprintPort(), processor, capturingLogger);
|
||||||
|
|
||||||
|
useCase.execute(new BatchRunContext(new RunId("log-precheck"), Instant.now()));
|
||||||
|
|
||||||
|
// Ohne logExtractionResult wären es 4 debug()-Aufrufe; mit logExtractionResult 5
|
||||||
|
assertTrue(capturingLogger.debugCallCount >= 5,
|
||||||
|
"logExtractionResult muss bei PdfExtractionSuccess debug() aufrufen (erwartet >= 5, war: "
|
||||||
|
+ capturingLogger.debugCallCount + ")");
|
||||||
|
// Ohne logProcessingOutcome wären es 4 info()-Aufrufe; mit logProcessingOutcome 5
|
||||||
|
assertTrue(capturingLogger.infoCallCount >= 5,
|
||||||
|
"logProcessingOutcome muss bei PreCheckPassed info() aufrufen (erwartet >= 5, war: "
|
||||||
|
+ capturingLogger.infoCallCount + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void execute_extractionContentError_logsDebugAndPreCheckFailedInfo() throws Exception {
|
||||||
|
// Prüft, dass bei PdfExtractionContentError debug (logExtractionResult) und info (logProcessingOutcome) geloggt wird.
|
||||||
|
// Erwartete debug()-Aufrufe: 5 (lock + processCandidate + fingerprint + logExtractionResult (content) + lock released)
|
||||||
|
// Erwartete info()-Aufrufe: 5 (L130 + L145 + L178 + L369 PreCheckFailed + L190)
|
||||||
|
CapturingProcessingLogger capturingLogger = new CapturingProcessingLogger();
|
||||||
|
RuntimeConfiguration config = buildConfig(tempDir);
|
||||||
|
|
||||||
|
SourceDocumentCandidate candidate = makeCandidate("encrypted.pdf");
|
||||||
|
PdfExtractionContentError contentError = new PdfExtractionContentError("PDF verschlüsselt");
|
||||||
|
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
|
||||||
|
FixedExtractionPort extractionPort = new FixedExtractionPort(contentError);
|
||||||
|
TrackingDocumentProcessingCoordinator processor = new TrackingDocumentProcessingCoordinator();
|
||||||
|
|
||||||
|
DefaultBatchRunProcessingUseCase useCase = new DefaultBatchRunProcessingUseCase(
|
||||||
|
config, new MockRunLockPort(), candidatesPort, extractionPort,
|
||||||
|
new AlwaysSuccessFingerprintPort(), processor, capturingLogger);
|
||||||
|
|
||||||
|
useCase.execute(new BatchRunContext(new RunId("log-content-error"), Instant.now()));
|
||||||
|
|
||||||
|
// Ohne logExtractionResult wären es 4 debug()-Aufrufe; mit logExtractionResult 5
|
||||||
|
assertTrue(capturingLogger.debugCallCount >= 5,
|
||||||
|
"logExtractionResult muss bei PdfExtractionContentError debug() aufrufen (erwartet >= 5, war: "
|
||||||
|
+ capturingLogger.debugCallCount + ")");
|
||||||
|
// Ohne logProcessingOutcome (PreCheckFailed) wären es 4 info()-Aufrufe; mit 5
|
||||||
|
assertTrue(capturingLogger.infoCallCount >= 5,
|
||||||
|
"logProcessingOutcome muss bei PreCheckFailed info() aufrufen (erwartet >= 5, war: "
|
||||||
|
+ capturingLogger.infoCallCount + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void execute_extractionTechnicalError_logsDebugAndWarn() throws Exception {
|
||||||
|
// Prüft, dass bei PdfExtractionTechnicalError debug (logExtractionResult) und warn (logProcessingOutcome) geloggt wird.
|
||||||
|
// Erwartete debug()-Aufrufe: 5 (lock + processCandidate + fingerprint + logExtractionResult + lock released)
|
||||||
|
CapturingProcessingLogger capturingLogger = new CapturingProcessingLogger();
|
||||||
|
RuntimeConfiguration config = buildConfig(tempDir);
|
||||||
|
|
||||||
|
SourceDocumentCandidate candidate = makeCandidate("broken.pdf");
|
||||||
|
PdfExtractionTechnicalError technicalError = new PdfExtractionTechnicalError("I/O-Fehler", null);
|
||||||
|
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
|
||||||
|
FixedExtractionPort extractionPort = new FixedExtractionPort(technicalError);
|
||||||
|
TrackingDocumentProcessingCoordinator processor = new TrackingDocumentProcessingCoordinator();
|
||||||
|
|
||||||
|
DefaultBatchRunProcessingUseCase useCase = new DefaultBatchRunProcessingUseCase(
|
||||||
|
config, new MockRunLockPort(), candidatesPort, extractionPort,
|
||||||
|
new AlwaysSuccessFingerprintPort(), processor, capturingLogger);
|
||||||
|
|
||||||
|
useCase.execute(new BatchRunContext(new RunId("log-tech-error"), Instant.now()));
|
||||||
|
|
||||||
|
// Ohne logExtractionResult wären es 4 debug()-Aufrufe; mit logExtractionResult 5
|
||||||
|
assertTrue(capturingLogger.debugCallCount >= 5,
|
||||||
|
"logExtractionResult muss bei PdfExtractionTechnicalError debug() aufrufen (erwartet >= 5, war: "
|
||||||
|
+ capturingLogger.debugCallCount + ")");
|
||||||
|
// logProcessingOutcome ruft warn() auf für TechnicalDocumentError
|
||||||
|
assertTrue(capturingLogger.warnCallCount > 0,
|
||||||
|
"logProcessingOutcome muss bei TechnicalDocumentError warn() aufrufen");
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -816,4 +1031,32 @@ class BatchRunProcessingUseCaseTest {
|
|||||||
// No-op
|
// No-op
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Zählt Logger-Aufrufe je Level, um VoidMethodCallMutator-Mutationen zu erkennen. */
|
||||||
|
private static class CapturingProcessingLogger implements ProcessingLogger {
|
||||||
|
int infoCallCount = 0;
|
||||||
|
int debugCallCount = 0;
|
||||||
|
int warnCallCount = 0;
|
||||||
|
int errorCallCount = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void info(String message, Object... args) {
|
||||||
|
infoCallCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void debug(String message, Object... args) {
|
||||||
|
debugCallCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void warn(String message, Object... args) {
|
||||||
|
warnCallCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void error(String message, Object... args) {
|
||||||
|
errorCallCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user