2143 lines · 65.9 KB · Rust
RawDownload
// Copyright 2024 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. use std::ffi::OsStr; use clap_complete::Shell;use indoc::indoc;use itertools::Itertools as _;use test_case::test_case; use crate::common::CommandOutput;use crate::common::TestEnvironment;use crate::common::TestWorkDir; impl TestEnvironment {    /// Runs `jj` as if shell completion had been triggered for the argument of    /// the given index.    #[must_use = "either snapshot the output or assert the exit status with .success()"]    fn complete_at<I>(&self, shell: Shell, index: usize, args: I) -> CommandOutput    where        I: IntoIterator,        I::IntoIter: ExactSizeIterator,        I::Item: AsRef<OsStr>,    {        self.work_dir("").complete_at(shell, index, args)    }     /// Run `jj` as if fish shell completion had been triggered at the last item    /// in `args`.    #[must_use = "either snapshot the output or assert the exit status with .success()"]    fn complete_fish<I>(&self, args: I) -> CommandOutput    where        I: IntoIterator,        I::IntoIter: ExactSizeIterator,        I::Item: AsRef<OsStr>,    {        self.work_dir("").complete_fish(args)    }} impl TestWorkDir<'_> {    /// Runs `jj` as if shell completion had been triggered for the argument of    /// the given index.    #[must_use = "either snapshot the output or assert the exit status with .success()"]    fn complete_at<I>(&self, shell: Shell, index: usize, args: I) -> CommandOutput    where        I: IntoIterator,        I::IntoIter: ExactSizeIterator,        I::Item: AsRef<OsStr>,    {        let args = args.into_iter();        assert!(            index <= args.len(),            "index out of bounds: append empty string to complete after the last argument"        );        self.run_jj_with(|cmd| {            let cmd = cmd.env("COMPLETE", shell.to_string()).args(["--", "jj"]);             match shell {                // Bash passes the whole command line except for intermediate empty tokens but                // preserves trailing empty tokens:                // `jj log <CURSOR>`            => ["--", "jj", "log", ""]                //                                 with _CLAP_COMPLETE_INDEX=2                // `jj log <CURSOR> --no-graph` => ["--", "jj", "log", "--no-graph"]                //                                 with _CLAP_COMPLETE_INDEX=2                // `jj log --no-graph<CURSOR>`  => ["--", "jj", "log", "--no-graph"]                //                                 with _CLAP_COMPLETE_INDEX=2                //                                 (indistinguishable from the above)                // `jj log --no-graph <CURSOR>` => ["--", "jj", "log", "--no-graph", ""]                //                                 with _CLAP_COMPLETE_INDEX=3                Shell::Bash => {                    cmd.env("_CLAP_COMPLETE_INDEX", index.to_string())                        .args(args.coalesce(|a, b| {                            if a.as_ref().is_empty() {                                Ok(b)                            } else {                                Err((a, b))                            }                        }))                }                 // Zsh passes the whole command line except for empty tokens, including trailing                // empty tokens:                // `jj log <CURSOR>`            => ["--", "jj", "log"]                //                                 with _CLAP_COMPLETE_INDEX=2                // `jj log <CURSOR> --no-graph` => ["--", "jj", "log", "--no-graph"]                //                                 with _CLAP_COMPLETE_INDEX=2                // `jj log --no-graph<CURSOR>`  => ["--", "jj", "log", "--no-graph"]                //                                 with _CLAP_COMPLETE_INDEX=2                //                                 (indistinguishable from the above)                // `jj log --no-graph <CURSOR>` => ["--", "jj", "log", "--no-graph"]                //                                 with _CLAP_COMPLETE_INDEX=3                Shell::Zsh => cmd                    .env("_CLAP_COMPLETE_INDEX", index.to_string())                    .args(args.filter(|a| !a.as_ref().is_empty())),                 // Fish truncates the command line at the cursor; empty tokens are preserved:                // `jj log <CURSOR>`            => ["--", "jj", "log", ""]                // `jj log <CURSOR> --no-graph` => ["--", "jj", "log", ""]                // `jj log --no-graph<CURSOR>`  => ["--", "jj", "log", "--no-graph"]                // `jj log --no-graph <CURSOR>` => ["--", "jj", "log", "--no-graph", ""]                Shell::Fish => cmd.args(args.take(index)),                 _ => todo!("{shell} completion behavior not implemented yet"),            }        })    }     /// Run `jj` as if fish shell completion had been triggered at the last item    /// in `args`.    #[must_use = "either snapshot the output or assert the exit status with .success()"]    fn complete_fish<I>(&self, args: I) -> CommandOutput    where        I: IntoIterator,        I::IntoIter: ExactSizeIterator,        I::Item: AsRef<OsStr>,    {        let args = args.into_iter();        self.complete_at(Shell::Fish, args.len(), args)    }} #[test]fn test_bookmark_names() {    let test_env = TestEnvironment::default();    test_env.run_jj_in(".", ["git", "init", "repo"]).success();    let work_dir = test_env.work_dir("repo");     test_env        .run_jj_in(".", ["git", "init", "--colocate", "origin"])        .success();    let origin_dir = test_env.work_dir("origin");    test_env        .run_jj_in(".", ["git", "init", "--colocate", "upstream"])        .success();    let _upstream_dir = test_env.work_dir("upstream");     work_dir        .run_jj(["bookmark", "create", "-r@", "aaa-local"])        .success();    work_dir        .run_jj(["bookmark", "create", "-r@", "bbb-local"])        .success();     // add various remote branches    work_dir        .run_jj(["git", "remote", "add", "origin", "../origin"])        .success();    work_dir        .run_jj(["git", "remote", "add", "upstream", "../upstream"])        .success();    work_dir        .run_jj(["bookmark", "create", "-r@", "aaa-tracked"])        .success();    work_dir        .run_jj(["desc", "-r", "aaa-tracked", "-m", "x"])        .success();    work_dir        .run_jj(["bookmark", "create", "-r@", "bbb-tracked"])        .success();    work_dir        .run_jj(["desc", "-r", "bbb-tracked", "-m", "x"])        .success();     work_dir        .run_jj(["bookmark", "track", "--remote=origin", "*-tracked"])        .success();    work_dir        .run_jj(["bookmark", "track", "--remote=upstream", "aaa-tracked"])        .success();    work_dir        .run_jj(["git", "push", "--remote=origin", "--tracked"])        .success();    work_dir        .run_jj(["git", "push", "--remote=upstream", "--tracked"])        .success();     origin_dir        .run_jj(["bookmark", "create", "-r@", "aaa-untracked"])        .success();    origin_dir        .run_jj(["desc", "-r", "aaa-untracked", "-m", "x"])        .success();    origin_dir        .run_jj(["bookmark", "create", "-r@", "bbb-untracked"])        .success();    origin_dir        .run_jj(["desc", "-r", "bbb-untracked", "-m", "x"])        .success();    work_dir.run_jj(["git", "fetch", "--all-remotes"]).success();     insta::assert_snapshot!(work_dir.run_jj(["bookmark", "list", "--all"]), @"    aaa-local: qpvuntsm fe38a82d (empty) x    aaa-tracked: qpvuntsm fe38a82d (empty) x      @origin: qpvuntsm fe38a82d (empty) x      @upstream: qpvuntsm fe38a82d (empty) x    aaa-untracked@origin: rlvkpnrz 434ae005 (empty) x    bbb-local: qpvuntsm fe38a82d (empty) x    bbb-tracked: qpvuntsm fe38a82d (empty) x      @origin: qpvuntsm fe38a82d (empty) x    bbb-untracked@origin: rlvkpnrz 434ae005 (empty) x    [EOF]    ");     // Every shell hook is a little different, e.g. the zsh hooks add some    // additional environment variables. But this is irrelevant for the purpose    // of testing our own logic, so it's fine to test a single shell only.    let work_dir = test_env.work_dir("repo");     let output = work_dir.complete_fish(["bookmark", "rename", ""]);    insta::assert_snapshot!(output, @"    aaa-local	x    aaa-tracked	x    bbb-local	x    bbb-tracked	x    --overwrite-existing	Allow renaming even if the new bookmark name already exists    --help	Print help (see more with '--help')    --repository	Path to repository to operate on    --ignore-working-copy	Don't snapshot the working copy, and don't update it    --no-integrate-operation	Run the command as usual but don't integrate any operations    --ignore-immutable	Allow rewriting immutable commits    --at-operation	Operation to load the repo at    --debug	Enable debug logging    --color	When to colorize output    --quiet	Silence non-primary command output    --no-pager	Disable the pager    --config	Additional configuration options (can be repeated)    --config-file	Additional configuration files (can be repeated)    [EOF]    ");     let output = work_dir.complete_fish(["bookmark", "rename", "a"]);    insta::assert_snapshot!(output, @"    aaa-local	x    aaa-tracked	x    [EOF]    ");     let output = work_dir.complete_fish(["bookmark", "delete", "a"]);    insta::assert_snapshot!(output, @"    aaa-local	x    aaa-tracked	x    [EOF]    ");     let output = work_dir.complete_fish(["bookmark", "forget", "a"]);    insta::assert_snapshot!(output, @"    aaa-local	x    aaa-tracked	x    aaa-untracked    [EOF]    ");     let output = work_dir.complete_fish(["bookmark", "list", "--bookmark", "a"]);    insta::assert_snapshot!(output, @"    aaa-local	x    aaa-tracked	x    aaa-untracked    [EOF]    ");     let output = work_dir.complete_fish(["bookmark", "move", "a"]);    insta::assert_snapshot!(output, @"    aaa-local	x    aaa-tracked	x    [EOF]    ");     let output = work_dir.complete_fish(["bookmark", "set", "a"]);    insta::assert_snapshot!(output, @"    aaa-local	x    aaa-tracked	x    [EOF]    ");     let output = work_dir.complete_fish(["bookmark", "track", "a"]);    insta::assert_snapshot!(output, @"    aaa-local	 x    aaa-untracked	 x    [EOF]    ");     let output = work_dir.complete_fish(["bookmark", "untrack", "a"]);    insta::assert_snapshot!(output, @"    aaa-tracked	x    [EOF]    ");     // TODO: Make it so this only lists untracked remotes    let output = work_dir.complete_fish(["bookmark", "track", "a", "--remote", ""]);    insta::assert_snapshot!(output, @"    origin    upstream    [EOF]    ");     // TODO: Make it so this only lists tracked remotes    let output = work_dir.complete_fish(["bookmark", "untrack", "a", "--remote", ""]);    insta::assert_snapshot!(output, @"    origin    upstream    [EOF]    ");     let output = work_dir.complete_fish(["git", "push", "-b", "a"]);    insta::assert_snapshot!(output, @"    aaa-local	x    aaa-tracked	x    [EOF]    ");     let output = work_dir.complete_fish(["git", "fetch", "-b", "a"]);    insta::assert_snapshot!(output, @"    aaa-local	x    aaa-tracked	x    aaa-untracked    [EOF]    ");} #[test]fn test_tag_names() {    let test_env = TestEnvironment::default();    test_env.run_jj_in(".", ["git", "init", "repo"]).success();    let work_dir = test_env.work_dir("repo");     work_dir.run_jj(["commit", "-mcommit1"]).success();    work_dir        .run_jj(["tag", "set", "-r@-", "aaa-local"])        .success();    work_dir        .run_jj(["tag", "set", "-r@-", "bbb-local"])        .success();     let output = work_dir.complete_fish(["tag", "set", "a"]);    insta::assert_snapshot!(output, @"    aaa-local	commit1    [EOF]    ");     let output = work_dir.complete_fish(["tag", "delete", "b"]);    insta::assert_snapshot!(output, @"    bbb-local	commit1    [EOF]    ");} #[test]fn test_global_arg_repository_is_respected() {    let test_env = TestEnvironment::default();    test_env.run_jj_in(".", ["git", "init", "repo"]).success();    let work_dir = test_env.work_dir("repo");     work_dir        .run_jj(["bookmark", "create", "-r@", "aaa"])        .success();     let output = test_env.complete_fish(["--repository", "repo", "bookmark", "rename", "a"]);    insta::assert_snapshot!(output, @"    aaa	(no description set)    [EOF]    ");} #[test_case(Shell::Bash; "bash")]#[test_case(Shell::Zsh; "zsh")]#[test_case(Shell::Fish; "fish")]fn test_aliases_are_resolved(shell: Shell) {    let test_env = TestEnvironment::default();    test_env.run_jj_in(".", ["git", "init", "repo"]).success();    let work_dir = test_env.work_dir("repo");     work_dir        .run_jj(["bookmark", "create", "-r@", "aaa"])        .success();     // user config alias    test_env.add_config(r#"aliases.b = ["bookmark"]"#);    test_env.add_config(r#"aliases.rlog = ["log", "--reversed"]"#);    // repo config alias    work_dir        .run_jj(["config", "set", "--repo", "aliases.b2", "['bookmark']"])        .success();     let output = work_dir.complete_at(shell, 3, ["b", "rename", "a"]);    match shell {        Shell::Bash => {            insta::assert_snapshot!(output, @"aaa[EOF]");        }        Shell::Zsh => {            insta::assert_snapshot!(output, @"aaa:(no description set)[EOF]");        }        Shell::Fish => {            insta::assert_snapshot!(output, @"            aaa	(no description set)            [EOF]            ");        }        _ => unimplemented!("unexpected shell '{shell}'"),    }     let output = work_dir.complete_at(shell, 3, ["b2", "rename", "a"]);    match shell {        Shell::Bash => {            insta::assert_snapshot!(output, @"aaa[EOF]");        }        Shell::Zsh => {            insta::assert_snapshot!(output, @"aaa:(no description set)[EOF]");        }        Shell::Fish => {            insta::assert_snapshot!(output, @"            aaa	(no description set)            [EOF]            ");        }        _ => unimplemented!("unexpected shell '{shell}'"),    }     let output = work_dir.complete_at(shell, 2, ["rlog", "--rev"]);    match shell {        Shell::Bash => {            insta::assert_snapshot!(output, @"            --revision            --reversed[EOF]            ");        }        Shell::Zsh => {            insta::assert_snapshot!(output, @"            --revision:Which revisions to show            --reversed:Show revisions in the opposite order (older revisions first)[EOF]            ");        }        Shell::Fish => {            insta::assert_snapshot!(output, @"            --revision	Which revisions to show            --reversed	Show revisions in the opposite order (older revisions first)            [EOF]            ");        }        _ => unimplemented!("unexpected shell '{shell}'"),    }} #[test]fn test_completions_are_generated() {    let mut test_env = TestEnvironment::default();    test_env.add_env_var("COMPLETE", "fish");    let mut insta_settings = insta::Settings::clone_current();    insta_settings.add_filter(r"(--arguments) .*", "$1 .."); // omit path to jj binary    let _guard = insta_settings.bind_to_scope();     let output = test_env.run_jj_in(".", [""; 0]);    insta::assert_snapshot!(output, @"    complete --keep-order --exclusive --command jj --arguments ..    [EOF]    ");    let output = test_env.run_jj_in(".", ["--"]);    insta::assert_snapshot!(output, @"    complete --keep-order --exclusive --command jj --arguments ..    [EOF]    ");} #[test]fn test_bad_complete_env() {    let mut test_env = TestEnvironment::default();     test_env.add_env_var("COMPLETE", "badshell");    let output = test_env.run_jj_in(".", [""; 0]);    insta::assert_snapshot!(output, @"    ------- stderr -------    error: unknown shell `badshell`, expected one of `bash`, `elvish`, `fish`, `powershell`, `zsh`[EOF]    [exit status: 2]    ");     // Empty value of COMPLETE is ignored as is the value of "0".  This could    // change if `clap` changes the way it interprets an empty COMPLETE env var.    //    // In other words, jj runs normally instead of returning completions. We get    // an error because the default jj command needs to be run in a jj repo.    test_env.add_env_var("COMPLETE", "");    let output = test_env.run_jj_in(".", [""; 0]);    insta::assert_snapshot!(output, @r#"    ------- stderr -------    Hint: Use `jj -h` for a list of available commands.    Run `jj config set --user ui.default-command log` to disable this message.    Error: There is no jj repo in "."    [EOF]    [exit status: 1]

Lines 1–500 of 2143.