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