Erhoehe new-code-Coverage durch gezielte Tests fuer Reliability-Fixes

Schliesst die durch die SonarQube-Reliability-Fixes (Commit 32e32a9)
neu eingefuehrten Quellzeilen testseitig ab, damit das new-coverage
Quality Gate von 80% wieder erreicht wird.

PdfPreviewPaneRenderingTest:
- erzeugt mit PDFBox echte ein- bzw. mehrseitige PDFs und ruft
  loadSource() auf, sodass loadAndRenderFirstPageOnWorker (inkl.
  AtomicReference-Setter fuer currentDocument/currentRenderer) und
  renderPageOnWorker (AtomicReference-Getter) tatsaechlich ausgefuehrt
  werden.

BootstrapRunnerGuiContextInitFailureTest:
- deckt die catch-Zweige fuer InvalidStartConfigurationException,
  DocumentPersistenceException und unspezifische RuntimeException in
  initializeApplicationRunContext ab. Damit werden die Pfade ausgefuehrt,
  in denen guiApplicationRunContext via AtomicReference.set(Optional.empty())
  zurueckgesetzt wird.

Keine Aenderungen an Produktivcode oder bestehenden Tests.
This commit is contained in:
2026-05-07 17:53:09 +02:00
parent 32e32a9b27
commit e9061d1b1f
2 changed files with 345 additions and 0 deletions
@@ -0,0 +1,149 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import javafx.application.Platform;
/**
* Headless (Monocle) Tests, die echte PDF-Dateien rendern, damit die
* Worker-Thread-Pfade {@code loadAndRenderFirstPageOnWorker} und
* {@code renderPageOnWorker} tatsächlich ausgeführt werden.
*/
class PdfPreviewPaneRenderingTest {
private static final long FX_TIMEOUT_SECONDS = 10;
private static final long WORKER_TIMEOUT_SECONDS = 15;
private static final AtomicBoolean PLATFORM_STARTED = new AtomicBoolean(false);
@BeforeAll
static void startPlatform() throws InterruptedException {
Platform.setImplicitExit(false);
if (PLATFORM_STARTED.compareAndSet(false, true)) {
CountDownLatch latch = new CountDownLatch(1);
try {
Platform.startup(latch::countDown);
} catch (IllegalStateException alreadyStarted) {
latch.countDown();
}
assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));
}
}
@Test
void loadSource_realSinglePagePdf_pageLabelShowsRenderedPage(@TempDir Path tempDir) throws Exception {
Path pdfFile = tempDir.resolve("single-page.pdf");
createPdfWithPages(pdfFile, 1);
AtomicReference<PdfPreviewPane> paneRef = new AtomicReference<>();
CountDownLatch firstPageRendered = new CountDownLatch(1);
runOnFx(() -> {
PdfPreviewPane pane = new PdfPreviewPane();
paneRef.set(pane);
pane.pageLabel().textProperty().addListener((obs, old, newText) -> {
if (newText != null && newText.contains("Seite 1 / 1")) {
firstPageRendered.countDown();
}
});
pane.loadSource(pdfFile);
});
assertTrue(firstPageRendered.await(WORKER_TIMEOUT_SECONDS, TimeUnit.SECONDS),
"Erste Seite eines einseitigen PDFs muss innerhalb der Worker-Timeout-Frist gerendert werden");
runOnFx(() -> paneRef.get().shutdown());
}
@Test
void navigateToNextPage_multiPagePdf_rendersSecondPage(@TempDir Path tempDir) throws Exception {
Path pdfFile = tempDir.resolve("multi-page.pdf");
createPdfWithPages(pdfFile, 3);
AtomicReference<PdfPreviewPane> paneRef = new AtomicReference<>();
CountDownLatch firstPageRendered = new CountDownLatch(1);
CountDownLatch secondPageRendered = new CountDownLatch(1);
AtomicBoolean firstSeen = new AtomicBoolean(false);
runOnFx(() -> {
PdfPreviewPane pane = new PdfPreviewPane();
paneRef.set(pane);
pane.pageLabel().textProperty().addListener((obs, old, newText) -> {
if (newText == null) {
return;
}
if (newText.contains("Seite 1 / 3") && firstSeen.compareAndSet(false, true)) {
firstPageRendered.countDown();
} else if (newText.contains("Seite 2 / 3")) {
secondPageRendered.countDown();
}
});
pane.loadSource(pdfFile);
});
assertTrue(firstPageRendered.await(WORKER_TIMEOUT_SECONDS, TimeUnit.SECONDS),
"Erste Seite muss innerhalb der Worker-Timeout-Frist gerendert werden");
// Auf zweite Seite navigieren triggert renderPageOnWorker
runOnFx(() -> paneRef.get().nextButton().fire());
assertTrue(secondPageRendered.await(WORKER_TIMEOUT_SECONDS, TimeUnit.SECONDS),
"Zweite Seite muss nach Klick auf Weiter gerendert werden");
runOnFx(() -> paneRef.get().shutdown());
}
// -------------------------------------------------------------------------
// Hilfsmethoden
// -------------------------------------------------------------------------
private static void createPdfWithPages(Path outputPath, int pages) throws IOException {
try (PDDocument doc = new PDDocument()) {
for (int i = 1; i <= pages; i++) {
PDPage page = new PDPage();
doc.addPage(page);
try (PDPageContentStream stream = new PDPageContentStream(doc, page)) {
stream.beginText();
stream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
stream.newLineAtOffset(50, 700);
stream.showText("Testseite " + i);
stream.endText();
}
}
doc.save(outputPath.toFile());
}
}
private void runOnFx(Runnable action) throws InterruptedException {
CountDownLatch done = new CountDownLatch(1);
AtomicReference<Throwable> error = new AtomicReference<>();
Platform.runLater(() -> {
try {
action.run();
} catch (Throwable t) {
error.set(t);
} finally {
done.countDown();
}
});
assertTrue(done.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS), "FX-Thread Timeout");
if (error.get() != null) {
throw new AssertionError(error.get());
}
}
}