Skip to content

Commit 99d1c4d

Browse files
committed
Add C++ test to obs-studio-server
* Add support for Catch2 C++ test * Turn obs-server into a static lib * Add osn-source test
1 parent e65febe commit 99d1c4d

6 files changed

Lines changed: 290 additions & 51 deletions

File tree

ci/run-unit-tests.js

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ const buildDirectory =
1010
process.env.BUILD_DIRECTORY ||
1111
"build";
1212
const buildConfig = process.env.BUILD_CONFIG || process.env.BuildConfig || "RelWithDebInfo";
13-
const target = "obs_studio_client_unit_tests";
14-
// Match the TEST_PREFIX passed to catch_discover_tests so ctest only runs this unit-test suite.
15-
const testPattern = `^${target}::`;
1613

17-
function hasDiscoveredTests() {
18-
const testsDirectory = path.join(buildDirectory, "obs-studio-client");
14+
const testSuites = [
15+
{ target: "obs_studio_client_unit_tests", sourceDir: "obs-studio-client" },
16+
{ target: "obs_studio_server_unit_tests", sourceDir: "obs-studio-server" },
17+
];
18+
19+
function hasDiscoveredTests(target, sourceDir) {
20+
const testsDirectory = path.join(buildDirectory, sourceDir);
1921

2022
try {
2123
return fs
@@ -39,16 +41,18 @@ function run(command, args) {
3941
}
4042
}
4143

42-
const skipBuild =
43-
process.env.OSN_SKIP_UNIT_TEST_BUILD === "1" ||
44-
// Test jobs run from uploaded build artifacts. Building again can force CMake
45-
// to reconfigure FetchContent checkouts whose hidden .git directories were not uploaded.
46-
(process.env.GITHUB_ACTIONS === "true" && hasDiscoveredTests());
44+
for (const { target, sourceDir } of testSuites) {
45+
const testPattern = `^${target}::`;
4746

48-
if (skipBuild) {
49-
console.log("Skipping unit-test build; discovered CTest tests are already present.");
50-
} else {
51-
run("cmake", ["--build", buildDirectory, "--config", buildConfig, "--target", target]);
52-
}
47+
const skipBuild =
48+
process.env.OSN_SKIP_UNIT_TEST_BUILD === "1" ||
49+
(process.env.GITHUB_ACTIONS === "true" && hasDiscoveredTests(target, sourceDir));
5350

54-
run("ctest", ["--test-dir", buildDirectory, "-C", buildConfig, "--output-on-failure", "-R", testPattern]);
51+
if (skipBuild) {
52+
console.log(`Skipping build for ${target}; discovered CTest tests are already present.`);
53+
} else {
54+
run("cmake", ["--build", buildDirectory, "--config", buildConfig, "--target", target]);
55+
}
56+
57+
run("ctest", ["--test-dir", buildDirectory, "-C", buildConfig, "--output-on-failure", "-R", testPattern]);
58+
}

obs-studio-server/CMakeLists.txt

Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -424,9 +424,39 @@ elseif(WIN32)
424424
)
425425
endif ()
426426

427+
set(OSN_SERVER_CORE_SOURCES ${osn-server_SOURCES})
428+
list(REMOVE_ITEM OSN_SERVER_CORE_SOURCES "${PROJECT_SOURCE_DIR}/source/main.cpp")
429+
430+
add_library(
431+
obs-studio-server-lib STATIC
432+
${OSN_SERVER_CORE_SOURCES}
433+
)
434+
435+
IF(WIN32)
436+
target_compile_definitions(
437+
obs-studio-server-lib
438+
PUBLIC
439+
WIN32_LEAN_AND_MEAN
440+
NOMINMAX
441+
UNICODE
442+
_UNICODE
443+
)
444+
ENDIF()
445+
446+
target_include_directories(obs-studio-server-lib PUBLIC ${PROJECT_INCLUDE_PATHS})
447+
448+
if(WIN32)
449+
target_link_libraries(obs-studio-server-lib PUBLIC ${PROJECT_LIBRARIES} optimized crashpad strmiids StackWalker)
450+
else()
451+
target_include_directories(obs-studio-server-lib PUBLIC ${COREFOUNDATION} ${COCOA} ${IOSURF} ${GLKIT} ${AVFOUNDATION} ${IOKit} ${SECURITY_LIBRARY} ${BSM_LIBRARY})
452+
target_link_libraries(obs-studio-server-lib PUBLIC ${PROJECT_LIBRARIES} crashpad ${COREFOUNDATION} ${COCOA} ${IOSURF} ${GLKIT} ${AVFOUNDATION} ${IOKit} ${SECURITY_LIBRARY} ${BSM_LIBRARY})
453+
endif()
454+
455+
target_link_libraries(obs-studio-server-lib PUBLIC libcurl)
456+
427457
add_executable(
428458
${PROJECT_NAME}
429-
${osn-server_SOURCES}
459+
"${PROJECT_SOURCE_DIR}/source/main.cpp"
430460
)
431461

432462
if(WIN32)
@@ -436,16 +466,7 @@ if(WIN32)
436466
target_sources(${PROJECT_NAME} PUBLIC "${PROJECT_BINARY_DIR}/version.rc")
437467
endif()
438468

439-
#target_link_libraries(${PROJECT_NAME} CURL::libcurl)
440-
target_link_libraries(${PROJECT_NAME} libcurl)
441-
442-
if(WIN32)
443-
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_INCLUDE_PATHS})
444-
target_link_libraries(${PROJECT_NAME} ${PROJECT_LIBRARIES} optimized crashpad strmiids)
445-
else()
446-
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_INCLUDE_PATHS} ${COREFOUNDATION} ${COCOA} ${IOSURF} ${GLKIT} ${AVFOUNDATION} ${IOKit} ${SECURITY_LIBRARY} ${BSM_LIBRARY})
447-
target_link_libraries(${PROJECT_NAME} ${PROJECT_LIBRARIES} crashpad ${COREFOUNDATION} ${COCOA} ${IOSURF} ${GLKIT} ${AVFOUNDATION} ${IOKit} ${SECURITY_LIBRARY} ${BSM_LIBRARY})
448-
endif()
469+
target_link_libraries(${PROJECT_NAME} obs-studio-server-lib)
449470

