1
0

Nachbearbeitung: M4DocumentProcessor fachlich neutral umbenannt

This commit is contained in:
2026-04-04 10:43:31 +02:00
parent 326e739e45
commit c3d207b742
5 changed files with 179 additions and 179 deletions

View File

@@ -38,11 +38,11 @@ import java.util.function.Consumer;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for {@link M4DocumentProcessor}.
* Unit tests for {@link DocumentProcessingCoordinator}.
* <p>
* Covers:
* <ul>
* <li>M4 minimal rules: status, counter and retryable flag mapping</li>
* <li>Minimal rules: status, counter and retryable flag mapping</li>
* <li>Skip logic for SUCCESS and FAILED_FINAL documents</li>
* <li>New document path (DocumentUnknown)</li>
* <li>Known processable document path (DocumentKnownProcessable)</li>
@@ -51,7 +51,7 @@ import static org.junit.jupiter.api.Assertions.*;
* <li>Skip events do not change error counters</li>
* </ul>
*/
class M4DocumentProcessorTest {
class DocumentProcessingCoordinatorTest {
private static final String FINGERPRINT_HEX =
"a".repeat(64); // 64 lowercase hex chars
@@ -59,7 +59,7 @@ class M4DocumentProcessorTest {
private CapturingDocumentRecordRepository recordRepo;
private CapturingProcessingAttemptRepository attemptRepo;
private CapturingUnitOfWorkPort unitOfWorkPort;
private M4DocumentProcessor processor;
private DocumentProcessingCoordinator processor;
private SourceDocumentCandidate candidate;
private DocumentFingerprint fingerprint;
@@ -71,7 +71,7 @@ class M4DocumentProcessorTest {
recordRepo = new CapturingDocumentRecordRepository();
attemptRepo = new CapturingProcessingAttemptRepository();
unitOfWorkPort = new CapturingUnitOfWorkPort(recordRepo, attemptRepo);
processor = new M4DocumentProcessor(recordRepo, attemptRepo, unitOfWorkPort);
processor = new DocumentProcessingCoordinator(recordRepo, attemptRepo, unitOfWorkPort);
candidate = new SourceDocumentCandidate(
"test.pdf", 1024L, new SourceDocumentLocator("/tmp/test.pdf"));
@@ -467,4 +467,4 @@ class M4DocumentProcessorTest {
operations.accept(mockOps);
}
}
}
}

View File

@@ -18,7 +18,7 @@ import de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableExcepti
import de.gecheckt.pdf.umbenenner.application.port.out.SourceDocumentAccessException;
import de.gecheckt.pdf.umbenenner.application.port.out.SourceDocumentCandidatesPort;
import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort;
import de.gecheckt.pdf.umbenenner.application.service.M4DocumentProcessor;
import de.gecheckt.pdf.umbenenner.application.service.DocumentProcessingCoordinator;
import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionContentError;
@@ -51,11 +51,11 @@ import static org.junit.jupiter.api.Assertions.*;
* <ul>
* <li>Lock acquisition and release lifecycle</li>
* <li>Source folder scanning and per-document processing loop</li>
* <li>Happy path: candidate passes pre-checks, M4 persistence is invoked</li>
* <li>Happy path: candidate passes pre-checks, persistence is invoked</li>
* <li>Deterministic content errors: no usable text, page limit exceeded</li>
* <li>Technical extraction errors: controlled per-document end, batch continues</li>
* <li>Source folder access failure: batch fails with FAILURE outcome</li>
* <li>M4 idempotency: fingerprint failure → not historised</li>
* <li>Idempotency: fingerprint failure → not historised</li>
* </ul>
*/
class BatchRunProcessingUseCaseTest {
@@ -74,7 +74,7 @@ class BatchRunProcessingUseCaseTest {
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort(),
new AlwaysSuccessFingerprintPort(), new NoOpM4DocumentProcessor());
new AlwaysSuccessFingerprintPort(), new NoOpDocumentProcessingCoordinator());
BatchRunContext context = new BatchRunContext(new RunId("test-run-1"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
@@ -91,7 +91,7 @@ class BatchRunProcessingUseCaseTest {
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort(),
new AlwaysSuccessFingerprintPort(), new NoOpM4DocumentProcessor());
new AlwaysSuccessFingerprintPort(), new NoOpDocumentProcessingCoordinator());
BatchRunContext context = new BatchRunContext(new RunId("test-run-2"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
@@ -111,7 +111,7 @@ class BatchRunProcessingUseCaseTest {
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort(),
new AlwaysSuccessFingerprintPort(), new NoOpM4DocumentProcessor());
new AlwaysSuccessFingerprintPort(), new NoOpDocumentProcessingCoordinator());
BatchRunContext context = new BatchRunContext(new RunId("test-run-f1"), Instant.now());
useCase.execute(context);
@@ -128,7 +128,7 @@ class BatchRunProcessingUseCaseTest {
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort(),
new AlwaysSuccessFingerprintPort(), new NoOpM4DocumentProcessor());
new AlwaysSuccessFingerprintPort(), new NoOpDocumentProcessingCoordinator());
BatchRunContext context = new BatchRunContext(new RunId("test-run-3"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
@@ -149,7 +149,7 @@ class BatchRunProcessingUseCaseTest {
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort(),
new AlwaysSuccessFingerprintPort(), new NoOpM4DocumentProcessor());
new AlwaysSuccessFingerprintPort(), new NoOpDocumentProcessingCoordinator());
BatchRunContext context = new BatchRunContext(new RunId("empty"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
@@ -158,7 +158,7 @@ class BatchRunProcessingUseCaseTest {
}
@Test
void execute_happyPath_candidatePassesPreChecks_m4PersistenceInvoked() throws Exception {
void execute_happyPath_candidatePassesPreChecks_persistenceInvoked() throws Exception {
MockRunLockPort lockPort = new MockRunLockPort();
StartConfiguration config = buildConfig(tempDir);
@@ -166,18 +166,18 @@ class BatchRunProcessingUseCaseTest {
PdfExtractionSuccess success = new PdfExtractionSuccess("Invoice text", new PdfPageCount(1));
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
FixedExtractionPort extractionPort = new FixedExtractionPort(success);
TrackingM4DocumentProcessor m4Processor = new TrackingM4DocumentProcessor();
TrackingDocumentProcessingCoordinator processor = new TrackingDocumentProcessingCoordinator();
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, candidatesPort, extractionPort,
new AlwaysSuccessFingerprintPort(), m4Processor);
new AlwaysSuccessFingerprintPort(), processor);
BatchRunContext context = new BatchRunContext(new RunId("happy"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
assertTrue(outcome.isSuccess(), "Happy path should yield SUCCESS");
assertEquals(1, extractionPort.callCount(), "Extraction should be called exactly once");
assertEquals(1, m4Processor.processCallCount(), "M4 processor should be called exactly once");
assertEquals(1, processor.processCallCount(), "processor should be called exactly once");
}
@Test
@@ -189,17 +189,17 @@ class BatchRunProcessingUseCaseTest {
PdfExtractionSuccess emptySuccess = new PdfExtractionSuccess(" ", new PdfPageCount(1));
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
FixedExtractionPort extractionPort = new FixedExtractionPort(emptySuccess);
TrackingM4DocumentProcessor m4Processor = new TrackingM4DocumentProcessor();
TrackingDocumentProcessingCoordinator processor = new TrackingDocumentProcessingCoordinator();
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, candidatesPort, extractionPort,
new AlwaysSuccessFingerprintPort(), m4Processor);
new AlwaysSuccessFingerprintPort(), processor);
BatchRunContext context = new BatchRunContext(new RunId("no-text"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
assertTrue(outcome.isSuccess(), "No-usable-text pre-check failure should not abort the batch run");
assertEquals(1, m4Processor.processCallCount(), "M4 processor should still be called for content errors");
assertEquals(1, processor.processCallCount(), "processor should still be called for content errors");
}
@Test
@@ -211,17 +211,17 @@ class BatchRunProcessingUseCaseTest {
PdfExtractionSuccess manyPages = new PdfExtractionSuccess("Some text", new PdfPageCount(10));
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
FixedExtractionPort extractionPort = new FixedExtractionPort(manyPages);
TrackingM4DocumentProcessor m4Processor = new TrackingM4DocumentProcessor();
TrackingDocumentProcessingCoordinator processor = new TrackingDocumentProcessingCoordinator();
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, candidatesPort, extractionPort,
new AlwaysSuccessFingerprintPort(), m4Processor);
new AlwaysSuccessFingerprintPort(), processor);
BatchRunContext context = new BatchRunContext(new RunId("page-limit"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
assertTrue(outcome.isSuccess(), "Page limit exceeded should not abort the batch run");
assertEquals(1, m4Processor.processCallCount(), "M4 processor should still be called for page limit errors");
assertEquals(1, processor.processCallCount(), "processor should still be called for page limit errors");
}
@Test
@@ -233,17 +233,17 @@ class BatchRunProcessingUseCaseTest {
PdfExtractionContentError contentError = new PdfExtractionContentError("PDF is encrypted");
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
FixedExtractionPort extractionPort = new FixedExtractionPort(contentError);
TrackingM4DocumentProcessor m4Processor = new TrackingM4DocumentProcessor();
TrackingDocumentProcessingCoordinator processor = new TrackingDocumentProcessingCoordinator();
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, candidatesPort, extractionPort,
new AlwaysSuccessFingerprintPort(), m4Processor);
new AlwaysSuccessFingerprintPort(), processor);
BatchRunContext context = new BatchRunContext(new RunId("content-error"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
assertTrue(outcome.isSuccess(), "Extraction content error should not abort the batch run");
assertEquals(1, m4Processor.processCallCount(), "M4 processor should be called for content errors");
assertEquals(1, processor.processCallCount(), "processor should be called for content errors");
}
@Test
@@ -255,17 +255,17 @@ class BatchRunProcessingUseCaseTest {
PdfExtractionTechnicalError technicalError = new PdfExtractionTechnicalError("I/O error reading file", null);
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
FixedExtractionPort extractionPort = new FixedExtractionPort(technicalError);
TrackingM4DocumentProcessor m4Processor = new TrackingM4DocumentProcessor();
TrackingDocumentProcessingCoordinator processor = new TrackingDocumentProcessingCoordinator();
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, candidatesPort, extractionPort,
new AlwaysSuccessFingerprintPort(), m4Processor);
new AlwaysSuccessFingerprintPort(), processor);
BatchRunContext context = new BatchRunContext(new RunId("tech-error"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
assertTrue(outcome.isSuccess(), "Technical extraction error should not abort the batch run");
assertEquals(1, m4Processor.processCallCount(), "M4 processor should be called for technical errors");
assertEquals(1, processor.processCallCount(), "processor should be called for technical errors");
}
@Test
@@ -279,7 +279,7 @@ class BatchRunProcessingUseCaseTest {
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, failingPort, new NoOpExtractionPort(),
new AlwaysSuccessFingerprintPort(), new NoOpM4DocumentProcessor());
new AlwaysSuccessFingerprintPort(), new NoOpDocumentProcessingCoordinator());
BatchRunContext context = new BatchRunContext(new RunId("access-fail"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
@@ -300,22 +300,22 @@ class BatchRunProcessingUseCaseTest {
SourceDocumentCandidate candidate = makeCandidate("unreadable.pdf");
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
TrackingM4DocumentProcessor m4Processor = new TrackingM4DocumentProcessor();
TrackingDocumentProcessingCoordinator processor = new TrackingDocumentProcessingCoordinator();
// Fingerprint always fails → M4 processor must NOT be called
// Fingerprint always fails → processor must NOT be called
FingerprintPort alwaysFailingFingerprintPort = c ->
new FingerprintTechnicalError("Cannot read file", null);
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, candidatesPort, new NoOpExtractionPort(),
alwaysFailingFingerprintPort, m4Processor);
alwaysFailingFingerprintPort, processor);
BatchRunContext context = new BatchRunContext(new RunId("fp-fail"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
assertTrue(outcome.isSuccess(), "Fingerprint failure should not abort the batch run");
assertEquals(0, m4Processor.processCallCount(),
"M4 processor must NOT be called when fingerprint computation fails (pre-fingerprint failure)");
assertEquals(0, processor.processCallCount(),
"processor must NOT be called when fingerprint computation fails (pre-fingerprint failure)");
}
@Test
@@ -333,7 +333,7 @@ class BatchRunProcessingUseCaseTest {
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, candidatesPort, extractionPort,
alwaysFailingFingerprintPort, new NoOpM4DocumentProcessor());
alwaysFailingFingerprintPort, new NoOpDocumentProcessingCoordinator());
BatchRunContext context = new BatchRunContext(new RunId("fp-fail-no-extract"), Instant.now());
useCase.execute(context);
@@ -377,20 +377,20 @@ class BatchRunProcessingUseCaseTest {
return new FingerprintSuccess(makeFingerprint(candidate.uniqueIdentifier()));
};
TrackingM4DocumentProcessor m4Processor = new TrackingM4DocumentProcessor();
TrackingDocumentProcessingCoordinator processor = new TrackingDocumentProcessingCoordinator();
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, candidatesPort, extractionPort,
mappedFingerprintPort, m4Processor);
mappedFingerprintPort, processor);
BatchRunContext context = new BatchRunContext(new RunId("mixed"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
assertTrue(outcome.isSuccess(), "Mixed batch with all outcome types must yield batch SUCCESS");
// 5 candidates with successful fingerprint → M4 processor called 5 times
// 1 candidate with fingerprint failure → M4 processor NOT called
assertEquals(5, m4Processor.processCallCount(),
"M4 processor must be called for each candidate with a successful fingerprint");
// 5 candidates with successful fingerprint → processor called 5 times
// 1 candidate with fingerprint failure → processor NOT called
assertEquals(5, processor.processCallCount(),
"processor must be called for each candidate with a successful fingerprint");
// Extraction called for 5 candidates (not for fpFailCandidate)
assertEquals(5, extractionPort.callCount(),
"Extraction must be attempted for each of the 5 candidates with a valid fingerprint");
@@ -409,18 +409,18 @@ class BatchRunProcessingUseCaseTest {
PdfExtractionSuccess success = new PdfExtractionSuccess("Invoice content", new PdfPageCount(2));
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(candidates);
FixedExtractionPort extractionPort = new FixedExtractionPort(success);
TrackingM4DocumentProcessor m4Processor = new TrackingM4DocumentProcessor();
TrackingDocumentProcessingCoordinator processor = new TrackingDocumentProcessingCoordinator();
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, candidatesPort, extractionPort,
new AlwaysSuccessFingerprintPort(), m4Processor);
new AlwaysSuccessFingerprintPort(), processor);
BatchRunContext context = new BatchRunContext(new RunId("multi"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
assertTrue(outcome.isSuccess(), "All three candidates processed should yield SUCCESS");
assertEquals(3, extractionPort.callCount(), "Extraction should be called once per candidate");
assertEquals(3, m4Processor.processCallCount(), "M4 processor should be called once per candidate");
assertEquals(3, processor.processCallCount(), "processor should be called once per candidate");
}
// -------------------------------------------------------------------------
@@ -433,9 +433,9 @@ class BatchRunProcessingUseCaseTest {
SourceDocumentCandidatesPort candidatesPort,
PdfTextExtractionPort extractionPort,
FingerprintPort fingerprintPort,
M4DocumentProcessor m4Processor) {
DocumentProcessingCoordinator processor) {
return new DefaultBatchRunProcessingUseCase(
config, lockPort, candidatesPort, extractionPort, fingerprintPort, m4Processor);
config, lockPort, candidatesPort, extractionPort, fingerprintPort, processor);
}
private static StartConfiguration buildConfig(Path tempDir) throws Exception {
@@ -612,22 +612,22 @@ class BatchRunProcessingUseCaseTest {
}
/**
* No-op M4DocumentProcessor that does nothing (for tests that only care about
* lock/batch lifecycle, not M4 persistence).
* No-op DocumentProcessingCoordinator that does nothing (for tests that only care about
* lock/batch lifecycle, not persistence).
*/
private static class NoOpM4DocumentProcessor extends M4DocumentProcessor {
NoOpM4DocumentProcessor() {
private static class NoOpDocumentProcessingCoordinator extends DocumentProcessingCoordinator {
NoOpDocumentProcessingCoordinator() {
super(new NoOpDocumentRecordRepository(), new NoOpProcessingAttemptRepository(), new NoOpUnitOfWorkPort());
}
}
/**
* Tracking M4DocumentProcessor that counts how many times {@code process()} is called.
* Tracking DocumentProcessingCoordinator that counts how many times {@code process()} is called.
*/
private static class TrackingM4DocumentProcessor extends M4DocumentProcessor {
private static class TrackingDocumentProcessingCoordinator extends DocumentProcessingCoordinator {
private int processCallCount = 0;
TrackingM4DocumentProcessor() {
TrackingDocumentProcessingCoordinator() {
super(new NoOpDocumentRecordRepository(), new NoOpProcessingAttemptRepository(), new NoOpUnitOfWorkPort());
}
@@ -658,11 +658,11 @@ class BatchRunProcessingUseCaseTest {
int processCallCount() { return processCallCount; }
}
/** No-op DocumentRecordRepository for use in test M4DocumentProcessor instances. */
/** No-op DocumentRecordRepository for use in test instances. */
private static class NoOpDocumentRecordRepository implements DocumentRecordRepository {
@Override
public DocumentRecordLookupResult findByFingerprint(DocumentFingerprint fingerprint) {
// Return DocumentUnknown so the M4 processor always takes the "new document" path
// Return DocumentUnknown so the processor always takes the "new document" path
return new DocumentUnknown();
}
@@ -677,7 +677,7 @@ class BatchRunProcessingUseCaseTest {
}
}
/** No-op ProcessingAttemptRepository for use in test M4DocumentProcessor instances. */
/** No-op ProcessingAttemptRepository for use in test instances. */
private static class NoOpProcessingAttemptRepository implements ProcessingAttemptRepository {
@Override
public int loadNextAttemptNumber(DocumentFingerprint fingerprint) {
@@ -695,7 +695,7 @@ class BatchRunProcessingUseCaseTest {
}
}
/** No-op UnitOfWorkPort for use in test M4DocumentProcessor instances. */
/** No-op UnitOfWorkPort for use in test instances. */
private static class NoOpUnitOfWorkPort implements UnitOfWorkPort {
@Override
public void executeInTransaction(Consumer<TransactionOperations> operations) {