diff --git a/.github/workflows/build-deb.yml b/.github/workflows/build-deb.yml index cadc95b7d..53b7084a9 100644 --- a/.github/workflows/build-deb.yml +++ b/.github/workflows/build-deb.yml @@ -14,15 +14,11 @@ jobs: steps: - name: "Checkout" uses: actions/checkout@v3 - with: - submodules: true - path: cpr - # Install packages necessary for building libcpr and package - name: "Update package list" run: sudo apt update - name: "Install cpr dependencies" - run: sudo apt install -y libssl-dev libcurl4-openssl-dev + run: sudo apt install -y libssl-dev libcurl4-openssl-dev libpsl-dev - name: "Install building tools" run: sudo apt install -y cmake debmake devscripts debhelper # Set version number @@ -34,7 +30,7 @@ jobs: run: | mkdir -p cpr/build pushd cpr/build - cmake .. -DCPR_BUILD_VERSION_OUTPUT_ONLY=ON + cmake .. -DCPR_BUILD_VERSION_OUTPUT_ONLY=ON -DCPR_USE_SYSTEM_LIB_PSL=ON -DCPR_USE_SYSTEM_CURL=ON echo "RELEASE_VERSION=$(cat version.txt)" >> $GITHUB_ENV popd rm -rf cpr/build diff --git a/.github/workflows/build-nuget.yml b/.github/workflows/build-nuget.yml index 72081b2f9..2b001c3b2 100644 --- a/.github/workflows/build-nuget.yml +++ b/.github/workflows/build-nuget.yml @@ -26,10 +26,10 @@ jobs: run: echo "NuGet version will be '${{ env.RELEASE_VERSION }}'" - name: Checkout uses: actions/checkout@v3 - with: - submodules: true - name: Setup NuGet.exe uses: nuget/setup-nuget@v2 + - name: Install meson + run: pip install meson - name: "[Release_x86] Build & Install" env: CMAKE_GENERATOR: "Visual Studio 16 2019" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09032b169..5fd57ce2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: ubuntu-clang-openssl: strategy: matrix: - container: ["ubuntu:20.04", "ubuntu:24.04", "ubuntu:latest"] + container: ["ubuntu:latest", "ubuntu:rolling"] systemCurl: [ON, OFF] buildType: [Debug, Release] runs-on: ubuntu-latest @@ -21,7 +21,7 @@ jobs: - name: Update package list run: apt update - name: Install Dependencies - run: apt install -y git libssl-dev cmake build-essential clang libcurl4-openssl-dev + run: apt install -y git libssl-dev cmake build-essential clang libcurl4-openssl-dev libpsl-dev meson libunistring-dev env: DEBIAN_FRONTEND: noninteractive - name: Setup cmake @@ -35,7 +35,7 @@ jobs: CPR_BUILD_TESTS: ON CPR_BUILD_TESTS_SSL: ON CPR_FORCE_OPENSSL_BACKEND: ON - USE_SYSTEM_CURL: ${{ matrix.systemCurl }} + CPR_USE_SYSTEM_CURL: ${{ matrix.systemCurl }} uses: ashutoshvarma/action-cmake-build@master with: build-dir: ${{ github.workspace }}/build @@ -49,7 +49,7 @@ jobs: ubuntu-gcc-openssl: strategy: matrix: - container: ["ubuntu:20.04", "ubuntu:24.04", "ubuntu:latest"] + container: ["ubuntu:latest", "ubuntu:rolling"] systemCurl: [ON, OFF] buildType: [Debug, Release] runs-on: ubuntu-latest @@ -58,7 +58,7 @@ jobs: - name: Update package list run: apt update - name: Install Dependencies - run: apt install -y git libssl-dev cmake build-essential libcurl4-openssl-dev + run: apt install -y git libssl-dev cmake build-essential libcurl4-openssl-dev libpsl-dev meson libunistring-dev env: DEBIAN_FRONTEND: noninteractive - name: Setup cmake @@ -72,7 +72,7 @@ jobs: CPR_BUILD_TESTS: ON CPR_BUILD_TESTS_SSL: ON CPR_FORCE_OPENSSL_BACKEND: ON - USE_SYSTEM_CURL: ${{ matrix.systemCurl }} + CPR_USE_SYSTEM_CURL: ${{ matrix.systemCurl }} uses: ashutoshvarma/action-cmake-build@master with: build-dir: ${{ github.workspace }}/build @@ -89,7 +89,7 @@ jobs: - name: Update package list run: sudo apt update - name: Install Dependencies - run: sudo apt install -y git libssl-dev libmbedtls-dev cmake build-essential + run: sudo apt install -y git libssl-dev libmbedtls-dev cmake build-essential libpsl-dev meson libunistring-dev env: DEBIAN_FRONTEND: noninteractive - name: Setup cmake @@ -114,16 +114,13 @@ jobs: ctest-options: ${{ env.CTEST_OPTIONS }} fedora-clang-openssl: - strategy: - matrix: - container: ["fedora:latest"] runs-on: ubuntu-latest - container: ${{ matrix.container }} + container: "fedora:latest" steps: - name: Update package list run: dnf update -y - name: Install Dependencies - run: dnf install -y gcc clang git gcc gdb make openssl-devel libcurl-devel cmake + run: dnf install -y gcc clang git gcc gdb make openssl-devel libcurl-devel cmake libpsl-devel libunistring-devel meson - name: Checkout uses: actions/checkout@v3 - name: "Build & Test" @@ -131,7 +128,7 @@ jobs: CPR_BUILD_TESTS: ON CPR_BUILD_TESTS_SSL: ON CPR_FORCE_OPENSSL_BACKEND: ON - USE_SYSTEM_CURL: OFF + CPR_USE_SYSTEM_CURL: OFF uses: ashutoshvarma/action-cmake-build@master with: build-dir: ${{ github.workspace }}/build @@ -145,16 +142,15 @@ jobs: fedora-gcc-openssl: strategy: matrix: - container: ["fedora:latest"] systemCurl: [ON, OFF] buildType: [Debug, Release] runs-on: ubuntu-latest - container: ${{ matrix.container }} + container: "fedora:latest" steps: - name: Update package list run: dnf update -y - name: Install Dependencies - run: dnf install -y gcc clang git gcc gdb make openssl-devel libcurl-devel cmake + run: dnf install -y gcc clang git gcc gdb make openssl-devel libcurl-devel cmake libpsl-devel libunistring-devel meson - name: Checkout uses: actions/checkout@v3 - name: "Build & Test" @@ -162,7 +158,7 @@ jobs: CPR_BUILD_TESTS: ON CPR_BUILD_TESTS_SSL: ON CPR_FORCE_OPENSSL_BACKEND: ON - USE_SYSTEM_CURL: ${{ matrix.systemCurl }} + CPR_USE_SYSTEM_CURL: ${{ matrix.systemCurl }} uses: ashutoshvarma/action-cmake-build@master with: build-dir: ${{ github.workspace }}/build @@ -183,7 +179,7 @@ jobs: - name: Update package list run: dnf update -y - name: Install Dependencies - run: dnf install -y gcc clang git gcc gdb make openssl-devel libasan libubsan liblsan libtsan cmake + run: dnf install -y gcc clang git gcc gdb make openssl-devel libasan libubsan liblsan libtsan cmake libpsl-devel libunistring-devel meson - name: Checkout uses: actions/checkout@v3 - name: "Build & Test" @@ -206,6 +202,9 @@ jobs: buildType: [Debug, Release] runs-on: windows-latest steps: + - uses: actions/setup-python@v1 + - name: Install meson + run: pip install meson - name: Checkout uses: actions/checkout@v3 - name: "Build & Test" @@ -213,6 +212,7 @@ jobs: CMAKE_GENERATOR: "Visual Studio 17 2022" CPR_BUILD_TESTS: ON CPR_BUILD_TESTS_SSL: OFF + CURL_USE_LIBPSL: OFF uses: ashutoshvarma/action-cmake-build@master with: build-dir: ${{ github.workspace }}/build @@ -224,6 +224,9 @@ jobs: windows-msvc-openssl: runs-on: windows-latest steps: + - uses: actions/setup-python@v1 + - name: Install meson + run: pip install meson - name: Install OpenSSL run: choco install openssl -y - name: Checkout @@ -234,6 +237,7 @@ jobs: CPR_BUILD_TESTS: ON CPR_BUILD_TESTS_SSL: ON CPR_FORCE_OPENSSL_BACKEND: ON + CURL_USE_LIBPSL: OFF uses: ashutoshvarma/action-cmake-build@master with: build-dir: ${{ github.workspace }}/build @@ -248,12 +252,15 @@ jobs: buildType: [Debug, Release] runs-on: macos-latest steps: + - name: Install libpsl + run: brew install libpsl - name: Checkout uses: actions/checkout@v3 - name: "Build & Test" env: CPR_BUILD_TESTS: ON CPR_BUILD_TESTS_SSL: OFF + CPR_USE_SYSTEM_LIB_PSL: ON uses: ashutoshvarma/action-cmake-build@master with: build-dir: ${{ github.workspace }}/build @@ -267,6 +274,8 @@ jobs: macos-clang-darwinssl: runs-on: macos-latest steps: + - name: Install libpsl + run: brew install libpsl - name: Checkout uses: actions/checkout@v3 - name: "Build & Test" @@ -274,6 +283,7 @@ jobs: CPR_BUILD_TESTS: ON CPR_BUILD_TESTS_SSL: OFF CPR_FORCE_DARWINSSL_BACKEND: ON + CPR_USE_SYSTEM_LIB_PSL: ON uses: ashutoshvarma/action-cmake-build@master with: build-dir: ${{ github.workspace }}/build @@ -289,6 +299,8 @@ jobs: steps: - name: Install OpenSSL run: brew install openssl + - name: Install libpsl + run: brew install libpsl - name: Checkout uses: actions/checkout@v3 - name: "Build & Test" @@ -296,6 +308,7 @@ jobs: CPR_BUILD_TESTS: ON CPR_BUILD_TESTS_SSL: ON CPR_FORCE_OPENSSL_BACKEND: ON + CPR_USE_SYSTEM_LIB_PSL: ON OPENSSL_ROOT_DIR: "${{ env.MACOS_OPENSSL_ROOT_DIR }}" OPENSSL_LIBRARIES: "${{ env.MACOS_OPENSSL_ROOT_DIR }}/lib" LDFLAGS: "-L${{ env.MACOS_OPENSSL_ROOT_DIR }}/lib" @@ -318,6 +331,8 @@ jobs: run: brew install boost - name: Install OpenSSL run: brew install openssl + - name: Install libpsl + run: brew install libpsl - name: Checkout uses: actions/checkout@v3 - name: "Build & Test" @@ -326,6 +341,7 @@ jobs: CPR_BUILD_TESTS_SSL: ON CPR_FORCE_OPENSSL_BACKEND: ON CPR_USE_BOOST_FILESYSTEM: ON + CPR_USE_SYSTEM_LIB_PSL: ON OPENSSL_ROOT_DIR: "${{ env.MACOS_OPENSSL_ROOT_DIR }}" OPENSSL_LIBRARIES: "${{ env.MACOS_OPENSSL_ROOT_DIR }}/lib" LDFLAGS: "-L${{ env.MACOS_OPENSSL_ROOT_DIR }}/lib" diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index 052465ef4..260c9c3ee 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -10,7 +10,7 @@ jobs: - name: Update package list run: sudo dnf update -y - name: Install dependencies - run: sudo dnf install -y openssl-devel cmake git gcc clang ninja-build + run: sudo dnf install -y openssl-devel cmake git gcc clang ninja-build libpsl-devel meson - name: Install clang-tidy run: sudo dnf install -y clang-tools-extra - name: Checkout diff --git a/.github/workflows/cppcheck.yml b/.github/workflows/cppcheck.yml index d3d674fa7..16127bbaf 100644 --- a/.github/workflows/cppcheck.yml +++ b/.github/workflows/cppcheck.yml @@ -5,15 +5,14 @@ on: [push, workflow_dispatch, pull_request] # Trigger for every push as well as jobs: cppcheck: runs-on: ubuntu-latest + container: "fedora:latest" # Use fedora for an up to date version of cppcheck steps: - name: Checkout uses: actions/checkout@v3 - with: - submodules: true - name: Update package list - run: sudo apt update - - name: Install cppcheck - run: sudo apt install cppcheck + run: dnf update -y + - name: Install Dependencies + run: dnf install -y gcc clang git gcc gdb make openssl-devel cmake libpsl-devel cppcheck meson - name: "[Release g++] Build" env: CPR_ENABLE_CPPCHECK: ON diff --git a/CMakeLists.txt b/CMakeLists.txt index a2f21bf58..547235d95 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,7 @@ cpr_option(CPR_CURL_NOSIGNAL "Set to ON to disable use of signals in libcurl." O cpr_option(CURL_VERBOSE_LOGGING "Curl verbose logging during building curl" OFF) cpr_option(CPR_USE_SYSTEM_GTEST "If ON, this project will look in the system paths for an installed gtest library. If none is found it will use the built-in one." OFF) cpr_option(CPR_USE_SYSTEM_CURL "If enabled we will use the curl lib already installed on this system." OFF) +cpr_option(CPR_USE_SYSTEM_LIB_PSL "Since curl 8.13 it depends on libpsl. If enabled we will use the psl lib already installed on this system. Else meson is required as build dependency." ${CPR_USE_SYSTEM_CURL}) cpr_option(CPR_ENABLE_CURL_HTTP_ONLY "If enabled we will only use the HTTP/HTTPS protocols from CURL. If disabled, all the CURL protocols are enabled. This is useful if your project uses libcurl and you need support for other CURL features e.g. sending emails." ON) cpr_option(CPR_ENABLE_SSL "Enables or disables the SSL backend. Required to perform HTTPS requests." ON) cpr_option(CPR_FORCE_OPENSSL_BACKEND "Force to use the OpenSSL backend. If CPR_FORCE_OPENSSL_BACKEND, CPR_FORCE_DARWINSSL_BACKEND, CPR_FORCE_MBEDTLS_BACKEND, and CPR_FORCE_WINSSL_BACKEND are set to to OFF, cpr will try to automatically detect the best available SSL backend (WinSSL - Windows, OpenSSL - Linux, DarwinSSL - Mac ...)." OFF) @@ -294,10 +295,14 @@ else() if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") cmake_policy(SET CMP0135 NEW) endif() - FetchContent_Declare(curl - URL https://github.com/curl/curl/releases/download/curl-8_10_1/curl-8.10.1.tar.xz - URL_HASH SHA256=73a4b0e99596a09fa5924a4fb7e4b995a85fda0d18a2c02ab9cf134bebce04ee # the file hash for curl-8.10.1.tar.xz - USES_TERMINAL_DOWNLOAD TRUE) # <---- This is needed only for Ninja to show download progress + + # Since curl 8.13, curl depends on lib psl + if(NOT ${CPR_USE_SYSTEM_LIB_PSL}) + include(libpsl) + endif() + + FetchContent_Declare(curl URL https://github.com/curl/curl/releases/download/curl-8_13_0/curl-8.13.0.tar.xz + URL_HASH SHA256=4a093979a3c2d02de2fbc00549a32771007f2e78032c6faa5ecd2f7a9e152025) # the file hash for curl-8.13.0.tar.xz FetchContent_MakeAvailable(curl) restore_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) @@ -377,7 +382,7 @@ if(CPR_BUILD_TESTS) USES_TERMINAL_DOWNLOAD TRUE) # <---- This is needed only for Ninja to show download progress # We can not use FetchContent_MakeAvailable, since we need to patch mongoose to use CMake if (NOT mongoose_POPULATED) - FetchContent_POPULATE(mongoose) + FetchContent_MakeAvailable(mongoose) file(INSTALL cmake/mongoose.CMakeLists.txt DESTINATION ${mongoose_SOURCE_DIR}) file(RENAME ${mongoose_SOURCE_DIR}/mongoose.CMakeLists.txt ${mongoose_SOURCE_DIR}/CMakeLists.txt) diff --git a/cmake/libpsl.cmake b/cmake/libpsl.cmake new file mode 100644 index 000000000..380bcb586 --- /dev/null +++ b/cmake/libpsl.cmake @@ -0,0 +1,59 @@ +# Builds libpsl which is especially necessary on Windows since there it is not available via e.g. a package manager. + +include(ExternalProject) +find_program(MESON_PATH meson) + +if(MESON_PATH STREQUAL "MESON_PATH-NOTFOUND") + message(FATAL_ERROR "meson not found. Please make sure you have meson installed on your system (https://mesonbuild.com/Getting-meson.html). Meson is required for building libpsl for curl on Windows.") + return() +endif() + +FetchContent_Declare(libpsl_src GIT_REPOSITORY https://github.com/rockdaboot/libpsl.git + GIT_TAG 0.21.5) +FetchContent_MakeAvailable(libpsl_src) # sets libpsl_src_SOURCE_DIR / _BINARY_DIR + +set(LIBPSL_SOURCE_DIR "${libpsl_src_SOURCE_DIR}") +set(LIBPSL_BUILD_DIR "${libpsl_src_BINARY_DIR}") +set(LIBPSL_INSTALL_DIR "${CMAKE_BINARY_DIR}/libpsl_src-install") +file(MAKE_DIRECTORY "${LIBPSL_BUILD_DIR}") + +# Meson configure +# We only care about static libraries of psl. In case you need a dynamic version, feel free to add support for it. +message(STATUS "Configuring libpsl...") +execute_process(COMMAND "${MESON_PATH}" setup + "${LIBPSL_BUILD_DIR}" + "${LIBPSL_SOURCE_DIR}" + -Dtests=false + -Ddocs=false + --buildtype=release + --prefix "${LIBPSL_INSTALL_DIR}" + --default-library=static + RESULT_VARIABLE MESON_SETUP_RC) +if(MESON_SETUP_RC) + message(FATAL_ERROR "Meson setup for libpsl failed!") +endif() + +# Meson build +message(STATUS "Building libpsl...") +execute_process(COMMAND "${MESON_PATH}" compile -C "${LIBPSL_BUILD_DIR}" + RESULT_VARIABLE MESON_COMPILE_RC +) +if(MESON_COMPILE_RC) + message(FATAL_ERROR "Meson compile for libpsl failed!") +endif() + +# Meson install +message(STATUS "Installing libpsl...") +execute_process(COMMAND "${MESON_PATH}" install -C "${LIBPSL_BUILD_DIR}" + RESULT_VARIABLE MESON_INSTALL_RC) +if(MESON_INSTALL_RC) + message(FATAL_ERROR "Meson install for libpsl failed!") +endif() + +list(APPEND CMAKE_LIBRARY_PATH "${LIBPSL_INSTALL_DIR}/lib64") +list(APPEND CMAKE_LIBRARY_PATH "${LIBPSL_INSTALL_DIR}/lib") +list(APPEND CMAKE_INCLUDE_PATH "${LIBPSL_INSTALL_DIR}/include") + +# Workaround for Windows compilation. +# Ref: https://github.com/microsoft/vcpkg/pull/38847/files#diff-922fe829582a7e5acf5b0c35181daa63064fc12a2c889c5d89a19e5e02113f1bL44 +add_compile_definitions(PSL_STATIC=1) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d78ff8229..fd5afe170 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -6,11 +6,13 @@ if (ENABLE_SSL_TESTS) add_library(test_server STATIC abstractServer.cpp httpServer.cpp - httpsServer.cpp) + httpsServer.cpp + testUtils.cpp) else () add_library(test_server STATIC abstractServer.cpp - httpServer.cpp) + httpServer.cpp + testUtils.cpp) endif() if(WIN32) target_link_libraries(test_server PRIVATE Threads::Threads cpr::cpr GTest::GTest @@ -67,6 +69,7 @@ add_cpr_test(multiasync) add_cpr_test(file_upload) add_cpr_test(singleton) add_cpr_test(threadpool) +add_cpr_test(testUtils) if (ENABLE_SSL_TESTS) add_cpr_test(ssl) diff --git a/test/session_tests.cpp b/test/session_tests.cpp index 781232b12..8441df860 100644 --- a/test/session_tests.cpp +++ b/test/session_tests.cpp @@ -13,6 +13,7 @@ #include "cpr/accept_encoding.h" #include "httpServer.hpp" +#include "testUtils.hpp" using namespace cpr; using namespace std::chrono_literals; @@ -1173,31 +1174,23 @@ TEST(CurlHolderManipulateTests, CustomOptionTest) { TEST(LocalPortTests, SetLocalPortTest) { Url url{server->GetBaseUrl() + "/local_port.html"}; Session session; - uint16_t local_port{0}; - uint16_t local_port_range{0}; Response response; - - // Try up to 10 times to get a free local port - for (size_t i = 0; i < 10; i++) { - session.SetUrl(url); - local_port = 40252 + (i * 100); // beware of HttpServer::GetPort when changing - local_port_range = 7000; - session.SetLocalPort(local_port); - session.SetLocalPortRange(local_port_range); - // expected response: body contains port number in specified range - // NOTE: even when trying up to 7000 ports there is the chance that all of them are occupied. - // It would be possible to also check here for ErrorCode::UNKNOWN_ERROR but that somehow seems - // wrong as then this test would pass in case SetLocalPort does not work at all - // or in other words: we have to assume that at least one port in the specified range is free. - response = session.Get(); - - if (response.error.code != ErrorCode::UNKNOWN_ERROR) { - break; - } - } + session.SetUrl(url); + uint16_t local_port{0}; + ASSERT_NO_THROW(local_port = cpr::test::get_free_port()); + uint16_t local_port_range{7000}; + session.SetLocalPort(local_port); + session.SetLocalPortRange(local_port_range); + // expected response: body contains port number in specified range + // NOTE: even when trying up to 7000 ports there is the chance that all of them are occupied. + // It would be possible to also check here for ErrorCode::UNKNOWN_ERROR but that somehow seems + // wrong as then this test would pass in case SetLocalPort does not work at all + // or in other words: we have to assume that at least one port in the specified range is free. + response = session.Get(); EXPECT_EQ(200, response.status_code); EXPECT_EQ(ErrorCode::OK, response.error.code); + errno = 0; // NOLINTNEXTLINE(google-runtime-int) unsigned long port_from_response = std::strtoul(response.text.c_str(), nullptr, 10); EXPECT_EQ(errno, 0); @@ -1208,31 +1201,23 @@ TEST(LocalPortTests, SetLocalPortTest) { TEST(LocalPortTests, SetOptionTest) { Url url{server->GetBaseUrl() + "/local_port.html"}; Session session; - uint16_t local_port{0}; - uint16_t local_port_range{0}; Response response; - - // Try up to 10 times to get a free local port - for (size_t i = 0; i < 10; i++) { - session.SetUrl(url); - local_port = 30252 + (i * 100); // beware of HttpServer::GetPort when changing - local_port_range = 7000; - session.SetOption(LocalPort(local_port)); - session.SetOption(LocalPortRange(local_port_range)); - // expected response: body contains port number in specified range - // NOTE: even when trying up to 7000 ports there is the chance that all of them are occupied. - // It would be possible to also check here for ErrorCode::UNKNOWN_ERROR but that somehow seems - // wrong as then this test would pass in case SetLocalPort does not work at all - // or in other words: we have to assume that at least one port in the specified range is free. - response = session.Get(); - - if (response.error.code != ErrorCode::UNKNOWN_ERROR) { - break; - } - } + session.SetUrl(url); + uint16_t local_port{0}; + uint16_t local_port_range{7000}; + ASSERT_NO_THROW(local_port = cpr::test::get_free_port()); + session.SetOption(LocalPort(local_port)); + session.SetOption(LocalPortRange(local_port_range)); + // expected response: body contains port number in specified range + // NOTE: even when trying up to 7000 ports there is the chance that all of them are occupied. + // It would be possible to also check here for ErrorCode::UNKNOWN_ERROR but that somehow seems + // wrong as then this test would pass in case SetLocalPort does not work at all + // or in other words: we have to assume that at least one port in the specified range is free. + response = session.Get(); EXPECT_EQ(200, response.status_code); EXPECT_EQ(ErrorCode::OK, response.error.code); + errno = 0; // NOLINTNEXTLINE(google-runtime-int) unsigned long port_from_response = std::strtoul(response.text.c_str(), nullptr, 10); EXPECT_EQ(errno, 0); diff --git a/test/testUtils.cpp b/test/testUtils.cpp new file mode 100644 index 000000000..b7f37bb2d --- /dev/null +++ b/test/testUtils.cpp @@ -0,0 +1,70 @@ +#include "testUtils.hpp" + +#include +#include + +#ifdef _WIN32 +#define NOMINMAX +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +namespace cpr::test { + +std::uint16_t get_free_port() { +#ifdef _WIN32 + static const WSAInit wsa_guard; // one-time Winsock init +#endif + + // 1. Create a TCP socket. + socket_t sock = ::socket(AF_INET, SOCK_STREAM, 0); + if (sock == INVALID_SOCKET_FD) { + throw std::runtime_error("socket() failed"); + } + + // 2. Bind to port 0 so the OS assigns an ephemeral port. + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(0); // 0 ⇒ “pick for me” + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + if (::bind(sock, reinterpret_cast(&addr), sizeof(addr)) != 0) { +#ifdef _WIN32 + ::closesocket(sock); +#else + ::close(sock); +#endif + throw std::runtime_error("bind() failed"); + } + + // 3. Ask what port we actually got. + socklen_t len = sizeof(addr); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + if (::getsockname(sock, reinterpret_cast(&addr), &len) != 0) { +#ifdef _WIN32 + ::closesocket(sock); +#else + ::close(sock); +#endif + throw std::runtime_error("getsockname() failed"); + } + + std::uint16_t port = ntohs(addr.sin_port); + + // 4. Close the socket ‒ we only needed it to grab the port number. +#ifdef _WIN32 + ::closesocket(sock); +#else + ::close(sock); +#endif + + return port; +} +} // namespace cpr::test diff --git a/test/testUtils.hpp b/test/testUtils.hpp new file mode 100644 index 000000000..3aaac7800 --- /dev/null +++ b/test/testUtils.hpp @@ -0,0 +1,38 @@ +#ifndef CPR_TEST_UTILS_H +#define CPR_TEST_UTILS_H +#include +#include + +#ifdef _WIN32 +#define NOMINMAX +#include +#pragma comment(lib, "ws2_32.lib") +#endif + +namespace cpr::test { + +#ifdef _WIN32 +using socket_t = SOCKET; +constexpr socket_t INVALID_SOCKET_FD = INVALID_SOCKET; + +struct WSAInit { + WSAInit() { + WSADATA data; + if (WSAStartup(MAKEWORD(2, 2), &data) != 0) { + throw std::runtime_error("WSAStartup failed"); + } + } + ~WSAInit() { + WSACleanup(); + } +}; +#else +using socket_t = int; +constexpr socket_t INVALID_SOCKET_FD = -1; +#endif + +/// Return a currently unused TCP port. +/// \throws std::runtime_error on failure. +uint16_t get_free_port(); +} // namespace cpr::test +#endif \ No newline at end of file diff --git a/test/testUtils_tests.cpp b/test/testUtils_tests.cpp new file mode 100644 index 000000000..a082da51a --- /dev/null +++ b/test/testUtils_tests.cpp @@ -0,0 +1,16 @@ +#include +#include + +#include "testUtils.hpp" + +TEST(TestUtils, GetUnusedPort) { + uint16_t port{0}; + ASSERT_NO_THROW(port = cpr::test::get_free_port()); + EXPECT_NE(port, 0); +} + + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}