From 4fe17a5d1d78d9f02ad3e0e9456c247eb2f18e11 Mon Sep 17 00:00:00 2001 From: JamBalaya56562 Date: Sun, 2 Aug 2026 17:33:13 +0900 Subject: [PATCH] test(windows): make the suite runnable on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cargo test --all --all-features` on a Windows machine with WSL gives 15 failures, and `cli/tests/shell_override.rs` never runs at all. None of it is a bug in usage; the tests are. `bash` there is the WSL launcher. The Win32 executable search order puts the system directory ahead of PATH and installing WSL puts `bash.exe` in it, so a bare `bash` cannot open the Windows paths these tests write. GitHub's windows-latest is the same: `where bash` lists Git Bash first, but `Command::new("bash")` still reaches System32, and without an override it exits 1 with "Windows Subsystem for Linux has no installed distributions". Both skip guards missed this by probing a proxy rather than the precondition: `--version` spawns fine under WSL bash, and in `complete_word.rs` the probe asked about `sh` while the mount fixtures need `bash` — on Windows the two need not be the same family. Both now run a script, and a real fixture, and require it to exit cleanly, so an unusable shell skips instead of failing. The fixture is named from `CARGO_MANIFEST_DIR` rather than `fs::canonicalize`, whose `\?\` prefix usage cannot open: probing with one made the guard fail on the shape of the path rather than on the shell, skipping every mount test on a machine where they run. Shell resolution goes through `USAGE_SHELL_`, the variable `shell_program_override` already reads, so one setting covers the whole suite. Every invocation is built by one `script_command`, so the guard and the tests it guards cannot drift apart — not in which program they run, nor in how. pwsh is where they would have: it needs `-File` to take a script path, and `-NoProfile -NonInteractive` so a developer's profile cannot add output to a run whose stdout is compared against a single token. The zsh pty test's `timeout` wrapper goes through the shell too, since spawning it directly walks into the same trap as `bash` — Windows keeps an unrelated `timeout.exe`, a sleep rather than a watchdog, in the system directory. Three places hand a Windows path to something that parses it, and each needed a different answer. Paths interpolated into shell script bodies are normalized to `/`, because `\` is an escape character there and `C:\Users\Jam` was arriving as `C:UsersJam`. `$PATH` entries go through `cygpath` instead: `$PATH` is colon-separated, so `C:/Users/...` splits into `C` and `/Users/...` and neither resolves, and only the shell in use knows whether the answer is `/c/...` or `/cygdrive/c/...`. And `sdk_compile.rs` put the SDK directory inside a Python string literal, where `C:\Users\RUNNER~1\...` fails to parse at all because `\U` opens a unicode escape — that one now travels as argv, clear of any escaping rules. `test_empty_defaults_example` now runs through `usage bash` like the nine other tests in its file. It was the only one relying on the shebang, and Windows cannot execute a `.sh` at all — os error 193, before the shebang is read. `shell_override.rs` loses `#![cfg(unix)]`. Its substitute program is `$CARGO` rather than `/bin/echo`, which has no Windows counterpart: cargo sets the variable for the processes it spawns, and handed a path it does not recognize it repeats it back, which is what makes the substitution observable. The three fallback tests compare against a baseline run with nothing overridden instead of asserting the script's output, since whether the default shell works is a property of the machine, while "same as unset" is what those tests are about. Tests only; no library or CLI source changes. Measured on a windows-latest runner: with `USAGE_SHELL_BASH` naming a real bash and `core.autocrlf` off, the whole suite passes with nothing skipped. Without the override the shell-dependent tests skip rather than fail. Linux is unchanged. --- cli/tests/complete_word.rs | 35 ++- cli/tests/examples.rs | 22 +- cli/tests/shell_completions_integration.rs | 282 ++++++++++++++------- cli/tests/shell_override.rs | 120 ++++++--- lib/tests/sdk_compile.rs | 8 +- 5 files changed, 317 insertions(+), 150 deletions(-) diff --git a/cli/tests/complete_word.rs b/cli/tests/complete_word.rs index eb74c753..5a5f3dcf 100644 --- a/cli/tests/complete_word.rs +++ b/cli/tests/complete_word.rs @@ -2,6 +2,7 @@ use assert_cmd::assert::Assert; use assert_cmd::cargo; use std::env; use std::fs; +use std::path::PathBuf; use std::process::Command; use assert_cmd::prelude::*; @@ -20,22 +21,48 @@ use predicates::str::contains; /// The probe runs a script and checks that it exited cleanly, rather than only that something /// spawned. On Windows `sh` can resolve to a program that starts and then fails, which would /// otherwise read as a working shell and put the tests back in the confusing state above. +/// +/// `sh` starting is necessary but not sufficient, so a fixture is run as well. Its shebang is +/// `usage bash`, and on Windows the two need not be the same family: `sh` can be Git Bash while +/// a bare `bash` is the WSL launcher that System32 puts ahead of `PATH`, which cannot open the +/// absolute path `sh` hands its shebang. Probing only `sh` reported such a machine as usable +/// and left these tests failing rather than skipping. `USAGE_SHELL_BASH` is the way out, and +/// the probe sees it because the fixture runs through `usage bash` too. fn skip_if_posix_shell_missing() -> bool { - let usable = Command::new("sh") + let sh_runs = Command::new("sh") .arg("-c") .arg("exit 0") .output() .is_ok_and(|out| out.status.success()); - if usable { + if sh_runs && mount_fixture_runs() { return false; } if env::var("CI").is_ok_and(|v| !v.is_empty()) { - panic!("no usable POSIX shell (`sh`) but CI is set — refusing to skip"); + panic!("no shell that can run the mount fixtures but CI is set — refusing to skip"); } - eprintln!("Skipping test - no usable POSIX shell (`sh`)"); + eprintln!("Skipping test - no shell that can run the mount fixtures"); true } +/// Whether a mount fixture actually runs, given as the absolute path `sh` would hand it. +fn mount_fixture_runs() -> bool { + // Built from `CARGO_MANIFEST_DIR`, not `fs::canonicalize`. On Windows canonicalize returns + // a `\\?\`-prefixed path, which usage cannot open — the probe would then fail on the shape + // of the path rather than on the shell, and every mount test would skip on a machine where + // they work perfectly well. + let fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("examples") + .join("mounted.sh"); + Command::new(cargo::cargo_bin!("usage")) + .arg("bash") + .arg(fixture) + .arg("--mount") + .output() + .is_ok_and(|out| out.status.success()) +} + #[test] fn complete_word_completer() { assert_cmd("basic.usage.kdl", &["plugins", "install", "pl"]) diff --git a/cli/tests/examples.rs b/cli/tests/examples.rs index 065dd863..f799bf41 100644 --- a/cli/tests/examples.rs +++ b/cli/tests/examples.rs @@ -1,26 +1,18 @@ use assert_cmd::cargo; use assert_cmd::prelude::*; use predicates::str::contains; -use std::{env, process::Command}; +use std::process::Command; /// Test that examples/test-empty-defaults.sh runs successfully and demonstrates /// the correct behavior of default="" vs no default #[test] fn test_empty_defaults_example() { - // Set up PATH to include the usage binary - let mut path = env::split_paths(&env::var("PATH").unwrap()).collect::>(); - path.insert( - 0, - env::current_dir() - .unwrap() - .join("..") - .join("target") - .join("debug"), - ); - path.insert(0, env::current_dir().unwrap().join("..").join("examples")); - - let mut cmd = Command::new("../examples/test-empty-defaults.sh"); - cmd.env("PATH", env::join_paths(path).unwrap()); + // Through `usage bash`, like every other test here, rather than spawning the script and + // letting its `#!/usr/bin/env -S usage bash` shebang do the work. Windows cannot execute a + // `.sh` at all — the spawn fails with os error 193 before the shebang is ever read — and + // going through the binary also drops the PATH juggling that let the shebang find `usage`. + let mut cmd = Command::new(cargo::cargo_bin!("usage")); + cmd.args(["bash", "../examples/test-empty-defaults.sh"]); let assert = cmd.assert().success(); diff --git a/cli/tests/shell_completions_integration.rs b/cli/tests/shell_completions_integration.rs index bd4e432a..cfa42251 100644 --- a/cli/tests/shell_completions_integration.rs +++ b/cli/tests/shell_completions_integration.rs @@ -1,23 +1,146 @@ use std::env; use std::fs; use std::path::{Path, PathBuf}; -use std::process::Command; +use std::process::{Command, Output}; +use std::sync::atomic::{AtomicUsize, Ordering}; -/// Returns `true` if the test should be skipped because the shell is not -/// installed. Panics under `CI=1` (or any non-empty `CI`) to prevent silent -/// false-positives in CI: if a shell is missing in CI it's a configuration -/// bug, not an excuse to skip the test. +/// The program to run for `shell`, honouring the same `USAGE_SHELL_` override the CLI +/// itself reads (`shell_program_override` in `cli/src/env.rs`). +/// +/// It matters on Windows, where the executable search order puts the system directory ahead of +/// `PATH` and installing WSL puts `bash.exe` there — so a bare `bash` is the WSL launcher, +/// which cannot open the Windows paths these tests write. Pointing the variable at a real bash +/// is what makes them runnable. Unset, which is every other platform, means the bare name. +fn shell_program(shell: &str) -> String { + env::var(format!("USAGE_SHELL_{}", shell.to_ascii_uppercase())) + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| shell.to_string()) +} + +/// A path as a POSIX shell will read it. +/// +/// These tests interpolate paths into script bodies — `source "{}"`, `export PATH="{}:$PATH"` — +/// where a Windows `\` is an escape character rather than a separator, so `C:\Users\x` arrives +/// as `C:Usersx`. Every shell that can run these scripts accepts `/` instead, and on Unix this +/// changes nothing. +fn sh_path(path: &Path) -> String { + path.display().to_string().replace('\\', "/") +} + +/// A path as it must appear inside the shell's `$PATH`. +/// +/// `$PATH` is colon-separated, so a Windows `C:/Users/…` splits into `C` and `/Users/…` and +/// neither resolves — a test that puts a directory there would silently look somewhere else. +/// The POSIX-flavoured shells that can run these scripts on Windows (Git Bash, MSYS2, Cygwin) +/// all ship `cygpath`, and only they know whether the answer is `/c/…`, `/cygdrive/c/…` or a +/// mount point set in fstab. Asking through `shell` rather than spawning `cygpath` directly +/// finds the one that belongs to the shell actually in use, which need not be on Windows' own +/// `PATH`. +/// +/// Off Windows, and if the conversion is unavailable, the path is already what `$PATH` wants. +fn path_var_entry(shell: &str, path: &Path) -> String { + if !cfg!(windows) { + return sh_path(path); + } + Command::new(shell_program(shell)) + .args(["-c", "cygpath -u \"$1\"", "--"]) + .arg(sh_path(path)) + .output() + .ok() + .filter(|out| out.status.success()) + .map(|out| String::from_utf8_lossy(&out.stdout).trim().to_string()) + .filter(|converted| !converted.is_empty()) + .unwrap_or_else(|| sh_path(path)) +} + +/// Run `script` under `shell`, giving up after `secs` seconds. +/// +/// `timeout` is spelled the same wherever these shells live, but spawning it directly walks +/// into the trap this whole file is about: Windows keeps an unrelated `timeout.exe` in the +/// system directory — a sleep, not a watchdog — and the search order puts it first. Going +/// through the shell resolves the one on *its* `PATH`, and reuses the program the guard +/// probed rather than a second, possibly different one. +fn run_with_timeout(shell: &str, secs: u32, script: &Path) -> std::io::Result { + let program = shell_program(shell); + Command::new(&program) + .arg("-c") + .arg(format!("timeout {secs} \"$0\" \"$1\"")) + .arg(&program) + .arg(sh_path(script)) + .output() +} + +/// Returns `true` if the test should be skipped because the shell cannot run a script. +/// +/// Not "is it installed": a WSL `bash` on Windows answers `--version` perfectly well and then +/// fails to open the very files these tests hand it, which is how five bash tests came to fail +/// there rather than skip. Running a script from a temp directory is the precondition every +/// test in this file actually needs, so that is what gets checked. +/// +/// It probes `shell_program(shell)`, which is also what every test here spawns — the guard and +/// the thing it guards must be the same executable, or an override would be honoured by one and +/// not the other. +/// +/// Panics under `CI=1` (or any non-empty `CI`) to prevent silent false-positives in CI: if a +/// shell is unusable in CI it's a configuration bug, not an excuse to skip the test. fn skip_if_shell_missing(shell: &str) -> bool { - if Command::new(shell).arg("--version").output().is_ok() { + if shell_can_run_a_script(shell) { return false; } if env::var("CI").is_ok_and(|v| !v.is_empty()) { - panic!("shell `{shell}` not installed but CI is set — refusing to skip"); + panic!("shell `{shell}` cannot run a script but CI is set — refusing to skip"); } - eprintln!("Skipping {shell} test - {shell} shell not installed"); + eprintln!( + "Skipping {shell} test - `{}` cannot run a script from {}", + shell_program(shell), + env::temp_dir().display() + ); true } +fn shell_can_run_a_script(shell: &str) -> bool { + static NEXT: AtomicUsize = AtomicUsize::new(0); + let dir = env::temp_dir().join(format!( + "usage_shell_probe_{}_{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + if fs::create_dir_all(&dir).is_err() { + return false; + } + // `echo ok` is the one thing bash, zsh, fish and pwsh all spell the same way. pwsh is the + // only one that insists on the extension. + let script = dir.join(if shell == "pwsh" { + "probe.ps1" + } else { + "probe" + }); + let usable = fs::write(&script, "echo ok\n").is_ok() + && script_command(shell, &script).output().is_ok_and(|out| { + out.status.success() && String::from_utf8_lossy(&out.stdout).trim() == "ok" + }); + let _ = fs::remove_dir_all(&dir); + usable +} + +/// The command that runs `script` under `shell`. +/// +/// One place, because the probe above and the tests it guards have to agree on more than the +/// program: a guard that invokes the shell differently can call a working shell unusable, or +/// miss one that is. pwsh is where they would have diverged — it needs `-File` to take a script +/// path at all, and `-NoProfile -NonInteractive` so a developer's profile cannot add output or +/// a prompt to a run whose stdout is being compared. +fn script_command(shell: &str, script: &Path) -> Command { + let mut cmd = Command::new(shell_program(shell)); + if shell == "pwsh" { + cmd.args(["-NoProfile", "-NonInteractive", "-File"]); + } + cmd.arg(sh_path(script)); + cmd +} + /// Path to a checked-in generated completion under `cli/assets/completions/`. /// These are what users actually source, so the shell-function collision tests /// assert against them rather than a freshly generated stand-in. @@ -196,17 +319,16 @@ end echo "COMPLETION_TEST_DONE" "#, - usage_bin.parent().unwrap().to_str().unwrap(), - comp_file.to_str().unwrap(), - temp_dir.to_str().unwrap() + sh_path(usage_bin.parent().unwrap()), + sh_path(&comp_file), + sh_path(&temp_dir) ); let script_file = temp_dir.join("test.fish"); fs::write(&script_file, &test_script).unwrap(); // Execute the test in fish - let result = Command::new("fish") - .arg(script_file.to_str().unwrap()) + let result = script_command("fish", &script_file) .output() .expect("Failed to run fish test"); @@ -384,17 +506,16 @@ fi echo "COMPLETION_TEST_DONE" "#, - usage_bin.parent().unwrap().to_str().unwrap(), - comp_file.to_str().unwrap(), - temp_dir.to_str().unwrap() + path_var_entry("bash", usage_bin.parent().unwrap()), + sh_path(&comp_file), + sh_path(&temp_dir) ); let script_file = temp_dir.join("test.sh"); fs::write(&script_file, &test_script).unwrap(); // Execute the test - let result = Command::new("bash") - .arg(script_file.to_str().unwrap()) + let result = script_command("bash", &script_file) .output() .expect("Failed to run bash test"); @@ -539,21 +660,16 @@ zpty -d comptest echo "COMPLETION_TEST_DONE" "#, - usage_bin.parent().unwrap().to_str().unwrap(), - temp_dir.to_str().unwrap(), - comp_file.to_str().unwrap() + path_var_entry("zsh", usage_bin.parent().unwrap()), + sh_path(&temp_dir), + sh_path(&comp_file) ); let script_file = temp_dir.join("test.zsh"); fs::write(&script_file, &test_script).unwrap(); - // Execute the test with a timeout - let result = Command::new("timeout") - .arg("5") // 5 second timeout - .arg("zsh") - .arg(script_file.to_str().unwrap()) - .output() - .expect("Failed to run zsh test"); + // Execute the test with a timeout: the script drives a pty, which can hang. + let result = run_with_timeout("zsh", 5, &script_file).expect("Failed to run zsh test"); let stdout = String::from_utf8_lossy(&result.stdout); let stderr = String::from_utf8_lossy(&result.stderr); @@ -727,16 +843,15 @@ words=(testcli "") CURRENT=2 _testcli "#, - usage_dir = usage_bin.parent().unwrap().to_str().unwrap(), - tmp = temp_dir.to_str().unwrap(), - comp = comp_file.to_str().unwrap(), + usage_dir = path_var_entry("zsh", usage_bin.parent().unwrap()), + tmp = sh_path(&temp_dir), + comp = sh_path(&comp_file), ); let script_file = temp_dir.join("test.zsh"); fs::write(&script_file, &test_script).unwrap(); - let result = Command::new("zsh") - .arg(script_file.to_str().unwrap()) + let result = script_command("zsh", &script_file) .output() .expect("Failed to run zsh test"); let stdout = String::from_utf8_lossy(&result.stdout); @@ -815,16 +930,15 @@ words=(testcli d) CURRENT=2 _testcli "#, - usage_dir = usage_bin.parent().unwrap().to_str().unwrap(), - tmp = temp_dir.to_str().unwrap(), - comp = comp_file.to_str().unwrap(), + usage_dir = path_var_entry("zsh", usage_bin.parent().unwrap()), + tmp = sh_path(&temp_dir), + comp = sh_path(&comp_file), ); let script_file = temp_dir.join("test.zsh"); fs::write(&script_file, &test_script).unwrap(); - let result = Command::new("zsh") - .arg(script_file.to_str().unwrap()) + let result = script_command("zsh", &script_file) .output() .expect("Failed to run zsh test"); let stdout = String::from_utf8_lossy(&result.stdout); @@ -949,19 +1063,17 @@ if ($flagStr -match "verbose" -or $flagStr -match "-v") {{ Write-Host "COMPLETION_TEST_DONE" "#, - usage_bin.parent().unwrap().to_str().unwrap(), - temp_dir.to_str().unwrap(), - comp_file.to_str().unwrap(), - spec_file.to_str().unwrap() + sh_path(usage_bin.parent().unwrap()), + sh_path(&temp_dir), + sh_path(&comp_file), + sh_path(&spec_file) ); let script_file = temp_dir.join("test.ps1"); fs::write(&script_file, &test_script).unwrap(); // Execute the test in PowerShell - let result = Command::new("pwsh") - .args(["-NoProfile", "-NonInteractive", "-File"]) - .arg(script_file.to_str().unwrap()) + let result = script_command("pwsh", &script_file) .output() .expect("Failed to run pwsh - PowerShell Core must be installed for this test"); @@ -1099,15 +1211,14 @@ run_case empty 1 ex "" run_case dashes 1 ex "--" run_case foo 1 ex "--f" "#, - bin_dir = bin_dir.display(), - usage_dir = usage_bin.parent().unwrap().display(), - init_script = init_script.display(), + bin_dir = path_var_entry("bash", &bin_dir), + usage_dir = path_var_entry("bash", usage_bin.parent().unwrap()), + init_script = sh_path(&init_script), ); let script_file = temp_dir.join("test.sh"); fs::write(&script_file, &test_script).unwrap(); - let result = Command::new("bash") - .arg(script_file.to_str().unwrap()) + let result = script_command("bash", &script_file) .output() .expect("Failed to run bash init test"); let stdout = String::from_utf8_lossy(&result.stdout); @@ -1177,15 +1288,14 @@ words=(plain "") CURRENT=2 _usage_default_complete "#, - bin_dir = bin_dir.display(), - usage_dir = usage_bin.parent().unwrap().display(), - init_script = init_script.display(), + bin_dir = path_var_entry("zsh", &bin_dir), + usage_dir = path_var_entry("zsh", usage_bin.parent().unwrap()), + init_script = sh_path(&init_script), ); let script_file = temp_dir.join("test.zsh"); fs::write(&script_file, &test_script).unwrap(); - let result = Command::new("zsh") - .arg(script_file.to_str().unwrap()) + let result = script_command("zsh", &script_file) .output() .expect("Failed to run zsh init test"); let stdout = String::from_utf8_lossy(&result.stdout); @@ -1281,17 +1391,16 @@ words=(testcli "") CURRENT=2 _usage_default_complete "#, - bin_dir = bin_dir.to_str().unwrap(), - usage_dir = usage_bin.parent().unwrap().to_str().unwrap(), - tmp = temp_dir.to_str().unwrap(), - init = init_script.to_str().unwrap(), + bin_dir = path_var_entry("zsh", &bin_dir), + usage_dir = path_var_entry("zsh", usage_bin.parent().unwrap()), + tmp = sh_path(&temp_dir), + init = sh_path(&init_script), ); let script_file = temp_dir.join("test.zsh"); fs::write(&script_file, &test_script).unwrap(); - let result = Command::new("zsh") - .arg(script_file.to_str().unwrap()) + let result = script_command("zsh", &script_file) .output() .expect("Failed to run zsh init test"); let stdout = String::from_utf8_lossy(&result.stdout); @@ -1346,15 +1455,14 @@ echo "[registered] ex" echo "[empty]" (complete -C 'ex ') echo "[foo]" (complete -C 'ex --f') "#, - bin_dir = bin_dir.display(), - usage_dir = usage_bin.parent().unwrap().display(), - init_script = init_script.display(), + bin_dir = sh_path(&bin_dir), + usage_dir = sh_path(usage_bin.parent().unwrap()), + init_script = sh_path(&init_script), ); let script_file = temp_dir.join("test.fish"); fs::write(&script_file, &test_script).unwrap(); - let result = Command::new("fish") - .arg(script_file.to_str().unwrap()) + let result = script_command("fish", &script_file) .output() .expect("Failed to run fish init test"); let stdout = String::from_utf8_lossy(&result.stdout); @@ -1433,13 +1541,12 @@ source "{comp}" _testcli testcli "" testcli 1 echo "GUARD_EXIT=$?" "#, - comp = comp_file.display(), + comp = sh_path(&comp_file), ); let script_file = temp_dir.join("test.sh"); fs::write(&script_file, &test_script).unwrap(); - let result = Command::new("bash") - .arg(script_file.to_str().unwrap()) + let result = script_command("bash", &script_file) .output() .expect("Failed to run bash guard test"); let stdout = String::from_utf8_lossy(&result.stdout); @@ -1484,15 +1591,14 @@ _usage usage "" usage 1 echo "SPEC_BEGIN" cat "$XDG_CACHE_HOME/usage/usage__usage_spec_usage.spec" "#, - usage_dir = usage_bin.parent().unwrap().display(), - cache = temp_dir.display(), - asset = checked_in_completion("usage.bash").display(), + usage_dir = path_var_entry("bash", usage_bin.parent().unwrap()), + cache = sh_path(&temp_dir), + asset = sh_path(&checked_in_completion("usage.bash")), ); let script_file = temp_dir.join("test.sh"); fs::write(&script_file, &test_script).unwrap(); - let result = Command::new("bash") - .arg(script_file.to_str().unwrap()) + let result = script_command("bash", &script_file) .output() .expect("Failed to run bash shadow test"); let stdout = String::from_utf8_lossy(&result.stdout); @@ -1537,14 +1643,13 @@ COMPREPLY=() _usage_default_complete ex "" ex echo "INIT_EXIT=$?" "#, - bin_dir = bin_dir.display(), - init_script = init_script.display(), + bin_dir = path_var_entry("bash", &bin_dir), + init_script = sh_path(&init_script), ); let script_file = temp_dir.join("test.sh"); fs::write(&script_file, &test_script).unwrap(); - let result = Command::new("bash") - .arg(script_file.to_str().unwrap()) + let result = script_command("bash", &script_file) .output() .expect("Failed to run bash init guard test"); let stdout = String::from_utf8_lossy(&result.stdout); @@ -1596,13 +1701,12 @@ end source "{comp}" echo "[completions]" (complete -c testcli | string collect) "#, - comp = comp_file.display(), + comp = sh_path(&comp_file), ); let script_file = temp_dir.join("test.fish"); fs::write(&script_file, &test_script).unwrap(); - let result = Command::new("fish") - .arg(script_file.to_str().unwrap()) + let result = script_command("fish", &script_file) .output() .expect("Failed to run fish guard test"); let stdout = String::from_utf8_lossy(&result.stdout); @@ -1641,15 +1745,14 @@ source "{asset}" echo "SPEC_BEGIN" cat "$XDG_CACHE_HOME/usage/usage__usage_spec_usage.spec" "#, - usage_dir = usage_bin.parent().unwrap().display(), - cache = temp_dir.display(), - asset = checked_in_completion("usage.fish").display(), + usage_dir = sh_path(usage_bin.parent().unwrap()), + cache = sh_path(&temp_dir), + asset = sh_path(&checked_in_completion("usage.fish")), ); let script_file = temp_dir.join("test.fish"); fs::write(&script_file, &test_script).unwrap(); - let result = Command::new("fish") - .arg(script_file.to_str().unwrap()) + let result = script_command("fish", &script_file) .output() .expect("Failed to run fish shadow test"); let stdout = String::from_utf8_lossy(&result.stdout); @@ -1688,14 +1791,13 @@ end source "{init_script}" echo "[registered]" (complete -c ex | string collect) "#, - bin_dir = bin_dir.display(), - init_script = init_script.display(), + bin_dir = sh_path(&bin_dir), + init_script = sh_path(&init_script), ); let script_file = temp_dir.join("test.fish"); fs::write(&script_file, &test_script).unwrap(); - let result = Command::new("fish") - .arg(script_file.to_str().unwrap()) + let result = script_command("fish", &script_file) .output() .expect("Failed to run fish init guard test"); let stdout = String::from_utf8_lossy(&result.stdout); diff --git a/cli/tests/shell_override.rs b/cli/tests/shell_override.rs index a8caa038..2f65077b 100644 --- a/cli/tests/shell_override.rs +++ b/cli/tests/shell_override.rs @@ -1,81 +1,123 @@ //! `USAGE_SHELL_` replaces the program `usage ` runs. //! -//! Unix-only because they pin the substitute to a real path (`/bin/echo`, `/bin/sh`). The -//! variable itself is not Unix-specific — it exists for Windows, where `bash` resolves to the -//! WSL launcher ahead of `PATH` — but the CI that runs these is Linux. - -#![cfg(unix)] +//! Runs on every platform, Windows above all: that is where `bash` resolves to the WSL +//! launcher ahead of `PATH`, which is the reason the variable exists. Nothing here assumes the +//! default shell works, because on the platform this feature is for, it may well not. use assert_cmd::cargo; use assert_cmd::prelude::*; -use predicates::prelude::*; use predicates::str::contains; -use std::process::Command; +use std::process::{Command, Output}; const SCRIPT: &str = "../examples/test-new-usage-syntax.sh"; -/// `usage bash