Class DockerAvailability

java.lang.Object
org.mockserver.test.DockerAvailability

public final class DockerAvailability extends Object
Fail-safe wrapper for the Docker availability probe used by Docker-gated tests.

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 (a DockerException, hence a plain RuntimeException) — 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 as NoClassDefFoundError or ExceptionInInitializerError — an incomplete or conflicting test classpath.
Any of these escapes the probe and turns "no usable Docker" into a hard ERROR, defeating the 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 Details

    • isAvailable

      public static boolean isAvailable(BooleanSupplier probe)
      Parameters:
      probe - the underlying availability check, e.g. () -> DockerClientFactory.instance().isDockerAvailable()
      Returns:
      true only when the probe returns true; false for every failure mode, including those the probe throws rather than reports
    • resetCacheForTest

      public static void resetCacheForTest()
      Resets the per-JVM cache. Exposed for DockerAvailabilityTest, which must exercise several probe outcomes in one JVM.