450471
if(MSVC)
451472
add_definitions(-D_CRT_SECURE_NO_WARNINGS -D_SILENCE_ALL_CXX17_DEPRECATION_WARNINGS)
@@ -483,17 +504,6 @@ else()
483504
)
484505
ENDIF()
485506
486-
IF(WIN32)
487-
target_compile_definitions(
488-
${PROJECT_NAME}
489-
PRIVATE
490-
WIN32_LEAN_AND_MEAN
491-
NOMINMAX
492-
UNICODE
493-
_UNICODE
494-
)
495-
ENDIF()
496-
497507
IF( NOT CLANG_ANALYZE_CONFIG)
498508
cppcheck_add_project(${PROJECT_NAME})
499509
ENDIF()
@@ -598,3 +608,49 @@ if (APPLE)
598608
DESTINATION "../../Contents/Frameworks" USE_SOURCE_PERMISSIONS
599609
)
600610
endif()
611+
612+
613+
614+
if(BUILD_TESTING)
615+
include(Catch)
616+
617+
add_executable(
618+
obs_studio_server_unit_tests
619+
"tests/test-osn-source.cpp"
620+
"tests/test-helper.cpp"
621+
"tests/test-helper.hpp"
622+
)
623+
624+
target_compile_definitions(
625+
obs_studio_server_unit_tests
626+
PRIVATE
627+
OSN_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
628+
)
629+
630+
target_link_libraries(
631+
obs_studio_server_unit_tests
632+
PRIVATE
633+
Catch2::Catch2WithMain
634+
obs-studio-server-lib
635+
)
636+
637+
if(APPLE)
638+
add_custom_command(
639+
TARGET obs_studio_server_unit_tests
640+
POST_BUILD
641+
COMMAND /usr/bin/codesign --force --sign - "$<TARGET_FILE:obs_studio_server_unit_tests>"
642+
COMMENT "Ad-hoc signing obs_studio_server_unit_tests before Catch2 test discovery"
643+
VERBATIM
644+
)
645+
endif()
646+
647+
catch_discover_tests(
648+
obs_studio_server_unit_tests
649+
TEST_PREFIX "obs_studio_server_unit_tests::"
650+
DL_PATHS
651+
"$<TARGET_FILE_DIR:Catch2::Catch2>"
652+
"$<TARGET_FILE_DIR:Catch2::Catch2WithMain>"
653+
"${libobs_SOURCE_DIR}/bin/64bit"
654+
"$<TARGET_FILE_DIR:libcurl>"
655+
)
656+
endif()

obs-studio-server/source/nodeobs_api.cpp

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -923,19 +923,21 @@ void OBS_API::OBS_API_initAPI(void *data, const int64_t id, const std::vector<ip
923923
}
924924
}
925925

