Fix #52: Race Condition auf pendingMessages beim Test-Trigger

pendingMessages.clear() wurde aus runTechnicalTestsAction() in den
Coordinator verlagert (erste Anweisung in triggerTechnicalTests()).
Damit liegen clear() und Worker-Start auf demselben Thread (FX),
und das Race-Fenster zwischen clear() und den per Platform.runLater
zurueckgefuehrten add()-Aufrufen entfaellt.

Die fachliche Produktionssemantik (Replace beim Trigger) bleibt
identisch. JavaDoc und Kommentare wurden auf Replace-Semantik
korrigiert. Der Smoke-Test trigger_twice_accumulatesTestEntries
wurde zu trigger_twice_replacesTestEntries umbenannt und prueft
nun die Replace-Erwartung des isolierten Coordinators.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 15:28:18 +02:00
parent 014b11abd2
commit 0da80849d4
3 changed files with 20 additions and 11 deletions
@@ -1925,7 +1925,8 @@ public final class GuiConfigurationEditorWorkspace {
*/ */
private void runTechnicalTestsAction() { private void runTechnicalTestsAction() {
LOG.info("Aktion Technische Tests ausführen gestartet."); LOG.info("Aktion Technische Tests ausführen gestartet.");
pendingMessages.clear(); // Hinweis: pendingMessages.clear() erfolgt jetzt im Coordinator selbst,
// damit clear() und Worker-Start auf demselben Thread (FX) liegen.
technicalTestsButton.setDisable(true); technicalTestsButton.setDisable(true);
technicalTestCoordinator.triggerTechnicalTests(); technicalTestCoordinator.triggerTechnicalTests();
} }
@@ -113,6 +113,9 @@ public final class GuiTechnicalTestCoordinator {
/** /**
* Löst die asynchrone Ausführung des vollständigen technischen Gesamttests aus. * Löst die asynchrone Ausführung des vollständigen technischen Gesamttests aus.
* <p> * <p>
* Vor dem Worker-Start wird die geteilte Nachrichtenliste auf dem FX-Thread geleert;
* jeder Aufruf ersetzt die zuvor angefügten Einträge (Replace-Semantik).
* <p>
* Liest den aktuellen Editorzustand und den Konfigurationsdateipfad, baut einen * Liest den aktuellen Editorzustand und den Konfigurationsdateipfad, baut einen
* {@link TechnicalTestRequest} und startet den {@link TechnicalTestOrchestrator} auf * {@link TechnicalTestRequest} und startet den {@link TechnicalTestOrchestrator} auf
* einem Hintergrund-Worker-Thread. Das Ergebnis wird via {@code resultDelivery} an den * einem Hintergrund-Worker-Thread. Das Ergebnis wird via {@code resultDelivery} an den
@@ -124,6 +127,11 @@ public final class GuiTechnicalTestCoordinator {
* Muss auf dem JavaFX Application Thread aufgerufen werden. * Muss auf dem JavaFX Application Thread aufgerufen werden.
*/ */
public void triggerTechnicalTests() { public void triggerTechnicalTests() {
// Bestehende Nachrichtenliste auf dem FX-Thread leeren, bevor der Worker-Thread
// startet. Dadurch laufen clear() und nachfolgende add()-Aufrufe (die per
// Platform.runLater wieder auf dem FX-Thread landen) auf demselben Thread und
// es entsteht kein Race-Fenster mit der UI.
pendingMessages.clear();
EditorValidationInput input = inputProvider.get(); EditorValidationInput input = inputProvider.get();
String configFilePath = configFilePathProvider.get(); String configFilePath = configFilePathProvider.get();
TechnicalTestRequest request = new TechnicalTestRequest(input, configFilePath); TechnicalTestRequest request = new TechnicalTestRequest(input, configFilePath);
@@ -146,15 +154,14 @@ public final class GuiTechnicalTestCoordinator {
* Wendet das Ergebnis des vollständigen Gesamttests auf die geteilte Nachrichtenliste an. * Wendet das Ergebnis des vollständigen Gesamttests auf die geteilte Nachrichtenliste an.
* <p> * <p>
* Fügt für jedes Checkpoint-Ergebnis einen neuen Eintrag zur geteilten Nachrichtenliste * Fügt für jedes Checkpoint-Ergebnis einen neuen Eintrag zur geteilten Nachrichtenliste
* hinzu; vorhandene Einträge bleiben erhalten, sodass die Meldungen über mehrere * hinzu. Die Liste wurde zuvor in {@link #triggerTechnicalTests()} geleert, sodass jeder
* Testläufe hinweg akkumulieren. Zusätzlich wird eine Zusammenfassung angehängt. * Aufruf einen frischen Stand erzeugt. Zusätzlich wird eine Zusammenfassung angehängt.
* <p> * <p>
* Muss nur auf dem JavaFX Application Thread aufgerufen werden (via {@code resultDelivery}). * Muss nur auf dem JavaFX Application Thread aufgerufen werden (via {@code resultDelivery}).
* *
* @param report der vollständige Gesamttestbericht; darf nicht {@code null} sein * @param report der vollständige Gesamttestbericht; darf nicht {@code null} sein
*/ */
private void applyResult(TechnicalTestReport report) { private void applyResult(TechnicalTestReport report) {
// Akkumulieren: Vorherige Einträge anderer Läufe bleiben erhalten.
long successCount = 0; long successCount = 0;
long failureErrorCount = 0; long failureErrorCount = 0;
@@ -162,18 +162,19 @@ class GuiTechnicalTestCoordinatorSmokeTest {
} }
// ========================================================================= // =========================================================================
// Scenario: accumulation semantics second trigger appends fresh entries // Scenario: replace semantics second trigger replaces the previous batch
// ========================================================================= // =========================================================================
/** /**
* Smoke test: triggering the coordinator twice accumulates both runs; the * Smoke test: triggering the coordinator twice replaces the previous batch;
* second trigger appends a fresh batch of SOURCE_TAG entries without * the second trigger clears the shared message list before applying its own
* removing the first batch. * SOURCE_TAG entries, so the count after the second run equals the count
* after the first run.
* *
* @throws Exception if the FX thread task fails or times out * @throws Exception if the FX thread task fails or times out
*/ */
@Test @Test
void trigger_twice_accumulatesTestEntries() throws Exception { void trigger_twice_replacesTestEntries() throws Exception {
runOnFx(() -> { runOnFx(() -> {
List<GuiMessageEntry> messages = new ArrayList<>(); List<GuiMessageEntry> messages = new ArrayList<>();
GuiTechnicalTestCoordinator coordinator = buildSyncCoordinator(messages, report -> { }); GuiTechnicalTestCoordinator coordinator = buildSyncCoordinator(messages, report -> { });
@@ -190,8 +191,8 @@ class GuiTechnicalTestCoordinatorSmokeTest {
&& GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get())) && GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get()))
.count(); .count();
assertEquals(countAfterFirst * 2, countAfterSecond, assertEquals(countAfterFirst, countAfterSecond,
"Second trigger must append a fresh batch, doubling the SOURCE_TAG entries"); "Second trigger must clear and replace the previous SOURCE_TAG batch");
}); });
} }