Implementierung für M2 vorläufig abgeschlossen
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
/**
|
||||
* Value object representing the outcome of a batch processing run.
|
||||
* <p>
|
||||
* This enum encodes the high-level result of executing a complete batch cycle,
|
||||
* allowing Bootstrap and CLI layers to map outcomes to exit codes and log messages
|
||||
* in a controlled and consistent manner.
|
||||
* <p>
|
||||
* The outcome is independent of individual document processing results;
|
||||
* it represents the batch operation itself (lock acquired, no critical startup failure, etc.).
|
||||
* <p>
|
||||
* Design Note: This contract is defined in AP-002 to enable AP-007 (exit code handling)
|
||||
* to derive exit codes systematically without requiring additional knowledge about
|
||||
* the batch run. Each outcome maps cleanly to an exit code semantic.
|
||||
* <p>
|
||||
* AP-007: Three distinct outcomes are now defined to make the difference between a
|
||||
* technically successful run, a controlled early termination due to start protection,
|
||||
* and a hard bootstrap failure explicit in both code and logs.
|
||||
*
|
||||
* @since M2-AP-002
|
||||
*/
|
||||
public enum BatchRunOutcome {
|
||||
|
||||
/**
|
||||
* Batch processing completed successfully.
|
||||
* <p>
|
||||
* The run acquired the lock, started cleanly, and completed the full processing cycle.
|
||||
* Individual documents within the run may have succeeded or failed, but that is
|
||||
* transparent to this outcome. The run itself executed as intended.
|
||||
* <p>
|
||||
* Maps to exit code 0.
|
||||
*/
|
||||
SUCCESS("Batch processing completed successfully"),
|
||||
|
||||
/**
|
||||
* Batch run aborted because another instance is already running.
|
||||
* <p>
|
||||
* The run lock could not be acquired because a concurrent instance holds it.
|
||||
* This is a controlled, expected termination (start protection), not a hard failure.
|
||||
* The new instance terminates immediately without performing any processing.
|
||||
* <p>
|
||||
* Maps to exit code 1.
|
||||
*
|
||||
* @since M2-AP-007
|
||||
*/
|
||||
LOCK_UNAVAILABLE("Another instance is already running; this run terminates immediately"),
|
||||
|
||||
/**
|
||||
* Batch processing failed due to an unexpected error.
|
||||
* <p>
|
||||
* The run encountered a critical error that prevented normal batch completion,
|
||||
* such as an unrecoverable I/O error or infrastructure failure.
|
||||
* <p>
|
||||
* Maps to exit code 1.
|
||||
*/
|
||||
FAILURE("Batch processing failed");
|
||||
|
||||
private final String message;
|
||||
|
||||
BatchRunOutcome(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable message for this outcome.
|
||||
*
|
||||
* @return descriptive message suitable for logging
|
||||
*/
|
||||
public String message() {
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this outcome represents successful completion.
|
||||
*
|
||||
* @return true if outcome is {@link #SUCCESS}, false otherwise
|
||||
*/
|
||||
public boolean isSuccess() {
|
||||
return this == SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this outcome represents any non-successful result.
|
||||
* <p>
|
||||
* Both {@link #FAILURE} and {@link #LOCK_UNAVAILABLE} are considered failures
|
||||
* for the purpose of exit code derivation (both map to exit code 1).
|
||||
* Use {@link #isLockUnavailable()} to distinguish the start-protection case.
|
||||
*
|
||||
* @return true if outcome is {@link #FAILURE} or {@link #LOCK_UNAVAILABLE}, false otherwise
|
||||
*/
|
||||
public boolean isFailure() {
|
||||
return this == FAILURE || this == LOCK_UNAVAILABLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this outcome represents a controlled early termination due to
|
||||
* the run lock being held by another instance.
|
||||
*
|
||||
* @return true if outcome is {@link #LOCK_UNAVAILABLE}, false otherwise
|
||||
* @since M2-AP-007
|
||||
*/
|
||||
public boolean isLockUnavailable() {
|
||||
return this == LOCK_UNAVAILABLE;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,51 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;
|
||||
|
||||
/**
|
||||
* Inbound port for batch processing execution.
|
||||
* This interface defines the contract for triggering batch operations.
|
||||
* <p>
|
||||
* AP-003 Implementation: Currently a no-op placeholder to establish the technical startup path.
|
||||
* This interface defines the contract for triggering batch operations.
|
||||
* It is the central use case entry point for the entire application.
|
||||
* <p>
|
||||
* Responsibilities:
|
||||
* <ul>
|
||||
* <li>Orchestrate the batch run lifecycle (start, process, end)</li>
|
||||
* <li>Interact with all outbound ports (lock, clock, configuration, persistence, etc.)</li>
|
||||
* <li>Return a structured outcome ({@link BatchRunOutcome}) that describes the run result</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* The returned outcome is designed to be independent of individual document results,
|
||||
* representing only the batch operation itself. Individual document successes/failures
|
||||
* are tracked separately in persistence (future milestones).
|
||||
* <p>
|
||||
* M2-AP-002 Implementation:
|
||||
* <ul>
|
||||
* <li>Port is defined with a structured return contract ({@link BatchRunOutcome})</li>
|
||||
* <li>Return model allows Bootstrap/CLI to systematically derive exit codes (AP-007)</li>
|
||||
* <li>No implementation of the use case itself yet (that is AP-004)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* M2-AP-003 Update:
|
||||
* <ul>
|
||||
* <li>execute() now accepts a {@link BatchRunContext} containing the run ID and timing</li>
|
||||
* <li>The context flows through the entire batch cycle for correlation and logging</li>
|
||||
* </ul>
|
||||
*/
|
||||
public interface RunBatchProcessingUseCase {
|
||||
|
||||
/**
|
||||
* Executes the batch processing workflow.
|
||||
* <p>
|
||||
* AP-003: This method performs no actual work, only validates the call chain.
|
||||
* The method orchestrates the entire batch cycle, from startup through completion.
|
||||
* It interacts with all necessary outbound ports and returns a structured outcome.
|
||||
* <p>
|
||||
* The outcome is not dependent on how many documents succeeded or failed within the run;
|
||||
* rather, it reflects the batch operation's overall health. Individual document results
|
||||
* are recorded separately in persistence.
|
||||
*
|
||||
* @return true if the workflow completed successfully, false otherwise
|
||||
* @param context the technical context for this batch run, containing run ID and timing
|
||||
* @return {@link BatchRunOutcome} indicating success or failure of the batch operation
|
||||
*/
|
||||
boolean execute();
|
||||
BatchRunOutcome execute(BatchRunContext context);
|
||||
}
|
||||
@@ -1,5 +1,25 @@
|
||||
/**
|
||||
* Inbound ports (application service interfaces) for the hexagonal architecture.
|
||||
* Use cases in this package are invoked by adapters from the outside world.
|
||||
* Inbound ports (use cases) for the application.
|
||||
* <p>
|
||||
* Inbound ports define the contracts for interacting with the application from external adapters.
|
||||
* All calls flow FROM adapters INTO the application through these ports.
|
||||
* <p>
|
||||
* Central inbound port:
|
||||
* <ul>
|
||||
* <li>{@link de.gecheckt.pdf.umbenenner.application.port.in.RunBatchProcessingUseCase}
|
||||
* — The primary use case for executing a complete batch run</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Return models:
|
||||
* <ul>
|
||||
* <li>{@link de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome}
|
||||
* — Structured result of a batch run, designed for exit code mapping (AP-007)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Architecture Rule: Inbound ports are independent of implementation and contain no business logic.
|
||||
* They define "what can be done to the application". All dependencies point inward;
|
||||
* adapters depend on ports, not vice versa.
|
||||
*
|
||||
* @since M2-AP-002
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* Outbound port for system time access.
|
||||
* <p>
|
||||
* This port abstracts access to the system clock, enabling the batch run to:
|
||||
* <ul>
|
||||
* <li>Record timestamps for batch run start and completion</li>
|
||||
* <li>Generate or verify document timestamps (later in M5+)</li>
|
||||
* <li>Support testing with controlled time values</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* By isolating time access behind a port, the application can be tested with
|
||||
* deterministic time values without requiring system clock manipulation.
|
||||
* <p>
|
||||
* This port is defined in M2 for use in later milestones where timestamps
|
||||
* become relevant (e.g., run history, document date fallback).
|
||||
*
|
||||
* @since M2-AP-002
|
||||
*/
|
||||
public interface ClockPort {
|
||||
|
||||
/**
|
||||
* Returns the current moment in time.
|
||||
* <p>
|
||||
* Implementations should return the current system time (or a controlled test value).
|
||||
* The returned instant is UTC-based and suitable for persistent storage and logging.
|
||||
*
|
||||
* @return the current instant
|
||||
*/
|
||||
Instant now();
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
/**
|
||||
* Outbound port for exclusive run locking.
|
||||
* <p>
|
||||
* This port abstracts the mechanism for ensuring that only one instance of the PDF Umbenenner
|
||||
* is executing at any given time. The port defines the contract without prescribing the
|
||||
* implementation (e.g., file-based locks, OS-level locks, distributed locks).
|
||||
* <p>
|
||||
* Responsibilities:
|
||||
* <ul>
|
||||
* <li>Guarantee exclusive access to shared resources (SQLite database, target directory)</li>
|
||||
* <li>Prevent concurrent batch runs from overwriting each other's work or causing inconsistencies</li>
|
||||
* <li>Allow controlled startup failure if another instance is already running</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Lock Lifecycle:
|
||||
* <ul>
|
||||
* <li>Acquire the lock at batch startup (before any processing)</li>
|
||||
* <li>Hold the lock for the entire batch run</li>
|
||||
* <li>Release the lock cleanly at batch end (even on failure, if possible)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* This port is used by the batch use case (M2-AP-004) but not implemented in M2;
|
||||
* implementation follows in M2-AP-006.
|
||||
*
|
||||
* @since M2-AP-002
|
||||
*/
|
||||
public interface RunLockPort {
|
||||
|
||||
/**
|
||||
* Acquires an exclusive lock for the batch run.
|
||||
* <p>
|
||||
* This method blocks or throws an exception if the lock cannot be acquired
|
||||
* (e.g., another instance already holds it). The behavior depends on the implementation.
|
||||
* <p>
|
||||
* If this method returns normally, the caller holds the lock and must ensure
|
||||
* {@link #release()} is called to free it, typically in a finally block.
|
||||
*
|
||||
* @throws RunLockUnavailableException if the lock cannot be acquired
|
||||
* (e.g., another instance already holds it or system error prevents acquiring)
|
||||
* @throws RuntimeException for other critical lock-related failures
|
||||
*/
|
||||
void acquire();
|
||||
|
||||
/**
|
||||
* Releases the exclusive lock held by this batch run.
|
||||
* <p>
|
||||
* This method is called after batch processing completes (successfully or not)
|
||||
* to allow other instances to run.
|
||||
* <p>
|
||||
* Implementations should handle the case where release is called multiple times
|
||||
* or when no lock is currently held, avoiding exceptions if possible.
|
||||
*
|
||||
* @throws RuntimeException if lock release fails critically
|
||||
*/
|
||||
void release();
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
/**
|
||||
* Exception thrown when the run lock cannot be acquired.
|
||||
* <p>
|
||||
* This indicates that another instance of the PDF Umbenenner is already running
|
||||
* and holding the exclusive lock. The current instance should terminate immediately
|
||||
* and allow the running instance to complete.
|
||||
* <p>
|
||||
* This is a controlled failure mode, not a programming error. Applications should
|
||||
* catch this exception and exit gracefully with the appropriate exit code.
|
||||
*
|
||||
* @since M2-AP-002
|
||||
*/
|
||||
public class RunLockUnavailableException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Creates a new RunLockUnavailableException with the given message.
|
||||
*
|
||||
* @param message description of why the lock could not be acquired
|
||||
*/
|
||||
public RunLockUnavailableException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new RunLockUnavailableException with the given message and cause.
|
||||
*
|
||||
* @param message description of why the lock could not be acquired
|
||||
* @param cause the underlying exception that prevented lock acquisition
|
||||
*/
|
||||
public RunLockUnavailableException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,29 @@
|
||||
/**
|
||||
* Outbound ports for the application layer.
|
||||
* Defines interfaces for infrastructure access from the application layer.
|
||||
* Outbound ports (interfaces) for the application.
|
||||
* <p>
|
||||
* Outbound ports define the contracts for interacting with external systems and infrastructure.
|
||||
* All calls flow FROM the application OUT TO infrastructure adapters through these ports.
|
||||
* <p>
|
||||
* M2-AP-002 ports:
|
||||
* <ul>
|
||||
* <li>{@link de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort}
|
||||
* — Loading application startup configuration (already M1)</li>
|
||||
* <li>{@link de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort}
|
||||
* — Exclusive run lock for preventing concurrent batch instances</li>
|
||||
* <li>{@link de.gecheckt.pdf.umbenenner.application.port.out.ClockPort}
|
||||
* — System time access for timestamps and run context</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Exception types:
|
||||
* <ul>
|
||||
* <li>{@link de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException}
|
||||
* — Thrown when run lock cannot be acquired (another instance running)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Architecture Rule: Outbound ports are implementation-agnostic and contain no business logic.
|
||||
* They define "what the application depends on externally". All dependencies point inward;
|
||||
* the application depends on ports, adapters implement ports.
|
||||
*
|
||||
* @since M2-AP-002
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.RunBatchProcessingUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* M2 implementation of {@link RunBatchProcessingUseCase}.
|
||||
* <p>
|
||||
* This use case orchestrates the batch processing workflow with start protection
|
||||
* and controlled execution lifecycle, but without actual document processing.
|
||||
* <p>
|
||||
* Responsibilities:
|
||||
* <ul>
|
||||
* <li>Acquire exclusive run lock to prevent concurrent instances</li>
|
||||
* <li>Initialize batch execution with the provided run context</li>
|
||||
* <li>Coordinate outbound port interactions (configuration, lock management)</li>
|
||||
* <li>Release lock and finish cleanly regardless of execution path</li>
|
||||
* <li>Return structured outcome for Bootstrap exit code mapping</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* M2 Non-Goals (not implemented):
|
||||
* <ul>
|
||||
* <li>No source folder scanning</li>
|
||||
* <li>No PDF filtering or text extraction</li>
|
||||
* <li>No fingerprinting</li>
|
||||
* <li>No SQLite persistence</li>
|
||||
* <li>No AI integration</li>
|
||||
* <li>No filename generation</li>
|
||||
* <li>No target file copying</li>
|
||||
* <li>No business-level retry logic</li>
|
||||
* <li>No single-document processing</li>
|
||||
* </ul>
|
||||
*
|
||||
* @since M2-AP-004
|
||||
*/
|
||||
public class M2BatchRunProcessingUseCase implements RunBatchProcessingUseCase {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(M2BatchRunProcessingUseCase.class);
|
||||
|
||||
private final ConfigurationPort configurationPort;
|
||||
private final RunLockPort runLockPort;
|
||||
|
||||
/**
|
||||
* Creates the M2 batch use case with required outbound ports.
|
||||
*
|
||||
* @param configurationPort for loading startup configuration
|
||||
* @param runLockPort for exclusive run locking
|
||||
* @throws NullPointerException if any port is null
|
||||
*/
|
||||
public M2BatchRunProcessingUseCase(ConfigurationPort configurationPort, RunLockPort runLockPort) {
|
||||
this.configurationPort = configurationPort;
|
||||
this.runLockPort = runLockPort;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BatchRunOutcome execute(BatchRunContext context) {
|
||||
LOG.info("M2 batch processing initiated with RunId: {}", context.runId());
|
||||
|
||||
try {
|
||||
// Step 1: Acquire exclusive run lock (prevents concurrent instances)
|
||||
try {
|
||||
runLockPort.acquire();
|
||||
LOG.debug("Run lock acquired successfully.");
|
||||
} catch (RunLockUnavailableException e) {
|
||||
LOG.warn("Run lock not available – another instance is already running. This instance terminates immediately.");
|
||||
return BatchRunOutcome.LOCK_UNAVAILABLE;
|
||||
}
|
||||
|
||||
// Step 2: Load configuration (already validated in Bootstrap, but accessible to use case)
|
||||
StartConfiguration config = configurationPort.loadConfiguration();
|
||||
LOG.debug("Configuration available: source={}, target={}", config.sourceFolder(), config.targetFolder());
|
||||
|
||||
// Step 3: M2 Batch execution frame (no document processing)
|
||||
LOG.info("Batch execution frame initialized - RunId: {}, Start: {}", context.runId(), context.startInstant());
|
||||
|
||||
// M2 Non-goal: No source folder scanning, PDF processing, persistence, or filename generation
|
||||
// This is a controlled no-op batch cycle that validates the entire orchestration path.
|
||||
|
||||
LOG.info("Batch execution frame completed successfully");
|
||||
return BatchRunOutcome.SUCCESS;
|
||||
|
||||
} catch (Exception e) {
|
||||
// Unexpected error during batch orchestration
|
||||
LOG.error("Unexpected error during batch processing", e);
|
||||
return BatchRunOutcome.FAILURE;
|
||||
} finally {
|
||||
// Step 4: Always release the run lock (critical for M2 start protection)
|
||||
try {
|
||||
runLockPort.release();
|
||||
LOG.debug("Run lock released");
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Warning: Failed to release run lock", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.RunBatchProcessingUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -14,6 +16,11 @@ import org.apache.logging.log4j.Logger;
|
||||
* without any business logic, PDF processing, or infrastructure access.
|
||||
* <p>
|
||||
* AP-005: Accepts {@link ConfigurationPort} to load typed startup configuration.
|
||||
* <p>
|
||||
* M2-AP-002 Update: Returns {@link BatchRunOutcome} instead of boolean,
|
||||
* enabling structured result handling by Bootstrap and CLI layers.
|
||||
* <p>
|
||||
* M2-AP-003 Update: Accepts {@link BatchRunContext} to enable run ID and timing tracking.
|
||||
*/
|
||||
public class NoOpRunBatchProcessingUseCase implements RunBatchProcessingUseCase {
|
||||
|
||||
@@ -30,12 +37,16 @@ public class NoOpRunBatchProcessingUseCase implements RunBatchProcessingUseCase
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean execute() {
|
||||
public BatchRunOutcome execute(BatchRunContext context) {
|
||||
// AP-005: Load configuration through the port (technical loading only)
|
||||
StartConfiguration config = configurationPort.loadConfiguration();
|
||||
LOG.info("Configuration loaded successfully. Source: {}, Target: {}", config.sourceFolder(), config.targetFolder());
|
||||
|
||||
// M2-AP-003: Log run context information for traceability
|
||||
LOG.info("Batch run started with RunId: {}, started at: {}", context.runId(), context.startInstant());
|
||||
|
||||
// AP-003: Intentional no-op - validates the technical call chain only
|
||||
return true;
|
||||
// M2-AP-002: Return structured outcome instead of boolean
|
||||
return BatchRunOutcome.SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
/**
|
||||
* Use case implementations that realize the inbound port contracts.
|
||||
* Currently contains minimal no-op implementations for AP-003 technical validation.
|
||||
* <p>
|
||||
* Implementations:
|
||||
* <ul>
|
||||
* <li>{@link de.gecheckt.pdf.umbenenner.application.usecase.NoOpRunBatchProcessingUseCase}
|
||||
* — Minimal no-op for technical validation without start protection</li>
|
||||
* <li>{@link de.gecheckt.pdf.umbenenner.application.usecase.M2BatchRunProcessingUseCase}
|
||||
* — M2 production implementation with run lock and controlled batch cycle (AP-004)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* All implementations are infrastructure-agnostic and interact only through ports.
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||
@@ -0,0 +1,188 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.RunId;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Tests for {@link M2BatchRunProcessingUseCase}.
|
||||
* <p>
|
||||
* Verifies correct orchestration of the M2 batch cycle including lock management,
|
||||
* configuration loading, and controlled execution flow.
|
||||
*/
|
||||
class M2BatchRunProcessingUseCaseTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void execute_successfullyAcquiresAndReleasesLock() {
|
||||
// Setup mock ports that track invocations
|
||||
MockRunLockPort lockPort = new MockRunLockPort();
|
||||
MockConfigurationPort configPort = new MockConfigurationPort(tempDir);
|
||||
|
||||
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase(configPort, lockPort);
|
||||
BatchRunContext context = new BatchRunContext(new RunId("test-run-1"), Instant.now());
|
||||
|
||||
// Execute
|
||||
BatchRunOutcome outcome = useCase.execute(context);
|
||||
|
||||
// Verify lock lifecycle
|
||||
assertTrue(lockPort.wasAcquireCalled(), "Lock acquire should be called");
|
||||
assertTrue(lockPort.wasReleaseCalled(), "Lock release should be called");
|
||||
assertTrue(outcome.isSuccess(), "Batch should complete successfully");
|
||||
}
|
||||
|
||||
@Test
|
||||
void execute_returnsLockUnavailableWhenLockCannotBeAcquired() {
|
||||
// Setup: lock port that fails to acquire (another instance is running)
|
||||
RunLockPort lockPort = new RunLockPort() {
|
||||
@Override
|
||||
public void acquire() {
|
||||
throw new RunLockUnavailableException("Another instance already running");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
// Nothing to release
|
||||
}
|
||||
};
|
||||
|
||||
ConfigurationPort configPort = new MockConfigurationPort(tempDir);
|
||||
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase(configPort, lockPort);
|
||||
BatchRunContext context = new BatchRunContext(new RunId("test-run-2"), Instant.now());
|
||||
|
||||
// Execute
|
||||
BatchRunOutcome outcome = useCase.execute(context);
|
||||
|
||||
// AP-007: lock unavailable is a distinct, controlled early-termination outcome
|
||||
assertTrue(outcome.isLockUnavailable(), "Outcome should be LOCK_UNAVAILABLE when lock cannot be acquired");
|
||||
assertTrue(outcome.isFailure(), "LOCK_UNAVAILABLE also reports as failure for exit code derivation");
|
||||
assertFalse(outcome.isSuccess(), "Batch should not succeed when lock unavailable");
|
||||
}
|
||||
|
||||
@Test
|
||||
void execute_releasesLockEvenOnError() {
|
||||
// Setup: mock lock port that tracks release calls
|
||||
MockRunLockPort lockPort = new MockRunLockPort();
|
||||
|
||||
// Config port that throws exception
|
||||
ConfigurationPort configPort = () -> {
|
||||
throw new RuntimeException("Configuration loading error");
|
||||
};
|
||||
|
||||
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase(configPort, lockPort);
|
||||
BatchRunContext context = new BatchRunContext(new RunId("test-run-3"), Instant.now());
|
||||
|
||||
// Execute (expect failure due to config error)
|
||||
BatchRunOutcome outcome = useCase.execute(context);
|
||||
|
||||
// Verify lock is still released despite error
|
||||
assertTrue(lockPort.wasReleaseCalled(), "Lock should be released even on configuration error");
|
||||
assertTrue(outcome.isFailure(), "Batch should fail");
|
||||
}
|
||||
|
||||
@Test
|
||||
void execute_loadsConfigurationDuringExecution() {
|
||||
// Setup
|
||||
MockRunLockPort lockPort = new MockRunLockPort();
|
||||
MockConfigurationPort configPort = new MockConfigurationPort(tempDir);
|
||||
|
||||
M2BatchRunProcessingUseCase useCase = new M2BatchRunProcessingUseCase(configPort, lockPort);
|
||||
BatchRunContext context = new BatchRunContext(new RunId("test-run-4"), Instant.now());
|
||||
|
||||
// Execute
|
||||
BatchRunOutcome outcome = useCase.execute(context);
|
||||
|
||||
// Verify configuration was loaded
|
||||
assertTrue(configPort.wasLoadConfigurationCalled(), "Configuration should be loaded");
|
||||
assertTrue(outcome.isSuccess(), "Batch should succeed");
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock ConfigurationPort for testing.
|
||||
*/
|
||||
private static class MockConfigurationPort implements ConfigurationPort {
|
||||
private final Path tempDir;
|
||||
private boolean loadConfigurationCalled = false;
|
||||
|
||||
MockConfigurationPort(Path tempDir) {
|
||||
this.tempDir = tempDir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StartConfiguration loadConfiguration() {
|
||||
loadConfigurationCalled = true;
|
||||
|
||||
try {
|
||||
Path sourceDir = Files.createDirectories(tempDir.resolve("source"));
|
||||
Path targetDir = Files.createDirectories(tempDir.resolve("target"));
|
||||
Path dbFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
return new StartConfiguration(
|
||||
sourceDir,
|
||||
targetDir,
|
||||
dbFile,
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
promptFile,
|
||||
tempDir.resolve("lock.lock"),
|
||||
tempDir.resolve("logs"),
|
||||
"INFO",
|
||||
"test-key"
|
||||
);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to create mock configuration", e);
|
||||
}
|
||||
}
|
||||
|
||||
boolean wasLoadConfigurationCalled() {
|
||||
return loadConfigurationCalled;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock RunLockPort for testing.
|
||||
*/
|
||||
private static class MockRunLockPort implements RunLockPort {
|
||||
private boolean acquireCalled = false;
|
||||
private boolean releaseCalled = false;
|
||||
|
||||
@Override
|
||||
public void acquire() {
|
||||
acquireCalled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
releaseCalled = true;
|
||||
}
|
||||
|
||||
boolean wasAcquireCalled() {
|
||||
return acquireCalled;
|
||||
}
|
||||
|
||||
boolean wasReleaseCalled() {
|
||||
return releaseCalled;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user