From 16245db53502cdfa9c2446916ba6fc80fdaa59b6 Mon Sep 17 00:00:00 2001 From: Hai Phuc Nguyen <“3423575+haiphucnguyen@users.noreply.github.com”> Date: Wed, 18 Jun 2025 17:38:38 -0700 Subject: [PATCH 1/2] Update --- examples/springboot-ollama/build.gradle.kts | 1 + .../examples/ollama/OllamaDemoAppTest.java | 15 ++- gradle.properties | 2 +- gradle/libs.versions.toml | 1 + .../ai/OllamaContainerProvider.java | 25 ++++- .../testcontainers/Slf4jOutputConsumer.java | 102 ++++++++++++++++++ .../SpringAwareContainerProvider.java | 14 ++- 7 files changed, 150 insertions(+), 10 deletions(-) create mode 100644 spring-testcontainers/src/main/java/io/flowinquiry/testcontainers/Slf4jOutputConsumer.java diff --git a/examples/springboot-ollama/build.gradle.kts b/examples/springboot-ollama/build.gradle.kts index 0577398..e879d43 100644 --- a/examples/springboot-ollama/build.gradle.kts +++ b/examples/springboot-ollama/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(libs.bundles.spring.ai) testImplementation(platform(libs.junit.bom)) testImplementation(libs.junit.jupiter) + testImplementation(libs.junit.jupiter.params) testImplementation(libs.junit.platform.launcher) testImplementation(libs.spring.boot.starter.test) } diff --git a/examples/springboot-ollama/src/test/java/io/flowinquiry/testcontainers/examples/ollama/OllamaDemoAppTest.java b/examples/springboot-ollama/src/test/java/io/flowinquiry/testcontainers/examples/ollama/OllamaDemoAppTest.java index e6445bd..7ff4221 100644 --- a/examples/springboot-ollama/src/test/java/io/flowinquiry/testcontainers/examples/ollama/OllamaDemoAppTest.java +++ b/examples/springboot-ollama/src/test/java/io/flowinquiry/testcontainers/examples/ollama/OllamaDemoAppTest.java @@ -9,6 +9,8 @@ import io.flowinquiry.testcontainers.ai.OllamaOptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.slf4j.Logger; import org.springframework.ai.chat.client.ChatClient; import org.springframework.beans.factory.annotation.Autowired; @@ -23,7 +25,7 @@ @EnableOllamaContainer( dockerImage = "ollama/ollama", version = "0.9.0", - model = "smollm2:135m", + model = "llama3:latest", options = @OllamaOptions(temperature = "0.7", topP = "0.5")) @ActiveProfiles("test") public class OllamaDemoAppTest { @@ -53,10 +55,13 @@ public void testHealthEndpoint() { assertTrue(response.contains("Ollama Chat Controller is up and running")); } - @Test - public void testChatClient() { + @ParameterizedTest + @CsvSource({ + "What is the result of 1+2? Give the value only, 3", + "How many letter 'r' in the word 'Hello'? Give the value only, 0" + }) + public void testChatClient(String prompt, String expectedResult) { log.info("Testing chat client directly"); - String prompt = "What is Spring AI?"; log.info("Sending prompt: {}", prompt); String content = chatClient.prompt().user(prompt).call().content(); @@ -64,5 +69,7 @@ public void testChatClient() { log.info("Received response: {}", content); assertNotNull(content); assertFalse(content.isEmpty()); + assertTrue( + content.contains(expectedResult), "Response should contain '" + expectedResult + "'"); } } diff --git a/gradle.properties b/gradle.properties index 6328e07..5e7100a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,5 +2,5 @@ # https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties org.gradle.configuration-cache=true -version=0.9.1 +version=0.9.2 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 04c9fc9..529c0b9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ spring-ai = "1.0.0" junit-bom = { group = "org.junit", name = "junit-bom", version.ref = "junit-jupiter" } junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter" } junit-jupiter-api = { group = "org.junit.jupiter", name = "junit-jupiter-api" } +junit-jupiter-params = { group = "org.junit.jupiter", name = "junit-jupiter-params" } junit-jupiter-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine" } junit-platform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher" } spring-bom = { group = "org.springframework", name = "spring-framework-bom", version.ref = "spring" } diff --git a/modules/ollama/src/main/java/io/flowinquiry/testcontainers/ai/OllamaContainerProvider.java b/modules/ollama/src/main/java/io/flowinquiry/testcontainers/ai/OllamaContainerProvider.java index 3452ba9..65b999c 100644 --- a/modules/ollama/src/main/java/io/flowinquiry/testcontainers/ai/OllamaContainerProvider.java +++ b/modules/ollama/src/main/java/io/flowinquiry/testcontainers/ai/OllamaContainerProvider.java @@ -1,8 +1,10 @@ package io.flowinquiry.testcontainers.ai; import static io.flowinquiry.testcontainers.ContainerType.OLLAMA; +import static org.testcontainers.containers.BindMode.READ_WRITE; import io.flowinquiry.testcontainers.ContainerType; +import io.flowinquiry.testcontainers.Slf4jOutputConsumer; import io.flowinquiry.testcontainers.SpringAwareContainerProvider; import java.io.IOException; import java.util.Properties; @@ -10,6 +12,7 @@ import org.slf4j.LoggerFactory; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.PropertiesPropertySource; +import org.testcontainers.containers.Container; import org.testcontainers.ollama.OllamaContainer; /** @@ -46,7 +49,8 @@ public ContainerType getContainerType() { */ @Override protected OllamaContainer createContainer() { - return new OllamaContainer(dockerImage + ":" + version); + return new OllamaContainer(dockerImage + ":" + version) + .withFileSystemBind("/tmp/ollama-cache", "/root/.ollama", READ_WRITE); } /** @@ -60,14 +64,31 @@ protected OllamaContainer createContainer() { @Override public void start() { super.start(); + + Logger containerLog = LoggerFactory.getLogger(OllamaContainerProvider.class); + container.followOutput(new Slf4jOutputConsumer(containerLog)); + try { log.info("Starting pull model {}", enableContainerAnnotation.model()); - container.execInContainer("ollama", "pull", enableContainerAnnotation.model()); + pullModelIfMissing(enableContainerAnnotation.model()); } catch (IOException | InterruptedException e) { throw new RuntimeException(e); } } + private void pullModelIfMissing(String modelName) throws IOException, InterruptedException { + Container.ExecResult result = container.execInContainer("ollama", "list"); + String output = result.getStdout(); + + if (!output.contains(modelName)) { + log.info("Model '{}' not found in ollama cache. Pulling...", modelName); + Container.ExecResult pullResult = container.execInContainer("ollama", "pull", modelName); + log.info("Pull complete: {}", pullResult.getStdout()); + } else { + log.info("Model '{}' already exists. Skipping pull.", modelName); + } + } + /** * Applies Ollama-specific configuration to the Spring environment. * diff --git a/spring-testcontainers/src/main/java/io/flowinquiry/testcontainers/Slf4jOutputConsumer.java b/spring-testcontainers/src/main/java/io/flowinquiry/testcontainers/Slf4jOutputConsumer.java new file mode 100644 index 0000000..ee6b501 --- /dev/null +++ b/spring-testcontainers/src/main/java/io/flowinquiry/testcontainers/Slf4jOutputConsumer.java @@ -0,0 +1,102 @@ +package io.flowinquiry.testcontainers; + +import org.slf4j.Logger; +import org.slf4j.event.Level; +import org.testcontainers.containers.output.BaseConsumer; +import org.testcontainers.containers.output.OutputFrame; + +/** + * An implementation of {@link BaseConsumer} that routes container output to SLF4J logging. This + * consumer allows for different log levels to be used for STDOUT and STDERR streams. + * + *
Usage example: + * + *
+ * Logger logger = LoggerFactory.getLogger(MyClass.class);
+ * GenericContainer container = new GenericContainer("some-image")
+ * .withLogConsumer(new Slf4jOutputConsumer(logger));
+ *
+ */
+public class Slf4jOutputConsumer extends BaseConsumerThe method: + * + *