Skip to content
Open
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]);
}
102 changes: 80 additions & 22 deletions obs-studio-server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -424,9 +424,38 @@ 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_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(obs-studio-server-lib PUBLIC libcurl)

add_executable(
${PROJECT_NAME}
${osn-server_SOURCES}
"${PROJECT_SOURCE_DIR}/source/main.cpp"
)

if(WIN32)
Expand All @@ -436,16 +465,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)
Expand Down Expand Up @@ -483,17 +503,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 +607,52 @@ 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"
)

target_compile_definitions(
obs_studio_server_unit_tests
PRIVATE
OSN_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
OSN_TEST_WD="${CMAKE_INSTALL_PREFIX}"
)

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()

catch_discover_tests(
obs_studio_server_unit_tests
TEST_PREFIX "obs_studio_server_unit_tests::"
DL_PATHS
"$<TARGET_FILE_DIR:Catch2::Catch2>"
"$<TARGET_FILE_DIR:Catch2::Catch2WithMain>"
"$<TARGET_FILE_DIR:libcurl>"
if(APPLE)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CMake does not parse if() blocks recursively inside another command’s parentheses.

In CMake, this is a command invocation:

catch_discover_tests(
  obs_studio_server_unit_tests
  DL_PATHS
    ...
if(APPLE)
    ...
endif()
)

Everything between catch_discover_tests( and its matching ) is parsed as that command’s argument list. The if(APPLE) there is not treated as the start of a conditional block; it is just an unquoted argument/token inside the catch_discover_tests call.

"${libobs_SOURCE_DIR}/OBS.app/Contents/Frameworks"
elseif(WIN32)
"${libobs_SOURCE_DIR}/bin/64bit"
endif()
)
endif()
28 changes: 15 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,21 @@ 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 *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);
}

#endif

Expand Down
58 changes: 58 additions & 0 deletions obs-studio-server/tests/obs-setup.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#include <catch2/catch_test_macros.hpp>
#include <fstream>
#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__)
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<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);
REQUIRE(response.size() >= 1);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://catch2-temp.readthedocs.io/en/latest/configuration.html#disabling-exceptions

By default, Catch2 uses exceptions to signal errors and to abort tests when an assertion from the REQUIRE family of assertions fails.

ErrorCode error = (ErrorCode)response[0].value_union.ui64;
CHECK(error == ErrorCode::Ok);
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
87 changes: 87 additions & 0 deletions obs-studio-server/tests/test-osn-source.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#include <catch2/catch_test_macros.hpp>
#include <mutex>
#include "nodeobs_api.h"
#include "osn-error.hpp"
#include "osn-input.hpp"
#include "osn-source.hpp"
#include <obs.h>
#include "shared.hpp"
#include <string>
#include "obs-setup.hpp"
#include <thread>
#include <vector>

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<std::thread> workers;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a later REQUIRE aborts after earlier iterations started threads, workers will be destroyed while still joinable and terminate the process. Please add an RAII join guard or create all inputs before starting worker threads.

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(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(std::thread([sourceId, i, &releaseOk]() {

#if defined(TRIGGER_CRASH)
// 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) {
obs_source_release(src);
releaseOk[i] = true;
}
#else
// TODO: delete this block after staging (commit cc4a0431) is merged.
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);
}
#endif
}));
}

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);
}
#if defined(TRIGGER_CRASH)
// 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
}
}
Loading