926-
// Register the pre and post server callbacks to log the data into the crashmanager
927-
g_server->set_pre_callback(
928-
[](std::string cname, std::string fname, const std::vector<ipc::value> &args, void *data) {
929-
util::CrashManager &crashManager = *static_cast<util::CrashManager *>(data);
930-
crashManager.ProcessPreServerCall(cname, fname, args);
931-
},
932-
&crashManager);
933-
g_server->set_post_callback(
934-
[](std::string cname, std::string fname, const std::vector<ipc::value> &args, void *data) {
935-
util::CrashManager &crashManager = *static_cast<util::CrashManager *>(data);
936-
crashManager.ProcessPostServerCall(cname, fname, args);
937-
},
938-
&crashManager);
926+
if (g_server) {
927+
// Register the pre and post server callbacks to log the data into the crashmanager
928+
g_server->set_pre_callback(
929+
[](std::string cname, std::string fname, const std::vector<ipc::value> &args, void *data) {
930+
util::CrashManager &crashManager = *static_cast<util::CrashManager *>(data);
931+
crashManager.ProcessPreServerCall(cname, fname, args);
932+
},
933+
&crashManager);
934+
g_server->set_post_callback(
935+
[](std::string cname, std::string fname, const std::vector<ipc::value> &args, void *data) {
936+
util::CrashManager &crashManager = *static_cast<util::CrashManager *>(data);
937+
crashManager.ProcessPostServerCall(cname, fname, args);
938+
},
939+
&crashManager);
940+
}
939941

