From c2461475c1f04942440f07005c7400d5405c0180 Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Mon, 12 Oct 2026 10:19:26 +0200 Subject: [PATCH 01/26] Cleanup of IdeLogEntry, IdeLogListener and IdeGuiLogListener --- .../devonfw/tools/ide/log/IdeLogEntry.java | 20 +++--- .../devonfw/tools/ide/log/IdeLogListener.java | 2 +- .../tools/ide/log/IdeLogListenerBuffer.java | 6 +- .../tools/ide/log/IdeLogListenerNone.java | 2 +- .../tools/ide/log/Slf4jLoggerAdapter.java | 2 +- .../ide/log/IdeLogListenerCollector.java | 6 +- .../gui/core/context/GuiOutputListener.java | 3 +- .../gui/core/context/IdeGuiLogListener.java | 19 ++---- .../mainwindow/console/ConsoleController.java | 23 ------- .../gui/core/service/CommandletService.java | 4 +- .../core/context/IdeTestGuiLogListener.java | 63 +++++++++++++++++++ .../mainwindow/console/ConsolePanelTest.java | 24 +++---- 12 files changed, 105 insertions(+), 69 deletions(-) create mode 100644 gui/src/test/java/com/devonfw/ide/gui/core/context/IdeTestGuiLogListener.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java index 1c16284af9..8cb4266d6a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java @@ -14,8 +14,12 @@ * @param error the {@link IdeSubLogger#log(Throwable, String) optional error that was logged}. * @param contains - {@code true} if the {@link IdeLogEntry} to create is used as sub-string pattern for {@link #matches(IdeLogEntry) matching}, * {@code false} otherwise. + * @param timestamp the {@link Instant} when the {@link IdeLogEntry} was created. */ -public record IdeLogEntry(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error, boolean contains, Long timeStamp) { +public record IdeLogEntry(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error, boolean contains, Instant timestamp) { + + /** The {@link DateTimeFormatter} used by {@link #displayValue()}. */ + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.SSS").withZone(ZoneId.systemDefault()); /** * The constructor. @@ -41,7 +45,7 @@ public record IdeLogEntry(IdeLogLevel level, String message, String rawMessage, */ public IdeLogEntry(IdeLogLevel level, String message) { - this(level, message, null, null, null, false, System.currentTimeMillis()); + this(level, message, null, null, null, false, Instant.now()); } /** @@ -51,7 +55,7 @@ public IdeLogEntry(IdeLogLevel level, String message) { */ public IdeLogEntry(IdeLogLevel level, String message, boolean contains) { - this(level, message, null, null, null, contains, System.currentTimeMillis()); + this(level, message, null, null, null, contains, Instant.now()); } /** @@ -62,7 +66,7 @@ public IdeLogEntry(IdeLogLevel level, String message, boolean contains) { */ public IdeLogEntry(IdeLogLevel level, String message, String rawMessage, Object[] args) { - this(level, message, rawMessage, args, null, false, System.currentTimeMillis()); + this(level, message, rawMessage, args, null, false, Instant.now()); } /** @@ -74,7 +78,7 @@ public IdeLogEntry(IdeLogLevel level, String message, String rawMessage, Object[ */ public IdeLogEntry(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error) { - this(level, message, rawMessage, args, error, false, System.currentTimeMillis()); + this(level, message, rawMessage, args, error, false, Instant.now()); } /** @@ -86,7 +90,7 @@ public IdeLogEntry(IdeLogLevel level, String message, String rawMessage, Object[ */ public IdeLogEntry(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error, boolean contains) { - this(level, message, rawMessage, args, error, contains, System.currentTimeMillis()); + this(level, message, rawMessage, args, error, contains, Instant.now()); } /** @@ -196,8 +200,6 @@ public static IdeLogEntry ofProcessable(String message) { /// @return A human-readable, formatted string of the log entry following the structure: `timestamp | [LogLevel] message` public String displayValue() { - DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss.SSS").withZone(ZoneId.systemDefault()); - String timeStampReadable = DATETIME_FORMAT.format(Instant.ofEpochMilli(this.timeStamp)); - return String.format("%s | [%s] %s", timeStampReadable, this.level(), this.message()); + return String.format("%s | [%s] %s", IdeLogEntry.TIME_FORMATTER.format(this.timestamp()), this.level(), this.message()); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java index f768befe98..d937d7fe24 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java @@ -14,6 +14,6 @@ public interface IdeLogListener { * @param error the optional error. * @return {@code true} to accept this log event (default), {@code false} otherwise to reject further processing and suppress the log message. */ - boolean onLog(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error); + boolean onLog(IdeLogEntry entry); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerBuffer.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerBuffer.java index 99ed845343..8ec71ca0bf 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerBuffer.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerBuffer.java @@ -36,10 +36,10 @@ public IdeLogListenerBuffer() { } @Override - public boolean onLog(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error) { + public boolean onLog(IdeLogEntry entry) { if (this.buffering) { - if (level.ordinal() >= threshold.ordinal()) { - this.buffer.add(new IdeLogEntry(level, message, rawMessage, args, error)); + if (entry.level().ordinal() >= threshold.ordinal()) { + this.buffer.add(entry); } return false; } diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerNone.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerNone.java index 64ac76964e..45d7947542 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerNone.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerNone.java @@ -13,7 +13,7 @@ private IdeLogListenerNone() { } @Override - public boolean onLog(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error) { + public boolean onLog(IdeLogEntry entry) { return true; } diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/Slf4jLoggerAdapter.java b/cli/src/main/java/com/devonfw/tools/ide/log/Slf4jLoggerAdapter.java index 47713db162..c1b0420308 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/Slf4jLoggerAdapter.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/Slf4jLoggerAdapter.java @@ -133,7 +133,7 @@ protected void handleNormalizedLoggingCall(Level level, Marker marker, String me argFormatter = startContext.getArgFormatter(); } String composedMessage = compose(argFormatter, message, args); - boolean accept = listener.onLog(ideLevel, composedMessage, message, args, error); + boolean accept = listener.onLog(new IdeLogEntry(ideLevel, composedMessage, message, args, error)); if (accept) { java.util.logging.Level julLevel = ideLevel.getJulLevel(); this.julLogger.log(julLevel, composedMessage, error); diff --git a/cli/src/test/java/com/devonfw/tools/ide/log/IdeLogListenerCollector.java b/cli/src/test/java/com/devonfw/tools/ide/log/IdeLogListenerCollector.java index 6099578011..77dc0c100b 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/log/IdeLogListenerCollector.java +++ b/cli/src/test/java/com/devonfw/tools/ide/log/IdeLogListenerCollector.java @@ -27,10 +27,10 @@ public List getEntries() { } @Override - public boolean onLog(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error) { - boolean accept = super.onLog(level, message, rawMessage, args, error); + public boolean onLog(IdeLogEntry entry) { + boolean accept = super.onLog(entry); if (accept) { - this.entries.add(new IdeLogEntry(level, message, rawMessage, args, error)); + this.entries.add(entry); } return accept; } diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/context/GuiOutputListener.java b/gui/src/main/java/com/devonfw/ide/gui/core/context/GuiOutputListener.java index 2c23ad3f29..15be47b850 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/context/GuiOutputListener.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/context/GuiOutputListener.java @@ -3,6 +3,7 @@ import javafx.application.Platform; import com.devonfw.ide.gui.core.mainwindow.console.ConsoleController; +import com.devonfw.tools.ide.log.IdeLogEntry; import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.process.OutputListener; @@ -24,7 +25,7 @@ public GuiOutputListener(ConsoleController consoleController) { public void onOutput(String message, boolean error) { if (this.consoleController != null && message != null) { String prefix = error ? "[STDERR] " : ""; - Platform.runLater(() -> this.consoleController.appendOutput(error ? IdeLogLevel.ERROR : IdeLogLevel.INFO, prefix + message)); + Platform.runLater(() -> this.consoleController.appendOutput(new IdeLogEntry(error ? IdeLogLevel.ERROR : IdeLogLevel.INFO, prefix + message))); } } } diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/context/IdeGuiLogListener.java b/gui/src/main/java/com/devonfw/ide/gui/core/context/IdeGuiLogListener.java index e4701f1838..d4b1e49384 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/context/IdeGuiLogListener.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/context/IdeGuiLogListener.java @@ -2,7 +2,7 @@ import com.devonfw.ide.gui.core.mainwindow.console.ConsoleController; -import com.devonfw.tools.ide.log.IdeLogLevel; +import com.devonfw.tools.ide.log.IdeLogEntry; import com.devonfw.tools.ide.log.IdeLogListenerBuffer; /// Listener class that listens to internal ideasy output, e.g. output from commandlets that are run. @@ -18,20 +18,13 @@ public IdeGuiLogListener(ConsoleController consoleController) { } @Override - public boolean onLog(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error) { + public boolean onLog(IdeLogEntry entry) { - //If we are in buffer mode, pass responsibility to superclass - if (this.isBuffering()) { - super.onLog(level, message, rawMessage, args, error); + boolean accept = super.onLog(entry); + if (accept && (this.consoleController != null)) { + this.consoleController.appendOutput(entry); } - - if (this.consoleController != null && message != null) { - this.consoleController.appendOutput(level, message); - if (error != null) { - this.consoleController.appendOutput(IdeLogLevel.ERROR, " Error: " + error.getMessage()); - } - } - return true; // continue processing (also log to standard output if needed) + return accept; } } diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java b/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java index c05b6ce55f..38be09b888 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java @@ -4,7 +4,6 @@ import java.util.Deque; import java.util.List; import java.util.stream.Collectors; - import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; @@ -18,7 +17,6 @@ import com.devonfw.ide.gui.core.helper.FxHelper; import com.devonfw.ide.gui.core.service.NlsService; import com.devonfw.tools.ide.log.IdeLogEntry; -import com.devonfw.tools.ide.log.IdeLogLevel; /** * Controller that manages an instance of a console using ListView for better performance with large outputs. @@ -83,16 +81,6 @@ private void setupEventHandlers() { clearButton.setOnAction(_ -> clearConsole()); } - /** - * Prints a simple message to the console. - * - * @param message message to be printed - */ - public void appendOutput(String message) { - - appendOutput(new IdeLogEntry(IdeLogLevel.INFO, message)); - } - /** * Prints a log entry to the console. * @@ -109,17 +97,6 @@ public void appendOutput(IdeLogEntry entry) { } } - /** - * Prints a message with a log level to the console. - * - * @param level the log level - * @param message the message - */ - public void appendOutput(IdeLogLevel level, String message) { - - appendOutput(new IdeLogEntry(level, message)); - } - /** * Flushes all buffered messages to the console. */ diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java b/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java index 2897bac612..c3c2c44a48 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java @@ -2,7 +2,6 @@ import java.nio.file.Path; import java.util.Objects; - import javafx.application.Platform; import javafx.concurrent.Task; import javafx.scene.control.Alert.AlertType; @@ -18,6 +17,7 @@ import com.devonfw.ide.gui.core.modal.IdeDialog; import com.devonfw.ide.gui.core.progress.ProgressBarTask; import com.devonfw.tools.ide.context.IdeStartContextImpl; +import com.devonfw.tools.ide.log.IdeLogEntry; import com.devonfw.tools.ide.log.IdeLogLevel; /** @@ -103,7 +103,7 @@ protected Void call() { LOG.info("Commandlet {} completed successfully.", commandlet); } catch (Exception e) { LOG.error("Failed to run commandlet {}: {}", commandlet, e.getMessage(), e); - consoleController.appendOutput("[ERROR] Failed to launch " + commandlet + ": " + e.getMessage()); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "[ERROR] Failed to launch " + commandlet + ": " + e.getMessage())); } return null; } diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeTestGuiLogListener.java b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeTestGuiLogListener.java new file mode 100644 index 0000000000..279e02d62f --- /dev/null +++ b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeTestGuiLogListener.java @@ -0,0 +1,63 @@ +package com.devonfw.ide.gui.core.context; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import com.devonfw.ide.gui.core.mainwindow.console.ConsoleController; +import com.devonfw.tools.ide.log.IdeLogEntry; +import com.devonfw.tools.ide.log.IdeLogLevel; + +/** + * Tests for {@link IdeGuiLogListener}. + */ +public class IdeTestGuiLogListener { + + @TempDir + private Path mockIdeRoot; + + /** + * A {@link ConsoleController} that records every entry forwarded to it instead of touching the JavaFX toolkit, so the number of times + * {@link IdeGuiLogListener#onLog(IdeLogEntry)} sends a message can be counted. + */ + private static class RecordingConsoleController extends ConsoleController { + + private final List appended = new ArrayList<>(); + + RecordingConsoleController(GuiStateManager guiStateManager) { + super(guiStateManager); + } + + @Override + public void appendOutput(IdeLogEntry entry) { + + this.appended.add(entry); + } + + List getEntries() { + return this.appended; + } + } + + @Test + void testMessageOnlySendOnce() { + + GuiStateManager guiStateManager = new GuiStateManager(this.mockIdeRoot.toString()); + RecordingConsoleController console = new RecordingConsoleController(guiStateManager); + IdeGuiLogListener listener = new IdeGuiLogListener(console); + listener.flushAndEndBuffering(); + + String message = "TestMessage"; + IdeLogEntry entry = new IdeLogEntry(IdeLogLevel.INFO, message); + boolean accepted = listener.onLog(entry); + + assertThat(accepted).as("onLog() should accept the entry").isTrue(); + assertThat(console.getEntries()).as("the entry must be forwarded to the console exactly once").hasSize(1); + assertThat(console.getEntries().get(0).message()).isEqualTo(message); + } +} diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java index dd5bd1cb83..bbfce60e4a 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java @@ -7,7 +7,6 @@ import java.net.URL; import java.nio.file.Path; import java.util.List; - import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; @@ -20,6 +19,7 @@ import com.devonfw.ide.gui.HeadlessApplicationTest; import com.devonfw.ide.gui.core.context.GuiStateManager; import com.devonfw.ide.gui.core.service.NlsService; +import com.devonfw.tools.ide.log.IdeLogEntry; import com.devonfw.tools.ide.log.IdeLogLevel; class ConsolePanelTest extends HeadlessApplicationTest { @@ -57,8 +57,8 @@ void testConsoleOutputPrintsCorrectly() { // Simulate output to the console Platform.runLater(() -> { - consoleController.appendOutput("Hello World!"); - consoleController.appendOutput("Test"); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Hello World")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Test")); }); waitForFxEvents(); @@ -75,10 +75,10 @@ void testConsoleOutputPrintsCorrectly() { void testConsoleOutputWithLogLevels() { Platform.runLater(() -> { - consoleController.appendOutput(IdeLogLevel.INFO, "Info message"); - consoleController.appendOutput(IdeLogLevel.ERROR, "Error message"); - consoleController.appendOutput(IdeLogLevel.WARNING, "Warning message"); - consoleController.appendOutput(IdeLogLevel.DEBUG, "Debug message"); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Info message")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.ERROR, "Error message")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.WARNING, "Warning message")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.DEBUG, "Debug message")); }); waitForFxEvents(); @@ -97,8 +97,8 @@ void testConsoleClear() { // Simulate output to the console Platform.runLater(() -> { - consoleController.appendOutput("Hello World!"); - consoleController.appendOutput("Test"); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Hello World")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Test")); }); waitForFxEvents(); @@ -113,9 +113,9 @@ void testConsoleClear() { @Test void testLineCountUpdates() { Platform.runLater(() -> { - consoleController.appendOutput("Line 1"); - consoleController.appendOutput("Line 2"); - consoleController.appendOutput("Line 3"); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Line 1")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Line 2")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Line 3")); }); waitForFxEvents(); From 27fa9bff63afbefb834053690fe5806990bc548b Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Mon, 12 Oct 2026 11:25:33 +0200 Subject: [PATCH 02/26] Cleanup of IdeLogEntry, IdeLogListener and IdeGuiLogListener --- .../ide/gui/core/mainwindow/console/ConsolePanelTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java index bbfce60e4a..d722067939 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java @@ -57,7 +57,7 @@ void testConsoleOutputPrintsCorrectly() { // Simulate output to the console Platform.runLater(() -> { - consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Hello World")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Hello World!")); consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Test")); }); waitForFxEvents(); @@ -97,7 +97,7 @@ void testConsoleClear() { // Simulate output to the console Platform.runLater(() -> { - consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Hello World")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Hello World!")); consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Test")); }); waitForFxEvents(); From 21e1d05dc2d7838864161c5ad53655ef0e2bcbca Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Mon, 12 Oct 2026 10:19:26 +0200 Subject: [PATCH 03/26] Cleanup of IdeLogEntry, IdeLogListener and IdeGuiLogListener --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- CHANGELOG.adoc | 1 + .../AbstractEnvironmentVariables.java | 4 +- .../devonfw/tools/ide/git/GitContextImpl.java | 2 +- .../devonfw/tools/ide/log/IdeLogEntry.java | 20 +++--- .../devonfw/tools/ide/log/IdeLogListener.java | 2 +- .../tools/ide/log/IdeLogListenerBuffer.java | 6 +- .../tools/ide/log/IdeLogListenerNone.java | 2 +- .../tools/ide/log/Slf4jLoggerAdapter.java | 2 +- .../tools/ide/process/ProcessContextImpl.java | 8 +++ .../environment/EnvironmentVariablesTest.java | 28 +++++++++ .../ide/log/IdeLogListenerCollector.java | 6 +- .../ide/process/ProcessContextImplTest.java | 20 +++++- .../gui/core/context/GuiOutputListener.java | 3 +- .../gui/core/context/IdeGuiLogListener.java | 19 ++---- .../mainwindow/console/ConsoleController.java | 23 ------- .../gui/core/service/CommandletService.java | 4 +- .../core/context/IdeTestGuiLogListener.java | 63 +++++++++++++++++++ .../mainwindow/console/ConsolePanelTest.java | 24 +++---- 19 files changed, 165 insertions(+), 74 deletions(-) create mode 100644 gui/src/test/java/com/devonfw/ide/gui/core/context/IdeTestGuiLogListener.java diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2aa82ea19d..92b89e3751 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -25,7 +25,7 @@ our [DoD](https://github.com/devonfw/IDEasy/blob/main/documentation/contributing - [ ] PR top-level comment summarizes what has been done and contains link to addressed issue(s) - [ ] PR and issue(s) have suitable labels - [ ] Issue is set to `In Progress` and assigned to you *or* there is no issue (might happen for very small PRs) -- [ ] You followed all [coding conventions](https://github.com/devonfw/IDEasy/blob/main/documentation/coding-conventions.adoc) +- [ ] You followed all [coding conventions](https://github.com/devonfw/IDEasy/blob/main/documentation/contributing/coding-conventions.adoc) - [ ] You have added the issue implemented by your PR in [CHANGELOG.adoc](https://github.com/devonfw/IDEasy/blob/main/CHANGELOG.adoc) unless issue is labelled with `internal` - [ ] You have not changed any dependency in `pom.xml` files or otherwise if runtime dependencies changed, diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 999b70ddc9..caf62630ea 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,6 +6,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/2546[#2546]: Include repository into git warning/errors * https://github.com/devonfw/IDEasy/issues/2293[#2293]: Rework spyder to have same features as IdeToolCommandlets * https://github.com/devonfw/IDEasy/issues/871[#871]: Add progressbar to *.dmg extract process * https://github.com/devonfw/IDEasy/issues/2345[#2345]: DOCKER_EDITION ignored on Linux diff --git a/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java b/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java index 64a82ddd40..6d4d1003fa 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java +++ b/cli/src/main/java/com/devonfw/tools/ide/environment/AbstractEnvironmentVariables.java @@ -334,7 +334,9 @@ public String inverseResolve(String string, Object src, VariableSyntax syntax) { if (value == null) { value = variable.getDefaultValueAsString(this.context); } - if (value != null) { + // skip empty values: they carry nothing to inverse-resolve and String.replace("", token) would + // insert the variable expression between every character, corrupting the value + if ((value != null) && !value.isEmpty()) { result = result.replace(value, syntax.create(name)); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java index 7d00255902..7e86b16cd1 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java @@ -272,7 +272,7 @@ public void fetch(Path repository, String remote, String branch) { ProcessResult result = runGitCommand(repository, ProcessMode.DEFAULT_CAPTURE, "fetch", remote, branch); if (!result.isSuccessful()) { - LOG.warn("Git fetch for '{}/{} failed.'.", remote, branch); + LOG.warn("Git fetch for '{}/{}' in repository '{}' failed.", remote, branch, repository); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java index 1c16284af9..8cb4266d6a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java @@ -14,8 +14,12 @@ * @param error the {@link IdeSubLogger#log(Throwable, String) optional error that was logged}. * @param contains - {@code true} if the {@link IdeLogEntry} to create is used as sub-string pattern for {@link #matches(IdeLogEntry) matching}, * {@code false} otherwise. + * @param timestamp the {@link Instant} when the {@link IdeLogEntry} was created. */ -public record IdeLogEntry(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error, boolean contains, Long timeStamp) { +public record IdeLogEntry(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error, boolean contains, Instant timestamp) { + + /** The {@link DateTimeFormatter} used by {@link #displayValue()}. */ + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.SSS").withZone(ZoneId.systemDefault()); /** * The constructor. @@ -41,7 +45,7 @@ public record IdeLogEntry(IdeLogLevel level, String message, String rawMessage, */ public IdeLogEntry(IdeLogLevel level, String message) { - this(level, message, null, null, null, false, System.currentTimeMillis()); + this(level, message, null, null, null, false, Instant.now()); } /** @@ -51,7 +55,7 @@ public IdeLogEntry(IdeLogLevel level, String message) { */ public IdeLogEntry(IdeLogLevel level, String message, boolean contains) { - this(level, message, null, null, null, contains, System.currentTimeMillis()); + this(level, message, null, null, null, contains, Instant.now()); } /** @@ -62,7 +66,7 @@ public IdeLogEntry(IdeLogLevel level, String message, boolean contains) { */ public IdeLogEntry(IdeLogLevel level, String message, String rawMessage, Object[] args) { - this(level, message, rawMessage, args, null, false, System.currentTimeMillis()); + this(level, message, rawMessage, args, null, false, Instant.now()); } /** @@ -74,7 +78,7 @@ public IdeLogEntry(IdeLogLevel level, String message, String rawMessage, Object[ */ public IdeLogEntry(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error) { - this(level, message, rawMessage, args, error, false, System.currentTimeMillis()); + this(level, message, rawMessage, args, error, false, Instant.now()); } /** @@ -86,7 +90,7 @@ public IdeLogEntry(IdeLogLevel level, String message, String rawMessage, Object[ */ public IdeLogEntry(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error, boolean contains) { - this(level, message, rawMessage, args, error, contains, System.currentTimeMillis()); + this(level, message, rawMessage, args, error, contains, Instant.now()); } /** @@ -196,8 +200,6 @@ public static IdeLogEntry ofProcessable(String message) { /// @return A human-readable, formatted string of the log entry following the structure: `timestamp | [LogLevel] message` public String displayValue() { - DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss.SSS").withZone(ZoneId.systemDefault()); - String timeStampReadable = DATETIME_FORMAT.format(Instant.ofEpochMilli(this.timeStamp)); - return String.format("%s | [%s] %s", timeStampReadable, this.level(), this.message()); + return String.format("%s | [%s] %s", IdeLogEntry.TIME_FORMATTER.format(this.timestamp()), this.level(), this.message()); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java index f768befe98..d937d7fe24 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java @@ -14,6 +14,6 @@ public interface IdeLogListener { * @param error the optional error. * @return {@code true} to accept this log event (default), {@code false} otherwise to reject further processing and suppress the log message. */ - boolean onLog(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error); + boolean onLog(IdeLogEntry entry); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerBuffer.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerBuffer.java index 99ed845343..8ec71ca0bf 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerBuffer.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerBuffer.java @@ -36,10 +36,10 @@ public IdeLogListenerBuffer() { } @Override - public boolean onLog(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error) { + public boolean onLog(IdeLogEntry entry) { if (this.buffering) { - if (level.ordinal() >= threshold.ordinal()) { - this.buffer.add(new IdeLogEntry(level, message, rawMessage, args, error)); + if (entry.level().ordinal() >= threshold.ordinal()) { + this.buffer.add(entry); } return false; } diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerNone.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerNone.java index 64ac76964e..45d7947542 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerNone.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListenerNone.java @@ -13,7 +13,7 @@ private IdeLogListenerNone() { } @Override - public boolean onLog(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error) { + public boolean onLog(IdeLogEntry entry) { return true; } diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/Slf4jLoggerAdapter.java b/cli/src/main/java/com/devonfw/tools/ide/log/Slf4jLoggerAdapter.java index 47713db162..c1b0420308 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/Slf4jLoggerAdapter.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/Slf4jLoggerAdapter.java @@ -133,7 +133,7 @@ protected void handleNormalizedLoggingCall(Level level, Marker marker, String me argFormatter = startContext.getArgFormatter(); } String composedMessage = compose(argFormatter, message, args); - boolean accept = listener.onLog(ideLevel, composedMessage, message, args, error); + boolean accept = listener.onLog(new IdeLogEntry(ideLevel, composedMessage, message, args, error)); if (accept) { java.util.logging.Level julLevel = ideLevel.getJulLevel(); this.julLogger.log(julLevel, composedMessage, error); diff --git a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java index 9ef23ebf0d..3869d3f2fc 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/process/ProcessContextImpl.java @@ -49,6 +49,8 @@ public class ProcessContextImpl implements ProcessContext { protected Path executable; + private Path workingDirectory; + private String overriddenPath; private final List extraPathEntries; @@ -104,6 +106,7 @@ public ProcessContext errorHandling(ProcessErrorHandling handling) { public ProcessContext directory(Path directory) { if (directory != null) { + this.workingDirectory = directory; this.processBuilder.directory(directory.toFile()); } else { LOG.debug( @@ -315,6 +318,11 @@ private String createCommandMessage(String interpreter, String suffix) { sb.append("Running command '"); sb.append(this.executable); sb.append("'"); + if (this.workingDirectory != null) { + sb.append(" in '"); + sb.append(this.workingDirectory); + sb.append("'"); + } if (interpreter != null) { sb.append(" using "); sb.append(interpreter); diff --git a/cli/src/test/java/com/devonfw/tools/ide/environment/EnvironmentVariablesTest.java b/cli/src/test/java/com/devonfw/tools/ide/environment/EnvironmentVariablesTest.java index ee55d7c667..03a6fa60a3 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/environment/EnvironmentVariablesTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/environment/EnvironmentVariablesTest.java @@ -301,4 +301,32 @@ void testPlainVariableIsNotMasked() { assertThat(context.getSecretLineCount()).isZero(); } + /** + * Test of {@link EnvironmentVariables#inverseResolve(String, Object)} that variables which resolve to an empty value (e.g. + * {@link IdeVariables#HTTP_VERSIONS} when not configured) are skipped instead of being substituted. Replacing an empty + * value with a variable expression used to corrupt the value by inserting the expression between every character. + */ + @Test + void testInverseResolveSkipsVariablesResolvedToEmptyValue() { + + // arrange + IdeTestContext context = newContext(ENVIRONMENT_PROJECT, null, false); + EnvironmentVariables variables = context.getVariables(); + + // IDE_TOOLS must resolve to a non-empty value, while HTTP_VERSIONS must resolve to the empty string (its + // default when not configured) - the input that used to corrupt the result when inverseResolve ran + String ideTools = variables.get(IdeVariables.IDE_TOOLS.getName()); + assertThat(ideTools).isNotEmpty(); + assertThat(variables.get(IdeVariables.HTTP_VERSIONS.getName())).isEmpty(); + + // the editor value is similar to the one of a main.prefs produced by PropertiesMerger.inverseMerge + String editor = "notepad --wait " + ideTools; + + // act + String inverseResolved = variables.inverseResolve(editor, "test"); + + // assert + assertThat(inverseResolved).isEqualTo("notepad --wait $[IDE_TOOLS]"); + } + } diff --git a/cli/src/test/java/com/devonfw/tools/ide/log/IdeLogListenerCollector.java b/cli/src/test/java/com/devonfw/tools/ide/log/IdeLogListenerCollector.java index 6099578011..77dc0c100b 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/log/IdeLogListenerCollector.java +++ b/cli/src/test/java/com/devonfw/tools/ide/log/IdeLogListenerCollector.java @@ -27,10 +27,10 @@ public List getEntries() { } @Override - public boolean onLog(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error) { - boolean accept = super.onLog(level, message, rawMessage, args, error); + public boolean onLog(IdeLogEntry entry) { + boolean accept = super.onLog(entry); if (accept) { - this.entries.add(new IdeLogEntry(level, message, rawMessage, args, error)); + this.entries.add(entry); } return accept; } diff --git a/cli/src/test/java/com/devonfw/tools/ide/process/ProcessContextImplTest.java b/cli/src/test/java/com/devonfw/tools/ide/process/ProcessContextImplTest.java index 57174c6bd5..0285ef2162 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/process/ProcessContextImplTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/process/ProcessContextImplTest.java @@ -12,6 +12,7 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.nio.file.Path; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -293,8 +294,8 @@ void windowsQuoteShouldThrowOnArgumentContainingDoubleQuote() throws Exception { } /** - * Verify that {@code windowsQuote} escapes the special {@code cmd.exe} metacharacters ({@code ^ & | > < %}) and surrounds the value with double quotes, - * so the argument is passed through as a single literal token. + * Verify that {@code windowsQuote} escapes the special {@code cmd.exe} metacharacters ({@code ^ & | > < %}) and surrounds the value with double quotes, so + * the argument is passed through as a single literal token. */ @Test void windowsQuoteShouldEscapeCommandInjectionCharactersAndWrapInDoubleQuotes() throws Exception { @@ -329,4 +330,19 @@ private String invokeWindowsQuote(String value) throws Exception { } } + /** + * Verify that the log message of a failing process contains the explicitly set working directory. + */ + @Test + void failingProcessShouldIncludeWorkingDirectoryInLogMessage() throws InterruptedException { + Path workingDirectory = Path.of("/opt/my-repo"); + when(this.processMock.waitFor()).thenReturn(ProcessResult.TOOL_NOT_INSTALLED); + this.processContextUnderTest.errorHandling(ProcessErrorHandling.LOG_WARNING); + this.processContextUnderTest.directory(workingDirectory); + + this.processContextUnderTest.run(ProcessMode.DEFAULT); + + assertThat(this.context).log(IdeLogLevel.WARNING).hasMessageContaining("Running command"); + assertThat(this.context).log(IdeLogLevel.WARNING).hasMessageContaining("in '" + workingDirectory + "'"); + } } diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/context/GuiOutputListener.java b/gui/src/main/java/com/devonfw/ide/gui/core/context/GuiOutputListener.java index 2c23ad3f29..15be47b850 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/context/GuiOutputListener.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/context/GuiOutputListener.java @@ -3,6 +3,7 @@ import javafx.application.Platform; import com.devonfw.ide.gui.core.mainwindow.console.ConsoleController; +import com.devonfw.tools.ide.log.IdeLogEntry; import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.process.OutputListener; @@ -24,7 +25,7 @@ public GuiOutputListener(ConsoleController consoleController) { public void onOutput(String message, boolean error) { if (this.consoleController != null && message != null) { String prefix = error ? "[STDERR] " : ""; - Platform.runLater(() -> this.consoleController.appendOutput(error ? IdeLogLevel.ERROR : IdeLogLevel.INFO, prefix + message)); + Platform.runLater(() -> this.consoleController.appendOutput(new IdeLogEntry(error ? IdeLogLevel.ERROR : IdeLogLevel.INFO, prefix + message))); } } } diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/context/IdeGuiLogListener.java b/gui/src/main/java/com/devonfw/ide/gui/core/context/IdeGuiLogListener.java index e4701f1838..d4b1e49384 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/context/IdeGuiLogListener.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/context/IdeGuiLogListener.java @@ -2,7 +2,7 @@ import com.devonfw.ide.gui.core.mainwindow.console.ConsoleController; -import com.devonfw.tools.ide.log.IdeLogLevel; +import com.devonfw.tools.ide.log.IdeLogEntry; import com.devonfw.tools.ide.log.IdeLogListenerBuffer; /// Listener class that listens to internal ideasy output, e.g. output from commandlets that are run. @@ -18,20 +18,13 @@ public IdeGuiLogListener(ConsoleController consoleController) { } @Override - public boolean onLog(IdeLogLevel level, String message, String rawMessage, Object[] args, Throwable error) { + public boolean onLog(IdeLogEntry entry) { - //If we are in buffer mode, pass responsibility to superclass - if (this.isBuffering()) { - super.onLog(level, message, rawMessage, args, error); + boolean accept = super.onLog(entry); + if (accept && (this.consoleController != null)) { + this.consoleController.appendOutput(entry); } - - if (this.consoleController != null && message != null) { - this.consoleController.appendOutput(level, message); - if (error != null) { - this.consoleController.appendOutput(IdeLogLevel.ERROR, " Error: " + error.getMessage()); - } - } - return true; // continue processing (also log to standard output if needed) + return accept; } } diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java b/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java index c05b6ce55f..38be09b888 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java @@ -4,7 +4,6 @@ import java.util.Deque; import java.util.List; import java.util.stream.Collectors; - import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; @@ -18,7 +17,6 @@ import com.devonfw.ide.gui.core.helper.FxHelper; import com.devonfw.ide.gui.core.service.NlsService; import com.devonfw.tools.ide.log.IdeLogEntry; -import com.devonfw.tools.ide.log.IdeLogLevel; /** * Controller that manages an instance of a console using ListView for better performance with large outputs. @@ -83,16 +81,6 @@ private void setupEventHandlers() { clearButton.setOnAction(_ -> clearConsole()); } - /** - * Prints a simple message to the console. - * - * @param message message to be printed - */ - public void appendOutput(String message) { - - appendOutput(new IdeLogEntry(IdeLogLevel.INFO, message)); - } - /** * Prints a log entry to the console. * @@ -109,17 +97,6 @@ public void appendOutput(IdeLogEntry entry) { } } - /** - * Prints a message with a log level to the console. - * - * @param level the log level - * @param message the message - */ - public void appendOutput(IdeLogLevel level, String message) { - - appendOutput(new IdeLogEntry(level, message)); - } - /** * Flushes all buffered messages to the console. */ diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java b/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java index 2897bac612..c3c2c44a48 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java @@ -2,7 +2,6 @@ import java.nio.file.Path; import java.util.Objects; - import javafx.application.Platform; import javafx.concurrent.Task; import javafx.scene.control.Alert.AlertType; @@ -18,6 +17,7 @@ import com.devonfw.ide.gui.core.modal.IdeDialog; import com.devonfw.ide.gui.core.progress.ProgressBarTask; import com.devonfw.tools.ide.context.IdeStartContextImpl; +import com.devonfw.tools.ide.log.IdeLogEntry; import com.devonfw.tools.ide.log.IdeLogLevel; /** @@ -103,7 +103,7 @@ protected Void call() { LOG.info("Commandlet {} completed successfully.", commandlet); } catch (Exception e) { LOG.error("Failed to run commandlet {}: {}", commandlet, e.getMessage(), e); - consoleController.appendOutput("[ERROR] Failed to launch " + commandlet + ": " + e.getMessage()); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "[ERROR] Failed to launch " + commandlet + ": " + e.getMessage())); } return null; } diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeTestGuiLogListener.java b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeTestGuiLogListener.java new file mode 100644 index 0000000000..279e02d62f --- /dev/null +++ b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeTestGuiLogListener.java @@ -0,0 +1,63 @@ +package com.devonfw.ide.gui.core.context; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import com.devonfw.ide.gui.core.mainwindow.console.ConsoleController; +import com.devonfw.tools.ide.log.IdeLogEntry; +import com.devonfw.tools.ide.log.IdeLogLevel; + +/** + * Tests for {@link IdeGuiLogListener}. + */ +public class IdeTestGuiLogListener { + + @TempDir + private Path mockIdeRoot; + + /** + * A {@link ConsoleController} that records every entry forwarded to it instead of touching the JavaFX toolkit, so the number of times + * {@link IdeGuiLogListener#onLog(IdeLogEntry)} sends a message can be counted. + */ + private static class RecordingConsoleController extends ConsoleController { + + private final List appended = new ArrayList<>(); + + RecordingConsoleController(GuiStateManager guiStateManager) { + super(guiStateManager); + } + + @Override + public void appendOutput(IdeLogEntry entry) { + + this.appended.add(entry); + } + + List getEntries() { + return this.appended; + } + } + + @Test + void testMessageOnlySendOnce() { + + GuiStateManager guiStateManager = new GuiStateManager(this.mockIdeRoot.toString()); + RecordingConsoleController console = new RecordingConsoleController(guiStateManager); + IdeGuiLogListener listener = new IdeGuiLogListener(console); + listener.flushAndEndBuffering(); + + String message = "TestMessage"; + IdeLogEntry entry = new IdeLogEntry(IdeLogLevel.INFO, message); + boolean accepted = listener.onLog(entry); + + assertThat(accepted).as("onLog() should accept the entry").isTrue(); + assertThat(console.getEntries()).as("the entry must be forwarded to the console exactly once").hasSize(1); + assertThat(console.getEntries().get(0).message()).isEqualTo(message); + } +} diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java index dd5bd1cb83..bbfce60e4a 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java @@ -7,7 +7,6 @@ import java.net.URL; import java.nio.file.Path; import java.util.List; - import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; @@ -20,6 +19,7 @@ import com.devonfw.ide.gui.HeadlessApplicationTest; import com.devonfw.ide.gui.core.context.GuiStateManager; import com.devonfw.ide.gui.core.service.NlsService; +import com.devonfw.tools.ide.log.IdeLogEntry; import com.devonfw.tools.ide.log.IdeLogLevel; class ConsolePanelTest extends HeadlessApplicationTest { @@ -57,8 +57,8 @@ void testConsoleOutputPrintsCorrectly() { // Simulate output to the console Platform.runLater(() -> { - consoleController.appendOutput("Hello World!"); - consoleController.appendOutput("Test"); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Hello World")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Test")); }); waitForFxEvents(); @@ -75,10 +75,10 @@ void testConsoleOutputPrintsCorrectly() { void testConsoleOutputWithLogLevels() { Platform.runLater(() -> { - consoleController.appendOutput(IdeLogLevel.INFO, "Info message"); - consoleController.appendOutput(IdeLogLevel.ERROR, "Error message"); - consoleController.appendOutput(IdeLogLevel.WARNING, "Warning message"); - consoleController.appendOutput(IdeLogLevel.DEBUG, "Debug message"); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Info message")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.ERROR, "Error message")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.WARNING, "Warning message")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.DEBUG, "Debug message")); }); waitForFxEvents(); @@ -97,8 +97,8 @@ void testConsoleClear() { // Simulate output to the console Platform.runLater(() -> { - consoleController.appendOutput("Hello World!"); - consoleController.appendOutput("Test"); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Hello World")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Test")); }); waitForFxEvents(); @@ -113,9 +113,9 @@ void testConsoleClear() { @Test void testLineCountUpdates() { Platform.runLater(() -> { - consoleController.appendOutput("Line 1"); - consoleController.appendOutput("Line 2"); - consoleController.appendOutput("Line 3"); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Line 1")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Line 2")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Line 3")); }); waitForFxEvents(); From 39c98daa20d5166a728afbc3ec0b0ed36eb22099 Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Mon, 12 Oct 2026 11:25:33 +0200 Subject: [PATCH 04/26] Cleanup of IdeLogEntry, IdeLogListener and IdeGuiLogListener --- .../ide/gui/core/mainwindow/console/ConsolePanelTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java index bbfce60e4a..d722067939 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java @@ -57,7 +57,7 @@ void testConsoleOutputPrintsCorrectly() { // Simulate output to the console Platform.runLater(() -> { - consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Hello World")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Hello World!")); consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Test")); }); waitForFxEvents(); @@ -97,7 +97,7 @@ void testConsoleClear() { // Simulate output to the console Platform.runLater(() -> { - consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Hello World")); + consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Hello World!")); consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Test")); }); waitForFxEvents(); From 8e4459bbf22952e5614379c02ba01b6b216812a3 Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Mon, 12 Oct 2026 13:48:46 +0200 Subject: [PATCH 05/26] fixed format violations --- .../core/mainwindow/console/ConsoleController.java | 1 + .../ide/gui/core/service/CommandletService.java | 1 + .../core/mainwindow/console/ConsolePanelTest.java | 12 ++---------- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java b/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java index 38be09b888..088fe90833 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java @@ -4,6 +4,7 @@ import java.util.Deque; import java.util.List; import java.util.stream.Collectors; + import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java b/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java index c3c2c44a48..1d8e6fd75b 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java @@ -2,6 +2,7 @@ import java.nio.file.Path; import java.util.Objects; + import javafx.application.Platform; import javafx.concurrent.Task; import javafx.scene.control.Alert.AlertType; diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java index d722067939..a8757519e6 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java @@ -7,6 +7,7 @@ import java.net.URL; import java.nio.file.Path; import java.util.List; + import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; @@ -45,7 +46,7 @@ public void start(Stage stage) throws IOException { fxmlLoader.setControllerFactory(clazz -> clazz == ConsoleController.class ? this.consoleController : null); Parent root = fxmlLoader.load(); stage.setScene(new Scene(root)); - stage.requestFocus(); // sometimes needed for headless setup to work + stage.requestFocus(); stage.show(); } @@ -55,14 +56,12 @@ public void start(Stage stage) throws IOException { @Test void testConsoleOutputPrintsCorrectly() { - // Simulate output to the console Platform.runLater(() -> { consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Hello World!")); consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Test")); }); waitForFxEvents(); - // Verify that the output is displayed in the ListView List snapshot = consoleController.getConsoleOutputSnapshot(); assertThat(snapshot).anyMatch(s -> s.contains("Hello World!")); assertThat(snapshot).anyMatch(s -> s.contains("Test")); @@ -84,8 +83,6 @@ void testConsoleOutputWithLogLevels() { List snapshot = consoleController.getConsoleOutputSnapshot(); assertThat(snapshot).hasSize(4); - // Check that all log levels appear in the output (format is "HH:mm:ss | [LEVEL] message") - // INFO has 3 spaces after bracket, ERROR has 2, WARN has 2, DEBUG has 2 assertThat(snapshot).anyMatch(s -> s.contains("[INFO] Info message")); assertThat(snapshot).anyMatch(s -> s.contains("[ERROR] Error message")); assertThat(snapshot).anyMatch(s -> s.contains("[WARNING] Warning message")); @@ -95,18 +92,15 @@ void testConsoleOutputWithLogLevels() { @Test void testConsoleClear() { - // Simulate output to the console Platform.runLater(() -> { consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Hello World!")); consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Test")); }); waitForFxEvents(); - // Clear the console Platform.runLater(() -> consoleController.clearConsole()); waitForFxEvents(); - // Verify that the console is empty assertThat(consoleController.getConsoleOutputSnapshot()).isEmpty(); } @@ -119,13 +113,11 @@ void testLineCountUpdates() { }); waitForFxEvents(); - // Line count should be 3 assertThat(consoleController.getConsoleOutputSnapshot()).hasSize(3); } @Test void testAutoScrollCheckboxEnabledByDefault() { - // Just verify auto-scroll is enabled by default assertThat(consoleController.isAutoScrollEnabled()).isTrue(); } } From 5db05ea6f093808b7092e7fe19cb7c5c3ca631fb Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Mon, 12 Oct 2026 13:48:46 +0200 Subject: [PATCH 06/26] #2385 cleanup --- .../core/mainwindow/console/ConsoleController.java | 1 + .../ide/gui/core/service/CommandletService.java | 1 + .../core/mainwindow/console/ConsolePanelTest.java | 12 ++---------- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java b/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java index 38be09b888..088fe90833 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java @@ -4,6 +4,7 @@ import java.util.Deque; import java.util.List; import java.util.stream.Collectors; + import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java b/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java index c3c2c44a48..1d8e6fd75b 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java @@ -2,6 +2,7 @@ import java.nio.file.Path; import java.util.Objects; + import javafx.application.Platform; import javafx.concurrent.Task; import javafx.scene.control.Alert.AlertType; diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java index d722067939..a8757519e6 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java @@ -7,6 +7,7 @@ import java.net.URL; import java.nio.file.Path; import java.util.List; + import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; @@ -45,7 +46,7 @@ public void start(Stage stage) throws IOException { fxmlLoader.setControllerFactory(clazz -> clazz == ConsoleController.class ? this.consoleController : null); Parent root = fxmlLoader.load(); stage.setScene(new Scene(root)); - stage.requestFocus(); // sometimes needed for headless setup to work + stage.requestFocus(); stage.show(); } @@ -55,14 +56,12 @@ public void start(Stage stage) throws IOException { @Test void testConsoleOutputPrintsCorrectly() { - // Simulate output to the console Platform.runLater(() -> { consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Hello World!")); consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Test")); }); waitForFxEvents(); - // Verify that the output is displayed in the ListView List snapshot = consoleController.getConsoleOutputSnapshot(); assertThat(snapshot).anyMatch(s -> s.contains("Hello World!")); assertThat(snapshot).anyMatch(s -> s.contains("Test")); @@ -84,8 +83,6 @@ void testConsoleOutputWithLogLevels() { List snapshot = consoleController.getConsoleOutputSnapshot(); assertThat(snapshot).hasSize(4); - // Check that all log levels appear in the output (format is "HH:mm:ss | [LEVEL] message") - // INFO has 3 spaces after bracket, ERROR has 2, WARN has 2, DEBUG has 2 assertThat(snapshot).anyMatch(s -> s.contains("[INFO] Info message")); assertThat(snapshot).anyMatch(s -> s.contains("[ERROR] Error message")); assertThat(snapshot).anyMatch(s -> s.contains("[WARNING] Warning message")); @@ -95,18 +92,15 @@ void testConsoleOutputWithLogLevels() { @Test void testConsoleClear() { - // Simulate output to the console Platform.runLater(() -> { consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Hello World!")); consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "Test")); }); waitForFxEvents(); - // Clear the console Platform.runLater(() -> consoleController.clearConsole()); waitForFxEvents(); - // Verify that the console is empty assertThat(consoleController.getConsoleOutputSnapshot()).isEmpty(); } @@ -119,13 +113,11 @@ void testLineCountUpdates() { }); waitForFxEvents(); - // Line count should be 3 assertThat(consoleController.getConsoleOutputSnapshot()).hasSize(3); } @Test void testAutoScrollCheckboxEnabledByDefault() { - // Just verify auto-scroll is enabled by default assertThat(consoleController.isAutoScrollEnabled()).isTrue(); } } From bf5c6fe7ff3b09752e0e6dd0ac765ea11adba830 Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Mon, 12 Oct 2026 14:26:27 +0200 Subject: [PATCH 07/26] fixed violations ConsoleController.java --- .../ide/gui/core/mainwindow/console/ConsoleController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java b/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java index 38be09b888..088fe90833 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/mainwindow/console/ConsoleController.java @@ -4,6 +4,7 @@ import java.util.Deque; import java.util.List; import java.util.stream.Collectors; + import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; From a1a1c78fdca2432375883f089817a3a2afbcfb65 Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Mon, 12 Oct 2026 14:27:50 +0200 Subject: [PATCH 08/26] Fixed violations --- .../ide/gui/core/mainwindow/console/ConsolePanelTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java index 623ba2f88b..a8757519e6 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java @@ -7,6 +7,7 @@ import java.net.URL; import java.nio.file.Path; import java.util.List; + import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; From 9904dd90cd1f371e9fb44deed06fbaa09fd31321 Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Mon, 12 Oct 2026 14:28:58 +0200 Subject: [PATCH 09/26] fixed violations --- .../java/com/devonfw/ide/gui/core/service/CommandletService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java b/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java index c3c2c44a48..1d8e6fd75b 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java @@ -2,6 +2,7 @@ import java.nio.file.Path; import java.util.Objects; + import javafx.application.Platform; import javafx.concurrent.Task; import javafx.scene.control.Alert.AlertType; From 062db3296d07603c7c33d340c7bcba6873387b53 Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Tue, 13 Oct 2026 15:34:49 +0200 Subject: [PATCH 10/26] Update gui/src/test/java/com/devonfw/ide/gui/core/context/IdeTestGuiLogListener.java Co-authored-by: quando632 --- .../com/devonfw/ide/gui/core/context/IdeTestGuiLogListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeTestGuiLogListener.java b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeTestGuiLogListener.java index 279e02d62f..6614ed9812 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeTestGuiLogListener.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeTestGuiLogListener.java @@ -16,7 +16,7 @@ /** * Tests for {@link IdeGuiLogListener}. */ -public class IdeTestGuiLogListener { +public class IdeGuiLogListenerTest extends Assertions { @TempDir private Path mockIdeRoot; From 52c1ec7affeaf746b67f581cb8765e250a425e52 Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Tue, 13 Oct 2026 15:37:42 +0200 Subject: [PATCH 11/26] Update cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java Co-authored-by: quando632 --- cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java index d937d7fe24..97c45da99a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java @@ -11,7 +11,7 @@ public interface IdeLogListener { * @param message the actual message to be logged. * @param rawMessage the raw message logged (without args filled in) * @param args the optional message arguments. - * @param error the optional error. + * @param entry the {@link IdeLogEntry} to log. * @return {@code true} to accept this log event (default), {@code false} otherwise to reject further processing and suppress the log message. */ boolean onLog(IdeLogEntry entry); From f9469be03c67d616ad034141522a54245a1f719d Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Tue, 13 Oct 2026 15:39:06 +0200 Subject: [PATCH 12/26] Update gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java Co-authored-by: quando632 --- .../ide/gui/core/mainwindow/console/ConsolePanelTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java index a8757519e6..ad6430c9f1 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java @@ -46,7 +46,7 @@ public void start(Stage stage) throws IOException { fxmlLoader.setControllerFactory(clazz -> clazz == ConsoleController.class ? this.consoleController : null); Parent root = fxmlLoader.load(); stage.setScene(new Scene(root)); - stage.requestFocus(); + stage.requestFocus(); // sometimes needed for headless setup to work stage.show(); } From 889a446a3b4b6b862df505028c866733fa35379d Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Wed, 14 Oct 2026 08:21:17 +0200 Subject: [PATCH 13/26] improved Cleanup of IdeLogEntry, IdeLogListener and IdeGuiLogListener --- .../gui/core/service/CommandletService.java | 3 +-- ...stener.java => IdeGuiLogListenerTest.java} | 23 +++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) rename gui/src/test/java/com/devonfw/ide/gui/core/context/{IdeTestGuiLogListener.java => IdeGuiLogListenerTest.java} (68%) diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java b/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java index 1d8e6fd75b..37102f69cd 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java @@ -18,7 +18,6 @@ import com.devonfw.ide.gui.core.modal.IdeDialog; import com.devonfw.ide.gui.core.progress.ProgressBarTask; import com.devonfw.tools.ide.context.IdeStartContextImpl; -import com.devonfw.tools.ide.log.IdeLogEntry; import com.devonfw.tools.ide.log.IdeLogLevel; /** @@ -103,8 +102,8 @@ protected Void call() { LOG.info("Commandlet {} completed successfully.", commandlet); } catch (Exception e) { + guiLogListener.flushAndEndBuffering(); LOG.error("Failed to run commandlet {}: {}", commandlet, e.getMessage(), e); - consoleController.appendOutput(new IdeLogEntry(IdeLogLevel.INFO, "[ERROR] Failed to launch " + commandlet + ": " + e.getMessage())); } return null; } diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeTestGuiLogListener.java b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java similarity index 68% rename from gui/src/test/java/com/devonfw/ide/gui/core/context/IdeTestGuiLogListener.java rename to gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java index 6614ed9812..d4ba84cd67 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeTestGuiLogListener.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java @@ -1,15 +1,17 @@ package com.devonfw.ide.gui.core.context; -import static org.assertj.core.api.Assertions.assertThat; - import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.devonfw.ide.gui.core.mainwindow.console.ConsoleController; +import com.devonfw.tools.ide.context.IdeStartContextImpl; import com.devonfw.tools.ide.log.IdeLogEntry; import com.devonfw.tools.ide.log.IdeLogLevel; @@ -60,4 +62,21 @@ void testMessageOnlySendOnce() { assertThat(console.getEntries()).as("the entry must be forwarded to the console exactly once").hasSize(1); assertThat(console.getEntries().get(0).message()).isEqualTo(message); } + + + @Test + void testBufferedMessageShownOnceAfterFlush() { + + Logger log = LoggerFactory.getLogger(IdeGuiLogListenerTest.class); + GuiStateManager guiStateManager = new GuiStateManager(this.mockIdeRoot.toString()); + IdeGuiLogListenerTest.RecordingConsoleController console = new RecordingConsoleController(guiStateManager); + IdeGuiLogListener listener = new IdeGuiLogListener(console); + new IdeStartContextImpl(IdeLogLevel.INFO, listener); + + log.warn("TestMessage"); + assertThat(console.getEntries()).as("nothing is shown while buffering").isEmpty(); + + listener.flushAndEndBuffering(); + assertThat(console.getEntries()).extracting(IdeLogEntry::message).containsExactly("TestMessage"); + } } From 1b84d177ae33f4522a9c339db71d60c48892e19f Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Wed, 14 Oct 2026 11:56:43 +0200 Subject: [PATCH 14/26] Update gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java Co-authored-by: quando632 --- .../ide/gui/core/mainwindow/console/ConsolePanelTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java index a8757519e6..ad6430c9f1 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java @@ -46,7 +46,7 @@ public void start(Stage stage) throws IOException { fxmlLoader.setControllerFactory(clazz -> clazz == ConsoleController.class ? this.consoleController : null); Parent root = fxmlLoader.load(); stage.setScene(new Scene(root)); - stage.requestFocus(); + stage.requestFocus(); // sometimes needed for headless setup to work stage.show(); } From 762abe8b2ad3bdaa744d09c0616ce0cbd8c1080c Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Wed, 14 Oct 2026 11:57:22 +0200 Subject: [PATCH 15/26] Update gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java Co-authored-by: quando632 --- .../com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java index d4ba84cd67..030798c98c 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java @@ -69,7 +69,7 @@ void testBufferedMessageShownOnceAfterFlush() { Logger log = LoggerFactory.getLogger(IdeGuiLogListenerTest.class); GuiStateManager guiStateManager = new GuiStateManager(this.mockIdeRoot.toString()); - IdeGuiLogListenerTest.RecordingConsoleController console = new RecordingConsoleController(guiStateManager); + RecordingConsoleController console = new RecordingConsoleController(guiStateManager); IdeGuiLogListener listener = new IdeGuiLogListener(console); new IdeStartContextImpl(IdeLogLevel.INFO, listener); From b9c974d9ee973ce50fff0ad3171f1ae94a582fe5 Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Wed, 14 Oct 2026 13:16:06 +0200 Subject: [PATCH 16/26] Javadoc changes to IdeLogListener --- .../main/java/com/devonfw/tools/ide/log/IdeLogListener.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java index 97c45da99a..dec84f9542 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogListener.java @@ -7,10 +7,6 @@ public interface IdeLogListener { /** - * @param level the {@link IdeLogLevel}. - * @param message the actual message to be logged. - * @param rawMessage the raw message logged (without args filled in) - * @param args the optional message arguments. * @param entry the {@link IdeLogEntry} to log. * @return {@code true} to accept this log event (default), {@code false} otherwise to reject further processing and suppress the log message. */ From cf813c3adda7d5a631baeee26d35084d932294f2 Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Thu, 15 Oct 2026 08:26:55 +0200 Subject: [PATCH 17/26] Update gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java Co-authored-by: JoelAdbu --- .../ide/gui/core/context/IdeGuiLogListenerTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java index 030798c98c..22c35eb70c 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java @@ -45,7 +45,16 @@ List getEntries() { return this.appended; } } +/** + * {@link IdeStartContextImpl} holds its state in a process-wide static field that is (re)assigned by every construction and offers no reset hook. This test + * installs a buffering {@link IdeGuiLogListener} into that static; without cleanup it would leak into every other gui test running in the same surefire JVM, + * routing their SLF4J log events into the leftover buffering listener. Restore a neutral, non-buffering no-op context afterwards. + */ + @AfterEach + void resetStartContext() { + new IdeStartContextImpl(IdeLogLevel.INFO, IdeLogListenerNone.INSTANCE); + } @Test void testMessageOnlySendOnce() { From 12d537058c8cf588da72120958dcb34e2b27ada5 Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Thu, 15 Oct 2026 08:35:56 +0200 Subject: [PATCH 18/26] added (... && error == null) --- cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java index 8cb4266d6a..3ac0e72bd8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java @@ -34,7 +34,7 @@ public record IdeLogEntry(IdeLogLevel level, String message, String rawMessage, public IdeLogEntry { // validation to be used by all constructors... - if ((message == null) && (rawMessage == null)) { + if ((message == null) && (rawMessage == null) && (error == null)) { throw new IllegalStateException(); } } From 1545c66a1d25c8e08bd350f7410828dbbeaaa696 Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Thu, 15 Oct 2026 08:42:04 +0200 Subject: [PATCH 19/26] fixed IdeGuiLogListenerTest --- .../devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java index 22c35eb70c..21868cd5d1 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java @@ -5,6 +5,7 @@ import java.util.List; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.slf4j.Logger; @@ -14,6 +15,7 @@ import com.devonfw.tools.ide.context.IdeStartContextImpl; import com.devonfw.tools.ide.log.IdeLogEntry; import com.devonfw.tools.ide.log.IdeLogLevel; +import com.devonfw.tools.ide.log.IdeLogListenerNone; /** * Tests for {@link IdeGuiLogListener}. @@ -45,7 +47,8 @@ List getEntries() { return this.appended; } } -/** + + /** * {@link IdeStartContextImpl} holds its state in a process-wide static field that is (re)assigned by every construction and offers no reset hook. This test * installs a buffering {@link IdeGuiLogListener} into that static; without cleanup it would leak into every other gui test running in the same surefire JVM, * routing their SLF4J log events into the leftover buffering listener. Restore a neutral, non-buffering no-op context afterwards. @@ -55,6 +58,7 @@ void resetStartContext() { new IdeStartContextImpl(IdeLogLevel.INFO, IdeLogListenerNone.INSTANCE); } + @Test void testMessageOnlySendOnce() { From a517cb84b4cc92bc57e16af97171c07773a4f5d2 Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Tue, 20 Oct 2026 07:43:09 +0200 Subject: [PATCH 20/26] Update gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java Co-authored-by: quando632 --- .../java/com/devonfw/ide/gui/core/service/CommandletService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java b/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java index 3a5964e02e..96fba40a69 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java @@ -107,7 +107,6 @@ protected Void call() { guiLogListener.flushAndEndBuffering(); LOG.error("Failed to run commandlet {}: {}", commandlet, e.getMessage(), e); - eventBus.sendEvent(new LogEvent(new IdeLogEntry(IdeLogLevel.ERROR, "[ERROR] Failed to launch " + commandlet + ": " + e.getMessage()))); } return null; } From ddc1c75cf48001b8b0a387d803e6483958ddf4e5 Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Tue, 20 Oct 2026 08:11:54 +0200 Subject: [PATCH 21/26] Fixed format violations --- cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java | 4 ---- .../com/devonfw/ide/gui/core/service/CommandletService.java | 2 -- 2 files changed, 6 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java index 3ac0e72bd8..64c8c59d3c 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java @@ -33,10 +33,6 @@ public record IdeLogEntry(IdeLogLevel level, String message, String rawMessage, */ public IdeLogEntry { - // validation to be used by all constructors... - if ((message == null) && (rawMessage == null) && (error == null)) { - throw new IllegalStateException(); - } } /** diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java b/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java index 96fba40a69..8b6ecad93c 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/service/CommandletService.java @@ -15,11 +15,9 @@ import com.devonfw.ide.gui.core.context.IdeGuiContext; import com.devonfw.ide.gui.core.context.IdeGuiLogListener; import com.devonfw.ide.gui.core.event.GuiEventBus; -import com.devonfw.ide.gui.core.event.console.LogEvent; import com.devonfw.ide.gui.core.modal.IdeDialog; import com.devonfw.ide.gui.core.progress.ProgressBarTask; import com.devonfw.tools.ide.context.IdeStartContextImpl; -import com.devonfw.tools.ide.log.IdeLogEntry; import com.devonfw.tools.ide.log.IdeLogLevel; /** From 5423db4f1fcc99dc20b84f6a60cbaa0717a26460 Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Tue, 20 Oct 2026 14:26:50 +0200 Subject: [PATCH 22/26] Added tests in IdeGuiLogListenerTest, changed IdeGuiLogListener and IdeLogEntry --- .../devonfw/tools/ide/log/IdeLogEntry.java | 14 --------- .../gui/core/context/IdeGuiLogListener.java | 8 ++--- .../core/context/IdeGuiLogListenerTest.java | 31 ++++++++++++++++--- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java index 64c8c59d3c..9af729c370 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java @@ -21,20 +21,6 @@ public record IdeLogEntry(IdeLogLevel level, String message, String rawMessage, /** The {@link DateTimeFormatter} used by {@link #displayValue()}. */ private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.SSS").withZone(ZoneId.systemDefault()); - /** - * The constructor. - * - * @param level - {@link #level()}. - * @param message - {@link #message()}. - * @param rawMessage - {@link #rawMessage()}. - * @param args - {@link #args()}. - * @param error - {@link #error()}. - * @param contains - {@link #contains()}. - */ - public IdeLogEntry { - - } - /** * @param level the {@link IdeLogLevel}. * @param message the {@link IdeSubLogger#log(String) logged message}. diff --git a/gui/src/main/java/com/devonfw/ide/gui/core/context/IdeGuiLogListener.java b/gui/src/main/java/com/devonfw/ide/gui/core/context/IdeGuiLogListener.java index 89952d9928..e250d7c0d2 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/core/context/IdeGuiLogListener.java +++ b/gui/src/main/java/com/devonfw/ide/gui/core/context/IdeGuiLogListener.java @@ -6,7 +6,6 @@ import com.devonfw.ide.gui.core.event.GuiEventBus; import com.devonfw.ide.gui.core.event.console.LogEvent; import com.devonfw.tools.ide.log.IdeLogEntry; -import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.log.IdeLogListenerBuffer; /// Listener class that listens to internal ideasy output, e.g. output from commandlets that are run. @@ -25,11 +24,8 @@ public IdeGuiLogListener(GuiEventBus eventBus) { public boolean onLog(IdeLogEntry entry) { boolean accept = super.onLog(entry); - if (accept && this.eventBus != null && entry.message() != null) { - this.eventBus.sendEvent(new LogEvent(new IdeLogEntry(entry.level(), entry.message()))); - if (entry.error() != null) { - this.eventBus.sendEvent(new LogEvent(new IdeLogEntry(IdeLogLevel.ERROR, " Error: " + entry.error().getMessage()))); - } + if (accept) { + this.eventBus.sendEvent(new LogEvent(entry)); } return accept; } diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java index 094bb01226..6357d5e42f 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java @@ -1,13 +1,11 @@ package com.devonfw.ide.gui.core.context; -import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,9 +23,6 @@ */ public class IdeGuiLogListenerTest extends Assertions { - @TempDir - private Path mockIdeRoot; - /** * An {@link EventListener} for {@link LogEvent} that records every event published on the {@link GuiEventBus} instead of touching the JavaFX toolkit, so the * number of times {@link IdeGuiLogListener#onLog(IdeLogEntry)} sends a message can be counted. @@ -93,4 +88,30 @@ void testBufferedMessageShownOnceAfterFlush() { listener.flushAndEndBuffering(); assertThat(recorder.getEntries()).extracting(IdeLogEntry::message).containsExactly("TestMessage"); } + + @Test + void testNullMessage() { + + Logger log = LoggerFactory.getLogger(IdeGuiLogListenerTest.class); + log.warn((String) null); + + } + + @Test + void testEntryWithErrorAndNullMessageIsForwardedUnchanged() { + + GuiEventBus eventBus = new GuiEventBus(); + RecordingLogListener recorder = new RecordingLogListener(); + eventBus.addListener(LogEvent.class, recorder); + IdeGuiLogListener listener = new IdeGuiLogListener(eventBus); + listener.flushAndEndBuffering(); + + RuntimeException error = new NullPointerException(); + IdeLogEntry entry = new IdeLogEntry(IdeLogLevel.ERROR, null, null, null, error); + listener.onLog(entry); + + assertThat(recorder.getEntries()).first().isSameAs(entry); + assertThat(recorder.getEntries().get(0).error()).isSameAs(error); + } + } From d5f78d5761763402fc839161a83583f3a7a39770 Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Tue, 20 Oct 2026 15:17:01 +0200 Subject: [PATCH 23/26] Changed ConsolePanelTest --- .../ide/gui/core/mainwindow/console/ConsolePanelTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java index d460d308e8..7d61c741ec 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java @@ -167,7 +167,7 @@ void testLogEventArrivingOverTheEventBusIsShown() { List snapshot = consoleViewModel.getConsoleOutputSnapshot(); assertThat(snapshot).anyMatch(s -> s.contains("[INFO] Info via EventBus")); assertThat(snapshot).anyMatch(s -> s.contains("[ERROR] Error via EventBus")); - assertThat(snapshot).anyMatch(s -> s.contains("[ERROR] Error: boom")); + } /** From f73abe6f504b2e768dbbf99abfbd61ba6824b3ba Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Wed, 21 Oct 2026 08:35:04 +0200 Subject: [PATCH 24/26] Small touch-ups --- .../com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java | 2 +- .../ide/gui/core/mainwindow/console/ConsolePanelTest.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java index 6357d5e42f..2505cadac9 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/context/IdeGuiLogListenerTest.java @@ -93,7 +93,7 @@ void testBufferedMessageShownOnceAfterFlush() { void testNullMessage() { Logger log = LoggerFactory.getLogger(IdeGuiLogListenerTest.class); - log.warn((String) null); + assertThatCode(() -> log.warn((String) null)).doesNotThrowAnyException(); } diff --git a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java index 7d61c741ec..43b99baa3c 100644 --- a/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java +++ b/gui/src/test/java/com/devonfw/ide/gui/core/mainwindow/console/ConsolePanelTest.java @@ -167,6 +167,7 @@ void testLogEventArrivingOverTheEventBusIsShown() { List snapshot = consoleViewModel.getConsoleOutputSnapshot(); assertThat(snapshot).anyMatch(s -> s.contains("[INFO] Info via EventBus")); assertThat(snapshot).anyMatch(s -> s.contains("[ERROR] Error via EventBus")); + assertThat(snapshot).as("the error is kept on the entry, not printed as an extra line").hasSize(2); } From e9d6050728efd9b871f7672b43c96eb5d11c421b Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Tue, 27 Oct 2026 11:03:48 +0100 Subject: [PATCH 25/26] Added validation for all constructors of IdeLogEntry --- CHANGELOG.adoc | 4 ++-- .../java/com/devonfw/tools/ide/log/IdeLogEntry.java | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 5ff0f0623a..f9715d20bc 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,6 +6,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/2725[#2725]: Cleanup of IdeLogEntry, IdeLogListener and IdeGuiLogListener * https://github.com/devonfw/IDEasy/issues/2365[#2365]: Reduce plugin reinstallations * https://github.com/devonfw/IDEasy/issues/2701[#2701]: Stop exporting UV_PROJECT_ENVIRONMENT which broke uv projects * https://github.com/devonfw/IDEasy/issues/1965[#1965]: Improve dependent installation, fixed npm and pip-based installation duplication @@ -47,8 +48,7 @@ The full list of changes for this release can be found in https://github.com/dev == 2026.09.001 -* https://github.com/devonfw/IDEasy/issues/2293[#2293]: Rework spyder to have same features as IdeToolCommandlets -Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/2293[#2293]: Rework spyder to have same features as IdeToolCommandlets Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/1525[#1525]: Document known issue and workaround for lombok plugin in Eclipse * https://github.com/devonfw/IDEasy/issues/1031[#1031]: Added OpenRewrite commandlet diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java index 9af729c370..5f4c541ca5 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java @@ -21,6 +21,17 @@ public record IdeLogEntry(IdeLogLevel level, String message, String rawMessage, /** The {@link DateTimeFormatter} used by {@link #displayValue()}. */ private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.SSS").withZone(ZoneId.systemDefault()); + public IdeLogEntry { + // validation to be used by all constructors... + if (level == null) { + throw new IllegalStateException(); + } + + if ((message == null) && (rawMessage == null)) { + throw new IllegalStateException(); + } + } + /** * @param level the {@link IdeLogLevel}. * @param message the {@link IdeSubLogger#log(String) logged message}. From fcd921c3678beb2767cc3436ce64ee134cb347a2 Mon Sep 17 00:00:00 2001 From: NikoFoutsitzis Date: Tue, 27 Oct 2026 16:00:11 +0100 Subject: [PATCH 26/26] Validate IdeLogEntry and prevent invalid log entries in Slf4jLoggerAdapter --- cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java | 4 ++-- .../java/com/devonfw/tools/ide/log/Slf4jLoggerAdapter.java | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java index 5f4c541ca5..25e5ba07df 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/IdeLogEntry.java @@ -24,10 +24,10 @@ public record IdeLogEntry(IdeLogLevel level, String message, String rawMessage, public IdeLogEntry { // validation to be used by all constructors... if (level == null) { - throw new IllegalStateException(); + throw new NullPointerException(); } - if ((message == null) && (rawMessage == null)) { + if ((message == null) && (rawMessage == null) && (error == null)) { throw new IllegalStateException(); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/log/Slf4jLoggerAdapter.java b/cli/src/main/java/com/devonfw/tools/ide/log/Slf4jLoggerAdapter.java index c1b0420308..d7cee8da4d 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/log/Slf4jLoggerAdapter.java +++ b/cli/src/main/java/com/devonfw/tools/ide/log/Slf4jLoggerAdapter.java @@ -133,7 +133,10 @@ protected void handleNormalizedLoggingCall(Level level, Marker marker, String me argFormatter = startContext.getArgFormatter(); } String composedMessage = compose(argFormatter, message, args); - boolean accept = listener.onLog(new IdeLogEntry(ideLevel, composedMessage, message, args, error)); + boolean accept = true; + if ((composedMessage != null) || (message != null) || (error != null)) { + accept = listener.onLog(new IdeLogEntry(ideLevel, composedMessage, message, args, error)); + } if (accept) { java.util.logging.Level julLevel = ideLevel.getJulLevel(); this.julLogger.log(julLevel, composedMessage, error);