Add tests for completions (#204)

This commit is contained in:
Ajeet D'Souza 2021-05-07 13:04:44 +05:30 committed by GitHub
parent efe11ec924
commit d33bfd111f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 119 additions and 42 deletions

View File

@ -20,7 +20,5 @@ jobs:
- uses: cachix/install-nix-action@v12 - uses: cachix/install-nix-action@v12
if: ${{ matrix.os != 'windows-latest' }} if: ${{ matrix.os != 'windows-latest' }}
with: with:
nix_path: nixpkgs=channel:nixos-stable nix_path: nixpkgs=https://github.com/NixOS/nixpkgs/archive/20.09.tar.gz
- run: mkdir -p /tmp/home && make test - run: make test
env:
HOME: /tmp/home

View File

@ -11,7 +11,7 @@ endif
.PHONY: build clean install test uninstall .PHONY: build clean install test uninstall
build: build:
cargo build --release $(ci_color_always) cargo build $(ci_color_always)
clean: clean:
cargo clean $(ci_color_always) cargo clean $(ci_color_always)
@ -22,16 +22,16 @@ install:
ifeq ($(NIX), true) ifeq ($(NIX), true)
test: test:
nix-shell --pure --run 'cargo fmt -- --check --files-with-diff $(ci_color_always)' 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 check --all-features $(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 clippy --all-features $(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 test --all-features --no-fail-fast $(ci_color_always)'
nix-shell --pure --run 'cargo audit --deny warnings $(ci_color_always) --ignore=RUSTSEC-2020-0095' nix-shell --pure --run 'cargo audit --deny warnings $(ci_color_always) --ignore=RUSTSEC-2020-0095'
else else
test: test:
cargo fmt -- --check --files-with-diff $(ci_color_always) cargo fmt -- --check --files-with-diff $(ci_color_always)
cargo check --all-features --release $(ci_color_always) cargo check --all-features $(ci_color_always)
cargo clippy --all-features --release $(ci_color_always) -- --deny warnings --deny clippy::all cargo clippy --all-features $(ci_color_always) -- --deny warnings --deny clippy::all
cargo test --no-fail-fast --release $(ci_color_always) cargo test --no-fail-fast $(ci_color_always)
cargo audit --deny warnings $(ci_color_always) --ignore=RUSTSEC-2020-0095 cargo audit --deny warnings $(ci_color_always) --ignore=RUSTSEC-2020-0095
endif endif

View File

@ -19,9 +19,8 @@ fn crate_version() -> String {
} }
fn generate_completions() { fn generate_completions() {
mod app { #[path = "src/app/_app.rs"]
include!("src/app.rs"); mod app;
}
use app::App; use app::App;
use clap::IntoApp; use clap::IntoApp;

View File

@ -38,6 +38,6 @@ impl Run for Init {
InitShell::Zsh => shell::Zsh(opts).render(), InitShell::Zsh => shell::Zsh(opts).render(),
} }
.context("could not render template")?; .context("could not render template")?;
writeln!(io::stdout(), "{}", source).wrap_write("stdout") writeln!(io::stdout(), "{}", source).pipe_exit("stdout")
} }
} }

View File

@ -1,10 +1,11 @@
mod _app;
mod add; mod add;
mod import; mod import;
mod init; mod init;
mod query; mod query;
mod remove; mod remove;
use crate::app::App; pub use crate::app::_app::*;
use anyhow::Result; use anyhow::Result;

View File

@ -27,7 +27,7 @@ impl Run for Query {
if self.interactive { if self.interactive {
let mut fzf = Fzf::new(false)?; let mut fzf = Fzf::new(false)?;
for dir in matches { 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()?; let selection = fzf.wait_select()?;
@ -53,9 +53,9 @@ impl Run for Query {
} else { } else {
writeln!(handle, "{}", dir.display()) writeln!(handle, "{}", dir.display())
} }
.wrap_write("stdout")?; .pipe_exit("stdout")?;
} }
handle.flush().wrap_write("stdout")?; handle.flush().pipe_exit("stdout")?;
} else { } else {
let dir = matches.next().context("no match found")?; let dir = matches.next().context("no match found")?;
if self.score { if self.score {
@ -63,7 +63,7 @@ impl Run for Query {
} else { } else {
writeln!(io::stdout(), "{}", dir.display()) writeln!(io::stdout(), "{}", dir.display())
} }
.wrap_write("stdout")?; .pipe_exit("stdout")?;
} }
Ok(()) Ok(())

View File

