a73x

tests/alias_test.rs

Ref:   Size: 8.6 KiB   History

//! Verb aliases across the command surface.
//!
//! The cost of a wrong verb guess is not the guess: it is that clap writes
//! usage text to stderr, exits 2, and a script that captured the output keeps
//! going with usage text where an id should be. Every alias here exists so a
//! plausible guess *succeeds* rather than failing several steps downstream.
//!
//! These tests assert at the parser: an alias must resolve to the same
//! subcommand, with the same fields, as the canonical spelling. Comparing the
//! parsed value rather than the exit status is what makes "resolves to the
//! same subcommand" a real claim.

use clap::Parser;
use git_collab::cli::Cli;

/// Parse an argv and render the resulting command for comparison.
fn parse(args: &[&str]) -> String {
    let mut argv = vec!["git-collab"];
    argv.extend_from_slice(args);
    let cli =
        Cli::try_parse_from(&argv).unwrap_or_else(|e| panic!("failed to parse {:?}: {}", args, e));
    format!("{:?}", cli.command)
}

/// The alias and the canonical spelling must produce an identical command.
#[track_caller]
fn assert_same(alias: &[&str], canonical: &[&str]) {
    assert_eq!(
        parse(alias),
        parse(canonical),
        "{:?} should resolve to the same command as {:?}",
        alias,
        canonical
    );
}

// ---------------------------------------------------------------------------
// The three asymmetries named in the issue
// ---------------------------------------------------------------------------

#[test]
fn issue_create_is_issue_open() {
    assert_same(
        &["issue", "create", "-t", "title", "-b", "body"],
        &["issue", "open", "-t", "title", "-b", "body"],
    );
}

#[test]
fn patch_open_is_patch_create() {
    assert_same(
        &["patch", "open", "-t", "title", "-b", "body"],
        &["patch", "create", "-t", "title", "-b", "body"],
    );
}

#[test]
fn keys_is_key() {
    assert_same(&["keys", "list"], &["key", "list"]);
    assert_same(&["keys", "add", "--self"], &["key", "add", "--self"]);
    assert_same(&["keys", "remove", "abc"], &["key", "remove", "abc"]);
}

/// Key *generation* lives at the top level as `init-key` while key
/// *management* lives under `key`. Someone who found `key add` will look for
/// `key generate` next, so it has to be there.
#[test]
fn key_generate_reaches_the_same_generator_as_init_key() {
    // A distinct variant by necessity — clap aliases cannot cross enums — so
    // this asserts the routing rather than the parse.
    use git_collab::cli::{Commands, KeyCmd};
    let generated = Cli::try_parse_from(["git-collab", "key", "generate", "--force"]).unwrap();
    assert!(matches!(
        generated.command,
        Commands::Key(KeyCmd::Generate { force: true, .. })
    ));
    assert_same(&["key", "init"], &["key", "generate"]);
    assert_same(&["key", "init-key"], &["key", "generate"]);
}

// ---------------------------------------------------------------------------
// The same asymmetries elsewhere on the surface
// ---------------------------------------------------------------------------

#[test]
fn new_is_a_synonym_for_open_and_create() {
    assert_same(&["issue", "new", "-t", "t"], &["issue", "open", "-t", "t"]);
    assert_same(
        &["patch", "new", "-t", "t"],
        &["patch", "create", "-t", "t"],
    );
}

#[test]
fn ls_is_list_everywhere_there_is_a_list() {
    assert_same(&["issue", "ls"], &["issue", "list"]);
    assert_same(&["patch", "ls"], &["patch", "list"]);
    assert_same(&["key", "ls"], &["key", "list"]);
    assert_same(&["release", "ls"], &["release", "list"]);
    assert_same(&["identity", "ls"], &["identity", "list"]);
}

/// Issues and patches spell removal `delete`; trusted keys spell it `remove`.
/// Both spellings, plus `rm`, work wherever removal exists.
#[test]
fn delete_remove_and_rm_are_interchangeable() {
    assert_same(&["issue", "remove", "abc"], &["issue", "delete", "abc"]);
    assert_same(&["issue", "rm", "abc"], &["issue", "delete", "abc"]);
    assert_same(&["patch", "remove", "abc"], &["patch", "delete", "abc"]);
    assert_same(&["patch", "rm", "abc"], &["patch", "delete", "abc"]);
    assert_same(&["key", "delete", "abc"], &["key", "remove", "abc"]);
    assert_same(&["key", "rm", "abc"], &["key", "remove", "abc"]);
    assert_same(&["release", "rm", "v1"], &["release", "delete", "v1"]);
    assert_same(&["release", "remove", "v1"], &["release", "delete", "v1"]);
}

