diff --git a/megamek/src/megamek/MegaMek.java b/megamek/src/megamek/MegaMek.java index ae066539dde..ba777df9875 100644 --- a/megamek/src/megamek/MegaMek.java +++ b/megamek/src/megamek/MegaMek.java @@ -92,7 +92,7 @@ public static void main(String... args) { final String name = t.getClass().getName(); final String message = String.format(MMLoggingConstants.UNHANDLED_EXCEPTION, name); final String title = String.format(MMLoggingConstants.UNHANDLED_EXCEPTION_TITLE, name); - logger.error(t, message, title); + logger.errorDialog(t, message, title); }); // Second, let's handle logging diff --git a/megamek/src/megamek/Version.java b/megamek/src/megamek/Version.java index b065b72649f..95ea6483ef6 100644 --- a/megamek/src/megamek/Version.java +++ b/megamek/src/megamek/Version.java @@ -240,7 +240,7 @@ public void fillFromText(final @Nullable String text) { final String nullOrBlank = ((text == null) ? "a null string" : "a blank string"); final String message = String.format(MMLoggingConstants.VERSION_ERROR_CANNOT_PARSE_VERSION_FROM_STRING, nullOrBlank); - logger.fatal(message, MMLoggingConstants.VERSION_PARSE_FAILURE); + logger.fatalDialog(message, MMLoggingConstants.VERSION_PARSE_FAILURE); return; } @@ -249,7 +249,7 @@ public void fillFromText(final @Nullable String text) { if ((snapshotSplit.length > 2) || (versionSplit.length < 3)) { final String message = String.format(MMLoggingConstants.VERSION_ILLEGAL_VERSION_FORMAT, text); - logger.fatal(message, MMLoggingConstants.VERSION_PARSE_FAILURE); + logger.fatalDialog(message, MMLoggingConstants.VERSION_PARSE_FAILURE); return; } @@ -257,7 +257,7 @@ public void fillFromText(final @Nullable String text) { setRelease(Integer.parseInt(versionSplit[0])); } catch (Exception e) { final String message = String.format(MMLoggingConstants.VERSION_FAILED_TO_PARSE_RELEASE, text); - logger.fatal(e, message, MMLoggingConstants.VERSION_PARSE_FAILURE); + logger.fatalDialog(e, message, MMLoggingConstants.VERSION_PARSE_FAILURE); return; } @@ -265,7 +265,7 @@ public void fillFromText(final @Nullable String text) { setMajor(Integer.parseInt(versionSplit[1])); } catch (Exception e) { final String message = String.format(MMLoggingConstants.VERSION_FAILED_TO_PARSE_MAJOR, text); - logger.fatal(e, message, MMLoggingConstants.VERSION_PARSE_FAILURE); + logger.fatalDialog(e, message, MMLoggingConstants.VERSION_PARSE_FAILURE); return; } @@ -273,7 +273,7 @@ public void fillFromText(final @Nullable String text) { setMinor(Integer.parseInt(versionSplit[2])); } catch (Exception e) { final String message = String.format(MMLoggingConstants.VERSION_FAILED_TO_PARSE_MINOR, text); - logger.fatal(e, message, MMLoggingConstants.VERSION_PARSE_FAILURE); + logger.fatalDialog(e, message, MMLoggingConstants.VERSION_PARSE_FAILURE); return; } diff --git a/megamek/src/megamek/client/ui/swing/ClientGUI.java b/megamek/src/megamek/client/ui/swing/ClientGUI.java index 5f044fdf4b2..5cf420c2f46 100644 --- a/megamek/src/megamek/client/ui/swing/ClientGUI.java +++ b/megamek/src/megamek/client/ui/swing/ClientGUI.java @@ -2250,7 +2250,7 @@ public void printList(ArrayList unitList, JButton button) { // If something goes wrong, probably ProcessBuild.start if anything, // Make sure to set the button text back to what it started as no matter what. button.setText(Messages.getString("ChatLounge.butPrintList")); - logger.error(e, "Operation failed", "Error printing unit list"); + logger.errorDialog(e, "Operation failed", "Error printing unit list"); } } diff --git a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java index 0ec1af35731..1803c71b438 100644 --- a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java +++ b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java @@ -442,9 +442,9 @@ public boolean startServer(@Nullable String serverPassword, int port, boolean is mailProperties.load(propsReader); mailer = new EmailService(mailProperties); } catch (Exception ex) { - logger.error(ex, - Messages.getFormattedString("MegaMek.StartServerError", port, ex.getMessage()), - Messages.getString("MegaMek.LoadGameAlert.title")); + logger.errorDialog(ex, + Messages.getFormattedString("MegaMek.StartServerError", port, ex.getMessage()), + Messages.getString("MegaMek.LoadGameAlert.title")); return false; } } @@ -457,14 +457,14 @@ public boolean startServer(@Nullable String serverPassword, int port, boolean is gameManager = getGameManager(gameType); server = new Server(serverPassword, port, gameManager, isRegister, metaServer, mailer, false); } catch (Exception ex) { - logger.error(ex, - Messages.getFormattedString("MegaMek.StartServerError", port, ex.getMessage()), - Messages.getString("MegaMek.LoadGameAlert.title")); + logger.errorDialog(ex, + Messages.getFormattedString("MegaMek.StartServerError", port, ex.getMessage()), + Messages.getString("MegaMek.LoadGameAlert.title")); return false; } if (saveGameFile != null && !server.loadGame(saveGameFile)) { - logger.error(Messages.getFormattedString("MegaMek.LoadGameAlert.message", saveGameFile.getAbsolutePath()), + logger.errorDialog(Messages.getFormattedString("MegaMek.LoadGameAlert.message", saveGameFile.getAbsolutePath()), Messages.getString("MegaMek.LoadGameAlert.title")); server.die(); server = null; @@ -520,7 +520,7 @@ public void startClient(String playerName, String serverAddress, int port, GameT serverAddress = Server.validateServerAddress(serverAddress); port = Server.validatePort(port); } catch (Exception ex) { - logger.error(ex, + logger.errorDialog(ex, Messages.getFormattedString("MegaMek.ServerConnectionError", serverAddress, port), Messages.getString("MegaMek.LoadGameAlert.title")); return; @@ -617,7 +617,7 @@ public String getDescription() { } } } catch (Exception ex) { - logger.error(ex, + logger.errorDialog(ex, Messages.getFormattedString("MegaMek.LoadGameAlert.message", fc.getSelectedFile().getAbsolutePath()), Messages.getString("MegaMek.LoadGameAlert.title")); @@ -764,15 +764,15 @@ void scenario() { scenario = sl.load(); game = scenario.createGame(); } catch (Exception e) { - logger.error(e, Messages.getString("MegaMek.HostScenarioAlert.message", e.getMessage()), + logger.errorDialog(e, Messages.getString("MegaMek.HostScenarioAlert.message", e.getMessage()), Messages.getString("MegaMek.HostScenarioAlert.title")); return; } // popup options dialog if (!scenario.hasFixedGameOptions() && game instanceof Game twGame) { - GameOptionsDialog god = new GameOptionsDialog(frame, (GameOptions) twGame.getOptions(), false); - god.update((GameOptions) twGame.getOptions()); + GameOptionsDialog god = new GameOptionsDialog(frame, twGame.getOptions(), false); + god.update(twGame.getOptions()); god.setEditable(true); god.setVisible(true); for (IBasicOption opt : god.getOptions()) { diff --git a/megamek/src/megamek/logging/MMLogger.java b/megamek/src/megamek/logging/MMLogger.java index c9cc3f26d59..a1ee98b00ea 100644 --- a/megamek/src/megamek/logging/MMLogger.java +++ b/megamek/src/megamek/logging/MMLogger.java @@ -19,33 +19,37 @@ package megamek.logging; -import javax.swing.JOptionPane; - +import io.sentry.Sentry; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.spi.AbstractLogger; import org.apache.logging.log4j.spi.ExtendedLoggerWrapper; -import io.sentry.Sentry; +import javax.swing.*; +import java.util.Collections; /** - * MMLogger - * - * Utility class to handle logging functions as well as reporting exceptions to + *

