diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index f55663c3..27e24aca 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -63,23 +63,23 @@ jobs: include: - os: ubuntu-latest name: linux-x64 - build_cmd: ./build.sh release-examples + build_cmd: ./build.sh release-tests && ./build.sh release-examples build_dir: build-release - os: ubuntu-24.04-arm name: linux-arm64 - build_cmd: ./build.sh release-examples + build_cmd: ./build.sh release-tests && ./build.sh release-examples build_dir: build-release - os: macos-latest name: macos-arm64 - build_cmd: ./build.sh release-examples + build_cmd: ./build.sh release-tests && ./build.sh release-examples build_dir: build-release - os: macos-latest name: macos-x64 - build_cmd: ./build.sh release-examples --macos-arch x86_64 + build_cmd: ./build.sh release-tests && ./build.sh release-examples --macos-arch x86_64 build_dir: build-release - os: windows-latest name: windows-x64 - build_cmd: .\build.cmd release-examples + build_cmd: .\build.cmd release-tests && .\build.cmd release-examples build_dir: build-release name: Build (${{ matrix.name }}) @@ -90,7 +90,10 @@ jobs: uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: submodules: recursive - fetch-depth: 0 + fetch-depth: 1 + + - name: Pull LFS files + run: git lfs pull # ---------- vcpkg caching for Windows ---------- - name: Export GitHub Actions cache environment variables @@ -283,6 +286,29 @@ jobs: } if ($failed) { exit 1 } else { exit 0 } + # ---------- Run unit tests ---------- + - name: Run unit tests (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + ${{ matrix.build_dir }}/bin/livekit_unit_tests \ + --gtest_output=xml:${{ matrix.build_dir }}/unit-test-results.xml + + - name: Run unit tests (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + & "${{ matrix.build_dir }}/bin/livekit_unit_tests.exe" ` + --gtest_output=xml:${{ matrix.build_dir }}/unit-test-results.xml + + - name: Upload test results + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: test-results-${{ matrix.name }} + path: ${{ matrix.build_dir }}/unit-test-results.xml + retention-days: 7 + # ---------- Upload artifacts ---------- - name: Upload build artifacts uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 diff --git a/README.md b/README.md index 0f1cb389..04b5ecfa 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,9 @@ git clone --recurse-submodules https://github.com/livekit/client-sdk-cpp.git git clone https://github.com/livekit/client-sdk-cpp.git cd client-sdk-cpp git submodule update --init --recursive + +# Note: If running tests, pull Git LFS to bring in test data: +git lfs pull ``` ## ⚙️ BUILD diff --git a/bridge/include/livekit_bridge/rpc_constants.h b/bridge/include/livekit_bridge/rpc_constants.h index 3511239f..2c08df96 100644 --- a/bridge/include/livekit_bridge/rpc_constants.h +++ b/bridge/include/livekit_bridge/rpc_constants.h @@ -21,6 +21,16 @@ #include +#ifdef _WIN32 +#ifdef livekit_bridge_EXPORTS +#define LIVEKIT_BRIDGE_API __declspec(dllexport) +#else +#define LIVEKIT_BRIDGE_API __declspec(dllimport) +#endif +#else +#define LIVEKIT_BRIDGE_API +#endif + namespace livekit_bridge { namespace rpc { @@ -34,20 +44,21 @@ namespace track_control { enum class Action { kActionMute, kActionUnmute }; /// RPC method name registered by the bridge for remote track control. -extern const char *const kMethod; +LIVEKIT_BRIDGE_API extern const char *const kMethod; /// Payload action strings. -extern const char *const kActionMute; -extern const char *const kActionUnmute; +LIVEKIT_BRIDGE_API extern const char *const kActionMute; +LIVEKIT_BRIDGE_API extern const char *const kActionUnmute; /// Delimiter between action and track name in the payload (e.g. "mute:cam"). -extern const char kDelimiter; +LIVEKIT_BRIDGE_API extern const char kDelimiter; /// Response payload returned on success. -extern const char *const kResponseOk; +LIVEKIT_BRIDGE_API extern const char *const kResponseOk; /// Build a track-control RPC payload: ":". -std::string formatPayload(const char *action, const std::string &track_name); +LIVEKIT_BRIDGE_API std::string formatPayload(const char *action, + const std::string &track_name); } // namespace track_control } // namespace rpc diff --git a/bridge/tests/CMakeLists.txt b/bridge/tests/CMakeLists.txt index c42274b8..227f0a0c 100644 --- a/bridge/tests/CMakeLists.txt +++ b/bridge/tests/CMakeLists.txt @@ -93,6 +93,7 @@ if(BRIDGE_TEST_SOURCES) # Register tests with CTest gtest_discover_tests(livekit_bridge_tests WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} + DISCOVERY_MODE PRE_TEST PROPERTIES LABELS "bridge_unit" ) diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 1ca804d3..8d06be87 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -24,6 +24,81 @@ FetchContent_MakeAvailable(googletest) enable_testing() include(GoogleTest) +# ============================================================================ +# Unit Tests +# ============================================================================ + +file(GLOB UNIT_TEST_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/unit/*.cpp" +) + +if(UNIT_TEST_SOURCES) + add_executable(livekit_unit_tests + ${UNIT_TEST_SOURCES} + ) + + target_link_libraries(livekit_unit_tests + PRIVATE + livekit + spdlog::spdlog + GTest::gtest_main + GTest::gmock + ) + + target_include_directories(livekit_unit_tests + PRIVATE + ${LIVEKIT_ROOT_DIR}/include + ${LIVEKIT_ROOT_DIR}/src + ) + + target_compile_definitions(livekit_unit_tests + PRIVATE + LIVEKIT_TEST_ACCESS + LIVEKIT_ROOT_DIR="${LIVEKIT_ROOT_DIR}" + SPDLOG_ACTIVE_LEVEL=${_SPDLOG_ACTIVE_LEVEL} + $<$:_USE_MATH_DEFINES> + ) + + if(WIN32) + add_custom_command(TARGET livekit_unit_tests POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$/livekit_ffi.dll" + $ + COMMENT "Copying DLLs to unit test directory" + ) + elseif(APPLE) + add_custom_command(TARGET livekit_unit_tests POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$/liblivekit_ffi.dylib" + $ + COMMENT "Copying dylibs to unit test directory" + ) + else() + add_custom_command(TARGET livekit_unit_tests POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$/liblivekit_ffi.so" + $ + COMMENT "Copying shared libraries to unit test directory" + ) + endif() + + gtest_discover_tests(livekit_unit_tests + WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} + DISCOVERY_MODE PRE_TEST + PROPERTIES + LABELS "unit" + ) +endif() + # ============================================================================ # Integration Tests # ============================================================================ @@ -38,10 +113,18 @@ if(INTEGRATION_TEST_SOURCES) ${INTEGRATION_TEST_SOURCES} ) + # On Windows, protobuf default-instance symbols (constinit globals) are not + # auto-exported from livekit.dll by WINDOWS_EXPORT_ALL_SYMBOLS. Link the + # proto object library directly so the test binary has its own copy. + if(WIN32 AND TARGET livekit_proto) + target_sources(livekit_integration_tests PRIVATE $) + endif() + target_link_libraries(livekit_integration_tests PRIVATE livekit spdlog::spdlog + $<$:${LIVEKIT_PROTOBUF_TARGET}> GTest::gtest_main GTest::gmock ) @@ -67,6 +150,7 @@ if(INTEGRATION_TEST_SOURCES) LIVEKIT_TEST_ACCESS LIVEKIT_ROOT_DIR="${LIVEKIT_ROOT_DIR}" SPDLOG_ACTIVE_LEVEL=${_SPDLOG_ACTIVE_LEVEL} + $<$:_USE_MATH_DEFINES> ) # Copy shared libraries to test executable directory @@ -105,6 +189,7 @@ if(INTEGRATION_TEST_SOURCES) # Register tests with CTest gtest_discover_tests(livekit_integration_tests WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} + DISCOVERY_MODE PRE_TEST PROPERTIES LABELS "integration" ) @@ -137,6 +222,11 @@ if(STRESS_TEST_SOURCES) ${LIVEKIT_ROOT_DIR}/src ) + target_compile_definitions(livekit_stress_tests + PRIVATE + $<$:_USE_MATH_DEFINES> + ) + # Copy shared libraries to test executable directory if(WIN32) add_custom_command(TARGET livekit_stress_tests POST_BUILD @@ -173,6 +263,7 @@ if(STRESS_TEST_SOURCES) # Register tests with CTest (longer timeout for stress tests) gtest_discover_tests(livekit_stress_tests WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} + DISCOVERY_MODE PRE_TEST PROPERTIES LABELS "stress" TIMEOUT 300 @@ -189,6 +280,10 @@ add_custom_target(run_all_tests COMMENT "Running all tests" ) +if(TARGET livekit_unit_tests) + add_dependencies(run_all_tests livekit_unit_tests) +endif() + if(TARGET livekit_integration_tests) add_dependencies(run_all_tests livekit_integration_tests) endif() diff --git a/src/tests/integration/test_audio_frame.cpp b/src/tests/unit/test_audio_frame.cpp similarity index 100% rename from src/tests/integration/test_audio_frame.cpp rename to src/tests/unit/test_audio_frame.cpp diff --git a/src/tests/integration/test_audio_processing_module.cpp b/src/tests/unit/test_audio_processing_module.cpp similarity index 99% rename from src/tests/integration/test_audio_processing_module.cpp rename to src/tests/unit/test_audio_processing_module.cpp index 4c4b48cf..2171fa40 100644 --- a/src/tests/integration/test_audio_processing_module.cpp +++ b/src/tests/unit/test_audio_processing_module.cpp @@ -802,9 +802,7 @@ TEST_F(AudioProcessingModuleTest, AGCAttenuatesLoudSpeech) { int num_channels = 0; std::string wav_path = std::string(LIVEKIT_ROOT_DIR) + "/data/welcome.wav"; - if (!readWavFile(wav_path, original_samples, sample_rate, num_channels)) { - GTEST_SKIP() << "Could not read " << wav_path; - } + ASSERT_TRUE(readWavFile(wav_path, original_samples, sample_rate, num_channels)) << "Could not read " << wav_path << " (is Git LFS pulled?)"; std::cout << "[AGC-LoudSpeech] Loaded " << original_samples.size() << " samples, " << sample_rate << " Hz, " << num_channels diff --git a/src/tests/integration/test_logging.cpp b/src/tests/unit/test_logging.cpp similarity index 100% rename from src/tests/integration/test_logging.cpp rename to src/tests/unit/test_logging.cpp diff --git a/src/tests/integration/test_room_callbacks.cpp b/src/tests/unit/test_room_callbacks.cpp similarity index 98% rename from src/tests/integration/test_room_callbacks.cpp rename to src/tests/unit/test_room_callbacks.cpp index fe759214..90ac35b4 100644 --- a/src/tests/integration/test_room_callbacks.cpp +++ b/src/tests/unit/test_room_callbacks.cpp @@ -202,7 +202,7 @@ TEST_F(RoomCallbackTest, ConcurrentRegistrationDoesNotCrash) { threads.reserve(kThreads); for (int t = 0; t < kThreads; ++t) { - threads.emplace_back([&room, t]() { + threads.emplace_back([&room, t, kIterations]() { for (int i = 0; i < kIterations; ++i) { const std::string id = "participant-" + std::to_string(t); room.setOnAudioFrameCallback(id, TrackSource::SOURCE_MICROPHONE, @@ -228,7 +228,7 @@ TEST_F(RoomCallbackTest, ConcurrentMixedRegistrationDoesNotCrash) { threads.reserve(kThreads); for (int t = 0; t < kThreads; ++t) { - threads.emplace_back([&room, t]() { + threads.emplace_back([&room, t, kIterations]() { const std::string id = "p-" + std::to_string(t); for (int i = 0; i < kIterations; ++i) { room.setOnAudioFrameCallback(id, TrackSource::SOURCE_MICROPHONE, diff --git a/src/tests/integration/test_sdk_initialization.cpp b/src/tests/unit/test_sdk_initialization.cpp similarity index 100% rename from src/tests/integration/test_sdk_initialization.cpp rename to src/tests/unit/test_sdk_initialization.cpp diff --git a/src/tests/integration/test_subscription_thread_dispatcher.cpp b/src/tests/unit/test_subscription_thread_dispatcher.cpp similarity index 99% rename from src/tests/integration/test_subscription_thread_dispatcher.cpp rename to src/tests/unit/test_subscription_thread_dispatcher.cpp index 26c2185e..bb3bdfd1 100644 --- a/src/tests/integration/test_subscription_thread_dispatcher.cpp +++ b/src/tests/unit/test_subscription_thread_dispatcher.cpp @@ -407,7 +407,7 @@ TEST_F(SubscriptionThreadDispatcherTest, ConcurrentRegistrationDoesNotCrash) { threads.reserve(kThreads); for (int t = 0; t < kThreads; ++t) { - threads.emplace_back([&dispatcher, t]() { + threads.emplace_back([&dispatcher, t, kIterations]() { for (int i = 0; i < kIterations; ++i) { std::string id = "participant-" + std::to_string(t); dispatcher.setOnAudioFrameCallback(id, TrackSource::SOURCE_MICROPHONE, @@ -435,7 +435,7 @@ TEST_F(SubscriptionThreadDispatcherTest, std::vector threads; for (int t = 0; t < kThreads; ++t) { - threads.emplace_back([&dispatcher, t]() { + threads.emplace_back([&dispatcher, t, kIterations]() { std::string id = "p-" + std::to_string(t); for (int i = 0; i < kIterations; ++i) { dispatcher.setOnAudioFrameCallback(id, TrackSource::SOURCE_MICROPHONE, @@ -714,7 +714,7 @@ TEST_F(SubscriptionThreadDispatcherTest, threads.reserve(kThreads); for (int t = 0; t < kThreads; ++t) { - threads.emplace_back([&dispatcher, t]() { + threads.emplace_back([&dispatcher, t, kIterations]() { for (int i = 0; i < kIterations; ++i) { auto id = dispatcher.addOnDataFrameCallback( "participant-" + std::to_string(t), "track",