From 6987c909fdd440d7f4d8577ecba4609760539d26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:42:03 +0000 Subject: [PATCH 1/6] Bump ch.qos.logback:logback-classic from 1.5.38 to 1.6.3 Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.38 to 1.6.3. - [Release notes](https://github.com/qos-ch/logback/releases) - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.38...v_1.6.3) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-version: 1.6.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 24275e83..cb1159de 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ dependencies { 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" + testRuntimeOnly "ch.qos.logback:logback-classic:1.6.3" } test { From 3777669faa129b388c13c978553d33f0e92ec9fe Mon Sep 17 00:00:00 2001 From: Alex Stockinger Date: Sun, 6 Sep 2026 07:50:38 +0200 Subject: [PATCH 2/6] Strengthen logging compatibility contract test --- .../dajudge/kindcontainer/LoggingTest.java | 48 +++++++++++++++++-- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/dajudge/kindcontainer/LoggingTest.java b/src/test/java/com/dajudge/kindcontainer/LoggingTest.java index 9b143e61..7f86f113 100644 --- a/src/test/java/com/dajudge/kindcontainer/LoggingTest.java +++ b/src/test/java/com/dajudge/kindcontainer/LoggingTest.java @@ -1,6 +1,7 @@ package com.dajudge.kindcontainer; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedOutputStream; @@ -8,12 +9,12 @@ import java.io.PrintStream; 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 @@ -22,11 +23,48 @@ public void logging_works() { try { final ByteArrayOutputStream temp = new ByteArrayOutputStream(); System.setOut(new PrintStream(new BufferedOutputStream(temp))); - LoggerFactory.getLogger(LoggingTest.class).info("Hello, world!"); + + assertEquals( + "ch.qos.logback.classic.LoggerContext", + LoggerFactory.getILoggerFactory().getClass().getName(), + "SLF4J must be bound to Logback" + ); + + final Logger logger = LoggerFactory.getLogger(LoggingTest.class); + final String infoMessage = "logging-contract-info-7f27a4d0"; + final String errorMessage = "logging-contract-error-cd132e43"; + final String exceptionMessage = "logging-contract-exception-8410b8d2"; + + logger.info(infoMessage); + logger.error(errorMessage, new IllegalStateException(exceptionMessage)); System.out.flush(); - assertTrue(new String(temp.toByteArray(), UTF_8).contains("Hello, world!")); + + final String output = new String(temp.toByteArray(), UTF_8); + + assertEquals(1, occurrences(output, infoMessage), "INFO event must be emitted exactly once"); + assertEquals(1, occurrences(output, errorMessage), "ERROR event must be emitted exactly once"); + assertEquals(1, occurrences(output, exceptionMessage), "Throwable message must be emitted exactly once"); + + assertTrue(output.contains("INFO com.dajudge.kindcontainer.LoggingTest - " + infoMessage), + "INFO formatting must preserve level, logger name and message"); + assertTrue(output.contains("ERROR com.dajudge.kindcontainer.LoggingTest - " + errorMessage), + "ERROR formatting must preserve level, logger name and message"); + assertTrue(output.contains("java.lang.IllegalStateException: " + exceptionMessage), + "Throwable type and message must be present"); + assertTrue(output.contains("at com.dajudge.kindcontainer.LoggingTest.logging_works"), + "Throwable stack trace must be present"); } finally { System.setOut(out); } } + + private static int occurrences(final String haystack, final String needle) { + int count = 0; + int offset = 0; + while ((offset = haystack.indexOf(needle, offset)) >= 0) { + count++; + offset += needle.length(); + } + return count; + } } From dd1574cca1d283ceb8c6d032ce742a7954a3bb21 Mon Sep 17 00:00:00 2001 From: Alex Stockinger Date: Sun, 6 Sep 2026 08:14:17 +0200 Subject: [PATCH 3/6] Fix logging contract assertions for abbreviated logger names --- .../java/com/dajudge/kindcontainer/LoggingTest.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/dajudge/kindcontainer/LoggingTest.java b/src/test/java/com/dajudge/kindcontainer/LoggingTest.java index 7f86f113..8b0b252a 100644 --- a/src/test/java/com/dajudge/kindcontainer/LoggingTest.java +++ b/src/test/java/com/dajudge/kindcontainer/LoggingTest.java @@ -45,10 +45,12 @@ public void logging_works() { assertEquals(1, occurrences(output, errorMessage), "ERROR event must be emitted exactly once"); assertEquals(1, occurrences(output, exceptionMessage), "Throwable message must be emitted exactly once"); - assertTrue(output.contains("INFO com.dajudge.kindcontainer.LoggingTest - " + infoMessage), - "INFO formatting must preserve level, logger name and message"); - assertTrue(output.contains("ERROR com.dajudge.kindcontainer.LoggingTest - " + errorMessage), - "ERROR formatting must preserve level, logger name and message"); + assertTrue(output.contains("INFO "), "INFO level must be present"); + assertTrue(output.contains("LoggingTest - " + infoMessage), + "INFO formatting must preserve logger identity and message"); + assertTrue(output.contains("ERROR "), "ERROR level must be present"); + assertTrue(output.contains("LoggingTest - " + errorMessage), + "ERROR formatting must preserve logger identity and message"); assertTrue(output.contains("java.lang.IllegalStateException: " + exceptionMessage), "Throwable type and message must be present"); assertTrue(output.contains("at com.dajudge.kindcontainer.LoggingTest.logging_works"), From 0d2d57c8c881768d76c03f68c41f595992ff3ee8 Mon Sep 17 00:00:00 2001 From: Alex Stockinger Date: Sun, 6 Sep 2026 14:51:33 +0200 Subject: [PATCH 4/6] Test Testcontainers logging through Logback --- .../dajudge/kindcontainer/LoggingTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/test/java/com/dajudge/kindcontainer/LoggingTest.java b/src/test/java/com/dajudge/kindcontainer/LoggingTest.java index 8b0b252a..2f8eb0d0 100644 --- a/src/test/java/com/dajudge/kindcontainer/LoggingTest.java +++ b/src/test/java/com/dajudge/kindcontainer/LoggingTest.java @@ -3,6 +3,7 @@ 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; @@ -60,6 +61,29 @@ public void logging_works() { } } + @Test + public void testcontainers_logging_works() { + final PrintStream out = System.out; + try { + final ByteArrayOutputStream temp = new ByteArrayOutputStream(); + System.setOut(new PrintStream(new BufferedOutputStream(temp))); + + try (GenericContainer container = new GenericContainer<>("alpine:3.20") + .withCommand("sh", "-c", "echo testcontainers-logging-smoke")) { + container.start(); + } + System.out.flush(); + + final String output = new String(temp.toByteArray(), UTF_8); + assertTrue(output.contains("GenericContainer"), + "Testcontainers lifecycle logs must reach the configured Logback appender"); + assertTrue(output.contains("alpine:3.20"), + "Testcontainers lifecycle logs must identify the container image"); + } finally { + System.setOut(out); + } + } + private static int occurrences(final String haystack, final String needle) { int count = 0; int offset = 0; From 3153dd22de419488958bf850efe5668695bff8e4 Mon Sep 17 00:00:00 2001 From: Alex Stockinger Date: Wed, 9 Sep 2026 16:25:13 +0200 Subject: [PATCH 5/6] Make Logback available to logging tests --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cb1159de..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.6.3" } test { From 1cb9448fbcce201ef3f583ded23fcf1b3e1386d8 Mon Sep 17 00:00:00 2001 From: Alex Stockinger Date: Wed, 9 Sep 2026 16:25:32 +0200 Subject: [PATCH 6/6] Capture Logback events without replacing stdout --- .../dajudge/kindcontainer/LoggingTest.java | 96 ++++++++----------- 1 file changed, 39 insertions(+), 57 deletions(-) diff --git a/src/test/java/com/dajudge/kindcontainer/LoggingTest.java b/src/test/java/com/dajudge/kindcontainer/LoggingTest.java index 2f8eb0d0..02dc0021 100644 --- a/src/test/java/com/dajudge/kindcontainer/LoggingTest.java +++ b/src/test/java/com/dajudge/kindcontainer/LoggingTest.java @@ -1,15 +1,15 @@ 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; @@ -20,77 +20,59 @@ public class LoggingTest { @Test public void logging_works() { - final PrintStream out = System.out; - try { - final ByteArrayOutputStream temp = new ByteArrayOutputStream(); - System.setOut(new PrintStream(new BufferedOutputStream(temp))); + assertEquals( + LoggerContext.class, + LoggerFactory.getILoggerFactory().getClass(), + "SLF4J must be bound to Logback" + ); - assertEquals( - "ch.qos.logback.classic.LoggerContext", - LoggerFactory.getILoggerFactory().getClass().getName(), - "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); - final String infoMessage = "logging-contract-info-7f27a4d0"; - final String errorMessage = "logging-contract-error-cd132e43"; - final String exceptionMessage = "logging-contract-exception-8410b8d2"; - logger.info(infoMessage); logger.error(errorMessage, new IllegalStateException(exceptionMessage)); - System.out.flush(); - - final String output = new String(temp.toByteArray(), UTF_8); + }); - assertEquals(1, occurrences(output, infoMessage), "INFO event must be emitted exactly once"); - assertEquals(1, occurrences(output, errorMessage), "ERROR event must be emitted exactly once"); - assertEquals(1, occurrences(output, exceptionMessage), "Throwable message must be emitted exactly once"); - - assertTrue(output.contains("INFO "), "INFO level must be present"); - assertTrue(output.contains("LoggingTest - " + infoMessage), - "INFO formatting must preserve logger identity and message"); - assertTrue(output.contains("ERROR "), "ERROR level must be present"); - assertTrue(output.contains("LoggingTest - " + errorMessage), - "ERROR formatting must preserve logger identity and message"); - assertTrue(output.contains("java.lang.IllegalStateException: " + exceptionMessage), - "Throwable type and message must be present"); - assertTrue(output.contains("at com.dajudge.kindcontainer.LoggingTest.logging_works"), - "Throwable stack trace must be present"); - } finally { - System.setOut(out); - } + 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 PrintStream out = System.out; - try { - final ByteArrayOutputStream temp = new ByteArrayOutputStream(); - System.setOut(new PrintStream(new BufferedOutputStream(temp))); - + final List events = captureLogs(() -> { try (GenericContainer container = new GenericContainer<>("alpine:3.20") .withCommand("sh", "-c", "echo testcontainers-logging-smoke")) { container.start(); } - System.out.flush(); + }); + + assertTrue(events.stream().anyMatch(event -> event.getFormattedMessage().contains("alpine:3.20")), + "Testcontainers lifecycle logs must identify the container image"); + } - final String output = new String(temp.toByteArray(), UTF_8); - assertTrue(output.contains("GenericContainer"), - "Testcontainers lifecycle logs must reach the configured Logback appender"); - assertTrue(output.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 { + operation.run(); + return List.copyOf(appender.list); } finally { - System.setOut(out); + rootLogger.detachAppender(appender); + appender.stop(); } } - private static int occurrences(final String haystack, final String needle) { - int count = 0; - int offset = 0; - while ((offset = haystack.indexOf(needle, offset)) >= 0) { - count++; - offset += needle.length(); - } - return count; + 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(); } }