Utility class to handle logging functions as well as reporting exceptions to * Sentry. To deal with general recommendations of confirming log level before * logging, additional checks are added to ensure we're only ever logging data - * based upon the currently active log level. - * - * To utilize this class properly, it must be initialized within each class that - * will use it. For example for use with the megamek.MegaMek class, - * - * private static final MMLogger logger = MMLogger.create(MegaMek.class); - * - * And then for use, use logger.info(message) and pass a string to it. Currently - * supported levels include info, warn, debug, error, and fatal. Error and Fatal + * based upon the currently active log level.

+ *

How to use:

+ *

To utilize this class properly, it must be initialized within each class that + * will use it. For example for use with the {@code megamek.MegaMek.class}.

+ *
{@code private static final MMLogger logger = MMLogger.create(MegaMek.class);}
+ *

And then for use, use {@code logger.info(message)} and pass a string to it. Currently + * supported levels include trace, info, warn, debug, error, and fatal. Warn, Error and Fatal * can take any Throwable for sending to Sentry and has an overload for a title - * to allow for displaying of a dialog box + * to allow for displaying of a dialog box.

+ *

This class also implements both the parametric pattern and the string format + * for the functions that are overriden and added here. This means that you can + * use the following formats for the messages in some cases:

+ *
{@code logger.info("number: {} string: {}", 42, "Hello");
+ * logger.info("number: %d string: %s", 42, "Hello");}
+ *

