From cac16996ffc4ef15e3eaf2943a7ac42c921f55ac Mon Sep 17 00:00:00 2001 From: Daniel Mueller Date: Sun, 5 Apr 2020 10:47:40 -0700 Subject: Add test for bash completion functionality This change adds a test for the previously introduced bash completion functionality. To test the generated completion script, we spin up a bash instance, source the script, and then perform a completion as the shell would do it. It seems impossible to convince compgen to do the heavy lifting for us and so we invoke the completion function with the expected environment variables present. --- var/shell-complete.rs | 109 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) (limited to 'var') diff --git a/var/shell-complete.rs b/var/shell-complete.rs index 0a525d8..c69a426 100644 --- a/var/shell-complete.rs +++ b/var/shell-complete.rs @@ -52,8 +52,113 @@ pub struct Args { pub command: String, } +fn generate_bash(command: &str, output: &mut W) +where + W: io::Write, +{ + let mut app = nitrocli::Args::clap(); + app.gen_completions_to(command, clap::Shell::Bash, output); +} + fn main() { let args = Args::from_args(); - let mut app = nitrocli::Args::clap(); - app.gen_completions_to(&args.command, clap::Shell::Bash, &mut io::stdout()); + generate_bash(&args.command, &mut io::stdout()) +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::io; + use std::ops::Add as _; + use std::process; + + /// Separate the given words by newlines. + fn lines<'w, W>(mut words: W) -> String + where + W: Iterator, + { + let first = words.next().unwrap_or(""); + words + .fold(first.to_string(), |words, word| { + format!("{}\n{}", words, word) + }) + .add("\n") + } + + /// Check if `bash` is present on the system. + fn has_bash() -> bool { + match process::Command::new("bash").arg("-c").arg("exit").spawn() { + // We deliberately only indicate that bash does not exist if we + // get a file-not-found error. We don't expect any other error but + // should there be one things will blow up later. + Err(ref err) if err.kind() == io::ErrorKind::NotFound => false, + _ => true, + } + } + + /// Perform a bash completion of the given arguments to nitrocli. + fn complete_bash<'w, W>(words: W) -> Vec + where + W: ExactSizeIterator, + { + let mut buffer = Vec::new(); + generate_bash("nitrocli", &mut buffer); + + let script = String::from_utf8(buffer).unwrap(); + let command = format!( + " +set -e; +eval '{script}'; +export COMP_WORDS=({words}); +export COMP_CWORD={index}; +_nitrocli; +echo -n ${{COMPREPLY}} + ", + index = words.len(), + words = lines(Some("nitrocli").into_iter().chain(words)), + script = script + ); + + let output = process::Command::new("bash") + .arg("-c") + .arg(command) + .output() + .unwrap(); + + output.stdout + } + + #[test] + fn array_lines() { + assert_eq!(&lines(vec![].into_iter()), "\n"); + assert_eq!(&lines(vec!["first"].into_iter()), "first\n"); + assert_eq!( + &lines(vec!["first", "second"].into_iter()), + "first\nsecond\n" + ); + assert_eq!( + &lines(vec!["first", "second", "third"].into_iter()), + "first\nsecond\nthird\n" + ); + } + + #[test] + fn complete_all_the_things() { + if !has_bash() { + return; + } + + assert_eq!(complete_bash(vec!["stat"].into_iter()), b"status"); + assert_eq!( + complete_bash(vec!["status", "--ver"].into_iter()), + b"--version" + ); + assert_eq!(complete_bash(vec!["--version"].into_iter()), b"--version"); + assert_eq!(complete_bash(vec!["--model", "s"].into_iter()), b"storage"); + assert_eq!( + complete_bash(vec!["otp", "get", "--model", "p"].into_iter()), + b"pro" + ); + } } -- cgit v1.2.3