diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index eda14fb..4be4cf0 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -24,13 +24,24 @@ jobs: submodules: 'true' fetch-depth: 0 - - name: Configure CMake # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type - run: cmake -B ${{github.workspace}}/_build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} + run: | + cmake -B ${{github.workspace}}/_build \ + -DTCP_PUBSUB_BUILD_SAMPLES=ON \ + -DTCP_PUBSUB_BUILD_TESTS=ON \ + -DTCP_PUBSUB_USE_BUILTIN_ASIO=ON \ + -DTCP_PUBSUB_USE_BUILTIN_RECYCLE=ON \ + -DTCP_PUBSUB_USE_BUILTIN_GTEST=ON \ + -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ + shell: bash - name: Build # Build your program with the given configuration run: cmake --build ${{github.workspace}}/_build --config ${{env.BUILD_TYPE}} + + - name: Run Tests + run: ctest -C Release -V + working-directory: ${{ github.workspace }}/_build diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml index d555299..b0e569e 100644 --- a/.github/workflows/build-ubuntu.yml +++ b/.github/workflows/build-ubuntu.yml @@ -14,9 +14,10 @@ jobs: build-ubuntu: strategy: + fail-fast: false matrix: - library_type: [static, shared] - os: [ubuntu-22.04, ubuntu-20.04] + library_type: [static, shared, object] + os: [ubuntu-24.04, ubuntu-22.04, ubuntu-20.04] # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. # You can convert this to a matrix build if you need cross-platform coverage. @@ -28,41 +29,19 @@ jobs: - name: Set Variables run: | if [[ '${{ matrix.library_type }}' == 'static' ]]; then - echo "build_shared_libs=OFF" >> "$GITHUB_ENV" - echo "package_postfix=static" >> "$GITHUB_ENV" - else - echo "build_shared_libs=ON" >> "$GITHUB_ENV" - echo "package_postfix=shared" >> "$GITHUB_ENV" + echo "build_shared_libs=OFF" >> "$GITHUB_ENV" + echo "tcp_pubsub_library_type=STATIC" >> "$GITHUB_ENV" + echo "package_postfix=static" >> "$GITHUB_ENV" + elif [[ '${{ matrix.library_type }}' == 'shared' ]]; then + echo "build_shared_libs=ON" >> "$GITHUB_ENV" + echo "tcp_pubsub_library_type=SHARED" >> "$GITHUB_ENV" + echo "package_postfix=shared" >> "$GITHUB_ENV" + elif [[ '${{ matrix.library_type }}' == 'object' ]]; then + echo "build_shared_libs=OFF" >> "$GITHUB_ENV" + echo "tcp_pubsub_library_type=OBJECT" >> "$GITHUB_ENV" + echo "package_postfix=object" >> "$GITHUB_ENV" fi - - - name: Getting closer to vanilla Ubuntu 18.04 - run: | - if [[ '${{ matrix.os }}' == 'ubuntu-18.04' ]]; then - - sudo apt-get -y update - - echo "Creating directory for libgcc1" - mkdir libgcc1_deb - cd libgcc1_deb - pwd - - echo "Downloading bionic-security libgcc1" - apt-get download -t bionic-security libgcc1 - - echo "Purging GH Actions toolchain PPA" - sudo apt-get -y install ppa-purge - sudo ppa-purge -y ubuntu-toolchain-r/test - - echo "installing libgcc1_deb" - sudo dpkg -i * - echo "Installing g++" - sudo apt-get -y install g++ - - cd .. - fi - shell: bash - - name: Checkout uses: actions/checkout@v4 with: @@ -78,6 +57,12 @@ jobs: # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type run: | cmake -B ${{github.workspace}}/_build \ + -DTCP_PUBSUB_BUILD_SAMPLES=ON \ + -DTCP_PUBSUB_BUILD_TESTS=ON \ + -DTCP_PUBSUB_USE_BUILTIN_ASIO=ON \ + -DTCP_PUBSUB_USE_BUILTIN_RECYCLE=ON \ + -DTCP_PUBSUB_USE_BUILTIN_GTEST=ON \ + -DTCP_PUBSUB_LIBRARY_TYPE=${{env.tcp_pubsub_library_type}} \ -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \ -DBUILD_SHARED_LIBS=${{ env.build_shared_libs }} @@ -85,6 +70,10 @@ jobs: # Build your program with the given configuration run: cmake --build ${{github.workspace}}/_build --config ${{env.BUILD_TYPE}} + - name: Run Tests + run: ctest -C Release -V + working-directory: ${{ github.workspace }}/_build + - name: Read Project Version from CMakeCache run: | cmake_project_version_string=$(cat "${{github.workspace}}/_build/CMakeCache.txt" | grep "^CMAKE_PROJECT_VERSION:") @@ -96,18 +85,21 @@ jobs: - name: CPack run: cpack -G DEB working-directory: ${{ github.workspace }}/_build + if: ${{ matrix.library_type != 'object' }} - name: Rename .deb installer run: | mv *.deb '${{ env.PROJECT_NAME }}-${{ env.CMAKE_PROJECT_VERSION }}-${{ matrix.os }}-${{ env.package_postfix }}.deb' shell: bash working-directory: ${{github.workspace}}/_build/_package/ + if: ${{ matrix.library_type != 'object' }} - name: Upload binaries uses: actions/upload-artifact@v4 with: name: ${{ env.PROJECT_NAME }}-${{ env.CMAKE_PROJECT_VERSION }}-${{ matrix.os }}-${{ env.package_postfix }} path: ${{github.workspace}}/_build/_package/*.deb + if: ${{ matrix.library_type != 'object' }} ############################################ # Test if our binary can be linked against @@ -116,24 +108,32 @@ jobs: - name: Install binaries shell: bash run: sudo dpkg -i ${{ github.workspace }}/_build/_package/*.deb + if: ${{ matrix.library_type != 'object' }} - name: Compile integration test (Release) run: | - cmake -B ${{github.workspace}}/samples/integration_test/_build/release -DCMAKE_BUILD_TYPE=Release + cmake -B ${{github.workspace}}/samples/integration_test/_build/release \ + -DCMAKE_BUILD_TYPE=Release cmake --build ${{github.workspace}}/samples/integration_test/_build/release + working-directory: ${{ github.workspace }}/samples/integration_test + if: ${{ matrix.library_type != 'object' }} - name: Run integration test (Release) run: ./integration_test working-directory: ${{ github.workspace }}/samples/integration_test/_build/release + if: ${{ matrix.library_type != 'object' }} - name: Compile integration test (Debug) run: | - cmake -B ${{github.workspace}}/samples/integration_test/_build/debug -DCMAKE_BUILD_TYPE=Debug - cmake --build ${{github.workspace}}/samples/integration_test/_build/debug + cmake -B ${{github.workspace}}/samples/integration_test/_build/debug \ + -DCMAKE_BUILD_TYPE=Debug + cmake --build ${{github.workspace}}/samples/integration_test/_build/debug + working-directory: ${{ github.workspace }}/samples/integration_test + if: ${{ matrix.library_type != 'object' }} - name: Run integration test (Debug) run: ./integration_test working-directory: ${{ github.workspace }}/samples/integration_test/_build/debug - + if: ${{ matrix.library_type != 'object' }} \ No newline at end of file diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 86950f2..d8b420b 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -16,9 +16,10 @@ jobs: build-windows: strategy: + fail-fast: false matrix: - library_type: [static, shared] - build_arch: [x64, win32] + library_type: [static, shared, object] + build_arch: [x64, win32] # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. # You can convert this to a matrix build if you need cross-platform coverage. @@ -31,13 +32,18 @@ jobs: run: | if ( '${{ matrix.library_type }}' -eq 'static' ) { - echo "build_shared_libs=OFF" >> "$Env:GITHUB_ENV" - echo "package_postfix=static" >> "$Env:GITHUB_ENV" + echo "tcp_pubsub_library_type=STATIC" >> "$Env:GITHUB_ENV" + echo "package_postfix=static" >> "$Env:GITHUB_ENV" } - else + elseif( '${{ matrix.library_type }}' -eq 'shared' ) { - echo "build_shared_libs=ON" >> "$Env:GITHUB_ENV" - echo "package_postfix=shared" >> "$Env:GITHUB_ENV" + echo "tcp_pubsub_library_type=SHARED" >> "$Env:GITHUB_ENV" + echo "package_postfix=shared" >> "$Env:GITHUB_ENV" + } + elseif( '${{ matrix.library_type }}' -eq 'object' ) + { + echo "tcp_pubsub_library_type=OBJECT" >> "$Env:GITHUB_ENV" + echo "package_postfix=object" >> "$Env:GITHUB_ENV" } - name: Checkout @@ -59,20 +65,39 @@ jobs: -G "Visual Studio 16 2019" ^ -A ${{ matrix.build_arch }} ^ -T ${{ env.VS_TOOLSET }} ^ - -DCMAKE_INSTALL_PREFIX=${{env.INSTALL_PREFIX}} ^ - -DBUILD_SHARED_LIBS=${{ env.build_shared_libs }} + -DTCP_PUBSUB_BUILD_SAMPLES=ON ^ + -DTCP_PUBSUB_BUILD_TESTS=ON ^ + -DTCP_PUBSUB_USE_BUILTIN_ASIO=ON ^ + -DTCP_PUBSUB_USE_BUILTIN_RECYCLE=ON ^ + -DTCP_PUBSUB_USE_BUILTIN_GTEST=ON ^ + -DTCP_PUBSUB_LIBRARY_TYPE=${{env.tcp_pubsub_library_type}} ^ + -DCMAKE_INSTALL_PREFIX=${{env.INSTALL_PREFIX}} - name: Build (Release) shell: cmd run: | cmake --build ${{github.workspace}}/_build --config Release --parallel + + - name: Install (Release) + shell: cmd + run: | cmake --build ${{github.workspace}}/_build --config Release --target INSTALL + if: ${{ matrix.library_type != 'object' }} - name: Build (Debug) shell: cmd run: | cmake --build ${{github.workspace}}/_build --config Debug --parallel + + - name: Install (Debug) + shell: cmd + run: | cmake --build ${{github.workspace}}/_build --config Debug --target INSTALL + if: ${{ matrix.library_type != 'object' }} + + - name: Run Tests + run: ctest -C Release -V + working-directory: ${{ github.workspace }}/_build - name: Read Project Version from CMakeCache run: | @@ -85,23 +110,27 @@ jobs: with: name: ${{ env.PROJECT_NAME }}-${{ env.CMAKE_PROJECT_VERSION }}-windows-${{ matrix.build_arch }}-${{ env.VS_NAME }}-${{ matrix.library_type }} path: ${{github.workspace}}/${{env.INSTALL_PREFIX}} + if: ${{ matrix.library_type != 'object' }} ############################################ # Test if our binary can be linked against ############################################ - name: CMake integration test - shell: cmd + shell: powershell run: | - cmake -B ${{github.workspace}}/samples/integration_test/_build ^ - -A ${{ matrix.build_arch }} ^ - -DCMAKE_PREFIX_PATH=${{github.workspace}}/${{env.INSTALL_PREFIX}} + cmake -B "${{github.workspace}}/samples/integration_test/_build" ` + -A ${{ matrix.build_arch }} ` + -DCMAKE_PREFIX_PATH="${{github.workspace}}/${{env.INSTALL_PREFIX}}" + working-directory: ${{ github.workspace }}/samples/integration_test + if: ${{ matrix.library_type != 'object' }} - name: Compile integration test (Release) shell: cmd run: cmake --build ${{github.workspace}}/samples/integration_test/_build --config Release working-directory: ${{ github.workspace }}/samples/integration_test + if: ${{ matrix.library_type != 'object' }} - name: Run integration test (Release) run: | @@ -111,11 +140,13 @@ jobs: } .\integration_test.exe working-directory: ${{ github.workspace }}/samples/integration_test/_build/Release + if: ${{ matrix.library_type != 'object' }} - name: Compile integration test (Debug) shell: cmd run: cmake --build ${{github.workspace}}/samples/integration_test/_build --config Debug working-directory: ${{ github.workspace }}/samples/integration_test + if: ${{ matrix.library_type != 'object' }} - name: Run integration test (Debug) run: | @@ -125,3 +156,4 @@ jobs: } .\integration_test.exe working-directory: ${{ github.workspace }}/samples/integration_test/_build/Debug + if: ${{ matrix.library_type != 'object' }} diff --git a/.gitmodules b/.gitmodules index d1f694a..92ab8e1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,9 @@ -[submodule "thirdparty/asio"] - path = thirdparty/asio +[submodule "thirdparty/asio/asio"] + path = thirdparty/asio/asio url = https://github.com/chriskohlhoff/asio.git -[submodule "thirdparty/recycle"] - path = thirdparty/recycle +[submodule "thirdparty/recycle/recycle"] + path = thirdparty/recycle/recycle url = https://github.com/steinwurf/recycle.git +[submodule "thirdparty/gtest/googletest"] + path = thirdparty/gtest/googletest + url = https://github.com/google/googletest.git diff --git a/CMakeLists.txt b/CMakeLists.txt index cdf73fa..92b25eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,6 @@ -cmake_minimum_required(VERSION 3.5.1) +cmake_minimum_required(VERSION 3.13) + +include(CMakeDependentOption) # Project call include("${CMAKE_CURRENT_LIST_DIR}/tcp_pubsub/version.cmake") @@ -19,6 +21,25 @@ option(TCP_PUBSUB_BUILD_ECAL_SAMPLES "Build eCAL-based project samples. Requires eCAL to be findable by CMake." OFF) +option(TCP_PUBSUB_USE_BUILTIN_ASIO + "Use the builtin asio submodule. If set to OFF, asio must be available from somewhere else (e.g. system libs)." + ON) + +option(TCP_PUBSUB_USE_BUILTIN_RECYCLE + "Use the builtin steinwurf::recycle submodule. If set to OFF, recycle must be available from somewhere else (e.g. system libs)." + ON) + +option(TCP_PUBSUB_BUILD_TESTS + "Build the tcp_pubsub tests. Requires Gtest::GTest to be findable by CMake." + OFF) + +cmake_dependent_option(TCP_PUBSUB_USE_BUILTIN_GTEST + "Use the builtin GoogleTest submodule. Only needed if TCP_PUBSUB_BUILD_TESTS is ON. If set to OFF, GoogleTest must be available from somewhere else (e.g. system libs)." + ON # Default value if dependency is met + "TCP_PUBSUB_BUILD_TESTS" # Dependency + OFF) # Default value if dependency is not met + + # Module path for finding asio list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/modules) @@ -27,12 +48,34 @@ set(CMAKE_DEBUG_POSTFIX d) set(CMAKE_MINSIZEREL_POSTFIX minsize) set(CMAKE_RELWITHDEBINFO_POSTFIX reldbg) + +# Use builtin asio +if (TCP_PUBSUB_USE_BUILTIN_ASIO) + include("${CMAKE_CURRENT_LIST_DIR}/thirdparty/asio/build-asio.cmake") +endif() + +# Use builtin recycle +if (TCP_PUBSUB_USE_BUILTIN_RECYCLE) + include("${CMAKE_CURRENT_LIST_DIR}/thirdparty/recycle/build-recycle.cmake") +endif() + +# Use builtin gtest +if (TCP_PUBSUB_USE_BUILTIN_GTEST) + include("${CMAKE_CURRENT_LIST_DIR}/thirdparty/gtest/build-gtest.cmake") +endif() + +# For tests we need to make sure that all shared libraries and executables are +# put into the same directory. Otherwise the tests will fail on windows. +if(TCP_PUBSUB_BUILD_TESTS AND (BUILD_SHARED_LIBS OR (TCP_PUBSUB_LIBRARY_TYPE STREQUAL "SHARED"))) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +endif() + +# Module path for finding tcp_pubsub +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/tcp_pubsub/Module) + # Add main tcp_pubsub library add_subdirectory(tcp_pubsub) -# Recycle dependency. It's header only and not in the API, so we add it with EXCLUDE_FOR_ALL, so it won't be installed -add_subdirectory(thirdparty/recycle EXCLUDE_FROM_ALL) - # Generic samples if (TCP_PUBSUB_BUILD_SAMPLES) add_subdirectory(samples/performance_publisher) @@ -47,5 +90,12 @@ if(TCP_PUBSUB_BUILD_ECAL_SAMPLES) add_subdirectory(samples/tcp_to_ecal) endif() + +# Add Tests if enabled +if (TCP_PUBSUB_BUILD_TESTS) + enable_testing() + add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/tests/tcp_pubsub_test") +endif() + # Make this package available for packing with CPack include("${CMAKE_CURRENT_LIST_DIR}/cpack_config.cmake") diff --git a/README.md b/README.md index 78eebc3..a28b92b 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,20 @@ int main() } ``` +## CMake Options + +You can set the following CMake Options to control how tcp_pubsub is built: + +| Option | Type | Default | Explanation | +|------------------------------------|-------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------| +| `TCP_PUBSUB_BUILD_SAMPLES` | `BOOL`| `ON` | Build project samples. | +| `TCP_PUBSUB_BUILD_ECAL_SAMPLES` | `BOOL`| `OFF` | Build eCAL-based project samples. Requires eCAL to be findable by CMake. | +| `TCP_PUBSUB_USE_BUILTIN_ASIO` | `BOOL`| `ON` | Use the builtin asio submodule. If set to `OFF`, asio must be available from somewhere else (e.g. system libs). | +| `TCP_PUBSUB_USE_BUILTIN_RECYCLE` | `BOOL`| `ON` | Use the builtin `steinwurf::recycle` submodule. If set to `OFF`, recycle must be available from somewhere else (e.g. system libs). | +| `TCP_PUBSUB_BUILD_TESTS` | `BOOL`| `OFF` | Build the tcp_pubsub tests. Requires Gtest::GTest to be findable by CMake. | +| `TCP_PUBSUB_USE_BUILTIN_GTEST` | `BOOL`| `ON` (if building tests) | Use the builtin GoogleTest submodule. Only needed if `TCP_PUBSUB_BUILD_TESTS` is `ON`. If set to `OFF`, GoogleTest must be available from elsewhere. | +| `TCP_PUBSUB_LIBRARY_TYPE` | `STRING` | | Controls the library type of tcp_pubsub by injecting the string into the `add_library` call. Can be set to STATIC / SHARED / OBJECT. If set, this will override the regular `BUILD_SHARED_LIBS` CMake option. If not set, CMake will use the default setting, which is controlled by `BUILD_SHARED_LIBS`. | + ## How to checkout and build There are several examples provided that aim to show you the functionality. diff --git a/tcp_pubsub/CMakeLists.txt b/tcp_pubsub/CMakeLists.txt index 754d236..025fcbc 100644 --- a/tcp_pubsub/CMakeLists.txt +++ b/tcp_pubsub/CMakeLists.txt @@ -48,7 +48,7 @@ set(sources src/tcp_pubsub_logger_abstraction.h ) -add_library (${PROJECT_NAME} +add_library (${PROJECT_NAME} ${TCP_PUBSUB_LIBRARY_TYPE} ${includes} ${sources} ) @@ -83,6 +83,16 @@ target_compile_definitions(${PROJECT_NAME} _WIN32_WINNT=0x0601 ) +# Check if tcp_pubsub is an object library. If so, define TCP_PUBSUB_STATIC_DEFINE, +# so the CMake generated export header will be correct. +get_target_property(tcp_pubsub_target_type tcp_pubsub TYPE) +if (tcp_pubsub_target_type STREQUAL OBJECT_LIBRARY) + target_compile_definitions(${PROJECT_NAME} + PUBLIC + TCP_PUBSUB_STATIC_DEFINE + ) +endif() + target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_14) target_compile_options(${PROJECT_NAME} PRIVATE @@ -110,8 +120,10 @@ set_target_properties(${PROJECT_NAME} PROPERTIES ################################## -include(sourcetree.cmake) -create_source_tree(${includes} ${sources}) +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES + ${includes} + ${sources} +) ################################################################################ ### Installation rules diff --git a/tcp_pubsub/sourcetree.cmake b/tcp_pubsub/sourcetree.cmake deleted file mode 100644 index 04969f5..0000000 --- a/tcp_pubsub/sourcetree.cmake +++ /dev/null @@ -1,12 +0,0 @@ -function(create_source_tree) - foreach(_source IN ITEMS ${ARGN}) - if (IS_ABSOLUTE "${_source}") - file(RELATIVE_PATH _source_rel "${CMAKE_CURRENT_SOURCE_DIR}" "${_source}") - else() - set(_source_rel "${_source}") - endif() - get_filename_component(_source_path "${_source_rel}" PATH) - string(REPLACE "/" "\\" _source_path_msvc "${_source_path}") - source_group("${_source_path_msvc}" FILES "${_source}") - endforeach() -endfunction(create_source_tree) diff --git a/tests/tcp_pubsub_test/CMakeLists.txt b/tests/tcp_pubsub_test/CMakeLists.txt new file mode 100644 index 0000000..b25dff0 --- /dev/null +++ b/tests/tcp_pubsub_test/CMakeLists.txt @@ -0,0 +1,36 @@ +################################################################################ +# Copyright (c) Continental. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for details. +# +# SPDX-License-Identifier: MIT +################################################################################ + +cmake_minimum_required(VERSION 3.13) + +project(tcp_pubsub_test) + +set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) + +find_package(tcp_pubsub REQUIRED) +find_package(GTest REQUIRED) + +set(sources + src/atomic_signalable.h + src/tcp_pubsub_test.cpp +) + +add_executable (${PROJECT_NAME} + ${sources} +) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_14) + +target_link_libraries (${PROJECT_NAME} + tcp_pubsub::tcp_pubsub + GTest::gtest_main +) + +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${sources}) + +include(GoogleTest) +gtest_discover_tests(${PROJECT_NAME}) \ No newline at end of file diff --git a/tests/tcp_pubsub_test/src/atomic_signalable.h b/tests/tcp_pubsub_test/src/atomic_signalable.h new file mode 100644 index 0000000..ab95f5e --- /dev/null +++ b/tests/tcp_pubsub_test/src/atomic_signalable.h @@ -0,0 +1,194 @@ +// Copyright (c) Continental. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include + +template +class atomic_signalable +{ +public: + atomic_signalable(T initial_value) : value(initial_value) {} + + atomic_signalable& operator=(const T new_value) + { + const std::lock_guard lock(mutex); + value = new_value; + cv.notify_all(); + return *this; + } + + T operator++() + { + const std::lock_guard lock(mutex); + T newValue = ++value; + cv.notify_all(); + return newValue; + } + + T operator++(T) + { + const std::lock_guard lock(mutex); + T oldValue = value++; + cv.notify_all(); + return oldValue; + } + + T operator--() + { + const std::lock_guard lock(mutex); + T newValue = --value; + cv.notify_all(); + return newValue; + } + + T operator--(T) + { + const std::lock_guard lock(mutex); + T oldValue = value--; + cv.notify_all(); + return oldValue; + } + + T operator+=(const T& other) + { + const std::lock_guard lock(mutex); + value += other; + cv.notify_all(); + return value; + } + + T operator-=(const T& other) + { + const std::lock_guard lock(mutex); + value -= other; + cv.notify_all(); + return value; + } + + T operator*=(const T& other) + { + const std::lock_guard lock(mutex); + value *= other; + cv.notify_all(); + return value; + } + + T operator/=(const T& other) + { + const std::lock_guard lock(mutex); + value /= other; + cv.notify_all(); + return value; + } + + T operator%=(const T& other) + { + const std::lock_guard lock(mutex); + value %= other; + cv.notify_all(); + return value; + } + + template + bool wait_for(Predicate predicate, std::chrono::milliseconds timeout) + { + std::unique_lock lock(mutex); + return cv.wait_for(lock, timeout, [&]() { return predicate(value); }); + } + + T get() const + { + const std::lock_guard lock(mutex); + return value; + } + + bool operator==(T other) const + { + const std::lock_guard lock(mutex); + return value == other; + } + + bool operator==(const atomic_signalable& other) const + { + std::lock_guard lock_this(mutex); + std::lock_guard lock_other(other.mutex); + return value == other.value; + } + + bool operator!=(T other) const + { + const std::lock_guard lock(mutex); + return value != other; + } + + bool operator<(T other) const + { + const std::lock_guard lock(mutex); + return value < other; + } + + bool operator<=(T other) const + { + const std::lock_guard lock(mutex); + return value <= other; + } + + bool operator>(T other) const + { + const std::lock_guard lock(mutex); + return value > other; + } + + bool operator>=(T other) const + { + const std::lock_guard lock(mutex); + return value >= other; + } + +private: + T value; + std::condition_variable cv; + mutable std::mutex mutex; +}; + + +template +bool operator==(const T& other, const atomic_signalable& atomic) +{ + return atomic == other; +} + +template +bool operator!=(const T& other, const atomic_signalable& atomic) +{ + return atomic != other; +} + +template +bool operator<(const T& other, const atomic_signalable& atomic) +{ + return atomic > other; +} + +template +bool operator<=(const T& other, const atomic_signalable& atomic) +{ + return atomic >= other; +} + +template +bool operator>(const T& other, const atomic_signalable& atomic) +{ + return atomic < other; +} + +template +bool operator>=(const T& other, const atomic_signalable& atomic) +{ + return atomic <= other; +} diff --git a/tests/tcp_pubsub_test/src/tcp_pubsub_test.cpp b/tests/tcp_pubsub_test/src/tcp_pubsub_test.cpp new file mode 100644 index 0000000..d42b10d --- /dev/null +++ b/tests/tcp_pubsub_test/src/tcp_pubsub_test.cpp @@ -0,0 +1,314 @@ +// Copyright (c) Continental. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "atomic_signalable.h" + +#include + +// Basic test that sends two messages from a publisher to a subscriber +TEST(tcp_pubsub, basic_test) +{ + atomic_signalable num_messages_received(0); + + // Create executor + std::shared_ptr executor = std::make_shared(1, tcp_pubsub::logger::logger_no_verbose_debug); + + // Create publisher + tcp_pubsub::Publisher hello_world_publisher(executor, 1588); + + // Create subscriber + tcp_pubsub::Subscriber hello_world_subscriber(executor); + + // Subscribe to localhost on port 1588 + hello_world_subscriber.addSession("127.0.0.1", 1588); + + std::string received_message; + + // Create a callback that will be called when a message is received + std::function callback_function = + [&received_message, &num_messages_received](const tcp_pubsub::CallbackData& callback_data) + { + received_message = std::string(callback_data.buffer_->data(), callback_data.buffer_->size()); + ++num_messages_received; + }; + + // Register the callback + hello_world_subscriber.setCallback(callback_function); + + // Wait up to 1 second for the subscriber to connect + for (int i = 0; i < 10; ++i) + { + if (hello_world_subscriber.getSessions().at(0)->isConnected() + && hello_world_publisher.getSubscriberCount() >= 1) + { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + // Check that the subscriber is connected + EXPECT_TRUE(hello_world_subscriber.getSessions().at(0)->isConnected()); + EXPECT_EQ(hello_world_publisher.getSubscriberCount(), 1); + + // Publish "Hello World 1" + { + const std::string message = "Hello World 1"; + hello_world_publisher.send(message.data(), message.size()); + } + + // wait for message to be received + num_messages_received.wait_for([](int value) { return value > 0; }, std::chrono::seconds(1)); + + // Check that the message was received + EXPECT_EQ(received_message, "Hello World 1"); + EXPECT_EQ(num_messages_received.get(), 1); + + // Publish "Hello World 2" + { + const std::string message = "Hello World 2"; + hello_world_publisher.send(message.data(), message.size()); + } + + // wait for message to be received + num_messages_received.wait_for([](int value) { return value > 1; }, std::chrono::seconds(1)); + + // Check that the message was received + EXPECT_EQ(received_message, "Hello World 2"); + EXPECT_EQ(num_messages_received.get(), 2); +} + +// Test that sends a very large message from a publisher to a subscriber +TEST(tcp_pubsub, large_message_test) +{ + constexpr size_t message_size = 1024 * 1024 * 16; + + atomic_signalable num_messages_received(0); + + // Create executor + std::shared_ptr executor = std::make_shared(1, tcp_pubsub::logger::logger_no_verbose_debug); + + // Create publisher + tcp_pubsub::Publisher hello_world_publisher(executor, 1588); + + // Create subscriber + tcp_pubsub::Subscriber hello_world_subscriber(executor); + + // Subscribe to localhost on port 1588 + hello_world_subscriber.addSession("127.0.0.1", 1588); + + std::string received_message; + + // Create a callback that will be called when a message is received + std::function callback_function = + [&received_message, &num_messages_received](const tcp_pubsub::CallbackData& callback_data) + { + received_message = std::string(callback_data.buffer_->data(), callback_data.buffer_->size()); + ++num_messages_received; + }; + + // Register the callback + hello_world_subscriber.setCallback(callback_function); + + // Wait up to 1 second for the subscriber to connect + for (int i = 0; i < 10; ++i) + { + if (hello_world_subscriber.getSessions().at(0)->isConnected() + && hello_world_publisher.getSubscriberCount() >= 1) + { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + // Check that the subscriber is connected + EXPECT_TRUE(hello_world_subscriber.getSessions().at(0)->isConnected()); + EXPECT_EQ(hello_world_publisher.getSubscriberCount(), 1); + + // Create a large message consisting of random bytes + std::string message; + message.resize(message_size); + for (size_t i = 0; i < message_size; ++i) + { + message[i] = static_cast(rand() % 256); + } + + // Publish the large message + hello_world_publisher.send(message.data(), message.size()); + + // wait for message to be received + num_messages_received.wait_for([](int value) { return value > 0; }, std::chrono::seconds(1)); + + // Check that the message was received + EXPECT_EQ(received_message, message); + EXPECT_EQ(num_messages_received.get(), 1); +} + +// Test that sends messages from 2 publishers to a single subscriber +TEST(tcp_pubsub, multiple_publishers_test) +{ + atomic_signalable num_messages_received(0); + + // Create executor + std::shared_ptr executor = std::make_shared(1, tcp_pubsub::logger::logger_no_verbose_debug); + + // Create publisher 1 + tcp_pubsub::Publisher hello_world_publisher1(executor, 1588); + + // Create publisher 2 + tcp_pubsub::Publisher hello_world_publisher2(executor, 1589); + + // Create subscriber + tcp_pubsub::Subscriber hello_world_subscriber(executor); + + // Subscribe to localhost on port 1588 + hello_world_subscriber.addSession("127.0.0.1", 1588); + hello_world_subscriber.addSession("127.0.0.1", 1589); + + std::string received_message; + + // Create a callback that will be called when a message is received + std::function callback_function = + [&received_message, &num_messages_received](const tcp_pubsub::CallbackData& callback_data) + { + received_message = std::string(callback_data.buffer_->data(), callback_data.buffer_->size()); + ++num_messages_received; + }; + + // Register the callback + hello_world_subscriber.setCallback(callback_function); + + // Wait up to 1 second for the subscriber to connect + for (int i = 0; i < 10; ++i) + { + if (hello_world_subscriber.getSessions().at(0)->isConnected() + && hello_world_subscriber.getSessions().at(1)->isConnected() + && hello_world_publisher1.getSubscriberCount() >= 1 + && hello_world_publisher2.getSubscriberCount() >= 1) + { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + // Check that the subscriber is connected + EXPECT_TRUE(hello_world_subscriber.getSessions().at(0)->isConnected()); + EXPECT_TRUE(hello_world_subscriber.getSessions().at(1)->isConnected()); + EXPECT_EQ(hello_world_publisher1.getSubscriberCount(), 1); + EXPECT_EQ(hello_world_publisher2.getSubscriberCount(), 1); + + // Publish "Hello World 1" + { + const std::string message = "Hello World 1"; + hello_world_publisher1.send(message.data(), message.size()); + } + + // wait for message to be received + num_messages_received.wait_for([](int value) { return value > 0; }, std::chrono::seconds(1)); + + // Check that the message was received + EXPECT_EQ(received_message, "Hello World 1"); + EXPECT_EQ(num_messages_received.get(), 1); + + // Publish "Hello World 2" + { + const std::string message = "Hello World 2"; + hello_world_publisher2.send(message.data(), message.size()); + } + + // wait for message to be received + num_messages_received.wait_for([](int value) { return value > 1; }, std::chrono::seconds(1)); + + // Check that the message was received + EXPECT_EQ(received_message, "Hello World 2"); + EXPECT_EQ(num_messages_received.get(), 2); +} + +// Test that sends messages from a single publisher to 2 subscribers +TEST(tcp_pubsub, multiple_subscribers_test) +{ + atomic_signalable num_messages_received1(0); + atomic_signalable num_messages_received2(0); + + // Create executor + std::shared_ptr executor = std::make_shared(1, tcp_pubsub::logger::logger_no_verbose_debug); + + // Create publisher + tcp_pubsub::Publisher hello_world_publisher(executor, 1588); + + // Create subscriber 1 + tcp_pubsub::Subscriber hello_world_subscriber1(executor); + + // Create subscriber 2 + tcp_pubsub::Subscriber hello_world_subscriber2(executor); + + // Subscribe to localhost on port 1588 + hello_world_subscriber1.addSession("127.0.0.1", 1588); + hello_world_subscriber2.addSession("127.0.0.1", 1588); + + std::string received_message1; + std::string received_message2; + + // Create a callback that will be called when a message is received + std::function callback_function1 = + [&received_message1, &num_messages_received1](const tcp_pubsub::CallbackData& callback_data) + { + received_message1 = std::string(callback_data.buffer_->data(), callback_data.buffer_->size()); + ++num_messages_received1; + }; + + std::function callback_function2 = + [&received_message2, &num_messages_received2](const tcp_pubsub::CallbackData& callback_data) + { + received_message2 = std::string(callback_data.buffer_->data(), callback_data.buffer_->size()); + ++num_messages_received2; + }; + + // Register the callback + hello_world_subscriber1.setCallback(callback_function1); + hello_world_subscriber2.setCallback(callback_function2); + + // Wait up to 1 second for the subscriber to connect + for (int i = 0; i < 10; ++i) + { + if (hello_world_subscriber1.getSessions().at(0)->isConnected() + && hello_world_subscriber2.getSessions().at(0)->isConnected() + && hello_world_publisher.getSubscriberCount() >= 2) + { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + // Check that the subscriber is connected + EXPECT_TRUE(hello_world_subscriber1.getSessions().at(0)->isConnected()); + EXPECT_TRUE(hello_world_subscriber2.getSessions().at(0)->isConnected()); + EXPECT_EQ(hello_world_publisher.getSubscriberCount(), 2); + + // Publish "Hello World 1" + { + const std::string message = "Hello World 1"; + hello_world_publisher.send(message.data(), message.size()); + } + + // wait for message to be received + num_messages_received1.wait_for([](int value) { return value > 0; }, std::chrono::seconds(1)); + num_messages_received2.wait_for([](int value) { return value > 0; }, std::chrono::seconds(1)); + + // Check that the message was received + EXPECT_EQ(received_message1, "Hello World 1"); + EXPECT_EQ(num_messages_received1.get(), 1); + EXPECT_EQ(received_message2, "Hello World 1"); + EXPECT_EQ(num_messages_received2.get(), 1); +} diff --git a/thirdparty/asio b/thirdparty/asio deleted file mode 160000 index dc67374..0000000 --- a/thirdparty/asio +++ /dev/null @@ -1 +0,0 @@ -Subproject commit dc67374fce1051b57ade8838c9aac6642e9bbf86 diff --git a/thirdparty/modules/Findasio.cmake b/thirdparty/asio/Module/Findasio.cmake similarity index 99% rename from thirdparty/modules/Findasio.cmake rename to thirdparty/asio/Module/Findasio.cmake index e2e12de..10ba9ed 100644 --- a/thirdparty/modules/Findasio.cmake +++ b/thirdparty/asio/Module/Findasio.cmake @@ -27,4 +27,4 @@ if(asio_FOUND) INTERFACE_COMPILE_DEFINITIONS ASIO_STANDALONE) mark_as_advanced(asio_INCLUDE_DIR) endif() -endif() +endif() \ No newline at end of file diff --git a/thirdparty/asio/asio b/thirdparty/asio/asio new file mode 160000 index 0000000..03ae834 --- /dev/null +++ b/thirdparty/asio/asio @@ -0,0 +1 @@ +Subproject commit 03ae834edbace31a96157b89bf50e5ee464e5ef9 diff --git a/thirdparty/asio/build-asio.cmake b/thirdparty/asio/build-asio.cmake new file mode 100644 index 0000000..b7c0a2d --- /dev/null +++ b/thirdparty/asio/build-asio.cmake @@ -0,0 +1 @@ +list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_LIST_DIR}/Module") \ No newline at end of file diff --git a/thirdparty/gtest/Module/FindGTest.cmake b/thirdparty/gtest/Module/FindGTest.cmake new file mode 100644 index 0000000..f62c081 --- /dev/null +++ b/thirdparty/gtest/Module/FindGTest.cmake @@ -0,0 +1 @@ +set(GTest_FOUND TRUE CACHE BOOL "Found Google Test" FORCE) \ No newline at end of file diff --git a/thirdparty/gtest/build-gtest.cmake b/thirdparty/gtest/build-gtest.cmake new file mode 100644 index 0000000..d67fe7b --- /dev/null +++ b/thirdparty/gtest/build-gtest.cmake @@ -0,0 +1,19 @@ +# Googletest automatically forces MT instead of MD if we do not set this option. +if(MSVC) + set(gtest_force_shared_crt ON CACHE BOOL "My option" FORCE) + set(BUILD_GMOCK OFF CACHE BOOL "My option" FORCE) + set(INSTALL_GTEST OFF CACHE BOOL "My option" FORCE) +endif() + +add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/googletest" EXCLUDE_FROM_ALL) + +if(NOT TARGET GTest::gtest) + add_library(GTest::gtest ALIAS gtest) +endif() + +if(NOT TARGET GTest::gtest_main) + add_library(GTest::gtest_main ALIAS gtest_main) +endif() + +# Prepend googletest-module/FindGTest.cmake to Module Path +list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_LIST_DIR}/Module") \ No newline at end of file diff --git a/thirdparty/gtest/googletest b/thirdparty/gtest/googletest new file mode 160000 index 0000000..d144031 --- /dev/null +++ b/thirdparty/gtest/googletest @@ -0,0 +1 @@ +Subproject commit d144031940543e15423a25ae5a8a74141044862f diff --git a/thirdparty/modules/Findrecycle.cmake b/thirdparty/modules/Findrecycle.cmake deleted file mode 100644 index bece5d0..0000000 --- a/thirdparty/modules/Findrecycle.cmake +++ /dev/null @@ -1,2 +0,0 @@ -# we're "building" recycle as part of tcp_pubsub so it will always be found. -set(recycle_FOUND True) diff --git a/thirdparty/recycle/Module/Findrecycle.cmake b/thirdparty/recycle/Module/Findrecycle.cmake new file mode 100644 index 0000000..bd3e37b --- /dev/null +++ b/thirdparty/recycle/Module/Findrecycle.cmake @@ -0,0 +1 @@ +set(recycle_FOUND TRUE CACHE BOOL "Found steinwurf::recycle" FORCE) \ No newline at end of file diff --git a/thirdparty/recycle/build-recycle.cmake b/thirdparty/recycle/build-recycle.cmake new file mode 100644 index 0000000..108892b --- /dev/null +++ b/thirdparty/recycle/build-recycle.cmake @@ -0,0 +1,5 @@ +add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/recycle" EXCLUDE_FROM_ALL) +add_library(steinwurf::recycle ALIAS recycle) + +# Prepend asio-module/Findrecycle.cmake to the module path +list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_LIST_DIR}/Module") \ No newline at end of file diff --git a/thirdparty/recycle b/thirdparty/recycle/recycle similarity index 100% rename from thirdparty/recycle rename to thirdparty/recycle/recycle diff --git a/thirdparty/modules/Findtcp_pubsub.cmake b/thirdparty/tcp_pubsub/Module/Findtcp_pubsub.cmake similarity index 64% rename from thirdparty/modules/Findtcp_pubsub.cmake rename to thirdparty/tcp_pubsub/Module/Findtcp_pubsub.cmake index 4e1f17e..9f89133 100644 --- a/thirdparty/modules/Findtcp_pubsub.cmake +++ b/thirdparty/tcp_pubsub/Module/Findtcp_pubsub.cmake @@ -1,2 +1,2 @@ # Stub find script for in-source build of samples -set(tcp_pubsub_FOUND True) +set(tcp_pubsub_FOUND True) \ No newline at end of file