Class DockerAvailability
Docker-gated suites are written as:
Assume.assumeTrue("Docker is not available",
DockerAvailability.isAvailable(() -> DockerClientFactory.instance().isDockerAvailable()));
so they SKIP where Docker is unusable and RUN where it is usable. That design only holds if
the probe yields a boolean in every circumstance.
Testcontainers' own probe does not.
DockerClientFactory.isDockerAvailable() is documented as "true if Docker is
available, false if not", but is implemented as
try { client(); return true; } catch (IllegalStateException ex) { return false; } —
only IllegalStateException becomes false. client() does much more
than connect: it starts the Ryuk reaper via ResourceReaper.instance().init() and runs
version/mount checks, and those stages throw types that are NOT IllegalStateException:
com.github.dockerjava.api.exception.BadRequestException(aDockerException, hence a plainRuntimeException) — e.g. a daemon with user-namespace remapping rejecting Ryuk's privileged container with "privileged mode is incompatible with user namespaces";ContainerFetchException/ContainerLaunchException— Ryuk's image cannot be pulled, or its container cannot start;Errors such asNoClassDefFoundErrororExceptionInInitializerError— an incomplete or conflicting test classpath.
assume guard everywhere it is used — in CI, and equally for a contributor whose Docker
is configured unusually. Testcontainers also caches the failure in cachedClientFailure
and rethrows it on every later call, so a single bad probe fails every Docker-gated test in
the JVM.
Why a BooleanSupplier rather than calling Testcontainers directly:
this module is the shared testing utility module and deliberately depends on almost nothing
(JUnit and Hamcrest only). Adding Testcontainers here purely for this helper would widen that
surface for every consumer, which the repository-wide dependencyConvergence enforcer
rule makes a real risk. Taking the probe as a lambda keeps one shared implementation without
the coupling, and keeps it compile-time type-safe — a reflective lookup would silently degrade
to "unavailable" if the method were renamed, silently skipping every Docker-gated test, which
is the exact failure mode this helper exists to prevent.
Pass a lambda, not a method reference. Use
() -> DockerClientFactory.instance().isDockerAvailable() rather than
DockerClientFactory.instance()::isDockerAvailable: a method reference evaluates
instance() eagerly at the call site, outside this class's try/catch, so a failure
constructing the factory would still escape.
In CI, pair this with a fail-closed assertion. Being fail-SAFE is a deliberate
trade: an unusable Docker becomes a SKIP rather than an ERROR, which is right off-CI but is a
silent false positive in CI, where skipping proves nothing. Note this WIDENS the silent-skip
surface for a suite that previously guarded with catch (Exception e), because an
Error used to escape that catch and fail loudly. Every Docker-gated suite that runs in CI
should therefore have its surefire/failsafe reports covered by
.buildkite/scripts/steps/assert-suite-ran.sh, which fails the build when a report shows
zero tests or all-skipped.
COVERAGE: all nine Docker-gated suites are covered — the three cloud blob-store contract suites by
java-cloud-store-test.sh, the five *LiveBrokerIntegrationTest suites in
mockserver-async by java-async-broker-test.sh, and
MockServerContainerIntegrationTest in mockserver-testcontainers by
java-testcontainers-test.sh. That last suite was previously named
MockServerContainerIT — a class ending IT matched neither Surefire
(**/*Test.java) nor Failsafe (**/*IntegrationTest.java), so it
executed nowhere and needed no assertion. Renaming it to *IntegrationTest makes Failsafe
collect it, so it now runs — and, like every other Docker-gated suite, would otherwise SKIP
silently in the socket-free main build, so it gets its own socket-bearing step with an
assert-suite-ran.sh guard (see docs/code/client-and-integrations.md).
WHY THE ASYNC STEP EXISTS — a diagnosis worth keeping, because the failure was invisible. Those
five suites WERE invoked in the main build (Failsafe's include matches them, and the main build
runs clean install, which reaches verify) but SKIPPED EVERY TEST ON EVERY RUN,
because the main build deliberately runs without a Docker socket — the socket-bearing work is
split into its own step since run-in-docker.sh exit-0s a socket step on PR builds.
Confirmed from Buildkite: the :maven: build job passed (exit 0) in mockserver-java builds
#1580 and #1583, each reporting
failsafe:integration-test @ mockserver-async → Tests run: 5, Skipped: 5. (Both builds
failed overall, on the separate cloud blob-store step — a green job that tested nothing is exactly
the false positive being removed here.)
The lesson generalises: "collected" is not "executed", and an include pattern is not evidence a
suite tests anything. Note also the ordering — adding assert-suite-ran.sh over those five
BEFORE supplying the socket would have turned the pipeline permanently red rather than closing a
gap. A fail-closed assertion is only safe once the suite can actually pass.
-
Method Summary
Modifier and TypeMethodDescriptionstatic booleanisAvailable(BooleanSupplier probe) static voidResets the per-JVM cache.
-
Method Details
-
isAvailable
- Parameters:
probe- the underlying availability check, e.g.() -> DockerClientFactory.instance().isDockerAvailable()- Returns:
trueonly when the probe returnstrue;falsefor every failure mode, including those the probe throws rather than reports
-
resetCacheForTest
public static void resetCacheForTest()Resets the per-JVM cache. Exposed forDockerAvailabilityTest, which must exercise several probe outcomes in one JVM.
-