From 99d1c4d6565f9ec4502ab904b7a5fb833400e33c Mon Sep 17 00:00:00 2001 From: "RICKPORTABLE\\rosbo" Date: Tue, 26 May 2026 11:59:41 -0500 Subject: [PATCH 01/16] Add C++ test to obs-studio-server * Add support for Catch2 C++ test * Turn obs-server into a static lib * Add osn-source test --- ci/run-unit-tests.js | 36 +++---- obs-studio-server/CMakeLists.txt | 100 +++++++++++++++----- obs-studio-server/source/nodeobs_api.cpp | 28 +++--- obs-studio-server/tests/test-helper.cpp | 89 +++++++++++++++++ obs-studio-server/tests/test-helper.hpp | 9 ++ obs-studio-server/tests/test-osn-source.cpp | 79 ++++++++++++++++ 6 files changed, 290 insertions(+), 51 deletions(-) create mode 100644 obs-studio-server/tests/test-helper.cpp create mode 100644 obs-studio-server/tests/test-helper.hpp create mode 100644 obs-studio-server/tests/test-osn-source.cpp 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..6ade63ecb 100644 --- a/obs-studio-server/CMakeLists.txt +++ b/obs-studio-server/CMakeLists.txt @@ -424,9 +424,39 @@ elseif(WIN32) ) endif () +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) + target_compile_definitions( + obs-studio-server-lib + PUBLIC + WIN32_LEAN_AND_MEAN + NOMINMAX + UNICODE + _UNICODE + ) +ENDIF() + +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_include_directories(obs-studio-server-lib PUBLIC ${COREFOUNDATION} ${COCOA} ${IOSURF} ${GLKIT} ${AVFOUNDATION} ${IOKit} ${SECURITY_LIBRARY} ${BSM_LIBRARY}) + 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(obs-studio-server-lib PUBLIC libcurl) + add_executable( ${PROJECT_NAME} - ${osn-server_SOURCES} + "${PROJECT_SOURCE_DIR}/source/main.cpp" ) if(WIN32) @@ -436,16 +466,7 @@ if(WIN32) target_sources(${PROJECT_NAME} PUBLIC "${PROJECT_BINARY_DIR}/version.rc") endif() -#target_link_libraries(${PROJECT_NAME} CURL::libcurl) -target_link_libraries(${PROJECT_NAME} libcurl) - -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}) -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) @@ -483,17 +504,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 +608,49 @@ 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/test-helper.cpp" + "tests/test-helper.hpp" + ) + + target_compile_definitions( + obs_studio_server_unit_tests + PRIVATE + OSN_SOURCE_DIR="${CMAKE_SOURCE_DIR}" + ) + + 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() + + catch_discover_tests( + obs_studio_server_unit_tests + TEST_PREFIX "obs_studio_server_unit_tests::" + DL_PATHS + "$" + "$" + "${libobs_SOURCE_DIR}/bin/64bit" + "$" + ) +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 570081273..32a1a0879 100644 --- a/obs-studio-server/source/nodeobs_api.cpp +++ b/obs-studio-server/source/nodeobs_api.cpp @@ -923,19 +923,21 @@ 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 *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); + } #endif diff --git a/obs-studio-server/tests/test-helper.cpp b/obs-studio-server/tests/test-helper.cpp new file mode 100644 index 000000000..b34677f05 --- /dev/null +++ b/obs-studio-server/tests/test-helper.cpp @@ -0,0 +1,89 @@ +#include +#include +#include "nodeobs_api.h" +#include "osn-error.hpp" +#include +#include "shared.hpp" +#include +#include "test-helper.hpp" +#include + +namespace osn::tests { + +// Reads the `wd` export from tests/osn-tests/osn/index.ts. +// The line has the form: const wd: string = "..."; +static std::string parseWdFromIndexTs() +{ + const std::string indexTsPath = std::string(OSN_SOURCE_DIR) + "/tests/osn-tests/osn/index.ts"; + + std::ifstream file(indexTsPath); + INFO("Could not open " + indexTsPath); + REQUIRE(file.is_open()); + + std::string line; + while (std::getline(file, line)) { + const std::string prefix = "const wd"; + if (line.find(prefix) == std::string::npos) + continue; + + auto first = line.find('"'); + auto last = line.rfind('"'); + INFO("Could not parse wd value from: " + line); + REQUIRE(first != std::string::npos); + REQUIRE(last != first); + + return line.substr(first + 1, last - first - 1); + } + + FAIL("wd declaration not found in " + indexTsPath); + return {}; +} + +void setWorkingFolder(const std::string &wd) +{ + std::vector args = {ipc::value(wd)}; + std::vector response; + OBS_API::SetWorkingDirectory(nullptr, 0, args, response); + CHECK(response.size() >= 2); + ErrorCode error = (ErrorCode)response[0].value_union.ui64; + CHECK(error == ErrorCode::Ok); +} + +void setupApi() +{ +#if defined(__APPLE__) + 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"; + // osn.NodeObs.OBS_API_initAPI(this.language, this.obsPath, this.version, this.crashServer); + 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); + CHECK(response.size() >= 2); + ErrorCode error = (ErrorCode)response[0].value_union.ui64; + CHECK(error == ErrorCode::Ok); +} + +void TestHelper::initializeOBS() +{ + const std::string wd = parseWdFromIndexTs(); + setWorkingFolder(wd); + setupApi(); +} + +void TestHelper::finalizeOBS() +{ + std::vector args = {}; + std::vector response; + OBS_API::OBS_API_destroyOBS_API(nullptr, 0, args, response); + CHECK(response.size() >= 1); + ErrorCode error = (ErrorCode)response[0].value_union.ui64; + CHECK(error == ErrorCode::Ok); +} + +} // namespace osn::tests diff --git a/obs-studio-server/tests/test-helper.hpp b/obs-studio-server/tests/test-helper.hpp new file mode 100644 index 000000000..0377ad46d --- /dev/null +++ b/obs-studio-server/tests/test-helper.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace osn::tests { +class TestHelper { +public: + static void initializeOBS(); + static void finalizeOBS(); +}; +} // 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..432896f5d --- /dev/null +++ b/obs-studio-server/tests/test-osn-source.cpp @@ -0,0 +1,79 @@ +#include +#include +#include "nodeobs_api.h" +#include "osn-error.hpp" +#include "osn-input.hpp" +#include "osn-source.hpp" +#include +#include "shared.hpp" +#include +#include "test-helper.hpp" +#include +#include + +TEST_CASE("Run osn::source tests") +{ + osn::tests::TestHelper::initializeOBS(); + + SECTION("Get properties of browser source while releasing concurrently does not crash") + { + 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(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(std::thread([sourceId, i, &releaseOk]() { + std::vector propArgs = {ipc::value(sourceId)}; + std::vector propResponse; + osn::Source::Release(nullptr, 0, propArgs, propResponse); +#if defined(TRIGGER_CRASH) + // Also release the refcount to trigger actual private data destruction + obs_source_t *src = osn::Source::Manager::GetInstance().find(sourceId); // may be null already + if (src) + obs_source_release(src); +#endif + + // Capture result for checking on the main thread after join. + if (propResponse.size() >= 1) { + releaseOk[i] = ((ErrorCode)propResponse[0].value_union.ui64 == ErrorCode::Ok); + } + })); + } + + for (std::thread &worker : workers) { + if (worker.joinable()) + worker.join(); + } + + // 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); + } + } + + osn::tests::TestHelper::finalizeOBS(); +} From c22425c301ddafdc90450d4877fcbf84a6137fa3 Mon Sep 17 00:00:00 2001 From: "RICKPORTABLE\\rosbo" Date: Tue, 26 May 2026 20:08:52 -0500 Subject: [PATCH 02/16] working dir via cmake & set testHelper use RAII --- obs-studio-server/CMakeLists.txt | 1 + obs-studio-server/tests/test-helper.cpp | 36 ++------------------- obs-studio-server/tests/test-helper.hpp | 4 +-- obs-studio-server/tests/test-osn-source.cpp | 4 +-- 4 files changed, 7 insertions(+), 38 deletions(-) diff --git a/obs-studio-server/CMakeLists.txt b/obs-studio-server/CMakeLists.txt index 6ade63ecb..c6b525fc9 100644 --- a/obs-studio-server/CMakeLists.txt +++ b/obs-studio-server/CMakeLists.txt @@ -625,6 +625,7 @@ if(BUILD_TESTING) obs_studio_server_unit_tests PRIVATE OSN_SOURCE_DIR="${CMAKE_SOURCE_DIR}" + OSN_TEST_WD="${libobs_SOURCE_DIR}" ) target_link_libraries( diff --git a/obs-studio-server/tests/test-helper.cpp b/obs-studio-server/tests/test-helper.cpp index b34677f05..fd184d4f9 100644 --- a/obs-studio-server/tests/test-helper.cpp +++ b/obs-studio-server/tests/test-helper.cpp @@ -10,35 +10,6 @@ namespace osn::tests { -// Reads the `wd` export from tests/osn-tests/osn/index.ts. -// The line has the form: const wd: string = "..."; -static std::string parseWdFromIndexTs() -{ - const std::string indexTsPath = std::string(OSN_SOURCE_DIR) + "/tests/osn-tests/osn/index.ts"; - - std::ifstream file(indexTsPath); - INFO("Could not open " + indexTsPath); - REQUIRE(file.is_open()); - - std::string line; - while (std::getline(file, line)) { - const std::string prefix = "const wd"; - if (line.find(prefix) == std::string::npos) - continue; - - auto first = line.find('"'); - auto last = line.rfind('"'); - INFO("Could not parse wd value from: " + line); - REQUIRE(first != std::string::npos); - REQUIRE(last != first); - - return line.substr(first + 1, last - first - 1); - } - - FAIL("wd declaration not found in " + indexTsPath); - return {}; -} - void setWorkingFolder(const std::string &wd) { std::vector args = {ipc::value(wd)}; @@ -69,14 +40,13 @@ void setupApi() CHECK(error == ErrorCode::Ok); } -void TestHelper::initializeOBS() +TestHelper::TestHelper() { - const std::string wd = parseWdFromIndexTs(); - setWorkingFolder(wd); + setWorkingFolder(OSN_TEST_WD); setupApi(); } -void TestHelper::finalizeOBS() +TestHelper::~TestHelper() { std::vector args = {}; std::vector response; diff --git a/obs-studio-server/tests/test-helper.hpp b/obs-studio-server/tests/test-helper.hpp index 0377ad46d..a160440b6 100644 --- a/obs-studio-server/tests/test-helper.hpp +++ b/obs-studio-server/tests/test-helper.hpp @@ -3,7 +3,7 @@ namespace osn::tests { class TestHelper { public: - static void initializeOBS(); - static void finalizeOBS(); + TestHelper(); + ~TestHelper(); }; } // namespace osn::tests diff --git a/obs-studio-server/tests/test-osn-source.cpp b/obs-studio-server/tests/test-osn-source.cpp index 432896f5d..3b512b595 100644 --- a/obs-studio-server/tests/test-osn-source.cpp +++ b/obs-studio-server/tests/test-osn-source.cpp @@ -13,7 +13,7 @@ TEST_CASE("Run osn::source tests") { - osn::tests::TestHelper::initializeOBS(); + osn::tests::TestHelper helper; SECTION("Get properties of browser source while releasing concurrently does not crash") { @@ -74,6 +74,4 @@ TEST_CASE("Run osn::source tests") CHECK(expectedErrorCode); } } - - osn::tests::TestHelper::finalizeOBS(); } From dd5a1c8c186aa352e07637f5db76d23a37019d3e Mon Sep 17 00:00:00 2001 From: "RICKPORTABLE\\rosbo" Date: Wed, 27 May 2026 08:02:56 -0500 Subject: [PATCH 03/16] fix MacOS build --- obs-studio-server/CMakeLists.txt | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/obs-studio-server/CMakeLists.txt b/obs-studio-server/CMakeLists.txt index c6b525fc9..1373b5ab2 100644 --- a/obs-studio-server/CMakeLists.txt +++ b/obs-studio-server/CMakeLists.txt @@ -645,13 +645,23 @@ if(BUILD_TESTING) ) endif() - catch_discover_tests( - obs_studio_server_unit_tests - TEST_PREFIX "obs_studio_server_unit_tests::" - DL_PATHS - "$" - "$" - "${libobs_SOURCE_DIR}/bin/64bit" - "$" - ) + if(APPLE) + catch_discover_tests( + obs_studio_server_unit_tests + TEST_PREFIX "obs_studio_server_unit_tests::" + DL_PATHS + "$" + "$" + ) + elseif(WIN32) + catch_discover_tests( + obs_studio_server_unit_tests + TEST_PREFIX "obs_studio_server_unit_tests::" + DL_PATHS + "$" + "$" + "${libobs_SOURCE_DIR}/bin/64bit" + "$" + ) + endif() endif() \ No newline at end of file From c0a23710080cead09bd4c208e6e7f68507dcb8be Mon Sep 17 00:00:00 2001 From: Richard Osborne Date: Wed, 27 May 2026 09:05:49 -0500 Subject: [PATCH 04/16] add libcurl to MacOS test --- obs-studio-server/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/obs-studio-server/CMakeLists.txt b/obs-studio-server/CMakeLists.txt index 1373b5ab2..bc527fb86 100644 --- a/obs-studio-server/CMakeLists.txt +++ b/obs-studio-server/CMakeLists.txt @@ -652,6 +652,7 @@ if(BUILD_TESTING) DL_PATHS "$" "$" + "$" ) elseif(WIN32) catch_discover_tests( From be78a667250b69f1e3a68ffeffb7d8b9675bbc4c Mon Sep 17 00:00:00 2001 From: Richard Osborne Date: Thu, 28 May 2026 09:48:07 -0500 Subject: [PATCH 05/16] use CMAKE_INSTALL_PREFIX for working directory --- obs-studio-server/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obs-studio-server/CMakeLists.txt b/obs-studio-server/CMakeLists.txt index bc527fb86..a9398bd61 100644 --- a/obs-studio-server/CMakeLists.txt +++ b/obs-studio-server/CMakeLists.txt @@ -625,7 +625,7 @@ if(BUILD_TESTING) obs_studio_server_unit_tests PRIVATE OSN_SOURCE_DIR="${CMAKE_SOURCE_DIR}" - OSN_TEST_WD="${libobs_SOURCE_DIR}" + OSN_TEST_WD="${CMAKE_INSTALL_PREFIX}" ) target_link_libraries( From 72bea8575597192a37714d2a25b6ceb51fa17acc Mon Sep 17 00:00:00 2001 From: Richard Osborne Date: Thu, 28 May 2026 10:08:21 -0500 Subject: [PATCH 06/16] fix missing framework issue on MacOS --- obs-studio-server/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/obs-studio-server/CMakeLists.txt b/obs-studio-server/CMakeLists.txt index a9398bd61..e4560d923 100644 --- a/obs-studio-server/CMakeLists.txt +++ b/obs-studio-server/CMakeLists.txt @@ -653,6 +653,7 @@ if(BUILD_TESTING) "$" "$" "$" + "${libobs_SOURCE_DIR}/OBS.app/Contents/Frameworks" ) elseif(WIN32) catch_discover_tests( From 323733b5ad3c835e417aa7e558dd1d1e7d92b0b7 Mon Sep 17 00:00:00 2001 From: Richard Osborne Date: Thu, 28 May 2026 10:18:13 -0500 Subject: [PATCH 07/16] rename TestHelper -> ObsSetup --- obs-studio-server/CMakeLists.txt | 4 ++-- obs-studio-server/tests/{test-helper.cpp => obs-setup.cpp} | 6 +++--- obs-studio-server/tests/{test-helper.hpp => obs-setup.hpp} | 6 +++--- obs-studio-server/tests/test-osn-source.cpp | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) rename obs-studio-server/tests/{test-helper.cpp => obs-setup.cpp} (95%) rename obs-studio-server/tests/{test-helper.hpp => obs-setup.hpp} (59%) diff --git a/obs-studio-server/CMakeLists.txt b/obs-studio-server/CMakeLists.txt index e4560d923..2c477ff64 100644 --- a/obs-studio-server/CMakeLists.txt +++ b/obs-studio-server/CMakeLists.txt @@ -617,8 +617,8 @@ if(BUILD_TESTING) add_executable( obs_studio_server_unit_tests "tests/test-osn-source.cpp" - "tests/test-helper.cpp" - "tests/test-helper.hpp" + "tests/obs-setup.cpp" + "tests/obs-setup.hpp" ) target_compile_definitions( diff --git a/obs-studio-server/tests/test-helper.cpp b/obs-studio-server/tests/obs-setup.cpp similarity index 95% rename from obs-studio-server/tests/test-helper.cpp rename to obs-studio-server/tests/obs-setup.cpp index fd184d4f9..d0b6c8244 100644 --- a/obs-studio-server/tests/test-helper.cpp +++ b/obs-studio-server/tests/obs-setup.cpp @@ -5,7 +5,7 @@ #include #include "shared.hpp" #include -#include "test-helper.hpp" +#include "obs-setup.hpp" #include namespace osn::tests { @@ -40,13 +40,13 @@ void setupApi() CHECK(error == ErrorCode::Ok); } -TestHelper::TestHelper() +ObsSetup::ObsSetup() { setWorkingFolder(OSN_TEST_WD); setupApi(); } -TestHelper::~TestHelper() +ObsSetup::~ObsSetup() { std::vector args = {}; std::vector response; diff --git a/obs-studio-server/tests/test-helper.hpp b/obs-studio-server/tests/obs-setup.hpp similarity index 59% rename from obs-studio-server/tests/test-helper.hpp rename to obs-studio-server/tests/obs-setup.hpp index a160440b6..dcc9e4a7b 100644 --- a/obs-studio-server/tests/test-helper.hpp +++ b/obs-studio-server/tests/obs-setup.hpp @@ -1,9 +1,9 @@ #pragma once namespace osn::tests { -class TestHelper { +class ObsSetup { public: - TestHelper(); - ~TestHelper(); + 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 index 3b512b595..e8123586e 100644 --- a/obs-studio-server/tests/test-osn-source.cpp +++ b/obs-studio-server/tests/test-osn-source.cpp @@ -7,13 +7,13 @@ #include #include "shared.hpp" #include -#include "test-helper.hpp" +#include "obs-setup.hpp" #include #include TEST_CASE("Run osn::source tests") { - osn::tests::TestHelper helper; + osn::tests::ObsSetup setupOBS; SECTION("Get properties of browser source while releasing concurrently does not crash") { From 290fc886823da3b9698c7fa9c29d36a736ecbeeb Mon Sep 17 00:00:00 2001 From: Richard Osborne Date: Thu, 28 May 2026 10:30:25 -0500 Subject: [PATCH 08/16] use REQUIRE to guard before indexing --- obs-studio-server/CMakeLists.txt | 1 - obs-studio-server/tests/obs-setup.cpp | 6 +++--- obs-studio-server/tests/obs-setup.hpp | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/obs-studio-server/CMakeLists.txt b/obs-studio-server/CMakeLists.txt index 2c477ff64..1c8c1b951 100644 --- a/obs-studio-server/CMakeLists.txt +++ b/obs-studio-server/CMakeLists.txt @@ -448,7 +448,6 @@ 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_include_directories(obs-studio-server-lib PUBLIC ${COREFOUNDATION} ${COCOA} ${IOSURF} ${GLKIT} ${AVFOUNDATION} ${IOKit} ${SECURITY_LIBRARY} ${BSM_LIBRARY}) target_link_libraries(obs-studio-server-lib PUBLIC ${PROJECT_LIBRARIES} crashpad ${COREFOUNDATION} ${COCOA} ${IOSURF} ${GLKIT} ${AVFOUNDATION} ${IOKit} ${SECURITY_LIBRARY} ${BSM_LIBRARY}) endif() diff --git a/obs-studio-server/tests/obs-setup.cpp b/obs-studio-server/tests/obs-setup.cpp index d0b6c8244..e09fc5e26 100644 --- a/obs-studio-server/tests/obs-setup.cpp +++ b/obs-studio-server/tests/obs-setup.cpp @@ -15,7 +15,7 @@ void setWorkingFolder(const std::string &wd) std::vector args = {ipc::value(wd)}; std::vector response; OBS_API::SetWorkingDirectory(nullptr, 0, args, response); - CHECK(response.size() >= 2); + REQUIRE(response.size() >= 2); ErrorCode error = (ErrorCode)response[0].value_union.ui64; CHECK(error == ErrorCode::Ok); } @@ -35,7 +35,7 @@ void setupApi() 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); - CHECK(response.size() >= 2); + REQUIRE(response.size() >= 2); ErrorCode error = (ErrorCode)response[0].value_union.ui64; CHECK(error == ErrorCode::Ok); } @@ -51,7 +51,7 @@ ObsSetup::~ObsSetup() std::vector args = {}; std::vector response; OBS_API::OBS_API_destroyOBS_API(nullptr, 0, args, response); - CHECK(response.size() >= 1); + REQUIRE(response.size() >= 1); ErrorCode error = (ErrorCode)response[0].value_union.ui64; CHECK(error == ErrorCode::Ok); } diff --git a/obs-studio-server/tests/obs-setup.hpp b/obs-studio-server/tests/obs-setup.hpp index dcc9e4a7b..75c124ffe 100644 --- a/obs-studio-server/tests/obs-setup.hpp +++ b/obs-studio-server/tests/obs-setup.hpp @@ -1,6 +1,7 @@ #pragma once namespace osn::tests { +// This helper object uses RAII pattern to initialize & destroy OBS API class ObsSetup { public: ObsSetup(); From 51ddb28a427ed2e17743401ce2b97afb0abe7b28 Mon Sep 17 00:00:00 2001 From: Richard Osborne Date: Thu, 28 May 2026 10:46:03 -0500 Subject: [PATCH 09/16] catch_discover_tests cleanup --- obs-studio-server/CMakeLists.txt | 34 ++++++++++++-------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/obs-studio-server/CMakeLists.txt b/obs-studio-server/CMakeLists.txt index 1c8c1b951..282d6d7a8 100644 --- a/obs-studio-server/CMakeLists.txt +++ b/obs-studio-server/CMakeLists.txt @@ -644,25 +644,17 @@ if(BUILD_TESTING) ) endif() - if(APPLE) - catch_discover_tests( - obs_studio_server_unit_tests - TEST_PREFIX "obs_studio_server_unit_tests::" - DL_PATHS - "$" - "$" - "$" - "${libobs_SOURCE_DIR}/OBS.app/Contents/Frameworks" - ) - elseif(WIN32) - catch_discover_tests( - obs_studio_server_unit_tests - TEST_PREFIX "obs_studio_server_unit_tests::" - 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 + "$" + "$" + "$" +if(APPLE) + "${libobs_SOURCE_DIR}/OBS.app/Contents/Frameworks" +elseif(WIN32) + "${libobs_SOURCE_DIR}/bin/64bit" +endif() + ) endif() \ No newline at end of file From 72e7d2019a780ca09a7a9ed848a0273196271b71 Mon Sep 17 00:00:00 2001 From: Richard Osborne Date: Thu, 28 May 2026 16:52:29 -0500 Subject: [PATCH 10/16] update test to trigger crash --- obs-studio-server/tests/test-osn-source.cpp | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/obs-studio-server/tests/test-osn-source.cpp b/obs-studio-server/tests/test-osn-source.cpp index e8123586e..15e90bf3b 100644 --- a/obs-studio-server/tests/test-osn-source.cpp +++ b/obs-studio-server/tests/test-osn-source.cpp @@ -17,6 +17,7 @@ TEST_CASE("Run osn::source tests") 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); @@ -44,20 +45,12 @@ TEST_CASE("Run osn::source tests") })); workers.push_back(std::thread([sourceId, i, &releaseOk]() { - std::vector propArgs = {ipc::value(sourceId)}; - std::vector propResponse; - osn::Source::Release(nullptr, 0, propArgs, propResponse); -#if defined(TRIGGER_CRASH) - // Also release the refcount to trigger actual private data destruction + // Release the refcount to trigger actual private data destruction obs_source_t *src = osn::Source::Manager::GetInstance().find(sourceId); // may be null already - if (src) - obs_source_release(src); -#endif - - // Capture result for checking on the main thread after join. - if (propResponse.size() >= 1) { - releaseOk[i] = ((ErrorCode)propResponse[0].value_union.ui64 == ErrorCode::Ok); - } + if (src) { + obs_source_release(src); + releaseOk[i] = true; + } })); } @@ -73,5 +66,6 @@ TEST_CASE("Run osn::source tests") 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. } } From c60068005921dd79a9b86bde8bcd05bd792817f6 Mon Sep 17 00:00:00 2001 From: Richard Osborne Date: Thu, 28 May 2026 17:14:24 -0500 Subject: [PATCH 11/16] enable test to pass on CI env until staging is merged --- obs-studio-server/tests/test-osn-source.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/obs-studio-server/tests/test-osn-source.cpp b/obs-studio-server/tests/test-osn-source.cpp index 15e90bf3b..253765e23 100644 --- a/obs-studio-server/tests/test-osn-source.cpp +++ b/obs-studio-server/tests/test-osn-source.cpp @@ -45,12 +45,25 @@ TEST_CASE("Run osn::source tests") })); workers.push_back(std::thread([sourceId, i, &releaseOk]() { - // Release the refcount to trigger actual private data destruction - obs_source_t *src = osn::Source::Manager::GetInstance().find(sourceId); // may be null already + +#if defined(TRIGGER_CRASH) + // Enable this code block once staging (commit cc4a0431) is merged. + // Release the refcount to trigger actual private data destruction + obs_source_t *src = osn::Source::Manager::GetInstance().find(sourceId); // may be null already if (src) { obs_source_release(src); releaseOk[i] = true; } +#else + // delete this block after staging (commit cc4a0431) is merged. + 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); + } +#endif })); } From 83be3502ec522929f33b81baa3c095c9d510c528 Mon Sep 17 00:00:00 2001 From: Richard Osborne Date: Thu, 28 May 2026 17:58:25 -0500 Subject: [PATCH 12/16] run clang-format --- obs-studio-server/tests/test-osn-source.cpp | 30 ++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/obs-studio-server/tests/test-osn-source.cpp b/obs-studio-server/tests/test-osn-source.cpp index 253765e23..e0a6ac1a1 100644 --- a/obs-studio-server/tests/test-osn-source.cpp +++ b/obs-studio-server/tests/test-osn-source.cpp @@ -17,7 +17,7 @@ TEST_CASE("Run osn::source tests") SECTION("Get properties of browser source while releasing concurrently does not crash") { - auto sourceCount = osn::Source::Manager::GetInstance().size(); + auto sourceCount = osn::Source::Manager::GetInstance().size(); const int iterations = 20; std::vector workers; std::vector releaseOk(iterations, 0); @@ -45,24 +45,24 @@ TEST_CASE("Run osn::source tests") })); workers.push_back(std::thread([sourceId, i, &releaseOk]() { - + #if defined(TRIGGER_CRASH) - // Enable this code block once staging (commit cc4a0431) is merged. - // Release the refcount to trigger actual private data destruction - obs_source_t *src = osn::Source::Manager::GetInstance().find(sourceId); // may be null already - if (src) { - obs_source_release(src); - releaseOk[i] = true; - } + // Enable this code block once staging (commit cc4a0431) is merged. + // Release the refcount to trigger actual private data destruction + obs_source_t *src = osn::Source::Manager::GetInstance().find(sourceId); // may be null already + if (src) { + obs_source_release(src); + releaseOk[i] = true; + } #else - // delete this block after staging (commit cc4a0431) is merged. + // delete this block after staging (commit cc4a0431) is merged. 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); - } + // Capture result for checking on the main thread after join. + if (propResponse.size() >= 1) { + releaseOk[i] = ((ErrorCode)propResponse[0].value_union.ui64 == ErrorCode::Ok); + } #endif })); } @@ -79,6 +79,6 @@ TEST_CASE("Run osn::source tests") 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. + CHECK(sourceCount == osn::Source::Manager::GetInstance().size()); // Check to see if all objects released. } } From 73c542a9584bf178d6bd671a5e9e5c0e0ba5ce3c Mon Sep 17 00:00:00 2001 From: Richard Osborne Date: Fri, 29 May 2026 10:16:59 -0500 Subject: [PATCH 13/16] wrap check for object destruction --- obs-studio-server/tests/test-osn-source.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/obs-studio-server/tests/test-osn-source.cpp b/obs-studio-server/tests/test-osn-source.cpp index e0a6ac1a1..23b58bd61 100644 --- a/obs-studio-server/tests/test-osn-source.cpp +++ b/obs-studio-server/tests/test-osn-source.cpp @@ -79,6 +79,9 @@ TEST_CASE("Run osn::source tests") bool expectedErrorCode = getPropertiesCode[i] == ErrorCode::Ok || getPropertiesCode[i] == ErrorCode::InvalidReference; CHECK(expectedErrorCode); } +#if defined(TRIGGER_CRASH) + // Enable this code block once staging (commit cc4a0431) is merged. CHECK(sourceCount == osn::Source::Manager::GetInstance().size()); // Check to see if all objects released. +#endif } } From 64f53397773122967052de25dc5700cb0bc83bd0 Mon Sep 17 00:00:00 2001 From: Richard Osborne Date: Fri, 29 May 2026 10:20:07 -0500 Subject: [PATCH 14/16] run clang-format --- obs-studio-server/tests/test-osn-source.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obs-studio-server/tests/test-osn-source.cpp b/obs-studio-server/tests/test-osn-source.cpp index 23b58bd61..9f2d10372 100644 --- a/obs-studio-server/tests/test-osn-source.cpp +++ b/obs-studio-server/tests/test-osn-source.cpp @@ -80,7 +80,7 @@ TEST_CASE("Run osn::source tests") CHECK(expectedErrorCode); } #if defined(TRIGGER_CRASH) - // Enable this code block once staging (commit cc4a0431) is merged. + // Enable this code block once staging (commit cc4a0431) is merged. CHECK(sourceCount == osn::Source::Manager::GetInstance().size()); // Check to see if all objects released. #endif } From 39d300415dee7bf05891fab658e9d26c65bb58e2 Mon Sep 17 00:00:00 2001 From: Richard Osborne Date: Fri, 29 May 2026 10:27:13 -0500 Subject: [PATCH 15/16] remove extra empty returns --- obs-studio-server/CMakeLists.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/obs-studio-server/CMakeLists.txt b/obs-studio-server/CMakeLists.txt index 282d6d7a8..3263b907b 100644 --- a/obs-studio-server/CMakeLists.txt +++ b/obs-studio-server/CMakeLists.txt @@ -608,8 +608,6 @@ if (APPLE) ) endif() - - if(BUILD_TESTING) include(Catch) From b1ba437c6c3d167866ea3b92b5a2d02da80549ec Mon Sep 17 00:00:00 2001 From: Richard Osborne Date: Fri, 29 May 2026 13:57:07 -0500 Subject: [PATCH 16/16] mark TODO comments --- obs-studio-server/tests/obs-setup.cpp | 1 - obs-studio-server/tests/test-osn-source.cpp | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/obs-studio-server/tests/obs-setup.cpp b/obs-studio-server/tests/obs-setup.cpp index e09fc5e26..88e2b192b 100644 --- a/obs-studio-server/tests/obs-setup.cpp +++ b/obs-studio-server/tests/obs-setup.cpp @@ -31,7 +31,6 @@ void setupApi() CHECK(g_util_osx->hasInitApi()); #endif const std::string appPath = std::string(OSN_SOURCE_DIR) + "/tests/osn-tests/osnData/slobs-client"; - // osn.NodeObs.OBS_API_initAPI(this.language, this.obsPath, this.version, this.crashServer); 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); diff --git a/obs-studio-server/tests/test-osn-source.cpp b/obs-studio-server/tests/test-osn-source.cpp index 9f2d10372..b59fd0bab 100644 --- a/obs-studio-server/tests/test-osn-source.cpp +++ b/obs-studio-server/tests/test-osn-source.cpp @@ -47,7 +47,7 @@ TEST_CASE("Run osn::source tests") workers.push_back(std::thread([sourceId, i, &releaseOk]() { #if defined(TRIGGER_CRASH) - // Enable this code block once staging (commit cc4a0431) is merged. + // TODO: Enable this code block once staging (commit cc4a0431) is merged. // Release the refcount to trigger actual private data destruction obs_source_t *src = osn::Source::Manager::GetInstance().find(sourceId); // may be null already if (src) { @@ -55,7 +55,7 @@ TEST_CASE("Run osn::source tests") releaseOk[i] = true; } #else - // delete this block after staging (commit cc4a0431) is merged. + // TODO: delete this block after staging (commit cc4a0431) is merged. std::vector propArgs = {ipc::value(sourceId)}; std::vector propResponse; osn::Source::Release(nullptr, 0, propArgs, propResponse); @@ -80,7 +80,7 @@ TEST_CASE("Run osn::source tests") CHECK(expectedErrorCode); } #if defined(TRIGGER_CRASH) - // Enable this code block once staging (commit cc4a0431) is merged. + // TODO: Enable this code block once staging (commit cc4a0431) is merged. CHECK(sourceCount == osn::Source::Manager::GetInstance().size()); // Check to see if all objects released. #endif }