diff --git a/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/DockerEnvironmentNotFoundFailureAnalyzer.java b/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/DockerEnvironmentNotFoundFailureAnalyzer.java new file mode 100644 index 000000000000..428ebfa77d05 --- /dev/null +++ b/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/DockerEnvironmentNotFoundFailureAnalyzer.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testcontainers.diagnostics; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.util.StringUtils; + +/** + * Failure analyzer for the Docker environment wasn't found. + * + * @author Dmytro Nosan + */ +class DockerEnvironmentNotFoundFailureAnalyzer extends AbstractFailureAnalyzer { + + private static final String EXPECTED_MESSAGE = "Could not find a valid Docker environment"; + + private static final String DESCRIPTION = "Could not find a valid Docker environment for Testcontainers."; + + private static final String ACTION = """ + - Ensure a Docker-compatible container engine is installed and running. + - If running Testcontainers in CI, ensure the runner has access to the daemon, typically by using a mounted socket or a Docker-in-Docker setup. + - Review the Testcontainers documentation for troubleshooting and advanced configuration options. + """; + + @Override + protected @Nullable FailureAnalysis analyze(Throwable rootFailure, IllegalStateException cause) { + if (isDockerEnvironmentNotFoundError(cause)) { + return new FailureAnalysis(DESCRIPTION, ACTION, cause); + } + return null; + } + + private boolean isDockerEnvironmentNotFoundError(IllegalStateException cause) { + return StringUtils.hasText(cause.getMessage()) && cause.getMessage().contains(EXPECTED_MESSAGE); + } + +} diff --git a/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/package-info.java b/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/package-info.java new file mode 100644 index 000000000000..c74bce645a40 --- /dev/null +++ b/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/diagnostics/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Classes for diagnosing issues with Testcontainers. + */ +@NullMarked +package org.springframework.boot.testcontainers.diagnostics; + +import org.jspecify.annotations.NullMarked; diff --git a/core/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories b/core/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories index 965d32bf8b61..fa136ad89588 100644 --- a/core/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories +++ b/core/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories @@ -6,3 +6,6 @@ org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleApplica org.springframework.test.context.ContextCustomizerFactory=\ org.springframework.boot.testcontainers.service.connection.ServiceConnectionContextCustomizerFactory +# Failure Analyzers +org.springframework.boot.diagnostics.FailureAnalyzer=\ +org.springframework.boot.testcontainers.diagnostics.DockerEnvironmentNotFoundFailureAnalyzer diff --git a/core/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/diagnostics/DockerEnvironmentNotFoundFailureAnalyzerTests.java b/core/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/diagnostics/DockerEnvironmentNotFoundFailureAnalyzerTests.java new file mode 100644 index 000000000000..f65d49f291e0 --- /dev/null +++ b/core/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/diagnostics/DockerEnvironmentNotFoundFailureAnalyzerTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testcontainers.diagnostics; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.diagnostics.FailureAnalyzer; +import org.springframework.core.io.support.SpringFactoriesLoader; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DockerEnvironmentNotFoundFailureAnalyzer}. + * + * @author Dmytro Nosan + */ +class DockerEnvironmentNotFoundFailureAnalyzerTests { + + private final DockerEnvironmentNotFoundFailureAnalyzer analyzer = new DockerEnvironmentNotFoundFailureAnalyzer(); + + @Test + void shouldReturnFailureAnalysisWhenMessageMatches() { + IllegalStateException cause = new IllegalStateException( + "Could not find a valid Docker environment. Please see logs and check configuration"); + FailureAnalysis analysis = this.analyzer + .analyze(new RuntimeException("Root", new RuntimeException("Intermediate", cause))); + assertThat(analysis).isNotNull(); + assertThat(analysis.getDescription()) + .isEqualTo("Could not find a valid Docker environment for Testcontainers."); + assertThat(analysis.getAction()) + .contains("Ensure a Docker-compatible container engine is installed and running"); + assertThat(analysis.getCause()).isSameAs(cause); + } + + @Test + void shouldReturnNullWhenMessageDoesNotMatch() { + FailureAnalysis analysis = this.analyzer.analyze(new IllegalStateException("Some message")); + assertThat(analysis).isNull(); + } + + @Test + void shouldReturnNullWhenMessageIsNull() { + FailureAnalysis analysis = this.analyzer.analyze(new IllegalStateException()); + assertThat(analysis).isNull(); + } + + @Test + void shouldBeRegisteredInSpringFactories() { + assertThat(SpringFactoriesLoader.forDefaultResourceLocation(getClass().getClassLoader()) + .load(FailureAnalyzer.class, (factoryType, factoryImplementationName, failure) -> { + })).anyMatch(DockerEnvironmentNotFoundFailureAnalyzer.class::isInstance); + } + +}