diff --git a/.lychee.yaml b/.lychee.yaml new file mode 100644 index 0000000000..c5c01d2b07 --- /dev/null +++ b/.lychee.yaml @@ -0,0 +1,3 @@ +# .lychee.yaml +exclude: + - "https://www.gnu.org/software/libc/" \ No newline at end of file diff --git a/.mise/tasks/check-links.sh b/.mise/tasks/check-links.sh index dbc44830b7..82244f1c41 100755 --- a/.mise/tasks/check-links.sh +++ b/.mise/tasks/check-links.sh @@ -8,6 +8,7 @@ find . -type f -name "*.md" \ -not -path "*/node_modules/*" \ + -not -path "*/CHANGELOG.md" \ -not -path "*/target/*" \ -not -path "*/docs/*" \ -not -path "*/.*/*" \ diff --git a/Cargo.lock b/Cargo.lock index c0710acce4..5bbe89460a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -970,6 +970,15 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_complete" +version = "4.5.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39615915e2ece2550c0149addac32fb5bd312c657f43845bb9088cb9c8a7c992" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.5.49" @@ -4254,6 +4263,7 @@ dependencies = [ "camino", "chrono", "clap", + "clap_complete", "comfy-table", "console 0.16.1", "derive-getters", @@ -4324,6 +4334,7 @@ dependencies = [ "tracing-test", "url", "uuid", + "which 8.0.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f338269d80..622b7e0f1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,6 +101,7 @@ cargo_metadata = "0.23" calm_io = "0.1" camino = { version = "1", features = ["serde1"] } clap = "4" +clap_complete = "4" chrono = "0.4" ci_info = "0.14" comfy-table = { version = "7", features = ["custom_styling"] } @@ -187,6 +188,7 @@ buildstructor = { workspace = true } calm_io = { workspace = true } camino = { workspace = true } clap = { workspace = true, features = ["color", "derive", "env"] } +clap_complete = { workspace = true } chrono = { workspace = true } console = { workspace = true } derive-getters = { workspace = true } @@ -263,6 +265,7 @@ speculoos = { workspace = true } tower-test = { workspace = true } tracing-test = { workspace = true } temp-env = { version = "0.3", features = ["async_closure"] } +which = { workspace = true } # For sputnik, run tests with debug_assertions disabled. This is necessary because telemetry is not sent if # debug_assertions is enabled, and the tests rely on telemetry being sent to mock APIs. diff --git a/docs/source/_sidebar.yaml b/docs/source/_sidebar.yaml index efa5786e28..333304cddf 100644 --- a/docs/source/_sidebar.yaml +++ b/docs/source/_sidebar.yaml @@ -23,6 +23,8 @@ items: href: ./commands/api-key - label: cloud href: ./commands/cloud + - label: completion + href: ./commands/completion - label: config href: ./commands/config - label: connector diff --git a/docs/source/commands/completion.mdx b/docs/source/commands/completion.mdx new file mode 100644 index 0000000000..d58bc586fd --- /dev/null +++ b/docs/source/commands/completion.mdx @@ -0,0 +1,91 @@ +--- +title: Rover completion Commands +subtitle: Generate shell completion scripts for bash and zsh +description: Generate shell completion scripts to enable tab completion for Rover commands in bash and zsh shells. +--- + +Rover provides shell completion support for bash and zsh, enabling tab completion for Rover commands and their options. This makes it easier to discover available commands and options while working in the terminal. + +## Generating completion scripts + +### `completion bash` + +The `completion bash` command generates a bash completion script: + +``` +rover completion bash +``` + +This outputs a bash completion script to `stdout`. To enable bash completion, save the output to a file and source it in your shell configuration: + +```bash +# Save the completion script +rover completion bash > ~/.rover-completion.bash + +# Add to your ~/.bashrc or ~/.bash_profile +echo "source ~/.rover-completion.bash" >> ~/.bashrc + +# Reload your shell configuration +source ~/.bashrc # or restart your terminal +``` + +You'll now have tab completion for Rover commands. + +### `completion zsh` + +The `completion zsh` command generates a zsh completion script: + +``` +rover completion zsh +``` + +This outputs a zsh completion script to `stdout`. To enable zsh completion, save the output to a file and source it in your shell configuration: + +```zsh +# Save the completion script +rover completion zsh > ~/.rover-completion.zsh + +# Add to your ~/.zshrc +echo "source ~/.rover-completion.zsh" >> ~/.zshrc + +# Reload your shell configuration +source ~/.zshrc # or restart your terminal +``` + +You'll now have tab completion for Rover commands. + +Alternatively, you can use zsh's completion system by placing the script in your `fpath`: + +```zsh +# Create completion directory if it doesn't exist +mkdir -p /usr/local/share/zsh/site-functions + +# Save the completion script to fpath +rover completion zsh > /usr/local/share/zsh/site-functions/_rover + +# Reload completions (or restart your terminal) +compinit +``` + +## Using completion + +Once enabled, you can use tab completion to: + +- Complete command names: Type `rover ` and press `Tab` to see available commands +- Complete subcommands: Type `rover graph ` and press `Tab` to see available graph subcommands +- Complete options: Type `rover graph publish ` and press `Tab` to see available options and flags + +## Testing completion + +To verify that completion is working correctly: + +1. Open a new terminal or reload your shell configuration +2. Type `rover ` (with a space) and press `Tab` - you should see a list of available commands +3. Type `rover gra` and press `Tab` - it should autocomplete to `rover graph` +4. Continue typing `rover graph ` and press `Tab` - you should see available subcommands like `check`, `publish`, `fetch`, etc. + +If completion isn't working, ensure that: +- You've reloaded your shell configuration or opened a new terminal +- The completion script file exists and is readable +- For bash: The script is being sourced in your `~/.bashrc` or `~/.bash_profile` +- For zsh: The script is being sourced in your `~/.zshrc` or is in your `fpath` \ No newline at end of file diff --git a/docs/source/getting-started.mdx b/docs/source/getting-started.mdx index 5cee4b7e10..5e3d50af4c 100644 --- a/docs/source/getting-started.mdx +++ b/docs/source/getting-started.mdx @@ -135,6 +135,10 @@ There are a few additional installation methods maintained by the community: 1. [Homebrew](https://formulae.brew.sh/formula/rover#default) 2. [Nix](https://search.nixos.org/packages?channel=unstable&show=rover&from=0&size=50&sort=relevance&type=packages&query=rover) +## Next steps + +After installing Rover, you can set up shell completion for bash or zsh to enable tab completion for Rover commands. See the [completion command documentation](./commands/completion) for instructions. + ## Connecting to GraphOS After you install Rover, you should authenticate it with [GraphOS](/graphos/), because many of its commands communicate with GraphOS. diff --git a/src/cli.rs b/src/cli.rs index 233ac4c247..94ff11970b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -194,6 +194,7 @@ impl Rover { match &self.command { Command::Init(command) => command.run(self.get_client_config()?).await, Command::Cloud(command) => command.run(self.get_client_config()?).await, + Command::Completion(command) => command.run(), Command::Config(command) => command.run(self.get_client_config()?).await, #[cfg(feature = "composition-js")] Command::Connector(command) => { @@ -392,6 +393,9 @@ pub enum Command { #[cfg(feature = "composition-js")] Connector(command::Connector), + /// Generate shell completion scripts + Completion(command::Completion), + /// Configuration profile commands Config(command::Config), diff --git a/src/command/completion/bash.rs b/src/command/completion/bash.rs new file mode 100644 index 0000000000..aa7c24f46b --- /dev/null +++ b/src/command/completion/bash.rs @@ -0,0 +1,17 @@ +use clap::{CommandFactory, Parser}; +use clap_complete::{generate, shells::Bash as BashShell}; +use serde::Serialize; + +use crate::{RoverOutput, RoverResult, cli::Rover}; + +#[derive(Debug, Serialize, Parser)] +pub struct Bash {} + +impl Bash { + pub fn run(&self) -> RoverResult { + let mut cmd = Rover::command(); + let name = "rover".to_string(); + generate(BashShell, &mut cmd, &name, &mut std::io::stdout()); + Ok(RoverOutput::EmptySuccess) + } +} diff --git a/src/command/completion/mod.rs b/src/command/completion/mod.rs new file mode 100644 index 0000000000..6d7d71e248 --- /dev/null +++ b/src/command/completion/mod.rs @@ -0,0 +1,31 @@ +mod bash; +mod zsh; + +use clap::Parser; +use serde::Serialize; + +use crate::{RoverOutput, RoverResult}; + +#[derive(Debug, Serialize, Parser)] +pub struct Completion { + #[clap(subcommand)] + command: Command, +} + +#[derive(Debug, Serialize, Parser)] +enum Command { + /// Generate bash completion script + Bash(bash::Bash), + + /// Generate zsh completion script + Zsh(zsh::Zsh), +} + +impl Completion { + pub fn run(&self) -> RoverResult { + match &self.command { + Command::Bash(command) => command.run(), + Command::Zsh(command) => command.run(), + } + } +} diff --git a/src/command/completion/zsh.rs b/src/command/completion/zsh.rs new file mode 100644 index 0000000000..8efe11a139 --- /dev/null +++ b/src/command/completion/zsh.rs @@ -0,0 +1,17 @@ +use clap::{CommandFactory, Parser}; +use clap_complete::{generate, shells::Zsh as ZshShell}; +use serde::Serialize; + +use crate::{RoverOutput, RoverResult, cli::Rover}; + +#[derive(Debug, Serialize, Parser)] +pub struct Zsh {} + +impl Zsh { + pub fn run(&self) -> RoverResult { + let mut cmd = Rover::command(); + let name = "rover".to_string(); + generate(ZshShell, &mut cmd, &name, &mut std::io::stdout()); + Ok(RoverOutput::EmptySuccess) + } +} diff --git a/src/command/mod.rs b/src/command/mod.rs index fc27876e83..793c08255b 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -1,5 +1,6 @@ mod api_key; mod cloud; +mod completion; mod config; #[cfg(feature = "composition-js")] pub mod connector; @@ -25,6 +26,7 @@ mod update; pub use api_key::ApiKeys; pub use cloud::Cloud; +pub use completion::Completion; pub use config::Config; #[cfg(feature = "composition-js")] pub use connector::Connector; diff --git a/tests/e2e/config/auth.rs b/tests/e2e/config/auth.rs index 46fc69698c..45a8a728d3 100644 --- a/tests/e2e/config/auth.rs +++ b/tests/e2e/config/auth.rs @@ -1,10 +1,11 @@ +use assert_cmd::cargo::cargo_bin_cmd; use predicates::prelude::*; use rstest::rstest; #[rstest] #[ignore] fn e2e_test_rover_auth_help() { - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = cargo_bin_cmd!("rover"); cmd.arg("config") .arg("auth") .arg("--help") @@ -15,7 +16,7 @@ fn e2e_test_rover_auth_help() { #[rstest] #[ignore] fn e2e_test_rover_auth_fail_empty_api_key() { - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = cargo_bin_cmd!("rover"); let result = cmd.arg("config").arg("auth").write_stdin("").assert(); result.stderr(predicate::str::contains("empty")); } diff --git a/tests/e2e/config/clear.rs b/tests/e2e/config/clear.rs index 0534692577..edc35f4e77 100644 --- a/tests/e2e/config/clear.rs +++ b/tests/e2e/config/clear.rs @@ -1,5 +1,6 @@ use std::convert::TryFrom; +use assert_cmd::cargo::cargo_bin_cmd; use camino::Utf8PathBuf; use houston::{Config, Profile}; use predicates::prelude::*; @@ -19,7 +20,7 @@ fn e2e_test_rover_config_clear() { Profile::set_api_key(CUSTOM_PROFILE, &config, CUSTOM_API_KEY).unwrap(); // when one is added - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = cargo_bin_cmd!("rover"); let result = cmd .env(RoverEnvKey::ConfigHome.to_string(), &temp_dir) .arg("config") @@ -28,7 +29,7 @@ fn e2e_test_rover_config_clear() { result.stdout(predicate::str::contains(CUSTOM_PROFILE)); // and then removed via `config clear` - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = cargo_bin_cmd!("rover"); let result = cmd .env(RoverEnvKey::ConfigHome.to_string(), &temp_dir) .arg("config") @@ -37,7 +38,7 @@ fn e2e_test_rover_config_clear() { result.stderr("Successfully cleared all configuration.\n"); // then we should have no profiles - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = cargo_bin_cmd!("rover"); let result = cmd .arg("config") .env(RoverEnvKey::ConfigHome.to_string(), &temp_dir) diff --git a/tests/e2e/config/list.rs b/tests/e2e/config/list.rs index 27fe9f6944..46c16de9c0 100644 --- a/tests/e2e/config/list.rs +++ b/tests/e2e/config/list.rs @@ -1,5 +1,6 @@ use std::convert::TryFrom; +use assert_cmd::cargo::cargo_bin_cmd; use camino::Utf8PathBuf; use houston::{Config, Profile}; use predicates::prelude::*; @@ -14,7 +15,7 @@ const CUSTOM_API_KEY: &str = "custom-api-key"; #[ignore] fn e2e_test_rover_config_list_empty() { let temp_dir = Utf8PathBuf::try_from(TempDir::new().unwrap().path().to_path_buf()).unwrap(); - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = cargo_bin_cmd!("rover"); let result = cmd .arg("config") .env(RoverEnvKey::ConfigHome.to_string(), &temp_dir) @@ -30,7 +31,7 @@ fn e2e_test_rover_config_list_one_profile() { let config = Config::new(Some(temp_dir.clone()).as_ref(), None).unwrap(); Profile::set_api_key(CUSTOM_PROFILE, &config, CUSTOM_API_KEY).unwrap(); - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = cargo_bin_cmd!("rover"); let result = cmd .env(RoverEnvKey::ConfigHome.to_string(), &temp_dir) .arg("config") diff --git a/tests/e2e/config/whoami.rs b/tests/e2e/config/whoami.rs index 38e824c4d0..f1d8740689 100644 --- a/tests/e2e/config/whoami.rs +++ b/tests/e2e/config/whoami.rs @@ -1,3 +1,4 @@ +use assert_cmd::cargo::cargo_bin_cmd; use rstest::rstest; use serde::Deserialize; use serde_json::Value; @@ -24,7 +25,7 @@ fn e2e_test_rover_config_whoami() { .tempfile() .expect("Could not create output file"); - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = cargo_bin_cmd!("rover"); cmd.args([ "config", "whoami", diff --git a/tests/e2e/dev.rs b/tests/e2e/dev.rs index b8240935c5..6ecb1a1a55 100644 --- a/tests/e2e/dev.rs +++ b/tests/e2e/dev.rs @@ -1,5 +1,6 @@ use std::{env, process::Command, time::Duration}; +use assert_cmd::cargo; use mime::APPLICATION_JSON; use portpicker::pick_unused_port; use reqwest::{Client, header::CONTENT_TYPE}; @@ -21,7 +22,7 @@ const ROVER_DEV_TIMEOUT: Duration = Duration::from_secs(45); #[once] #[allow(clippy::zombie_processes)] fn run_rover_dev(run_subgraphs_retail_supergraph: &RetailSupergraph) -> String { - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("rover")); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); let port = pick_unused_port().expect("No ports free"); let router_url = format!("http://localhost:{port}"); let client = Client::new(); diff --git a/tests/e2e/graph/check.rs b/tests/e2e/graph/check.rs index 49068466de..bea34edcbb 100644 --- a/tests/e2e/graph/check.rs +++ b/tests/e2e/graph/check.rs @@ -1,5 +1,6 @@ -use std::path::PathBuf; +use std::{path::PathBuf, process::Command}; +use assert_cmd::cargo; use regex::Regex; use rstest::rstest; use speculoos::{assert_that, boolean::BooleanAssertions}; @@ -24,7 +25,7 @@ async fn e2e_test_rover_graph_check( // WHEN // - the command is run - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.args([ "graph", "check", diff --git a/tests/e2e/graph/fetch.rs b/tests/e2e/graph/fetch.rs index 1a8ef2d009..720a860679 100644 --- a/tests/e2e/graph/fetch.rs +++ b/tests/e2e/graph/fetch.rs @@ -1,3 +1,6 @@ +use std::process::Command; + +use assert_cmd::cargo; use regex::Regex; use rstest::rstest; use speculoos::{assert_that, boolean::BooleanAssertions}; @@ -15,7 +18,7 @@ async fn e2e_test_rover_graph_fetch(remote_supergraph_graphref: String) { // - rover graph fetch to stdout // WHEN // - the command is run - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.args(["graph", "fetch", &remote_supergraph_graphref]); let output = cmd.output().expect("Could not run command"); diff --git a/tests/e2e/graph/introspect.rs b/tests/e2e/graph/introspect.rs index e4ad821249..e6b168000e 100644 --- a/tests/e2e/graph/introspect.rs +++ b/tests/e2e/graph/introspect.rs @@ -6,6 +6,7 @@ use std::{ time::Duration, }; +use assert_cmd::cargo; use graphql_schema_diff::diff; use regex::Regex; use rstest::rstest; @@ -44,7 +45,7 @@ async fn e2e_test_rover_graph_introspect( .suffix(".json") .tempfile() .expect("Could not create output file"); - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.args([ "graph", "introspect", @@ -89,7 +90,7 @@ async fn e2e_test_rover_graph_introspect_watch( .tempfile() .expect("Could not create output file"); // Create the Rover command to run the introspection in `--watch` mode - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("rover")); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); let mut child = cmd .args([ "graph", diff --git a/tests/e2e/init/mod.rs b/tests/e2e/init/mod.rs index 0b2d27fe09..7a73babf58 100644 --- a/tests/e2e/init/mod.rs +++ b/tests/e2e/init/mod.rs @@ -1,8 +1,9 @@ +use assert_cmd::cargo::cargo_bin_cmd; use rstest::rstest; #[rstest] #[ignore] fn e2e_test_rover_init_help() { - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = cargo_bin_cmd!("rover"); cmd.arg("init").arg("--help").assert().success(); } diff --git a/tests/e2e/install/plugin.rs b/tests/e2e/install/plugin.rs index 9b0dd3dbcd..1733df22b6 100644 --- a/tests/e2e/install/plugin.rs +++ b/tests/e2e/install/plugin.rs @@ -1,5 +1,6 @@ -use std::str::from_utf8; +use std::{process::Command, str::from_utf8}; +use assert_cmd::cargo; use assert_fs::TempDir; use camino::Utf8PathBuf; use regex::Regex; @@ -25,7 +26,7 @@ async fn e2e_test_rover_install_plugin(#[case] args: Vec<&str>, #[case] binary_n // - it's run let temp_dir = Utf8PathBuf::try_from(TempDir::new().unwrap().path().to_path_buf()).unwrap(); let bin_path = temp_dir.join(".rover/bin"); - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.env("APOLLO_HOME", temp_dir); cmd.args(args); let output = cmd.output().expect("Could not run command"); @@ -83,7 +84,7 @@ async fn e2e_test_rover_install_plugin_with_force_opt( .collect(); // FIRST INSTALLATION, NO FORCE - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.env("APOLLO_HOME", temp_dir.clone()); cmd.args(args_without_force_option.clone()); let output = cmd.output().expect("Could not run command"); @@ -104,7 +105,7 @@ async fn e2e_test_rover_install_plugin_with_force_opt( assert_that(&installed).is_true(); // SECOND INSTALLATION, NO FORCE, USES EXISTING BINARY - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.env("APOLLO_HOME", temp_dir.clone()); cmd.args(args_without_force_option.clone()); let output = cmd.output().expect("Could not run command"); @@ -124,7 +125,7 @@ async fn e2e_test_rover_install_plugin_with_force_opt( assert_that!(installed).is_true(); // THIRD INSTALLATION, USES FORCE, BINARY EXISTS - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.env("APOLLO_HOME", temp_dir.clone()); cmd.args(forced_args); let output = cmd.output().expect("Could not run command"); @@ -146,7 +147,7 @@ async fn e2e_test_rover_install_plugins_from_latest_plugin_config_file( ) { let temp_dir = Utf8PathBuf::try_from(TempDir::new().unwrap().path().to_path_buf()).unwrap(); let bin_path = temp_dir.join(".rover/bin"); - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); let config_file_contents = std::fs::read_to_string("latest_plugin_versions.json") .expect("Should have been able to read the file"); diff --git a/tests/e2e/options/client_timeout.rs b/tests/e2e/options/client_timeout.rs index e2a9a28c4e..7930b3e918 100644 --- a/tests/e2e/options/client_timeout.rs +++ b/tests/e2e/options/client_timeout.rs @@ -1,3 +1,6 @@ +use std::process::Command; + +use assert_cmd::cargo; use rstest::*; use crate::e2e::remote_supergraph_graphref; @@ -25,7 +28,7 @@ async fn e2e_test_rover_client_timeout_option( // WHEN // - a command supporting the --client-timeout option is invoked - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.env("APOLLO_REGISTRY_URL", fake_registry); cmd.args([ "subgraph", diff --git a/tests/e2e/subgraph/check.rs b/tests/e2e/subgraph/check.rs index 14a525171b..e8cc8e886a 100644 --- a/tests/e2e/subgraph/check.rs +++ b/tests/e2e/subgraph/check.rs @@ -1,5 +1,6 @@ -use std::path::PathBuf; +use std::{path::PathBuf, process::Command}; +use assert_cmd::cargo; use regex::Regex; use rstest::rstest; use speculoos::{assert_that, boolean::BooleanAssertions}; @@ -23,7 +24,7 @@ async fn e2e_test_rover_subgraph_check( // WHEN // - the command is run - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.args([ "subgraph", "check", diff --git a/tests/e2e/subgraph/fetch.rs b/tests/e2e/subgraph/fetch.rs index 8c8ffe4002..a0fd1f5155 100644 --- a/tests/e2e/subgraph/fetch.rs +++ b/tests/e2e/subgraph/fetch.rs @@ -1,5 +1,6 @@ -use std::{fs::read_to_string, path::PathBuf}; +use std::{fs::read_to_string, path::PathBuf, process::Command}; +use assert_cmd::cargo; use graphql_schema_diff::diff; use rstest::rstest; use speculoos::{assert_that, prelude::VecAssertions}; @@ -22,7 +23,7 @@ async fn e2e_test_rover_subgraph_fetch( .suffix(".graphql") .tempfile() .expect("Could not create output file"); - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.args([ "subgraph", "fetch", diff --git a/tests/e2e/subgraph/introspect.rs b/tests/e2e/subgraph/introspect.rs index 97efdf5431..6f66609a21 100644 --- a/tests/e2e/subgraph/introspect.rs +++ b/tests/e2e/subgraph/introspect.rs @@ -6,6 +6,7 @@ use std::{ time::Duration, }; +use assert_cmd::cargo; use graphql_schema_diff::diff; use regex::Regex; use rstest::rstest; @@ -41,7 +42,7 @@ async fn e2e_test_rover_subgraph_introspect( .suffix(".json") .tempfile() .expect("Could not create output file"); - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.args([ "subgraph", "introspect", @@ -87,7 +88,7 @@ async fn e2e_test_rover_subgraph_introspect_watch( .tempfile() .expect("Could not create output file"); // Create the Rover command to run the introspection in `--watch` mode - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("rover")); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); let mut child = cmd .args([ "subgraph", diff --git a/tests/e2e/subgraph/lint.rs b/tests/e2e/subgraph/lint.rs index 7ef8b6c665..6402689d15 100644 --- a/tests/e2e/subgraph/lint.rs +++ b/tests/e2e/subgraph/lint.rs @@ -1,5 +1,6 @@ -use std::path::PathBuf; +use std::{path::PathBuf, process::Command}; +use assert_cmd::cargo; use rstest::rstest; use speculoos::assert_that; use tracing::error; @@ -20,7 +21,7 @@ async fn e2e_test_rover_subgraph_lint( .to_str() .expect("failed to get path to perfSubgraph00.graphql file"); - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.args([ "subgraph", "lint", diff --git a/tests/e2e/subgraph/list.rs b/tests/e2e/subgraph/list.rs index 1a1d03e962..f76127e882 100644 --- a/tests/e2e/subgraph/list.rs +++ b/tests/e2e/subgraph/list.rs @@ -1,3 +1,6 @@ +use std::process::Command; + +use assert_cmd::cargo; use regex::Regex; use rstest::rstest; use speculoos::{assert_that, boolean::BooleanAssertions}; @@ -15,7 +18,7 @@ async fn e2e_test_rover_subgraph_list(remote_supergraph_graphref: String) { // - rover subgraph list to stdout // WHEN // - the command is run - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.args(["subgraph", "list", &remote_supergraph_graphref]); let output = cmd.output().expect("Could not run command"); diff --git a/tests/e2e/subgraph/publish.rs b/tests/e2e/subgraph/publish.rs index 055418ba4e..c6bda57384 100644 --- a/tests/e2e/subgraph/publish.rs +++ b/tests/e2e/subgraph/publish.rs @@ -1,5 +1,6 @@ -use std::{path::PathBuf, str::from_utf8}; +use std::{path::PathBuf, process::Command, str::from_utf8}; +use assert_cmd::cargo; use rand::Rng; use rstest::rstest; use serde::Deserialize; @@ -50,7 +51,7 @@ async fn e2e_test_rover_subgraph_publish( info!("Using name {} for subgraph", &id); // Grab the initial list of subgraphs to check that what we want doesn't already exist - let mut subgraph_list_cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut subgraph_list_cmd = Command::new(cargo::cargo_bin!("rover")); subgraph_list_cmd.args([ "subgraph", "list", @@ -74,7 +75,7 @@ async fn e2e_test_rover_subgraph_publish( // Construct a command to publish a new subgraph to a variant that's specifically for this // purpose info!("Creating subgraph with name {}", &id); - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.args([ "subgraph", "publish", @@ -112,7 +113,7 @@ async fn e2e_test_rover_subgraph_publish( // left with subgraphs lying around. In the future we should move to something like // test-context (https://docs.rs/test-context/latest/test_context/) so that we get cleanup // for free. Until then we can manually clean up if it becomes necessary. - let mut subgraph_delete_cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut subgraph_delete_cmd = Command::new(cargo::cargo_bin!("rover")); subgraph_delete_cmd.args([ "subgraph", "delete", diff --git a/tests/e2e/supergraph/compose.rs b/tests/e2e/supergraph/compose.rs index 4e2f3633f0..6faa5047ff 100644 --- a/tests/e2e/supergraph/compose.rs +++ b/tests/e2e/supergraph/compose.rs @@ -1,5 +1,6 @@ use std::{env, process::Command}; +use assert_cmd::cargo; use regex::RegexSet; use rstest::*; use tracing::error; @@ -16,7 +17,7 @@ async fn e2e_test_run_rover_supergraph_compose(retail_supergraph: &RetailSupergr // - a supergraph config yaml (fixture) // - retail supergraphs representing any set of subgraphs to be composed into a supergraph // (fixture) - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); let mut args: Vec = vec![ "supergraph", "compose", @@ -80,7 +81,7 @@ async fn e2e_test_run_rover_supergraph_compose(retail_supergraph: &RetailSupergr async fn it_fails_without_a_config() { // GIVEN // - an invocation of `rover supergraph compose` without any config file - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("rover")); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.args(["supergraph", "compose"]); // WHEN diff --git a/tests/e2e/supergraph/config.rs b/tests/e2e/supergraph/config.rs index 72ccf3883e..fa5e2f5c52 100644 --- a/tests/e2e/supergraph/config.rs +++ b/tests/e2e/supergraph/config.rs @@ -1,3 +1,6 @@ +use std::process::Command; + +use assert_cmd::cargo; use rstest::rstest; use tracing::error; use tracing_test::traced_test; @@ -7,7 +10,7 @@ use tracing_test::traced_test; #[tokio::test(flavor = "multi_thread")] #[traced_test] async fn e2e_test_rover_supergraph_config_schema() { - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.args(["supergraph", "config", "schema"]); let output = cmd.output().expect("Could not run command"); diff --git a/tests/e2e/supergraph/fetch.rs b/tests/e2e/supergraph/fetch.rs index db659881d4..7b03aac82a 100644 --- a/tests/e2e/supergraph/fetch.rs +++ b/tests/e2e/supergraph/fetch.rs @@ -1,5 +1,6 @@ -use std::fs::read_to_string; +use std::{fs::read_to_string, process::Command}; +use assert_cmd::cargo; use regex::Regex; use rstest::rstest; use tempfile::Builder; @@ -22,7 +23,7 @@ async fn e2e_test_rover_supergraph_fetch(remote_supergraph_graphref: String) { // WHEN // - invoked - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.args([ "supergraph", "fetch", diff --git a/tests/e2e/supergraph/print_json_schema.rs b/tests/e2e/supergraph/print_json_schema.rs index 89f9d044fa..df9ee40ecc 100644 --- a/tests/e2e/supergraph/print_json_schema.rs +++ b/tests/e2e/supergraph/print_json_schema.rs @@ -1,6 +1,6 @@ use std::process::Command; -use assert_cmd::cargo::CommandCargoExt; +use assert_cmd::cargo; use rstest::rstest; use tracing::error; use tracing_test::traced_test; @@ -10,7 +10,7 @@ use tracing_test::traced_test; #[tokio::test(flavor = "multi_thread")] #[traced_test] async fn e2e_test_rover_supergraph_print_json_schema() { - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = Command::new(cargo::cargo_bin!("rover")); cmd.args(["supergraph", "print-json-schema"]); let output = cmd.output().expect("Could not run command"); diff --git a/tests/integration/completion/mod.rs b/tests/integration/completion/mod.rs new file mode 100644 index 0000000000..f58b8a5a1e --- /dev/null +++ b/tests/integration/completion/mod.rs @@ -0,0 +1,110 @@ +#[cfg(not(target_os = "windows"))] +use std::{ + io::Write, + process::{Command as StdCommand, Stdio}, +}; + +#[cfg(not(target_os = "windows"))] +use assert_cmd::cargo::cargo_bin_cmd; +#[cfg(not(target_os = "windows"))] +use predicates::prelude::*; +#[cfg(not(target_os = "windows"))] +use which::which; + +#[cfg(not(target_os = "windows"))] +#[test] +fn it_generates_bash_completion() { + let mut cmd = cargo_bin_cmd!("rover"); + let result = cmd.args(["completion", "bash"]).assert().success(); + + // Bash completion scripts should contain function definitions + result.stdout(predicate::str::contains("_rover")); + + // Validate bash syntax by piping output to bash -n + // Skip syntax validation if bash is not available (e.g., in some CI environments) + if which("bash").is_ok() { + let mut rover_cmd = cargo_bin_cmd!("rover"); + let rover_output = rover_cmd + .args(["completion", "bash"]) + .output() + .expect("Failed to run rover completion bash"); + assert!( + rover_output.status.success(), + "rover completion bash should succeed" + ); + + let mut bash_cmd = StdCommand::new("bash") + .arg("-n") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("Failed to spawn bash"); + + bash_cmd + .stdin + .as_mut() + .expect("Failed to get bash stdin") + .write_all(&rover_output.stdout) + .expect("Failed to write to bash stdin"); + + let bash_result = bash_cmd + .wait_with_output() + .expect("Failed to wait for bash"); + assert!( + bash_result.status.success(), + "bash syntax check failed. stderr: {}", + String::from_utf8_lossy(&bash_result.stderr) + ); + } else { + eprintln!("Skipping bash syntax validation: bash not installed"); + } +} + +#[cfg(not(target_os = "windows"))] +#[test] +fn it_generates_zsh_completion() { + let mut cmd = cargo_bin_cmd!("rover"); + let result = cmd.args(["completion", "zsh"]).assert().success(); + + // Zsh completion scripts should contain completion function definitions + result.stdout(predicate::str::contains("_rover")); + + // Validate zsh syntax by piping output to zsh -n + // Skip syntax validation if zsh is not available (e.g., in some CI environments) + if which("zsh").is_ok() { + let mut rover_cmd = cargo_bin_cmd!("rover"); + let rover_output = rover_cmd + .args(["completion", "zsh"]) + .output() + .expect("Failed to run rover completion zsh"); + assert!( + rover_output.status.success(), + "rover completion zsh should succeed" + ); + + let mut zsh_cmd = StdCommand::new("zsh") + .arg("-n") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("Failed to spawn zsh"); + + zsh_cmd + .stdin + .as_mut() + .expect("Failed to get zsh stdin") + .write_all(&rover_output.stdout) + .expect("Failed to write to zsh stdin"); + + let zsh_result = zsh_cmd.wait_with_output().expect("Failed to wait for zsh"); + assert!( + zsh_result.status.success(), + "zsh syntax check failed. stderr: {}", + String::from_utf8_lossy(&zsh_result.stderr) + ); + } else { + eprintln!("Skipping zsh syntax validation: zsh not installed"); + } +} diff --git a/tests/integration/dev/arg_conflicts.rs b/tests/integration/dev/arg_conflicts.rs index d2d29289e3..e4b6d6cc90 100644 --- a/tests/integration/dev/arg_conflicts.rs +++ b/tests/integration/dev/arg_conflicts.rs @@ -1,8 +1,9 @@ +use assert_cmd::cargo::cargo_bin_cmd; use predicates::prelude::*; #[test] fn super_conflicts_with_url() { - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = cargo_bin_cmd!("rover"); let assert = cmd .arg("dev") .arg("--supergraph-config=supergraph.yaml") @@ -16,7 +17,7 @@ fn super_conflicts_with_url() { #[test] fn super_conflicts_with_schema() { - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = cargo_bin_cmd!("rover"); let assert = cmd .arg("dev") .arg("--supergraph-config=supergraph.yaml") @@ -30,7 +31,7 @@ fn super_conflicts_with_schema() { #[test] fn super_conflicts_with_name() { - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = cargo_bin_cmd!("rover"); let assert = cmd .arg("dev") .arg("--supergraph-config=supergraph.yaml") diff --git a/tests/integration/dev/arg_validation.rs b/tests/integration/dev/arg_validation.rs index 2f32a9f55e..692c0ae940 100644 --- a/tests/integration/dev/arg_validation.rs +++ b/tests/integration/dev/arg_validation.rs @@ -1,8 +1,9 @@ +use assert_cmd::cargo::cargo_bin_cmd; use predicates::prelude::predicate; #[test] fn invalid_ip() { - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = cargo_bin_cmd!("rover"); let assert = cmd .arg("dev") .arg("--supergraph-address=notanip") @@ -15,7 +16,7 @@ fn invalid_ip() { #[test] fn invalid_port() { - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = cargo_bin_cmd!("rover"); let assert = cmd .arg("dev") .arg("--supergraph-port=notaport") diff --git a/tests/integration/info/mod.rs b/tests/integration/info/mod.rs index 35797ddb32..65fa5053cb 100644 --- a/tests/integration/info/mod.rs +++ b/tests/integration/info/mod.rs @@ -1,9 +1,10 @@ +use assert_cmd::cargo::cargo_bin_cmd; use predicates::prelude::*; use rover::PKG_VERSION; #[test] fn it_prints_info() { - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = cargo_bin_cmd!("rover"); let result = cmd.arg("info").assert().success(); // the version should always be available in the `info` output diff --git a/tests/integration/mod.rs b/tests/integration/mod.rs index 7e10f370f5..e3dfbf2261 100644 --- a/tests/integration/mod.rs +++ b/tests/integration/mod.rs @@ -1,3 +1,4 @@ +mod completion; mod dev; mod info; mod installers; diff --git a/tests/integration/output/to_file.rs b/tests/integration/output/to_file.rs index eb51360cb1..d772440287 100644 --- a/tests/integration/output/to_file.rs +++ b/tests/integration/output/to_file.rs @@ -1,5 +1,6 @@ use std::fs; +use assert_cmd::cargo::cargo_bin_cmd; use camino::Utf8PathBuf; use houston::{Config, Profile}; use rover::utils::env::RoverEnvKey; @@ -44,7 +45,7 @@ fn it_can_write_files_correctly_no_matter_the_input_path( fs::write(file, "foo bar bash").expect("Could not create existing directories"); } - let mut starter_cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut starter_cmd = cargo_bin_cmd!("rover"); starter_cmd .env(RoverEnvKey::ConfigHome.to_string(), temp_config_dir_path) .args(vec![ diff --git a/tests/integration/schema/fetch.rs b/tests/integration/schema/fetch.rs index eb50294c10..bb24fb0d07 100644 --- a/tests/integration/schema/fetch.rs +++ b/tests/integration/schema/fetch.rs @@ -1,6 +1,8 @@ +use assert_cmd::cargo::cargo_bin_cmd; + #[test] fn it_has_a_graph_fetch_command() { - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = cargo_bin_cmd!("rover"); cmd.arg("graph") .arg("fetch") .arg("--help") diff --git a/tests/main.rs b/tests/main.rs index 9992702af0..0307b3c080 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -1,9 +1,10 @@ +use assert_cmd::cargo::cargo_bin_cmd; use predicates::prelude::*; mod integration; #[test] fn its_executable() { - let mut cmd = assert_cmd::cargo::cargo_bin_cmd!("rover"); + let mut cmd = cargo_bin_cmd!("rover"); // running the CLI with no command returns the help message to stderr let result = cmd.assert();