mirror of
https://github.com/LizardByte/Sunshine.git
synced 2026-11-03 02:37:07 +09:00
312 lines
12 KiB
C++
312 lines
12 KiB
C++
/**
|
|
* @file tests/unit/test_config.cpp
|
|
* @brief Tests for Sunshine configuration parsing, range validation, and persistence helpers.
|
|
*/
|
|
|
|
// test includes
|
|
#include "../tests_common.h"
|
|
|
|
// standard includes
|
|
#include <array>
|
|
#include <filesystem>
|
|
#include <format>
|
|
#include <string>
|
|
#include <utility>
|
|
|
|
// local includes
|
|
#include <src/config.h>
|
|
#include <src/file_handler.h>
|
|
#include <src/utility.h>
|
|
|
|
using namespace std::literals;
|
|
|
|
/**
|
|
* @brief Preparation commands own their command strings and copy privilege settings.
|
|
*/
|
|
TEST(ConfigPrepCommandTest, PreservesCommandsAndElevationWithBothConstructors) {
|
|
const config::prep_cmd_t paired {"launch"s, "undo"s, true};
|
|
EXPECT_EQ(paired.do_cmd, "launch");
|
|
EXPECT_EQ(paired.undo_cmd, "undo");
|
|
EXPECT_TRUE(paired.elevated);
|
|
|
|
const config::prep_cmd_t single {"launch-only"s, false};
|
|
EXPECT_EQ(single.do_cmd, "launch-only");
|
|
EXPECT_TRUE(single.undo_cmd.empty());
|
|
EXPECT_FALSE(single.elevated);
|
|
}
|
|
|
|
TEST(ConfigParsingTest, ParsedEntriesOwnKeysAndSupportStringViewLookup) {
|
|
std::string input = "encoder = software\nencoder = nvenc\n";
|
|
const auto entries = config::parse_config(input);
|
|
input.assign(input.size(), 'x');
|
|
|
|
const std::string padded = "__encoder__";
|
|
const auto key = std::string_view {padded}.substr(2, 7);
|
|
const auto found = entries.find(key);
|
|
ASSERT_NE(found, entries.end());
|
|
EXPECT_EQ(found->second, "software");
|
|
EXPECT_FALSE(entries.contains("missing"sv));
|
|
EXPECT_EQ(entries.size(), 1);
|
|
}
|
|
|
|
/**
|
|
* @brief Duplicate configuration keys retain their first nonempty entry.
|
|
*/
|
|
TEST(ConfigParsingTest, DuplicateKeysPreserveFirstValueAfterIgnoredEmptyEntry) {
|
|
const auto entries = config::parse_config("encoder = software\nencoder = nvenc\noutput_name =\noutput_name = later\noutput_name = ignored\n");
|
|
EXPECT_EQ(entries.at("encoder"), "software");
|
|
EXPECT_EQ(entries.at("output_name"), "later");
|
|
EXPECT_EQ(entries.size(), 2);
|
|
}
|
|
|
|
TEST(ConfigDefaultsTest, UsesAutomaticGamepadSelection) {
|
|
EXPECT_EQ(config::input.gamepad, "auto");
|
|
}
|
|
|
|
TEST(ConfigDefaultsTest, TreatsFp16SdrCapturesAsLinear) {
|
|
EXPECT_FALSE(config::video.fp16_sdr_gamma_encoded);
|
|
}
|
|
|
|
TEST(ConfigDefaultsTest, PreservesAmdEncoderNumericDefaults) {
|
|
EXPECT_EQ(config::video.amd.amd_usage_h264, 1);
|
|
EXPECT_EQ(config::video.amd.amd_usage_hevc, 1);
|
|
EXPECT_EQ(config::video.amd.amd_usage_av1, 2);
|
|
EXPECT_EQ(config::video.amd.amd_rc_h264, 3);
|
|
EXPECT_EQ(config::video.amd.amd_rc_hevc, 1);
|
|
EXPECT_EQ(config::video.amd.amd_rc_av1, 1);
|
|
EXPECT_EQ(config::video.amd.amd_quality_h264, 0);
|
|
EXPECT_EQ(config::video.amd.amd_quality_hevc, 5);
|
|
EXPECT_EQ(config::video.amd.amd_quality_av1, 70);
|
|
EXPECT_EQ(config::video.amd.amd_coder, 0);
|
|
}
|
|
|
|
using NvencPresetNameParam = std::pair<int, std::string_view>;
|
|
|
|
/**
|
|
* @brief Parameterized coverage for FFmpeg's symbolic NVENC preset names.
|
|
*/
|
|
struct NvencPresetNameTest: testing::TestWithParam<NvencPresetNameParam> {};
|
|
|
|
TEST_P(NvencPresetNameTest, UsesStableSymbolicName) {
|
|
const auto &[quality_preset, expected] = GetParam();
|
|
|
|
EXPECT_EQ(expected, config::nv::ffmpeg_preset_from_quality(quality_preset));
|
|
}
|
|
|
|
INSTANTIATE_TEST_SUITE_P(
|
|
NvencQualityPresets,
|
|
NvencPresetNameTest,
|
|
testing::Values(
|
|
NvencPresetNameParam {1, "p1"sv},
|
|
NvencPresetNameParam {2, "p2"sv},
|
|
NvencPresetNameParam {3, "p3"sv},
|
|
NvencPresetNameParam {4, "p4"sv},
|
|
NvencPresetNameParam {5, "p5"sv},
|
|
NvencPresetNameParam {6, "p6"sv},
|
|
NvencPresetNameParam {7, "p7"sv}
|
|
)
|
|
);
|
|
|
|
namespace {
|
|
|
|
/**
|
|
* @brief Fixture that redirects configuration persistence to a temporary file.
|
|
*/
|
|
class ConfigPersistenceTest: public testing::Test {
|
|
protected:
|
|
/**
|
|
* @brief Preserve the active config path and prepare an empty temporary path.
|
|
*/
|
|
void SetUp() override {
|
|
original_config_file_ = config::sunshine.config_file;
|
|
original_gamepad_driver_ = config::input.gamepad_driver;
|
|
original_gamepad_ = config::input.gamepad;
|
|
original_apps_file_ = config::stream.file_apps;
|
|
config_file_ = std::filesystem::temp_directory_path() / "sunshine_test_config_persistence.conf"; // NOSONAR(cpp:S5443): safe for tests
|
|
std::filesystem::remove(config_file_);
|
|
config::sunshine.config_file = config_file_.string();
|
|
config::input.gamepad_driver.clear();
|
|
}
|
|
|
|
/**
|
|
* @brief Remove the temporary file and restore the active config path.
|
|
*/
|
|
void TearDown() override {
|
|
config::sunshine.config_file = std::move(original_config_file_);
|
|
config::input.gamepad_driver = std::move(original_gamepad_driver_);
|
|
config::input.gamepad = std::move(original_gamepad_);
|
|
config::stream.file_apps = std::move(original_apps_file_);
|
|
std::filesystem::remove(config_file_.parent_path() / "sunshine_test_config_apps.json");
|
|
std::filesystem::remove(config_file_);
|
|
}
|
|
|
|
/**
|
|
* @brief Get the temporary configuration file used by the test.
|
|
*
|
|
* @return Temporary configuration file path.
|
|
*/
|
|
const std::filesystem::path &config_file() const {
|
|
return config_file_;
|
|
}
|
|
|
|
private:
|
|
std::filesystem::path config_file_; ///< Temporary configuration file used by the test.
|
|
std::string original_config_file_; ///< Active configuration path restored after each test.
|
|
std::string original_gamepad_driver_; ///< Gamepad driver preference restored after each test.
|
|
std::string original_gamepad_; ///< Gamepad profile restored after each test.
|
|
std::string original_apps_file_; ///< Applications file path restored after each test.
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_F(ConfigPersistenceTest, AppendsMissingOptionWithoutReplacingExistingText) {
|
|
ASSERT_EQ(file_handler::write_file(config_file().string().c_str(), "# retained comment"), 0);
|
|
|
|
EXPECT_TRUE(config::persist_config_option_if_missing("gamepad_driver", "all"));
|
|
EXPECT_EQ(
|
|
file_handler::read_file(config_file().string().c_str()),
|
|
"# retained comment\ngamepad_driver = all\n"
|
|
);
|
|
|
|
EXPECT_FALSE(config::persist_config_option_if_missing("gamepad_driver", "virtualhid"));
|
|
EXPECT_EQ(
|
|
file_handler::read_file(config_file().string().c_str()),
|
|
"# retained comment\ngamepad_driver = all\n"
|
|
);
|
|
}
|
|
|
|
TEST_F(ConfigPersistenceTest, DoesNotReplaceAnExistingOption) {
|
|
ASSERT_EQ(file_handler::write_file(config_file().string().c_str(), "gamepad_driver = vigembus\n"), 0);
|
|
|
|
EXPECT_FALSE(config::persist_config_option_if_missing("gamepad_driver", "all"));
|
|
EXPECT_EQ(file_handler::read_file(config_file().string().c_str()), "gamepad_driver = vigembus\n");
|
|
}
|
|
|
|
TEST_F(ConfigPersistenceTest, SelectsAllDriversOnlyForLicensedUsersWithoutAPreference) {
|
|
EXPECT_FALSE(config::select_all_gamepad_drivers_if_licensed(false));
|
|
EXPECT_TRUE(config::input.gamepad_driver.empty());
|
|
EXPECT_TRUE(file_handler::read_file(config_file().string().c_str()).empty());
|
|
|
|
EXPECT_TRUE(config::select_all_gamepad_drivers_if_licensed(true));
|
|
EXPECT_EQ(config::input.gamepad_driver, config::GAMEPAD_DRIVER_ALL);
|
|
EXPECT_EQ(file_handler::read_file(config_file().string().c_str()), "gamepad_driver = all\n");
|
|
|
|
EXPECT_FALSE(config::select_all_gamepad_drivers_if_licensed(true));
|
|
EXPECT_EQ(file_handler::read_file(config_file().string().c_str()), "gamepad_driver = all\n");
|
|
}
|
|
|
|
TEST_F(ConfigPersistenceTest, AcceptsAllGamepadProfilesAcrossRepeatedConfigurationChanges) {
|
|
const auto apps_file = config_file().parent_path() / "sunshine_test_config_apps.json";
|
|
ASSERT_EQ(file_handler::write_file(apps_file.string().c_str(), "{}"), 0);
|
|
config::stream.file_apps = apps_file.string();
|
|
|
|
constexpr std::array profiles {"generic"sv, "x360"sv, "xone"sv, "xseries"sv, "ds4"sv, "ds5"sv, "switch"sv};
|
|
for (const auto profile : profiles) {
|
|
config::input.gamepad = "auto";
|
|
config::apply_config_for_test(std::string {"gamepad = "} + std::string {profile} + "\n");
|
|
EXPECT_EQ(config::input.gamepad, profile);
|
|
}
|
|
}
|
|
|
|
TEST_F(ConfigPersistenceTest, ParsesFp16SdrGammaEncodedOption) {
|
|
const auto apps_file = config_file().parent_path() / "sunshine_test_config_apps.json";
|
|
ASSERT_EQ(file_handler::write_file(apps_file.string().c_str(), "{}"), 0);
|
|
config::stream.file_apps = apps_file.string();
|
|
|
|
config::apply_config_for_test("fp16_sdr_gamma_encoded = enabled\n");
|
|
EXPECT_TRUE(config::video.fp16_sdr_gamma_encoded);
|
|
|
|
config::apply_config_for_test("fp16_sdr_gamma_encoded = disabled\n");
|
|
EXPECT_FALSE(config::video.fp16_sdr_gamma_encoded);
|
|
}
|
|
|
|
/**
|
|
* @brief Verify mixed-case boolean values update configuration correctly.
|
|
*/
|
|
TEST_F(ConfigPersistenceTest, ParsesBooleanOptionsCaseInsensitively) {
|
|
const auto original_video = config::video;
|
|
const auto restore_video = util::fail_guard([original_video]() {
|
|
config::video = original_video;
|
|
});
|
|
const auto apps_file = config_file().parent_path() / "sunshine_test_config_apps.json";
|
|
ASSERT_EQ(file_handler::write_file(apps_file.string().c_str(), "{}"), 0);
|
|
config::stream.file_apps = apps_file.string();
|
|
|
|
constexpr std::array cases {
|
|
std::pair {"Enabled"sv, true},
|
|
std::pair {"TRUE"sv, true},
|
|
std::pair {"On"sv, true},
|
|
std::pair {"Yes"sv, true},
|
|
std::pair {"Disabled"sv, false},
|
|
std::pair {"False"sv, false},
|
|
std::pair {"OFF"sv, false},
|
|
};
|
|
for (const auto &[setting, expected] : cases) {
|
|
config::video.fp16_sdr_gamma_encoded = !expected;
|
|
config::apply_config_for_test(std::string {"fp16_sdr_gamma_encoded = "} + std::string {setting} + "\n");
|
|
EXPECT_EQ(config::video.fp16_sdr_gamma_encoded, expected) << setting;
|
|
}
|
|
}
|
|
|
|
TEST_F(ConfigPersistenceTest, ParsesHexIntegersWiderThanAnInt) {
|
|
const auto original_video = config::video;
|
|
const auto restore_video = util::fail_guard([original_video]() {
|
|
config::video = original_video;
|
|
});
|
|
const auto apps_file = config_file().parent_path() / "sunshine_test_config_apps.json";
|
|
ASSERT_EQ(file_handler::write_file(apps_file.string().c_str(), "{}"), 0);
|
|
config::stream.file_apps = apps_file.string();
|
|
|
|
config::apply_config_for_test("min_threads = 0x1234567890\n");
|
|
EXPECT_EQ(config::video.min_threads, 0x34567890);
|
|
}
|
|
|
|
TEST_F(ConfigPersistenceTest, ParsesFecPercentagesAndRejectsOutOfRangeValues) {
|
|
const auto original_stream = config::stream;
|
|
const auto restore_stream = util::fail_guard([original_stream]() {
|
|
config::stream = original_stream;
|
|
});
|
|
const auto apps_file = config_file().parent_path() / "sunshine_test_config_apps.json";
|
|
ASSERT_EQ(file_handler::write_file(apps_file.string().c_str(), "{}"), 0);
|
|
config::stream.file_apps = apps_file.string();
|
|
|
|
constexpr std::array cases {
|
|
std::pair {0, 0},
|
|
std::pair {1, 1},
|
|
std::pair {20, 20},
|
|
std::pair {99, 99},
|
|
std::pair {100, 100},
|
|
std::pair {-1, 20},
|
|
std::pair {101, 20},
|
|
std::pair {255, 20},
|
|
};
|
|
for (const auto &[setting, expected] : cases) {
|
|
config::stream.fec_percentage = 20;
|
|
config::apply_config_for_test(std::format("fec_percentage = {}\n", setting));
|
|
EXPECT_EQ(config::stream.fec_percentage, expected) << setting;
|
|
}
|
|
}
|
|
|
|
TEST_F(ConfigPersistenceTest, ParsesNvencTwoPassModesAndDefaultsUnknownValues) {
|
|
using enum nvenc::nvenc_two_pass;
|
|
const auto original_video = config::video;
|
|
const auto restore_video = util::fail_guard([original_video]() {
|
|
config::video = original_video;
|
|
});
|
|
const auto apps_file = config_file().parent_path() / "sunshine_test_config_apps.json";
|
|
ASSERT_EQ(file_handler::write_file(apps_file.string().c_str(), "{}"), 0);
|
|
config::stream.file_apps = apps_file.string();
|
|
|
|
constexpr std::array cases {
|
|
std::pair {"disabled"sv, disabled},
|
|
std::pair {"quarter_res"sv, quarter_resolution},
|
|
std::pair {"full_res"sv, full_resolution},
|
|
std::pair {"unknown"sv, quarter_resolution},
|
|
};
|
|
for (const auto &[setting, expected] : cases) {
|
|
config::video.nv.two_pass = full_resolution;
|
|
config::apply_config_for_test(std::string {"nvenc_twopass = "} + std::string {setting} + "\n");
|
|
EXPECT_EQ(config::video.nv.two_pass, expected) << setting;
|
|
}
|
|
}
|