76ab5351a9
Groups added: - Help topics (all valid topics, bad topic, bad settings group filter) - version/help flags - status command - All 16 toggle settings reject invalid values (maybe/yes/1/ON/empty) - All 6 enum settings reject bad values - 14 numeric bad-value cases (fps=0, sharpness=21, abc, negatives, floats) - 5 numeric boundary-valid cases (sharpness 0/20, fps 1, native, pixels) - Unknown/wrong-case setting names rejected - Alias round-trips: host-libs → steam-host-libs, laa → large-address-aware - config reset edge cases (no args, --all conflict, unknown name) - config export to stdout and to file (sparse) - config import error cases (missing file, bad TOML, missing-profile binding) - profile reserved name "default" rejected in create/export/import - profile duplicate edge cases (nonexistent src, existing dest, reserved dest, valid copy) - profile import error cases (missing file, duplicate name) - profile delete nonexistent - profile reset edge cases (no-op on unset field, nonexistent profile) - profile env edge cases (unset missing key, list/clear when empty) - game command error cases (show/unbind/forget/rename nonexistent, bind to missing profile) - game list empty state - game note multiword - game list --full flag - dry-run with nonexistent binary (exit 4) - dry-run shows gamescope flags in plan - launch with option-like target rejected (exit 2) - launch with no command - pre-hook fail mode does NOT trigger post-hook - both hooks run in order on success - post-hook runs when binary is missing (fixed bug) - missing dependency errors for mangohud/gamemoderun/gamescope/mangoapp - last command with no history - last shows most recently launched game - config settings / profile settings list all names - completion edge cases (no args, bash/zsh/fish output) Co-Authored-By: claude-flow <ruv@ruv.net>
3040 lines
100 KiB
Rust
3040 lines
100 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,
|
|
stdout: 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)
|
|
.current_dir(&self.home)
|
|
.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")
|
|
.env_remove("DISPLAY")
|
|
.env_remove("WAYLAND_DISPLAY")
|
|
.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,
|
|
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
|
|
}
|
|
}
|
|
|
|
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 {
|
|
create_fake_bin(&bin_dir.join(name));
|
|
}
|
|
|
|
let current_path = std::env::var_os("PATH").unwrap_or_default();
|
|
format!("{}:{}", bin_dir.display(), current_path.to_string_lossy())
|
|
}
|
|
|
|
// Like path_with_fake_bins but returns ONLY the fake bin dir, not prepended to system PATH.
|
|
// Use this for "missing binary" dependency tests where the real binary may be installed.
|
|
fn path_isolated_fake_bins(&self, names: &[&str]) -> String {
|
|
let bin_dir = self.home.join("bin-isolated");
|
|
fs::create_dir_all(&bin_dir).expect("isolated bin dir");
|
|
for name in names {
|
|
create_fake_bin(&bin_dir.join(name));
|
|
}
|
|
bin_dir.display().to_string()
|
|
}
|
|
}
|
|
|
|
fn create_fake_bin(path: &std::path::Path) {
|
|
fs::write(path, "#!/bin/sh\nexit 0\n").expect("write fake bin");
|
|
let mut perms = fs::metadata(path).expect("fake bin metadata").permissions();
|
|
perms.set_mode(0o755);
|
|
fs::set_permissions(path, perms).expect("chmod fake bin");
|
|
}
|
|
|
|
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(&["help", "settings"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("── MangoHud"));
|
|
assert!(result.output.contains("── Gamescope"));
|
|
|
|
let result = env.run(&["help", "settings", "gamescope"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Gamescope"));
|
|
assert!(result.output.contains("gamescope-filter"));
|
|
assert!(!result.output.contains("MangoHud"));
|
|
|
|
let result = env.run(&["help", "settings", "mangohud"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("MangoHud"));
|
|
assert!(result.output.contains("mangohud"));
|
|
assert!(!result.output.contains("Gamescope"));
|
|
|
|
let result = env.run(&["help", "settings", "unknowngroup"]);
|
|
assert_exit(&result, 2);
|
|
assert!(result.output.contains("not a known settings group"));
|
|
|
|
for retired_setting in ["overlay", "performance"] {
|
|
let result = env.run(&["config", "set", retired_setting, "on"]);
|
|
assert_exit(&result, 3);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains(&format!("`{retired_setting}` is not a known setting"))
|
|
);
|
|
}
|
|
|
|
let result = env.run(&["config", "settings"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("mangohud"));
|
|
assert!(result.output.contains("gamemode"));
|
|
assert!(result.output.contains("gamescope-filter"));
|
|
assert!(result.output.contains("gamescope-mangoapp"));
|
|
assert!(result.output.contains("gamewrap help settings"));
|
|
|
|
let result = env.run(&["config", "show", "--effective"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("gamescope-fps = (not set — unlimited)")
|
|
);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("gamescope-filter = (not set — linear)")
|
|
);
|
|
|
|
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_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", "--effective"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("mangohud = on"));
|
|
|
|
let result = env.run_with_env(&["config", "edit"], &[("EDITOR", "true")]);
|
|
assert_ok(&result);
|
|
assert!(result.output.is_empty());
|
|
|
|
for (setting, value) in [
|
|
("mangohud", "off"),
|
|
("gamemode", "off"),
|
|
("steam-host-libs", "off"),
|
|
("host-libs", "on"),
|
|
("game-libs", "keep"),
|
|
("verbose", "on"),
|
|
("gamescope", "on"),
|
|
("gamescope-width", "1920"),
|
|
("gamescope-height", "1080"),
|
|
("gamescope-fps", "60"),
|
|
("gamescope-nested-width", "1280"),
|
|
("gamescope-nested-height", "720"),
|
|
("gamescope-unfocused-fps", "15"),
|
|
("gamescope-filter", "fsr"),
|
|
("gamescope-scaler", "fit"),
|
|
("gamescope-sharpness", "5"),
|
|
("gamescope-mode", "fullscreen"),
|
|
("gamescope-adaptive-sync", "on"),
|
|
("gamescope-hdr", "on"),
|
|
("gamescope-steam", "on"),
|
|
("gamescope-expose-wayland", "on"),
|
|
("gamescope-mangoapp", "on"),
|
|
("fps-cap", "60"),
|
|
("vkbasalt", "on"),
|
|
("vkbasalt-config", "/tmp/default-vkbasalt.conf"),
|
|
("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("mangohud = off"));
|
|
assert!(result.output.contains("gamemode = 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("gamescope-nested-width = 1280"));
|
|
assert!(result.output.contains("gamescope-nested-height = 720"));
|
|
assert!(result.output.contains("gamescope-unfocused-fps = 15"));
|
|
assert!(result.output.contains("gamescope-filter = fsr"));
|
|
assert!(result.output.contains("gamescope-scaler = fit"));
|
|
assert!(result.output.contains("gamescope-sharpness = 5"));
|
|
assert!(result.output.contains("gamescope-mode = fullscreen"));
|
|
assert!(result.output.contains("gamescope-adaptive-sync = on"));
|
|
assert!(result.output.contains("gamescope-hdr = on"));
|
|
assert!(result.output.contains("gamescope-steam = on"));
|
|
assert!(result.output.contains("gamescope-expose-wayland = on"));
|
|
assert!(result.output.contains("gamescope-mangoapp = on"));
|
|
assert!(result.output.contains("fps-cap = 60"));
|
|
assert!(result.output.contains("vkbasalt = on"));
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("vkbasalt-config = /tmp/default-vkbasalt.conf")
|
|
);
|
|
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", "mangohud", "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 resolution"));
|
|
|
|
let result = env.run(&["config", "set", "gamescope-mode", "maximized"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("valid window mode"));
|
|
|
|
assert_ok(&env.run(&["config", "set", "gamescope-width", "native"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-height", "native"]));
|
|
let result = env.run(&["config", "show"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("gamescope-width = native"));
|
|
assert!(result.output.contains("gamescope-height = native"));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-width", "1920"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-height", "1080"]));
|
|
|
|
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", "mangohud"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Reset default setting `mangohud`."));
|
|
|
|
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("reset"));
|
|
assert!(result.output.contains("duplicate"));
|
|
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", "--effective"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("mangohud = (default: on)"));
|
|
assert!(result.output.contains("gamemode = (default: off)"));
|
|
assert!(result.output.contains("gamescope-fps = (default: 60)"));
|
|
assert!(result.output.contains("gamescope-filter = (default: fsr)"));
|
|
|
|
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"));
|
|
|
|
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("(no env vars set on this profile)"));
|
|
|
|
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 (1 binding)"));
|
|
let result = env.run(&["game", "unbind", "Demo.exe"]);
|
|
assert_ok(&result);
|
|
|
|
for (setting, value) in [
|
|
("mangohud", "off"),
|
|
("gamemode", "on"),
|
|
("steam-host-libs", "off"),
|
|
("game-libs", "gamemode"),
|
|
("verbose", "off"),
|
|
("gamescope", "on"),
|
|
("gamescope-width", "2560"),
|
|
("gamescope-height", "1440"),
|
|
("gamescope-fps", "120"),
|
|
("gamescope-nested-width", "1920"),
|
|
("gamescope-nested-height", "1080"),
|
|
("gamescope-unfocused-fps", "30"),
|
|
("gamescope-filter", "nis"),
|
|
("gamescope-scaler", "fill"),
|
|
("gamescope-sharpness", "10"),
|
|
("gamescope-mode", "borderless"),
|
|
("gamescope-adaptive-sync", "on"),
|
|
("gamescope-hdr", "off"),
|
|
("gamescope-steam", "on"),
|
|
("gamescope-expose-wayland", "off"),
|
|
("gamescope-mangoapp", "on"),
|
|
("fps-cap", "120"),
|
|
("vkbasalt", "off"),
|
|
("vkbasalt-config", "/tmp/profile-vkbasalt.conf"),
|
|
("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("mangohud = off"));
|
|
assert!(result.output.contains("gamemode = 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("gamescope-nested-width = 1920"));
|
|
assert!(result.output.contains("gamescope-nested-height = 1080"));
|
|
assert!(result.output.contains("gamescope-unfocused-fps = 30"));
|
|
assert!(result.output.contains("gamescope-filter = nis"));
|
|
assert!(result.output.contains("gamescope-scaler = fill"));
|
|
assert!(result.output.contains("gamescope-sharpness = 10"));
|
|
assert!(result.output.contains("gamescope-mode = borderless"));
|
|
assert!(result.output.contains("gamescope-adaptive-sync = on"));
|
|
assert!(result.output.contains("gamescope-steam = on"));
|
|
assert!(result.output.contains("gamescope-mangoapp = on"));
|
|
assert!(result.output.contains("fps-cap = 120"));
|
|
assert!(result.output.contains("vkbasalt = off"));
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("vkbasalt-config = /tmp/profile-vkbasalt.conf")
|
|
);
|
|
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("mangohud = 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", "mangohud", "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", "mangohud", "maybe"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("valid on/off value"));
|
|
|
|
for setting in [
|
|
"mangohud",
|
|
"gamemode",
|
|
"steam-host-libs",
|
|
"game-libs",
|
|
"verbose",
|
|
"gamescope",
|
|
"gamescope-width",
|
|
"gamescope-height",
|
|
"gamescope-fps",
|
|
"gamescope-nested-width",
|
|
"gamescope-nested-height",
|
|
"gamescope-unfocused-fps",
|
|
"gamescope-filter",
|
|
"gamescope-scaler",
|
|
"gamescope-sharpness",
|
|
"gamescope-mode",
|
|
"gamescope-adaptive-sync",
|
|
"gamescope-hdr",
|
|
"gamescope-steam",
|
|
"gamescope-expose-wayland",
|
|
"gamescope-mangoapp",
|
|
"fps-cap",
|
|
"vkbasalt",
|
|
"vkbasalt-config",
|
|
"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", "--effective"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("mangohud = (default: on)"));
|
|
assert!(result.output.contains("gamemode = (default: off)"));
|
|
assert!(result.output.contains("steam-host-libs = (default: on)"));
|
|
assert!(result.output.contains("game-libs = (default: keep)"));
|
|
assert!(result.output.contains("verbose = (default: on)"));
|
|
assert!(result.output.contains("gamescope = (default: on)"));
|
|
assert!(result.output.contains("gamescope-width = (default: 1920)"));
|
|
assert!(result.output.contains("gamescope-height = (default: 1080)"));
|
|
assert!(result.output.contains("gamescope-fps = (default: 60)"));
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("gamescope-mode = (default: fullscreen)")
|
|
);
|
|
assert!(result.output.contains("fps-cap = (default: 60)"));
|
|
assert!(result.output.contains("vkbasalt = (default: on)"));
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("vkbasalt-config = (default: /tmp/default-vkbasalt.conf)")
|
|
);
|
|
assert!(result.output.contains("esync = (default: off)"));
|
|
assert!(result.output.contains("fsync = (default: on)"));
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("large-address-aware = (default: on)")
|
|
);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("pre-launch = (default: printf default-pre)")
|
|
);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("post-launch = (default: printf default-post)")
|
|
);
|
|
|
|
let result = env.run(&["profile", "reset", "missing", "mangohud"]);
|
|
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");
|
|
// no-path export writes config.gamewrap.toml in cwd (self.home via current_dir)
|
|
let default_export_file = env.path("config.gamewrap.toml");
|
|
let result = env.run(&["config", "export"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Exported config to"));
|
|
let default_exported = fs::read_to_string(&default_export_file).expect("default export file");
|
|
assert!(default_exported.contains("kind = \"gamewrap-config\""));
|
|
assert!(default_exported.contains("version = 1"));
|
|
assert!(default_exported.contains("[defaults]"));
|
|
assert!(default_exported.contains("[profiles.base]"));
|
|
assert!(default_exported.contains("[profiles.benchmark]"));
|
|
assert!(default_exported.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("benchmark"));
|
|
assert!(result.output.contains("benchmark-copy"));
|
|
|
|
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("(no settings configured for this profile)")
|
|
);
|
|
|
|
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 profile_export_is_sparse() {
|
|
let env = TestEnv::new();
|
|
assert_ok(&env.run(&["profile", "create", "benchmark"]));
|
|
assert_ok(&env.run(&["profile", "set", "benchmark", "gamescope", "on"]));
|
|
assert_ok(&env.run(&["profile", "set", "benchmark", "gamescope-width", "1920"]));
|
|
|
|
let export_base = env.path("sparse-benchmark");
|
|
let result = env.run(&[
|
|
"profile",
|
|
"export",
|
|
"benchmark",
|
|
export_base.to_str().expect("utf8 path"),
|
|
]);
|
|
assert_ok(&result);
|
|
|
|
let exported = fs::read_to_string(env.path("sparse-benchmark.gamewrap-profile.toml"))
|
|
.expect("profile export");
|
|
assert!(exported.contains("gamescope = true"));
|
|
assert!(exported.contains("gamescope_width = 1920"));
|
|
assert!(!exported.contains("mangohud"));
|
|
assert!(!exported.contains("gamemode"));
|
|
}
|
|
|
|
#[test]
|
|
fn config_reset_all_restores_built_in_defaults() {
|
|
let env = TestEnv::new();
|
|
|
|
assert_ok(&env.run(&["config", "set", "mangohud", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "fps-cap", "60"]));
|
|
|
|
let result = env.run(&["config", "reset"]);
|
|
assert_exit(&result, 2);
|
|
|
|
let result = env.run(&["config", "reset", "--all"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Reset all default settings to built-in defaults.")
|
|
);
|
|
|
|
let result = env.run(&["config", "show", "--effective"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("mangohud = on"));
|
|
assert!(result.output.contains("fps-cap = (not set — no cap)"));
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("gamescope-width = (not set — gamescope default: 1280)")
|
|
);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("vkbasalt-config = (not set — ~/.config/vkBasalt/vkBasalt.conf)")
|
|
);
|
|
assert!(result.output.contains("esync = (not set — Proton default)"));
|
|
}
|
|
|
|
#[test]
|
|
fn games_bindings_notes_and_filters_work() {
|
|
let env = TestEnv::new();
|
|
|
|
assert_ok(&env.run(&["config", "set", "mangohud", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "gamemode", "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("Set 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", "mangohud", "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 mangohud and gamemode so we don't need mangohud/gamemoderun.
|
|
assert_ok(&env.run(&["config", "set", "mangohud", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "gamemode", "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 launch_decision_log_records_hooks_exit_and_playtime() {
|
|
let env = TestEnv::new();
|
|
let log_path = env.state_home.join("custom/gamewrap.log");
|
|
let log_path_str = log_path.to_string_lossy().into_owned();
|
|
|
|
assert_ok(&env.run(&["config", "set", "mangohud", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "gamemode", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "log-file", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "log-path", &log_path_str]));
|
|
assert_ok(&env.run(&["config", "set", "pre-launch", "exit 3"]));
|
|
assert_ok(&env.run(&["config", "set", "post-launch", "exit 4"]));
|
|
|
|
let result = env.run(&["run", "/bin/sh", "-c", "exit 7"]);
|
|
assert_exit(&result, 7);
|
|
|
|
let log = fs::read_to_string(log_path).expect("decision log");
|
|
assert!(log.contains("--- launch ---"));
|
|
assert!(log.contains("executable: sh"));
|
|
assert!(log.contains("profile: default"));
|
|
assert!(log.contains("command: /bin/sh -c exit 7"));
|
|
assert!(log.contains("pre-launch: exit 3 → exit 3"));
|
|
assert!(log.contains("exit: 7"));
|
|
assert!(log.contains("playtime: 0s"));
|
|
assert!(log.contains("post-launch: exit 4 → exit 4"));
|
|
}
|
|
|
|
#[test]
|
|
fn logging_settings_round_trip_and_validate_values() {
|
|
let env = TestEnv::new();
|
|
|
|
for (setting, value) in [
|
|
("log-file", "on"),
|
|
("log-path", "/tmp/gamewrap.log"),
|
|
("mangohud-log", "on"),
|
|
("mangohud-log-path", "/tmp/mangohud.csv"),
|
|
("vkbasalt-log-level", "warning"),
|
|
] {
|
|
assert_ok(&env.run(&["config", "set", setting, value]));
|
|
}
|
|
|
|
let result = env.run(&["config", "show"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("log-file = on"));
|
|
assert!(result.output.contains("log-path = /tmp/gamewrap.log"));
|
|
assert!(result.output.contains("mangohud-log = on"));
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("mangohud-log-path = /tmp/mangohud.csv")
|
|
);
|
|
assert!(result.output.contains("vkbasalt-log-level = warning"));
|
|
|
|
let invalid = env.run(&["config", "set", "vkbasalt-log-level", "warn"]);
|
|
assert_exit(&invalid, 3);
|
|
assert!(invalid.output.contains("debug, info, warning, error, none"));
|
|
}
|
|
|
|
#[test]
|
|
fn env_vars_appear_in_verbose_dry_run() {
|
|
let env = TestEnv::new();
|
|
|
|
assert_ok(&env.run(&["config", "set", "mangohud", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "gamemode", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "verbose", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "vkbasalt", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "vkbasalt-config", "/tmp/vkbasalt.conf"]));
|
|
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("VKBASALT_CONFIG_FILE=/tmp/vkbasalt.conf")
|
|
);
|
|
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 gamescope_extended_settings_and_mangoapp_work() {
|
|
let env = TestEnv::new();
|
|
|
|
// Set up: gamescope on, full output + nested res, filter, scaler, sharpness.
|
|
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-nested-width", "1280"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-nested-height", "720"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-fps", "60"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-unfocused-fps", "15"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-filter", "fsr"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-scaler", "fit"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-sharpness", "5"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-mode", "fullscreen"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-adaptive-sync", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-hdr", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-steam", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-expose-wayland", "on"]));
|
|
|
|
// Verify config show reflects the new keys.
|
|
let result = env.run(&["config", "show"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("gamescope-nested-width = 1280"));
|
|
assert!(result.output.contains("gamescope-nested-height = 720"));
|
|
assert!(result.output.contains("gamescope-unfocused-fps = 15"));
|
|
assert!(result.output.contains("gamescope-filter = fsr"));
|
|
assert!(result.output.contains("gamescope-scaler = fit"));
|
|
assert!(result.output.contains("gamescope-sharpness = 5"));
|
|
assert!(result.output.contains("gamescope-mode = fullscreen"));
|
|
assert!(result.output.contains("gamescope-adaptive-sync = on"));
|
|
assert!(result.output.contains("gamescope-hdr = on"));
|
|
assert!(result.output.contains("gamescope-steam = on"));
|
|
assert!(result.output.contains("gamescope-expose-wayland = on"));
|
|
|
|
// Dry-run: mangohud off — all new flags should appear in gamescope args.
|
|
assert_ok(&env.run(&["config", "set", "mangohud", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "gamemode", "off"]));
|
|
let fake_path = env.path_with_fake_bins(&["gamescope"]);
|
|
let result = env.run_with_env(&["dry-run", "/usr/bin/true"], &[("PATH", &fake_path)]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("-W 1920"));
|
|
assert!(result.output.contains("-H 1080"));
|
|
assert!(result.output.contains("-w 1280"));
|
|
assert!(result.output.contains("-h 720"));
|
|
assert!(result.output.contains("-r 60"));
|
|
assert!(result.output.contains("-o 15"));
|
|
assert!(result.output.contains("-F fsr"));
|
|
assert!(result.output.contains("-S fit"));
|
|
assert!(result.output.contains("--sharpness 5"));
|
|
assert!(result.output.contains("-f"));
|
|
assert!(result.output.contains("--adaptive-sync"));
|
|
assert!(result.output.contains("--hdr-enabled"));
|
|
assert!(result.output.contains("-e"));
|
|
assert!(result.output.contains("--expose-wayland"));
|
|
// No mangoapp flag since gamescope-mangoapp is off.
|
|
assert!(!result.output.contains("--mangoapp"));
|
|
|
|
// Validation: invalid filter value rejected.
|
|
let result = env.run(&["config", "set", "gamescope-filter", "magic"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("not a valid gamescope filter"));
|
|
|
|
// Validation: invalid scaler value rejected.
|
|
let result = env.run(&["config", "set", "gamescope-scaler", "zoom"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("not a valid gamescope scaler"));
|
|
|
|
// Validation: sharpness out of range rejected.
|
|
let result = env.run(&["config", "set", "gamescope-sharpness", "25"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("out of range"));
|
|
|
|
// mangoapp ON + mangohud ON: --mangoapp in gamescope args, no mangohud prefix.
|
|
assert_ok(&env.run(&["config", "set", "mangohud", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-mangoapp", "on"]));
|
|
let fake_path = env.path_with_fake_bins(&["gamescope", "mangoapp"]);
|
|
let result = env.run_with_env(&["dry-run", "/usr/bin/true"], &[("PATH", &fake_path)]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("--mangoapp"));
|
|
assert!(!result.output.contains("mangohud"));
|
|
|
|
// mangoapp OFF + mangohud ON + gamescope ON: mangohud prefix inside gamescope.
|
|
assert_ok(&env.run(&["config", "set", "gamescope-mangoapp", "off"]));
|
|
let fake_path = env.path_with_fake_bins(&["gamescope", "mangohud"]);
|
|
let result = env.run_with_env(&["dry-run", "/usr/bin/true"], &[("PATH", &fake_path)]);
|
|
assert_ok(&result);
|
|
assert!(!result.output.contains("--mangoapp"));
|
|
assert!(result.output.contains("-- mangohud"));
|
|
|
|
// Doctor: warns about mangohud prefix inside gamescope when mangoapp is off.
|
|
let result = env.run_with_env(&["doctor"], &[("PATH", &fake_path)]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("gamescope overlay"));
|
|
|
|
// mangoapp dependency error when mangoapp binary is missing.
|
|
assert_ok(&env.run(&["config", "set", "gamescope-mangoapp", "on"]));
|
|
// Use isolated PATH so the system's real mangoapp (from mangohud package) can't be found.
|
|
let fake_path_no_mangoapp = env.path_isolated_fake_bins(&["gamescope"]);
|
|
let result = env.run_with_env(
|
|
&["dry-run", "/usr/bin/true"],
|
|
&[("PATH", &fake_path_no_mangoapp)],
|
|
);
|
|
assert_exit(&result, 4);
|
|
assert!(result.output.contains("mangoapp"));
|
|
assert!(result.output.contains("mangohud"));
|
|
|
|
// Profile: gamescope-mangoapp and new keys round-trip through profile set/show.
|
|
assert_ok(&env.run(&["profile", "create", "gaming"]));
|
|
assert_ok(&env.run(&["profile", "set", "gaming", "gamescope-mangoapp", "on"]));
|
|
assert_ok(&env.run(&["profile", "set", "gaming", "gamescope-filter", "nis"]));
|
|
assert_ok(&env.run(&["profile", "set", "gaming", "gamescope-sharpness", "10"]));
|
|
assert_ok(&env.run(&["profile", "set", "gaming", "gamescope-adaptive-sync", "on"]));
|
|
assert_ok(&env.run(&["profile", "set", "gaming", "gamescope-hdr", "on"]));
|
|
assert_ok(&env.run(&["profile", "set", "gaming", "gamescope-nested-width", "2560"]));
|
|
assert_ok(&env.run(&[
|
|
"profile",
|
|
"set",
|
|
"gaming",
|
|
"gamescope-nested-height",
|
|
"1440",
|
|
]));
|
|
|
|
let result = env.run(&["profile", "show", "gaming"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("gamescope-mangoapp = on"));
|
|
assert!(result.output.contains("gamescope-filter = nis"));
|
|
assert!(result.output.contains("gamescope-sharpness = 10"));
|
|
assert!(result.output.contains("gamescope-adaptive-sync = on"));
|
|
assert!(result.output.contains("gamescope-hdr = on"));
|
|
assert!(result.output.contains("gamescope-nested-width = 2560"));
|
|
assert!(result.output.contains("gamescope-nested-height = 1440"));
|
|
|
|
// Reset new keys.
|
|
for setting in [
|
|
"gamescope-nested-width",
|
|
"gamescope-nested-height",
|
|
"gamescope-unfocused-fps",
|
|
"gamescope-filter",
|
|
"gamescope-scaler",
|
|
"gamescope-sharpness",
|
|
"gamescope-mode",
|
|
"gamescope-adaptive-sync",
|
|
"gamescope-hdr",
|
|
"gamescope-steam",
|
|
"gamescope-expose-wayland",
|
|
"gamescope-mangoapp",
|
|
] {
|
|
let result = env.run(&["profile", "reset", "gaming", setting]);
|
|
assert_ok(&result);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_show_modes() {
|
|
let env = TestEnv::new();
|
|
|
|
// Configured-only (default) with nothing set: show placeholder
|
|
let result = env.run(&["config", "show"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result.output.contains("no global settings configured"),
|
|
"expected placeholder for empty config, got: {}",
|
|
result.output
|
|
);
|
|
|
|
// Set a single value, then show: only that setting appears
|
|
assert_ok(&env.run(&["config", "set", "mangohud", "off"]));
|
|
let result = env.run(&["config", "show"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("mangohud = off"));
|
|
assert!(
|
|
!result.output.contains("gamemode"),
|
|
"configured mode should not show unset fields"
|
|
);
|
|
|
|
// --effective shows all settings including defaults
|
|
let result = env.run(&["config", "show", "--effective"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("mangohud = off"));
|
|
assert!(
|
|
result.output.contains("gamemode = on"),
|
|
"--effective should show default settings"
|
|
);
|
|
assert!(
|
|
result.output.contains("gamescope = off"),
|
|
"--effective should show all fields"
|
|
);
|
|
|
|
assert_ok(&env.run(&["profile", "create", "benchmark"]));
|
|
|
|
let result = env.run(&["config", "show"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("(no settings configured for this profile)")
|
|
);
|
|
assert!(!result.output.contains("(default:"));
|
|
|
|
let result = env.run(&["config", "show", "--effective"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("gamemode = (default: on)"));
|
|
|
|
let result = env.run(&["profile", "show", "benchmark"]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("(no settings configured for this profile)")
|
|
);
|
|
assert!(!result.output.contains("(default:"));
|
|
|
|
let result = env.run(&["profile", "show", "benchmark", "--effective"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("gamemode = (default: on)"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_auto_migrate() {
|
|
let env = TestEnv::new();
|
|
let config_dir = env.config_home.join("gamewrap");
|
|
let config_file = config_dir.join("config.toml");
|
|
fs::create_dir_all(&config_dir).expect("config dir");
|
|
fs::write(
|
|
&config_file,
|
|
"[defaults]\noverlay = true\nperformance = false\n",
|
|
)
|
|
.expect("write old config");
|
|
|
|
let result = env.run(&["config", "show"]);
|
|
assert_ok(&result);
|
|
|
|
let migrated = fs::read_to_string(config_file).expect("migrated config");
|
|
assert!(migrated.contains("mangohud = true"));
|
|
assert!(migrated.contains("gamemode = false"));
|
|
assert!(!migrated.contains("overlay"));
|
|
assert!(!migrated.contains("performance"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_migrate_command() {
|
|
let env = TestEnv::new();
|
|
let config_dir = env.config_home.join("gamewrap");
|
|
let config_file = config_dir.join("config.toml");
|
|
fs::create_dir_all(&config_dir).expect("config dir");
|
|
fs::write(
|
|
&config_file,
|
|
"[defaults]\noverlay = true\nperformance = false\nfps_cap = 60\n",
|
|
)
|
|
.expect("write old config");
|
|
|
|
let result = env.run(&["config", "migrate"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("Config migrated successfully."));
|
|
|
|
let migrated = fs::read_to_string(config_file).expect("migrated config");
|
|
assert!(migrated.contains("mangohud = true"));
|
|
assert!(migrated.contains("gamemode = false"));
|
|
assert!(migrated.contains("fps_cap = 60"));
|
|
assert!(!migrated.contains("overlay"));
|
|
assert!(!migrated.contains("performance"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_profile_migrate_command() {
|
|
let env = TestEnv::new();
|
|
let path = env.path("old-bench.gamewrap-profile.toml");
|
|
fs::write(
|
|
&path,
|
|
r#"kind = "gamewrap-profile"
|
|
version = 1
|
|
name = "old-bench"
|
|
|
|
[settings]
|
|
overlay = true
|
|
performance = false
|
|
fps_cap = 60
|
|
gamescope = false
|
|
"#,
|
|
)
|
|
.expect("write old profile");
|
|
|
|
let result = env.run(&["profile", "migrate", path.to_str().expect("utf8 path")]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
result
|
|
.output
|
|
.contains("Profile export migrated successfully.")
|
|
);
|
|
|
|
let migrated = fs::read_to_string(path).expect("migrated profile");
|
|
assert!(migrated.contains("mangohud = true"));
|
|
assert!(migrated.contains("gamemode = false"));
|
|
assert!(migrated.contains("fps_cap = 60"));
|
|
assert!(!migrated.contains("overlay"));
|
|
assert!(!migrated.contains("performance"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_profile_migrate_dry_run() {
|
|
let env = TestEnv::new();
|
|
let path = env.path("dry-run.gamewrap-profile.toml");
|
|
let original = r#"kind = "gamewrap-profile"
|
|
version = 1
|
|
name = "old-bench"
|
|
|
|
[settings]
|
|
overlay = true
|
|
performance = false
|
|
"#;
|
|
fs::write(&path, original).expect("write old profile");
|
|
|
|
let result = env.run(&[
|
|
"profile",
|
|
"migrate",
|
|
"--dry-run",
|
|
path.to_str().expect("utf8 path"),
|
|
]);
|
|
assert_ok(&result);
|
|
assert!(result.stdout.contains("mangohud = true"));
|
|
assert!(result.stdout.contains("gamemode = false"));
|
|
assert_eq!(
|
|
fs::read_to_string(path).expect("original profile"),
|
|
original
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_profile_import_warns_on_old_keys() {
|
|
let env = TestEnv::new();
|
|
let path = env.path("old-import.gamewrap-profile.toml");
|
|
fs::write(
|
|
&path,
|
|
r#"kind = "gamewrap-profile"
|
|
version = 1
|
|
name = "old-import"
|
|
|
|
[settings]
|
|
overlay = true
|
|
performance = false
|
|
fps_cap = 60
|
|
"#,
|
|
)
|
|
.expect("write old profile");
|
|
|
|
let result = env.run(&["profile", "import", path.to_str().expect("utf8 path")]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("renamed settings that were skipped"));
|
|
assert!(result.output.contains("overlay"));
|
|
assert!(result.output.contains("performance"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_migrate_no_op() {
|
|
let env = TestEnv::new();
|
|
let config_dir = env.config_home.join("gamewrap");
|
|
let config_file = config_dir.join("config.toml");
|
|
fs::create_dir_all(&config_dir).expect("config dir");
|
|
fs::write(
|
|
config_file,
|
|
"[defaults]\nmangohud = true\ngamemode = false\n",
|
|
)
|
|
.expect("write current config");
|
|
|
|
let result = env.run(&["config", "migrate"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("no migration needed"));
|
|
}
|
|
|
|
#[test]
|
|
fn launch_count_and_playtime_tracked() {
|
|
let env = TestEnv::new();
|
|
|
|
assert_ok(&env.run(&["config", "set", "mangohud", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "gamemode", "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"));
|
|
}
|
|
|
|
#[test]
|
|
fn hook_errors_setting_round_trip() {
|
|
let env = TestEnv::new();
|
|
|
|
// Default: hook-errors is unset (show only shows explicitly configured)
|
|
let result = env.run(&["config", "show"]);
|
|
assert_ok(&result);
|
|
// Not shown by default (no setting configured)
|
|
assert!(!result.output.contains("hook-errors"));
|
|
|
|
// Set to fail
|
|
let result = env.run(&["config", "set", "hook-errors", "fail"]);
|
|
assert_ok(&result);
|
|
|
|
let result = env.run(&["config", "show"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("hook-errors"));
|
|
assert!(result.output.contains("fail"));
|
|
|
|
// Reset
|
|
let result = env.run(&["config", "reset", "hook-errors"]);
|
|
assert_ok(&result);
|
|
|
|
let result = env.run(&["config", "show"]);
|
|
assert_ok(&result);
|
|
assert!(!result.output.contains("hook-errors"));
|
|
|
|
// Invalid value
|
|
let result = env.run(&["config", "set", "hook-errors", "crash"]);
|
|
assert_exit(&result, 3);
|
|
assert!(result.output.contains("not a valid hook-errors value"));
|
|
}
|
|
|
|
#[test]
|
|
fn pre_hook_failure_warns_by_default() {
|
|
let env = TestEnv::new();
|
|
let path = env.path_with_fake_bins(&["mangohud", "gamemoderun", "game.exe"]);
|
|
|
|
// Configure a pre-hook that exits 1
|
|
let result = env.run(&["config", "set", "pre-launch", "exit 1"]);
|
|
assert_ok(&result);
|
|
|
|
// Default is warn mode — game should still launch (exit 0 from fake bin)
|
|
let result = env.run_with_env(
|
|
&["run", &env.path("bin/game.exe").to_string_lossy()],
|
|
&[("PATH", &path)],
|
|
);
|
|
// The fake game exits 0, so overall success
|
|
assert_ok(&result);
|
|
// Stderr should mention the hook failure
|
|
assert!(
|
|
result.output.contains("pre-launch hook exited 1") || result.output.contains("pre-launch")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn pre_hook_failure_aborts_in_fail_mode() {
|
|
let env = TestEnv::new();
|
|
let path = env.path_with_fake_bins(&["mangohud", "gamemoderun", "game.exe"]);
|
|
|
|
env.run(&["config", "set", "pre-launch", "exit 1"]);
|
|
env.run(&["config", "set", "hook-errors", "fail"]);
|
|
|
|
let result = env.run_with_env(
|
|
&["run", &env.path("bin/game.exe").to_string_lossy()],
|
|
&[("PATH", &path)],
|
|
);
|
|
// Should fail because pre-hook failed and hook-errors=fail
|
|
assert_exit(&result, 1);
|
|
assert!(result.output.contains("pre-launch hook exited 1"));
|
|
}
|
|
|
|
#[test]
|
|
fn post_hook_runs_after_game_exits_nonzero() {
|
|
let env = TestEnv::new();
|
|
|
|
// Create a game binary that exits with code 42
|
|
let bin_dir = env.home.join("bin");
|
|
std::fs::create_dir_all(&bin_dir).expect("bin dir");
|
|
let game_path = bin_dir.join("failing_game.sh");
|
|
std::fs::write(&game_path, "#!/bin/sh\nexit 42\n").expect("write failing game");
|
|
let mut perms = std::fs::metadata(&game_path).expect("meta").permissions();
|
|
perms.set_mode(0o755);
|
|
std::fs::set_permissions(&game_path, perms).expect("chmod");
|
|
|
|
// Create a post-hook marker file so we can verify the hook ran
|
|
let marker = env.home.join("post_hook_ran");
|
|
let post_hook_cmd = format!("touch {}", marker.display());
|
|
|
|
let fake_path = env.path_with_fake_bins(&["mangohud", "gamemoderun"]);
|
|
|
|
env.run(&["config", "set", "mangohud", "off"]);
|
|
env.run(&["config", "set", "gamemode", "off"]);
|
|
let set_result = env.run(&["config", "set", "post-launch", &post_hook_cmd]);
|
|
assert_ok(&set_result);
|
|
|
|
let result = env.run_with_env(
|
|
&["run", &game_path.to_string_lossy()],
|
|
&[("PATH", &fake_path)],
|
|
);
|
|
|
|
// Game exited 42, so gamewrap should exit 42
|
|
assert_eq!(
|
|
result.status, 42,
|
|
"expected game exit code propagated:\n{}",
|
|
result.output
|
|
);
|
|
// Post-hook must have run
|
|
assert!(
|
|
marker.exists(),
|
|
"post-launch hook did not run after nonzero game exit"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn post_hook_runs_after_successful_game_exit() {
|
|
let env = TestEnv::new();
|
|
|
|
let bin_dir = env.home.join("bin");
|
|
std::fs::create_dir_all(&bin_dir).expect("bin dir");
|
|
let game_path = bin_dir.join("ok_game.sh");
|
|
std::fs::write(&game_path, "#!/bin/sh\nexit 0\n").expect("write ok game");
|
|
let mut perms = std::fs::metadata(&game_path).expect("meta").permissions();
|
|
perms.set_mode(0o755);
|
|
std::fs::set_permissions(&game_path, perms).expect("chmod");
|
|
|
|
let marker = env.home.join("post_hook_ok_ran");
|
|
let post_hook_cmd = format!("touch {}", marker.display());
|
|
|
|
env.run(&["config", "set", "mangohud", "off"]);
|
|
env.run(&["config", "set", "gamemode", "off"]);
|
|
env.run(&["config", "set", "post-launch", &post_hook_cmd]);
|
|
|
|
let result = env.run_with_env(&["run", &game_path.to_string_lossy()], &[]);
|
|
assert_ok(&result);
|
|
assert!(
|
|
marker.exists(),
|
|
"post-launch hook did not run after successful game exit"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn post_hook_failure_is_reported() {
|
|
let env = TestEnv::new();
|
|
|
|
let bin_dir = env.home.join("bin");
|
|
std::fs::create_dir_all(&bin_dir).expect("bin dir");
|
|
let game_path = bin_dir.join("noop_game.sh");
|
|
std::fs::write(&game_path, "#!/bin/sh\nexit 0\n").expect("write noop game");
|
|
let mut perms = std::fs::metadata(&game_path).expect("meta").permissions();
|
|
perms.set_mode(0o755);
|
|
std::fs::set_permissions(&game_path, perms).expect("chmod");
|
|
|
|
env.run(&["config", "set", "mangohud", "off"]);
|
|
env.run(&["config", "set", "gamemode", "off"]);
|
|
env.run(&["config", "set", "post-launch", "exit 7"]);
|
|
|
|
let result = env.run_with_env(&["run", &game_path.to_string_lossy()], &[]);
|
|
// Overall should succeed (game exited 0, post-hook failure is reported but not fatal)
|
|
assert_ok(&result);
|
|
// Should report post-hook failure on stderr (captured in result.output)
|
|
assert!(
|
|
result.output.contains("post-launch hook exited 7"),
|
|
"Expected post-hook failure report in output:\n{}",
|
|
result.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hook_errors_profile_round_trip() {
|
|
let env = TestEnv::new();
|
|
|
|
env.run(&["profile", "create", "myprofile"]);
|
|
let result = env.run(&["profile", "set", "myprofile", "hook-errors", "fail"]);
|
|
assert_ok(&result);
|
|
|
|
let result = env.run(&["profile", "show", "myprofile"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("hook-errors"));
|
|
assert!(result.output.contains("fail"));
|
|
|
|
let result = env.run(&["profile", "reset", "myprofile", "hook-errors"]);
|
|
assert_ok(&result);
|
|
|
|
let result = env.run(&["profile", "show", "myprofile"]);
|
|
assert_ok(&result);
|
|
// After reset, hook-errors should not appear (no explicit setting)
|
|
assert!(!result.output.contains("hook-errors"));
|
|
}
|
|
|
|
#[test]
|
|
fn hook_errors_config_export_import() {
|
|
let env = TestEnv::new();
|
|
let export_path = env.path("my-config");
|
|
|
|
env.run(&["config", "set", "hook-errors", "fail"]);
|
|
let result = env.run(&["config", "export", &export_path.to_string_lossy()]);
|
|
assert_ok(&result);
|
|
|
|
// The exported file should contain hook-errors
|
|
let suffix_path = env.path("my-config.gamewrap.toml");
|
|
let content = std::fs::read_to_string(&suffix_path).expect("read export");
|
|
assert!(
|
|
content.contains("hook-errors"),
|
|
"export should contain hook-errors:\n{content}"
|
|
);
|
|
assert!(content.contains("fail"));
|
|
|
|
// Import into a fresh env
|
|
let env2 = TestEnv::new();
|
|
let result = env2.run(&["config", "import", &suffix_path.to_string_lossy()]);
|
|
assert_ok(&result);
|
|
let result = env2.run(&["config", "show"]);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("hook-errors"));
|
|
assert!(result.output.contains("fail"));
|
|
}
|
|
|
|
#[test]
|
|
fn dry_run_shows_hook_errors_when_hooks_configured() {
|
|
let env = TestEnv::new();
|
|
let path = env.path_with_fake_bins(&["mangohud", "gamemoderun", "game.exe"]);
|
|
|
|
env.run(&["config", "set", "pre-launch", "echo hello"]);
|
|
env.run(&["config", "set", "hook-errors", "fail"]);
|
|
|
|
let result = env.run_with_env(
|
|
&["dry-run", &env.path("bin/game.exe").to_string_lossy()],
|
|
&[("PATH", &path)],
|
|
);
|
|
assert_ok(&result);
|
|
assert!(result.output.contains("hook-errors") || result.output.contains("Hook errors"));
|
|
assert!(result.output.contains("fail"));
|
|
}
|
|
|
|
#[test]
|
|
fn help_topics_all_valid_and_invalid() {
|
|
let env = TestEnv::new();
|
|
|
|
// Valid help topics
|
|
for topic in &[
|
|
"settings",
|
|
"profiles",
|
|
"bindings",
|
|
"doctor",
|
|
"completion",
|
|
"troubleshooting",
|
|
] {
|
|
let r = env.run(&["help", topic]);
|
|
assert_ok(&r);
|
|
assert!(!r.output.is_empty(), "help {topic} gave empty output");
|
|
}
|
|
|
|
// help settings with filter
|
|
let r = env.run(&["help", "settings", "mangohud"]);
|
|
assert_ok(&r);
|
|
assert!(
|
|
r.output.contains("mangohud"),
|
|
"help settings mangohud should mention mangohud"
|
|
);
|
|
|
|
let r = env.run(&["help", "settings", "gamescope"]);
|
|
assert_ok(&r);
|
|
assert!(r.output.contains("gamescope"));
|
|
|
|
// help settings bad group -> non-zero with message
|
|
let r = env.run(&["help", "settings", "badgroup"]);
|
|
assert!(r.status != 0, "help settings badgroup should fail");
|
|
assert!(
|
|
r.output.contains("badgroup")
|
|
|| r.output.to_lowercase().contains("unknown")
|
|
|| r.output.to_lowercase().contains("not a known")
|
|
);
|
|
|
|
// help bad topic -> non-zero with message
|
|
let r = env.run(&["help", "completely-unknown-topic"]);
|
|
assert!(r.status != 0, "help on unknown topic should fail");
|
|
assert!(
|
|
r.output.contains("completely-unknown-topic")
|
|
|| r.output.to_lowercase().contains("not a known")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn version_and_bare_help() {
|
|
let env = TestEnv::new();
|
|
|
|
let r = env.run(&["--version"]);
|
|
assert_ok(&r);
|
|
assert!(
|
|
!r.output.trim().is_empty(),
|
|
"--version should print something"
|
|
);
|
|
|
|
let r = env.run(&["--help"]);
|
|
assert_ok(&r);
|
|
assert!(
|
|
r.output.contains("gamewrap"),
|
|
"--help should mention gamewrap"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn status_runs_cleanly() {
|
|
let env = TestEnv::new();
|
|
let path = env.path_with_fake_bins(&["mangohud", "gamemoderun", "gamescope"]);
|
|
|
|
let r = env.run_with_env(&["status"], &[("PATH", &path)]);
|
|
assert_ok(&r);
|
|
|
|
// status with no deps: still exits 0 (informational command)
|
|
let empty_path = env.path_isolated_fake_bins(&[]);
|
|
let r = env.run_with_env(&["status"], &[("PATH", &empty_path)]);
|
|
assert_ok(&r);
|
|
}
|
|
|
|
#[test]
|
|
fn all_toggle_settings_reject_invalid_values() {
|
|
let env = TestEnv::new();
|
|
|
|
let toggle_settings = &[
|
|
"mangohud",
|
|
"gamemode",
|
|
"steam-host-libs",
|
|
"verbose",
|
|
"log-file",
|
|
"gamescope",
|
|
"gamescope-adaptive-sync",
|
|
"gamescope-hdr",
|
|
"gamescope-steam",
|
|
"gamescope-expose-wayland",
|
|
"gamescope-mangoapp",
|
|
"mangohud-log",
|
|
"vkbasalt",
|
|
"esync",
|
|
"fsync",
|
|
"large-address-aware",
|
|
];
|
|
|
|
let bad_values = &["maybe", "yes", "1", "ON", ""];
|
|
|
|
for setting in toggle_settings {
|
|
for bad in bad_values {
|
|
let r = env.run(&["config", "set", setting, bad]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"toggle `{setting}` bad value `{bad}`:\n{}",
|
|
r.output
|
|
);
|
|
// Verify error output mentions the bad value or the setting name
|
|
assert!(
|
|
r.output.contains(bad)
|
|
|| r.output.contains(setting)
|
|
|| r.output.to_lowercase().contains("valid"),
|
|
"toggle setting `{setting}` with value `{bad}` gave unhelpful error: {}",
|
|
r.output
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn enum_settings_reject_invalid_values() {
|
|
let env = TestEnv::new();
|
|
|
|
let cases = &[
|
|
("game-libs", "badvalue"),
|
|
("gamescope-scaler", "badvalue"),
|
|
("gamescope-filter", "badvalue"),
|
|
("gamescope-mode", "badvalue"),
|
|
("vkbasalt-log-level", "badvalue"),
|
|
("hook-errors", "badvalue"),
|
|
];
|
|
|
|
for (setting, value) in cases {
|
|
let r = env.run(&["config", "set", setting, value]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"enum `{setting}` with `{value}` should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn numeric_settings_reject_bad_values() {
|
|
let env = TestEnv::new();
|
|
|
|
let cases: &[(&str, &str)] = &[
|
|
("gamescope-fps", "4294967296"),
|
|
("gamescope-fps", "abc"),
|
|
("gamescope-fps", "1.5"),
|
|
("gamescope-unfocused-fps", "4294967296"),
|
|
("gamescope-nested-width", "4294967296"),
|
|
("gamescope-nested-height", "4294967296"),
|
|
("fps-cap", "4294967296"),
|
|
("fps-cap", "abc"),
|
|
("gamescope-sharpness", "21"),
|
|
("gamescope-sharpness", "4294967296"),
|
|
("gamescope-sharpness", "abc"),
|
|
("gamescope-width", "abc"),
|
|
("gamescope-height", "abc"),
|
|
];
|
|
|
|
for (setting, value) in cases {
|
|
let r = env.run(&["config", "set", setting, value]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"numeric `{setting}` with `{value}` should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn numeric_settings_boundary_valid_values() {
|
|
let env = TestEnv::new();
|
|
|
|
let valid_cases: &[(&str, &str)] = &[
|
|
("gamescope-sharpness", "0"),
|
|
("gamescope-sharpness", "20"),
|
|
("gamescope-fps", "1"),
|
|
("gamescope-width", "native"),
|
|
("gamescope-width", "1920"),
|
|
("gamescope-height", "1080"),
|
|
("gamescope-height", "native"),
|
|
("gamescope-nested-width", "1"),
|
|
("gamescope-nested-height", "1"),
|
|
("gamescope-unfocused-fps", "1"),
|
|
("fps-cap", "1"),
|
|
];
|
|
|
|
for (setting, value) in valid_cases {
|
|
let r = env.run(&["config", "set", setting, value]);
|
|
assert_ok(&r);
|
|
// reset afterwards so settings don't bleed across sub-cases
|
|
env.run(&["config", "reset", setting]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_setting_name_rejected() {
|
|
let env = TestEnv::new();
|
|
|
|
let bad_names = &[
|
|
"nonexistent-setting",
|
|
"MangoHud", // wrong case
|
|
"mangohud-extra", // plausible but wrong
|
|
"GAMEMODE", // all-caps
|
|
"hook_errors", // underscore instead of dash
|
|
];
|
|
|
|
for name in bad_names {
|
|
let r = env.run(&["config", "set", name, "on"]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"unknown setting `{name}` should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
assert!(
|
|
r.output.contains(name)
|
|
|| r.output.to_lowercase().contains("unknown")
|
|
|| r.output.to_lowercase().contains("not a known"),
|
|
"error for `{name}` should mention the name or say 'unknown':\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn setting_aliases_work() {
|
|
let env = TestEnv::new();
|
|
|
|
// host-libs alias for steam-host-libs
|
|
assert_ok(&env.run(&["config", "set", "host-libs", "on"]));
|
|
let r = env.run(&["config", "show"]);
|
|
assert_ok(&r);
|
|
assert!(
|
|
r.output.contains("steam-host-libs"),
|
|
"alias host-libs should set steam-host-libs:\n{}",
|
|
r.output
|
|
);
|
|
assert_ok(&env.run(&["config", "reset", "host-libs"]));
|
|
let r = env.run(&["config", "show"]);
|
|
assert!(
|
|
!r.output.contains("steam-host-libs"),
|
|
"after reset via alias, steam-host-libs should be gone:\n{}",
|
|
r.output
|
|
);
|
|
|
|
// laa alias for large-address-aware
|
|
assert_ok(&env.run(&["config", "set", "laa", "on"]));
|
|
let r = env.run(&["config", "show"]);
|
|
assert!(
|
|
r.output.contains("large-address-aware"),
|
|
"alias laa should set large-address-aware:\n{}",
|
|
r.output
|
|
);
|
|
assert_ok(&env.run(&["config", "reset", "laa"]));
|
|
let r = env.run(&["config", "show"]);
|
|
assert!(
|
|
!r.output.contains("large-address-aware"),
|
|
"after reset via laa alias, large-address-aware should be gone:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn config_reset_edge_cases() {
|
|
let env = TestEnv::new();
|
|
|
|
// config reset with no setting and no --all -> exit 2
|
|
let r = env.run(&["config", "reset"]);
|
|
assert_eq!(
|
|
r.status, 2,
|
|
"config reset with no args should exit 2:\n{}",
|
|
r.output
|
|
);
|
|
|
|
// config reset --all with a setting name -> error (conflicts)
|
|
let r = env.run(&["config", "reset", "--all", "mangohud"]);
|
|
assert!(
|
|
r.status != 0,
|
|
"config reset --all with setting name should fail:\n{}",
|
|
r.output
|
|
);
|
|
|
|
// config reset unknown setting -> exit 3
|
|
let r = env.run(&["config", "reset", "nonexistent-setting"]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"config reset nonexistent setting should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn config_export_stdout_and_file() {
|
|
let env = TestEnv::new();
|
|
|
|
assert_ok(&env.run(&["config", "set", "mangohud", "off"]));
|
|
|
|
// Export to stdout
|
|
let stdout_path = env.path("stdout.gamewrap.toml");
|
|
std::os::unix::fs::symlink("/dev/stdout", &stdout_path).unwrap();
|
|
let r = env.run(&["config", "export", stdout_path.to_str().unwrap()]);
|
|
assert_ok(&r);
|
|
assert!(
|
|
r.stdout.contains("mangohud"),
|
|
"export to stdout should contain set field:\n{}",
|
|
r.stdout
|
|
);
|
|
// Should be sparse - only the set field
|
|
assert!(
|
|
!r.stdout.contains("gamemode"),
|
|
"export should be sparse, not contain unset gamemode:\n{}",
|
|
r.stdout
|
|
);
|
|
|
|
// Export to file
|
|
let out_path = env.path("out");
|
|
let r = env.run(&["config", "export", out_path.to_str().unwrap()]);
|
|
assert_ok(&r);
|
|
let file_path = out_path.with_extension("gamewrap.toml");
|
|
assert!(file_path.exists(), "exported file should exist");
|
|
let content = std::fs::read_to_string(&file_path).unwrap();
|
|
assert!(
|
|
content.contains("mangohud"),
|
|
"exported file should contain the set field"
|
|
);
|
|
assert!(
|
|
!content.contains("gamemode"),
|
|
"exported file should be sparse"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn config_import_error_cases() {
|
|
let env = TestEnv::new();
|
|
|
|
// Import nonexistent file -> non-zero (likely exit 1)
|
|
let r = env.run(&["config", "import", "/nonexistent/path/config.gamewrap.toml"]);
|
|
assert!(
|
|
r.status != 0,
|
|
"import nonexistent file should fail:\n{}",
|
|
r.output
|
|
);
|
|
|
|
// Import invalid TOML -> exit 3
|
|
let bad_toml = env.path("bad.gamewrap.toml");
|
|
std::fs::write(&bad_toml, "not valid toml [[[").unwrap();
|
|
let r = env.run(&["config", "import", bad_toml.to_str().unwrap()]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"import invalid TOML should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
|
|
// Import config whose bindings reference a missing profile -> exit 3
|
|
let bad_config = env.path("bad-binding.gamewrap.toml");
|
|
std::fs::write(
|
|
&bad_config,
|
|
r#"
|
|
[defaults]
|
|
mangohud = true
|
|
|
|
[[bindings]]
|
|
matcher = "game.exe"
|
|
profile = "nonexistent-profile"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let r = env.run(&["config", "import", bad_config.to_str().unwrap()]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"import config with missing-profile binding should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn profile_reserved_name_rejected() {
|
|
let env = TestEnv::new();
|
|
|
|
// profile create default -> exit 3
|
|
let r = env.run(&["profile", "create", "default"]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"profile create default should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
assert!(
|
|
r.output.to_lowercase().contains("reserved") || r.output.to_lowercase().contains("default"),
|
|
"error should mention reserved:\n{}",
|
|
r.output
|
|
);
|
|
|
|
// profile export default -> exit 3
|
|
let r = env.run(&["profile", "export", "default"]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"profile export default should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
|
|
// profile import a file where profile name field is "default" -> exit 3
|
|
let default_profile = env.path("default.gamewrap-profile.toml");
|
|
std::fs::write(
|
|
&default_profile,
|
|
r#"
|
|
kind = "profile"
|
|
name = "default"
|
|
version = 1
|
|
|
|
[settings]
|
|
mangohud = false
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let r = env.run(&["profile", "import", default_profile.to_str().unwrap()]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"importing a profile named 'default' should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn profile_duplicate_edge_cases() {
|
|
let env = TestEnv::new();
|
|
|
|
// Duplicate nonexistent source -> exit 3
|
|
let r = env.run(&["profile", "duplicate", "nonexistent", "dest"]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"duplicate nonexistent source should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
|
|
// Duplicate to existing destination -> exit 3
|
|
assert_ok(&env.run(&["profile", "create", "src"]));
|
|
assert_ok(&env.run(&["profile", "create", "dest"]));
|
|
let r = env.run(&["profile", "duplicate", "src", "dest"]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"duplicate to existing dest should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
|
|
// Duplicate to reserved name "default" -> exit 3
|
|
let r = env.run(&["profile", "duplicate", "src", "default"]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"duplicate to 'default' should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
|
|
// Valid duplicate copies settings
|
|
assert_ok(&env.run(&["profile", "set", "src", "mangohud", "off"]));
|
|
let r = env.run(&["profile", "duplicate", "src", "dst-copy"]);
|
|
assert_ok(&r);
|
|
let r = env.run(&["profile", "show", "dst-copy"]);
|
|
assert_ok(&r);
|
|
assert!(
|
|
r.output.contains("mangohud"),
|
|
"duplicated profile should have copied settings:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn profile_import_error_cases() {
|
|
let env = TestEnv::new();
|
|
|
|
// Import nonexistent file -> non-zero
|
|
let r = env.run(&["profile", "import", "/no/such/file.gamewrap-profile.toml"]);
|
|
assert!(
|
|
r.status != 0,
|
|
"import nonexistent profile file should fail:\n{}",
|
|
r.output
|
|
);
|
|
|
|
// Create and export a profile, then create a same-named profile and try importing -> exit 3
|
|
assert_ok(&env.run(&["profile", "create", "dupe-test"]));
|
|
let export_path = env.path("dupe-test");
|
|
assert_ok(&env.run(&[
|
|
"profile",
|
|
"export",
|
|
"dupe-test",
|
|
export_path.to_str().unwrap(),
|
|
]));
|
|
// profile "dupe-test" still exists; re-importing should fail
|
|
let export_file = export_path.with_extension("gamewrap-profile.toml");
|
|
let r = env.run(&["profile", "import", export_file.to_str().unwrap()]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"import duplicate profile name should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
assert!(
|
|
r.output.to_lowercase().contains("already exists")
|
|
|| r.output.to_lowercase().contains("dupe-test"),
|
|
"error should mention the name:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn profile_delete_nonexistent() {
|
|
let env = TestEnv::new();
|
|
let r = env.run(&["profile", "delete", "no-such-profile"]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"delete nonexistent profile should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn profile_reset_edge_cases() {
|
|
let env = TestEnv::new();
|
|
|
|
// Resetting a setting that was never set is a no-op -> exit 0
|
|
assert_ok(&env.run(&["profile", "create", "p"]));
|
|
let r = env.run(&["profile", "reset", "p", "mangohud"]);
|
|
assert_ok(&r);
|
|
|
|
// Reset in nonexistent profile -> exit 3
|
|
let r = env.run(&["profile", "reset", "nonexistent", "mangohud"]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"reset in nonexistent profile should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn profile_env_edge_cases() {
|
|
let env = TestEnv::new();
|
|
assert_ok(&env.run(&["profile", "create", "p"]));
|
|
|
|
// env list when no vars set -> exit 0, informational message
|
|
let r = env.run(&["profile", "env", "list", "p"]);
|
|
assert_ok(&r);
|
|
|
|
// env clear when no vars set -> exit 0
|
|
let r = env.run(&["profile", "env", "clear", "p"]);
|
|
assert_ok(&r);
|
|
|
|
// env unset a key that was never set -> exit 0
|
|
let r = env.run(&["profile", "env", "unset", "p", "NONEXISTENT_KEY"]);
|
|
assert_ok(&r);
|
|
|
|
// Set a var, then unset it, then list -> empty again
|
|
assert_ok(&env.run(&["profile", "env", "set", "p", "MY_VAR", "hello"]));
|
|
let r = env.run(&["profile", "env", "list", "p"]);
|
|
assert_ok(&r);
|
|
assert!(r.output.contains("MY_VAR"));
|
|
assert_ok(&env.run(&["profile", "env", "unset", "p", "MY_VAR"]));
|
|
let r = env.run(&["profile", "env", "list", "p"]);
|
|
assert_ok(&r);
|
|
assert!(
|
|
!r.output.contains("MY_VAR"),
|
|
"after unset, var should be gone:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn game_command_error_cases() {
|
|
let env = TestEnv::new();
|
|
|
|
// game show nonexistent matcher -> exit 3
|
|
let r = env.run(&["game", "show", "nonexistent.exe"]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"game show nonexistent should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
|
|
// game bind to nonexistent profile -> exit 3
|
|
let r = env.run(&["game", "bind", "game.exe", "no-such-profile"]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"game bind to nonexistent profile should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
|
|
// game unbind when no binding exists -> exit 3
|
|
let r = env.run(&["game", "unbind", "unbound.exe"]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"game unbind when no binding should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
|
|
// game forget nonexistent -> exit 3
|
|
let r = env.run(&["game", "forget", "unknown.exe"]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"game forget unknown game should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
|
|
// game rename nonexistent -> exit 3
|
|
let r = env.run(&["game", "rename", "nonexistent.exe", "New Name"]);
|
|
assert_eq!(
|
|
r.status, 3,
|
|
"game rename nonexistent should exit 3:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn game_list_empty() {
|
|
let env = TestEnv::new();
|
|
let r = env.run(&["game", "list"]);
|
|
assert_ok(&r);
|
|
// Should print a "no games" placeholder rather than silently printing nothing
|
|
assert!(
|
|
!r.output.trim().is_empty(),
|
|
"game list on empty state should print a message"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn game_note_multiword() {
|
|
let env = TestEnv::new();
|
|
|
|
// Observe a game via a Steam-context launch
|
|
let path = env.path_with_fake_bins(&["gamemoderun", "mangohud"]);
|
|
env.run_with_env(
|
|
&["run", "--", "/bin/echo", "hello"],
|
|
&[
|
|
("PATH", &path),
|
|
("SteamAppId", "999"),
|
|
("SteamGameId", "999"),
|
|
],
|
|
);
|
|
|
|
// After the launch, the game is observed as "/bin/echo"
|
|
// Use the executable basename as matcher
|
|
let r = env.run(&["game", "note", "echo", "word1", "word2", "word3"]);
|
|
assert_ok(&r);
|
|
|
|
let r = env.run(&["game", "show", "echo"]);
|
|
assert_ok(&r);
|
|
assert!(
|
|
r.output.contains("word1 word2 word3") || r.output.contains("word1"),
|
|
"note should contain joined words:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn game_list_full_flag() {
|
|
let env = TestEnv::new();
|
|
let path = env.path_with_fake_bins(&["gamemoderun", "mangohud"]);
|
|
|
|
// Observe a game first
|
|
env.run_with_env(
|
|
&["run", "--", "/bin/echo", "hi"],
|
|
&[("PATH", &path), ("SteamAppId", "99"), ("SteamGameId", "99")],
|
|
);
|
|
|
|
// list without --full: path may be abbreviated
|
|
let r_short = env.run_with_env(&["game", "list"], &[("PATH", &path)]);
|
|
assert_ok(&r_short);
|
|
|
|
// list with --full: path is not abbreviated
|
|
let r_full = env.run_with_env(&["game", "list", "--full"], &[("PATH", &path)]);
|
|
assert_ok(&r_full);
|
|
// The full path /bin/echo should appear somewhere
|
|
assert!(
|
|
r_full.output.contains("/bin/echo"),
|
|
"game list --full should show full path:\n{}",
|
|
r_full.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dry_run_with_invalid_target() {
|
|
let env = TestEnv::new();
|
|
let path = env.path_with_fake_bins(&["mangohud", "gamemoderun"]);
|
|
|
|
let r = env.run_with_env(
|
|
&["dry-run", "--", "/nonexistent-binary"],
|
|
&[("PATH", &path)],
|
|
);
|
|
assert_eq!(
|
|
r.status, 4,
|
|
"dry-run with nonexistent binary should exit 4:\n{}",
|
|
r.output
|
|
);
|
|
assert!(
|
|
r.output.to_lowercase().contains("nonexistent")
|
|
|| r.output.to_lowercase().contains("does not exist"),
|
|
"error should mention the path:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dry_run_shows_gamescope_command() {
|
|
let env = TestEnv::new();
|
|
let path = env.path_with_fake_bins(&["mangohud", "gamemoderun", "gamescope"]);
|
|
|
|
assert_ok(&env.run(&["config", "set", "gamescope", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-width", "1920"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-fps", "60"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-filter", "fsr"]));
|
|
assert_ok(&env.run(&["config", "set", "mangohud", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "gamemode", "off"]));
|
|
|
|
let r = env.run_with_env(&["dry-run", "--", "/bin/echo", "test"], &[("PATH", &path)]);
|
|
assert_ok(&r);
|
|
assert!(
|
|
r.output.contains("gamescope"),
|
|
"dry-run with gamescope=on should show gamescope in command:\n{}",
|
|
r.output
|
|
);
|
|
assert!(
|
|
r.output.contains("-W") && r.output.contains("1920"),
|
|
"dry-run should show -W 1920:\n{}",
|
|
r.output
|
|
);
|
|
assert!(
|
|
r.output.contains("-r") && r.output.contains("60"),
|
|
"dry-run should show -r 60:\n{}",
|
|
r.output
|
|
);
|
|
assert!(
|
|
r.output.contains("-F") && r.output.contains("fsr"),
|
|
"dry-run should show -F fsr:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn launch_option_like_target_rejected() {
|
|
let env = TestEnv::new();
|
|
let path = env.path_with_fake_bins(&["mangohud", "gamemoderun"]);
|
|
|
|
// A target starting with '--' should be rejected
|
|
let r = env.run_with_env(&["run", "--", "--help"], &[("PATH", &path)]);
|
|
assert_eq!(
|
|
r.status, 2,
|
|
"option-like target should exit 2:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn launch_no_command() {
|
|
let env = TestEnv::new();
|
|
|
|
let r = env.run(&["run"]);
|
|
// run with no target is a usage error
|
|
assert!(
|
|
r.status != 0,
|
|
"run with no command should fail:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn pre_fail_does_not_trigger_post_hook() {
|
|
let env = TestEnv::new();
|
|
let path = env.path_with_fake_bins(&["mangohud", "gamemoderun"]);
|
|
|
|
assert_ok(&env.run(&["config", "set", "hook-errors", "fail"]));
|
|
assert_ok(&env.run(&["config", "set", "pre-launch", "exit 1"]));
|
|
assert_ok(&env.run(&["config", "set", "post-launch", "echo POSTHOOK_RAN"]));
|
|
|
|
let r = env.run_with_env(&["run", "--", "/bin/echo", "game"], &[("PATH", &path)]);
|
|
assert_eq!(
|
|
r.status, 1,
|
|
"pre-hook fail mode should exit 1:\n{}",
|
|
r.output
|
|
);
|
|
assert!(
|
|
!r.output.contains("POSTHOOK_RAN"),
|
|
"post-hook should NOT run when pre-hook aborts:\n{}",
|
|
r.output
|
|
);
|
|
assert!(
|
|
!r.output.contains("game"),
|
|
"game should NOT have run:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn both_hooks_run_on_success() {
|
|
let env = TestEnv::new();
|
|
let path = env.path_with_fake_bins(&["mangohud", "gamemoderun"]);
|
|
|
|
assert_ok(&env.run(&["config", "set", "mangohud", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "gamemode", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "pre-launch", "echo PRE_HOOK"]));
|
|
assert_ok(&env.run(&["config", "set", "post-launch", "echo POST_HOOK"]));
|
|
|
|
let r = env.run_with_env(
|
|
&["run", "--", "/bin/echo", "GAME_OUTPUT"],
|
|
&[("PATH", &path)],
|
|
);
|
|
assert_ok(&r);
|
|
assert!(
|
|
r.output.contains("PRE_HOOK"),
|
|
"pre-hook should have run:\n{}",
|
|
r.output
|
|
);
|
|
assert!(
|
|
r.output.contains("POST_HOOK"),
|
|
"post-hook should have run:\n{}",
|
|
r.output
|
|
);
|
|
assert!(
|
|
r.output.contains("GAME_OUTPUT"),
|
|
"game should have run:\n{}",
|
|
r.output
|
|
);
|
|
|
|
// Verify ordering: PRE comes before GAME comes before POST
|
|
let pre_pos = r.output.find("PRE_HOOK").unwrap();
|
|
let game_pos = r.output.find("GAME_OUTPUT").unwrap();
|
|
let post_pos = r.output.find("POST_HOOK").unwrap();
|
|
assert!(
|
|
pre_pos < game_pos,
|
|
"PRE_HOOK should appear before GAME_OUTPUT"
|
|
);
|
|
assert!(
|
|
game_pos < post_pos,
|
|
"GAME_OUTPUT should appear before POST_HOOK"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn post_hook_runs_when_binary_missing() {
|
|
let env = TestEnv::new();
|
|
let path = env.path_with_fake_bins(&["mangohud", "gamemoderun"]);
|
|
|
|
assert_ok(&env.run(&["config", "set", "post-launch", "echo POSTHOOK_RAN"]));
|
|
|
|
let r = env.run_with_env(&["run", "--", "/nonexistent-binary"], &[("PATH", &path)]);
|
|
assert_eq!(
|
|
r.status, 4,
|
|
"nonexistent binary should exit 4:\n{}",
|
|
r.output
|
|
);
|
|
assert!(
|
|
r.output.contains("POSTHOOK_RAN"),
|
|
"post-hook should run even when binary is missing:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_dependency_errors() {
|
|
let env = TestEnv::new();
|
|
|
|
// mangohud=on but mangohud missing
|
|
let no_mangohud = env.path_isolated_fake_bins(&["gamemoderun"]);
|
|
assert_ok(&env.run(&["config", "set", "mangohud", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "gamemode", "off"]));
|
|
let r = env.run_with_env(&["run", "--", "/bin/echo", "hi"], &[("PATH", &no_mangohud)]);
|
|
assert_eq!(r.status, 4, "missing mangohud should exit 4:\n{}", r.output);
|
|
assert!(
|
|
r.output.to_lowercase().contains("mangohud"),
|
|
"error should mention mangohud:\n{}",
|
|
r.output
|
|
);
|
|
assert_ok(&env.run(&["config", "reset", "--all"]));
|
|
|
|
// gamemode=on but gamemoderun missing
|
|
let env = TestEnv::new();
|
|
let no_gamemode = env.path_isolated_fake_bins(&["mangohud"]);
|
|
assert_ok(&env.run(&["config", "set", "gamemode", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "mangohud", "off"]));
|
|
let r = env.run_with_env(&["run", "--", "/bin/echo", "hi"], &[("PATH", &no_gamemode)]);
|
|
assert_eq!(
|
|
r.status, 4,
|
|
"missing gamemoderun should exit 4:\n{}",
|
|
r.output
|
|
);
|
|
assert!(
|
|
r.output.to_lowercase().contains("gamemoderun")
|
|
|| r.output.to_lowercase().contains("gamemode"),
|
|
"error should mention gamemode:\n{}",
|
|
r.output
|
|
);
|
|
assert_ok(&env.run(&["config", "reset", "--all"]));
|
|
|
|
// gamescope=on but gamescope missing
|
|
let env = TestEnv::new();
|
|
let no_gamescope = env.path_isolated_fake_bins(&["mangohud", "gamemoderun"]);
|
|
assert_ok(&env.run(&["config", "set", "gamescope", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "mangohud", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "gamemode", "off"]));
|
|
let r = env.run_with_env(
|
|
&["run", "--", "/bin/echo", "hi"],
|
|
&[("PATH", &no_gamescope)],
|
|
);
|
|
assert_eq!(
|
|
r.status, 4,
|
|
"missing gamescope should exit 4:\n{}",
|
|
r.output
|
|
);
|
|
assert!(
|
|
r.output.to_lowercase().contains("gamescope"),
|
|
"error should mention gamescope:\n{}",
|
|
r.output
|
|
);
|
|
assert_ok(&env.run(&["config", "reset", "--all"]));
|
|
|
|
// gamescope-mangoapp=on but mangoapp missing
|
|
let env = TestEnv::new();
|
|
let no_mangoapp = env.path_isolated_fake_bins(&["gamescope", "gamemoderun", "mangohud"]);
|
|
assert_ok(&env.run(&["config", "set", "gamescope", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "gamescope-mangoapp", "on"]));
|
|
assert_ok(&env.run(&["config", "set", "mangohud", "off"]));
|
|
assert_ok(&env.run(&["config", "set", "gamemode", "off"]));
|
|
let r = env.run_with_env(&["run", "--", "/bin/echo", "hi"], &[("PATH", &no_mangoapp)]);
|
|
assert_eq!(r.status, 4, "missing mangoapp should exit 4:\n{}", r.output);
|
|
assert!(
|
|
r.output.to_lowercase().contains("mangoapp"),
|
|
"error should mention mangoapp:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn last_with_no_history() {
|
|
let env = TestEnv::new();
|
|
let r = env.run(&["last"]);
|
|
assert_ok(&r);
|
|
// Should print an informational message about no history
|
|
assert!(
|
|
!r.output.trim().is_empty(),
|
|
"last with no history should print a message"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn last_shows_most_recent() {
|
|
let env = TestEnv::new();
|
|
let path = env.path_with_fake_bins(&["mangohud", "gamemoderun"]);
|
|
let steam_env: &[(&str, &str)] = &[("PATH", &path), ("SteamAppId", "1"), ("SteamGameId", "1")];
|
|
|
|
// Launch first game
|
|
env.run_with_env(&["run", "--", "/bin/echo", "first"], steam_env);
|
|
// Launch second game (different binary)
|
|
env.run_with_env(&["run", "--", "/bin/true"], steam_env);
|
|
|
|
let r = env.run(&["last"]);
|
|
assert_ok(&r);
|
|
// The second game (true) should be shown as last played
|
|
assert!(
|
|
r.output.contains("true") || r.output.contains("/bin/true") || r.output.contains("true"),
|
|
"last should show the most recently launched game:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn settings_list_command() {
|
|
let env = TestEnv::new();
|
|
|
|
let r = env.run(&["config", "settings"]);
|
|
assert_ok(&r);
|
|
// Spot-check a representative set of setting names
|
|
for name in &[
|
|
"mangohud",
|
|
"gamemode",
|
|
"gamescope",
|
|
"fps-cap",
|
|
"vkbasalt",
|
|
"hook-errors",
|
|
"pre-launch",
|
|
"post-launch",
|
|
"log-file",
|
|
"esync",
|
|
"fsync",
|
|
"large-address-aware",
|
|
] {
|
|
assert!(
|
|
r.output.contains(name),
|
|
"config settings should list `{name}`:\n{}",
|
|
r.output
|
|
);
|
|
}
|
|
|
|
// profile settings should produce the same list
|
|
let r2 = env.run(&["profile", "settings"]);
|
|
assert_ok(&r2);
|
|
for name in &["mangohud", "hook-errors", "gamescope"] {
|
|
assert!(
|
|
r2.output.contains(name),
|
|
"profile settings should list `{name}`:\n{}",
|
|
r2.output
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn completion_edge_cases() {
|
|
let env = TestEnv::new();
|
|
|
|
// completion with no shell and no subcommand -> exit 2
|
|
let r = env.run(&["completion"]);
|
|
assert_eq!(
|
|
r.status, 2,
|
|
"completion with no args should exit 2:\n{}",
|
|
r.output
|
|
);
|
|
|
|
// completion bash -> exit 0, produces a script
|
|
let r = env.run(&["completion", "bash"]);
|
|
assert_ok(&r);
|
|
assert!(
|
|
!r.stdout.is_empty(),
|
|
"completion bash should produce output"
|
|
);
|
|
|
|
// completion zsh -> exit 0, produces a script
|
|
let r = env.run(&["completion", "zsh"]);
|
|
assert_ok(&r);
|
|
assert!(!r.stdout.is_empty(), "completion zsh should produce output");
|
|
|
|
// completion fish -> exit 0, produces a script
|
|
let r = env.run(&["completion", "fish"]);
|
|
assert_ok(&r);
|
|
assert!(
|
|
!r.stdout.is_empty(),
|
|
"completion fish should produce output"
|
|
);
|
|
}
|