Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
42 changes: 32 additions & 10 deletions cli/tests/complete_word.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
// 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
Expand All @@ -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]
Expand Down
9 changes: 6 additions & 3 deletions cli/tests/shell_completions_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,16 @@ fn run_with_timeout(shell: &str, secs: u32, script: &Path) -> std::io::Result<Ou
/// 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.
/// Panics under `CI=1` (or any non-empty `CI`) to prevent silent false-positives: a shell that
/// is unusable on a runner which went to the trouble of installing it is a configuration bug,
/// not an excuse to skip. Only the Linux job installs zsh and fish, so on Windows their absence
/// is the expected state rather than a bug and the rule does not apply — mise draws the same
/// line, installing no POSIX shells on its Windows runners at all.
fn skip_if_shell_missing(shell: &str) -> 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!(
Expand Down
Loading