Files
InstanceBrick--nightcap/tests/command_line_test.cc
T

389 lines
15 KiB
C++

#include "runtime/command_line.h"
#include <gtest/gtest.h>
#include "compat/guest_abi.h"
#include <algorithm>
#include <array>
#include <cstdlib>
#include <filesystem>
#include <string>
#include <vector>
namespace mocktail {
namespace runtime {
namespace {
TEST(CommandLineTest, ShortVrAliasSurvivesReexecAndLastOverrideWins) {
const char* arguments[] = {"mocktail", "-vr"};
const auto parsed = ParseCommandLine(2, arguments);
ASSERT_TRUE(parsed);
ASSERT_TRUE(parsed.options.vr_enabled.has_value());
EXPECT_TRUE(*parsed.options.vr_enabled);
std::vector<std::string> reexec;
std::string error;
ASSERT_TRUE(BuildCommandLineReexecArguments(parsed.options, 2, arguments, &reexec, &error));
EXPECT_EQ(reexec, (std::vector<std::string>{"-vr"}));
const char* disabled[] = {"mocktail", "-vr", "--no-vr"};
EXPECT_FALSE(*ParseCommandLine(3, disabled).options.vr_enabled);
}
TEST(CommandLineTest, VrOverrideSurvivesReexecAndCanDisableEnvironmentDefault) {
const std::array<const char*, 4> arguments = {"mocktail", "--vr",
"--headless", "--no-vr"};
const auto parsed = ParseCommandLine(arguments.size(), arguments.data());
ASSERT_TRUE(parsed) << parsed.error;
ASSERT_TRUE(parsed.options.vr_enabled.has_value());
EXPECT_FALSE(*parsed.options.vr_enabled);
std::vector<std::string> reexec;
std::string error;
ASSERT_TRUE(BuildCommandLineReexecArguments(
parsed.options, arguments.size(), arguments.data(), &reexec, &error))
<< error;
EXPECT_EQ(reexec,
(std::vector<std::string>{"--vr", "--headless", "--no-vr"}));
const char* previous = std::getenv("MOCKTAIL_VR_ENABLED");
const std::optional<std::string> saved =
previous ? std::optional<std::string>(previous) : std::nullopt;
const char* previous_headless = std::getenv("MOCKTAIL_HEADLESS");
const std::optional<std::string> saved_headless =
previous_headless ? std::optional<std::string>(previous_headless)
: std::nullopt;
setenv("MOCKTAIL_VR_ENABLED", "true", 1);
EXPECT_TRUE(ApplyCommandLineEnvironment(parsed.options, &error)) << error;
EXPECT_STREQ(std::getenv("MOCKTAIL_VR_ENABLED"), "0");
if (saved)
setenv("MOCKTAIL_VR_ENABLED", saved->c_str(), 1);
else
unsetenv("MOCKTAIL_VR_ENABLED");
if (saved_headless)
setenv("MOCKTAIL_HEADLESS", saved_headless->c_str(), 1);
else
unsetenv("MOCKTAIL_HEADLESS");
}
TEST(CommandLineTest, ParsesExplicitVrAndKeepsUnspecifiedDistinctFromFalse) {
const char* enabled[] = {"mocktail", "--vr"};
const auto parsed = ParseCommandLine(2, enabled);
ASSERT_TRUE(parsed) << parsed.error;
EXPECT_EQ(parsed.options.vr_enabled, std::optional<bool>(true));
const char* normal[] = {"mocktail"};
EXPECT_FALSE(ParseCommandLine(1, normal).options.vr_enabled.has_value());
const char* incompatible[] = {"mocktail", "--force-run-latest", "--vr"};
EXPECT_FALSE(ParseCommandLine(3, incompatible));
}
TEST(CommandLineTest, ParsesCompleteRunContractOnce) {
const std::array<const char*, 11> arguments = {
"mocktail",
"--roblox-lib",
"/payload/libroblox.so",
"--headless",
"--graphics",
"direct-vulkan",
"--windowed",
"--allow-unverified-build",
"--launch-uri",
"roblox://experiences/start?placeId=17580461965",
nullptr};
const CommandLineParseResult result = ParseCommandLine(10, arguments.data());
ASSERT_TRUE(result);
EXPECT_EQ(result.options.mode, CommandMode::kRun);
EXPECT_EQ(result.options.roblox_library_path, "/payload/libroblox.so");
EXPECT_EQ(result.options.graphics_backend, "direct-vulkan");
EXPECT_EQ(result.options.window_mode, WindowMode::kWindowed);
EXPECT_TRUE(result.options.allow_unverified_build);
EXPECT_EQ(result.options.raw_launch_argument,
"roblox://experiences/start?placeId=17580461965");
EXPECT_FALSE(result.options.launch_request_json.empty());
EXPECT_EQ(result.options.launch_argument_index, 9);
}
TEST(CommandLineTest, PreservesDesktopLaunchUriAsOneOpaqueArgument) {
constexpr char kLaunchUri[] =
"roblox://experiences/start?placeId=1962086868&"
"gameInstanceId=97ea-49e4&launchData=%257B%2522key%2522%253A%2522a%"
"252Bb%2522%257D";
const std::array<const char*, 3> arguments = {"mocktail", "--launch-uri",
kLaunchUri};
const CommandLineParseResult result =
ParseCommandLine(arguments.size(), arguments.data());
ASSERT_TRUE(result) << result.error;
EXPECT_EQ(result.options.mode, CommandMode::kRun);
EXPECT_EQ(result.options.raw_launch_argument, kLaunchUri);
EXPECT_FALSE(result.options.launch_request_json.empty());
EXPECT_EQ(result.options.launch_argument_index, 2);
}
TEST(CommandLineTest, AcceptsDesktopUriWithoutBreakingMenuLaunch) {
constexpr char kUri[] =
"roblox://experiences/start?placeId=17580461965&"
"gameInstanceId=instance%2Bserver";
const std::array<const char*, 2> arguments = {"mocktail", kUri};
const CommandLineParseResult result =
ParseCommandLine(arguments.size(), arguments.data());
ASSERT_TRUE(result) << result.error;
EXPECT_EQ(result.options.raw_launch_argument, kUri);
EXPECT_FALSE(result.options.launch_request_json.empty());
EXPECT_EQ(result.options.launch_argument_index, 1);
std::vector<std::string> reexec_arguments;
std::string reexec_error;
ASSERT_TRUE(BuildCommandLineReexecArguments(result.options, arguments.size(),
arguments.data(),
&reexec_arguments, &reexec_error))
<< reexec_error;
ASSERT_EQ(reexec_arguments.size(), 2u);
EXPECT_EQ(reexec_arguments[0], "--launch-request-json");
EXPECT_EQ(reexec_arguments[1], result.options.launch_request_json);
const std::array<const char*, 1> menu_launch = {"mocktail"};
const CommandLineParseResult menu_result =
ParseCommandLine(menu_launch.size(), menu_launch.data());
ASSERT_TRUE(menu_result) << menu_result.error;
EXPECT_TRUE(menu_result.options.raw_launch_argument.empty());
EXPECT_TRUE(menu_result.options.launch_request_json.empty());
}
TEST(CommandLineTest, RedactsBrowserTicketAcrossCgroupReexec) {
constexpr char kSecret[] = "SECRET-BROWSER-GAMEINFO-TICKET";
constexpr char kLaunchUri[] =
"roblox-player:1+launchmode:play+"
"gameinfo:SECRET-BROWSER-GAMEINFO-TICKET+"
"placelauncherurl:https%3A%2F%2Fwww.roblox.com%2FGame%2F"
"PlaceLauncher.ashx%3Frequest%3DRequestGame%26placeId%3D17580461965";
const std::array<const char*, 3> arguments = {"mocktail", "--launch-uri",
kLaunchUri};
const CommandLineParseResult result =
ParseCommandLine(arguments.size(), arguments.data());
ASSERT_TRUE(result) << result.error;
ASSERT_EQ(result.options.launch_request_json.find(kSecret),
std::string::npos);
std::vector<std::string> reexec_arguments;
std::string error;
ASSERT_TRUE(BuildCommandLineReexecArguments(result.options, arguments.size(),
arguments.data(),
&reexec_arguments, &error))
<< error;
ASSERT_EQ(reexec_arguments.size(), 2u);
EXPECT_EQ(reexec_arguments[0], "--launch-request-json");
EXPECT_EQ(reexec_arguments[1], result.options.launch_request_json);
EXPECT_EQ(reexec_arguments[1].find(kSecret), std::string::npos);
const std::array<const char*, 3> reexec = {
"mocktail", reexec_arguments[0].c_str(), reexec_arguments[1].c_str()};
const CommandLineParseResult reparsed =
ParseCommandLine(reexec.size(), reexec.data());
ASSERT_TRUE(reparsed) << reparsed.error;
EXPECT_FALSE(reparsed.options.raw_launch_argument.empty());
EXPECT_EQ(reparsed.options.launch_argument_index, 2);
EXPECT_EQ(reparsed.options.launch_request_json,
result.options.launch_request_json);
}
TEST(CommandLineTest, DoesNotEchoInvalidInternalLaunchDocument) {
constexpr char kSecret[] = "SECRET-INTERNAL-LAUNCH-DOCUMENT";
const std::array<const char*, 3> arguments = {
"mocktail", "--launch-request-json", kSecret};
const CommandLineParseResult result =
ParseCommandLine(arguments.size(), arguments.data());
EXPECT_FALSE(result);
EXPECT_EQ(result.error, "invalid controlled launch request");
EXPECT_EQ(result.error.find(kSecret), std::string::npos);
}
TEST(CommandLineTest, ScrubsInternalLaunchDocumentFromOwnedAndProcessArgv) {
char program[] = "mocktail";
char option[] = "--launch-request-json";
char document[] =
R"({"placeId":17580461965,"accessCode":"private-access-code"})";
char* mutable_arguments[] = {program, option, document};
const char* parse_arguments[] = {program, option, document};
CommandLineParseResult result = ParseCommandLine(3, parse_arguments);
ASSERT_TRUE(result) << result.error;
ASSERT_FALSE(result.options.raw_launch_argument.empty());
ASSERT_FALSE(result.options.launch_request_json.empty());
ScrubCommandLineLaunchArguments(&result.options, 3, mutable_arguments);
EXPECT_TRUE(result.options.raw_launch_argument.empty());
EXPECT_TRUE(result.options.launch_request_json.empty());
EXPECT_EQ(result.options.launch_argument_index, -1);
EXPECT_TRUE(std::all_of(std::begin(document), std::end(document) - 1,
[](char byte) { return byte == '\0'; }));
}
TEST(CommandLineTest, ParsesProfileTracePath) {
const std::array<const char*, 3> arguments = {"mocktail", "--profile",
"/traces/session.json"};
const CommandLineParseResult result =
ParseCommandLine(arguments.size(), arguments.data());
ASSERT_TRUE(result) << result.error;
EXPECT_EQ(result.options.profile_trace_path, "/traces/session.json");
}
TEST(CommandLineTest, AppliesProfileTraceAsAbsolutePath) {
unsetenv("MOCKTAIL_PROFILE_TRACE");
CommandLineOptions options;
options.profile_trace_path = "session.json";
std::string error;
ASSERT_TRUE(ApplyCommandLineEnvironment(options, &error)) << error;
const char* applied = std::getenv("MOCKTAIL_PROFILE_TRACE");
ASSERT_NE(applied, nullptr);
EXPECT_EQ(std::string(applied),
(std::filesystem::current_path() / "session.json").string());
unsetenv("MOCKTAIL_PROFILE_TRACE");
}
TEST(CommandLineTest, LeavesProfileTraceUnsetWithoutOption) {
unsetenv("MOCKTAIL_PROFILE_TRACE");
std::string error;
ASSERT_TRUE(ApplyCommandLineEnvironment(CommandLineOptions{}, &error))
<< error;
EXPECT_EQ(std::getenv("MOCKTAIL_PROFILE_TRACE"), nullptr);
}
TEST(CommandLineTest, RejectsUnsupportedPositionalArgument) {
const std::array<const char*, 2> arguments = {"mocktail", "/tmp/payload"};
const CommandLineParseResult result =
ParseCommandLine(arguments.size(), arguments.data());
EXPECT_FALSE(result);
EXPECT_EQ(result.error, "unknown argument");
}
TEST(CommandLineTest, RejectsUnknownOptionBeforeRuntimeWork) {
const std::array<const char*, 2> arguments = {"mocktail", "--unknown"};
const CommandLineParseResult result =
ParseCommandLine(arguments.size(), arguments.data());
EXPECT_FALSE(result);
EXPECT_EQ(result.error, "unknown option: --unknown");
}
TEST(CommandLineTest, RejectsEveryMissingValue) {
for (const char* option :
{"--roblox-lib", "--graphics", "--launch-uri", "--profile"}) {
const std::array<const char*, 2> arguments = {"mocktail", option};
const CommandLineParseResult result =
ParseCommandLine(arguments.size(), arguments.data());
EXPECT_FALSE(result) << option;
EXPECT_EQ(result.error, std::string("missing value for ") + option)
<< option;
}
}
TEST(CommandLineTest, RejectsDuplicateLaunchUri) {
const std::array<const char*, 5> arguments = {
"mocktail", "--launch-uri",
"roblox://experiences/start?placeId=17580461965", "--launch-uri",
"roblox://experiences/start?placeId=1962086868"};
const CommandLineParseResult result =
ParseCommandLine(arguments.size(), arguments.data());
EXPECT_FALSE(result);
EXPECT_NE(result.error.find("--launch-uri"), std::string::npos);
}
TEST(CommandLineTest, DoesNotEchoASecondRawLaunchTicket) {
constexpr char kSecret[] = "SECRET-BROWSER-TICKET";
const std::array<const char*, 3> arguments = {
"mocktail", "roblox://experiences/start?placeId=17580461965",
"roblox-player:1+launchmode:play+gameinfo:SECRET-BROWSER-TICKET"};
const CommandLineParseResult result =
ParseCommandLine(arguments.size(), arguments.data());
EXPECT_FALSE(result);
EXPECT_EQ(result.error, "unknown argument");
EXPECT_EQ(result.error.find(kSecret), std::string::npos);
}
TEST(CommandLineTest, RejectsInvalidLaunchUriBeforeRuntimeWork) {
const std::array<const char*, 3> arguments = {
"mocktail", "--launch-uri",
"https://www.roblox.com/experiences/start?placeId=17580461965"};
const CommandLineParseResult result =
ParseCommandLine(arguments.size(), arguments.data());
EXPECT_FALSE(result);
EXPECT_TRUE(result.options.raw_launch_argument.empty());
EXPECT_NE(result.error.find("invalid Roblox launch URI"), std::string::npos);
}
TEST(CommandLineTest, RejectsRemovedStandaloneLoginOptions) {
for (const char* option : {"--login", "--import-cookie"}) {
const std::array<const char*, 2> arguments = {"mocktail", option};
const CommandLineParseResult result =
ParseCommandLine(arguments.size(), arguments.data());
EXPECT_FALSE(result) << option;
EXPECT_EQ(result.error, std::string("unknown option: ") + option);
}
}
TEST(CommandLineTest, RejectsAnotherOptionAsMissingValue) {
const std::array<const char*, 3> arguments = {"mocktail", "--roblox-lib",
"--headless"};
const CommandLineParseResult result =
ParseCommandLine(arguments.size(), arguments.data());
EXPECT_FALSE(result);
EXPECT_EQ(result.error, "missing value for --roblox-lib");
}
TEST(CommandLineTest, HelpIsATypedAuxiliaryMode) {
const std::array<const char*, 2> help = {"mocktail", "--help"};
const CommandLineParseResult help_result =
ParseCommandLine(help.size(), help.data());
ASSERT_TRUE(help_result);
EXPECT_EQ(help_result.options.mode, CommandMode::kHelp);
}
TEST(CommandLineTest, UsageContainsEverySupportedOption) {
const std::string usage = CommandLineUsage("mocktail-test");
EXPECT_NE(usage.find("mocktail-test"), std::string::npos);
for (const char* option :
{"--roblox-lib", "--headless", "--windowed", "--graphics",
"--allow-unverified-build", "--force-run-latest", "--launch-uri",
"--profile", "--help"}) {
EXPECT_NE(usage.find(option), std::string::npos) << option;
}
EXPECT_EQ(usage.find("--login"), std::string::npos);
EXPECT_EQ(usage.find("--import-cookie"), std::string::npos);
EXPECT_NE(usage.find("native sign-in flow"), std::string::npos);
EXPECT_NE(
usage.find("verified managed " +
std::string(mocktail::compat::kGuestAbi) + " Roblox payload"),
std::string::npos);
EXPECT_EQ(usage.find("Normal startup uses rbx_bin/libroblox.so"),
std::string::npos);
EXPECT_EQ(usage.find("--launch-request-json"), std::string::npos);
}
} // namespace
} // namespace runtime
} // namespace mocktail