diff --git a/CMakeLists.txt b/CMakeLists.txt index b6183a0..10a39b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,17 @@ -cmake_minimum_required (VERSION 3.16.3) -project (TESTCONTAINERS-C) - -include(CTest) - -include_directories(${CMAKE_CURRENT_BINARY_DIR}/testcontainers-c) - -add_subdirectory(testcontainers-c) -add_subdirectory(modules) -if(NOT DEFINED SKIP_DEMOS) - add_subdirectory(demo) -endif() +cmake_minimum_required (VERSION 3.26) +project (TESTCONTAINERS-C + VERSION 0.1.0 + DESCRIPTION "Testcontainers for C and other native languages" + LANGUAGES C CXX +) + +include(GNUInstallDirs) +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) +endif() 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/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/generic-container/CMakeLists.txt b/demo/generic-container/CMakeLists.txt index 7559e5f..716c9bd 100644 --- a/demo/generic-container/CMakeLists.txt +++ b/demo/generic-container/CMakeLists.txt @@ -1,15 +1,15 @@ -project(testcontainers-c-generic-container-demo - VERSION 0.0.1 - DESCRIPTION "Demonstrates usage of the generic container API in a simple main app") +cmake_minimum_required (VERSION 3.26) +project (generic-container-demo + VERSION 0.1.0 + DESCRIPTION "Demonstrates usage of the generic container API in a simple main app" + LANGUAGES C +) -set(TARGET_OUT demo_generic_container.out) +set(TARGET_OUT ${PROJECT_NAME}.out) -include_directories(${testcontainers-c_SOURCE_DIR}) -# WORKING_DIRECTORY breaks shared lib loading file(COPY test_data DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) # Vanilla Demo for WireMock add_executable(${TARGET_OUT} generic_container_demo.c) -add_dependencies(${TARGET_OUT} testcontainers-c-shim) target_link_libraries(${TARGET_OUT} PRIVATE testcontainers-c) add_test(NAME generic_container_demo COMMAND ${TARGET_OUT}) diff --git a/demo/generic-container/generic_container_demo.c b/demo/generic-container/generic_container_demo.c index ff600f4..417f840 100644 --- a/demo/generic-container/generic_container_demo.c +++ b/demo/generic-container/generic_container_demo.c @@ -1,5 +1,5 @@ #include -#include "testcontainers-c.h" +#include "testcontainers-c/container.h" #define DEFAULT_IMAGE "wiremock/wiremock:3.0.1-1" @@ -7,28 +7,29 @@ int main() { printf("Using WireMock with the Testcontainers C binding:\n"); printf("Creating new container: %s\n", DEFAULT_IMAGE); - int requestId = tc_new_container_request(DEFAULT_IMAGE); - tc_with_exposed_tcp_port(requestId, 8080); - tc_with_wait_for_http(requestId, 8080, "/__admin/mappings"); - tc_with_file(requestId, "test_data/hello.json", "/home/wiremock/mappings/hello.json"); - struct tc_run_container_return ret = tc_run_container(requestId); - int containerId = ret.r0; - if (!ret.r1) { - printf("Failed to run the container: %s\n", ret.r2); + int requestId = tc_container_create(DEFAULT_IMAGE); + tc_container_with_exposed_tcp_port(requestId, 8080); + tc_container_with_wait_for_http(requestId, 8080, "/__admin/mappings"); + tc_container_with_file(requestId, "test_data/hello.json", "/home/wiremock/mappings/hello.json"); + char* error; + int containerId = tc_container_run(requestId, error); + if (containerId == -1) { + printf("Failed to run the container: %s\n", error); return -1; } printf("Sending HTTP request to the container\n"); - struct tc_send_http_get_return response = tc_send_http_get(containerId, 8080, "/hello"); - if (response.r0 == -1) { - printf("Failed to send HTTP request: %s\n", response.r2); + char *response_body; + int response_code = tc_container_send_http_get(containerId, 8080, "/hello", response_body, error); + if (response_code == -1) { + printf("Failed to send HTTP request: %s\n", error); return -1; } - if (response.r0 != 200) { - printf("Received wrong response code: %d instead of %d\n%s\n%s\n", response.r0, 200, response.r1, response.r2); + if (response_code != 200) { + printf("Received wrong response code: %d instead of %d\n%s\n%s\n", response_code, 200, response_body, error); return -1; } - printf("Server Response: HTTP-%d\n%s\n\n", response.r0, response.r1); + printf("Server Response: HTTP-%d\n%s\n\n", response_code, response_body); return 0; } 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..55b9706 --- /dev/null +++ b/demo/google-test-cpp/README.md @@ -0,0 +1,28 @@ +# 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 . +cmake --build . +cd demo/google-test +ctest --output-on-failure +``` + +## Sample output + +```shell +onenashev:~/testcontainers-c/demo/google-test$ ctest --output-on-failure +Test project /home/onenashev/testcontainers-c/demo/google-test + Start 1: WireMockTestContainer.HelloWorld +1/5 Test #1: WireMockTestContainer.HelloWorld ...................... Passed 3.31 sec + Start 2: WireMockTestContainer.HelloWorldFromResource +2/5 Test #2: WireMockTestContainer.HelloWorldFromResource .......... Passed 3.97 sec + Start 3: WireMockTestContainer.HelloWorldFromMissingResource +3/5 Test #3: WireMockTestContainer.HelloWorldFromMissingResource ... Passed 3.91 sec + +100% tests passed, 0 tests failed out of 3 +``` diff --git a/demo/google-test-cpp/test.cpp b/demo/google-test-cpp/test.cpp new file mode 100644 index 0000000..58eb5ba --- /dev/null +++ b/demo/google-test-cpp/test.cpp @@ -0,0 +1,61 @@ +#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 { + std::cout << "Creating new container: " << WIREMOCK_IMAGE << '\n'; + + container = std::make_unique(WIREMOCK_IMAGE); + container->expose_port(TcpPort{8080}); + container->wait_for_http(TcpPort{8080}, WIREMOCK_ADMIN_MAPPING_ENDPOINT); + container->with_file("test_data/hello.json", "/home/wiremock/mappings/hello.json"); + container->with_file("test_data/hello_with_resource.json", "/home/wiremock/mappings/hello2.json"); + container->with_file("test_data/hello_with_missing_resource.json", "/home/wiremock/mappings/hello3.json"); + container->with_file("test_data/response.xml", "/home/wiremock/__files/response.xml"); + + auto result = container->run(); + + EXPECT_TRUE(!result.has_value()) << "Failed to run the container: " << result.error(); + }; + + std::unique_ptr container; +}; + +TEST_F(WireMockTestContainerClass, HelloWorld) { + std::cout << "Sending HTTP request to the container\n"; + + auto [code, response] = container->send_http(HttpMethod::Get, TcpPort{8080}, "/hello"); + + ASSERT_TRUE(response.has_value()) << "Failed to send HTTP request: " << response.error(); + ASSERT_EQ(code, 200) << "Received wrong response code: " << response.error(); + + std::cout << "Server Response: HTTP-" << code << '\n' << response.value() << '\n'; +} + +TEST_F(WireMockTestContainerClass, HelloWorldFromResource) { + std::cout << "Sending HTTP request to the container\n"; + + auto [code, response] = container->send_http(HttpMethod::Get, TcpPort{8080}, "/hello-from-resource"); + + ASSERT_TRUE(response.has_value()) << "Failed to send HTTP request: " << response.error(); + ASSERT_EQ(code, 200) << "Received wrong response code: " << response.error(); + + std::cout << "Server Response: HTTP-" << code << '\n' << response.value() << '\n'; +} + +TEST_F(WireMockTestContainerClass, HelloWorldFromMissingResource) { + std::cout << "Sending HTTP request to the container\n"; + + auto [code, response] = container->send_http(HttpMethod::Get, TcpPort{8080}, "/hello-from-missing-resource"); + + ASSERT_EQ(code, 500) << "The request should have failed"; +} 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/demo/google-test/CMakeLists.txt b/demo/google-test/CMakeLists.txt index ede0901..4d0bd09 100644 --- a/demo/google-test/CMakeLists.txt +++ b/demo/google-test/CMakeLists.txt @@ -1,8 +1,11 @@ # Google Test demo # This is based on https://google.github.io/googletest/quickstart-cmake.html -project(google-test-demo - VERSION 0.0.1 - DESCRIPTION "Demonstrates usage of Testcontainers C in Google Test") +cmake_minimum_required (VERSION 3.26) +project (google-test-demo + VERSION 0.1.0 + DESCRIPTION "Demonstrates usage of Testcontainers C in Google Test" + LANGUAGES CXX +) set(TARGET_OUT ${PROJECT_NAME}.out) @@ -12,7 +15,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) include(FetchContent) FetchContent_Declare( googletest - URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip + 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) @@ -21,7 +25,6 @@ enable_testing() file(COPY test_data DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) add_executable(${TARGET_OUT} test.cpp) -add_dependencies(${TARGET_OUT} testcontainers-c-shim) target_link_libraries(${TARGET_OUT} PRIVATE testcontainers-c) target_link_libraries(${TARGET_OUT} PRIVATE GTest::gtest_main) diff --git a/demo/google-test/test.cpp b/demo/google-test/test.cpp index ae87581..56782c3 100644 --- a/demo/google-test/test.cpp +++ b/demo/google-test/test.cpp @@ -1,7 +1,10 @@ #include #include #include -#include "testcontainers-c.h" + +extern "C" { +#include "testcontainers-c/container.h" +} class WireMockTestContainer : public ::testing::Test { @@ -11,23 +14,23 @@ const char* WIREMOCK_ADMIN_MAPPING_ENDPOINT = "/__admin/mappings"; protected: void SetUp() override { std::cout << "Creating new container: " << WIREMOCK_IMAGE << '\n'; - - int requestId = tc_new_container_request(WIREMOCK_IMAGE); - 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"); - tc_with_file(requestId, "test_data/hello_with_resource.json", "/home/wiremock/mappings/hello2.json"); - tc_with_file(requestId, "test_data/hello_with_missing_resource.json", "/home/wiremock/mappings/hello3.json"); - tc_with_file(requestId, "test_data/response.xml", "/home/wiremock/__files/response.xml"); - - struct tc_run_container_return ret = tc_run_container(requestId); - containerId = ret.r0; - - EXPECT_TRUE(ret.r1) << "Failed to run the container: " << ret.r2; + + int requestId = tc_container_create(WIREMOCK_IMAGE); + tc_container_with_exposed_tcp_port(requestId, 8080); + tc_container_with_wait_for_http(requestId, 8080, WIREMOCK_ADMIN_MAPPING_ENDPOINT); + tc_container_with_file(requestId, "test_data/hello.json", "/home/wiremock/mappings/hello.json"); + tc_container_with_file(requestId, "test_data/hello_with_resource.json", "/home/wiremock/mappings/hello2.json"); + tc_container_with_file(requestId, "test_data/hello_with_missing_resource.json", "/home/wiremock/mappings/hello3.json"); + tc_container_with_file(requestId, "test_data/response.xml", "/home/wiremock/__files/response.xml"); + + char* error; + int containerId = tc_container_run(requestId, error); + + EXPECT_TRUE(containerId != -1) << "Failed to run the container: " << error; }; void TearDown() override { - char* error = tc_terminate_container(containerId); + char* error = tc_container_terminate(containerId); ASSERT_EQ(error, nullptr) << "Failed to terminate the container after the test: " << error; }; @@ -36,32 +39,33 @@ const char* WIREMOCK_ADMIN_MAPPING_ENDPOINT = "/__admin/mappings"; 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'; + char *response_body; + char *error; + int response_code = tc_container_send_http_get(containerId, 8080, "/hello", response_body, error); + + ASSERT_NE(response_code, -1) << "Failed to send HTTP request: " << error; + ASSERT_EQ(response_code, 200) << "Received wrong response code: " << response_body << error; + + std::cout << "Server Response: HTTP-" << response_code << '\n' << response_body << '\n'; } TEST_F(WireMockTestContainer, HelloWorldFromResource) { std::cout << "Sending HTTP request to the container\n"; - struct tc_send_http_get_return response = tc_send_http_get(containerId, 8080, "/hello-from-resource"); - - 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'; + char *response_body; + char *error; + int response_code = tc_container_send_http_get(containerId, 8080, "/hello-from-resource", response_body, error); + + ASSERT_NE(response_code, -1) << "Failed to send HTTP request: " << error; + ASSERT_EQ(response_code, 200) << "Received wrong response code: " << response_body << error; + + std::cout << "Server Response: HTTP-" << response_code << '\n' << response_body << '\n'; } TEST_F(WireMockTestContainer, HelloWorldFromMissingResource) { std::cout << "Sending HTTP request to the container\n"; - struct tc_send_http_get_return response = tc_send_http_get(containerId, 8080, "/hello-from-missing-resource"); - - ASSERT_EQ(response.r0, 500) << "The request should have failed"; -} + char *response_body; + char *error; + int response_code = tc_container_send_http_get(containerId, 8080, "/hello-from-missing-resource", response_body, error); -int main(int argc, char **argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); + ASSERT_EQ(response_code, 500) << "The request should have failed"; } diff --git a/demo/wiremock/CMakeLists.txt b/demo/wiremock/CMakeLists.txt index aa232ba..6f861c8 100644 --- a/demo/wiremock/CMakeLists.txt +++ b/demo/wiremock/CMakeLists.txt @@ -1,17 +1,16 @@ -project(testcontainers-c-wiremock-demo - VERSION 0.0.1 - DESCRIPTION "Demonstrates usage of the WireMock module for Testcontainers C in a simple main app") - -set(TARGET_OUT demo_wiremock_module.out) - -include_directories(${testcontainers-c_SOURCE_DIR}) -# WORKING_DIRECTORY breaks shared lib loading -file(COPY test_data DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) - -# WireMock Module demo -add_executable(${TARGET_OUT} wiremock_module_demo.c) -add_dependencies(${TARGET_OUT} testcontainers-c-shim) -target_include_directories(${TARGET_OUT} PRIVATE ${testcontainers-c-wiremock_SOURCE_DIR}) -target_link_libraries(${TARGET_OUT} PRIVATE testcontainers-c) -target_link_libraries(${TARGET_OUT} PRIVATE testcontainers-c-wiremock) -add_test(NAME wiremock_module_demo COMMAND ${TARGET_OUT}) +cmake_minimum_required (VERSION 3.26) +project (wiremock-demo + VERSION 0.1.0 + DESCRIPTION "Demonstrates usage of the WireMock module for Testcontainers C in a simple main app" + LANGUAGES C +) + +set(TARGET_OUT demo_wiremock_module.out) + +file(COPY test_data DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) + +# WireMock Module demo +add_executable(${TARGET_OUT} wiremock_module_demo.c) +target_link_libraries(${TARGET_OUT} PRIVATE testcontainers-c) +target_link_libraries(${TARGET_OUT} PRIVATE testcontainers-c-wiremock) +add_test(NAME wiremock_module_demo COMMAND ${TARGET_OUT}) diff --git a/demo/wiremock/wiremock_module_demo.c b/demo/wiremock/wiremock_module_demo.c index 9e70598..740de8d 100644 --- a/demo/wiremock/wiremock_module_demo.c +++ b/demo/wiremock/wiremock_module_demo.c @@ -1,6 +1,7 @@ #include #include #include "testcontainers-c-wiremock.h" +#include "testcontainers-c/container.h" int main() { printf("Using WireMock with the Testcontainers C binding:\n"); @@ -8,14 +9,14 @@ int main() { printf("Creating new container: %s\n", DEFAULT_WIREMOCK_IMAGE); int requestId = tc_wm_new_default_container(); //FIXME: This method is bogus - tc_wm_with_mapping(requestId, "test_data/hello.json", "hello"); - tc_with_file(requestId, "test_data/hello.json", "/home/wiremock/mappings/hello2.json"); - struct tc_run_container_return ret = tc_run_container(requestId); - int containerId = ret.r0; - if (!ret.r1) { - printf("Failed to run the container: %s\n", ret.r2); + // tc_wm_with_mapping(requestId, "test_data/hello.json", "hello"); + tc_container_with_file(requestId, "test_data/hello.json", "/home/wiremock/mappings/hello2.json"); + char* error; + int containerId = tc_container_run(requestId, error); + if (containerId == -1) { + printf("Failed to run the container: %s\n", error); if (containerId != -1) { // Print container log - char* log = tc_get_container_log(containerId); + char* log = tc_container_get_log(containerId); if (log != NULL) { printf("\n%s\n", log); } @@ -33,16 +34,17 @@ int main() { } printf("Sending HTTP request to the container\n"); - struct tc_send_http_get_return response = tc_send_http_get(containerId, 8080, "/hello"); - if (response.r0 == -1) { - printf("Failed to send HTTP request: %s\n", response.r2); + char *response_body; + int response_code = tc_container_send_http_get(containerId, 8080, "/hello", response_body, error); + if (response_code == -1) { + printf("Failed to send HTTP request: %s\n", error); return -1; } - if (response.r0 != 200) { - printf("Received wrong response code: %d instead of %d\n%s\n", response.r0, 200, response.r2); + if (response_code != 200) { + printf("Received wrong response code: %d instead of %d\n%s\n", response_code, 200, error); return -1; } - printf("Server Response: HTTP-%d\n%s\n\n", response.r0, response.r1); + printf("Server Response: HTTP-%d\n%s\n\n", response_code, response_body); return 0; } 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 ef5ccf2..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) @@ -121,7 +119,6 @@ enable_testing() file(COPY test_data DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) add_executable(${TARGET_OUT} mytest.cpp) -add_dependencies(${TARGET_OUT} testcontainers-c-shim) target_link_libraries(${TARGET_OUT} PRIVATE testcontainers-c) add_test(NAME wiremock_module_demo COMMAND ${TARGET_OUT}) ``` 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 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 + ]; + }; + }; +} diff --git a/modules/wiremock/CMakeLists.txt b/modules/wiremock/CMakeLists.txt index 640928a..6527ca6 100644 --- a/modules/wiremock/CMakeLists.txt +++ b/modules/wiremock/CMakeLists.txt @@ -1,24 +1,22 @@ +set(TARGET testcontainers-c-wiremock) +set(TARGET_NAME ${TARGET}) +set(TARGET_DESCRIPTION "Wiremock testcontainer abstractions for C") +set(TARGET_VERSION ${PROJECT_VERSION}) -cmake_minimum_required(VERSION 3.9) -project(testcontainers-c-wiremock VERSION 0.0.1 - DESCRIPTION "WireMock module for Testcontainers C") - -include(GNUInstallDirs) - -add_library(${PROJECT_NAME} SHARED - testcontainers-c-wiremock.h +add_library(${TARGET} SHARED impl.c ) -add_dependencies(${PROJECT_NAME} testcontainers-c) -include_directories(${testcontainers-c_SOURCE_DIR}) +target_sources(${TARGET} + PUBLIC FILE_SET HEADERS + BASE_DIRS . + FILES testcontainers-c-wiremock.h +) -set_target_properties(${PROJECT_NAME} PROPERTIES - VERSION ${PROJECT_VERSION} - PUBLIC_HEADER testcontainers-c-wiremock.h) +target_link_libraries(${TARGET} PRIVATE testcontainers-c) -configure_file(cmake.pc.in ${PROJECT_NAME}.pc @ONLY) -install(TARGETS ${PROJECT_NAME} +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}/${PROJECT_NAME}.pc DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig) +install(FILES ${CMAKE_BINARY_DIR}/${TARGET}.pc DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig) diff --git a/modules/wiremock/cmake.pc.in b/modules/wiremock/cmake.pc.in index 9b15f62..8c83ec9 100644 --- a/modules/wiremock/cmake.pc.in +++ b/modules/wiremock/cmake.pc.in @@ -3,10 +3,10 @@ exec_prefix=@CMAKE_INSTALL_PREFIX@ libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ -Name: @PROJECT_NAME@ -Description: @PROJECT_DESCRIPTION@ -Version: @PROJECT_VERSION@ +Name: @TARGET_NAME@ +Description: @TARGET_DESCRIPTION@ +Version: @TARGET_VERSION@ Requires: -Libs: -L${libdir} -lmylib +Libs: -L${libdir} Cflags: -I${includedir} diff --git a/modules/wiremock/impl.c b/modules/wiremock/impl.c index ad0bf34..6462566 100644 --- a/modules/wiremock/impl.c +++ b/modules/wiremock/impl.c @@ -1,23 +1,24 @@ #include "testcontainers-c-wiremock.h" +#include "testcontainers-c/container.h" #include #include -GoInt tc_wm_new_default_container() { +int tc_wm_new_default_container() { return tc_wm_new_container(DEFAULT_WIREMOCK_IMAGE); } -GoInt tc_wm_new_container(char* image) { - GoInt requestId = tc_new_container_request(image); - tc_with_exposed_tcp_port(requestId, 8080); - tc_with_wait_for_http(requestId, 8080, "/__admin/mappings"); +int tc_wm_new_container(char* image) { + int requestId = tc_container_create(image); + tc_container_with_exposed_tcp_port(requestId, 8080); + tc_container_with_wait_for_http(requestId, 8080, "/__admin/mappings"); return requestId; }; -void tc_wm_with_mapping(GoInt requestID, char* filePath, char* destination) { +void tc_wm_with_mapping(int requestID, char* filePath, char* destination) { char dest_file[128] = ""; strcat(dest_file, WIREMOCK_MAPPINGS_DIR); strcat(dest_file, destination); - + // Append extension if missing if(strlen(destination) > 5 && !strcmp(destination + strlen(destination) - 5, ".json")) { strcat(dest_file, ".json"); @@ -27,21 +28,23 @@ void tc_wm_with_mapping(GoInt requestID, char* filePath, char* destination) { } printf("DEBUG: %s to %s.\n", filePath, dest_file); - tc_with_file(requestID, filePath, dest_file); + tc_container_with_file(requestID, filePath, dest_file); }; -struct WireMock_Mapping tc_wm_get_mappings(GoInt containerId) { - struct tc_send_http_get_return response = tc_send_http_get(containerId, 8080, "/__admin/mappings"); - if (response.r0 == -1) { +struct WireMock_Mapping tc_wm_get_mappings(int containerId) { + char *response_body; + char *error; + int response_code = tc_container_send_http_get(containerId, 8080, "/__admin/mappings", response_body, error); + if (response_code == -1) { char errorMsg[8000] = ""; - sprintf(errorMsg, "Failed to send HTTP request: %s\n", response.r2); + sprintf(errorMsg, "Failed to send HTTP request: %s\n", error); return (struct WireMock_Mapping) { -1, NULL, errorMsg}; } - if (response.r0 != 200) { + if (response_code != 200) { char errorMsg[8000] = ""; - sprintf(errorMsg, "Received wrong response code: %d instead of %d\n%s\n%s\n", response.r0, 200, response.r1, response.r2); - return (struct WireMock_Mapping) { response.r0, NULL, errorMsg}; + sprintf(errorMsg, "Received wrong response code: %d instead of %d\n%s\n%s\n", response_code, 200, response_body, error); + return (struct WireMock_Mapping) { response_code, NULL, errorMsg}; } - return (struct WireMock_Mapping) {response.r0, response.r1, NULL}; + return (struct WireMock_Mapping) {response_code, response_body, NULL}; }; diff --git a/modules/wiremock/testcontainers-c-wiremock.h b/modules/wiremock/testcontainers-c-wiremock.h index 4ed78b9..a892182 100644 --- a/modules/wiremock/testcontainers-c-wiremock.h +++ b/modules/wiremock/testcontainers-c-wiremock.h @@ -1,5 +1,3 @@ -#include "testcontainers-c.h" - #ifndef TESTCONTAINERS_WIREMOCK_H #define TESTCONTAINERS_WIREMOCK_H @@ -13,29 +11,29 @@ struct WireMock_Mapping { - GoInt responseCode; + int responseCode; char* json; char* error; }; /// @brief Creates a container request with a default image, exposed port and init logic /// @return Container request ID -GoInt tc_wm_new_default_container(); +int tc_wm_new_default_container(); /// @brief Creates a container request with a default image, exposed port and init logic /// @param image Full image name /// @return Container request ID -GoInt tc_wm_new_container(char* image); +int tc_wm_new_container(char* image); /// @brief Adds WireMock mapping to the request /// @param requestID Container Request ID /// @param filePath Source file in the local filesystem /// @param destination Destination file, relative to the mappings dir. Extension is optional -void tc_wm_with_mapping(GoInt requestID, char* filePath, char* destination); +void tc_wm_with_mapping(int requestID, char* filePath, char* destination); /// @brief Gets WireMock mappings using Admin API /// @param containerId Container ID /// @return Mapping information if response code is 200, error details otherwise -struct WireMock_Mapping tc_wm_get_mappings(GoInt containerId); +struct WireMock_Mapping tc_wm_get_mappings(int containerId); #endif diff --git a/testcontainers-bridge/CMakeLists.txt b/testcontainers-bridge/CMakeLists.txt new file mode 100644 index 0000000..fff34e4 --- /dev/null +++ b/testcontainers-bridge/CMakeLists.txt @@ -0,0 +1,29 @@ +set(SRCS testcontainers-bridge.go) + +set(SHIM_TARGET testcontainers-bridge-shim) +set(SHIM_TARGET_LIB testcontainers-bridge.so) +set(SHIM_TARGET_HEADER testcontainers-bridge.h) + +add_custom_command(OUTPUT ${SHIM_TARGET_LIB} ${SHIM_TARGET_HEADER} + DEPENDS ${SRCS} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMAND env GOPATH=${GOPATH} go build -buildmode=c-archive -linkshared + -o "${CMAKE_CURRENT_BINARY_DIR}/${SHIM_TARGET_LIB}" + ${CMAKE_GO_FLAGS} ${SRCS} + COMMENT "Building Testcontainers Go to C bridge library") + +add_custom_target(${SHIM_TARGET} DEPENDS ${SHIM_TARGET_LIB} ${SHIM_TARGET_HEADER}) + +set(TARGET testcontainers-bridge) +set(TARGET_NAME ${TARGET}) +set(TARGET_DESCRIPTION "Go to C bridge for Testcontainers functionality") +set(TARGET_VERSION ${PROJECT_VERSION}) + +add_library(${TARGET} STATIC IMPORTED GLOBAL) +add_dependencies(${TARGET} ${SHIM_TARGET}) +set_target_properties(${TARGET} PROPERTIES + LINKER_LANGUAGE C + VERSION ${TARGET_VERSION} + PUBLIC_HEADER ${SHIM_TARGET_HEADER} + IMPORTED_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/${SHIM_TARGET_LIB} + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/testcontainers-c/go.mod b/testcontainers-bridge/go.mod similarity index 97% rename from testcontainers-c/go.mod rename to testcontainers-bridge/go.mod index 201e396..778b9ac 100644 --- a/testcontainers-c/go.mod +++ b/testcontainers-bridge/go.mod @@ -1,4 +1,4 @@ -module github.com/testcontainers/testcontainers-c +module github.com/testcontainers/testcontainers-native go 1.19 diff --git a/testcontainers-c/go.sum b/testcontainers-bridge/go.sum similarity index 100% rename from testcontainers-c/go.sum rename to testcontainers-bridge/go.sum diff --git a/testcontainers-c/testcontainers-c.go b/testcontainers-bridge/testcontainers-bridge.go similarity index 78% rename from testcontainers-c/testcontainers-c.go rename to testcontainers-bridge/testcontainers-bridge.go index 769b721..6acef3f 100644 --- a/testcontainers-c/testcontainers-c.go +++ b/testcontainers-bridge/testcontainers-bridge.go @@ -21,8 +21,8 @@ var customizers map[int][]*testcontainers.CustomizeRequestOption // Creates Unique container request and returns its ID // -//export tc_new_container_request -func tc_new_container_request(image *C.cchar_t) (id int) { +//export tc_bridge_new_container_request +func tc_bridge_new_container_request(image *C.cchar_t) (id int) { req := testcontainers.ContainerRequest{ Image: C.GoString(image), } @@ -31,8 +31,8 @@ func tc_new_container_request(image *C.cchar_t) (id int) { return len(containerRequests) - 1 } -//export tc_run_container -func tc_run_container(requestID int) (id int, ok bool, errstr *C.char) { +//export tc_bridge_run_container +func tc_bridge_run_container(requestID int) (id int, ok bool, errstr *C.char) { id, ok, err := _RunContainer(requestID) if err != nil { return -1, ok, ToCString(err) @@ -68,15 +68,15 @@ func _RunContainer(requestID int) (id int, ok bool, err error) { return containerId, true, nil } -//export tc_terminate_container -func tc_terminate_container(containerID int) *C.char { +//export tc_bridge_terminate_container +func tc_bridge_terminate_container(containerID int) *C.char { ctx := context.Background() container := *containers[containerID] return ToCString(container.Terminate(ctx)) } -//export tc_get_container_log -func tc_get_container_log(containerID int) (log *C.char) { +//export tc_bridge_get_container_log +func tc_bridge_get_container_log(containerID int) (log *C.char) { ctx := context.Background() container := *containers[containerID] @@ -94,11 +94,15 @@ func tc_get_container_log(containerID int) (log *C.char) { return C.CString(string(bytes)) } -//export tc_get_uri -func tc_get_uri(containerID int, port int) (uri string, e error) { +//export tc_bridge_get_uri +func tc_bridge_get_uri(containerID int, port int) (uri *C.char, ok bool, errstr *C.char) { ctx := context.Background() container := *containers[containerID] - return _GetURI(ctx, container, port) + str, err := _GetURI(ctx, container, port) + if err != nil { + return nil, false, ToCString(err) + } + return C.CString(str), true, nil } func _GetURI(ctx context.Context, container testcontainers.Container, port int) (string, error) { @@ -115,8 +119,8 @@ func _GetURI(ctx context.Context, container testcontainers.Container, port int) return "http://" + hostIP + ":" + mappedPort.Port(), nil } -//export tc_with_wait_for_http -func tc_with_wait_for_http(requestID int, port int, url *C.cchar_t) { +//export tc_bridge_with_wait_for_http +func tc_bridge_with_wait_for_http(requestID int, port int, url *C.cchar_t) { req := func(req *testcontainers.GenericContainerRequest) { req.WaitingFor = wait.ForHTTP(C.GoString(url)).WithPort(nat.Port(strconv.Itoa(port))) } @@ -124,8 +128,8 @@ func tc_with_wait_for_http(requestID int, port int, url *C.cchar_t) { registerCustomizer(requestID, req) } -//export tc_with_file -func tc_with_file(requestID int, filePath *C.cchar_t, targetPath *C.cchar_t) { +//export tc_bridge_with_file +func tc_bridge_with_file(requestID int, filePath *C.cchar_t, targetPath *C.cchar_t) { req := func(req *testcontainers.GenericContainerRequest) { cfgFile := testcontainers.ContainerFile{ HostFilePath: C.GoString(filePath), @@ -138,8 +142,8 @@ func tc_with_file(requestID int, filePath *C.cchar_t, targetPath *C.cchar_t) { registerCustomizer(requestID, req) } -//export tc_with_exposed_tcp_port -func tc_with_exposed_tcp_port(requestID int, port int) { +//export tc_bridge_with_exposed_tcp_port +func tc_bridge_with_exposed_tcp_port(requestID int, port int) { req := func(req *testcontainers.GenericContainerRequest) { req.ExposedPorts = append(req.ExposedPorts, strconv.Itoa(port)+"/tcp") } @@ -155,8 +159,8 @@ func registerCustomizer(requestID int, customizer testcontainers.CustomizeReques return len(customizers[requestID]) - 1 } -//export tc_send_http_get -func tc_send_http_get(containerID int, port int, endpoint *C.cchar_t) (responseCode C.int, responseBody *C.char, errstr *C.char) { +//export tc_bridge_send_http_get +func tc_bridge_send_http_get(containerID int, port int, endpoint *C.cchar_t) (responseCode C.int, responseBody *C.char, errstr *C.char) { container := *containers[containerID] responseCodeVal, responseBodyStr, err := SendHttpRequest(http.MethodGet, container, port, C.GoString(endpoint), nil) if err != nil { diff --git a/testcontainers-c/CMakeLists.txt b/testcontainers-c/CMakeLists.txt index 9583bd6..611e326 100644 --- a/testcontainers-c/CMakeLists.txt +++ b/testcontainers-c/CMakeLists.txt @@ -1,37 +1,23 @@ -cmake_minimum_required(VERSION 3.0) -project(testcontainers-c VERSION 0.0.1 - DESCRIPTION "Testcontainers C library") - -include(GNUInstallDirs) - -set(SRCS testcontainers-c.go) - -set(TARGET testcontainers-c-shim) -set(TARGET_LIB testcontainers-c.so) -set(TARGET_HEADER testcontainers-c.h) - -add_custom_command(OUTPUT ${TARGET_LIB} ${TARGET_HEADER} - DEPENDS ${SRCS} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - COMMAND env GOPATH=${GOPATH} go build -buildmode=c-shared - -o "${CMAKE_CURRENT_BINARY_DIR}/${TARGET_LIB}" - ${CMAKE_GO_FLAGS} ${SRCS} - COMMENT "Building Testcontainers C shared library") - -add_custom_target(${TARGET} DEPENDS ${TARGET_LIB} ${TARGET_HEADER}) - -add_library(${PROJECT_NAME} SHARED IMPORTED GLOBAL) -add_dependencies(${PROJECT_NAME} ${TARGET}) -set_target_properties(${PROJECT_NAME} PROPERTIES - LINKER_LANGUAGE C - VERSION ${PROJECT_VERSION} - PUBLIC_HEADER testcontainers-c.h - IMPORTED_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/${TARGET_LIB} - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_BINARY_DIR}) - -configure_file(cmake.pc.in ${PROJECT_NAME}.pc @ONLY) -install(FILES ${TARGET_LIB} - DESTINATION ${CMAKE_INSTALL_LIBDIR}) -install(FILES ${TARGET_HEADER} - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) -install(FILES ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.pc DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig) +set(TARGET testcontainers-c) +set(TARGET_NAME ${TARGET}) +set(TARGET_DESCRIPTION "Testcontainer library for C") +set(TARGET_VERSION ${PROJECT_VERSION}) + +add_library(${TARGET} SHARED + src/container.c +) + +target_sources(${TARGET} + PUBLIC FILE_SET HEADERS + BASE_DIRS include + FILES include/testcontainers-c/container.h +) + +target_link_libraries(${TARGET} PRIVATE testcontainers-bridge) + +configure_file(cmake.pc.in ${TARGET}.pc @ONLY) +install(FILES ${SHIM_TARGET_LIB} + DESTINATION ${CMAKE_INSTALL_LIBDIR}) +install(FILES ${SHIM_TARGET_HEADER} + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(FILES ${CMAKE_BINARY_DIR}/${TARGET}.pc DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig) diff --git a/testcontainers-c/cmake.pc.in b/testcontainers-c/cmake.pc.in index 9b15f62..8c83ec9 100644 --- a/testcontainers-c/cmake.pc.in +++ b/testcontainers-c/cmake.pc.in @@ -3,10 +3,10 @@ exec_prefix=@CMAKE_INSTALL_PREFIX@ libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ -Name: @PROJECT_NAME@ -Description: @PROJECT_DESCRIPTION@ -Version: @PROJECT_VERSION@ +Name: @TARGET_NAME@ +Description: @TARGET_DESCRIPTION@ +Version: @TARGET_VERSION@ Requires: -Libs: -L${libdir} -lmylib +Libs: -L${libdir} Cflags: -I${includedir} diff --git a/testcontainers-c/include/testcontainers-c/container.h b/testcontainers-c/include/testcontainers-c/container.h new file mode 100644 index 0000000..938ec19 --- /dev/null +++ b/testcontainers-c/include/testcontainers-c/container.h @@ -0,0 +1,59 @@ +#ifndef CONTAINER_H +#define CONTAINER_H + +/** + * @param The container image name. + * @return -1 if no image is provided or creation fails. Otherwise, + * the ID of the created container. + */ +int tc_container_create(const char* image); + +/** + * + */ +int tc_container_run(int container_id, char* error); + +/** + * + */ +char* tc_container_terminate(int container_id); + +/** + * + */ +char* tc_container_get_log(int container_id); + +/** + * Retrieves the URI for container with id `container_id` + * It optionally stores an error message in `error`. + */ +char* tc_container_get_uri(int container_id, int port, char* error); + +/** + * + */ +void tc_container_with_wait_for_http(int request_id, int port, const char* url); + +/** + * + */ +void tc_container_with_file(int request_id, const char* file_path, const char* target_path); + +/** + * + */ +void tc_container_with_exposed_tcp_port(int request_id, int port); + +/** + * Sends an HTTP GET request to an endpoint in a container. + * + * @param container_id The ID of the container. + * @param port The port of the container HTTP server. + * @param endpoint The endpoint to request. + * @param response_body The body of the response. + * @param error The error message if the request fails. + * @return Response code from the HTTP request. + */ +int tc_container_send_http_get(int container_id, int port, const char* endpoint, char* response_body, char* error); + +#endif // !CONTAINER_H diff --git a/testcontainers-c/src/container.c b/testcontainers-c/src/container.c new file mode 100644 index 0000000..3f84567 --- /dev/null +++ b/testcontainers-c/src/container.c @@ -0,0 +1,67 @@ +#include + +#include "testcontainers-c/container.h" + +int tc_container_create(const char* image) { + if (image == NULL) { + return -1; + } + + return tc_bridge_new_container_request(image); +} + +int tc_container_run(int container_id, char* error) { + struct tc_bridge_run_container_return result = tc_bridge_run_container(container_id); + + if (error != NULL) { + error = result.r2; + } + + if (!result.r1) { + return -1; + } + + return result.r0; +} + +char* tc_container_terminate(int container_id) { return tc_bridge_terminate_container(container_id); } + +char* tc_container_get_log(int container_id) { return tc_bridge_get_container_log(container_id); } + +char* tc_container_get_uri(int container_id, int port, char* error) { + struct tc_bridge_get_uri_return result = tc_bridge_get_uri(container_id, port); + + if (error != NULL) { + error = result.r2; + } + + if (result.r1) { + return result.r0; + } + + return NULL; +} + +void tc_container_with_wait_for_http(int request_id, int port, const char* url) { + tc_bridge_with_wait_for_http(request_id, port, url); +} + +void tc_container_with_file(int request_id, const char* file_path, const char* target_path) { + tc_bridge_with_file(request_id, file_path, target_path); +} + +void tc_container_with_exposed_tcp_port(int request_id, int port) { tc_bridge_with_exposed_tcp_port(request_id, port); } + +int tc_container_send_http_get(int container_id, int port, const char* endpoint, char* response_body, char* error) { + struct tc_bridge_send_http_get_return result = tc_bridge_send_http_get(container_id, port, endpoint); + + if (error != NULL) { + error = result.r2; + } + + if (response_body != NULL) { + response_body = result.r1; + } + + return result.r0; +} 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..a4ef29e --- /dev/null +++ b/testcontainers-cpp/testcontainers.hpp @@ -0,0 +1,68 @@ +#ifndef TESTCONTAINERS_HPP +#define TESTCONTAINERS_HPP + +#include +#include +#include +#include +#include + +namespace testcontainers { + +namespace { +extern "C" { +#include "testcontainers-c/container.h" +} +} + +struct TcpPort { + uint16_t underlying; +}; + +struct UdpPort { + uint16_t underlying; +}; + +enum struct HttpMethod { + Get, + Post, + Head, +}; + +class Container { +public: + constexpr Container(std::string image) : request_id(tc_container_create(image.c_str())) {} + + constexpr ~Container() { + tc_container_terminate(request_id); + } + + std::expected run() { + return std::unexpected{"unimplemented"}; + } + + void with_file(std::filesystem::path source, std::filesystem::path destination) { + tc_container_with_file(request_id, source.c_str(), destination.c_str()); + } + + void wait_for_http(TcpPort port, std::filesystem::path endpoint) { + tc_container_with_wait_for_http(request_id, port.underlying, endpoint.c_str()); + } + + void expose_port(TcpPort port) { + tc_container_with_exposed_tcp_port(request_id, port.underlying); + } + + void expose_port(UdpPort port); + + std::pair> send_http(HttpMethod method, TcpPort port, std::filesystem::path endpoint) { + return {0, std::unexpected{"unimplemented"}}; + } + +private: + int request_id; +}; + +} + +#endif // !TESTCONTAINERS_HPP