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
3 changes: 1 addition & 2 deletions .github/workflows/ci-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,7 @@ jobs:
id: test
shell: msys2 {0}
working-directory: build/tests
run: |
./test_sunshine.exe --gtest_color=yes --gtest_output=xml:test_results.xml
run: ./test_sunshine.exe --gtest_color=yes --gtest_output=xml:test_results.xml

- name: Generate gcov report
id: test_report
Expand Down
38 changes: 32 additions & 6 deletions cmake/FindUdev.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@
#
# UDEV_FOUND - system has udev
# UDEV_RULES_INSTALL_DIR - the udev rules install directory
# UDEVADM_EXECUTABLE - path to udevadm executable
# UDEV_VERSION - version of udev/systemd

IF (NOT WIN32)

if(NOT WIN32)
find_package(PkgConfig QUIET)
if(PKG_CONFIG_FOUND)
pkg_check_modules(UDEV "udev")
endif()

if (UDEV_FOUND)
if(UDEV_FOUND)
if(UDEV_VERSION)
message(STATUS "Found udev/systemd version: ${UDEV_VERSION}")
else()
message(WARNING "Could not determine udev/systemd version")
set(UDEV_VERSION "0")
endif()

execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE}
--variable=udevdir udev
OUTPUT_VARIABLE UDEV_RULES_INSTALL_DIR)
Expand All @@ -23,6 +31,24 @@ IF (NOT WIN32)

mark_as_advanced(UDEV_RULES_INSTALL_DIR)

endif ()

ENDIF ()
# Check if udevadm is available
find_program(UDEVADM_EXECUTABLE udevadm
PATHS /usr/bin /bin /usr/sbin /sbin
DOC "Path to udevadm executable")
mark_as_advanced(UDEVADM_EXECUTABLE)

# Handle version requirements
if(Udev_FIND_VERSION)
if(UDEV_VERSION VERSION_LESS Udev_FIND_VERSION)
set(UDEV_FOUND FALSE)
if(Udev_FIND_REQUIRED)
message(FATAL_ERROR "Udev version ${UDEV_VERSION} less than required version ${Udev_FIND_VERSION}")
else()
message(STATUS "Udev version ${UDEV_VERSION} less than required version ${Udev_FIND_VERSION}")
endif()
else()
message(STATUS "Udev version ${UDEV_VERSION} meets requirement (>= ${Udev_FIND_VERSION})")
endif()
endif()
endif()
endif()
15 changes: 13 additions & 2 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ set(TEST_DEFINITIONS) # list will be appended as needed

# this indicates we're building tests in case sunshine needs to adjust some code or add private tests
list(APPEND TEST_DEFINITIONS SUNSHINE_TESTS)
list(APPEND TEST_DEFINITIONS SUNSHINE_SOURCE_DIR="${CMAKE_SOURCE_DIR}")
list(APPEND TEST_DEFINITIONS SUNSHINE_TEST_BIN_DIR="${CMAKE_CURRENT_BINARY_DIR}")

if(NOT WIN32)
find_package(Udev 255) # we need 255+ for udevadm verify
message(STATUS "UDEV_FOUND: ${UDEV_FOUND}")
if(UDEV_FOUND)
list(APPEND TEST_DEFINITIONS UDEVADM_EXECUTABLE="${UDEVADM_EXECUTABLE}")
endif()
endif()

file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS
${CMAKE_SOURCE_DIR}/tests/*.h
Expand All @@ -55,15 +65,16 @@ add_executable(${PROJECT_NAME}
# Copy files needed for config consistency tests to build directory
# This ensures both CLI and CLion can access the same files relative to the test executable
# Using configure_file ensures files are copied when they change between builds
set(CONFIG_TEST_FILES
set(INTEGRATION_TEST_FILES
"src/config.cpp"
"src_assets/common/assets/web/config.html"
"docs/configuration.md"
"src_assets/common/assets/web/public/assets/locale/en.json"
"src_assets/common/assets/web/configs/tabs/General.vue"
"src_assets/linux/misc/60-sunshine.rules"
)

foreach(file ${CONFIG_TEST_FILES})
foreach(file ${INTEGRATION_TEST_FILES})
configure_file(
"${CMAKE_SOURCE_DIR}/${file}"
"${CMAKE_CURRENT_BINARY_DIR}/${file}"
Expand Down
194 changes: 194 additions & 0 deletions tests/integration/test_external_commands.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/**
* @file tests/integration/test_external_commands.cpp
* @brief Integration tests for running external commands with platform-specific validation
*/
#include "../tests_common.h"

// standard includes
#include <format>
#include <string>
#include <tuple>
#include <vector>

// lib includes
#include <boost/process/v1.hpp>

// local includes
#include "src/platform/common.h"

