From d33bfd111fda1c8580a52904356ff239bce42c19 Mon Sep 17 00:00:00 2001 From: Ajeet D'Souza <98ajeet@gmail.com> Date: Fri, 7 May 2021 13:04:44 +0530 Subject: [PATCH] Add tests for completions (#204) --- .github/workflows/ci.yml | 6 ++-- Makefile | 14 ++++---- build.rs | 5 ++- src/{app.rs => app/_app.rs} | 0 src/{cmd => app}/add.rs | 0 src/{cmd => app}/import.rs | 0 src/{cmd => app}/init.rs | 2 +- src/{cmd => app}/mod.rs | 3 +- src/{cmd => app}/query.rs | 8 ++--- src/{cmd => app}/remove.rs | 2 +- src/error.rs | 4 +-- src/fzf.rs | 13 +++++-- src/main.rs | 4 +-- src/shell.rs | 29 ++++++++------- tests/completion.rs | 71 +++++++++++++++++++++++++++++++++++++ 15 files changed, 119 insertions(+), 42 deletions(-) rename src/{app.rs => app/_app.rs} (100%) rename src/{cmd => app}/add.rs (100%) rename src/{cmd => app}/import.rs (100%) rename src/{cmd => app}/init.rs (94%) rename src/{cmd => app}/mod.rs (91%) rename src/{cmd => app}/query.rs (93%) rename src/{cmd => app}/remove.rs (98%) create mode 100644 tests/completion.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b3d8e7..bcadc42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,5 @@ jobs: - uses: cachix/install-nix-action@v12 if: ${{ matrix.os != 'windows-latest' }} with: - nix_path: nixpkgs=channel:nixos-stable - - run: mkdir -p /tmp/home && make test - env: - HOME: /tmp/home + nix_path: nixpkgs=https://github.com/NixOS/nixpkgs/archive/20.09.tar.gz + - run: make test diff --git a/Makefile b/Makefile index e600c1e..3f5b180 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ endif .PHONY: build clean install test uninstall build: - cargo build --release $(ci_color_always) + cargo build $(ci_color_always) clean: cargo clean $(ci_color_always) @@ -22,16 +22,16 @@ install: ifeq ($(NIX), true) test: nix-shell --pure --run 'cargo fmt -- --check --files-with-diff $(ci_color_always)' - nix-shell --pure --run 'cargo check --all-features --release $(ci_color_always)' - nix-shell --pure --run 'cargo clippy --all-features --release $(ci_color_always) -- --deny warnings --deny clippy::all' - nix-shell --pure --run 'cargo test --all-features --no-fail-fast --release $(ci_color_always)' + nix-shell --pure --run 'cargo check --all-features $(ci_color_always)' + nix-shell --pure --run 'cargo clippy --all-features $(ci_color_always) -- --deny warnings --deny clippy::all' + nix-shell --pure --run 'cargo test --all-features --no-fail-fast $(ci_color_always)' nix-shell --pure --run 'cargo audit --deny warnings $(ci_color_always) --ignore=RUSTSEC-2020-0095' else test: cargo fmt -- --check --files-with-diff $(ci_color_always) - cargo check --all-features --release $(ci_color_always) - cargo clippy --all-features --release $(ci_color_always) -- --deny warnings --deny clippy::all - cargo test --no-fail-fast --release $(ci_color_always) + cargo check --all-features $(ci_color_always) + cargo clippy --all-features $(ci_color_always) -- --deny warnings --deny clippy::all + cargo test --no-fail-fast $(ci_color_always) cargo audit --deny warnings $(ci_color_always) --ignore=RUSTSEC-2020-0095 endif diff --git a/build.rs b/build.rs index dd5e40c..ccb77d8 100644 --- a/build.rs +++ b/build.rs @@ -19,9 +19,8 @@ fn crate_version() -> String { } fn generate_completions() { - mod app { - include!("src/app.rs"); - } + #[path = "src/app/_app.rs"] + mod app; use app::App; use clap::IntoApp; diff --git a/src/app.rs b/src/app/_app.rs similarity index 100% rename from src/app.rs rename to src/app/_app.rs diff --git a/src/cmd/add.rs b/src/app/add.rs similarity index 100% rename from src/cmd/add.rs rename to src/app/add.rs diff --git a/src/cmd/import.rs b/src/app/import.rs similarity index 100% rename from src/cmd/import.rs rename to src/app/import.rs diff --git a/src/cmd/init.rs b/src/app/init.rs similarity index 94% rename from src/cmd/init.rs rename to src/app/init.rs index 1d9718e..d9c2965 100644 --- a/src/cmd/init.rs +++ b/src/app/init.rs @@ -38,6 +38,6 @@ impl Run for Init { InitShell::Zsh => shell::Zsh(opts).render(), } .context("could not render template")?; - writeln!(io::stdout(), "{}", source).wrap_write("stdout") + writeln!(io::stdout(), "{}", source).pipe_exit("stdout") } } diff --git a/src/cmd/mod.rs b/src/app/mod.rs similarity index 91% rename from src/cmd/mod.rs rename to src/app/mod.rs index f41c576..b6c17a2 100644 --- a/src/cmd/mod.rs +++ b/src/app/mod.rs @@ -1,10 +1,11 @@ +mod _app; mod add; mod import; mod init; mod query; mod remove; -use crate::app::App; +pub use crate::app::_app::*; use anyhow::Result; diff --git a/src/cmd/query.rs b/src/app/query.rs similarity index 93% rename from src/cmd/query.rs rename to src/app/query.rs index 7e8bd33..1381315 100644 --- a/src/cmd/query.rs +++ b/src/app/query.rs @@ -27,7 +27,7 @@ impl Run for Query { if self.interactive { let mut fzf = Fzf::new(false)?; for dir in matches { - writeln!(fzf.stdin(), "{}", dir.display_score(now)).wrap_write("fzf")?; + writeln!(fzf.stdin(), "{}", dir.display_score(now)).pipe_exit("fzf")?; } let selection = fzf.wait_select()?; @@ -53,9 +53,9 @@ impl Run for Query { } else { writeln!(handle, "{}", dir.display()) } - .wrap_write("stdout")?; + .pipe_exit("stdout")?; } - handle.flush().wrap_write("stdout")?; + handle.flush().pipe_exit("stdout")?; } else { let dir = matches.next().context("no match found")?; if self.score { @@ -63,7 +63,7 @@ impl Run for Query { } else { writeln!(io::stdout(), "{}", dir.display()) } - .wrap_write("stdout")?; + .pipe_exit("stdout")?; } Ok(()) diff --git a/src/cmd/remove.rs b/src/app/remove.rs similarity index 98% rename from src/cmd/remove.rs rename to src/app/remove.rs index a912385..9f69c1d 100644 --- a/src/cmd/remove.rs +++ b/src/app/remove.rs @@ -25,7 +25,7 @@ impl Run for Remove { let mut fzf = Fzf::new(true)?; for dir in db.iter_matches(&query, now, resolve_symlinks) { - writeln!(fzf.stdin(), "{}", dir.display_score(now)).wrap_write("fzf")?; + writeln!(fzf.stdin(), "{}", dir.display_score(now)).pipe_exit("fzf")?; } selection = fzf.wait_select()?; diff --git a/src/error.rs b/src/error.rs index 5948a37..a1942cd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,11 +16,11 @@ impl Display for SilentExit { } pub trait WriteErrorHandler { - fn wrap_write(self, device: &str) -> Result<()>; + fn pipe_exit(self, device: &str) -> Result<()>; } impl WriteErrorHandler for io::Result<()> { - fn wrap_write(self, device: &str) -> Result<()> { + fn pipe_exit(self, device: &str) -> Result<()> { match self { Err(e) if e.kind() == io::ErrorKind::BrokenPipe => bail!(SilentExit { code: 0 }), result => result.with_context(|| format!("could not write to {}", device)), diff --git a/src/fzf.rs b/src/fzf.rs index 9f692c3..2e87bad 100644 --- a/src/fzf.rs +++ b/src/fzf.rs @@ -3,6 +3,7 @@ use crate::error::SilentExit; use anyhow::{bail, Context, Result}; +use std::io; use std::process::{Child, ChildStdin, Command, Stdio}; pub struct Fzf { @@ -23,9 +24,15 @@ impl Fzf { command.env("FZF_DEFAULT_OPTS", fzf_opts); } - Ok(Fzf { - child: command.spawn().context("could not launch fzf")?, - }) + let child = match command.spawn() { + Ok(child) => child, + Err(e) if e.kind() == io::ErrorKind::NotFound => { + bail!("could not find fzf, is it installed?") + } + Err(e) => Err(e).context("could not launch fzf")?, + }; + + Ok(Fzf { child }) } pub fn stdin(&mut self) -> &mut ChildStdin { diff --git a/src/main.rs b/src/main.rs index 5eb52ef..3f8d0e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ mod app; -mod cmd; mod config; mod db; mod error; @@ -7,8 +6,7 @@ mod fzf; mod shell; mod util; -use crate::app::App; -use crate::cmd::Run; +use crate::app::{App, Run}; use crate::error::SilentExit; use clap::Clap; diff --git a/src/shell.rs b/src/shell.rs index eb83c8f..c4dfabc 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -60,13 +60,12 @@ mod tests { for &resolve_symlinks in BOOLS { for &hook in HOOKS { for &cmd in CMDS { - let opt = Opts { + opts.push(Opts { cmd, hook, echo, resolve_symlinks, - }; - opts.push(opt); + }); } } } @@ -84,7 +83,7 @@ mod tests { }) } - macro_rules! generate_tests { + macro_rules! make_tests { ($N:literal) => { seq!(i in 0..$N { #[test] @@ -92,7 +91,7 @@ mod tests { let opts = dbg!(&opts()[i]); let source = Bash(opts).render().unwrap(); Command::new("bash") - .args(&["-c", &source, "--noediting", "--noprofile", "--norc"]) + .args(&["--noprofile", "--norc", "-c", &source]) .assert() .success() .stdout("") @@ -213,7 +212,7 @@ mod tests { let opts = dbg!(&opts()[i]); let source = Posix(opts).render().unwrap(); let assert = Command::new("bash") - .args(&["--posix", "-c", &source, "--noediting", "--noprofile", "--norc"]) + .args(&["--posix", "--noprofile", "--norc", "-c", &source]) .assert() .success() .stderr(""); @@ -256,7 +255,6 @@ mod tests { let opts = dbg!(&opts()[i]); let mut source = Posix(opts).render().unwrap(); source.push('\n'); - Command::new("shfmt") .args(&["-d", "-s", "-ln", "posix", "-i", "4", "-ci", "-"]) .write_stdin(source) @@ -271,7 +269,7 @@ mod tests { let opts = dbg!(&opts()[i]); let source = Powershell(opts).render().unwrap(); Command::new("pwsh") - .args(&["-Command", &source, "-NoLogo", "-NonInteractive", "-NoProfile"]) + .args(&["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", &source]) .assert() .success() .stdout("") @@ -316,14 +314,19 @@ mod tests { } #[test] - // Xonsh complains about type-hinting here, although it works fine in practice. - // - #[ignore] fn xonsh_xonsh_#i() { let opts = dbg!(&opts()[i]); let source = Xonsh(opts).render().unwrap(); + + // We can't pass the source directly to `xonsh -c` due to + // a bug: Command::new("xonsh") - .args(&["-c", &source, "--no-rc"]) + .args(&[ + "-c", + "import sys; execx(sys.stdin.read(), 'exec', __xonsh__.ctx, filename='zoxide')", + "--no-rc" + ]) + .write_stdin(source.as_bytes()) .assert() .success() .stdout("") @@ -360,5 +363,5 @@ mod tests { } } - with_opts_size!(generate_tests); + with_opts_size!(make_tests); } diff --git a/tests/completion.rs b/tests/completion.rs new file mode 100644 index 0000000..f9136bb --- /dev/null +++ b/tests/completion.rs @@ -0,0 +1,71 @@ +//! Syntax checking for auto-generated shell completions. + +#![cfg(feature = "shell_tests")] +use assert_cmd::Command; + +#[test] +fn completions_bash() { + let source = include_str!("../contrib/completions/zoxide.bash"); + Command::new("bash") + .args(&["--noprofile", "--norc", "-c", source]) + .assert() + .success() + .stdout("") + .stderr(""); +} + +// Elvish: the completions file uses editor commands to add completions to the +// shell. However, Elvish does not support running editor commands from a +// script, so we can't create a test for this. +// + +#[test] +fn completions_fish() { + let source = include_str!("../contrib/completions/zoxide.fish"); + let tempdir = tempfile::tempdir().unwrap(); + let tempdir = tempdir.path().to_str().unwrap(); + + Command::new("fish") + .env("HOME", tempdir) + .args(&["--command", source, "--private"]) + .assert() + .success() + .stdout("") + .stderr(""); +} + +#[test] +fn completions_powershell() { + let source = include_str!("../contrib/completions/_zoxide.ps1"); + Command::new("pwsh") + .args(&[ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + source, + ]) + .assert() + .success() + .stdout("") + .stderr(""); +} + +#[test] +fn completions_zsh() { + let source = r#" + set -eu + completions='./contrib/completions' + test -d "$completions" + fpath=("$completions" $fpath) + autoload -Uz compinit + compinit -u + "#; + + Command::new("zsh") + .args(&["-c", source, "--no-rcs"]) + .assert() + .success() + .stdout("") + .stderr(""); +}