mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-19 15:00:54 +02:00
Defaults images.small to the roblox_small asset on the bundled Discord application, with "Roblox" as its hover text.
1200 lines
46 KiB
C++
1200 lines
46 KiB
C++
// Modified by vii from komaruworld/mocktail. See README "About this fork".
|
|
#include "runtime/runtime_config_file.h"
|
|
|
|
#include <gtest/gtest.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <thread>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "runtime/runtime_config_bootstrap.h"
|
|
|
|
#ifndef MOCKTAIL_TEST_SOURCE_DIR
|
|
#error "MOCKTAIL_TEST_SOURCE_DIR must point at the Mocktail source tree"
|
|
#endif
|
|
|
|
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));
|
|
return found == values_.end() ? std::nullopt
|
|
: std::optional<std::string>(found->second);
|
|
}
|
|
|
|
private:
|
|
std::unordered_map<std::string, std::string> values_;
|
|
};
|
|
|
|
class TemporaryDirectory {
|
|
public:
|
|
TemporaryDirectory() {
|
|
char pattern[] = "/tmp/mocktail_runtime_config_XXXXXX";
|
|
char* created = mkdtemp(pattern);
|
|
if (created != nullptr) {
|
|
path_ = created;
|
|
}
|
|
}
|
|
|
|
~TemporaryDirectory() {
|
|
std::error_code error;
|
|
std::filesystem::remove_all(path_, error);
|
|
}
|
|
|
|
std::filesystem::path Write(std::string_view contents) const {
|
|
const std::filesystem::path file = path_ / "config.yaml";
|
|
std::ofstream output(file);
|
|
output << contents;
|
|
return file;
|
|
}
|
|
|
|
const std::filesystem::path& path() const { return path_; }
|
|
|
|
private:
|
|
std::filesystem::path path_;
|
|
};
|
|
|
|
std::string ReadFile(const std::filesystem::path& path) {
|
|
std::ifstream input(path, std::ios::binary);
|
|
return std::string(std::istreambuf_iterator<char>(input),
|
|
std::istreambuf_iterator<char>());
|
|
}
|
|
|
|
TEST(RuntimeConfigBootstrapTest, CreatesCompletePrivateFirstRunFile) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path file =
|
|
temporary.path() / "nested/mocktail/config.yaml";
|
|
|
|
const mode_t previous_umask = umask(0);
|
|
const RuntimeConfigBootstrapResult bootstrapped =
|
|
EnsureRuntimeConfigFile(file);
|
|
umask(previous_umask);
|
|
|
|
ASSERT_TRUE(bootstrapped) << bootstrapped.error;
|
|
EXPECT_TRUE(bootstrapped.created());
|
|
struct stat metadata = {};
|
|
ASSERT_EQ(lstat(file.c_str(), &metadata), 0);
|
|
EXPECT_TRUE(S_ISREG(metadata.st_mode));
|
|
EXPECT_EQ(metadata.st_mode & 0777, 0600);
|
|
ASSERT_EQ(lstat(file.parent_path().c_str(), &metadata), 0);
|
|
EXPECT_TRUE(S_ISDIR(metadata.st_mode));
|
|
EXPECT_EQ(metadata.st_mode & 0777, 0700);
|
|
EXPECT_EQ(ReadFile(file), DefaultRuntimeConfigYaml());
|
|
|
|
const RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(MapEnvironment(), file);
|
|
ASSERT_TRUE(loaded) << loaded.error;
|
|
EXPECT_TRUE(loaded.file_loaded);
|
|
EXPECT_EQ(loaded.config.graphics_backend(), GraphicsBackend::kVulkan);
|
|
EXPECT_EQ(loaded.config.theme_mode(), "roblox");
|
|
EXPECT_EQ(loaded.config.frame_rate().mode, FrameRateLimitMode::kUnmanaged);
|
|
EXPECT_EQ(loaded.config.vsync_mode(), "auto");
|
|
EXPECT_FALSE(loaded.config.performance().multithreaded_rendering);
|
|
EXPECT_EQ(loaded.config.performance().memory_limit_mb, 0U);
|
|
EXPECT_EQ(loaded.config.performance().game_mode, GameModePolicy::kAuto);
|
|
EXPECT_EQ(loaded.config.performance().physics_worker_mode,
|
|
PhysicsWorkerMode::kThroughput);
|
|
EXPECT_EQ(loaded.config.audio_output_device(), "default");
|
|
EXPECT_EQ(loaded.config.audio_input_device(), "default");
|
|
EXPECT_FALSE(loaded.config.input_capabilities().touch_enabled);
|
|
EXPECT_TRUE(loaded.config.desktop_playability());
|
|
EXPECT_EQ(loaded.config.device_profile().name, "pc-windows-11");
|
|
}
|
|
|
|
TEST(RuntimeConfigBootstrapTest,
|
|
MatchesShippedExampleAndDocumentsEveryDefault) {
|
|
const std::filesystem::path example =
|
|
std::filesystem::path(MOCKTAIL_TEST_SOURCE_DIR) /
|
|
"config/mocktail.example.yaml";
|
|
const std::string defaults(DefaultRuntimeConfigYaml());
|
|
|
|
EXPECT_EQ(ReadFile(example), defaults);
|
|
for (const std::string_view documented_setting : {
|
|
"# Integer: configuration schema version. Only version 1 is "
|
|
"supported.\nversion: 1",
|
|
"# Presets: pc-windows-11, mobile-pixel-7, "
|
|
"console-ps5.\n# Use the mapping below for a custom "
|
|
"profile.\ndevice: pc-windows-11",
|
|
"# Boolean (default: false): start without a visible SDL "
|
|
"window.\n headless: false",
|
|
"# String (default: roblox): use Roblox's saved account theme. "
|
|
"Supported\n # overrides: dark, light, or system (follow the "
|
|
"desktop color scheme).\n theme: roblox",
|
|
"# Recommended values: direct-vulkan, opengl, system, "
|
|
"angle-vulkan.\n backend: direct-vulkan",
|
|
"# DFIntTaskSchedulerTargetFps without a whitelist.\n "
|
|
"# frame_rate_limit: -1",
|
|
"# Optional presentation synchronization override: auto, on, or "
|
|
"off. auto\n # presents through the lowest-latency synchronized "
|
|
"mode the driver offers\n # unless frame_rate_limit is unlimited.\n"
|
|
" # vsync: off",
|
|
"# physical CPU core. A place's Lua/main thread can still remain "
|
|
"serial.\n multithreaded_rendering: false",
|
|
"# sizes and coalesces midphase work. Supported: auto, latency, "
|
|
"throughput.\n physics_worker_mode: throughput",
|
|
"# Integer MiB (default: 0): hard RAM cap for the Mocktail process "
|
|
"tree.\n # 0 disables the cap.",
|
|
"# String (default: auto): request Feral GameMode when its host "
|
|
"daemon and\n # client library are available. Supported values: "
|
|
"auto, on, off.\n gamemode: auto",
|
|
"# To pin output, copy an exact SDL device name printed during "
|
|
"startup.\n output_device: default",
|
|
"# between boots; prefer the exact device name when it is unique.\n"
|
|
" input_device: default",
|
|
"# Boolean (default: true): publish Nightcap activity to Discord "
|
|
"Desktop.\n # This never signs in to Discord and never reads an "
|
|
"account token.\n enabled: true",
|
|
"# Boolean (default: true): show the current Roblox experience "
|
|
"name.\n show_place_name: true",
|
|
"# Boolean (default: true): show how long the current session has "
|
|
"run.\n show_elapsed_time: true",
|
|
"# Boolean (default: true): let friends open the current "
|
|
"experience. When\n # Roblox provides a public server ID, the "
|
|
"button targets that server.\n enabled: true",
|
|
"# Boolean (default: true): never expose private or reserved "
|
|
"joins.\n public_servers_only: true",
|
|
"# playing: \"Playing {place_name}\"\n # state: \"by "
|
|
"{creator_name}\"",
|
|
"# title: Roblox",
|
|
"# large: \"{place_icon}\"\n # large_text: \"{place_name}\"\n"
|
|
" # small: roblox_small\n # small_text: Roblox",
|
|
"# Integer (default: 1280): initial window width in logical desktop "
|
|
"units.\n "
|
|
"width: 1280",
|
|
"# Integer (default: 720): initial window height in logical desktop "
|
|
"units.\n "
|
|
"height: 720",
|
|
"# String (default: Roblox): window title.\n title: Roblox",
|
|
"# Boolean (default: false): render at physical display-pixel density instead\n "
|
|
"# of the logical desktop resolution. Enable only for sharper high-DPI output.\n "
|
|
"high_dpi: false",
|
|
"# HostAbi profile, run two isolated canaries with the selected "
|
|
"graphics\n "
|
|
"# backend, and promote only on success. An existing current "
|
|
"payload is\n "
|
|
"# preserved when probation fails.\n "
|
|
"automatic: true",
|
|
"# String (default: apk-pure): direct x86_64 APK provider "
|
|
"selection. Both\n "
|
|
"# accepted values resolve the provider chain, which currently "
|
|
"starts with\n "
|
|
"# APKPure. Supported values: auto, apk-pure.\n "
|
|
"source: apk-pure",
|
|
"# Reserved for desktop update integrations. `mocktail_updater` "
|
|
"itself never\n # launches another process after changing the "
|
|
"active payload.\n launch_after_update: false",
|
|
}) {
|
|
EXPECT_NE(defaults.find(documented_setting), std::string::npos)
|
|
<< documented_setting;
|
|
}
|
|
EXPECT_EQ(defaults.find("testing_latest_only"), std::string::npos);
|
|
}
|
|
|
|
TEST(RuntimeConfigBootstrapTest, PreservesExistingRegularFile) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path regular = temporary.Write("user: settings\n");
|
|
RuntimeConfigBootstrapResult bootstrapped = EnsureRuntimeConfigFile(regular);
|
|
ASSERT_TRUE(bootstrapped) << bootstrapped.error;
|
|
EXPECT_FALSE(bootstrapped.created());
|
|
EXPECT_EQ(ReadFile(regular), "user: settings\n");
|
|
}
|
|
|
|
TEST(RuntimeConfigBootstrapTest, RejectsSymlinkAndNonRegularEntry) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path target = temporary.path() / "owned.yaml";
|
|
std::ofstream(target) << "owned: true\n";
|
|
const std::filesystem::path symlink = temporary.path() / "linked.yaml";
|
|
ASSERT_EQ(::symlink(target.c_str(), symlink.c_str()), 0);
|
|
|
|
RuntimeConfigBootstrapResult bootstrapped = EnsureRuntimeConfigFile(symlink);
|
|
EXPECT_FALSE(bootstrapped);
|
|
EXPECT_NE(bootstrapped.error.find("symlink"), std::string::npos);
|
|
EXPECT_TRUE(
|
|
std::filesystem::is_symlink(std::filesystem::symlink_status(symlink)));
|
|
EXPECT_EQ(ReadFile(target), "owned: true\n");
|
|
|
|
const std::filesystem::path directory = temporary.path() / "directory.yaml";
|
|
ASSERT_TRUE(std::filesystem::create_directory(directory));
|
|
bootstrapped = EnsureRuntimeConfigFile(directory);
|
|
EXPECT_FALSE(bootstrapped);
|
|
EXPECT_NE(bootstrapped.error.find("regular file"), std::string::npos);
|
|
EXPECT_TRUE(std::filesystem::is_directory(directory));
|
|
}
|
|
|
|
TEST(RuntimeConfigBootstrapTest, PublishesOnlyOneWinnerDuringFirstRunRace) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path file = temporary.path() / "race/config.yaml";
|
|
std::vector<RuntimeConfigBootstrapResult> results(12);
|
|
std::vector<std::thread> workers;
|
|
workers.reserve(results.size());
|
|
for (std::size_t index = 0; index < results.size(); ++index) {
|
|
workers.emplace_back([&file, &results, index]() {
|
|
results[index] = EnsureRuntimeConfigFile(file);
|
|
});
|
|
}
|
|
for (std::thread& worker : workers) {
|
|
worker.join();
|
|
}
|
|
|
|
std::size_t created = 0;
|
|
for (const RuntimeConfigBootstrapResult& result : results) {
|
|
ASSERT_TRUE(result) << result.error;
|
|
created += result.created() ? 1 : 0;
|
|
}
|
|
EXPECT_EQ(created, 1);
|
|
EXPECT_EQ(ReadFile(file), DefaultRuntimeConfigYaml());
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, UsesDefaultsWhenFileDoesNotExist) {
|
|
const MapEnvironment environment;
|
|
const RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(environment, "/does/not/exist/config.yaml");
|
|
|
|
ASSERT_TRUE(loaded) << loaded.error;
|
|
EXPECT_FALSE(loaded.file_loaded);
|
|
EXPECT_EQ(loaded.config.graphics_backend(), GraphicsBackend::kVulkan);
|
|
EXPECT_EQ(loaded.config.theme_mode(), "roblox");
|
|
EXPECT_EQ(loaded.config.roblox_library_path(), "rbx_bin/libroblox.so");
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, RejectsSymlinkAndOversizedConfiguration) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path target = temporary.Write("version: 1\n");
|
|
const std::filesystem::path symlink = temporary.path() / "linked.yaml";
|
|
ASSERT_EQ(::symlink(target.c_str(), symlink.c_str()), 0);
|
|
|
|
RuntimeConfigLoadResult loaded = LoadRuntimeConfig(MapEnvironment(), symlink);
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("cannot open"), std::string::npos);
|
|
|
|
const std::filesystem::path oversized = temporary.path() / "large.yaml";
|
|
std::ofstream output(oversized, std::ios::binary);
|
|
output.seekp(1024 * 1024);
|
|
output.put('\n');
|
|
output.close();
|
|
loaded = LoadRuntimeConfig(MapEnvironment(), oversized);
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("no larger than 1 MiB"), std::string::npos);
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, RawMouseDefaultsOffAndLoadsFromYaml) {
|
|
TemporaryDirectory defaulted_directory;
|
|
const RuntimeConfigLoadResult defaulted = LoadRuntimeConfig(
|
|
MapEnvironment(), defaulted_directory.Write(R"yaml(
|
|
version: 1
|
|
input:
|
|
touch_enabled: false
|
|
)yaml"));
|
|
ASSERT_TRUE(defaulted) << defaulted.error;
|
|
EXPECT_FALSE(defaulted.config.input_capabilities().raw_mouse);
|
|
|
|
TemporaryDirectory enabled_directory;
|
|
const RuntimeConfigLoadResult enabled = LoadRuntimeConfig(
|
|
MapEnvironment(), enabled_directory.Write(R"yaml(
|
|
version: 1
|
|
input:
|
|
raw_mouse: true
|
|
)yaml"));
|
|
ASSERT_TRUE(enabled) << enabled.error;
|
|
EXPECT_TRUE(enabled.config.input_capabilities().raw_mouse);
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, LoadsTypedDesktopAndGraphicsSettings) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path file = temporary.Write(R"yaml(
|
|
version: 1
|
|
runtime:
|
|
headless: false
|
|
roblox_library: /payload/libroblox.so
|
|
appearance:
|
|
theme: dark
|
|
graphics:
|
|
backend: vulkan
|
|
frame_rate_limit: unlimited
|
|
vsync: off
|
|
performance:
|
|
multithreaded_rendering: true
|
|
physics_worker_mode: latency
|
|
memory_limit_mb: 6144
|
|
gamemode: off
|
|
audio:
|
|
output_device: Built-in Audio Analog Stereo
|
|
window:
|
|
width: 1920
|
|
height: 1080
|
|
title: Mocktail Desktop
|
|
high_dpi: true
|
|
input:
|
|
touch_enabled: false
|
|
network:
|
|
proxy_host: proxy.example.test
|
|
proxy_port: 3128
|
|
integrations:
|
|
discord_rpc:
|
|
enabled: true
|
|
show_place_name: false
|
|
show_elapsed_time: false
|
|
application_id: 123456789012345678
|
|
join:
|
|
enabled: true
|
|
public_servers_only: true
|
|
button_label: Play Together
|
|
text:
|
|
browsing: Looking for games
|
|
joining: Connecting
|
|
playing: "Inside {place_name}"
|
|
state: Playing Roblox on Linux
|
|
unknown_place: Unknown world
|
|
title: "{place_name}"
|
|
images:
|
|
large: ""
|
|
small: nightcap_logo
|
|
small_text: On Linux
|
|
updates:
|
|
automatic: true
|
|
)yaml");
|
|
|
|
const MapEnvironment environment;
|
|
const RuntimeConfigLoadResult loaded = LoadRuntimeConfig(environment, file);
|
|
|
|
ASSERT_TRUE(loaded) << loaded.error;
|
|
EXPECT_TRUE(loaded.file_loaded);
|
|
EXPECT_EQ(loaded.config.roblox_library_path(), "/payload/libroblox.so");
|
|
EXPECT_EQ(loaded.config.graphics_backend(), GraphicsBackend::kVulkan);
|
|
EXPECT_EQ(loaded.config.frame_rate().mode, FrameRateLimitMode::kUnlimited);
|
|
EXPECT_EQ(loaded.config.vsync_mode(), "off");
|
|
EXPECT_EQ(loaded.config.theme_mode(), "dark");
|
|
EXPECT_TRUE(loaded.config.performance().multithreaded_rendering);
|
|
EXPECT_EQ(loaded.config.performance().physics_worker_mode,
|
|
PhysicsWorkerMode::kLatency);
|
|
EXPECT_GT(loaded.config.performance().physical_core_count, 0);
|
|
EXPECT_EQ(loaded.config.performance().memory_limit_mb, 6144U);
|
|
EXPECT_EQ(loaded.config.performance().game_mode, GameModePolicy::kOff);
|
|
EXPECT_EQ(loaded.config.audio_output_device(),
|
|
"Built-in Audio Analog Stereo");
|
|
EXPECT_EQ(loaded.config.window().width, 1920);
|
|
EXPECT_EQ(loaded.config.window().height, 1080);
|
|
EXPECT_EQ(loaded.config.window().title, "Mocktail Desktop");
|
|
EXPECT_TRUE(loaded.config.window().high_dpi);
|
|
EXPECT_FALSE(loaded.config.input_capabilities().touch_enabled);
|
|
EXPECT_TRUE(loaded.config.desktop_playability());
|
|
ASSERT_TRUE(loaded.config.roblox_http_user_agent().has_value());
|
|
EXPECT_EQ(*loaded.config.roblox_http_user_agent(),
|
|
kRobloxDesktopHttpUserAgent);
|
|
ASSERT_TRUE(loaded.config.network_proxy().has_value());
|
|
EXPECT_EQ(loaded.config.network_proxy()->host, "proxy.example.test");
|
|
EXPECT_EQ(loaded.config.network_proxy()->port, 3128);
|
|
EXPECT_TRUE(loaded.config.discord_rpc().enabled);
|
|
EXPECT_FALSE(loaded.config.discord_rpc().show_place_name);
|
|
EXPECT_FALSE(loaded.config.discord_rpc().show_elapsed_time);
|
|
EXPECT_EQ(loaded.config.discord_rpc().application_id,
|
|
"123456789012345678");
|
|
EXPECT_TRUE(loaded.config.discord_rpc().join_enabled);
|
|
EXPECT_TRUE(loaded.config.discord_rpc().public_servers_only);
|
|
EXPECT_EQ(loaded.config.discord_rpc().join_button_label, "Play Together");
|
|
EXPECT_EQ(loaded.config.discord_rpc().text.browsing, "Looking for games");
|
|
EXPECT_EQ(loaded.config.discord_rpc().text.joining, "Connecting");
|
|
EXPECT_EQ(loaded.config.discord_rpc().text.playing,
|
|
"Inside {place_name}");
|
|
EXPECT_EQ(loaded.config.discord_rpc().text.state,
|
|
"Playing Roblox on Linux");
|
|
EXPECT_EQ(loaded.config.discord_rpc().text.unknown_place, "Unknown world");
|
|
EXPECT_EQ(loaded.config.discord_rpc().text.title, "{place_name}");
|
|
EXPECT_TRUE(loaded.config.discord_rpc().images.large.empty());
|
|
EXPECT_EQ(loaded.config.discord_rpc().images.large_text, "{place_name}");
|
|
EXPECT_EQ(loaded.config.discord_rpc().images.small, "nightcap_logo");
|
|
EXPECT_EQ(loaded.config.discord_rpc().images.small_text, "On Linux");
|
|
EXPECT_TRUE(loaded.config.discord_rpc_valid());
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, RejectsControlBytesInDiscordPresenceFields) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path file = temporary.Write(R"yaml(
|
|
version: 1
|
|
integrations:
|
|
discord_rpc:
|
|
images:
|
|
small_text: "bad\x01text"
|
|
)yaml");
|
|
|
|
const RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(MapEnvironment(), file);
|
|
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("integrations.discord_rpc.images.small_text"),
|
|
std::string::npos);
|
|
EXPECT_NE(loaded.error.find("control bytes"), std::string::npos)
|
|
<< loaded.error;
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, EmptyDiscordStateLoadsAsHidden) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path file = temporary.Write(R"yaml(
|
|
version: 1
|
|
integrations:
|
|
discord_rpc:
|
|
text:
|
|
state: ""
|
|
)yaml");
|
|
|
|
const RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(MapEnvironment(), file);
|
|
|
|
ASSERT_TRUE(loaded) << loaded.error;
|
|
EXPECT_TRUE(loaded.config.discord_rpc().text.state.empty());
|
|
EXPECT_TRUE(loaded.config.discord_rpc_valid());
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, ExportsDiscordPresenceFieldsIncludingEmptyValues) {
|
|
std::string error;
|
|
const RuntimeConfig configured = RuntimeConfig::FromEnvironment(
|
|
MapEnvironment({{"MOCKTAIL_DISCORD_RPC_TEXT_TITLE", "Nightcap"},
|
|
{"MOCKTAIL_DISCORD_RPC_TEXT_STATE", ""},
|
|
{"MOCKTAIL_DISCORD_RPC_IMAGE_LARGE", ""},
|
|
{"MOCKTAIL_DISCORD_RPC_IMAGE_SMALL", "linux"}}));
|
|
ASSERT_TRUE(ExportRuntimeConfigEnvironment(configured, &error)) << error;
|
|
|
|
const RuntimeConfig exported =
|
|
RuntimeConfig::FromEnvironment(ProcessEnvironment());
|
|
EXPECT_EQ(exported.discord_rpc().text.title, "Nightcap");
|
|
EXPECT_TRUE(exported.discord_rpc().text.state.empty());
|
|
EXPECT_TRUE(exported.discord_rpc_valid());
|
|
EXPECT_TRUE(exported.discord_rpc().images.large.empty());
|
|
EXPECT_EQ(exported.discord_rpc().images.large_text, "{place_name}");
|
|
EXPECT_EQ(exported.discord_rpc().images.small, "linux");
|
|
EXPECT_EQ(exported.discord_rpc().images.small_text, "Roblox");
|
|
for (const char* variable :
|
|
{"MOCKTAIL_DISCORD_RPC_TEXT_TITLE", "MOCKTAIL_DISCORD_RPC_TEXT_STATE",
|
|
"MOCKTAIL_DISCORD_RPC_IMAGE_LARGE",
|
|
"MOCKTAIL_DISCORD_RPC_IMAGE_LARGE_TEXT",
|
|
"MOCKTAIL_DISCORD_RPC_IMAGE_SMALL",
|
|
"MOCKTAIL_DISCORD_RPC_IMAGE_SMALL_TEXT"}) {
|
|
unsetenv(variable);
|
|
}
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, EnvironmentOverridesYaml) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path file = temporary.Write(R"yaml(
|
|
version: 1
|
|
graphics:
|
|
backend: system
|
|
frame_rate_limit: 60
|
|
vsync: on
|
|
performance:
|
|
multithreaded_rendering: true
|
|
physics_worker_mode: latency
|
|
memory_limit_mb: 4096
|
|
gamemode: on
|
|
audio:
|
|
output_device: YAML Speakers
|
|
input_device: YAML Microphone
|
|
input:
|
|
touch_enabled: true
|
|
compatibility:
|
|
desktop_playability: false
|
|
window:
|
|
high_dpi: true
|
|
network:
|
|
proxy_host: yaml-proxy.example.test
|
|
proxy_port: 8080
|
|
)yaml");
|
|
const MapEnvironment environment({
|
|
{"MOCKTAIL_GRAPHICS_BACKEND", "vulkan"},
|
|
{"MOCKTAIL_FRAME_RATE_LIMIT", "144"},
|
|
{"MOCKTAIL_VSYNC", "off"},
|
|
{"MOCKTAIL_MULTITHREADED_RENDERING", "0"},
|
|
{"MOCKTAIL_PHYSICS_WORKER_MODE", "auto"},
|
|
{"MOCKTAIL_MEMORY_LIMIT_MB", "8192"},
|
|
{"MOCKTAIL_GAMEMODE", "off"},
|
|
{"MOCKTAIL_AUDIO_OUTPUT_DEVICE", "Environment Headset"},
|
|
{"MOCKTAIL_AUDIO_INPUT_DEVICE", "Environment Microphone"},
|
|
{"MOCKTAIL_TOUCH_MODE", "off"},
|
|
{"MOCKTAIL_DESKTOP_PLAYABILITY", "1"},
|
|
{"MOCKTAIL_WIN_HIGH_DPI", "0"},
|
|
{"MOCKTAIL_HTTP_PROXY_HOST", "env-proxy.example.test"},
|
|
{"MOCKTAIL_HTTP_PROXY_PORT", "1080"},
|
|
});
|
|
|
|
const RuntimeConfigLoadResult loaded = LoadRuntimeConfig(environment, file);
|
|
|
|
ASSERT_TRUE(loaded) << loaded.error;
|
|
EXPECT_EQ(loaded.config.graphics_backend(), GraphicsBackend::kVulkan);
|
|
EXPECT_EQ(loaded.config.frame_rate().fixed_fps, 144);
|
|
EXPECT_EQ(loaded.config.vsync_mode(), "off");
|
|
EXPECT_FALSE(loaded.config.performance().multithreaded_rendering);
|
|
EXPECT_EQ(loaded.config.performance().physics_worker_mode,
|
|
PhysicsWorkerMode::kAuto);
|
|
EXPECT_EQ(loaded.config.performance().memory_limit_mb, 8192U);
|
|
EXPECT_EQ(loaded.config.performance().game_mode, GameModePolicy::kOff);
|
|
EXPECT_EQ(loaded.config.audio_output_device(), "Environment Headset");
|
|
EXPECT_EQ(loaded.config.audio_input_device(), "Environment Microphone");
|
|
EXPECT_FALSE(loaded.config.input_capabilities().touch_enabled);
|
|
EXPECT_FALSE(loaded.config.window().high_dpi);
|
|
EXPECT_TRUE(loaded.config.desktop_playability());
|
|
ASSERT_TRUE(loaded.config.roblox_http_user_agent().has_value());
|
|
EXPECT_EQ(*loaded.config.roblox_http_user_agent(),
|
|
kRobloxDesktopHttpUserAgent);
|
|
ASSERT_TRUE(loaded.config.network_proxy().has_value());
|
|
EXPECT_EQ(loaded.config.network_proxy()->host, "env-proxy.example.test");
|
|
EXPECT_EQ(loaded.config.network_proxy()->port, 1080);
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, RejectsUnsafeAudioOutputDevice) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path empty = temporary.Write(R"yaml(
|
|
version: 1
|
|
audio:
|
|
output_device: ""
|
|
)yaml");
|
|
RuntimeConfigLoadResult loaded = LoadRuntimeConfig(MapEnvironment(), empty);
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("audio.output_device"), std::string::npos);
|
|
|
|
const RuntimeConfig from_environment = RuntimeConfig::FromEnvironment(
|
|
MapEnvironment({{"MOCKTAIL_AUDIO_OUTPUT_DEVICE", "Speaker\nInjected"}}));
|
|
EXPECT_FALSE(from_environment.audio_output_device_valid());
|
|
std::string error;
|
|
EXPECT_FALSE(ExportRuntimeConfigEnvironment(from_environment, &error));
|
|
EXPECT_NE(error.find("invalid audio output device"), std::string::npos);
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, RejectsInvalidHighDpiPolicy) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path file = temporary.Write(R"yaml(
|
|
version: 1
|
|
window:
|
|
high_dpi: yes
|
|
)yaml");
|
|
|
|
const RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(MapEnvironment(), file);
|
|
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("window.high_dpi"), std::string::npos);
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, RejectsUnsafeAudioInputDevice) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path empty = temporary.Write(R"yaml(
|
|
version: 1
|
|
audio:
|
|
input_device: ""
|
|
)yaml");
|
|
RuntimeConfigLoadResult loaded = LoadRuntimeConfig(MapEnvironment(), empty);
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("audio.input_device"), std::string::npos);
|
|
|
|
const RuntimeConfig from_environment = RuntimeConfig::FromEnvironment(
|
|
MapEnvironment({{"MOCKTAIL_AUDIO_INPUT_DEVICE", "Mic\nInjected"}}));
|
|
EXPECT_FALSE(from_environment.audio_input_device_valid());
|
|
std::string error;
|
|
EXPECT_FALSE(ExportRuntimeConfigEnvironment(from_environment, &error));
|
|
EXPECT_NE(error.find("invalid audio input device"), std::string::npos);
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, RejectsIncompleteOrInvalidNetworkProxy) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path incomplete = temporary.Write(R"yaml(
|
|
version: 1
|
|
network:
|
|
proxy_host: proxy.example.test
|
|
)yaml");
|
|
RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(MapEnvironment(), incomplete);
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("configured together"), std::string::npos);
|
|
|
|
const std::filesystem::path scheme = temporary.Write(R"yaml(
|
|
version: 1
|
|
network:
|
|
proxy_host: http://proxy.example.test
|
|
proxy_port: 8080
|
|
)yaml");
|
|
loaded = LoadRuntimeConfig(MapEnvironment(), scheme);
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("without a scheme"), std::string::npos);
|
|
|
|
const std::filesystem::path path = temporary.Write(R"yaml(
|
|
version: 1
|
|
network:
|
|
proxy_host: proxy.example.test/path
|
|
proxy_port: 8080
|
|
)yaml");
|
|
loaded = LoadRuntimeConfig(MapEnvironment(), path);
|
|
EXPECT_FALSE(loaded);
|
|
|
|
const std::filesystem::path port = temporary.Write(R"yaml(
|
|
version: 1
|
|
network:
|
|
proxy_host: proxy.example.test
|
|
proxy_port: 65536
|
|
)yaml");
|
|
loaded = LoadRuntimeConfig(MapEnvironment(), port);
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("1 to 65535"), std::string::npos);
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, LoadsSystemProxyFlagWithoutFixedEndpoint) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path path = temporary.Write(R"yaml(
|
|
version: 1
|
|
network:
|
|
use_system_proxy: true
|
|
)yaml");
|
|
const RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(MapEnvironment(), path);
|
|
ASSERT_TRUE(loaded) << loaded.error;
|
|
EXPECT_TRUE(loaded.config.use_system_proxy());
|
|
EXPECT_FALSE(loaded.config.network_proxy().has_value());
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, ExportsProxyVariablesOnlyWhenConfigured) {
|
|
unsetenv("MOCKTAIL_HTTP_PROXY_HOST");
|
|
unsetenv("MOCKTAIL_HTTP_PROXY_PORT");
|
|
unsetenv("MOCKTAIL_HTTP_PROXY_SCHEME");
|
|
unsetenv("MOCKTAIL_NATIVE_SET_HTTP_CLIENT_PROXY");
|
|
std::string error;
|
|
ASSERT_TRUE(ExportRuntimeConfigEnvironment(
|
|
RuntimeConfig::FromEnvironment(MapEnvironment()), &error))
|
|
<< error;
|
|
EXPECT_EQ(getenv("MOCKTAIL_HTTP_PROXY_HOST"), nullptr);
|
|
EXPECT_EQ(getenv("MOCKTAIL_HTTP_PROXY_PORT"), nullptr);
|
|
EXPECT_EQ(getenv("MOCKTAIL_NATIVE_SET_HTTP_CLIENT_PROXY"), nullptr);
|
|
|
|
const RuntimeConfig configured = RuntimeConfig::FromEnvironment(
|
|
MapEnvironment({{"MOCKTAIL_HTTP_PROXY_HOST", "127.0.0.1"},
|
|
{"MOCKTAIL_HTTP_PROXY_PORT", "7890"}}));
|
|
ASSERT_TRUE(ExportRuntimeConfigEnvironment(configured, &error)) << error;
|
|
ASSERT_NE(getenv("MOCKTAIL_HTTP_PROXY_HOST"), nullptr);
|
|
ASSERT_NE(getenv("MOCKTAIL_HTTP_PROXY_PORT"), nullptr);
|
|
ASSERT_NE(getenv("MOCKTAIL_HTTP_PROXY_SCHEME"), nullptr);
|
|
ASSERT_NE(getenv("MOCKTAIL_NATIVE_SET_HTTP_CLIENT_PROXY"), nullptr);
|
|
EXPECT_STREQ(getenv("MOCKTAIL_HTTP_PROXY_HOST"), "127.0.0.1");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_HTTP_PROXY_PORT"), "7890");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_HTTP_PROXY_SCHEME"), "http");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_NATIVE_SET_HTTP_CLIENT_PROXY"), "1");
|
|
unsetenv("MOCKTAIL_HTTP_PROXY_HOST");
|
|
unsetenv("MOCKTAIL_HTTP_PROXY_PORT");
|
|
unsetenv("MOCKTAIL_HTTP_PROXY_SCHEME");
|
|
unsetenv("MOCKTAIL_NATIVE_SET_HTTP_CLIENT_PROXY");
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, ExportsMultithreadedRenderingPolicy) {
|
|
unsetenv("MOCKTAIL_MULTITHREADED_RENDERING");
|
|
std::string error;
|
|
const RuntimeConfig enabled = RuntimeConfig::FromEnvironment(
|
|
MapEnvironment({{"MOCKTAIL_MULTITHREADED_RENDERING", "1"}}));
|
|
ASSERT_TRUE(ExportRuntimeConfigEnvironment(enabled, &error)) << error;
|
|
ASSERT_NE(getenv("MOCKTAIL_MULTITHREADED_RENDERING"), nullptr);
|
|
EXPECT_STREQ(getenv("MOCKTAIL_MULTITHREADED_RENDERING"), "1");
|
|
|
|
const RuntimeConfig disabled =
|
|
RuntimeConfig::FromEnvironment(MapEnvironment());
|
|
ASSERT_TRUE(ExportRuntimeConfigEnvironment(disabled, &error)) << error;
|
|
EXPECT_STREQ(getenv("MOCKTAIL_MULTITHREADED_RENDERING"), "0");
|
|
unsetenv("MOCKTAIL_MULTITHREADED_RENDERING");
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, ExportsHighDpiWindowPolicy) {
|
|
unsetenv("MOCKTAIL_WIN_HIGH_DPI");
|
|
std::string error;
|
|
const RuntimeConfig default_config =
|
|
RuntimeConfig::FromEnvironment(MapEnvironment());
|
|
ASSERT_TRUE(ExportRuntimeConfigEnvironment(default_config, &error)) << error;
|
|
ASSERT_NE(getenv("MOCKTAIL_WIN_HIGH_DPI"), nullptr);
|
|
EXPECT_STREQ(getenv("MOCKTAIL_WIN_HIGH_DPI"), "0");
|
|
|
|
const RuntimeConfig high_dpi = RuntimeConfig::FromEnvironment(
|
|
MapEnvironment({{"MOCKTAIL_WIN_HIGH_DPI", "1"}}));
|
|
ASSERT_TRUE(ExportRuntimeConfigEnvironment(high_dpi, &error)) << error;
|
|
EXPECT_STREQ(getenv("MOCKTAIL_WIN_HIGH_DPI"), "1");
|
|
unsetenv("MOCKTAIL_WIN_HIGH_DPI");
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, ExportsPhysicsWorkerMode) {
|
|
unsetenv("MOCKTAIL_PHYSICS_WORKER_MODE");
|
|
std::string error;
|
|
const RuntimeConfig latency = RuntimeConfig::FromEnvironment(
|
|
MapEnvironment({{"MOCKTAIL_PHYSICS_WORKER_MODE", "latency"}}));
|
|
ASSERT_TRUE(ExportRuntimeConfigEnvironment(latency, &error)) << error;
|
|
ASSERT_NE(getenv("MOCKTAIL_PHYSICS_WORKER_MODE"), nullptr);
|
|
EXPECT_STREQ(getenv("MOCKTAIL_PHYSICS_WORKER_MODE"), "latency");
|
|
|
|
const RuntimeConfig default_config =
|
|
RuntimeConfig::FromEnvironment(MapEnvironment());
|
|
ASSERT_TRUE(ExportRuntimeConfigEnvironment(default_config, &error)) << error;
|
|
EXPECT_STREQ(getenv("MOCKTAIL_PHYSICS_WORKER_MODE"), "throughput");
|
|
unsetenv("MOCKTAIL_PHYSICS_WORKER_MODE");
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, ValidatesAndExportsMemoryLimitPolicy) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path negative = temporary.Write(R"yaml(
|
|
version: 1
|
|
performance:
|
|
memory_limit_mb: -1
|
|
)yaml");
|
|
RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(MapEnvironment(), negative);
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("memory_limit_mb"), std::string::npos);
|
|
|
|
loaded = LoadRuntimeConfig(
|
|
MapEnvironment({{"MOCKTAIL_MEMORY_LIMIT_MB", "not-a-number"}}),
|
|
"/does/not/exist/config.yaml");
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("memory-limit policy"), std::string::npos);
|
|
|
|
unsetenv("MOCKTAIL_MEMORY_LIMIT_MB");
|
|
std::string error;
|
|
const RuntimeConfig configured = RuntimeConfig::FromEnvironment(
|
|
MapEnvironment({{"MOCKTAIL_MEMORY_LIMIT_MB", "6144"}}));
|
|
ASSERT_TRUE(ExportRuntimeConfigEnvironment(configured, &error)) << error;
|
|
ASSERT_NE(getenv("MOCKTAIL_MEMORY_LIMIT_MB"), nullptr);
|
|
EXPECT_STREQ(getenv("MOCKTAIL_MEMORY_LIMIT_MB"), "6144");
|
|
unsetenv("MOCKTAIL_MEMORY_LIMIT_MB");
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, ValidatesAndExportsGameModePolicy) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path invalid = temporary.Write(R"yaml(
|
|
version: 1
|
|
performance:
|
|
gamemode: required
|
|
)yaml");
|
|
RuntimeConfigLoadResult loaded = LoadRuntimeConfig(MapEnvironment(), invalid);
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("performance.gamemode"), std::string::npos);
|
|
|
|
loaded = LoadRuntimeConfig(MapEnvironment({{"MOCKTAIL_GAMEMODE", "invalid"}}),
|
|
"/does/not/exist/config.yaml");
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("GameMode policy"), std::string::npos);
|
|
|
|
unsetenv("MOCKTAIL_GAMEMODE");
|
|
std::string error;
|
|
const RuntimeConfig configured = RuntimeConfig::FromEnvironment(
|
|
MapEnvironment({{"MOCKTAIL_GAMEMODE", "on"}}));
|
|
ASSERT_TRUE(ExportRuntimeConfigEnvironment(configured, &error)) << error;
|
|
ASSERT_NE(getenv("MOCKTAIL_GAMEMODE"), nullptr);
|
|
EXPECT_STREQ(getenv("MOCKTAIL_GAMEMODE"), "on");
|
|
unsetenv("MOCKTAIL_GAMEMODE");
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, ExportsAudioOutputDevice) {
|
|
unsetenv("MOCKTAIL_AUDIO_OUTPUT_DEVICE");
|
|
std::string error;
|
|
const RuntimeConfig configured = RuntimeConfig::FromEnvironment(
|
|
MapEnvironment({{"MOCKTAIL_AUDIO_OUTPUT_DEVICE", "USB Headset"}}));
|
|
ASSERT_TRUE(ExportRuntimeConfigEnvironment(configured, &error)) << error;
|
|
ASSERT_NE(getenv("MOCKTAIL_AUDIO_OUTPUT_DEVICE"), nullptr);
|
|
EXPECT_STREQ(getenv("MOCKTAIL_AUDIO_OUTPUT_DEVICE"), "USB Headset");
|
|
unsetenv("MOCKTAIL_AUDIO_OUTPUT_DEVICE");
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, ExportsAudioInputDevice) {
|
|
unsetenv("MOCKTAIL_AUDIO_INPUT_DEVICE");
|
|
std::string error;
|
|
const RuntimeConfig configured = RuntimeConfig::FromEnvironment(
|
|
MapEnvironment({{"MOCKTAIL_AUDIO_INPUT_DEVICE", "USB Microphone"}}));
|
|
ASSERT_TRUE(ExportRuntimeConfigEnvironment(configured, &error)) << error;
|
|
ASSERT_NE(getenv("MOCKTAIL_AUDIO_INPUT_DEVICE"), nullptr);
|
|
EXPECT_STREQ(getenv("MOCKTAIL_AUDIO_INPUT_DEVICE"), "USB Microphone");
|
|
unsetenv("MOCKTAIL_AUDIO_INPUT_DEVICE");
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, ExportsDesktopPlayabilityUserAgent) {
|
|
unsetenv("MOCKTAIL_USER_AGENT");
|
|
unsetenv("MOCKTAIL_DESKTOP_PLAYABILITY");
|
|
unsetenv("MOCKTAIL_DEVICE_PROFILE");
|
|
std::string error;
|
|
const RuntimeConfig desktop =
|
|
RuntimeConfig::FromEnvironment(MapEnvironment());
|
|
|
|
ASSERT_TRUE(ExportRuntimeConfigEnvironment(desktop, &error)) << error;
|
|
ASSERT_NE(getenv("MOCKTAIL_DESKTOP_PLAYABILITY"), nullptr);
|
|
EXPECT_STREQ(getenv("MOCKTAIL_DESKTOP_PLAYABILITY"), "1");
|
|
ASSERT_NE(getenv("MOCKTAIL_DEVICE_PROFILE"), nullptr);
|
|
EXPECT_STREQ(getenv("MOCKTAIL_DEVICE_PROFILE"), "pc-windows-11");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_DEVICE_CLASS"), "pc");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_DEVICE_PLATFORM_NAME"), "Windows");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_DEVICE_NAME"), "Windows 11 PC");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_DEVICE_MANUFACTURER"), "Microsoft");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_DEVICE_MODEL"), "Windows 11 PC");
|
|
ASSERT_NE(getenv("MOCKTAIL_USER_AGENT"), nullptr);
|
|
EXPECT_STREQ(getenv("MOCKTAIL_USER_AGENT"),
|
|
kRobloxDesktopHttpUserAgent.data());
|
|
unsetenv("MOCKTAIL_USER_AGENT");
|
|
unsetenv("MOCKTAIL_DESKTOP_PLAYABILITY");
|
|
unsetenv("MOCKTAIL_DEVICE_PROFILE");
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, LoadsOneLineDevicePresetsAndAliases) {
|
|
struct ExpectedProfile {
|
|
const char* configured;
|
|
const char* canonical;
|
|
DeviceClass device_class;
|
|
const char* model;
|
|
bool touch;
|
|
const char* user_agent;
|
|
};
|
|
for (const ExpectedProfile& expected : {
|
|
ExpectedProfile{"pc-windows-11", "pc-windows-11", DeviceClass::kPc,
|
|
"Windows 11 PC", false, "Roblox/WinInet"},
|
|
ExpectedProfile{"mobile", "mobile-pixel-7", DeviceClass::kMobile,
|
|
"Google Pixel 7", true, nullptr},
|
|
ExpectedProfile{"console", "console-ps5", DeviceClass::kConsole,
|
|
"PlayStation 5", false, "Roblox/XboxOne"},
|
|
}) {
|
|
SCOPED_TRACE(expected.configured);
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path file = temporary.Write(
|
|
"version: 1\ndevice: " + std::string(expected.configured) + "\n");
|
|
|
|
const RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(MapEnvironment(), file);
|
|
|
|
ASSERT_TRUE(loaded) << loaded.error;
|
|
EXPECT_EQ(loaded.config.device_profile().name, expected.canonical);
|
|
EXPECT_EQ(loaded.config.device_profile().device_class,
|
|
expected.device_class);
|
|
EXPECT_EQ(loaded.config.device_profile().display_name, expected.model);
|
|
EXPECT_EQ(loaded.config.input_capabilities().touch_enabled, expected.touch);
|
|
if (expected.user_agent == nullptr) {
|
|
EXPECT_FALSE(loaded.config.roblox_http_user_agent().has_value());
|
|
} else {
|
|
ASSERT_TRUE(loaded.config.roblox_http_user_agent().has_value());
|
|
EXPECT_EQ(*loaded.config.roblox_http_user_agent(), expected.user_agent);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, LoadsDetailedDeviceConfiguration) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path file = temporary.Write(R"yaml(
|
|
version: 1
|
|
device:
|
|
type: mobile
|
|
platform: Android
|
|
name: Google Pixel 9 Pro
|
|
manufacturer: Google
|
|
model: Pixel 9 Pro
|
|
brand: google
|
|
code: komodo
|
|
sku: pixel-9-pro
|
|
soc_model: Google Tensor G4
|
|
touch: true
|
|
mouse: true
|
|
keyboard: false
|
|
)yaml");
|
|
|
|
const RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(MapEnvironment(), file);
|
|
|
|
ASSERT_TRUE(loaded) << loaded.error;
|
|
EXPECT_EQ(loaded.config.device_profile().name, "mobile-pixel-7");
|
|
EXPECT_EQ(loaded.config.device_profile().platform_name, "Android");
|
|
EXPECT_EQ(loaded.config.device_profile().display_name, "Google Pixel 9 Pro");
|
|
EXPECT_EQ(loaded.config.device_profile().manufacturer, "Google");
|
|
EXPECT_EQ(loaded.config.device_profile().model, "Pixel 9 Pro");
|
|
EXPECT_EQ(loaded.config.device_profile().brand, "google");
|
|
EXPECT_EQ(loaded.config.device_profile().device_code, "komodo");
|
|
EXPECT_EQ(loaded.config.device_profile().device_sku, "pixel-9-pro");
|
|
EXPECT_EQ(loaded.config.device_profile().soc_model, "Google Tensor G4");
|
|
EXPECT_NE(loaded.config.device_profile().cache_key,
|
|
loaded.config.device_profile().name);
|
|
EXPECT_TRUE(loaded.config.input_capabilities().touch_enabled);
|
|
EXPECT_TRUE(loaded.config.input_capabilities().mouse_enabled);
|
|
EXPECT_FALSE(loaded.config.input_capabilities().keyboard_enabled);
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, ExportsMobileIdentityAndClearsDesktopUserAgent) {
|
|
ASSERT_EQ(0, setenv("MOCKTAIL_USER_AGENT", "stale-desktop", 1));
|
|
const RuntimeConfig mobile = RuntimeConfig::FromEnvironment(
|
|
MapEnvironment({{"MOCKTAIL_DEVICE_PROFILE", "mobile-pixel-7"}}));
|
|
std::string error;
|
|
|
|
ASSERT_TRUE(ExportRuntimeConfigEnvironment(mobile, &error)) << error;
|
|
EXPECT_EQ(getenv("MOCKTAIL_USER_AGENT"), nullptr);
|
|
EXPECT_STREQ(getenv("MOCKTAIL_DEVICE_PROFILE"), "mobile-pixel-7");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_DEVICE_CLASS"), "mobile");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_DEVICE_PLATFORM_NAME"), "Android");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_DEVICE_NAME"), "Google Pixel 7");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_DEVICE_MODEL"), "Pixel 7");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_DEVICE_CODE"), "panther");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_DEVICE_SOC_MODEL"), "Google Tensor G2");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_TOUCH_MODE"), "on");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_MOUSE_MODE"), "off");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_KEYBOARD_MODE"), "off");
|
|
EXPECT_STREQ(getenv("MOCKTAIL_DESKTOP_PLAYABILITY"), "0");
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, EnvironmentDevicePresetOverridesYamlPreset) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path file = temporary.Write(R"yaml(
|
|
version: 1
|
|
device: mobile-pixel-7
|
|
)yaml");
|
|
|
|
const RuntimeConfigLoadResult loaded = LoadRuntimeConfig(
|
|
MapEnvironment({{"MOCKTAIL_DEVICE_PROFILE", "console"}}), file);
|
|
|
|
ASSERT_TRUE(loaded) << loaded.error;
|
|
EXPECT_EQ(loaded.config.device_profile().name, "console-ps5");
|
|
EXPECT_EQ(loaded.config.device_profile().device_class, DeviceClass::kConsole);
|
|
ASSERT_TRUE(loaded.config.roblox_http_user_agent().has_value());
|
|
EXPECT_EQ(*loaded.config.roblox_http_user_agent(),
|
|
kRobloxConsoleAdmissionUserAgent);
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, RejectsUnknownOrContradictoryDevicePreset) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path unknown = temporary.Write(R"yaml(
|
|
version: 1
|
|
device: smart-fridge
|
|
)yaml");
|
|
RuntimeConfigLoadResult loaded = LoadRuntimeConfig(MapEnvironment(), unknown);
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("device must be"), std::string::npos);
|
|
|
|
const std::filesystem::path contradictory = temporary.Write(R"yaml(
|
|
version: 1
|
|
device: mobile-pixel-7
|
|
input:
|
|
touch_enabled: false
|
|
)yaml");
|
|
loaded = LoadRuntimeConfig(MapEnvironment(), contradictory);
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("cannot be combined"), std::string::npos);
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, RejectsInvalidDetailedDeviceConfiguration) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path missing_type = temporary.Write(R"yaml(
|
|
version: 1
|
|
device:
|
|
model: Pixel 9 Pro
|
|
)yaml");
|
|
RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(MapEnvironment(), missing_type);
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("device.type is required"), std::string::npos);
|
|
|
|
const std::filesystem::path empty_model = temporary.Write(R"yaml(
|
|
version: 1
|
|
device:
|
|
type: mobile
|
|
model: ""
|
|
)yaml");
|
|
loaded = LoadRuntimeConfig(MapEnvironment(), empty_model);
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("device.model"), std::string::npos);
|
|
|
|
const std::filesystem::path invalid_input = temporary.Write(R"yaml(
|
|
version: 1
|
|
device:
|
|
type: console
|
|
touch: false
|
|
mouse: false
|
|
keyboard: false
|
|
)yaml");
|
|
loaded = LoadRuntimeConfig(MapEnvironment(), invalid_input);
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("at least one usable input"), std::string::npos);
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, LoadsMobilePlayabilityWithoutChangingTouch) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path file = temporary.Write(R"yaml(
|
|
version: 1
|
|
input:
|
|
touch_enabled: false
|
|
compatibility:
|
|
desktop_playability: false
|
|
)yaml");
|
|
|
|
const RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(MapEnvironment(), file);
|
|
|
|
ASSERT_TRUE(loaded) << loaded.error;
|
|
EXPECT_FALSE(loaded.config.input_capabilities().touch_enabled);
|
|
EXPECT_FALSE(loaded.config.desktop_playability());
|
|
EXPECT_FALSE(loaded.config.roblox_http_user_agent().has_value());
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, RejectsInvalidDesktopPlayabilityValue) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path file = temporary.Write(R"yaml(
|
|
version: 1
|
|
compatibility:
|
|
desktop_playability: automatic
|
|
)yaml");
|
|
|
|
const RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(MapEnvironment(), file);
|
|
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("compatibility.desktop_playability"),
|
|
std::string::npos);
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, AcceptsProductionDirectVulkanBackendName) {
|
|
MapEnvironment environment({{"MOCKTAIL_GRAPHICS_BACKEND", "direct-vulkan"}});
|
|
const RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(environment, "/does/not/exist/direct-vulkan.yaml");
|
|
|
|
ASSERT_TRUE(loaded) << loaded.error;
|
|
EXPECT_EQ(loaded.config.graphics_backend(), GraphicsBackend::kVulkan);
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, AcceptsStrictOpenGlBackendName) {
|
|
MapEnvironment environment({{"MOCKTAIL_GRAPHICS_BACKEND", "opengl"}});
|
|
const RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(environment, "/does/not/exist/opengl.yaml");
|
|
|
|
ASSERT_TRUE(loaded) << loaded.error;
|
|
EXPECT_EQ(loaded.config.graphics_backend(), GraphicsBackend::kSystem);
|
|
EXPECT_EQ(loaded.config.graphics_backend_name(), "opengl");
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, LoadsStrictOpenGlFromYamlWithoutEnvironment) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path file = temporary.Write(R"yaml(
|
|
version: 1
|
|
graphics:
|
|
backend: opengl
|
|
)yaml");
|
|
|
|
const RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(MapEnvironment(), file);
|
|
ASSERT_TRUE(loaded) << loaded.error;
|
|
EXPECT_EQ(loaded.config.graphics_backend(), GraphicsBackend::kSystem);
|
|
EXPECT_EQ(loaded.config.graphics_backend_name(), "opengl");
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, RejectsInvalidKnownSettings) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path file = temporary.Write(R"yaml(
|
|
version: 1
|
|
graphics:
|
|
frame_rate_limit: fastest
|
|
)yaml");
|
|
|
|
const RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(MapEnvironment(), file);
|
|
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("frame_rate_limit"), std::string::npos);
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, RejectsNonBooleanPerformancePolicy) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path file = temporary.Write(R"yaml(
|
|
version: 1
|
|
performance:
|
|
multithreaded_rendering: automatic
|
|
)yaml");
|
|
|
|
const RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(MapEnvironment(), file);
|
|
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("performance.multithreaded_rendering"),
|
|
std::string::npos);
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, RejectsUnknownPhysicsWorkerMode) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path file = temporary.Write(R"yaml(
|
|
version: 1
|
|
performance:
|
|
physics_worker_mode: maximum
|
|
)yaml");
|
|
|
|
const RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(MapEnvironment(), file);
|
|
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("performance.physics_worker_mode"),
|
|
std::string::npos);
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, RejectsMalformedYaml) {
|
|
TemporaryDirectory temporary;
|
|
const std::filesystem::path file = temporary.Write("graphics: [\n");
|
|
|
|
const RuntimeConfigLoadResult loaded =
|
|
LoadRuntimeConfig(MapEnvironment(), file);
|
|
|
|
EXPECT_FALSE(loaded);
|
|
EXPECT_NE(loaded.error.find("invalid YAML"), std::string::npos);
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, FleasionRoutesThroughConfiguredLoopbackPort) {
|
|
TemporaryDirectory temporary;
|
|
const auto file = temporary.Write(
|
|
"integrations:\n fleasion:\n enabled: true\n proxy_mode: env\n"
|
|
" proxy_port: 59443\n ca_certificate: /tmp/fleasion-ca.crt\n");
|
|
auto loaded = LoadRuntimeConfig(MapEnvironment(), file);
|
|
ASSERT_TRUE(loaded) << loaded.error;
|
|
EXPECT_TRUE(loaded.config.fleasion_enabled());
|
|
ASSERT_TRUE(loaded.config.network_proxy());
|
|
EXPECT_EQ(loaded.config.network_proxy()->host, "127.0.0.1");
|
|
EXPECT_EQ(loaded.config.network_proxy()->port, 59443);
|
|
EXPECT_EQ(*loaded.config.fleasion_ca_certificate(), "/tmp/fleasion-ca.crt");
|
|
loaded = LoadRuntimeConfig(MapEnvironment({{"MOCKTAIL_FLEASION_ENABLED", "0"}}), file);
|
|
ASSERT_TRUE(loaded) << loaded.error;
|
|
EXPECT_FALSE(loaded.config.fleasion_enabled());
|
|
EXPECT_FALSE(loaded.config.network_proxy());
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, FleasionRejectsInvalidConfigurationAndProxyConflicts) {
|
|
TemporaryDirectory temporary;
|
|
for (const char* setting : {"proxy_port: 0", "proxy_port: 65536", "proxy_port: 123x",
|
|
"proxy_mode: invalid", "ca_certificate: relative.pem"}) {
|
|
const auto file = temporary.Write(std::string(
|
|
"integrations:\n fleasion:\n enabled: true\n ") + setting + "\n");
|
|
EXPECT_FALSE(LoadRuntimeConfig(MapEnvironment(), file)) << setting;
|
|
}
|
|
const auto file = temporary.Write("integrations:\n fleasion:\n enabled: true\n");
|
|
EXPECT_FALSE(LoadRuntimeConfig(MapEnvironment({{"MOCKTAIL_USE_SYSTEM_PROXY", "1"}}), file));
|
|
EXPECT_FALSE(LoadRuntimeConfig(MapEnvironment({{"MOCKTAIL_HTTP_PROXY_HOST", "other"},
|
|
{"MOCKTAIL_HTTP_PROXY_PORT", "3128"}}), file));
|
|
}
|
|
|
|
TEST(RuntimeConfigFileTest, FleasionEnvironmentRoundTripsAndHostsModeDoesNotSetProxy) {
|
|
TemporaryDirectory temporary;
|
|
const auto file = temporary.Write("integrations:\n fleasion:\n enabled: true\n"
|
|
" proxy_mode: hosts\n");
|
|
const auto loaded = LoadRuntimeConfig(MapEnvironment(), file);
|
|
ASSERT_TRUE(loaded) << loaded.error;
|
|
EXPECT_FALSE(loaded.config.network_proxy());
|
|
std::string error;
|
|
ASSERT_TRUE(ExportRuntimeConfigEnvironment(loaded.config, &error)) << error;
|
|
const auto resolved = RuntimeConfig::FromEnvironment(ProcessEnvironment());
|
|
EXPECT_TRUE(resolved.fleasion_enabled());
|
|
EXPECT_TRUE(resolved.fleasion_valid());
|
|
EXPECT_EQ(resolved.fleasion_proxy_mode(), "hosts");
|
|
ASSERT_TRUE(ExportRuntimeConfigEnvironment(RuntimeConfig::FromEnvironment(MapEnvironment()), &error));
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace runtime
|
|
} // namespace mocktail
|