diff --git a/build.gradle b/build.gradle index 24275e83..c849417b 100644 --- a/build.gradle +++ b/build.gradle @@ -40,10 +40,10 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" testImplementation 'com.sparkjava:spark-core:2.9.4' testImplementation "org.slf4j:slf4j-api:2.0.18" + testImplementation "ch.qos.logback:logback-classic:1.6.3" testRuntimeOnly "org.junit.platform:junit-platform-launcher:6.1.3" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - testRuntimeOnly "ch.qos.logback:logback-classic:1.5.38" } test { diff --git a/src/test/java/com/dajudge/kindcontainer/LoggingTest.java b/src/test/java/com/dajudge/kindcontainer/LoggingTest.java index 9b143e61..02dc0021 100644 --- a/src/test/java/com/dajudge/kindcontainer/LoggingTest.java +++ b/src/test/java/com/dajudge/kindcontainer/LoggingTest.java @@ -1,32 +1,78 @@ package com.dajudge.kindcontainer; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; +import java.util.List; -import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; /* - * Testcontainers uses the 1.x API of slf4j and newer versions of logback are based on - * slf4j 2.x, so updates to logback silently break logging for tests. This test ensures - * that the logback version is compatible with the slf4j version used by testcontainers. + * Logging is a user-visible diagnostic contract of this library. Dependency updates must not + * silently replace the SLF4J provider, drop log events, duplicate them, or lose throwable output. */ public class LoggingTest { @Test public void logging_works() { - final PrintStream out = System.out; + assertEquals( + LoggerContext.class, + LoggerFactory.getILoggerFactory().getClass(), + "SLF4J must be bound to Logback" + ); + + final String infoMessage = "logging-contract-info-7f27a4d0"; + final String errorMessage = "logging-contract-error-cd132e43"; + final String exceptionMessage = "logging-contract-exception-8410b8d2"; + + final List events = captureLogs(() -> { + final Logger logger = LoggerFactory.getLogger(LoggingTest.class); + logger.info(infoMessage); + logger.error(errorMessage, new IllegalStateException(exceptionMessage)); + }); + + assertEquals(1, occurrences(events, infoMessage), "INFO event must be emitted exactly once"); + assertEquals(1, occurrences(events, errorMessage), "ERROR event must be emitted exactly once"); + assertEquals(1, occurrences(events, exceptionMessage), "Throwable message must be emitted exactly once"); + } + + @Test + public void testcontainers_logging_works() { + final List events = captureLogs(() -> { + try (GenericContainer container = new GenericContainer<>("alpine:3.20") + .withCommand("sh", "-c", "echo testcontainers-logging-smoke")) { + container.start(); + } + }); + + assertTrue(events.stream().anyMatch(event -> event.getFormattedMessage().contains("alpine:3.20")), + "Testcontainers lifecycle logs must identify the container image"); + } + + private static List captureLogs(final Runnable operation) { + final LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + final ch.qos.logback.classic.Logger rootLogger = context.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + final ListAppender appender = new ListAppender<>(); + appender.start(); + rootLogger.addAppender(appender); try { - final ByteArrayOutputStream temp = new ByteArrayOutputStream(); - System.setOut(new PrintStream(new BufferedOutputStream(temp))); - LoggerFactory.getLogger(LoggingTest.class).info("Hello, world!"); - System.out.flush(); - assertTrue(new String(temp.toByteArray(), UTF_8).contains("Hello, world!")); + operation.run(); + return List.copyOf(appender.list); } finally { - System.setOut(out); + rootLogger.detachAppender(appender); + appender.stop(); } } + + private static long occurrences(final List events, final String message) { + return events.stream() + .filter(event -> event.getFormattedMessage().contains(message) + || (event.getThrowableProxy() != null && event.getThrowableProxy().getMessage().contains(message))) + .count(); + } }