Files
dim-ghub ea248b1ecf feat(vr): add vr.mode with on, off and auto
vr.mode in config.yaml picks how Nightcap starts: off on the desktop, on in
VR, auto in VR only when this user's SteamVR is running and its OpenXR
runtime reports a headset. auto never starts SteamVR, and asks its runtime
from a short-lived copy of the process so the runtime's libraries do not load
before the VR backend arms. vr.enabled still works as on/off, and --vr or
--no-vr still override the file for one launch.
2026-10-15 19:37:32 -05:00

526 lines
22 KiB
C++

// Modified by vii from komaruworld/mocktail. See README "About this fork".
#include "runtime/runtime_config.h"
#include "runtime/system_proxy.h"
#include <gtest/gtest.h>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
namespace mocktail {
namespace runtime {
namespace {
class MapEnvironment final : public Environment {
public:
explicit MapEnvironment(
std::unordered_map<std::string, std::string> values = {})
: values_(std::move(values)) {}
std::optional<std::string> Get(std::string_view name) const override {
const auto found = values_.find(std::string(name));
if (found == values_.end()) {
return std::nullopt;
}
return found->second;
}
private:
std::unordered_map<std::string, std::string> values_;
};
TEST(RuntimeConfigTest, UsesSupportedDefaults) {
const MapEnvironment environment;
const RuntimeConfig config = RuntimeConfig::FromEnvironment(environment);
EXPECT_FALSE(config.headless());
EXPECT_EQ(config.vr_enabled(), kDefaultVrEnabled);
EXPECT_TRUE(config.vr_valid());
EXPECT_EQ(config.roblox_library_path(), "rbx_bin/libroblox.so");
EXPECT_EQ(config.graphics_backend(), GraphicsBackend::kVulkan);
EXPECT_EQ(config.graphics_backend_name(), "direct-vulkan");
EXPECT_EQ(config.window().width, 1280);
EXPECT_EQ(config.window().height, 720);
EXPECT_EQ(config.window().title, "Roblox");
EXPECT_FALSE(config.window().high_dpi);
EXPECT_TRUE(config.window().high_dpi_valid);
EXPECT_EQ(config.theme_mode(), "roblox");
EXPECT_FALSE(config.input_capabilities().touch_enabled);
EXPECT_TRUE(config.input_capabilities().mouse_enabled);
EXPECT_TRUE(config.input_capabilities().keyboard_enabled);
EXPECT_EQ(config.device_profile().name, "pc-windows-11");
EXPECT_EQ(config.device_profile().cache_key, "pc-windows-11");
EXPECT_EQ(config.device_profile().display_name, "Windows 11 PC");
EXPECT_TRUE(config.desktop_playability());
ASSERT_TRUE(config.roblox_http_user_agent().has_value());
EXPECT_EQ(*config.roblox_http_user_agent(), kRobloxDesktopHttpUserAgent);
EXPECT_EQ(config.frame_rate().mode, FrameRateLimitMode::kUnmanaged);
EXPECT_FALSE(config.performance().multithreaded_rendering);
EXPECT_GT(config.performance().physical_core_count, 0);
EXPECT_EQ(config.performance().memory_limit_mb, 0U);
EXPECT_TRUE(config.performance().memory_limit_valid);
EXPECT_EQ(config.performance().game_mode, GameModePolicy::kAuto);
EXPECT_TRUE(config.performance().game_mode_valid);
EXPECT_EQ(config.performance().physics_worker_mode,
PhysicsWorkerMode::kThroughput);
EXPECT_TRUE(config.performance().physics_worker_mode_valid);
EXPECT_EQ(config.audio_output_device(), "default");
EXPECT_TRUE(config.audio_output_device_valid());
EXPECT_EQ(config.audio_input_device(), "default");
EXPECT_TRUE(config.audio_input_device_valid());
EXPECT_TRUE(config.microphone_enabled());
EXPECT_FALSE(config.use_system_proxy());
EXPECT_FALSE(config.fleasion_enabled());
EXPECT_TRUE(config.fleasion_valid());
EXPECT_FALSE(config.network_proxy().has_value());
EXPECT_TRUE(config.discord_rpc().enabled);
EXPECT_TRUE(config.discord_rpc().show_place_name);
EXPECT_TRUE(config.discord_rpc().show_elapsed_time);
EXPECT_TRUE(config.discord_rpc().join_enabled);
EXPECT_TRUE(config.discord_rpc().public_servers_only);
EXPECT_EQ(config.discord_rpc().join_button_label, "Join Server");
EXPECT_EQ(config.discord_rpc().application_id, "1549428231927500843");
EXPECT_EQ(config.discord_rpc().text.browsing, "Browsing experiences");
EXPECT_EQ(config.discord_rpc().text.joining, "Joining an experience");
EXPECT_EQ(config.discord_rpc().text.playing, "Playing {place_name}");
EXPECT_EQ(config.discord_rpc().text.state, "by {creator_name}");
EXPECT_EQ(config.discord_rpc().text.unknown_place, "Unknown experience");
EXPECT_EQ(config.discord_rpc().text.title, "Roblox");
EXPECT_EQ(config.discord_rpc().images.large, "{place_icon}");
EXPECT_EQ(config.discord_rpc().images.large_text, "{place_name}");
EXPECT_EQ(config.discord_rpc().images.small, "roblox_small");
EXPECT_EQ(config.discord_rpc().images.small_text, "Roblox");
EXPECT_TRUE(config.discord_rpc_valid());
EXPECT_FALSE(config.has_unsafe_detached_thread_overrides());
EXPECT_TRUE(config.unsafe_detached_thread_overrides().empty());
}
TEST(RuntimeConfigTest, RejectsMalformedVrEnvironmentInsteadOfEnablingIt) {
const auto config = RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_VR_ENABLED", "maybe"}}));
EXPECT_FALSE(config.vr_valid());
EXPECT_EQ(config.vr_enabled(), kDefaultVrEnabled);
}
TEST(RuntimeConfigTest, ReadsVrModeAndLetsVrEnabledWin) {
EXPECT_EQ(RuntimeConfig::FromEnvironment(MapEnvironment()).vr_mode(),
kDefaultVrEnabled ? VrMode::kOn : VrMode::kOff);
const auto automatic = RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_VR_MODE", "auto"}}));
EXPECT_TRUE(automatic.vr_valid());
EXPECT_EQ(automatic.vr_mode(), VrMode::kAuto);
EXPECT_FALSE(automatic.vr_enabled());
EXPECT_TRUE(RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_VR_MODE", "on"}}))
.vr_enabled());
const auto overridden = RuntimeConfig::FromEnvironment(MapEnvironment(
{{"MOCKTAIL_VR_MODE", "auto"}, {"MOCKTAIL_VR_ENABLED", "0"}}));
EXPECT_EQ(overridden.vr_mode(), VrMode::kOff);
EXPECT_FALSE(RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_VR_MODE", "sometimes"}}))
.vr_valid());
EXPECT_STREQ(VrModeName(VrMode::kAuto), "auto");
}
TEST(RuntimeConfigTest, ReadsTypedRuntimeValues) {
const MapEnvironment environment({
{"MOCKTAIL_HEADLESS", "1"},
{"MOCKTAIL_VR_ENABLED", "true"},
{"ROBLOX_LIB_PATH", "/tmp/libroblox.so"},
{"MOCKTAIL_GRAPHICS_BACKEND", "vulkan"},
{"MOCKTAIL_WIN_WIDTH", "1920"},
{"MOCKTAIL_WIN_HEIGHT", "1080"},
{"MOCKTAIL_WIN_TITLE", "Mocktail Test"},
{"MOCKTAIL_WIN_HIGH_DPI", "true"},
{"MOCKTAIL_THEME", "system"},
{"MOCKTAIL_TOUCH_MODE", "on"},
{"MOCKTAIL_DESKTOP_PLAYABILITY", "0"},
{"MOCKTAIL_FRAME_RATE_LIMIT", "144"},
{"MOCKTAIL_MULTITHREADED_RENDERING", "true"},
{"MOCKTAIL_MEMORY_LIMIT_MB", "6144"},
{"MOCKTAIL_GAMEMODE", "off"},
{"MOCKTAIL_PHYSICS_WORKER_MODE", "latency"},
{"MOCKTAIL_AUDIO_OUTPUT_DEVICE", "USB Headset"},
{"MOCKTAIL_AUDIO_INPUT_DEVICE", "id:42"},
{"MOCKTAIL_HTTP_PROXY_HOST", "proxy.example.test"},
{"MOCKTAIL_HTTP_PROXY_PORT", "3128"},
{"MOCKTAIL_DISCORD_RPC_ENABLED", "1"},
{"MOCKTAIL_DISCORD_RPC_SHOW_PLACE_NAME", "0"},
{"MOCKTAIL_DISCORD_RPC_SHOW_ELAPSED_TIME", "0"},
{"MOCKTAIL_DISCORD_RPC_JOIN_ENABLED", "0"},
{"MOCKTAIL_DISCORD_RPC_PUBLIC_SERVERS_ONLY", "0"},
{"MOCKTAIL_DISCORD_RPC_JOIN_BUTTON_LABEL", "Play Together"},
{"MOCKTAIL_DISCORD_APPLICATION_ID", "123456789012345678"},
{"MOCKTAIL_DISCORD_RPC_TEXT_PLAYING", "In {place_name}"},
{"MOCKTAIL_DISCORD_RPC_TEXT_TITLE", "Nightcap"},
{"MOCKTAIL_DISCORD_RPC_IMAGE_LARGE", ""},
{"MOCKTAIL_DISCORD_RPC_IMAGE_SMALL", "linux"},
{"MOCKTAIL_DISCORD_RPC_IMAGE_SMALL_TEXT", "On Linux"},
});
const RuntimeConfig config = RuntimeConfig::FromEnvironment(environment);
EXPECT_TRUE(config.headless());
EXPECT_TRUE(config.vr_enabled());
EXPECT_TRUE(config.vr_valid());
EXPECT_EQ(config.roblox_library_path(), "/tmp/libroblox.so");
EXPECT_EQ(config.graphics_backend(), GraphicsBackend::kVulkan);
EXPECT_EQ(config.window().width, 1920);
EXPECT_EQ(config.window().height, 1080);
EXPECT_EQ(config.window().title, "Mocktail Test");
EXPECT_TRUE(config.window().high_dpi);
EXPECT_TRUE(config.window().high_dpi_valid);
EXPECT_EQ(config.theme_mode(), "system");
EXPECT_TRUE(config.input_capabilities().touch_enabled);
EXPECT_FALSE(config.desktop_playability());
EXPECT_FALSE(config.roblox_http_user_agent().has_value());
EXPECT_EQ(config.frame_rate().fixed_fps, 144);
EXPECT_TRUE(config.performance().multithreaded_rendering);
EXPECT_GT(config.performance().physical_core_count, 0);
EXPECT_EQ(config.performance().memory_limit_mb, 6144U);
EXPECT_TRUE(config.performance().memory_limit_enabled());
EXPECT_EQ(config.performance().game_mode, GameModePolicy::kOff);
EXPECT_TRUE(config.performance().game_mode_valid);
EXPECT_EQ(config.performance().physics_worker_mode,
PhysicsWorkerMode::kLatency);
EXPECT_EQ(config.audio_output_device(), "USB Headset");
EXPECT_TRUE(config.audio_output_device_valid());
EXPECT_EQ(config.audio_input_device(), "id:42");
EXPECT_TRUE(config.audio_input_device_valid());
ASSERT_TRUE(config.network_proxy().has_value());
EXPECT_EQ(config.network_proxy()->host, "proxy.example.test");
EXPECT_EQ(config.network_proxy()->port, 3128);
EXPECT_EQ(BuildNetworkProxyUrl(*config.network_proxy()),
"http://proxy.example.test:3128");
EXPECT_TRUE(config.discord_rpc().enabled);
EXPECT_FALSE(config.discord_rpc().show_place_name);
EXPECT_FALSE(config.discord_rpc().show_elapsed_time);
EXPECT_FALSE(config.discord_rpc().join_enabled);
EXPECT_FALSE(config.discord_rpc().public_servers_only);
EXPECT_EQ(config.discord_rpc().join_button_label, "Play Together");
EXPECT_EQ(config.discord_rpc().application_id, "123456789012345678");
EXPECT_EQ(config.discord_rpc().text.playing, "In {place_name}");
EXPECT_EQ(config.discord_rpc().text.title, "Nightcap");
EXPECT_TRUE(config.discord_rpc().images.large.empty());
EXPECT_EQ(config.discord_rpc().images.large_text, "{place_name}");
EXPECT_EQ(config.discord_rpc().images.small, "linux");
EXPECT_EQ(config.discord_rpc().images.small_text, "On Linux");
EXPECT_TRUE(config.discord_rpc_valid());
}
TEST(RuntimeConfigTest, BuildsBracketedIpv6ProxyUrl) {
const std::optional<NetworkProxyConfig> proxy =
ParseNetworkProxyConfig("::1", "8080");
ASSERT_TRUE(proxy.has_value());
EXPECT_EQ(BuildNetworkProxyUrl(*proxy), "http://[::1]:8080");
}
TEST(SystemProxyTest, SelectsHttpAndSocksProxies) {
const SystemProxyResult https =
SelectSystemProxy({"https://127.0.0.1:7890"});
ASSERT_TRUE(https) << https.error;
ASSERT_TRUE(https.proxy.has_value());
EXPECT_EQ(BuildNetworkProxyUrl(*https.proxy), "http://127.0.0.1:7890");
const SystemProxyResult socks =
SelectSystemProxy({"socks://127.0.0.1:1080"});
ASSERT_TRUE(socks) << socks.error;
ASSERT_TRUE(socks.proxy.has_value());
EXPECT_EQ(BuildNetworkProxyUrl(*socks.proxy), "socks5h://127.0.0.1:1080");
}
TEST(RuntimeConfigTest, RejectsMalformedDiscordApplicationId) {
const RuntimeConfig config = RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_DISCORD_APPLICATION_ID", "not-a-snowflake"}}));
EXPECT_FALSE(config.discord_rpc_valid());
const RuntimeConfig invalid_boolean = RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_DISCORD_RPC_ENABLED", "sometimes"}}));
EXPECT_FALSE(invalid_boolean.discord_rpc_valid());
}
TEST(RuntimeConfigTest, RejectsInvalidEnvironmentProxyValues) {
for (const std::pair<const char*, const char*> invalid : {
std::pair{"proxy.example.test", "not-a-port"},
std::pair{"proxy.example.test", "0"},
std::pair{"proxy.example.test", "65536"},
std::pair{"https://proxy.example.test", "443"},
std::pair{"proxy.example.test/path", "443"},
std::pair{"proxy.example.test\nInjected", "443"},
}) {
SCOPED_TRACE(invalid.first);
const MapEnvironment environment({
{"MOCKTAIL_HTTP_PROXY_HOST", invalid.first},
{"MOCKTAIL_HTTP_PROXY_PORT", invalid.second},
});
EXPECT_FALSE(RuntimeConfig::FromEnvironment(environment)
.network_proxy()
.has_value());
}
}
TEST(RuntimeConfigTest, KeepsUnknownTouchModeDisabled) {
const MapEnvironment environment({{"MOCKTAIL_TOUCH_MODE", "tablet"}});
EXPECT_FALSE(RuntimeConfig::FromEnvironment(environment)
.input_capabilities()
.touch_enabled);
}
TEST(RuntimeConfigTest, PreservesExplicitRobloxHttpUserAgent) {
const MapEnvironment environment({
{"MOCKTAIL_USER_AGENT", "Roblox/TestDesktop"},
{"MOCKTAIL_DESKTOP_PLAYABILITY", "0"},
{"MOCKTAIL_TOUCH_MODE", "on"},
});
const RuntimeConfig config = RuntimeConfig::FromEnvironment(environment);
EXPECT_FALSE(config.desktop_playability());
ASSERT_TRUE(config.roblox_http_user_agent().has_value());
EXPECT_EQ(*config.roblox_http_user_agent(), "Roblox/TestDesktop");
}
TEST(RuntimeConfigTest, KeepsLegacyTouchAndPlayabilityOverridesIndependent) {
const RuntimeConfig touch_desktop = RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_TOUCH_MODE", "on"}}));
EXPECT_TRUE(touch_desktop.input_capabilities().touch_enabled);
EXPECT_TRUE(touch_desktop.desktop_playability());
ASSERT_TRUE(touch_desktop.roblox_http_user_agent().has_value());
EXPECT_EQ(*touch_desktop.roblox_http_user_agent(),
kRobloxDesktopHttpUserAgent);
const RuntimeConfig no_touch_mobile = RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_DESKTOP_PLAYABILITY", "false"}}));
EXPECT_FALSE(no_touch_mobile.input_capabilities().touch_enabled);
EXPECT_FALSE(no_touch_mobile.desktop_playability());
EXPECT_FALSE(no_touch_mobile.roblox_http_user_agent().has_value());
}
TEST(RuntimeConfigTest, ResolvesOneLineDevicePresetsAndAliases) {
struct ExpectedProfile {
const char* configured;
const char* canonical;
DeviceClass device_class;
const char* display_name;
bool touch;
bool mouse;
bool keyboard;
const char* user_agent;
};
for (const ExpectedProfile& expected : {
ExpectedProfile{"pc", "pc-windows-11", DeviceClass::kPc,
"Windows 11 PC", false, true, true,
"Roblox/WinInet"},
ExpectedProfile{"mobile", "mobile-pixel-7", DeviceClass::kMobile,
"Google Pixel 7", true, false, false, nullptr},
ExpectedProfile{"console", "console-ps5", DeviceClass::kConsole,
"PlayStation 5", false, true, true,
"Roblox/XboxOne"},
}) {
SCOPED_TRACE(expected.configured);
const RuntimeConfig config = RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_DEVICE_PROFILE", expected.configured}}));
ASSERT_TRUE(config.device_profile_valid());
EXPECT_EQ(config.device_profile().name, expected.canonical);
EXPECT_EQ(config.device_profile().device_class, expected.device_class);
EXPECT_EQ(config.device_profile().display_name, expected.display_name);
EXPECT_EQ(config.input_capabilities().touch_enabled, expected.touch);
EXPECT_EQ(config.input_capabilities().mouse_enabled, expected.mouse);
EXPECT_EQ(config.input_capabilities().keyboard_enabled, expected.keyboard);
if (expected.user_agent == nullptr) {
EXPECT_FALSE(config.roblox_http_user_agent().has_value());
} else {
ASSERT_TRUE(config.roblox_http_user_agent().has_value());
EXPECT_EQ(*config.roblox_http_user_agent(), expected.user_agent);
}
}
}
TEST(RuntimeConfigTest, ExplicitPresetWinsOverDeprecatedPlayabilityVariable) {
const RuntimeConfig config = RuntimeConfig::FromEnvironment(MapEnvironment({
{"MOCKTAIL_DEVICE_PROFILE", "console-xbox-series-x"},
{"MOCKTAIL_DESKTOP_PLAYABILITY", "1"},
{"MOCKTAIL_TOUCH_MODE", "on"},
}));
EXPECT_EQ(config.device_profile().device_class, DeviceClass::kConsole);
EXPECT_EQ(config.device_profile().name, "console-ps5");
EXPECT_EQ(config.device_profile().display_name, "PlayStation 5");
EXPECT_TRUE(config.input_capabilities().touch_enabled);
EXPECT_FALSE(config.desktop_playability());
ASSERT_TRUE(config.roblox_http_user_agent().has_value());
EXPECT_EQ(*config.roblox_http_user_agent(), kRobloxConsoleAdmissionUserAgent);
}
TEST(RuntimeConfigTest, ResolvesDetailedIdentityAndInputOverrides) {
const RuntimeConfig config = RuntimeConfig::FromEnvironment(MapEnvironment({
{"MOCKTAIL_DEVICE_PROFILE", "mobile"},
{"MOCKTAIL_DEVICE_PLATFORM_NAME", "Android"},
{"MOCKTAIL_DEVICE_NAME", "Google Pixel 9 Pro"},
{"MOCKTAIL_DEVICE_MANUFACTURER", "Google"},
{"MOCKTAIL_DEVICE_MODEL", "Pixel 9 Pro"},
{"MOCKTAIL_DEVICE_BRAND", "google"},
{"MOCKTAIL_DEVICE_CODE", "komodo"},
{"MOCKTAIL_DEVICE_SKU", "komodo"},
{"MOCKTAIL_DEVICE_SOC_MODEL", "Google Tensor G4"},
{"MOCKTAIL_TOUCH_MODE", "on"},
{"MOCKTAIL_MOUSE_MODE", "on"},
{"MOCKTAIL_KEYBOARD_MODE", "off"},
}));
ASSERT_TRUE(config.device_profile_valid());
EXPECT_EQ(config.device_profile().name, "mobile-pixel-7");
EXPECT_EQ(config.device_profile().display_name, "Google Pixel 9 Pro");
EXPECT_EQ(config.device_profile().model, "Pixel 9 Pro");
EXPECT_EQ(config.device_profile().device_code, "komodo");
EXPECT_EQ(config.device_profile().soc_model, "Google Tensor G4");
EXPECT_NE(config.device_profile().cache_key, config.device_profile().name);
EXPECT_EQ(config.device_profile().cache_key,
BuildCustomDeviceProfileCacheKey(config.device_profile()));
EXPECT_TRUE(config.input_capabilities().touch_enabled);
EXPECT_TRUE(config.input_capabilities().mouse_enabled);
EXPECT_FALSE(config.input_capabilities().keyboard_enabled);
EXPECT_FALSE(config.roblox_http_user_agent().has_value());
}
TEST(RuntimeConfigTest, RejectsUnsafeDetailedIdentityFromEnvironment) {
const RuntimeConfig empty = RuntimeConfig::FromEnvironment(MapEnvironment({
{"MOCKTAIL_DEVICE_PROFILE", "pc"},
{"MOCKTAIL_DEVICE_MODEL", ""},
}));
EXPECT_FALSE(empty.device_profile_valid());
const RuntimeConfig control = RuntimeConfig::FromEnvironment(MapEnvironment({
{"MOCKTAIL_DEVICE_PROFILE", "pc"},
{"MOCKTAIL_DEVICE_MODEL", "Windows\nInjected"},
}));
EXPECT_FALSE(control.device_profile_valid());
}
TEST(RuntimeConfigTest, MarksUnknownDeviceProfileInvalid) {
const RuntimeConfig config = RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_DEVICE_PROFILE", "smart-fridge"}}));
EXPECT_FALSE(config.device_profile_valid());
EXPECT_EQ(config.device_profile().name, kDefaultDeviceProfileName);
}
TEST(RuntimeConfigTest, RetainsLegacyBooleanSemantics) {
const MapEnvironment false_text({{"MOCKTAIL_HEADLESS", "false"}});
const MapEnvironment zero({{"MOCKTAIL_HEADLESS", "0"}});
EXPECT_TRUE(RuntimeConfig::FromEnvironment(false_text).headless());
EXPECT_FALSE(RuntimeConfig::FromEnvironment(zero).headless());
}
TEST(RuntimeConfigTest, RejectsUnsafeWindowSizes) {
const MapEnvironment environment({
{"MOCKTAIL_WIN_WIDTH", "-1"},
{"MOCKTAIL_WIN_HEIGHT", "999999999999999999999"},
});
const RuntimeConfig config = RuntimeConfig::FromEnvironment(environment);
EXPECT_EQ(config.window().width, 1280);
EXPECT_EQ(config.window().height, 720);
}
TEST(RuntimeConfigTest, RejectsUnsafeAudioOutputDevice) {
const RuntimeConfig empty = RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_AUDIO_OUTPUT_DEVICE", ""}}));
EXPECT_TRUE(empty.audio_output_device_valid());
EXPECT_EQ(empty.audio_output_device(), "default");
const RuntimeConfig control = RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_AUDIO_OUTPUT_DEVICE", "Speaker\nInjected"}}));
EXPECT_FALSE(control.audio_output_device_valid());
}
TEST(RuntimeConfigTest, ValidatesAudioInputDeviceAndMicrophonePolicy) {
const RuntimeConfig empty = RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_AUDIO_INPUT_DEVICE", ""}}));
EXPECT_TRUE(empty.audio_input_device_valid());
EXPECT_EQ(empty.audio_input_device(), "default");
EXPECT_TRUE(empty.microphone_enabled());
const RuntimeConfig disabled = RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_AUDIO_INPUT_DEVICE", "disabled"}}));
EXPECT_TRUE(disabled.audio_input_device_valid());
EXPECT_FALSE(disabled.microphone_enabled());
const RuntimeConfig control = RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_AUDIO_INPUT_DEVICE", "Mic\nInjected"}}));
EXPECT_FALSE(control.audio_input_device_valid());
}
TEST(RuntimeConfigTest, PreservesUnknownBackendName) {
const MapEnvironment environment(
{{"MOCKTAIL_GRAPHICS_BACKEND", "future-backend"}});
const RuntimeConfig config = RuntimeConfig::FromEnvironment(environment);
EXPECT_EQ(config.graphics_backend(), GraphicsBackend::kUnknown);
EXPECT_EQ(config.graphics_backend_name(), "future-backend");
}
TEST(RuntimeConfigTest, RejectsEveryUnsafeDetachedThreadOverride) {
const std::vector<std::string> unsafe_overrides = {
"MOCKTAIL_APP_BRIDGE_APP_START_THREAD",
"MOCKTAIL_CALL_REAL_APP_BRIDGE_INIT_THREAD",
"MOCKTAIL_START_LUA_APP_DM_THREAD",
"MOCKTAIL_CALL_REAL_APP_BRIDGE_UPDATE_SURFACE_THREAD",
"MOCKTAIL_CALL_REAL_APP_BRIDGE_START_THREAD",
"MOCKTAIL_SEND_APP_READY_THREAD",
"MOCKTAIL_SEND_GAME_LOADED_THREAD",
};
for (const std::string& name : unsafe_overrides) {
SCOPED_TRACE(name);
const MapEnvironment environment({{name, "enabled"}});
const RuntimeConfig config = RuntimeConfig::FromEnvironment(environment);
ASSERT_TRUE(config.has_unsafe_detached_thread_overrides());
EXPECT_EQ(config.unsafe_detached_thread_overrides(),
std::vector<std::string>({name}));
}
}
TEST(RuntimeConfigTest, AllowsOnlyEmptyOrZeroDetachedThreadOverrides) {
const MapEnvironment environment({
{"MOCKTAIL_APP_BRIDGE_APP_START_THREAD", ""},
{"MOCKTAIL_CALL_REAL_APP_BRIDGE_INIT_THREAD", "0"},
{"MOCKTAIL_START_LUA_APP_DM_THREAD", "0"},
{"MOCKTAIL_CALL_REAL_APP_BRIDGE_UPDATE_SURFACE_THREAD", ""},
{"MOCKTAIL_CALL_REAL_APP_BRIDGE_START_THREAD", "0"},
{"MOCKTAIL_SEND_APP_READY_THREAD", ""},
{"MOCKTAIL_SEND_GAME_LOADED_THREAD", "0"},
});
const RuntimeConfig config = RuntimeConfig::FromEnvironment(environment);
EXPECT_FALSE(config.has_unsafe_detached_thread_overrides());
EXPECT_TRUE(config.unsafe_detached_thread_overrides().empty());
}
TEST(RuntimeConfigTest, ReportsUnsafeOverridesInStablePolicyOrder) {
const MapEnvironment environment({
{"MOCKTAIL_SEND_GAME_LOADED_THREAD", "1"},
{"MOCKTAIL_APP_BRIDGE_APP_START_THREAD", "false"},
{"MOCKTAIL_CALL_REAL_APP_BRIDGE_START_THREAD", "yes"},
});
const RuntimeConfig config = RuntimeConfig::FromEnvironment(environment);
EXPECT_EQ(config.unsafe_detached_thread_overrides(),
std::vector<std::string>({
"MOCKTAIL_APP_BRIDGE_APP_START_THREAD",
"MOCKTAIL_CALL_REAL_APP_BRIDGE_START_THREAD",
"MOCKTAIL_SEND_GAME_LOADED_THREAD",
}));
}
} // namespace
} // namespace runtime
} // namespace mocktail