test: add unit tests for the shell & manpage generators (#1098)

* test: add unit tests for the shell & manpage generators

* chore: propagate key bindings generation errors
This commit is contained in:
LoricAndre 2026-06-24 20:18:15 +02:00 committed by GitHub
parent b56eed4125
commit 8187a12196
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 164 additions and 7 deletions

View file

@ -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 }}

View file

@ -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(());
}

View file

@ -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}'");
}
}
}

View file

@ -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"
);
}
}
}