diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 7d307c75..77254c81 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -61,23 +61,23 @@ jobs: include: - os: ubuntu-latest name: linux-x64 - build_cmd: ./build.sh release-examples + build_cmd: ./build.sh release-tests 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_dir: build-release - os: macos-latest name: macos-arm64 - build_cmd: ./build.sh release-examples + build_cmd: ./build.sh release-tests 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 --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_dir: build-release name: Build (${{ matrix.name }}) @@ -88,7 +88,7 @@ jobs: uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: submodules: recursive - fetch-depth: 0 + fetch-depth: 1 # ---------- vcpkg caching for Windows ---------- - name: Export GitHub Actions cache environment variables @@ -181,105 +181,28 @@ jobs: shell: pwsh run: ${{ matrix.build_cmd }} - # ---------- Smoke test cpp-example-collection binaries ---------- - # Built under cpp-example-collection-build/ (not build-dir/bin). Visual Studio - # multi-config places executables in per-target Release/ (or Debug/) subdirs. - - name: Smoke test examples (Unix) + # ---------- Run unit tests ---------- + - name: Run unit tests (Unix) if: runner.os != 'Windows' shell: bash run: | - set -x - failed=false - examples_base="${{ matrix.build_dir }}/cpp-example-collection-build" - resolve_exe() { - local dir="$1" name="$2" - local p="${examples_base}/${dir}/${name}" - if [[ -x "${p}" ]]; then - printf '%s' "${p}" - return 0 - fi - p="${examples_base}/${dir}/Release/${name}" - if [[ -x "${p}" ]]; then - printf '%s' "${p}" - return 0 - fi - return 1 - } - for pair in "SimpleRoom:simple_room" "SimpleRpc:simple_rpc" "SimpleDataStream:simple_data_stream"; do - exe="${pair%%:*}" - dir="${pair#*:}" - if ! exe_path="$(resolve_exe "${dir}" "${exe}")"; then - echo "ERROR: ${exe} not found under ${examples_base}/${dir}/" - failed=true - continue - fi - echo "Testing ${exe}..." - output=$("${exe_path}" --help 2>&1) || true - if [[ -z "${output}" ]]; then - echo "ERROR: ${exe} produced no output" - failed=true - else - echo "${output}" - echo "${exe} ran successfully" - fi - done - if [[ "$failed" == "true" ]]; then exit 1; fi - - - name: Smoke test examples (Windows) + ${{ 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: | - $ErrorActionPreference = 'Continue' - $examplesBase = "${{ matrix.build_dir }}/cpp-example-collection-build" - $examples = @( - @{ Name = 'SimpleRoom'; Dir = 'simple_room' }, - @{ Name = 'SimpleRpc'; Dir = 'simple_rpc' }, - @{ Name = 'SimpleDataStream'; Dir = 'simple_data_stream' } - ) - $failed = $false - foreach ($ex in $examples) { - $name = $ex.Name - $dir = $ex.Dir - $inDir = Join-Path $examplesBase $dir - $candidates = @( - (Join-Path $inDir "$name.exe"), - (Join-Path (Join-Path $inDir 'Release') "$name.exe") - ) - $exePath = $null - foreach ($p in $candidates) { - if (Test-Path -LiteralPath $p) { - $exePath = $p - break - } - } - if ($null -ne $exePath) { - Write-Host "Testing ${name}..." - $pinfo = New-Object System.Diagnostics.ProcessStartInfo - $pinfo.FileName = $exePath - $pinfo.Arguments = "--help" - $pinfo.RedirectStandardOutput = $true - $pinfo.RedirectStandardError = $true - $pinfo.UseShellExecute = $false - $p = New-Object System.Diagnostics.Process - $p.StartInfo = $pinfo - $p.Start() | Out-Null - $stdout = $p.StandardOutput.ReadToEnd() - $stderr = $p.StandardError.ReadToEnd() - $p.WaitForExit() - $output = $stdout + $stderr - if ([string]::IsNullOrWhiteSpace($output)) { - Write-Host "ERROR: ${name} produced no output" - $failed = $true - } else { - Write-Host $output - Write-Host "${name} ran successfully" - } - } else { - Write-Host "ERROR: ${name} not found under ${examplesBase}/${dir}/" - $failed = $true - } - } - if ($failed) { exit 1 } else { exit 0 } + & "${{ 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 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..22067e8e 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -24,6 +24,91 @@ 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 + ${LIVEKIT_BINARY_DIR}/generated + ${Protobuf_INCLUDE_DIRS} + ) + if(TARGET absl::base) + get_target_property(_livekit_unit_test_absl_inc absl::base INTERFACE_INCLUDE_DIRECTORIES) + if(_livekit_unit_test_absl_inc) + target_include_directories(livekit_unit_tests PRIVATE + ${_livekit_unit_test_absl_inc} + ) + endif() + endif() + + 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 +123,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 +160,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 +199,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 +232,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 +273,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 +290,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..3bfcb410 100644 --- a/src/tests/integration/test_audio_processing_module.cpp +++ b/src/tests/unit/test_audio_processing_module.cpp @@ -801,6 +801,7 @@ TEST_F(AudioProcessingModuleTest, AGCAttenuatesLoudSpeech) { int sample_rate = 0; int num_channels = 0; + // TODO: figure out what generates this welcome.wav file such that this test isn't skipped 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; 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",