9e2431e58a
Steam-first Linux game launcher wrapper for MangoHud and GameMode. Manages launch behavior via TOML config with named profiles, per-game bindings, and full diagnostics. All v1 criteria validated. Co-Authored-By: claude-flow <ruv@ruv.net>
1183 lines
40 KiB
Rust
1183 lines
40 KiB
Rust
use std::process::Command;
|
|
use std::{fs, path::PathBuf};
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use tempfile::TempDir;
|
|
|
|
struct TestEnv {
|
|
_root: TempDir,
|
|
config_home: PathBuf,
|
|
state_home: PathBuf,
|
|
home: PathBuf,
|
|
}
|
|
|
|
struct CmdResult {
|
|
status: i32,
|
|
output: String,
|
|
}
|
|
|
|
impl TestEnv {
|
|
fn new() -> Self {
|
|
let root = tempfile::tempdir().expect("temp dir");
|
|
let config_home = root.path().join("config");
|
|
let state_home = root.path().join("state");
|
|
let home = root.path().join("home");
|
|
std::fs::create_dir_all(&config_home).expect("config dir");
|
|
std::fs::create_dir_all(&state_home).expect("state dir");
|
|
std::fs::create_dir_all(&home).expect("home dir");
|
|
Self {
|
|
_root: root,
|
|
config_home,
|
|
state_home,
|
|
home,
|
|
}
|
|
}
|
|
|
|
fn run(&self, args: &[&str]) -> CmdResult {
|
|
self.run_with_env(args, &[])
|
|
}
|
|
|
|
fn run_with_env(&self, args: &[&str], extra_env: &[(&str, &str)]) -> CmdResult {
|
|
let output = Command::new(env!("CARGO_BIN_EXE_gamewrap"))
|
|
.args(args)
|
|
.env("XDG_CONFIG_HOME", &self.config_home)
|
|
.env("XDG_STATE_HOME", &self.state_home)
|
|
.env("HOME", &self.home)
|
|
.env("NO_COLOR", "1")
|
|
.env_remove("SteamAppId")
|
|
.env_remove("SteamGameId")
|
|
.env_remove("STEAM_COMPAT_DATA_PATH")
|
|
.env_remove("STEAM_COMPAT_CLIENT_INSTALL_PATH")
|
|
.envs(extra_env.iter().copied())
|
|
.output()
|
|
.expect("run gamewrap");
|
|
|
|
let mut combined = String::new();
|
|
combined.push_str(&String::from_utf8_lossy(&output.stdout));
|
|
combined.push_str(&String::from_utf8_lossy(&output.stderr));
|
|
|
|
CmdResult {
|
|
status: output.status.code().unwrap_or(-1),
|
|
output: combined,
|
|
}
|
|
}
|
|
|
|
fn path(&self, relative: &str) -> PathBuf {
|
|
self.home.join(relative)
|
|
}
|
|
|
|
fn path_with_fake_bins(&self, names: &[&str]) -> String {
|
|
let bin_dir = self.home.join("bin");
|
|
fs::create_dir_all(&bin_dir).expect("fake bin dir");
|
|
for name in names {
|
|
let path = bin_dir.join(name);
|
|
fs::write(&path, "#!/bin/sh\nexit 0\n").expect("write fake bin");
|
|
let mut permissions = fs::metadata(&path)
|
|
.expect("fake bin metadata")
|
|
.permissions();
|
|
permissions.set_mode(0o755);
|
|
fs::set_permissions(&path, permissions).expect("chmod fake bin");
|
|
}
|
|
|
|
let current_path = std::env::var_os("PATH").unwrap_or_default();
|
|
format!("{}:{}", bin_dir.display(), current_path.to_string_lossy())
|
|
}
|
|
}
|
|
|
|
fn assert_ok(result: &CmdResult) {
|
|
assert_eq!(result.status, 0, "unexpected failure:\n{}", result.output);
|
|
}
|
|
|
|
fn assert_exit(result: &CmdResult, code: i32) {
|
|
assert_eq!(result.status, code, "unexpected output:\n{}", result.output);
|
|
}
|
|
|
|
#[test]
|
|
fn help_and_launch_commands_work() {
|
|
let env = TestEnv::new();
|
|
|
|
let result = env.run(&["--version"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains(env!("CARGO_PKG_VERSION")));
|
|
|
|
let result = env.run(&["--help"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Commands:"));
|
|
assert!(result.output.contains("profile"));
|
|
assert!(result.output.contains("game"));
|
|
assert!(result.output.contains("last"));
|
|
assert!(result.output.contains("notify"));
|
|
assert!(result.output.contains("completion"));
|
|
|
|
let result = env.run(&["help"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Common tasks:"));
|
|
assert!(result.output.contains("gamewrap %command%"));
|
|
assert!(result.output.contains("gamewrap completion bash"));
|
|
assert!(result.output.contains("gamescope-width"));
|
|
assert!(result.output.contains("profile env set"));
|
|
assert!(result.output.contains("pre-launch"));
|
|
|
|
let result = env.run(&[]);
|
|
assert_exit(&result, 2);
|
|
assert!(result.output.contains("No command was provided."));
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Steam and terminal usage are different.")
|
|
);
|
|
|
|
let result = env.run(&["profils"]);
|
|
assert_exit(&result, 2);
|
|
assert!(result.output.contains("not a known gamewrap command"));
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("gamewrap run /path/to/game/executable")
|
|
);
|
|
|
|
for topic in [
|
|
"settings",
|
|
"doctor",
|
|
"profiles",
|
|
"bindings",
|
|
"completion",
|
|
"troubleshooting",
|
|
] {
|
|
let result = env.run(&["help", topic]);
|
|
assert_ok(&result);
|
|
assert!(!result.output.is_empty());
|
|
}
|
|
|
|
let result = env.run(&["help", "nonsense"]);
|
|
assert_exit(&result, 2);
|
|
assert!(result.output.contains("not a known help topic"));
|
|
|
|
let result = env.run(&["status"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Resolved defaults:"));
|
|
assert!(result.output.contains("gamescope:"));
|
|
assert!(result.output.contains("vkbasalt:"));
|
|
assert!(result.output.contains("Bindings:"));
|
|
|
|
let result = env.run(&["doctor"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Assumed launch context: Steam"));
|
|
assert!(result.output.contains("gamescope: off"));
|
|
assert!(result.output.contains("vkbasalt: off"));
|
|
assert!(result.output.contains("gamescope compositor"));
|
|
assert!(result.output.contains("vkbasalt layer"));
|
|
|
|
let result = env.run(&["doctor", "/usr/bin/true"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("/usr/bin/true"));
|
|
|
|
let result = env.run(&["run", "--help"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Usage: gamewrap run [--] <COMMAND>...")
|
|
);
|
|
|
|
let result = env.run(&["dry-run", "--help"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Usage: gamewrap dry-run [--] <COMMAND>...")
|
|
);
|
|
|
|
let result = env.run(&["run", "/usr/bin/true"]);
|
|
assert_ok(&result);
|
|
|
|
let result = env.run(&["dry-run", "/usr/bin/true"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Resolved profile: default"));
|
|
assert!(result.output.contains("Final command:"));
|
|
|
|
assert_ok(&env.run(&["config", "set", "pre-launch", "printf pre"]));
|
|
assert_ok(&env.run(&["config", "set", "post-launch", "printf post"]));
|
|
let result = env.run(&["dry-run", "/usr/bin/true"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Pre-launch hook:"));
|
|
assert!(result.output.contains("sh -c \"printf pre\""));
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Post-launch hook (runs after game exits):")
|
|
);
|
|
assert!(result.output.contains("sh -c \"printf post\""));
|
|
|
|
let result = env.run(&["doctor", "/usr/bin/true"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("pre-launch: printf pre"));
|
|
assert!(result.output.contains("post-launch: printf post"));
|
|
|
|
assert_ok(&env.run(&["config", "set", "gamescope", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-width", "1920"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-height", "1080"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-fps", "60"]));
|
|
let fake_path = env.path_with_fake_bins(&["gamescope", "mangohud", "gamemoderun"]);
|
|
let result = env.run_with_env(&["dry-run", "/usr/bin/true"], &[("PATH", &fake_path)]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("gamescope -W 1920 -H 1080 -r 60 -- mangohud gamemoderun /usr/bin/true")
|
|
);
|
|
|
|
let result = env.run(&["completion", "bash"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("GAMEWRAP_COMPLETE=\"bash\""));
|
|
|
|
let result = env.run(&["completion", "path", "zsh"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Completion script path:"));
|
|
assert!(result.output.contains(".zshrc"));
|
|
|
|
let result = env.run(&["notify", "test"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result.output.contains("Sent test notification via")
|
|
|| result.output.contains("No notifier available.")
|
|
);
|
|
|
|
let result = env.run(&["completion", "install", "zsh"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Installed zsh completion"));
|
|
assert!(result.output.contains("show up automatically"));
|
|
|
|
let script_path = env
|
|
.config_home
|
|
.join("gamewrap")
|
|
.join("completions")
|
|
.join("gamewrap.zsh");
|
|
assert!(script_path.exists(), "missing installed script");
|
|
let script = fs::read_to_string(&script_path).expect("read installed zsh script");
|
|
assert!(script.contains("GAMEWRAP_COMPLETE=\"zsh\""));
|
|
|
|
let zshrc_path = env.home.join(".zshrc");
|
|
let zshrc = fs::read_to_string(&zshrc_path).expect("read zshrc");
|
|
assert!(zshrc.contains("gamewrap completion"));
|
|
assert!(zshrc.contains("source"));
|
|
|
|
let result = env.run(&["run", "--", "definitely-not-a-real-command"]);
|
|
assert_exit(&result, 4);
|
|
assert!(result.output.contains("was not found in PATH"));
|
|
|
|
let result = env.run(&["run", "--", "--help"]);
|
|
assert_exit(&result, 2);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("No runnable target command was provided.")
|
|
);
|
|
|
|
let result = env.run(&["dry-run", "--", "--help"]);
|
|
assert_exit(&result, 2);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("No runnable target command was provided.")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn config_profiles_import_export_and_inheritance_work() {
|
|
let env = TestEnv::new();
|
|
|
|
let result = env.run(&["config", "--help"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("show"));
|
|
assert!(result.output.contains("edit"));
|
|
assert!(result.output.contains("reset"));
|
|
assert!(result.output.contains("export"));
|
|
assert!(result.output.contains("import"));
|
|
|
|
let result = env.run(&["config", "show"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("overlay = on"));
|
|
|
|
let result = env.run_with_env(&["config", "edit"], &[("EDITOR", "true")]);
|
|
assert_ok(&result);
|
|
assert!(result.output.is_empty());
|
|
|
|
for (setting, value) in [
|
|
("overlay", "off"),
|
|
("performance", "off"),
|
|
("steam-host-libs", "off"),
|
|
("host-libs", "on"),
|
|
("game-libs", "keep"),
|
|
("verbose", "on"),
|
|
("gamescope", "on"),
|
|
("gamescope-width", "1920"),
|
|
("gamescope-height", "1080"),
|
|
("gamescope-fps", "60"),
|
|
("fps-cap", "60"),
|
|
("vkbasalt", "on"),
|
|
("esync", "off"),
|
|
("fsync", "on"),
|
|
("large-address-aware", "on"),
|
|
("pre-launch", "printf default-pre"),
|
|
("post-launch", "printf default-post"),
|
|
] {
|
|
let result = env.run(&["config", "set", setting, value]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Updated default setting"));
|
|
}
|
|
|
|
let result = env.run(&["config", "show"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("overlay = off"));
|
|
assert!(result.output.contains("performance = off"));
|
|
assert!(result.output.contains("steam-host-libs = on"));
|
|
assert!(result.output.contains("game-libs = keep"));
|
|
assert!(result.output.contains("verbose = on"));
|
|
assert!(result.output.contains("gamescope = on"));
|
|
assert!(result.output.contains("gamescope-width = 1920"));
|
|
assert!(result.output.contains("gamescope-height = 1080"));
|
|
assert!(result.output.contains("gamescope-fps = 60"));
|
|
assert!(result.output.contains("fps-cap = 60"));
|
|
assert!(result.output.contains("vkbasalt = on"));
|
|
assert!(result.output.contains("esync = off"));
|
|
assert!(result.output.contains("fsync = on"));
|
|
assert!(result.output.contains("large-address-aware = on"));
|
|
assert!(result.output.contains("pre-launch = printf default-pre"));
|
|
assert!(result.output.contains("post-launch = printf default-post"));
|
|
|
|
let result = env.run(&["config", "set", "banana", "on"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("not a known setting"));
|
|
|
|
let result = env.run(&["config", "set", "overlay", "maybe"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("valid on/off value"));
|
|
|
|
let result = env.run(&["config", "set", "game-libs", "nonsense"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("valid value for game-libs"));
|
|
|
|
let result = env.run(&["config", "set", "fps-cap", "fast"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("valid FPS cap"));
|
|
|
|
let result = env.run(&["config", "set", "gamescope-width", "wide"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("valid pixel count"));
|
|
|
|
let result = env.run(&["config", "set", "gamescope-fps", "fast"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("valid FPS"));
|
|
|
|
let result = env.run(&["config", "reset", "overlay"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Reset default setting `overlay`."));
|
|
|
|
let result = env.run(&["config", "reset", "nope"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("not a known setting"));
|
|
|
|
let result = env.run(&["profile", "--help"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("tree"));
|
|
assert!(result.output.contains("reset"));
|
|
assert!(result.output.contains("duplicate"));
|
|
assert!(result.output.contains("inherit"));
|
|
assert!(result.output.contains("clear-inherit"));
|
|
assert!(result.output.contains("env"));
|
|
assert!(result.output.contains("export"));
|
|
assert!(result.output.contains("import"));
|
|
|
|
let result = env.run(&["profile", "list"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("No profiles configured."));
|
|
|
|
for name in ["base", "benchmark", "recording"] {
|
|
let result = env.run(&["profile", "create", name]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Created profile"));
|
|
}
|
|
|
|
let result = env.run(&["profile", "create", "default"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("`default` is reserved"));
|
|
|
|
let result = env.run(&["profile", "create", "benchmark"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("already exists"));
|
|
|
|
let result = env.run(&["profile", "show", "default"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("[defaults]"));
|
|
|
|
let result = env.run(&["profile", "show", "benchmark"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("overlay = (inherits: on)"));
|
|
assert!(result.output.contains("performance = (inherits: off)"));
|
|
|
|
let result = env.run(&["profile", "set", "base", "overlay", "on"]);
|
|
assert_ok(&result);
|
|
let result = env.run(&["profile", "set", "base", "performance", "on"]);
|
|
assert_ok(&result);
|
|
let result = env.run(&["profile", "inherit", "benchmark", "base"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Profile `benchmark` now inherits from `base`.")
|
|
);
|
|
|
|
let result = env.run(&["profile", "show", "benchmark"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("inherits = base"));
|
|
assert!(result.output.contains("overlay = (inherits: on)"));
|
|
|
|
let result = env.run(&["profile", "env", "set", "base", "GW_PARENT", "base-value"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Set env `GW_PARENT=base-value` on profile `base`.")
|
|
);
|
|
let result = env.run(&[
|
|
"profile",
|
|
"env",
|
|
"set",
|
|
"benchmark",
|
|
"GW_CHILD",
|
|
"bench-value",
|
|
]);
|
|
assert_ok(&result);
|
|
let result = env.run(&[
|
|
"profile",
|
|
"env",
|
|
"set",
|
|
"benchmark",
|
|
"GW_PARENT",
|
|
"child-value",
|
|
]);
|
|
assert_ok(&result);
|
|
let result = env.run(&["profile", "env", "list", "benchmark"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("GW_CHILD=bench-value"));
|
|
assert!(result.output.contains("GW_PARENT=child-value"));
|
|
assert!(!result.output.contains("GW_PARENT=base-value"));
|
|
|
|
let result = env.run(&["profile", "env", "unset", "benchmark", "GW_CHILD"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Unset env `GW_CHILD` on profile `benchmark`.")
|
|
);
|
|
let result = env.run(&["profile", "env", "unset", "benchmark", "GW_MISSING"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("No env var `GW_MISSING` on profile `benchmark`.")
|
|
);
|
|
let result = env.run(&["profile", "env", "clear", "benchmark"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Cleared all env vars from profile `benchmark`.")
|
|
);
|
|
let result = env.run(&["profile", "env", "list", "benchmark"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("GW_PARENT=base-value"));
|
|
|
|
let result = env.run(&["profile", "tree"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("default (built-in)"));
|
|
assert!(result.output.contains("base"));
|
|
assert!(result.output.contains("benchmark"));
|
|
|
|
let result = env.run(&["game", "bind", "Demo.exe", "benchmark"]);
|
|
assert_ok(&result);
|
|
let result = env.run(&["profile", "list"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("benchmark (inherits: base, 1 binding)")
|
|
);
|
|
let result = env.run(&["game", "unbind", "Demo.exe"]);
|
|
assert_ok(&result);
|
|
|
|
for (setting, value) in [
|
|
("overlay", "off"),
|
|
("performance", "on"),
|
|
("steam-host-libs", "off"),
|
|
("game-libs", "gamemode"),
|
|
("verbose", "off"),
|
|
("gamescope", "on"),
|
|
("gamescope-width", "2560"),
|
|
("gamescope-height", "1440"),
|
|
("gamescope-fps", "120"),
|
|
("fps-cap", "120"),
|
|
("vkbasalt", "off"),
|
|
("esync", "on"),
|
|
("fsync", "off"),
|
|
("laa", "off"),
|
|
("pre-launch", "printf profile-pre"),
|
|
("post-launch", "printf profile-post"),
|
|
] {
|
|
let result = env.run(&["profile", "set", "benchmark", setting, value]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Updated profile `benchmark`"));
|
|
}
|
|
|
|
let result = env.run(&["profile", "show", "benchmark"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("inherits = base"));
|
|
assert!(result.output.contains("overlay = off"));
|
|
assert!(result.output.contains("performance = on"));
|
|
assert!(result.output.contains("steam-host-libs = off"));
|
|
assert!(result.output.contains("game-libs = gamemode"));
|
|
assert!(result.output.contains("verbose = off"));
|
|
assert!(result.output.contains("gamescope = on"));
|
|
assert!(result.output.contains("gamescope-width = 2560"));
|
|
assert!(result.output.contains("gamescope-height = 1440"));
|
|
assert!(result.output.contains("gamescope-fps = 120"));
|
|
assert!(result.output.contains("fps-cap = 120"));
|
|
assert!(result.output.contains("vkbasalt = off"));
|
|
assert!(result.output.contains("esync = on"));
|
|
assert!(result.output.contains("fsync = off"));
|
|
assert!(result.output.contains("large-address-aware = off"));
|
|
assert!(result.output.contains("pre-launch = printf profile-pre"));
|
|
assert!(result.output.contains("post-launch = printf profile-post"));
|
|
|
|
let result = env.run(&["profile", "duplicate", "benchmark", "benchmark-copy"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Duplicated profile `benchmark` to `benchmark-copy`.")
|
|
);
|
|
|
|
let result = env.run(&["profile", "show", "benchmark-copy"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("inherits = base"));
|
|
assert!(result.output.contains("overlay = off"));
|
|
|
|
let result = env.run(&["profile", "duplicate", "missing", "whatever"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("Profile `missing` does not exist."));
|
|
|
|
let result = env.run(&["profile", "duplicate", "benchmark", "benchmark-copy"]);
|
|
assert_exit(&result, 3);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Profile `benchmark-copy` already exists.")
|
|
);
|
|
|
|
let result = env.run(&["profile", "set", "missing", "overlay", "on"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("Profile `missing` does not exist."));
|
|
|
|
let result = env.run(&["profile", "set", "benchmark", "nope", "on"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("not a known setting"));
|
|
|
|
let result = env.run(&["profile", "set", "benchmark", "overlay", "maybe"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("valid on/off value"));
|
|
|
|
for setting in [
|
|
"overlay",
|
|
"performance",
|
|
"steam-host-libs",
|
|
"game-libs",
|
|
"verbose",
|
|
"gamescope",
|
|
"gamescope-width",
|
|
"gamescope-height",
|
|
"gamescope-fps",
|
|
"fps-cap",
|
|
"vkbasalt",
|
|
"esync",
|
|
"fsync",
|
|
"large-address-aware",
|
|
"pre-launch",
|
|
"post-launch",
|
|
] {
|
|
let result = env.run(&["profile", "reset", "benchmark", setting]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Reset profile `benchmark` setting"));
|
|
}
|
|
|
|
let result = env.run(&["profile", "show", "benchmark"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("inherits = base"));
|
|
assert!(result.output.contains("overlay = (inherits: on)"));
|
|
assert!(result.output.contains("performance = (inherits: on)"));
|
|
assert!(result.output.contains("steam-host-libs = (inherits: on)"));
|
|
assert!(result.output.contains("game-libs = (inherits: keep)"));
|
|
assert!(result.output.contains("verbose = (inherits: on)"));
|
|
assert!(result.output.contains("gamescope = (inherits: on)"));
|
|
assert!(result.output.contains("gamescope-width = (inherits: 1920)"));
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("gamescope-height = (inherits: 1080)")
|
|
);
|
|
assert!(result.output.contains("gamescope-fps = (inherits: 60)"));
|
|
assert!(result.output.contains("fps-cap = (inherits: 60)"));
|
|
assert!(result.output.contains("vkbasalt = (inherits: on)"));
|
|
assert!(result.output.contains("esync = (inherits: off)"));
|
|
assert!(result.output.contains("fsync = (inherits: on)"));
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("large-address-aware = (inherits: on)")
|
|
);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("pre-launch = (inherits: printf default-pre)")
|
|
);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("post-launch = (inherits: printf default-post)")
|
|
);
|
|
|
|
let result = env.run(&["profile", "inherit", "benchmark-copy", "recording"]);
|
|
assert_ok(&result);
|
|
let result = env.run(&["profile", "show", "benchmark-copy"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("inherits = recording"));
|
|
|
|
let result = env.run(&["profile", "inherit", "benchmark-copy", "default"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("`default` is already the base"));
|
|
|
|
let result = env.run(&["profile", "inherit", "benchmark-copy", "missing"]);
|
|
assert_exit(&result, 3);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Parent profile `missing` does not exist.")
|
|
);
|
|
|
|
let result = env.run(&["profile", "inherit", "recording", "benchmark-copy"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("Profile inheritance cycle detected"));
|
|
|
|
let result = env.run(&["profile", "delete", "recording"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("Cannot delete profile `recording`"));
|
|
|
|
let result = env.run(&["profile", "clear-inherit", "benchmark-copy"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Cleared inherited parent for `benchmark-copy`.")
|
|
);
|
|
|
|
let result = env.run(&["profile", "show", "benchmark-copy"]);
|
|
assert_ok(&result);
|
|
assert!(!result.output.contains("inherits = "));
|
|
assert!(result.output.contains("overlay = off"));
|
|
|
|
let result = env.run(&["profile", "reset", "missing", "overlay"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("Profile `missing` does not exist."));
|
|
|
|
let result = env.run(&["profile", "show", "missing"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("does not exist"));
|
|
|
|
let export_base = env.path("exported-config");
|
|
let export_file = env.path("exported-config.gamewrap.toml");
|
|
let result = env.run(&["config", "export"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("kind = \"gamewrap-config\""));
|
|
assert!(result.output.contains("version = 1"));
|
|
assert!(result.output.contains("[defaults]"));
|
|
assert!(result.output.contains("overlay = true"));
|
|
assert!(result.output.contains("[profiles.base]"));
|
|
assert!(result.output.contains("[profiles.benchmark]"));
|
|
assert!(result.output.contains("[profiles.benchmark-copy]"));
|
|
|
|
let result = env.run(&["config", "export", export_base.to_str().expect("utf8 path")]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Exported config to"));
|
|
let exported = fs::read_to_string(&export_file).expect("export file");
|
|
assert!(exported.contains("kind = \"gamewrap-config\""));
|
|
assert!(exported.contains("[profiles.base]"));
|
|
assert!(exported.contains("[profiles.benchmark-copy]"));
|
|
|
|
let imported_env = TestEnv::new();
|
|
let result = imported_env.run(&["config", "import", export_base.to_str().expect("utf8 path")]);
|
|
assert_ok(&result);
|
|
let result = imported_env.run(&["config", "show"]);
|
|
assert_ok(&result);
|
|
assert!(!result.output.contains("(inherits:"));
|
|
assert!(result.output.contains("benchmark"));
|
|
assert!(result.output.contains("benchmark-copy"));
|
|
assert!(result.output.contains("overlay = on"));
|
|
|
|
let profile_export_base = env.path("benchmark");
|
|
let profile_export = env.path("benchmark.gamewrap-profile.toml");
|
|
let result = env.run(&[
|
|
"profile",
|
|
"export",
|
|
"benchmark",
|
|
profile_export_base.to_str().expect("utf8 path"),
|
|
]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Exported profile `benchmark`"));
|
|
let exported_profile = fs::read_to_string(&profile_export).expect("profile export");
|
|
assert!(exported_profile.contains("kind = \"gamewrap-profile\""));
|
|
assert!(exported_profile.contains("name = \"benchmark\""));
|
|
assert!(exported_profile.contains("[settings]"));
|
|
|
|
let imported_profile_env = TestEnv::new();
|
|
let result = imported_profile_env.run(&[
|
|
"profile",
|
|
"import",
|
|
profile_export_base.to_str().expect("utf8 path"),
|
|
]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Imported profile `benchmark`"));
|
|
let result = imported_profile_env.run(&["profile", "show", "benchmark"]);
|
|
assert_ok(&result);
|
|
assert!(!result.output.contains("inherits ="));
|
|
assert!(result.output.contains("overlay = on"));
|
|
|
|
let result = imported_profile_env.run(&[
|
|
"profile",
|
|
"import",
|
|
profile_export_base.to_str().expect("utf8 path"),
|
|
]);
|
|
assert_exit(&result, 3);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Profile `benchmark` already exists.")
|
|
);
|
|
|
|
let invalid_file = env.path("invalid.toml");
|
|
fs::write(&invalid_file, "not = [valid").expect("write invalid config");
|
|
let result = env.run(&[
|
|
"config",
|
|
"import",
|
|
invalid_file.to_str().expect("utf8 path"),
|
|
]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("is invalid"));
|
|
|
|
let invalid_profile = env.path("invalid.gamewrap-profile.toml");
|
|
fs::write(
|
|
&invalid_profile,
|
|
"kind = \"gamewrap-profile\"\nversion = 1\n",
|
|
)
|
|
.expect("write invalid profile");
|
|
let result = env.run(&[
|
|
"profile",
|
|
"import",
|
|
invalid_profile.to_str().expect("utf8 path"),
|
|
]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("Profile import file"));
|
|
}
|
|
|
|
#[test]
|
|
fn games_bindings_notes_and_filters_work() {
|
|
let env = TestEnv::new();
|
|
|
|
assert_ok(&env.run(&["config", "set", "overlay", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "performance", "off"]));
|
|
assert_ok(&env.run(&["profile", "create", "benchmark"]));
|
|
assert_ok(&env.run(&["profile", "create", "recording"]));
|
|
|
|
let result = env.run(&["game", "--help"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("bind"));
|
|
assert!(result.output.contains("unbind"));
|
|
assert!(result.output.contains("forget"));
|
|
assert!(result.output.contains("note"));
|
|
assert!(result.output.contains("clear-note"));
|
|
|
|
let result = env.run(&["game", "list"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("(no observed games yet)"));
|
|
|
|
let result = env.run(&["game", "show", "missing.exe"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("No observed game matched"));
|
|
|
|
let result = env.run(&["game", "bind", "Example.exe", "unknown"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("Profile `unknown` does not exist."));
|
|
|
|
let result = env.run_with_env(
|
|
&[
|
|
"/usr/bin/true",
|
|
"--",
|
|
"/usr/bin/true",
|
|
"waitforexitandrun",
|
|
"/games/Grind Survivors/GrindSurvivors.exe",
|
|
],
|
|
&[("SteamAppId", "12345")],
|
|
);
|
|
assert_ok(&result);
|
|
|
|
let result = env.run_with_env(
|
|
&[
|
|
"/usr/bin/true",
|
|
"--",
|
|
"/usr/bin/true",
|
|
"waitforexitandrun",
|
|
"/games/StarRupture/StarRuptureGameSteam.exe",
|
|
],
|
|
&[("SteamAppId", "67890")],
|
|
);
|
|
assert_ok(&result);
|
|
|
|
let result = env.run(&["game", "list"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("GrindSurvivors.exe"));
|
|
assert!(result.output.contains("StarRuptureGameSteam.exe"));
|
|
assert!(result.output.contains("default"));
|
|
|
|
let result = env.run(&["last"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Last played: StarRuptureGameSteam.exe")
|
|
);
|
|
assert!(result.output.contains("Launches: 1"));
|
|
assert!(result.output.contains("Last launch: 20"));
|
|
|
|
let result = env.run(&["game", "list", "grind"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("GrindSurvivors.exe"));
|
|
assert!(!result.output.contains("StarRuptureGameSteam.exe"));
|
|
|
|
let result = env.run(&["game", "show", "starrupturegamesteam.exe"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Executable: StarRuptureGameSteam.exe")
|
|
);
|
|
assert!(result.output.contains("Launch count: 1"));
|
|
assert!(result.output.contains("Last launched: 20"));
|
|
|
|
let result = env.run(&["game", "show", "Grind Survivors"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("/games/Grind Survivors/GrindSurvivors.exe")
|
|
);
|
|
|
|
let result = env.run(&["game", "rename", "StarRuptureGameSteam.exe", "Star Rupture"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Renamed `StarRuptureGameSteam.exe` to `Star Rupture`.")
|
|
);
|
|
|
|
let result = env.run(&["game", "list"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Star Rupture"));
|
|
assert!(!result.output.contains("StarRuptureGameSteam.exe default"));
|
|
|
|
let result = env.run(&["game", "show", "StarRuptureGameSteam.exe"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Display name: Star Rupture"));
|
|
|
|
let result = env.run(&["game", "bind", "StarRuptureGameSteam.exe", "benchmark"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Bound `StarRuptureGameSteam.exe` to profile `benchmark`.")
|
|
);
|
|
|
|
let result = env.run(&[
|
|
"game",
|
|
"bind",
|
|
"/games/Grind Survivors/GrindSurvivors.exe",
|
|
"recording",
|
|
]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Bound `/games/Grind Survivors/GrindSurvivors.exe` to profile `recording`.")
|
|
);
|
|
|
|
let result = env.run(&["game", "list"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Game"));
|
|
assert!(result.output.contains("Profile"));
|
|
assert!(result.output.contains("Path"));
|
|
assert!(result.output.contains("benchmark"));
|
|
assert!(result.output.contains("recording"));
|
|
|
|
let result = env.run(&["game", "show", "StarRuptureGameSteam.exe"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Resolved profile: benchmark"));
|
|
assert!(result.output.contains("Last launched profile: default"));
|
|
|
|
let result = env.run(&[
|
|
"game",
|
|
"note",
|
|
"StarRuptureGameSteam.exe",
|
|
"needs",
|
|
"game-libs",
|
|
"gamemode",
|
|
]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Saved note for `StarRuptureGameSteam.exe`.")
|
|
);
|
|
|
|
let result = env.run(&["game", "show", "StarRuptureGameSteam.exe"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Note: needs game-libs gamemode"));
|
|
|
|
let result = env.run(&["status"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("note: needs game-libs gamemode"));
|
|
|
|
let result = env.run(&["config", "show"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("StarRuptureGameSteam.exe -> benchmark")
|
|
);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("/games/Grind Survivors/GrindSurvivors.exe -> recording")
|
|
);
|
|
|
|
let result = env.run(&["game", "unbind", "starrupture"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Removed binding for `starrupture`."));
|
|
|
|
let result = env.run(&["config", "show"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
!result
|
|
.output
|
|
.contains("StarRuptureGameSteam.exe -> benchmark")
|
|
);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("/games/Grind Survivors/GrindSurvivors.exe -> recording")
|
|
);
|
|
|
|
let result = env.run(&["game", "list", "star"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("StarRuptureGameSteam.exe"));
|
|
assert!(result.output.contains("default"));
|
|
assert!(!result.output.contains("GrindSurvivors.exe"));
|
|
|
|
let result = env.run(&["game", "clear-note", "StarRuptureGameSteam.exe"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Cleared note for `StarRuptureGameSteam.exe`.")
|
|
);
|
|
|
|
let result = env.run(&["game", "show", "StarRuptureGameSteam.exe"]);
|
|
assert_ok(&result);
|
|
assert!(!result.output.contains("Note:"));
|
|
|
|
let result = env.run(&["game", "forget", "StarRuptureGameSteam.exe"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Removed `StarRuptureGameSteam.exe` from observed games.")
|
|
);
|
|
|
|
let result = env.run(&["game", "show", "StarRuptureGameSteam.exe"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("No observed game matched"));
|
|
|
|
let result = env.run(&["game", "list", "star"]);
|
|
assert_ok(&result);
|
|
assert!(!result.output.contains("StarRuptureGameSteam.exe"));
|
|
|
|
let result = env.run(&["game", "unbind", "StarRuptureGameSteam.exe"]);
|
|
assert_exit(&result, 3);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("No binding exists for `StarRuptureGameSteam.exe`.")
|
|
);
|
|
|
|
let result = env.run(&["profile", "delete", "benchmark"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Deleted profile `benchmark` and removed bindings that pointed to it.")
|
|
);
|
|
|
|
let result = env.run(&["game", "bind", "StarRuptureGameSteam.exe", "benchmark"]);
|
|
assert_exit(&result, 3);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Profile `benchmark` does not exist.")
|
|
);
|
|
|
|
let result = env.run(&["profile", "delete", "recording"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Deleted profile `recording` and removed bindings that pointed to it.")
|
|
);
|
|
|
|
let result = env.run(&["profile", "list"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("No profiles configured."));
|
|
|
|
let result = env.run(&["config", "show"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("[bindings]"));
|
|
assert!(result.output.contains("(none)"));
|
|
}
|
|
|
|
#[test]
|
|
fn subcommand_typos_and_extra_args_fail_cleanly() {
|
|
let env = TestEnv::new();
|
|
|
|
for args in [&["game"][..], &["profile"][..], &["config"][..]] {
|
|
let result = env.run(args);
|
|
assert_exit(&result, 2);
|
|
assert!(result.output.contains("Usage: gamewrap"));
|
|
}
|
|
|
|
let result = env.run(&["game", "shwo", "StarRuptureGameSteam.exe"]);
|
|
assert_exit(&result, 2);
|
|
assert!(result.output.contains("unrecognized subcommand 'shwo'"));
|
|
|
|
let result = env.run(&["profile", "crtate", "foo"]);
|
|
assert_exit(&result, 2);
|
|
assert!(result.output.contains("unrecognized subcommand 'crtate'"));
|
|
|
|
let result = env.run(&["config", "sett", "overlay", "on"]);
|
|
assert_exit(&result, 2);
|
|
assert!(result.output.contains("unrecognized subcommand 'sett'"));
|
|
|
|
let result = env.run(&["status", "now"]);
|
|
assert_exit(&result, 2);
|
|
assert!(result.output.contains("unexpected argument 'now' found"));
|
|
}
|
|
|
|
#[test]
|
|
fn post_launch_hook_runs_after_game_exits() {
|
|
let env = TestEnv::new();
|
|
|
|
// Disable overlay and performance so we don't need mangohud/gamemoderun.
|
|
assert_ok(&env.run(&["config", "set", "overlay", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "performance", "off"]));
|
|
|
|
// Set a post-launch hook that prints a distinctive string to stdout.
|
|
assert_ok(&env.run(&["config", "set", "post-launch", "printf POSTHOOK_RAN"]));
|
|
|
|
// Run a real command (true exits immediately).
|
|
let result = env.run(&["run", "/usr/bin/true"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result.output.contains("POSTHOOK_RAN"),
|
|
"post-launch hook did not run; output was:\n{}",
|
|
result.output
|
|
);
|
|
|
|
// dry-run should show the post-launch hook in preview.
|
|
let result = env.run(&["dry-run", "/usr/bin/true"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Post-launch hook (runs after game exits):")
|
|
);
|
|
assert!(result.output.contains("printf POSTHOOK_RAN"));
|
|
}
|
|
|
|
#[test]
|
|
fn env_vars_appear_in_verbose_dry_run() {
|
|
let env = TestEnv::new();
|
|
|
|
assert_ok(&env.run(&["config", "set", "overlay", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "performance", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "verbose", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "vkbasalt", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "esync", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "fsync", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "large-address-aware", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "fps-cap", "60"]));
|
|
|
|
let fake_path = env.path_with_fake_bins(&["mangohud"]);
|
|
let result = env.run_with_env(&["dry-run", "/usr/bin/true"], &[("PATH", &fake_path)]);
|
|
assert_ok(&result);
|
|
|
|
// Verbose mode shows environment changes.
|
|
assert!(result.output.contains("Environment changes:"));
|
|
assert!(result.output.contains("ENABLE_VKBASALT=1"));
|
|
assert!(result.output.contains("PROTON_NO_ESYNC=0"));
|
|
assert!(result.output.contains("PROTON_NO_FSYNC=1"));
|
|
assert!(result.output.contains("PROTON_LARGE_ADDRESS_AWARE=1"));
|
|
assert!(result.output.contains("MANGOHUD_PARAMS=fps_limit=60"));
|
|
|
|
// Final command should have mangohud prefix.
|
|
assert!(result.output.contains("mangohud /usr/bin/true"));
|
|
}
|
|
|
|
#[test]
|
|
fn launch_count_and_playtime_tracked() {
|
|
let env = TestEnv::new();
|
|
|
|
assert_ok(&env.run(&["config", "set", "overlay", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "performance", "off"]));
|
|
|
|
// Simulate two Steam launches of the same game.
|
|
for _ in 0..2 {
|
|
assert_ok(&env.run_with_env(
|
|
&[
|
|
"/usr/bin/true",
|
|
"--",
|
|
"/usr/bin/true",
|
|
"waitforexitandrun",
|
|
"/games/TestGame/TestGame.exe",
|
|
],
|
|
&[("SteamAppId", "99999")],
|
|
));
|
|
}
|
|
|
|
// game show should show launch count of 2.
|
|
let result = env.run(&["game", "show", "TestGame.exe"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Launch count: 2"));
|
|
assert!(result.output.contains("Last launched: 20"));
|
|
|
|
// gamewrap last should show it.
|
|
let result = env.run(&["last"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Last played: TestGame.exe"));
|
|
assert!(result.output.contains("Launches: 2"));
|
|
}
|