Due to how the logger works, the first format using curly braces is preferred, but the second one + * is grandfathered exclusively for logs already existing, and it is not recommended for any new log.

*/ public class MMLogger extends ExtendedLoggerWrapper { private final ExtendedLoggerWrapper exLoggerWrapper; @@ -53,14 +57,14 @@ public class MMLogger extends ExtendedLoggerWrapper { private static final String FQCN = MMLogger.class.getName(); /** - * Private constructor as there should never be an instance of this - * class. + * Package protected constructor, this class should not be created directly. */ - private MMLogger(final Logger logger) { + MMLogger(final Logger logger) { super((AbstractLogger) logger, logger.getName(), logger.getMessageFactory()); this.exLoggerWrapper = this; } + /** * Returns a custom Logger with the name of the calling class. * @@ -88,77 +92,69 @@ public static MMLogger create(final Class loggerName) { * Info Level Logging. * * @param message Message to be sent to the log file. - * @param args Variable list of arguments for message to be passed to - * String.format() + * @param args Variable list of arguments for the message */ @Override public void info(String message, Object... args) { - message = String.format(message, args); + message = parametrizedStringIfEnabled(Level.INFO, message, args); exLoggerWrapper.logIfEnabled(MMLogger.FQCN, Level.INFO, null, message); } /** * Warning Level Logging * - * @param message Message to be written to the log file. - * @param args Variable list of arguments for message to be - * passed to String.format() - * + * @param message Message to be logged. + * @param args Variable list of arguments for the message */ @Override public void warn(String message, Object... args) { - message = String.format(message, args); + message = parametrizedStringIfEnabled(Level.WARN, message, args); exLoggerWrapper.logIfEnabled(MMLogger.FQCN, Level.WARN, null, message); } /** * Warning Level Logging * - * @param exception Exception that was caught via a try/catch block. - * @param message Message to be written to the log file. - * @param args Variable list of arguments for message to be - * passed to String.format() - * + * @param exception Exception to be logged. + * @param message Message to be logged. + * @param args Variable list of arguments for the message */ public void warn(Throwable exception, String message, Object... args) { Sentry.captureException(exception); - message = String.format(message, args); + message = parametrizedStringIfEnabled(Level.WARN, message, args); exLoggerWrapper.logIfEnabled(MMLogger.FQCN, Level.WARN, null, message, exception); } /** * Debug Level Logging * - * @param message Message to be written to the log file. - * @param args Variable list of arguments for message to be passed to - * String.format() + * @param message Message to be logged. + * @param args Variable list of arguments for the message */ @Override public void debug(String message, Object... args) { - message = String.format(message, args); + message = parametrizedStringIfEnabled(Level.DEBUG, message, args); exLoggerWrapper.logIfEnabled(MMLogger.FQCN, Level.DEBUG, null, message); } /** * Debug Level Logging * - * @param exception Exception that was caught via a try/catch block. - * @param message Message to be written to the log file. - * @param args Variable list of arguments for message to be - * passed to String.format() - * + * @param exception Exception to be logged. + * @param message Message to be logged. + * @param args Variable list of arguments for the message */ public void debug(Throwable exception, String message, Object... args) { Sentry.captureException(exception); - message = String.format(message, args); + message = parametrizedStringIfEnabled(Level.DEBUG, message, args); exLoggerWrapper.logIfEnabled(MMLogger.FQCN, Level.DEBUG, null, message, exception); } /** * Error Level Logging w/ Exception * - * @param exception Exception that was caught via a try/catch block. - * @param message Additional message to report to the log file. + * @param exception Exception to be logged. + * @param message Additional message. */ public void error(Throwable exception, String message) { Sentry.captureException(exception); @@ -168,10 +164,22 @@ public void error(Throwable exception, String message) { /** * Error Level Logging w/ Exception * + * @param exception Exception to be logged. + * @param message Additional message to be logged. + * @param args Variable list of arguments for the message + */ + public void error(Throwable exception, String message, Object... args) { + message = parametrizedStringIfEnabled(Level.ERROR, message, args); + Sentry.captureException(exception); + exLoggerWrapper.logIfEnabled(MMLogger.FQCN, Level.ERROR, null, message, exception); + } + + /** + * Error Level Logging w/ Exception * This one was made to make it easier to replace the Log4J Calls * - * @param message Additional message to report to the log file. - * @param exception Exception that was caught via a try/catch block. + * @param message Message to be logged. + * @param exception Exception to be logged. */ public void error(String message, Throwable exception) { Sentry.captureException(exception); @@ -181,7 +189,7 @@ public void error(String message, Throwable exception) { /** * Error Level Logging w/o Exception. * - * @param message Message to be written to the log file. + * @param message Message to be logged. */ @Override public void error(String message) { @@ -190,15 +198,56 @@ public void error(String message) { /** * Error Level Logging w/ Exception w/ Dialog. - * - * @param exception Exception that was caught. + * @deprecated (since 21-feb-2025) Use {@link #errorDialog(Throwable, String, String, Object...)} instead. + * @param exception Exception to be logged. * @param message Message to write to the log file AND be displayed in the * error pane. * @param title Title of the error message box. */ + @Deprecated public void error(Throwable exception, String message, String title) { - error(exception, message); + errorDialog(exception, message, title, Collections.emptyList()); + } + + /** + * Formatted Error Level Logging w/ Exception w/ Dialog. + * @param exception Exception to be logged. + * @param message Message to write to the log file AND be displayed in the + * error pane. + * @param title Title of the error message box. + * @param args Variable list of arguments for the message. + */ + public void errorDialog(Throwable exception, String message, String title, Object... args) { + Sentry.captureException(exception); + message = parametrizedStringAnyway(message, args); + exLoggerWrapper.logIfEnabled(MMLogger.FQCN, Level.ERROR, null, message, exception); + popupErrorDialog(title, message); + } + + /** + * Formatted Error Level Logging w/o Exception w/ Dialog. + * + * @param message Message to write to the log file AND be displayed in the + * error pane. + * @param title Title of the error message box. + * @param args Variable list of arguments for the message + */ + public void errorDialog(String title, String message, Object... args) { + message = parametrizedStringAnyway(message, args); + exLoggerWrapper.logIfEnabled(MMLogger.FQCN, Level.ERROR, null, message); + popupErrorDialog(title, message, args); + } + /** + * Formatted Error Level Logging w/o Exception w/ Dialog. + * + * @param message Message to write to the log file AND be displayed in the + * error pane. + * @param title Title of the error message box. + * @param args Variable list of arguments for the message + */ + private void popupErrorDialog(String title, String message, Object... args) { + message = parametrizedStringAnyway(message, args); try { JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE); } catch (Exception ignored) { @@ -215,7 +264,6 @@ public void error(Throwable exception, String message, String title) { */ public void error(String message, String title) { error(message); - try { JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE); } catch (Exception ignored) { @@ -237,24 +285,46 @@ public void fatal(Throwable exception, String message) { /** * Fatal Level Logging w/ Exception w/ Dialog * + * @deprecated (since 21-feb-2025) Use {@link #fatalDialog(Throwable, String, String)} instead. * @param exception Exception that was triggered. Probably uncaught. * @param message Message to report to the log file. * @param title Title of the error message box. - * */ + @Deprecated public void fatal(Throwable exception, String message, String title) { + fatalDialog(exception, message, title); + } + + /** + * Fatal Level Logging w/ Exception w/ Dialog + * + * @param exception Exception that was triggered. Probably uncaught. + * @param message Message to report to the log file. + * @param title Title of the error message box. + */ + public void fatalDialog(Throwable exception, String message, String title) { fatal(exception, message); JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE); } + /** + * Fatal Level Logging w/o Exception w/ Dialog + * @deprecated (since 21-feb-2025) Use {@link #fatalDialog(String, String)} instead. + * @param message Message to report to the log file. + * @param title Title of the error message box. + */ + @Deprecated + public void fatal(String message, String title) { + fatalDialog(message, title); + } + /** * Fatal Level Logging w/o Exception w/ Dialog * * @param message Message to report to the log file. * @param title Title of the error message box. - * */ - public void fatal(String message, String title) { + public void fatalDialog(String message, String title) { fatal(message); JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE); } @@ -265,7 +335,7 @@ public void fatal(String message, String title) { * method. * * @param checkedLevel Passed in Level to compare to. - * @return + * @return True if the current log level is more specific than the passed in */ public boolean isLevelMoreSpecificThan(Level checkedLevel) { return exLoggerWrapper.getLevel().isMoreSpecificThan(checkedLevel); @@ -277,10 +347,28 @@ public boolean isLevelMoreSpecificThan(Level checkedLevel) { * method. * * @param checkedLevel Passed in Level to compare to. - * @return + * @return True if the current log level is less specific than the passed in */ public boolean isLevelLessSpecificThan(Level checkedLevel) { return exLoggerWrapper.getLevel().isLessSpecificThan(checkedLevel); } + private String parametrizedStringIfEnabled(Level level, String message, Object... args) { + if (isEnabled(level, null, message) && args.length > 0) { + message = parametrizedStringAnyway(message, args); + } + return message; + } + + private String parametrizedStringAnyway(String message, Object... args) { + if (args.length > 0) { + if (message.contains("{}")) { + message = new ParameterizedMessage(message, args).getFormattedMessage(); + } else { + message = String.format(message, args); + } + } + return message; + } + } diff --git a/megamek/unittests/megamek/logging/MMLoggerTest.java b/megamek/unittests/megamek/logging/MMLoggerTest.java new file mode 100644 index 00000000000..c0be9df8db6 --- /dev/null +++ b/megamek/unittests/megamek/logging/MMLoggerTest.java @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2025 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.logging; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.message.Message; +import org.apache.logging.log4j.spi.AbstractLogger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockitoAnnotations; + +import java.awt.*; +import java.awt.event.KeyEvent; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +public class MMLoggerTest { + private final static MMLogger logger = MMLogger.create(MMLoggerTest.class); + + private CustomLogger mockLogger; + private MMLogger testMMLogger; + private final static String LOGGER_NAME = "TestLogger"; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + mockLogger = spy(new CustomLogger(LOGGER_NAME)); + testMMLogger = new MMLogger(mockLogger); + } + + @Test + public void testDebugLogging() { + testMMLogger.debug("Debug message: {}", "test"); + verifyLog(Level.DEBUG, "Debug message: test"); + } + + @Test + public void testInfoLogging() { + testMMLogger.info("Info message: {}", "test"); + verifyLog(Level.INFO, "Info message: test"); + } + + @Test + public void testWarnLogging() { + testMMLogger.warn("Warn message: {}", "test"); + verifyLog(Level.WARN, "Warn message: test"); + } + + @Test + public void testWarnLoggingWithException() { + Exception e = new Exception("Test exception"); + testMMLogger.warn(e, "Warn message: {}", "test"); + verifyLog(Level.WARN, "Warn message: test", e); + } + + @Test + public void testWarnLoggingWithExceptionWithStringFormat() { + Exception e = new Exception("Test exception"); + testMMLogger.warn(e, "Warn message: %s", "test"); + verifyLog(Level.WARN, "Warn message: test", e); + } + + @Test + public void testDebugLoggingWithException() { + Exception e = new Exception("Test exception"); + testMMLogger.debug(e, "Debug message: %s", "test"); + verifyLog(Level.DEBUG, "Debug message: test", e); + } + + @Test + public void testErrorLogging() { + if (GraphicsEnvironment.isHeadless()) { + // Skip this test if running in headless mode + return; + } + automaticallyDismissDialog(); + testMMLogger.errorDialog("test", "Error message: {}", "test"); + verifyLog(Level.ERROR, "Error message: test"); + } + + @Test + public void testErrorLoggingWithStringFormat() { + if (GraphicsEnvironment.isHeadless()) { + // Skip this test if running in headless mode + return; + } + automaticallyDismissDialog(); + testMMLogger.errorDialog("test", "Error message: %s", "test"); + verifyLog(Level.ERROR, "Error message: test"); + } + + @Test + public void testDeprecatedErrorLoggingWithExceptionNoParams() { + if (GraphicsEnvironment.isHeadless()) { + // Skip this test if running in headless mode + return; + } + automaticallyDismissDialog(); + Exception e = new Exception("Test exception"); + testMMLogger.error(e, "Error message: noparameter accepted", "test"); + verifyLog(Level.ERROR, "Error message: noparameter accepted", e); + } + + @Test + public void testErrorLoggingWithException() { + if (GraphicsEnvironment.isHeadless()) { + // Skip this test if running in headless mode + return; + } + automaticallyDismissDialog(); + Exception e = new Exception("Test exception"); + testMMLogger.errorDialog(e, "Error message: {}", "test", "test"); + verifyLog(Level.ERROR, "Error message: test", e); + } + + @Test + public void testFatalLogging() { + testMMLogger.fatal("Fatal without dialog without exception"); + verifyLog(Level.FATAL, "Fatal without dialog without exception"); + } + + @Test + public void testFatalLoggingWithDialog() { + if (GraphicsEnvironment.isHeadless()) { + // Skip this test if running in headless mode + return; + } + automaticallyDismissDialog(); + testMMLogger.fatalDialog("Fatal with dialog with exception", "Fatal dialog title"); + verifyLog(Level.FATAL, "Fatal with dialog with exception"); + } + + @Test + public void testDeprecatedFatalLoggingWithDialog() { + if (GraphicsEnvironment.isHeadless()) { + // Skip this test if running in headless mode + return; + } + automaticallyDismissDialog(); + testMMLogger.fatal("Fatal with dialog with exception", "Fatal dialog title"); + verifyLog(Level.FATAL, "Fatal with dialog with exception"); + } + + @Test + public void testFatalLoggingWithException() { + Exception e = new Exception("Test exception"); + testMMLogger.fatal(e, "Fatal without dialog with exception"); + verifyLog(Level.FATAL, "Fatal without dialog with exception", e); + } + + @Test + public void testDeprecatedFatalLoggingWithException() { + if (GraphicsEnvironment.isHeadless()) { + // Skip this test if running in headless mode + return; + } + automaticallyDismissDialog(); + Exception e = new Exception("Test exception"); + testMMLogger.fatal(e, "Fatal without dialog with exception" , "Fatal dialog title"); + verifyLog(Level.FATAL, "Fatal without dialog with exception", e); + } + + private void verifyLog(Level level, String message) { + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Message.class); + verify(mockLogger).logMessage(anyString(), eq(level), nullable(Marker.class), messageCaptor.capture(), nullable(Throwable.class)); + assertEquals(message, messageCaptor.getValue().getFormattedMessage()); + } + + private void verifyLog(Level level, String message, Throwable e) { + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Message.class); + ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(mockLogger).logMessage(anyString(), eq(level), nullable(Marker.class), messageCaptor.capture(), throwableCaptor.capture()); + assertEquals(message, messageCaptor.getValue().getFormattedMessage()); + assertEquals(e, throwableCaptor.getValue()); + } + + // Simulate pressing the Enter key, necessary to dismiss the dialogs created by the logger on some types of logs + private static void automaticallyDismissDialog() { + try { + Robot robot = new Robot(); + new Thread(() -> { + try { + Thread.sleep(1500); + robot.keyPress(KeyEvent.VK_ENTER); + Thread.sleep(100); + robot.keyRelease(KeyEvent.VK_ENTER); + } catch (Exception e) { + e.printStackTrace(); + } + }).start(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + // Custom logger implementation for testing + private static class CustomLogger extends AbstractLogger { + protected CustomLogger(String name) { + super(name, null); + } + + @Override + public void logMessage(String fqcn, Level level, org.apache.logging.log4j.Marker marker, String message, Throwable t) { + // Custom implementation for logging messages + } + + @Override + public boolean isEnabled(Level level, org.apache.logging.log4j.Marker marker, String message) { + return true; + } + + @Override + public boolean isEnabled(Level level, org.apache.logging.log4j.Marker marker, String message, Throwable t) { + return true; + } + + @Override + public boolean isEnabled(Level level, org.apache.logging.log4j.Marker marker, String message, Object... params) { + return true; + } + + @Override + public boolean isEnabled(Level level, Marker marker, String message, Object p0) { + return true; + } + + @Override + public boolean isEnabled(Level level, Marker marker, String message, Object p0, Object p1) { + return true; + } + + @Override + public boolean isEnabled(Level level, Marker marker, String message, Object p0, Object p1, Object p2) { + return true; + } + + @Override + public boolean isEnabled(Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3) { + return true; + } + + @Override + public boolean isEnabled(Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4) { + return true; + } + + @Override + public boolean isEnabled(Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5) { + return true; + } + + @Override + public boolean isEnabled(Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6) { + return true; + } + + @Override + public boolean isEnabled(Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7) { + return true; + } + + @Override + public boolean isEnabled(Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7, Object p8) { + return true; + } + + @Override + public boolean isEnabled(Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7, Object p8, Object p9) { + return true; + } + + @Override + public void logMessage(String fqcn, Level level, Marker marker, Message message, Throwable t) { + + } + + @Override + public boolean isEnabled(Level level, org.apache.logging.log4j.Marker marker, Object message, Throwable t) { + return true; + } + + @Override + public boolean isEnabled(Level level, org.apache.logging.log4j.Marker marker, org.apache.logging.log4j.message.Message message, Throwable t) { + return true; + } + + @Override + public boolean isEnabled(Level level, Marker marker, CharSequence message, Throwable t) { + return true; + } + + @Override + public Level getLevel() { + return Level.ALL; + } + } +}