Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
74 changes: 60 additions & 14 deletions src/test/java/com/dajudge/kindcontainer/LoggingTest.java
Original file line number Diff line number Diff line change
@@ -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<ILoggingEvent> 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<ILoggingEvent> 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<ILoggingEvent> 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<ILoggingEvent> 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<ILoggingEvent> events, final String message) {
return events.stream()
.filter(event -> event.getFormattedMessage().contains(message)
|| (event.getThrowableProxy() != null && event.getThrowableProxy().getMessage().contains(message)))
.count();
}
}
Loading