Skip to content

Releases: cppudge/testcontainers-cpp

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 12 Jul 12:20
Immutable release. Only release title and notes can be modified.

This release ships the entire post-0.1.0 backlog — 59 commits: a new ecosystem-module layer on top of a core round-out across wait strategies, streaming transfers, exec, networks and volumes, compose, environment configuration, lifecycle, and a pre-release hardening audit.

Ecosystem modules (testcontainers::modules)

Seven prebuilt technology wrappers over GenericImage: Redis (redis:7.2), PostgreSQL (postgres:16-alpine), MySQL (mysql:8.4), MariaDB (mariadb:11), Kafka (apache/kafka:3.9.1, single-node KRaft), RabbitMQ (rabbitmq:3.13-management), MongoDB (mongo:7, single-node replica set — transactions and change streams work). Each is a copyable config builder modules::XxxImage whose start() returns a move-only started handle modules::XxxContainer — the naming mirrors the core GenericImageContainer split: the Image is the reusable recipe, the Container the running instance.

#include "testcontainers/modules.hpp"

const modules::PostgreSQLContainer pg = modules::PostgreSQLImage()
        .with_init_script("schema.sql", "CREATE TABLE t(id int);")
        .start();   // returns once every init script ran and TCP serves
// pg.connection_string() -> "postgresql://test:test@localhost:<port>/test"
  • Typed connection getters instead of client drivers: connection_string() (plus PostgreSQL's libpq conninfo() and exec_sql(), Kafka's bootstrap_servers() / internal_bootstrap_servers(), RabbitMQ's amqp_url(), MongoDB's mongosh(); MongoDB's DSN pins directConnection=true so every spec-compliant driver behaves the same). Getters are pure — host and mapped port resolve once at start().
  • Every module pins its image and encodes the readiness probe that actually works there: pg_isready forced onto TCP (the postgres image's first boot runs a temporary socket-only server that log and socket probes misread as ready), Kafka's two-phase KRaft boot (the broker must advertise the mapped host port, which does not exist until after start), RabbitMQ's log-wait-before-any-exec order (an early exec as root bricks the Erlang cookie), MongoDB's rs.initiate + PRIMARY poll from a started hook.
  • A uniform curated pass-through surface on all seven — with_env / with_label / with_network / with_network_alias / with_reuse / with_startup_timeout (a per-phase budget) / with_startup_attempts — plus two escape hatches: with_customizer(fn), applied after the module's own rendering so it wins, and to_generic().
  • Opt-in by include + link: umbrella header testcontainers/modules.hpp, link target testcontainers::modules (its own Conan component; core-only consumers are untouched). One core rider: GenericImage::with_image re-points a configured builder at another image.

Per-module contracts and known limits are documented in docs/feature-notes.md.

Core additions

Wait strategies, host resolution, DSNs

  • wait_for::successful_command({argv...}) / wait_for::successful_shell_command("script") — the seventh wait strategy polls a deadline-bounded exec until an attempt exits 0. Runs on every transport (named pipe and TLS included); non-zero exits and daemon 409s mean "not ready yet", a 404 propagates; the timeout error reports the last completed attempt (exit code + output snippet).
  • One host-address rule behind Container::host(), the HTTP/port wait probes, compose service hosts, and Ryuk registration: TESTCONTAINERS_HOST_OVERRIDE / host.override wins; tcp:///https:// daemons hand out their hostname; a local socket/pipe means "localhost" — unless the test process itself runs inside a Linux container, where the docker-bridge gateway from /proc/net/route is returned. This is the DinD / Testcontainers-Cloud enabler.
  • ConnectionString — a public, render-only DSN builder (scheme://[user[:password]@]host[:port][/database][?k=v&…]) with per-component percent-encoding and IPv6 bracketing; the building block behind the module getters.

Streaming

  • Transfers no longer buffer payloads whole: copy_to_container sends the tar as a chunked upload with host files read in 64 KiB blocks, and GenericBuildableImage::build() streams the context through POST /build the same way (build_image gained a BodyProducer overload). The tar writer moved to pax(restricted), lifting USTAR's 100-char path and 8 GiB file caps.
  • Directory build-context sources honor .dockerignore with docker-build (moby/patternmatcher) semantics.
  • Batched copy_to_container(id, vector<CopyToContainer>) ships a request's whole copy set in one PUT — the runner's create→copy→start path now uses it.
  • Copy-from grew a raw-bytes sink overload, copy_from_container_to(id, path, dest_dir) — a streaming download + extraction, wire to disk, tar-slip protected (the Windows interior-drive-letter escape included) — and container_path_stat(id, path), a cheap HEAD existence/size probe.
  • set_max_response_body(bytes) caps what the buffered paths will hold: a runaway body throws a DockerError naming the cap instead of allocating without bound.

Exec

  • Deadline-bounded streaming exec: exec(cmd, opts, consumer, deadline)ExecStreamResult — why delivery ended (FollowEnd) plus the exit code, present only when the command actually finished (closing the attach stream does not kill it).
  • Stdin now interleaves with the output read on a full-duplex pump (64 KiB chunks), so a command echoing a multi-megabyte stdin back can no longer backpressure the write into a timeout.
  • TTY sizing: ExecOptions::console_size (initial rows × columns at create, API 1.42+), ExecOptions::on_started (hands out the exec id at the first resize-valid moment), and resize_exec / Container::resize_tty.
  • All exec overloads now run one implementation with one end-of-stream contract: any read-side end (EOF, closed pipe, daemon reset) is the peer finishing — output is kept and exec-inspect settles the exit code (the buffered path used to throw on a reset).

Networks & volumes

  • IPAM multi-pool create: repeatable Network::Builder::with_ipam_pool (subnet, IP range, gateway, auxiliary addresses), echoed back typed in NetworkInspect.
  • Network::Builder::with_reuse — find-or-create with the container-reuse semantics (an exact with_name is required; a same-name network with a different config throws instead of creating an ambiguous duplicate).
  • DockerClient::list_networks / list_volumes / prune_volumes — filters pass through verbatim; API 1.42+ daemons prune only anonymous unused volumes unless the {"all","true"} filter is given.
  • Volume::populate now seeds volumes on Windows daemons too: it stages into the created helper's layer, then copies onto the volume from inside as ContainerAdministrator (a Windows daemon extracts archive uploads into the layer, never through mounts).

Compose

  • with_profile (repeatable) activates compose profiles — emitted on up and the teardown down, so profile-gated services are stopped gracefully instead of stranded.
  • with_scale(service, n) plus per-instance accessors: get_service_port / get_service_container_id take an optional instance number, service_instances lists the running ones, and with_exposed_service can probe a specific instance.
  • Per-service logs: get_service_logs (snapshot) and follow_service_logs (blocking, or deadline-bounded returning FollowEnd) — identical behavior under the Local, Containerised, and Auto client modes.
  • with_ambassador(service, port) reaches a service port the YAML never publishes: one socat relay joined to the project network, an ephemeral host port per target, resolved transparently by get_service_port.
  • Compose stacks are crash-safe now: start() registers the com.docker.compose.project filter with Ryuk before up, so even a crash mid-up is swept.

Environment configuration

  • Every env switch gained a ~/.testcontainers.properties key (docker.host, docker.tls.verify, hub.image.name.prefix, host.override, ryuk.disabled, testcontainers.reuse.enable, …) behind one shared parser, read once per process; the file format is shared with testcontainers-java.
  • The active docker context's TLS store (~/.docker/contexts/tls/…) is consumed: a tcp://-spelled context endpoint dials TLS like the docker CLI, SkipTLSVerify is honored, and tcp:// + DOCKER_TLS_VERIFY upgrades to https (default port 2375 → 2376).
  • Every internal utility image is configurable for corporate mirrors — ryuk.container.image, sshd.container.image, socat.container.image, compose.container.image (env or properties) — and TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX now reaches all of them, not just GenericImage.
  • Age-based pull policy: with_image_pull_policy(std::chrono::seconds) re-pulls when the local image's Created timestamp is older than the budget (java PullPolicy.ageBased parity; caveat: Created is the image build time — docker records no pull time).

Lifecycle & reaping

  • Built images carry the session labels (BuildOptions::labels, sent as the ?labels= query), so Ryuk removes them with the rest of the session's resources; base images and pulled layers are untouched.
  • Per-daemon reaper and host-port-forwarder registries keyed by endpoint URL: a second daemon used in the same process boots its own Ryuk and sshd sidecar; compose project filters always reach the environment daemon's reaper.
  • Reuse-hash freshness: copy-to sources now contribute their content identity to the hash (bytes verbatim; path + per-file size+mtime for host sources), so editing a fixture in place no longer adopts the stale reused container.
  • Container::stop(timeout_secs) forwards the daemon grace period (unset = the create-time StopTimeout, 0 = kill now, negative = wait indefinitely), and `Conta...
Read more

v0.1.0

Choose a tag to compare

@cppudge cppudge released this 09 Jul 20:30

First tagged release. Native C++20 port of Testcontainers: spin up real Docker containers from your integration tests and tear them down automatically — a pure C++ client over the Docker Engine HTTP API. No docker CLI, no Rust bridge.

Highlights

  • GenericImage / GenericBuildableImage / Container / Network / Volume / DockerComposeContainer
  • Six wait strategies; exec (stdin / tty / streaming); copy to/from container; lifecycle hooks; container reuse
  • Ryuk crash-safety reaper; registry auth including credential helpers; image build from Dockerfile
  • Host-port exposure (with_exposed_host_port: sshd sidecar + SSH tunnel back to the host)
  • Transports: unix socket (Linux/macOS), named pipe (Windows), tcp:// / https:// (TLS) for remote daemons
  • First-class Windows containers: CI drives real Windows containers with mirror suites for build/volumes/networks/exec/copy/ports/waits/lifecycle

Quality gates

~300 unit + ~115 integration tests, green against real daemons in both engine modes (Linux and Windows containers). CI gates on -Wall -Wextra / /W4 as errors across gcc, clang, and MSVC, pinned clang-format + clang-tidy, an ASan+UBSan run of both suites, and CodeQL. A 100+-iteration stability soak of the integration suites on each engine passed with zero library failures.

Install

Conan 2 (in-repo recipe; ConanCenter submission is in progress):

conan create . --build=missing -s compiler.cppstd=20

CMake consumers: find_package(testcontainers CONFIG) and link testcontainers::testcontainers.

Requires C++20 (gcc ≥ 12, clang ≥ 15, MSVC ≥ 193, apple-clang ≥ 15). Dependencies: Boost (Beast/Asio, header-only usage), nlohmann_json, libarchive; optional OpenSSL (tls) and libssh2 (host_port_forwarding) — both features can be disabled to drop the dependencies entirely.

Known limits

Per-feature limits are documented in docs/feature-notes.md; the per-API engine-mode coverage matrix lives in docs/public-api-test-coverage.md. The main unverified path is end-to-end TLS against a real remote daemon. Shared builds are static-only on Windows (no symbol-export macros yet).

v0.1.0-alpha.1

v0.1.0-alpha.1 Pre-release
Pre-release

Choose a tag to compare

@cppudge cppudge released this 05 Jul 08:42

Second alpha of the native C++ Testcontainers port. Highlights since v0.1.0-alpha.0:

Docker client

  • Engine API version pinning — docker-CLI-style negotiation (min(1.44, daemon's Api-Version) from one unversioned GET /_ping); every typed client call now hits /v1.NN/... paths, so a Docker 29+ daemon that drops old API versions keeps working. The raw request() escape hatch stays unversioned by design.
  • Streaming log waitwait_for::log(...) rides ONE deadline-bounded follow_logs connection (history + live) instead of snapshot polling every 200 ms; occurrence counting is incremental and chunk-boundary-safe. New public follow_logs overload with a hard deadline (FollowEnd::{ConsumerStopped,StreamEnded,DeadlineExpired}).
  • Host port exposurewith_exposed_host_port makes services listening on the test host reachable from containers through an sshd sidecar + libssh2 remote-forward tunnel.
  • Exec now always hijacks with Upgrade: tcp (CLI parity); named-pipe transport treats a peer CloseWrite as EOF.

Packaging (new)

  • conan create . produces a consumer-grade package: test_package/ proves find_package + link + run; the recipe runs the unit suite unless tools.build:skip_test.
  • cmake --install ships a working testcontainersConfig.cmake (find_package(testcontainers)testcontainers::testcontainers) for non-Conan consumers.
  • The version lives in ONE place (TC_VERSION_FULL in CMakeLists.txt); shared is refused on Windows (no export macros yet).

Quality gates

  • CI grew clang-format / clang-tidy (pinned) / ASan+UBSan / CodeQL gates, warnings-as-errors on project targets, and a conan create job on Linux, Windows and macOS — apple-clang compiles and runs the unit suite on every push now.
  • The Windows-engine integration mirror covers the docs/07 matrix; hang dumps are captured automatically.

Full log: v0.1.0-alpha.0...v0.1.0-alpha.1

v0.1.0-alpha.0 — first public alpha

Pre-release

Choose a tag to compare

@cppudge cppudge released this 04 Jul 14:02

First public alpha of testcontainers-cpp — a native C++20 port of Testcontainers: spin up real Docker containers from your integration tests and tear them down automatically. No docker CLI, no Rust bridge — a pure C++ client talking to the Docker Engine HTTP API directly (Boost.Beast/Asio).

Highlights

  • Containers: GenericImageContainer lifecycle with typed config (env, ports, mounts, networks, labels, entrypoint, workdir/user, tty, host config patches), lifecycle hooks, startup retry, and container reuse.
  • Wait strategies: log, HTTP, health, port, exit, and composite waits with per-strategy deadlines.
  • Exec: stdin (HTTP 101 upgrade + half-close), tty, streaming output, exit codes.
  • Images: pull with registry auth (config.json incl. credential helpers), build from Dockerfile (GenericBuildableImage), image substitution.
  • Networks & volumes: create/inspect/remove, IPAM basics, volume seeding.
  • Copy: to/from container (tar over the API, no CLI).
  • Docker Compose: local (host CLI), containerised (docker:cli ambassador), and auto client modes; per-service wait + TCP probes.
  • Ryuk: crash-safe reaping via the standard testcontainers/ryuk sidecar.
  • Transports: Windows named pipe (message-mode half-close), unix socket, TCP, TLS (client certs; end-to-end TLS against a remote daemon is the main unverified gap).

Platforms & CI

Green on Windows (named pipe, MSVC) and Linux (unix socket, gcc): ~300 unit tests + ~65 integration tests against a real daemon. CI runs the full integration suite on Linux and the unit suite on Windows for every push.

Status

Alpha: the core is feature-complete, but the API may still change. Known limits per subsystem are documented in docs/06-feature-notes.md; the actionable backlog lives in TODO.md.

Licensed under MIT or Apache-2.0, at your option.