Add failure analysis for invalid Docker environments

This commit introduces a failure analyzer implementation that provides
actionable guidance when Testcontainers fails to detect a running
Docker-compatible environment.

See gh-47797

Signed-off-by: Dmytro Nosan <dimanosan@gmail.com>
This commit is contained in:
Dmytro Nosan
2025-12-19 11:45:20 +01:00
committed by Stéphane Nicoll
parent 784fd6c13e
commit 81f7caad24
4 changed files with 149 additions and 0 deletions
@@ -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<IllegalStateException> {
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);
}
}
@@ -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;
@@ -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
@@ -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);
}
}