diff --git a/justfile b/justfile index 087ed1a8..15d0d709 100644 --- a/justfile +++ b/justfile @@ -86,3 +86,6 @@ pr-review id="": just generate-files (git add man/ shell/ && git commit -m 'chore: generate-files' && git push) || echo "Nothing to do" + +coverage args="": + cargo llvm-cov nextest --lib --bins --examples --tests {{ args }} diff --git a/src/bin/main.rs b/src/bin/main.rs index 9a7ea1f0..ff4385c4 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -80,9 +80,9 @@ fn main() -> Result<()> { // Shell completion scripts if let Some(shell) = opts.shell { // Generate completion script directly to stdout - skim::shell::generate_completions(&shell); + skim::shell::generate_completions(&shell, &mut std::io::stdout()); if opts.shell_bindings { - skim::shell::generate_key_bindings(&shell); + skim::shell::generate_key_bindings(&shell, &mut std::io::stdout())?; } return Ok(()); } diff --git a/src/manpage.rs b/src/manpage.rs index d4d1c44e..b612a13d 100644 --- a/src/manpage.rs +++ b/src/manpage.rs @@ -365,3 +365,40 @@ Example: base.render_version_section(w)?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + fn manpage_str() -> String { + let mut buf = Vec::new(); + generate(&mut buf).expect("manpage generation should not fail"); + String::from_utf8(buf).expect("manpage output is valid UTF-8") + } + + #[test] + fn manpage_contains_version() { + let out = manpage_str(); + let version = env!("CARGO_PKG_VERSION"); + assert!( + out.contains(version), + "manpage should contain package version {version}" + ); + } + + #[test] + fn manpage_contains_key_options() { + let out = manpage_str(); + for flag in ["query", "multi", "preview", "bind", "color"] { + assert!(out.contains(flag), "manpage should mention '--{flag}'"); + } + } + + #[test] + fn manpage_contains_sections() { + let out = manpage_str(); + for section in ["MODES", "SEARCH", "KEYBINDS", "EXIT CODES"] { + assert!(out.contains(section), "manpage should contain section '{section}'"); + } + } +} diff --git a/src/shell.rs b/src/shell.rs index 28bf3f9f..7d0c5c40 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -1,4 +1,6 @@ //! Provides helpers to easily generate shell completions +use std::io::Write; + use clap::CommandFactory; use crate::SkimOptions; @@ -21,9 +23,8 @@ pub enum Shell { } /// Generate the completion and write it to stdout -pub fn generate_completions(sh: &Shell) { +pub fn generate_completions(sh: &Shell, output: &mut impl Write) { use Shell::{Bash, Elvish, Fish, Nushell, PowerShell, Zsh}; - let output = &mut std::io::stdout(); let cmd = &mut SkimOptions::command(); let bin_name = "sk"; @@ -42,8 +43,10 @@ pub fn generate_completions(sh: &Shell) { } } -/// Generate the key-bindings script and write it to stdout -pub fn generate_key_bindings(sh: &Shell) { +/// Generate the key-bindings script and write it to the given writer +/// # Errors +/// This errors if it fails to write the bytes to the output +pub fn generate_key_bindings(sh: &Shell, output: &mut impl Write) -> std::io::Result<()> { use Shell::{Bash, Fish, Zsh}; let binds_script = match sh { Bash => include_str!("../shell/key-bindings.bash"), @@ -52,6 +55,120 @@ pub fn generate_key_bindings(sh: &Shell) { _ => "", }; if !binds_script.is_empty() { - println!("{binds_script}"); + output.write_all(binds_script.as_bytes())?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn completions_for(sh: &Shell) -> String { + let mut buf = Vec::new(); + generate_completions(sh, &mut buf); + String::from_utf8(buf).expect("completion output is valid UTF-8") + } + + #[test] + fn completions_bash_contains_sk() { + let out = completions_for(&Shell::Bash); + assert!(out.contains("sk"), "bash completion should reference 'sk'"); + assert!( + out.contains("--query") || out.contains("query"), + "bash completion should include --query" + ); + } + + #[test] + fn completions_zsh_contains_sk() { + let out = completions_for(&Shell::Zsh); + assert!(out.contains("sk"), "zsh completion should reference 'sk'"); + assert!( + out.contains("--multi") || out.contains("multi"), + "zsh completion should include --multi" + ); + } + + #[test] + fn completions_fish_contains_sk() { + let out = completions_for(&Shell::Fish); + assert!(out.contains("sk"), "fish completion should reference 'sk'"); + } + + #[test] + fn completions_nushell_is_non_empty() { + let out = completions_for(&Shell::Nushell); + assert!(!out.is_empty(), "nushell completion should not be empty"); + } + + #[test] + fn completions_elvish_is_non_empty() { + let out = completions_for(&Shell::Elvish); + assert!(!out.is_empty(), "elvish completion should not be empty"); + } + + #[test] + fn completions_powershell_is_non_empty() { + let out = completions_for(&Shell::PowerShell); + assert!(!out.is_empty(), "powershell completion should not be empty"); + } + + fn key_bindings_for(sh: &Shell) -> String { + let mut buf = Vec::new(); + generate_key_bindings(sh, &mut buf).expect("key-bindings generation failed"); + String::from_utf8(buf).expect("key-bindings output is valid UTF-8") + } + + #[test] + fn key_bindings_bash() { + let out = key_bindings_for(&Shell::Bash); + assert!( + out.starts_with("# skim key bindings for bash"), + "unexpected bash header" + ); + assert!(out.contains("__skim_select__()"), "missing __skim_select__ function"); + } + + #[test] + fn key_bindings_zsh() { + let out = key_bindings_for(&Shell::Zsh); + assert!(out.starts_with("# skim key bindings for zsh"), "unexpected zsh header"); + for func in [ + "__skimcmd()", + "__skim_comprun()", + "__skim_extract_command()", + "__skim_generic_path_completion()", + "_skim_complete()", + "_skim_complete_kill()", + ] { + assert!(out.contains(func), "missing zsh function {func}"); + } + } + + #[test] + fn key_bindings_fish() { + let out = key_bindings_for(&Shell::Fish); + assert!( + out.starts_with("#!/bin/fish"), + "fish key-bindings should start with shebang" + ); + for func in [ + "function __skimcmd", + "function __skim_parse_commandline", + "function __skim_get_dir", + ] { + assert!(out.contains(func), "missing fish function '{func}'"); + } + } + + #[test] + fn key_bindings_unsupported_shells_are_empty() { + for sh in [Shell::Elvish, Shell::Nushell, Shell::PowerShell] { + assert!( + key_bindings_for(&sh).is_empty(), + "{sh:?} should produce no key-bindings output" + ); + } } }