From 30e0be9da3c24e6ae704ed4b1ec1e7c415a98cd0 Mon Sep 17 00:00:00 2001 From: Hampus Avekvist Date: Sat, 9 Aug 2025 21:37:06 +0200 Subject: [PATCH 1/3] build: Add rudimentary Nix shell flake --- flake.lock | 27 +++++++++++++++++++++++++++ flake.nix | 24 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6005341 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1754498491, + "narHash": "sha256-erbiH2agUTD0Z30xcVSFcDHzkRvkRXOQ3lb887bcVrs=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "c2ae88e026f9525daf89587f3cbee584b92b6134", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..04527a2 --- /dev/null +++ b/flake.nix @@ -0,0 +1,24 @@ +{ + description = "Testcontainers Native"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + }; + + outputs = { self, nixpkgs }: let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + in { + devShells.${system}.default = pkgs.mkShell { + packages = with pkgs; [ + clang-tools + cmake + gcc15 + gdb + go + ninja + pre-commit + ]; + }; + }; +} From ccccd3a627fa9c257bcab282341c1491be3218ec Mon Sep 17 00:00:00 2001 From: Hampus Avekvist Date: Tue, 29 Jul 2025 18:53:35 +0200 Subject: [PATCH 2/3] docs: Rephrase sentences, and improve grammar and spelling --- CONTRIBUTING.md | 4 ++-- README.md | 12 ++++-------- docs/SUPPORT.md | 8 ++------ docs/architecture/README.md | 5 ++--- docs/c/README.md | 10 ++++------ docs/cpp/README.md | 10 +++++----- docs/getting-started.md | 11 +++++------ 7 files changed, 24 insertions(+), 36 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bbc49c1..38ea6e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ cmake -DSKIP_DEMOS=true . ## Contributing to the Documentation The documentation is structured in the MkDocs format and uses Material for MkDocs. -To develop the site in this repository, start it in the [Dev Containers](.devcontainer/README.md) +To develop the site in this repository, start it in a [Dev Container](.devcontainer/README.md) and use the following commands: ```shell @@ -57,4 +57,4 @@ mkdocs build - `tc_` is used as a prefix for all exported Testcontainers functions - When possible, we try to avoid special Golang types in public API and try to expose wrapper types - `const` is important for users, and please add it to your arguments when possible. - There is no Const in Golang, so some `typedef` injection is needed when importing CGo + There is no `const` in Golang, so some `typedef` injection is needed when importing CGo diff --git a/README.md b/README.md index 9f8a53e..44d4af3 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,8 @@ [![Stability: Experimental](https://masterminds.github.io/stability/experimental.svg)](https://masterminds.github.io/stability/experimental.html) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/oleg-nenashev/testcontainers-c)](https://github.com/oleg-nenashev/testcontainers-c/releases) -!!! warning - This is a prototype. - There is a lot to do before it can be distributed and used in production, see the GitHub Issues - and the [project roadmap](./ROADMAP.md) +> [!WARNING] +> This is a prototype. There is a lot to do before it can be distributed and used in production, see the GitHub Issues and the [project roadmap](./ROADMAP.md) This is not a standalone [Testcontainers](https://testcontainers.org/) engine, but a C-style shared library adapter for native languages like C/C++, D, Lua, Swift, etc. @@ -88,9 +86,7 @@ describes how it can be done in principle. ## Credits Using a complex Golang framework from C/C++ is not trivial. -Neither the CMake files are. -This project would not succeed without many quality articles -and help from the community. +Neither are the CMake files. This project would not succeed without many quality articles and help from the community. Kudos to: @@ -102,7 +98,7 @@ Kudos to: [An Adventure into CGO - Calling Go code with C](https://medium.com/@ben.mcclelland/an-adventure-into-cgo-calling-go-code-with-c-b20aa6637e75) - [Insu Jang](https://github.com/insujang) for [Implementing Kubernetes C++ Client Library using Go Client Library](https://insujang.github.io/2019-11-28/implementing-kubernetes-cpp-client-library) -- Infinite number of StackOverflow contributors +- An infinite number of StackOverflow contributors ## Discuss diff --git a/docs/SUPPORT.md b/docs/SUPPORT.md index 1cfbfac..7263e77 100644 --- a/docs/SUPPORT.md +++ b/docs/SUPPORT.md @@ -13,17 +13,13 @@ At the moment, this is single channel for all project matters. ## Raising Issues and Feature Requests -Use [GitHub Issues](https://github.com/testcontainers/testcontainers-c/issues). +Use [GitHub Issues](https://github.com/testcontainers/testcontainers-native/issues). Note that it may take some time to get a response, thanks for your patience. Contributions are always welcome, see the [Contributor Guide](../CONTRIBUTING.md). ## Reporting Security Issues -You can submit any security issue or suspected vulnerability -on [GitHub Security](https://github.com/testcontainers/testcontainers-c/security/advisories). -Please do NOT use public GitHub Issues for reporting vulnerabilities. - -Read More - [Security Policy](./SECURITY.md). +See the [Security Policy](./SECURITY.md). ## Commercial Support and Customization diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 73fa218..1476f99 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -1,8 +1,7 @@ # Architecture -!!! note - This section is coming soon. - All contributions are welcome, just submit a pull request! +> [!NOTE] +> This section is coming soon. All contributions are welcome, just submit a pull request! ## Build Process diff --git a/docs/c/README.md b/docs/c/README.md index b6a29de..33ca908 100644 --- a/docs/c/README.md +++ b/docs/c/README.md @@ -3,9 +3,8 @@ You can use the `testcontainers-c` library with common C unit testing frameworks and, soon, with package managers. -!!! note - This section is coming soon. - All contributions are welcome, just submit a pull request! +> [!NOTE] +> This section is coming soon. All contributions are welcome, just submit a pull request! ## Installing the library @@ -39,9 +38,8 @@ CPMAddPackage( ## Using the Library -!!! note - More frameworks will be documented soon. - All contributions are welcome, just submit a pull request! +> [!NOTE] +> More frameworks will be documented soon. All contributions are welcome, just submit a pull request! ### CMake Tests (CTest) diff --git a/docs/cpp/README.md b/docs/cpp/README.md index c0913ed..43ffe30 100644 --- a/docs/cpp/README.md +++ b/docs/cpp/README.md @@ -2,7 +2,7 @@ At the moment, there is no dedicated C++ binding library/header, but it is on [our roadmap](../../ROADMAP.md). -Tou can use the `testcontainers-c` library directly +You can use the `testcontainers-c` library directly in all C++ testing frameworks. ## Google Test @@ -29,10 +29,10 @@ protected: tc_with_exposed_tcp_port(requestId, 8080); tc_with_wait_for_http(requestId, 8080, WIREMOCK_ADMIN_MAPPING_ENDPOINT); tc_with_file(requestId, "test_data/hello.json", "/home/wiremock/mappings/hello.json"); - + struct tc_run_container_return ret = tc_run_container(requestId); containerId = ret.r0; - + EXPECT_TRUE(ret.r1) << "Failed to run the container: " << ret.r2; }; @@ -51,10 +51,10 @@ Then, you can define new tests by referring to the container via `containerId`. TEST_F(WireMockTestContainer, HelloWorld) { std::cout << "Sending HTTP request to the container\n"; struct tc_send_http_get_return response = tc_send_http_get(containerId, 8080, "/hello"); - + ASSERT_NE(response.r0, -1) << "Failed to send HTTP request: " << response.r2; ASSERT_EQ(response.r0, 200) << "Received wrong response code: " << response.r1 << response.r2; - + std::cout << "Server Response: HTTP-" << response.r0 << '\n' << response.r1 << '\n'; } ``` diff --git a/docs/getting-started.md b/docs/getting-started.md index ded08ff..654c245 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,13 +1,12 @@ # Getting Started with Testcontainers for C/C++ -In this section, we will build a demo C application that uses Testcontainers -in a simple C application -for deploying a [WireMock](https://wiremock.org/) API server, -sends a simple HTTP request to this service, +In this section, we will build a simple demo C application that uses Testcontainers +for deploying a [WireMock](https://wiremock.org/) API server. +It sends a simple HTTP request to this service, and verifies the response. -We will not be using any C/C++ test framework for that. +We will not be using any test framework for that. -For test framework framework examples, see the [demos](../demo/README.md). +For test framework examples, see the [demos](../demo/README.md). ## Build the Project From f6912625cea6c1a8ca478c5d55ff5e26c09400d8 Mon Sep 17 00:00:00 2001 From: Hampus Avekvist Date: Sun, 10 Aug 2025 03:10:49 +0200 Subject: [PATCH 3/3] feat: Implement initial C++ library --- CMakeLists.txt | 1 + demo/CMakeLists.txt | 1 + demo/google-test-cpp/CMakeLists.txt | 32 +++ demo/google-test-cpp/README.md | 13 + demo/google-test-cpp/test.cpp | 54 +++++ demo/google-test-cpp/test_data/hello.json | 12 + .../hello_with_missing_resource.json | 10 + .../test_data/hello_with_resource.json | 10 + demo/google-test-cpp/test_data/response.xml | 6 + testcontainers-cpp/CMakeLists.txt | 23 ++ testcontainers-cpp/cmake.pc.in | 0 testcontainers-cpp/testcontainers.hpp | 222 ++++++++++++++++++ 12 files changed, 384 insertions(+) create mode 100644 demo/google-test-cpp/CMakeLists.txt create mode 100644 demo/google-test-cpp/README.md create mode 100644 demo/google-test-cpp/test.cpp create mode 100644 demo/google-test-cpp/test_data/hello.json create mode 100644 demo/google-test-cpp/test_data/hello_with_missing_resource.json create mode 100644 demo/google-test-cpp/test_data/hello_with_resource.json create mode 100644 demo/google-test-cpp/test_data/response.xml create mode 100644 testcontainers-cpp/CMakeLists.txt create mode 100644 testcontainers-cpp/cmake.pc.in create mode 100644 testcontainers-cpp/testcontainers.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 14a4c5e..10a39b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,7 @@ include(CTest) add_subdirectory(testcontainers-bridge) add_subdirectory(testcontainers-c) +add_subdirectory(testcontainers-cpp) add_subdirectory(modules) if(NOT DEFINED SKIP_DEMOS) add_subdirectory(demo) diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt index 3b46c2c..d516a0c 100644 --- a/demo/CMakeLists.txt +++ b/demo/CMakeLists.txt @@ -2,3 +2,4 @@ add_subdirectory(generic-container) add_subdirectory(wiremock) add_subdirectory(google-test) +add_subdirectory(google-test-cpp) diff --git a/demo/google-test-cpp/CMakeLists.txt b/demo/google-test-cpp/CMakeLists.txt new file mode 100644 index 0000000..1e4a440 --- /dev/null +++ b/demo/google-test-cpp/CMakeLists.txt @@ -0,0 +1,32 @@ +# Google Test demo +# This is based on https://google.github.io/googletest/quickstart-cmake.html +cmake_minimum_required (VERSION 3.26) +project (google-test-cpp-demo + VERSION 0.1.0 + DESCRIPTION "Demonstrates usage of Testcontainers C++ in Google Test" + LANGUAGES CXX +) + +set(TARGET_OUT ${PROJECT_NAME}.out) + +# GoogleTest requires at least C++14 +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +include(FetchContent) +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.17.0 +) +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +enable_testing() +file(COPY test_data DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) + +add_executable(${TARGET_OUT} test.cpp) +target_link_libraries(${TARGET_OUT} PRIVATE testcontainers-cpp) +target_link_libraries(${TARGET_OUT} PRIVATE GTest::gtest_main) + +include(GoogleTest) +gtest_discover_tests(${TARGET_OUT}) diff --git a/demo/google-test-cpp/README.md b/demo/google-test-cpp/README.md new file mode 100644 index 0000000..f5b9bd5 --- /dev/null +++ b/demo/google-test-cpp/README.md @@ -0,0 +1,13 @@ +# Using Testcontainers C++ in Google Test + +Demonstrates usage of Testcontainers C++ in [Google +Test](https://github.com/google/googletest). See +[test.cpp](./test.cpp) for the code. + +## Run the demo + +```bash +cmake -S . -B /tmp/tc-native +cmake --build /tmp/tc-native/ +ctest --output-on-failure -R Class +``` diff --git a/demo/google-test-cpp/test.cpp b/demo/google-test-cpp/test.cpp new file mode 100644 index 0000000..13f1b70 --- /dev/null +++ b/demo/google-test-cpp/test.cpp @@ -0,0 +1,54 @@ +#include + +#include +#include + +#include "testcontainers.hpp" + +using namespace testcontainers; + +class WireMockTestContainerClass : public ::testing::Test { + const char* WIREMOCK_IMAGE = "wiremock/wiremock:3.0.1-1"; + const char* WIREMOCK_ADMIN_MAPPING_ENDPOINT = "/__admin/mappings"; + +protected: + void SetUp() override { + using namespace std::literals; + + builder.expose_port(TcpPort{8080}).wait_for_http(TcpPort{8080}, WIREMOCK_ADMIN_MAPPING_ENDPOINT); + } + + Container::Builder builder = Container::Builder{WIREMOCK_IMAGE}; +}; + +/// This test runs a "Hello World" example. +TEST_F(WireMockTestContainerClass, HelloWorld) { + auto container = builder.with_file("test_data/hello.json", "/home/wiremock/mappings/hello.json").build(); + ASSERT_TRUE(container.has_value()) << "Failed to run the container"; + + auto [code, response] = container->send_http(HttpMethod::Get, TcpPort{8080}, "/hello"); + ASSERT_EQ(code, 200) << "Expected 200 OK. Received response: " << response; +} + +/// This test responds to an HTTP request with response from a +/// prepared file. +TEST_F(WireMockTestContainerClass, HelloWorldFromResource) { + auto container = builder.with_file("test_data/hello_with_resource.json", "/home/wiremock/mappings/hello2.json") + .with_file("test_data/response.xml", "/home/wiremock/__files/response.xml") + .build(); + ASSERT_TRUE(container.has_value()) << "Failed to run the container"; + + auto [code, response] = container->send_http(HttpMethod::Get, TcpPort{8080}, "/hello-from-resource"); + ASSERT_EQ(code, 200) << "Expected 200 OK. Received response: " << response; +} + +/// This test performs a request that is guaranteed to fail and +/// return an HTTP-500. +TEST_F(WireMockTestContainerClass, HelloWorldFromMissingResource) { + auto container = + builder.with_file("test_data/hello_with_missing_resource.json", "/home/wiremock/mappings/hello3.json").build(); + ASSERT_TRUE(container.has_value()) << "Failed to run the container"; + + auto [code, response] = container->send_http(HttpMethod::Get, TcpPort{8080}, "/hello-from-missing-resource"); + ASSERT_EQ(code, 500) << "Expected 500 Internal Server Error. Received response: " << response; +} diff --git a/demo/google-test-cpp/test_data/hello.json b/demo/google-test-cpp/test_data/hello.json new file mode 100644 index 0000000..0525333 --- /dev/null +++ b/demo/google-test-cpp/test_data/hello.json @@ -0,0 +1,12 @@ +{ + "request": { + "method": "GET", + "url": "/hello" + }, + + "response": { + "status": 200, + "body": "Hello, world!" + } + } + \ No newline at end of file diff --git a/demo/google-test-cpp/test_data/hello_with_missing_resource.json b/demo/google-test-cpp/test_data/hello_with_missing_resource.json new file mode 100644 index 0000000..cf6abd2 --- /dev/null +++ b/demo/google-test-cpp/test_data/hello_with_missing_resource.json @@ -0,0 +1,10 @@ +{ + "request": { + "method": "GET", + "url": "/hello-from-missing-resource" + }, + "response": { + "status": 200, + "bodyFileName": "response_missing.xml" + } +} diff --git a/demo/google-test-cpp/test_data/hello_with_resource.json b/demo/google-test-cpp/test_data/hello_with_resource.json new file mode 100644 index 0000000..afe6f7e --- /dev/null +++ b/demo/google-test-cpp/test_data/hello_with_resource.json @@ -0,0 +1,10 @@ +{ + "request": { + "method": "GET", + "url": "/hello-from-resource" + }, + "response": { + "status": 200, + "bodyFileName": "response.xml" + } +} diff --git a/demo/google-test-cpp/test_data/response.xml b/demo/google-test-cpp/test_data/response.xml new file mode 100644 index 0000000..33c5aef --- /dev/null +++ b/demo/google-test-cpp/test_data/response.xml @@ -0,0 +1,6 @@ + + you + WireMock + Response + Hello, world! + diff --git a/testcontainers-cpp/CMakeLists.txt b/testcontainers-cpp/CMakeLists.txt new file mode 100644 index 0000000..23be3ce --- /dev/null +++ b/testcontainers-cpp/CMakeLists.txt @@ -0,0 +1,23 @@ +set(TARGET testcontainers-cpp) +set(TARGET_NAME ${TARGET}) +set(TARGET_DESCRIPTION "Testcontainer library for C++") +set(TARGET_VERSION ${PROJECT_VERSION}) + +add_library(${TARGET} INTERFACE) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +target_sources(${TARGET} + PUBLIC FILE_SET HEADERS + BASE_DIRS . + FILES testcontainers.hpp +) + +target_link_libraries(${TARGET} INTERFACE testcontainers-c) + +configure_file(cmake.pc.in ${TARGET}.pc @ONLY) +install(TARGETS ${TARGET} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(FILES ${CMAKE_BINARY_DIR}/${TARGET}.pc DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig) diff --git a/testcontainers-cpp/cmake.pc.in b/testcontainers-cpp/cmake.pc.in new file mode 100644 index 0000000..e69de29 diff --git a/testcontainers-cpp/testcontainers.hpp b/testcontainers-cpp/testcontainers.hpp new file mode 100644 index 0000000..b73cedb --- /dev/null +++ b/testcontainers-cpp/testcontainers.hpp @@ -0,0 +1,222 @@ +#ifndef TESTCONTAINERS_HPP +#define TESTCONTAINERS_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace testcontainers { + +namespace { +extern "C" { +#include "testcontainers-c/container.h" +} +} + +struct TcpPort { + uint16_t underlying; +}; + +enum struct HttpMethod { + Get, + Post, + Head, +}; + +class Builder; + +auto materialize(const auto& in) { +#if __cplusplus >= 202300L + return std::ranges::to(in | std::ranges::views::filter([](char c) { return c != 0x00; })); +#else + auto out = std::string{}; + std::ranges::for_each(in | std::ranges::views::filter([](char c) { return c != 0x00; }), [&](char c) { out += c; }); + return out; +#endif +} + +class Container { +public: + /// A `Container` cannot be explicitly created; use the + /// `Container::Builder` instead. + constexpr Container() = delete; + + /// A `Container` cannot be copied. + explicit constexpr Container(const Container&) = delete; + + /// A `Container` cannot be copied. + constexpr Container& operator=(const Container&) = delete; + + explicit constexpr Container(Container&& other) noexcept : container_id(-1) { *this = std::move(other); } + + constexpr Container& operator=(Container&& other) noexcept { + if (this == &other) { + return *this; + } + + std::swap(container_id, other.container_id); + + return *this; + } + + constexpr ~Container() { + if (container_id != -1) { + tc_container_terminate(container_id); + } + } + + /// Send an HTTP request, type denoted by `method`, to the + /// container. Perform the request on port `port` and to + /// endpoint `endpoint`. + std::pair send_http(HttpMethod method, TcpPort port, const std::filesystem::path& endpoint) { + switch (method) { + case HttpMethod::Get: + return send_http_get(port, endpoint); + case HttpMethod::Post: + return {0, "unimplemented"}; + case HttpMethod::Head: + return {0, "unimplemented"}; + } + + std::unreachable(); + } + +private: + /// The `Configuration` structure holds all expected + /// configuration related to the instantiation of a `Container` + /// instance. + struct Configuration { + using Endpoint = std::filesystem::path; + using Http = std::pair; + using Timeout = std::chrono::milliseconds; + using WaitFor = std::variant; + + int request_id = -1; + WaitFor wait_for_start = std::chrono::milliseconds{1000}; + }; + + /// The `Awaiter` is for different types of waiting. It may be + /// visited (with `std::visit`) when used on e.g. a + /// `Container::Configuration::WaitFor` instance. + struct Awaiter { + explicit constexpr Awaiter(int container_id) : container_id(container_id) {} + + void operator()(const Configuration::Timeout& timeout) { std::this_thread::sleep_for(timeout); } + + void operator()(const Configuration::Http& http) { + const auto& [port, endpoint] = http; + tc_container_with_wait_for_http(container_id, port.underlying, endpoint.c_str()); + } + + private: + int container_id; + }; + + /// Instantiate a container, and optionally wait for it to be + /// ready. + explicit constexpr Container(Configuration configuration) { + char* error = nullptr; + + // Awaiting HTTP-events must occur before the container is + // running. + if (std::holds_alternative(configuration.wait_for_start)) { + Awaiter{container_id}(std::get(configuration.wait_for_start)); + } + + if (container_id = tc_container_run(configuration.request_id, error); container_id == -1) { + throw std::runtime_error{error}; + } + + // Awaiting a timeout must occur after a container is + // running. + if (std::holds_alternative(configuration.wait_for_start)) { + Awaiter{container_id}(std::get(configuration.wait_for_start)); + } + } + + /// Send an HTTP GET request to the container, on port `port` + /// and with endpoint `endpoint`. + constexpr std::pair send_http_get(TcpPort port, const std::filesystem::path& endpoint) const { + static constexpr size_t response_size = 0xFFFF; + static constexpr size_t error_size = 0xFFFF; + auto response_body_raw = std::array{0x00}; + auto error_raw = std::array{0x00}; + + auto code = tc_container_send_http_get(container_id, port.underlying, endpoint.c_str(), + response_body_raw.data(), error_raw.data()); + + if (code == -1) { + auto error = materialize(error_raw); + return std::pair{code, error}; + } + + auto response_body = materialize(response_body_raw); + return std::pair{code, response_body}; + } + + int container_id; + +public: + /// `Builder` is the mechanism to instantiate `Container`s. It + /// serves to configure the setup, and provides an interface to + /// simply check for errors upon container creation. + class Builder { + public: + constexpr Builder(const std::string& image) + : configuration(Container::Configuration{.request_id = tc_container_create(image.c_str())}) {} + + /// Add a file from host `source` to container path + /// `destination`. + Builder with_file(std::filesystem::path source, std::filesystem::path destination) { + tc_container_with_file(configuration.request_id, source.c_str(), destination.c_str()); + return *this; + } + + /// Expose a TCP port out from the container. + Builder expose_port(TcpPort port) { + tc_container_with_exposed_tcp_port(configuration.request_id, port.underlying); + return *this; + } + + /// Add a timeout duration that's waited-for during + /// container startup. + Builder wait_for(std::chrono::milliseconds duration) { + configuration.wait_for_start = duration; + return *this; + } + + /// Add an endpoint to act as health-check, determining if + /// the container is ready for requests. + Builder wait_for_http(TcpPort port, std::filesystem::path endpoint) { + configuration.wait_for_start = std::pair{port, endpoint}; + return *this; + } + + /// Construct a `Container` instance with the configured + /// options. + std::optional build() { + try { + return std::make_optional(Container{configuration}); + } catch (const std::runtime_error& exception) { + return std::nullopt; + } + } + + private: + Configuration configuration; + }; +}; + +} + +#endif // !TESTCONTAINERS_HPP