diff --git a/ci/run-unit-tests.js b/ci/run-unit-tests.js index aabf4d4ff..586feb5d5 100644 --- a/ci/run-unit-tests.js +++ b/ci/run-unit-tests.js @@ -10,12 +10,14 @@ const buildDirectory = process.env.BUILD_DIRECTORY || "build"; const buildConfig = process.env.BUILD_CONFIG || process.env.BuildConfig || "RelWithDebInfo"; -const target = "obs_studio_client_unit_tests"; -// Match the TEST_PREFIX passed to catch_discover_tests so ctest only runs this unit-test suite. -const testPattern = `^${target}::`; -function hasDiscoveredTests() { - const testsDirectory = path.join(buildDirectory, "obs-studio-client"); +const testSuites = [ + { target: "obs_studio_client_unit_tests", sourceDir: "obs-studio-client" }, + { target: "obs_studio_server_unit_tests", sourceDir: "obs-studio-server" }, +]; + +function hasDiscoveredTests(target, sourceDir) { + const testsDirectory = path.join(buildDirectory, sourceDir); try { return fs @@ -39,16 +41,18 @@ function run(command, args) { } } -const skipBuild = - process.env.OSN_SKIP_UNIT_TEST_BUILD === "1" || - // Test jobs run from uploaded build artifacts. Building again can force CMake - // to reconfigure FetchContent checkouts whose hidden .git directories were not uploaded. - (process.env.GITHUB_ACTIONS === "true" && hasDiscoveredTests()); +for (const { target, sourceDir } of testSuites) { + const testPattern = `^${target}::`; -if (skipBuild) { - console.log("Skipping unit-test build; discovered CTest tests are already present."); -} else { - run("cmake", ["--build", buildDirectory, "--config", buildConfig, "--target", target]); -} + const skipBuild = + process.env.OSN_SKIP_UNIT_TEST_BUILD === "1" || + (process.env.GITHUB_ACTIONS === "true" && hasDiscoveredTests(target, sourceDir)); -run("ctest", ["--test-dir", buildDirectory, "-C", buildConfig, "--output-on-failure", "-R", testPattern]); + if (skipBuild) { + console.log(`Skipping build for ${target}; discovered CTest tests are already present.`); + } else { + run("cmake", ["--build", buildDirectory, "--config", buildConfig, "--target", target]); + } + + run("ctest", ["--test-dir", buildDirectory, "-C", buildConfig, "--output-on-failure", "-R", testPattern]); +} diff --git a/obs-studio-server/CMakeLists.txt b/obs-studio-server/CMakeLists.txt index d83aab614..9c118cbc3 100644 --- a/obs-studio-server/CMakeLists.txt +++ b/obs-studio-server/CMakeLists.txt @@ -424,29 +424,46 @@ elseif(WIN32) ) endif () -add_executable( - ${PROJECT_NAME} - ${osn-server_SOURCES} +set(OSN_SERVER_CORE_SOURCES ${osn-server_SOURCES}) +list(REMOVE_ITEM OSN_SERVER_CORE_SOURCES "${PROJECT_SOURCE_DIR}/source/main.cpp") + +add_library( + obs-studio-server-lib STATIC + ${OSN_SERVER_CORE_SOURCES} ) -if(WIN32) - # Include/link crash manager dependencies - target_link_libraries(${PROJECT_NAME} StackWalker) +IF(WIN32) + target_compile_definitions( + obs-studio-server-lib + PUBLIC + WIN32_LEAN_AND_MEAN + NOMINMAX + UNICODE + _UNICODE + ) +ENDIF() - target_sources(${PROJECT_NAME} PUBLIC "${PROJECT_BINARY_DIR}/version.rc") +target_include_directories(obs-studio-server-lib PUBLIC ${PROJECT_INCLUDE_PATHS}) + +if(WIN32) + target_link_libraries(obs-studio-server-lib PUBLIC ${PROJECT_LIBRARIES} optimized crashpad strmiids StackWalker) +else() + target_link_libraries(obs-studio-server-lib PUBLIC ${PROJECT_LIBRARIES} crashpad ${COREFOUNDATION} ${COCOA} ${IOSURF} ${GLKIT} ${AVFOUNDATION} ${IOKit} ${SECURITY_LIBRARY} ${BSM_LIBRARY}) endif() -#target_link_libraries(${PROJECT_NAME} CURL::libcurl) -target_link_libraries(${PROJECT_NAME} libcurl) +target_link_libraries(obs-studio-server-lib PUBLIC libcurl) + +add_executable( + ${PROJECT_NAME} + "${PROJECT_SOURCE_DIR}/source/main.cpp" +) if(WIN32) - target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_INCLUDE_PATHS}) - target_link_libraries(${PROJECT_NAME} ${PROJECT_LIBRARIES} optimized crashpad strmiids) -else() - target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_INCLUDE_PATHS} ${COREFOUNDATION} ${COCOA} ${IOSURF} ${GLKIT} ${AVFOUNDATION} ${IOKit} ${SECURITY_LIBRARY} ${BSM_LIBRARY}) - target_link_libraries(${PROJECT_NAME} ${PROJECT_LIBRARIES} crashpad ${COREFOUNDATION} ${COCOA} ${IOSURF} ${GLKIT} ${AVFOUNDATION} ${IOKit} ${SECURITY_LIBRARY} ${BSM_LIBRARY}) + target_sources(${PROJECT_NAME} PUBLIC "${PROJECT_BINARY_DIR}/version.rc") endif() +target_link_libraries(${PROJECT_NAME} obs-studio-server-lib) + if(MSVC) add_definitions(-D_CRT_SECURE_NO_WARNINGS -D_SILENCE_ALL_CXX17_DEPRECATION_WARNINGS) endif() @@ -483,17 +500,6 @@ else() ) ENDIF() -IF(WIN32) - target_compile_definitions( - ${PROJECT_NAME} - PRIVATE - WIN32_LEAN_AND_MEAN - NOMINMAX - UNICODE - _UNICODE - ) -ENDIF() - IF( NOT CLANG_ANALYZE_CONFIG) cppcheck_add_project(${PROJECT_NAME}) ENDIF() @@ -598,3 +604,60 @@ if (APPLE) DESTINATION "../../Contents/Frameworks" USE_SOURCE_PERMISSIONS ) endif() + +if(BUILD_TESTING) + include(Catch) + + add_executable( + obs_studio_server_unit_tests + "tests/test-osn-source.cpp" + "tests/obs-setup.cpp" + "tests/obs-setup.hpp" + ) + + file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}" OSN_SOURCE_DIR_PATH) + file(TO_CMAKE_PATH "${CMAKE_INSTALL_PREFIX}" OSN_TEST_WD_PATH) + + target_compile_definitions( + obs_studio_server_unit_tests + PRIVATE + OSN_SOURCE_DIR="${OSN_SOURCE_DIR_PATH}" + OSN_TEST_WD="${OSN_TEST_WD_PATH}" + ) + + target_link_libraries( + obs_studio_server_unit_tests + PRIVATE + Catch2::Catch2WithMain + obs-studio-server-lib + ) + + if(APPLE) + add_custom_command( + TARGET obs_studio_server_unit_tests + POST_BUILD + COMMAND /usr/bin/codesign --force --sign - "$" + COMMENT "Ad-hoc signing obs_studio_server_unit_tests before Catch2 test discovery" + VERBATIM + ) + endif() + + set(_osn_server_test_dl_paths + "$" + "$" + "$" + ) + + if(APPLE) + list(APPEND _osn_server_test_dl_paths "${libobs_SOURCE_DIR}/OBS.app/Contents/Frameworks") + elseif(WIN32) + list(APPEND _osn_server_test_dl_paths "${libobs_SOURCE_DIR}/bin/64bit") + endif() + + catch_discover_tests( + obs_studio_server_unit_tests + TEST_PREFIX "obs_studio_server_unit_tests::" + DL_PATHS + ${_osn_server_test_dl_paths} + ) +endif() \ No newline at end of file diff --git a/obs-studio-server/source/nodeobs_api.cpp b/obs-studio-server/source/nodeobs_api.cpp index 527b2868e..c71d99e80 100644 --- a/obs-studio-server/source/nodeobs_api.cpp +++ b/obs-studio-server/source/nodeobs_api.cpp @@ -923,19 +923,15 @@ void OBS_API::OBS_API_initAPI(void *data, const int64_t id, const std::vectorset_pre_callback( - [](std::string cname, std::string fname, const std::vector &args, void *data) { - util::CrashManager &crashManager = *static_cast(data); - crashManager.ProcessPreServerCall(cname, fname, args); - }, - &crashManager); - g_server->set_post_callback( - [](std::string cname, std::string fname, const std::vector &args, void *data) { - util::CrashManager &crashManager = *static_cast(data); - crashManager.ProcessPostServerCall(cname, fname, args); - }, - &crashManager); + if (g_server) { + // Register the pre and post server callbacks to log the data into the crashmanager + g_server->set_pre_callback([](std::string cname, std::string fname, const std::vector &args, + void *) { util::CrashManager::ProcessPreServerCall(cname, fname, args); }, + nullptr); + g_server->set_post_callback([](std::string cname, std::string fname, const std::vector &args, + void *) { util::CrashManager::ProcessPostServerCall(cname, fname, args); }, + nullptr); + } #endif diff --git a/obs-studio-server/source/util-osx-impl.mm b/obs-studio-server/source/util-osx-impl.mm index bd08a39c4..352b727b5 100644 --- a/obs-studio-server/source/util-osx-impl.mm +++ b/obs-studio-server/source/util-osx-impl.mm @@ -62,7 +62,7 @@ @implementation UtilImplObj -UtilObjCInt::UtilObjCInt(void) : self(NULL) {} +UtilObjCInt::UtilObjCInt(void) : self(NULL), state(EState::Idle), worker(nullptr) {} UtilObjCInt::~UtilObjCInt(void) { diff --git a/obs-studio-server/tests/obs-setup.cpp b/obs-studio-server/tests/obs-setup.cpp new file mode 100644 index 000000000..4582540eb --- /dev/null +++ b/obs-studio-server/tests/obs-setup.cpp @@ -0,0 +1,68 @@ +#include +#include "nodeobs_api.h" +#include "osn-error.hpp" +#include +#include "shared.hpp" +#include +#include "obs-setup.hpp" +#include + +namespace osn::tests { + +void setWorkingFolder(const std::string &wd) +{ + std::vector args = {ipc::value(wd)}; + std::vector response; + OBS_API::SetWorkingDirectory(nullptr, 0, args, response); + REQUIRE(response.size() >= 2); + ErrorCode error = (ErrorCode)response[0].value_union.ui64; + CHECK(error == ErrorCode::Ok); +} + +void setupApi() +{ +#if defined(__APPLE__) + if (g_util_osx) { + delete g_util_osx; + g_util_osx = nullptr; + } + g_util_osx = new UtilInt(); + g_util_osx->init(); + // Workaround normal app startup where "browser_source" plugin is initialized + CHECK(!g_util_osx->hasInitApi()); + g_util_osx->nextState(); + CHECK(g_util_osx->hasInitApi()); +#endif + const std::string appPath = std::string(OSN_SOURCE_DIR) + "/tests/osn-tests/osnData/slobs-client"; + std::vector args = {ipc::value(appPath), ipc::value("en-US"), ipc::value("0.00.00-preview.0"), ipc::value("")}; + std::vector response; + OBS_API::OBS_API_initAPI(nullptr, 0, args, response); + REQUIRE(response.size() >= 2); + ErrorCode error = (ErrorCode)response[0].value_union.ui64; + CHECK(error == ErrorCode::Ok); +} + +ObsSetup::ObsSetup() +{ + setWorkingFolder(OSN_TEST_WD); + setupApi(); +} + +ObsSetup::~ObsSetup() +{ + std::vector args = {}; + std::vector response; + OBS_API::OBS_API_destroyOBS_API(nullptr, 0, args, response); + CHECK(response.size() >= 1); + if (response.size() >= 1) { + ErrorCode error = (ErrorCode)response[0].value_union.ui64; + CHECK(error == ErrorCode::Ok); + } + +#if defined(__APPLE__) + delete g_util_osx; + g_util_osx = nullptr; +#endif +} + +} // namespace osn::tests diff --git a/obs-studio-server/tests/obs-setup.hpp b/obs-studio-server/tests/obs-setup.hpp new file mode 100644 index 000000000..75c124ffe --- /dev/null +++ b/obs-studio-server/tests/obs-setup.hpp @@ -0,0 +1,10 @@ +#pragma once + +namespace osn::tests { +// This helper object uses RAII pattern to initialize & destroy OBS API +class ObsSetup { +public: + ObsSetup(); + ~ObsSetup(); +}; +} // namespace osn::tests diff --git a/obs-studio-server/tests/test-osn-source.cpp b/obs-studio-server/tests/test-osn-source.cpp new file mode 100644 index 000000000..c334202d0 --- /dev/null +++ b/obs-studio-server/tests/test-osn-source.cpp @@ -0,0 +1,85 @@ +#include +#include "nodeobs_api.h" +#include "osn-error.hpp" +#include "osn-input.hpp" +#include "osn-source.hpp" +#include +#include "shared.hpp" +#include +#include +#include "obs-setup.hpp" +#include +#include +#include + +// Since we do not use C++ 20 (std::jthread), defining a scoped thread. +struct joining_thread { + std::thread t; + explicit joining_thread(std::thread t_) : t(std::move(t_)) {} + joining_thread(joining_thread &&) = default; + joining_thread &operator=(joining_thread &&) = default; + joining_thread(const joining_thread &) = delete; + joining_thread &operator=(const joining_thread &) = delete; + ~joining_thread() + { + if (t.joinable()) + t.join(); + } +}; + +TEST_CASE("Run osn::source tests") +{ + osn::tests::ObsSetup setupOBS; + + SECTION("Get properties of browser source while releasing concurrently does not crash") + { + auto sourceCount = osn::Source::Manager::GetInstance().size(); + const int iterations = 20; + std::vector workers; + std::vector releaseOk(iterations, 0); + std::vector getPropertiesCode(iterations, ErrorCode::Error); + + for (int i = 0; i < iterations; i++) { + const std::string sourceName = "test-input-" + std::to_string(i); + std::vector args = {ipc::value("browser_source"), ipc::value(sourceName)}; + std::vector response; + + osn::Input::Create(nullptr, 0, args, response); + REQUIRE(response.size() >= 2); + ErrorCode error = (ErrorCode)response[0].value_union.ui64; + REQUIRE(error == ErrorCode::Ok); + + uint64_t sourceId = response[1].value_union.ui64; + + workers.push_back(joining_thread(std::thread([sourceId, i, &getPropertiesCode]() { + std::vector propArgs = {ipc::value(sourceId)}; + std::vector propResponse; + osn::Source::GetProperties(nullptr, 0, propArgs, propResponse); + if (propResponse.size() >= 1) { + getPropertiesCode[i] = (ErrorCode)propResponse[0].value_union.ui64; + } + }))); + + workers.push_back(joining_thread(std::thread([sourceId, i, &releaseOk]() { + std::vector propArgs = {ipc::value(sourceId)}; + std::vector propResponse; + osn::Source::Release(nullptr, 0, propArgs, propResponse); + // Capture result for checking on the main thread after join. + if (propResponse.size() >= 1) { + releaseOk[i] = ((ErrorCode)propResponse[0].value_union.ui64 == ErrorCode::Ok); + } + }))); + } + + workers.clear(); + // Check release results on the main thread where Catch2 is safe to use. + for (int i = 0; i < iterations; i++) { + CHECK(releaseOk[i]); + // ErrorCode::InvalidReference is possible if the source was deleted before we could acquire the source + bool expectedErrorCode = getPropertiesCode[i] == ErrorCode::Ok || getPropertiesCode[i] == ErrorCode::InvalidReference; + CHECK(expectedErrorCode); + } + + CHECK(sourceCount == osn::Source::Manager::GetInstance().size()); // Check to see if all objects released. + } +}