// Test data structure for parameterized testing
struct ExternalCommandTestData {
std::string command;
std::string platform; // "windows", "linux", "macos", or "all"
bool should_succeed;
std::string description;
std::string working_directory; // Optional: if empty, uses SUNSHINE_SOURCE_DIR
bool xfail_condition = false; // Optional: condition for expected failure
std::string xfail_reason = ""; // Optional: reason for expected failure

// Constructor with xfail parameters
ExternalCommandTestData(std::string cmd, std::string plat, const bool succeed, std::string desc, std::string work_dir = "", const bool xfail_cond = false, std::string xfail_rsn = ""):
command(std::move(cmd)),
platform(std::move(plat)),
should_succeed(succeed),
description(std::move(desc)),
working_directory(std::move(work_dir)),
xfail_condition(xfail_cond),
xfail_reason(std::move(xfail_rsn)) {}
};

class ExternalCommandTest: public ::testing::TestWithParam<ExternalCommandTestData> {
protected:
void SetUp() override {
if constexpr (IS_WINDOWS) {
current_platform = "windows";
} else if constexpr (IS_MACOS) {
current_platform = "macos";
} else if constexpr (IS_LINUX) {
current_platform = "linux";
}
}

[[nodiscard]] bool shouldRunOnCurrentPlatform(const std::string_view &test_platform) const {
return test_platform == "all" || test_platform == current_platform;
}

// Helper function to run a command using the existing process infrastructure
static std::pair<int, std::string> runCommand(const std::string &cmd, const std::string_view &working_dir) {
const auto env = boost::this_process::environment();

// Determine the working directory: use the provided working_dir or fall back to SUNSHINE_SOURCE_DIR
boost::filesystem::path effective_working_dir;

if (!working_dir.empty()) {
effective_working_dir = working_dir;
} else {
// Use SUNSHINE_SOURCE_DIR CMake definition as the default working directory
effective_working_dir = SUNSHINE_SOURCE_DIR;
}

std::error_code ec;

// Create a temporary file to capture output
const auto temp_file = std::tmpfile();
if (!temp_file) {
return {-1, "Failed to create temporary file for output"};
}

// Run the command using the existing platf::run_command function
auto child = platf::run_command(
false, // not elevated
false, // not interactive
cmd,
effective_working_dir,
env,
temp_file,
ec,
nullptr // no process group
);

if (ec) {
std::fclose(temp_file);
return {-1, std::format("Failed to start command: {}", ec.message())};
}

// Wait for the command to complete
child.wait();
int exit_code = child.exit_code();

// Read the output from the temporary file
std::rewind(temp_file);
std::string output;
std::array<char, 1024> buffer {};
while (std::fgets(buffer.data(), static_cast<int>(buffer.size()), temp_file)) {
// std::string constructor automatically handles null-terminated strings
output += std::string(buffer.data());
}
std::fclose(temp_file);

return {exit_code, output};
}

public:
std::string current_platform;
};

// Test case implementation
TEST_P(ExternalCommandTest, RunExternalCommand) {
const auto &[command, platform, should_succeed, description, working_directory, xfail_condition, xfail_reason] = GetParam();

// Skip test if not for the current platform
if (!shouldRunOnCurrentPlatform(platform)) {
GTEST_SKIP() << "Test not applicable for platform: " << current_platform;
}

// Use the xfail condition and reason from test data
XFAIL_IF(xfail_condition, xfail_reason);

BOOST_LOG(info) << "Running external command test: " << description;
BOOST_LOG(debug) << "Command: " << command;

auto [exit_code, output] = runCommand(command, working_directory);

BOOST_LOG(debug) << "Command exit code: " << exit_code;
if (!output.empty()) {
BOOST_LOG(debug) << "Command output: " << output;
}

if (should_succeed) {
HANDLE_XFAIL_ASSERT_EQ(exit_code, 0, std::format("Command should have succeeded but failed with exit code {}\nOutput: {}", std::to_string(exit_code), output));
} else {
HANDLE_XFAIL_ASSERT_NE(exit_code, 0, std::format("Command should have failed but succeeded\nOutput: {}", output));
}
}

// Platform-specific command strings
constexpr auto SIMPLE_COMMAND = IS_WINDOWS ? "where cmd" : "which sh";

#ifdef UDEVADM_EXECUTABLE
#define UDEV_TESTS \
ExternalCommandTestData { \
std::format("{} verify {}/src_assets/linux/misc/60-sunshine.rules", UDEVADM_EXECUTABLE, SUNSHINE_TEST_BIN_DIR), \
"linux", \
true, \
"Test udev rules file" \
},
#else
#define UDEV_TESTS
#endif

// Test data
INSTANTIATE_TEST_SUITE_P(
ExternalCommands,
ExternalCommandTest,
::testing::Values(
UDEV_TESTS
// Cross-platform tests with xfail on Windows CI
ExternalCommandTestData {
SIMPLE_COMMAND,
"all",
true,
"Simple command test",
"", // working_directory
IS_WINDOWS, // xfail_condition
"Simple command test fails on Windows CI environment" // xfail_reason
},
// Cross-platform failing test
ExternalCommandTestData {
"non_existent_command_12345",
"all",
false,
"Test command that should fail"
}
),
[](const ::testing::TestParamInfo<ExternalCommandTestData> &info) {
// Generate test names from a description
std::string name = info.param.description;
// Replace spaces and special characters with underscores for valid test names
std::replace_if(name.begin(), name.end(), [](char c) {
return !std::isalnum(c);
},
'_');
return name;
}
);
Loading
Loading