From e75c72ea65f91757e93211bf75aff3206c4c513e Mon Sep 17 00:00:00 2001 From: JamBalaya56562 Date: Sun, 9 Aug 2026 16:54:15 +0900 Subject: [PATCH] ci: run the test suite on windows-latest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit usage is used on Windows and released for it, but nothing has been testing it there. #771 made the suite runnable on the platform; this runs it. Build and test only. `render` asserts a clean git diff, which line endings and the checked-in symlinks make unreliable, and `lint` is a property of the source rather than of the platform — the Linux job already covers both. mise's Windows jobs are shaped the same way. Two settings the job needs, both measured on a runner rather than assumed: `core.autocrlf false`, set before checkout. git defaults it to true on Windows and the repo has no .gitattributes, so fixtures and expected output arrive with CRLF. Seven help tests in lib/tests/parse.rs and two in cli/tests/markdown.rs fail on it, and the failure is near-unreadable: pretty_assertions prints both sides as identical, because the only difference is invisible. `USAGE_SHELL_BASH`, because the image carries System32's bash.exe — the WSL launcher, with no distribution installed. `where bash` lists Git Bash first and CreateProcess still reaches System32, since it searches the system directory before PATH. Without the variable `usage bash` exits 1 with "Windows Subsystem for Linux has no installed distributions". This is the case USAGE_SHELL_BASH was added for in #767, and the runner turns out to be an example of it. zsh and fish are not installed, and are not worth installing: nothing on Windows ships them, and the completion tests skip a shell they cannot run. So the `CI` rule in `skip_if_shell_missing` — which refuses to skip, on the grounds that a missing shell on a runner that installs one is a configuration bug — now applies on Unix only. Only the Linux job installs shells, so only there is absence a bug. mise draws the same line, installing no POSIX shells on Windows at all. Measured on windows-latest: 538 passed, 0 failed, 0 skipped. --- .github/workflows/test.yml | 42 ++++++++++++++++++++++ cli/tests/complete_word.rs | 42 ++++++++++++++++------ cli/tests/shell_completions_integration.rs | 9 +++-- 3 files changed, 80 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6eabf9e7..be60ab87 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,3 +50,45 @@ jobs: exit 1 fi - run: mise r lint + + # Build and test only. `render` asserts a clean git diff, which line endings and the + # checked-in symlinks make unreliable here, and `lint` is a property of the source rather + # than of the platform — the Linux job already covers both. + # + # No shells are installed. The image has Git Bash and PowerShell but no zsh or fish, and the + # completion tests skip a shell they cannot run. mise makes the same call: its Windows jobs + # install no POSIX shells at all. + test-windows: + runs-on: windows-latest + permissions: + contents: read + steps: + # Before checkout, or it is too late. git defaults to core.autocrlf=true on Windows and + # the repo has no .gitattributes, so fixtures and expected output would arrive with CRLF + # and comparisons against checked-in strings fail on an ending nobody can see — the diff + # prints the two sides as identical. + - run: git config --global core.autocrlf false + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: recursive + persist-credentials: false + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + shared-key: test-windows + - uses: jdx/mise-action@9e7f7633ff6f6d6048a9418a68d48f288f50eb14 # v4.2.3 + - run: mise r build + # `cargo test` rather than `mise r test`, which is the same command, because a mise task + # cannot receive USAGE_SHELL_BASH. mise clears `usage_*` from a task's environment so + # that its own parsed arguments do not leak in, and on Windows that match is + # case-insensitive (`is_usage_env_key` in mise's src/task/mod.rs) — correct for a + # platform where `usage_foo` and `USAGE_FOO` are one variable, but it takes usage's + # settings namespace along with its parsed-argument one. Measured: under `mise run` the + # variable arrives empty, under `mise exec` and plain cargo it arrives intact. + - run: cargo test --all --all-features + env: + # The image carries System32's bash.exe — the WSL launcher, with no distribution + # installed — and CreateProcess searches the system directory before PATH, so a bare + # `bash` reaches it even though `where bash` lists Git Bash first. Measured: without + # this, `usage bash` exits 1 with "Windows Subsystem for Linux has no installed + # distributions". Naming the shell is what USAGE_SHELL_BASH exists for. + USAGE_SHELL_BASH: C:\Program Files\Git\bin\bash.exe diff --git a/cli/tests/complete_word.rs b/cli/tests/complete_word.rs index 5a5f3dcf..74d2f43b 100644 --- a/cli/tests/complete_word.rs +++ b/cli/tests/complete_word.rs @@ -34,18 +34,30 @@ fn skip_if_posix_shell_missing() -> bool { .arg("exit 0") .output() .is_ok_and(|out| out.status.success()); - if sh_runs && mount_fixture_runs() { + // Which half failed, because the two mean different things: no `sh` at all is a machine + // without a POSIX shell, while `sh` running and the fixture not is usually `usage bash` + // reaching something that cannot open the path — the case `USAGE_SHELL_BASH` settles. + let reason = if !sh_runs { + "`sh` cannot run a script".to_string() + } else if let Some(detail) = mount_fixture_failure() { + format!("`sh` runs but a mount fixture does not — {detail}") + } else { return false; - } + }; if env::var("CI").is_ok_and(|v| !v.is_empty()) { - panic!("no shell that can run the mount fixtures but CI is set — refusing to skip"); + panic!("{reason}, and CI is set — refusing to skip"); } - eprintln!("Skipping test - no shell that can run the mount fixtures"); + eprintln!("Skipping test - {reason}"); true } -/// Whether a mount fixture actually runs, given as the absolute path `sh` would hand it. -fn mount_fixture_runs() -> bool { +/// `None` if a mount fixture runs, given as the absolute path `sh` would hand it; otherwise +/// what went wrong. +/// +/// The detail is carried rather than dropped because this guard panics under `CI`, and a bare +/// "no usable shell" there leaves whoever reads the log with nothing to go on — the failure is +/// in a child process whose output would otherwise be discarded. +fn mount_fixture_failure() -> Option { // 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 @@ -55,12 +67,22 @@ fn mount_fixture_runs() -> bool { .unwrap() .join("examples") .join("mounted.sh"); - Command::new(cargo::cargo_bin!("usage")) + let out = Command::new(cargo::cargo_bin!("usage")) .arg("bash") - .arg(fixture) + .arg(&fixture) .arg("--mount") - .output() - .is_ok_and(|out| out.status.success()) + .output(); + match out { + Ok(out) if out.status.success() => None, + Ok(out) => Some(format!( + "`usage bash {}` exited {:?}; stderr: {}; stdout: {}", + fixture.display(), + out.status.code(), + String::from_utf8_lossy(&out.stderr).trim(), + String::from_utf8_lossy(&out.stdout).trim(), + )), + Err(err) => Some(format!("could not start the usage binary: {err}")), + } } #[test] diff --git a/cli/tests/shell_completions_integration.rs b/cli/tests/shell_completions_integration.rs index cfa42251..871c1bcf 100644 --- a/cli/tests/shell_completions_integration.rs +++ b/cli/tests/shell_completions_integration.rs @@ -83,13 +83,16 @@ fn run_with_timeout(shell: &str, secs: u32, script: &Path) -> std::io::Result bool { if shell_can_run_a_script(shell) { return false; } - if env::var("CI").is_ok_and(|v| !v.is_empty()) { + if cfg!(unix) && env::var("CI").is_ok_and(|v| !v.is_empty()) { panic!("shell `{shell}` cannot run a script but CI is set — refusing to skip"); } eprintln!(