Files
Sunshine/tests/integration/test_external_commands.cpp
2025-09-21 00:22:42 -04:00

195 lines
6.1 KiB
C++

/**
* @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;
}
);