diff --git a/src/main/java/org/kiwiproject/test/logback/InMemoryAppenderExtension.java b/src/main/java/org/kiwiproject/test/logback/InMemoryAppenderExtension.java index 728f7b8..ff7022c 100644 --- a/src/main/java/org/kiwiproject/test/logback/InMemoryAppenderExtension.java +++ b/src/main/java/org/kiwiproject/test/logback/InMemoryAppenderExtension.java @@ -1,11 +1,20 @@ package org.kiwiproject.test.logback; +import static java.util.Objects.nonNull; import static org.assertj.core.api.Assertions.assertThat; +import ch.qos.logback.classic.ClassicConstants; import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.joran.JoranConfigurator; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.joran.spi.JoranException; import com.google.common.annotations.Beta; +import com.google.common.io.Resources; import lombok.Getter; import lombok.experimental.Accessors; +import org.checkerframework.checker.nullness.qual.Nullable; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; @@ -28,6 +37,9 @@ public class InMemoryAppenderExtension implements BeforeEachCallback, AfterEachC private final Class loggerClass; private final String appenderName; + // Use the default Logback test configuration file as our default value. + private String logbackConfigFilePath = ClassicConstants.TEST_AUTOCONFIG_FILE; + @Getter @Accessors(fluent = true) private InMemoryAppender appender; @@ -67,7 +79,7 @@ public InMemoryAppenderExtension(Class loggerClass) { * {@code appenderName}. *

* This appender must exist, e.g., in your {@code logback-test.xml} - * configuration file.} + * configuration file. *

* For example, for a test named {@code MyAwesomeTest}, the * {@code src/test/resources/logback-test.xml} must contain: @@ -93,6 +105,26 @@ public InMemoryAppenderExtension(Class loggerClass, String appenderName) { this.appenderName = appenderName; } + /** + * The Logback configuration to use if the logging system needs to be reset. + *

+ * For example: + *

+     * {@literal @}RegisterExtension
+     *  private final InMemoryAppenderExtension inMemoryAppenderExtension =
+     *          new InMemoryAppenderExtension(InMemoryAppenderTest.class)
+     *                  .withLogbackConfigFilePath("acme-logback-test.xml");
+     * 
+ * + * @param logbackConfigFilePath + * @return this extension, so this can be chained after the constructor + * @see https://github.com/kiwiproject/kiwi-test/issues/457 + */ + public InMemoryAppenderExtension withLogbackConfigFilePath(String logbackConfigFilePath) { + this.logbackConfigFilePath = logbackConfigFilePath; + return this; + } + /** * Exposes the {@link InMemoryAppender} associated with {@code loggerClass}. * It can be obtained via the {@link #appender()} method. Usually, tests @@ -121,9 +153,9 @@ public InMemoryAppenderExtension(Class loggerClass, String appenderName) { * @param context the current extension context; never {@code null} */ @Override - public void beforeEach(ExtensionContext context) { + public void beforeEach(ExtensionContext context) throws Exception { var logbackLogger = (Logger) LoggerFactory.getLogger(loggerClass); - var rawAppender = logbackLogger.getAppender(appenderName); + var rawAppender = getAppender(logbackLogger); assertThat(rawAppender) .describedAs("Expected an appender named '%s' for logger '%s' of type %s", @@ -132,6 +164,36 @@ public void beforeEach(ExtensionContext context) { appender = (InMemoryAppender) rawAppender; } + @Nullable + @SuppressWarnings("java:S106") + private Appender getAppender(Logger logbackLogger) throws JoranException { + var rawAppender = logbackLogger.getAppender(appenderName); + + if (nonNull(rawAppender)) { + return rawAppender; + } + + // Write to stdout since the logging system might be hosed... + System.out.printf( + "Appender %s not found on logger %s; attempt fix by resetting logging configuration using config: %s%n", + appenderName, loggerClass, logbackConfigFilePath); + System.out.println("You can customize the logging configuration using #withLogbackConfigFilePath"); + + // Reset the Logback logging system + var loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + loggerContext.stop(); + + var joranConfigurator = new JoranConfigurator(); + joranConfigurator.setContext(loggerContext); + var logbackConfigUrl = Resources.getResource(logbackConfigFilePath); + joranConfigurator.doConfigure(logbackConfigUrl); + loggerContext.start(); + + // Try again and return whatever we get. It should not be null after resetting, unless + // the reset failed, or the appender was not configured correctly. + return logbackLogger.getAppender(appenderName); + } + /** * Clears all logging events from the {@link InMemoryAppender} to ensure each * test starts with an empty appender.