Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 20 additions & 16 deletions ci/run-unit-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]);
}
113 changes: 88 additions & 25 deletions obs-studio-server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Comment thread
sandboxcoder marked this conversation as resolved.

#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()
Comment thread
sandboxcoder marked this conversation as resolved.

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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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}"
)
Comment thread
sandboxcoder marked this conversation as resolved.

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 - "$<TARGET_FILE:obs_studio_server_unit_tests>"
COMMENT "Ad-hoc signing obs_studio_server_unit_tests before Catch2 test discovery"
VERBATIM
)
endif()

set(_osn_server_test_dl_paths
"$<TARGET_FILE_DIR:Catch2::Catch2>"
"$<TARGET_FILE_DIR:Catch2::Catch2WithMain>"
"$<TARGET_FILE_DIR:libcurl>"
)

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()
22 changes: 9 additions & 13 deletions obs-studio-server/source/nodeobs_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -923,19 +923,15 @@ void OBS_API::OBS_API_initAPI(void *data, const int64_t id, const std::vector<ip
}
}

// 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<ipc::value> &args, void *data) {
util::CrashManager &crashManager = *static_cast<util::CrashManager *>(data);
crashManager.ProcessPreServerCall(cname, fname, args);
},
&crashManager);
g_server->set_post_callback(
[](std::string cname, std::string fname, const std::vector<ipc::value> &args, void *data) {
util::CrashManager &crashManager = *static_cast<util::CrashManager *>(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<ipc::value> &args,
void *) { util::CrashManager::ProcessPreServerCall(cname, fname, args); },
nullptr);
g_server->set_post_callback([](std::string cname, std::string fname, const std::vector<ipc::value> &args,
void *) { util::CrashManager::ProcessPostServerCall(cname, fname, args); },
nullptr);
}

#endif

Expand Down
2 changes: 1 addition & 1 deletion obs-studio-server/source/util-osx-impl.mm
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@

@implementation UtilImplObj

UtilObjCInt::UtilObjCInt(void) : self(NULL) {}
UtilObjCInt::UtilObjCInt(void) : self(NULL), state(EState::Idle), worker(nullptr) {}

UtilObjCInt::~UtilObjCInt(void)
{
Expand Down
68 changes: 68 additions & 0 deletions obs-studio-server/tests/obs-setup.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#include <catch2/catch_test_macros.hpp>
#include "nodeobs_api.h"
#include "osn-error.hpp"
#include <obs.h>
#include "shared.hpp"
#include <string>
#include "obs-setup.hpp"
#include <vector>

namespace osn::tests {

void setWorkingFolder(const std::string &wd)
{
std::vector<ipc::value> args = {ipc::value(wd)};
std::vector<ipc::value> response;
OBS_API::SetWorkingDirectory(nullptr, 0, args, response);
REQUIRE(response.size() >= 2);
ErrorCode error = (ErrorCode)response[0].value_union.ui64;
CHECK(error == ErrorCode::Ok);
Comment thread
sandboxcoder marked this conversation as resolved.
}
Comment thread
sandboxcoder marked this conversation as resolved.

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());
Comment thread
sandboxcoder marked this conversation as resolved.
Comment thread
sandboxcoder marked this conversation as resolved.
#endif
const std::string appPath = std::string(OSN_SOURCE_DIR) + "/tests/osn-tests/osnData/slobs-client";
std::vector<ipc::value> args = {ipc::value(appPath), ipc::value("en-US"), ipc::value("0.00.00-preview.0"), ipc::value("")};
std::vector<ipc::value> 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);
Comment thread
sandboxcoder marked this conversation as resolved.
}

ObsSetup::ObsSetup()
{
setWorkingFolder(OSN_TEST_WD);
setupApi();
}

ObsSetup::~ObsSetup()
{
std::vector<ipc::value> args = {};
std::vector<ipc::value> 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
}
Comment thread
sandboxcoder marked this conversation as resolved.

} // namespace osn::tests
10 changes: 10 additions & 0 deletions obs-studio-server/tests/obs-setup.hpp
Original file line number Diff line number Diff line change
@@ -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
85 changes: 85 additions & 0 deletions obs-studio-server/tests/test-osn-source.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#include <catch2/catch_test_macros.hpp>
#include "nodeobs_api.h"
#include "osn-error.hpp"
#include "osn-input.hpp"
#include "osn-source.hpp"
#include <obs.h>
#include "shared.hpp"
#include <cstdint>
#include <string>
#include "obs-setup.hpp"
#include <thread>
#include <utility>
#include <vector>
Comment thread
sandboxcoder marked this conversation as resolved.

// 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;

Comment thread
sandboxcoder marked this conversation as resolved.
SECTION("Get properties of browser source while releasing concurrently does not crash")
Comment thread
sandboxcoder marked this conversation as resolved.
{
auto sourceCount = osn::Source::Manager::GetInstance().size();
const int iterations = 20;
std::vector<joining_thread> workers;
std::vector<uint8_t> releaseOk(iterations, 0);
std::vector<ErrorCode> getPropertiesCode(iterations, ErrorCode::Error);
Comment thread
sandboxcoder marked this conversation as resolved.

for (int i = 0; i < iterations; i++) {
const std::string sourceName = "test-input-" + std::to_string(i);
std::vector<ipc::value> args = {ipc::value("browser_source"), ipc::value(sourceName)};
std::vector<ipc::value> 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<ipc::value> propArgs = {ipc::value(sourceId)};
std::vector<ipc::value> 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<ipc::value> propArgs = {ipc::value(sourceId)};
std::vector<ipc::value> 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);
}
})));
Comment thread
sandboxcoder marked this conversation as resolved.
}

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.
}
}
Loading