#[test]
fn view_and_info_are_show() {
    assert_same(&["issue", "view", "abc"], &["issue", "show", "abc"]);
    assert_same(&["issue", "info", "abc"], &["issue", "show", "abc"]);
    assert_same(&["patch", "view", "abc"], &["patch", "show", "abc"]);
    assert_same(&["patch", "info", "abc"], &["patch", "show", "abc"]);
}

/// `hooks` is the only plural noun at the top level; `hook` must work too,
/// for the same reason `keys` must.
#[test]
fn hook_is_hooks() {
    assert_same(&["hook", "status"], &["hooks", "status"]);
    assert_same(&["hook", "install"], &["hooks", "install"]);
}

#[test]
fn init_key_answers_to_the_obvious_guesses() {
    assert_same(&["keygen"], &["init-key"]);
    assert_same(&["generate-key"], &["init-key"]);
}

#[test]
fn patch_update_is_patch_revise() {
    assert_same(&["patch", "update", "abc"], &["patch", "revise", "abc"]);
}

#[test]
fn patch_co_is_patch_checkout() {
    assert_same(&["patch", "co", "abc"], &["patch", "checkout", "abc"]);
}

#[test]
fn release_upload_and_create_are_release_publish() {
    assert_same(
        &["release", "upload", "v1", "f"],
        &["release", "publish", "v1", "f"],
    );
    assert_same(
        &["release", "create", "v1", "f"],
        &["release", "publish", "v1", "f"],
    );
}

/// `identity alias`/`unalias` are the odd verbs out: every other collection on
/// the surface is managed with add/remove.
#[test]
fn identity_add_and_remove_are_alias_and_unalias() {
    assert_same(&["identity", "add", "a@b"], &["identity", "alias", "a@b"]);
    assert_same(
        &["identity", "remove", "a@b"],
        &["identity", "unalias", "a@b"],
    );
    assert_same(&["identity", "rm", "a@b"], &["identity", "unalias", "a@b"]);
}

#[test]
fn find_and_grep_are_search() {
    assert_same(&["find", "needle"], &["search", "needle"]);
    assert_same(&["grep", "needle"], &["search", "needle"]);
}

// ---------------------------------------------------------------------------
// Aliases must not shadow anything, and must stay out of the advertised help.
// ---------------------------------------------------------------------------

/// An alias that collided with a real subcommand name would silently reroute a
/// correct invocation, which is worse than the problem being fixed. clap
/// panics on a duplicate, so building the command at all proves the point —
/// but assert it explicitly so the reason is on the record.
#[test]
fn the_command_tree_builds_without_alias_collisions() {
    use clap::CommandFactory;
    let cmd = Cli::command();
    check_no_duplicate_names(&cmd);
}

fn check_no_duplicate_names(cmd: &clap::Command) {
    let mut seen: Vec<String> = Vec::new();
    for sub in cmd.get_subcommands() {
        for name in std::iter::once(sub.get_name()).chain(sub.get_all_aliases()) {
            assert!(
                !seen.contains(&name.to_string()),
                "'{}' appears twice under '{}'",
                name,
                cmd.get_name()
            );
            seen.push(name.to_string());
        }
        check_no_duplicate_names(sub);
    }
}

/// Aliases are a safety net for wrong guesses, not a second vocabulary to
/// learn: `--help` must keep advertising exactly one spelling per command.
#[test]
fn every_alias_is_hidden_from_help() {
    use clap::CommandFactory;
    check_aliases_hidden(&Cli::command());
}

fn check_aliases_hidden(cmd: &clap::Command) {
    for sub in cmd.get_subcommands() {
        let visible: Vec<&str> = sub.get_visible_aliases().collect();
        assert!(
            visible.is_empty(),
            "'{} {}' advertises aliases {:?}; aliases should be hidden",
            cmd.get_name(),
            sub.get_name(),
            visible
        );
        check_aliases_hidden(sub);
    }
}

/// …and the canonical spellings must still be advertised, so hiding aliases
/// has not accidentally hidden a command.
#[test]
fn canonical_commands_are_still_listed_in_help() {
    let mut cmd = <Cli as clap::CommandFactory>::command();
    let help = cmd.render_long_help().to_string();
    for advertised in ["issue", "patch", "release", "key", "status", "search"] {
        assert!(
            help.lines()
                .any(|l| l.trim_start().starts_with(&format!("{} ", advertised))),
            "help should still list '{}':\n{}",
            advertised,
            help
        );
    }
}