940942
#endif
941943

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#include <catch2/catch_test_macros.hpp>
2+
#include <fstream>
3+
#include "nodeobs_api.h"
4+
#include "osn-error.hpp"
5+
#include <obs.h>
6+
#include "shared.hpp"
7+
#include <string>
8+
#include "test-helper.hpp"
9+
#include <vector>
10+
11+
namespace osn::tests {
12+
13+
// Reads the `wd` export from tests/osn-tests/osn/index.ts.
14+
// The line has the form: const wd: string = "...";
15+
static std::string parseWdFromIndexTs()
16+
{
17+
const std::string indexTsPath = std::string(OSN_SOURCE_DIR) + "/tests/osn-tests/osn/index.ts";
18+
19+
std::ifstream file(indexTsPath);
20+
INFO("Could not open " + indexTsPath);
21+
REQUIRE(file.is_open());
22+
23+
std::string line;
24+
while (std::getline(file, line)) {
25+
const std::string prefix = "const wd";
26+
if (line.find(prefix) == std::string::npos)
27+
continue;
28+
29+
auto first = line.find('"');
30+
auto last = line.rfind('"');
31+
INFO("Could not parse wd value from: " + line);
32+
REQUIRE(first != std::string::npos);
33+
REQUIRE(last != first);
34+
35+
return line.substr(first + 1, last - first - 1);
36+
}
37+
38+
FAIL("wd declaration not found in " + indexTsPath);
39+
return {};
40+
}
41+
42+
void setWorkingFolder(const std::string &wd)
43+
{
44+
std::vector<ipc::value> args = {ipc::value(wd)};
45+
std::vector<ipc::value> response;
46+
OBS_API::SetWorkingDirectory(nullptr, 0, args, response);
47+
CHECK(response.size() >= 2);
48+
ErrorCode error = (ErrorCode)response[0].value_union.ui64;
49+
CHECK(error == ErrorCode::Ok);
50+
}
51+
52+
void setupApi()
53+
{
54+
#if defined(__APPLE__)
55+
g_util_osx = new UtilInt();
56+
g_util_osx->init();
57+
// Workaround normal app startup where "browser_source" plugin is initialized
58+
CHECK(!g_util_osx->hasInitApi());
59+
g_util_osx->nextState();
60+
CHECK(g_util_osx->hasInitApi());
61+
#endif
62+
const std::string appPath = std::string(OSN_SOURCE_DIR) + "/tests/osn-tests/osnData/slobs-client";
63+
// osn.NodeObs.OBS_API_initAPI(this.language, this.obsPath, this.version, this.crashServer);
64+
std::vector<ipc::value> args = {ipc::value(appPath), ipc::value("en-US"), ipc::value("0.00.00-preview.0"), ipc::value("")};
65+
std::vector<ipc::value> response;
66+
OBS_API::OBS_API_initAPI(nullptr, 0, args, response);
67+
CHECK(response.size() >= 2);
68+
ErrorCode error = (ErrorCode)response[0].value_union.ui64;
69+
CHECK(error == ErrorCode::Ok);
70+
}
71+
72+
void TestHelper::initializeOBS()
73+
{
74+
const std::string wd = parseWdFromIndexTs();
75+
setWorkingFolder(wd);
76+
setupApi();
77+
}
78+
79+
void TestHelper::finalizeOBS()
80+
{
81+
std::vector<ipc::value> args = {};
82+
std::vector<ipc::value> response;
83+
OBS_API::OBS_API_destroyOBS_API(nullptr, 0, args, response);
84+
CHECK(response.size() >= 1);
85+
ErrorCode error = (ErrorCode)response[0].value_union.ui64;
86+
CHECK(error == ErrorCode::Ok);
87+
}
88+
89+
} // namespace osn::tests
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#pragma once
2+
3+
namespace osn::tests {
4+
class TestHelper {
5+
public:
6+
static void initializeOBS();
7+
static void finalizeOBS();
8+
};
9+
} // namespace osn::tests
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#include <catch2/catch_test_macros.hpp>
2+
#include <mutex>
3+
#include "nodeobs_api.h"
4+
#include "osn-error.hpp"
5+
#include "osn-input.hpp"
6+
#include "osn-source.hpp"
7+
#include <obs.h>
8+
#include "shared.hpp"
9+
#include <string>
10+
#include "test-helper.hpp"
11+
#include <thread>
12+
#include <vector>
13+
14+
TEST_CASE("Run osn::source tests")
15+
{
16+
osn::tests::TestHelper::initializeOBS();
17+
18+
SECTION("Get properties of browser source while releasing concurrently does not crash")
19+
{
20+
const int iterations = 20;
21+
std::vector<std::thread> workers;
22+
std::vector<uint8_t> releaseOk(iterations, 0);
23+
std::vector<ErrorCode> getPropertiesCode(iterations, ErrorCode::Error);
24+
25+
for (int i = 0; i < iterations; i++) {
26+
const std::string sourceName = "test-input-" + std::to_string(i);
27+
std::vector<ipc::value> args = {ipc::value("browser_source"), ipc::value(sourceName)};
28+
std::vector<ipc::value> response;
29+
30+
osn::Input::Create(nullptr, 0, args, response);
31+
REQUIRE(response.size() >= 2);
32+
ErrorCode error = (ErrorCode)response[0].value_union.ui64;
33+
REQUIRE(error == ErrorCode::Ok);
34+
35+
uint64_t sourceId = response[1].value_union.ui64;
36+
37+
workers.push_back(std::thread([sourceId, i, &getPropertiesCode]() {
38+
std::vector<ipc::value> propArgs = {ipc::value(sourceId)};
39+
std::vector<ipc::value> propResponse;
40+
osn::Source::GetProperties(nullptr, 0, propArgs, propResponse);
41+
if (propResponse.size() >= 1) {
42+
getPropertiesCode[i] = (ErrorCode)propResponse[0].value_union.ui64;
43+
}
44+
}));
45+
46+
workers.push_back(std::thread([sourceId, i, &releaseOk]() {
47+
std::vector<ipc::value> propArgs = {ipc::value(sourceId)};
48+
std::vector<ipc::value> propResponse;
49+
osn::Source::Release(nullptr, 0, propArgs, propResponse);
50+
#if defined(TRIGGER_CRASH)
51+
// Also release the refcount to trigger actual private data destruction
52+
obs_source_t *src = osn::Source::Manager::GetInstance().find(sourceId); // may be null already
53+
if (src)
54+
obs_source_release(src);
55+
#endif
56+
57+
// Capture result for checking on the main thread after join.
58+
if (propResponse.size() >= 1) {
59+
releaseOk[i] = ((ErrorCode)propResponse[0].value_union.ui64 == ErrorCode::Ok);
60+
}
61+
}));
62+
}
63+
64+
for (std::thread &worker : workers) {
65+
if (worker.joinable())
66+
worker.join();
67+
}
68+
69+
// Check release results on the main thread where Catch2 is safe to use.
70+
for (int i = 0; i < iterations; i++) {
71+
CHECK(releaseOk[i]);
72+
// ErrorCode::InvalidReference is possible if the source was deleted before we could acquire the source
73+
bool expectedErrorCode = getPropertiesCode[i] == ErrorCode::Ok || getPropertiesCode[i] == ErrorCode::InvalidReference;
74+
CHECK(expectedErrorCode);
75+
}
76+
}
77+
78+
osn::tests::TestHelper::finalizeOBS();
79+
}

0 commit comments

Comments
 (0)