@ -25,7 +25,7 @@ impl Run for Remove {
let mut fzf = Fzf::new(true)?; let mut fzf = Fzf::new(true)?;
for dir in db.iter_matches(&query, now, resolve_symlinks) { 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()?; selection = fzf.wait_select()?;

View File

@ -16,11 +16,11 @@ impl Display for SilentExit {
} }
pub trait WriteErrorHandler { pub trait WriteErrorHandler {
fn wrap_write(self, device: &str) -> Result<()>; fn pipe_exit(self, device: &str) -> Result<()>;
} }
impl WriteErrorHandler for io::Result<()> { impl WriteErrorHandler for io::Result<()> {
fn wrap_write(self, device: &str) -> Result<()> { fn pipe_exit(self, device: &str) -> Result<()> {
match self { match self {
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => bail!(SilentExit { code: 0 }), Err(e) if e.kind() == io::ErrorKind::BrokenPipe => bail!(SilentExit { code: 0 }),
result => result.with_context(|| format!("could not write to {}", device)), result => result.with_context(|| format!("could not write to {}", device)),

View File

@ -3,6 +3,7 @@ use crate::error::SilentExit;
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use std::io;
use std::process::{Child, ChildStdin, Command, Stdio}; use std::process::{Child, ChildStdin, Command, Stdio};
pub struct Fzf { pub struct Fzf {
@ -23,9 +24,15 @@ impl Fzf {
command.env("FZF_DEFAULT_OPTS", fzf_opts); command.env("FZF_DEFAULT_OPTS", fzf_opts);
} }
Ok(Fzf { let child = match command.spawn() {
child: command.spawn().context("could not launch fzf")?, 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 { pub fn stdin(&mut self) -> &mut ChildStdin {

View File

@ -1,5 +1,4 @@
mod app; mod app;
mod cmd;
mod config; mod config;
mod db; mod db;
mod error; mod error;
@ -7,8 +6,7 @@ mod fzf;
mod shell; mod shell;
mod util; mod util;
use crate::app::App; use crate::app::{App, Run};
use crate::cmd::Run;
use crate::error::SilentExit; use crate::error::SilentExit;
use clap::Clap; use clap::Clap;

View File

@ -60,13 +60,12 @@ mod tests {
for &resolve_symlinks in BOOLS { for &resolve_symlinks in BOOLS {
for &hook in HOOKS { for &hook in HOOKS {
for &cmd in CMDS { for &cmd in CMDS {
let opt = Opts { opts.push(Opts {
cmd, cmd,
hook, hook,
echo, echo,
resolve_symlinks, resolve_symlinks,
}; });
opts.push(opt);
} }
} }
} }
@ -84,7 +83,7 @@ mod tests {
}) })
} }
macro_rules! generate_tests { macro_rules! make_tests {
($N:literal) => { ($N:literal) => {
seq!(i in 0..$N { seq!(i in 0..$N {
#[test] #[test]
@ -92,7 +91,7 @@ mod tests {
let opts = dbg!(&opts()[i]); let opts = dbg!(&opts()[i]);
let source = Bash(opts).render().unwrap(); let source = Bash(opts).render().unwrap();
Command::new("bash") Command::new("bash")
.args(&["-c", &source, "--noediting", "--noprofile", "--norc"]) .args(&["--noprofile", "--norc", "-c", &source])
.assert() .assert()
.success() .success()
.stdout("") .stdout("")
@ -213,7 +212,7 @@ mod tests {
let opts = dbg!(&opts()[i]); let opts = dbg!(&opts()[i]);
let source = Posix(opts).render().unwrap(); let source = Posix(opts).render().unwrap();
let assert = Command::new("bash") let assert = Command::new("bash")
.args(&["--posix", "-c", &source, "--noediting", "--noprofile", "--norc"]) .args(&["--posix", "--noprofile", "--norc", "-c", &source])
.assert() .assert()
.success() .success()
.stderr(""); .stderr("");
@ -256,7 +255,6 @@ mod tests {
let opts = dbg!(&opts()[i]); let opts = dbg!(&opts()[i]);
let mut source = Posix(opts).render().unwrap(); let mut source = Posix(opts).render().unwrap();
source.push('\n'); source.push('\n');
Command::new("shfmt") Command::new("shfmt")
.args(&["-d", "-s", "-ln", "posix", "-i", "4", "-ci", "-"]) .args(&["-d", "-s", "-ln", "posix", "-i", "4", "-ci", "-"])
.write_stdin(source) .write_stdin(source)
@ -271,7 +269,7 @@ mod tests {
let opts = dbg!(&opts()[i]); let opts = dbg!(&opts()[i]);
let source = Powershell(opts).render().unwrap(); let source = Powershell(opts).render().unwrap();
Command::new("pwsh") Command::new("pwsh")
.args(&["-Command", &source, "-NoLogo", "-NonInteractive", "-NoProfile"]) .args(&["-NoLogo", "-NonInteractive", "-NoProfile", "-Command", &source])
.assert() .assert()
.success() .success()
.stdout("") .stdout("")
@ -316,14 +314,19 @@ mod tests {
} }
#[test] #[test]
// Xonsh complains about type-hinting here, although it works fine in practice.
// <https://github.com/xonsh/xonsh/issues/3959>
#[ignore]
fn xonsh_xonsh_#i() { fn xonsh_xonsh_#i() {
let opts = dbg!(&opts()[i]); let opts = dbg!(&opts()[i]);
let source = Xonsh(opts).render().unwrap(); let source = Xonsh(opts).render().unwrap();
// We can't pass the source directly to `xonsh -c` due to
// a bug: <https://github.com/xonsh/xonsh/issues/3959>
Command::new("xonsh") 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() .assert()
.success() .success()
.stdout("") .stdout("")
@ -360,5 +363,5 @@ mod tests {
} }
} }
with_opts_size!(generate_tests); with_opts_size!(make_tests);
} }

71
tests/completion.rs Normal file
View File

@ -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.
// <https://github.com/elves/elvish/issues/1299>
#[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("");
}