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");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 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
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -490,4 +849,32 @@ class DocumentProcessingCoordinatorTest {
|
||||
// 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");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 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
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -816,4 +1031,32 @@ class BatchRunProcessingUseCaseTest {
|
||||
// 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