diff --git a/radicle-cli/examples/rad-clone-unknown.md b/radicle-cli/examples/rad-clone-unknown.md new file mode 100644 index 00000000..58fe4055 --- /dev/null +++ b/radicle-cli/examples/rad-clone-unknown.md @@ -0,0 +1,7 @@ +Trying to clone a repository that is not in our routing table returns an error: + +``` +$ rad clone rad:zVNuptPuk5XauitpCWSNVCXGGfXW +ok Tracking relationship established for rad:zVNuptPuk5XauitpCWSNVCXGGfXW +== Clone failed no seeds found for rad:zVNuptPuk5XauitpCWSNVCXGGfXW +``` diff --git a/radicle-cli/examples/rad-init.md b/radicle-cli/examples/rad-init.md index 31343d1a..a47bcc91 100644 --- a/radicle-cli/examples/rad-init.md +++ b/radicle-cli/examples/rad-init.md @@ -3,7 +3,7 @@ To create your first radicle project, navigate to a git repository, and run the `init` command: ``` -$ rad init --name heartwood --description "Radicle Heartwood Protocol & Stack" --no-confirm +$ rad init --name heartwood --description "Radicle Heartwood Protocol & Stack" --no-confirm --no-sync Initializing local 🌱 project in . diff --git a/radicle-cli/src/commands/clone.rs b/radicle-cli/src/commands/clone.rs index bf78c88f..81f41b48 100644 --- a/radicle-cli/src/commands/clone.rs +++ b/radicle-cli/src/commands/clone.rs @@ -131,6 +131,8 @@ pub enum CloneError { Payload(#[from] doc::PayloadError), #[error("project error: {0}")] Project(#[from] ProjectError), + #[error("no seeds found for {0}")] + NotFound(Id), } pub fn clone( @@ -149,8 +151,11 @@ pub fn clone( ); } - // Get seeds. + // Get seeds. This consults the local routing table only. let seeds = node.seeds(id)?; + if seeds.is_empty() { + return Err(CloneError::NotFound(id)); + } // Fetch from all seeds. for seed in seeds { let spinner = term::spinner(format!( diff --git a/radicle-cli/src/terminal/io.rs b/radicle-cli/src/terminal/io.rs index 56b046c3..6df37e71 100644 --- a/radicle-cli/src/terminal/io.rs +++ b/radicle-cli/src/terminal/io.rs @@ -87,7 +87,7 @@ pub fn help(name: &str, version: &str, description: &str, usage: &str) { } pub fn usage(name: &str, usage: &str) { - eprintln!( + println!( "{} {}\n{}", style("==").red(), style(format!("Error: rad-{name}: invalid usage")).red(), @@ -95,8 +95,8 @@ pub fn usage(name: &str, usage: &str) { ); } -pub fn eprintln(prefix: impl fmt::Display, msg: impl fmt::Display) { - eprintln!("{prefix} {msg}"); +pub fn println(prefix: impl fmt::Display, msg: impl fmt::Display) { + println!("{prefix} {msg}"); } pub fn indented(msg: impl fmt::Display) { @@ -108,7 +108,7 @@ pub fn subcommand(msg: impl fmt::Display) { } pub fn warning(warning: &str) { - eprintln!( + println!( "{} {} {}", style("**").yellow(), style("Warning:").yellow().bold(), @@ -117,7 +117,7 @@ pub fn warning(warning: &str) { } pub fn error(error: impl fmt::Display) { - eprintln!("{} {}", style("==").red(), style(error).red()); + println!("{} {}", style("==").red(), style(error).red()); } pub fn fail(header: &str, error: &anyhow::Error) { @@ -129,7 +129,7 @@ pub fn fail(header: &str, error: &anyhow::Error) { " " }; - eprintln!( + println!( "{} {}{}{}", style("==").red(), style(header).red().reverse(), @@ -139,7 +139,7 @@ pub fn fail(header: &str, error: &anyhow::Error) { let cause = error.root_cause(); if cause.to_string() != error.to_string() { - eprintln!( + println!( "{} {}", style("==").red().dim(), style(error.root_cause()).red().dim() @@ -148,7 +148,7 @@ pub fn fail(header: &str, error: &anyhow::Error) { } if let Some(Error::WithHint { hint, .. }) = error.downcast_ref::() { - eprintln!("{} {}", style("==").yellow(), style(hint).yellow(),); + println!("{} {}", style("==").yellow(), style(hint).yellow(),); blank(); } } diff --git a/radicle-cli/src/terminal/spinner.rs b/radicle-cli/src/terminal/spinner.rs index ad9ac8dc..036d971f 100644 --- a/radicle-cli/src/terminal/spinner.rs +++ b/radicle-cli/src/terminal/spinner.rs @@ -35,7 +35,7 @@ impl Spinner { pub fn error(mut self, msg: impl ToString) { let msg = msg.to_string(); - self.message = format!("{} (error: {})", self.message, msg); + self.message = format!("{} error: {}", self.message, msg); self.set_failed(); } @@ -52,7 +52,7 @@ impl Spinner { pub fn set_failed(&mut self) { self.progress.finish_and_clear(); - term::eprintln(style("!!").red().reverse(), &self.message); + term::println(style("!!").red().reverse(), &self.message); } } diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs index 73838fea..6fa2b015 100644 --- a/radicle-cli/tests/commands.rs +++ b/radicle-cli/tests/commands.rs @@ -173,6 +173,25 @@ fn rad_clone() { test("examples/rad-clone.md", working, Some(&bob.home), []).unwrap(); } +#[test] +fn rad_clone_unknown() { + logger::init(log::Level::Debug); + + let mut environment = Environment::new(); + let alice = environment.node("alice"); + let working = environment.tmp().join("working"); + + let alice = alice.spawn(Config::default()); + + test( + "examples/rad-clone-unknown.md", + working, + Some(&alice.home), + [], + ) + .unwrap(); +} + #[test] fn rad_init_announce_refs() { logger::init(log::Level::Debug); diff --git a/radicle-cli/tests/framework/mod.rs b/radicle-cli/tests/framework/mod.rs index be2487aa..47f81995 100644 --- a/radicle-cli/tests/framework/mod.rs +++ b/radicle-cli/tests/framework/mod.rs @@ -8,6 +8,9 @@ use snapbox::cmd::{Command, OutputAssert}; use snapbox::{Assert, Substitutions}; use thiserror::Error; +/// Error lines in the CLI are prefixed with this string. +const ERROR_PREFIX: &str = "=="; + #[derive(Error, Debug)] pub enum Error { #[error("parsing failed")] @@ -18,6 +21,12 @@ pub enum Error { Snapbox(#[from] snapbox::Error), } +#[derive(Debug, PartialEq, Eq)] +enum ExitStatus { + Success, + Failure, +} + /// A test which may contain multiple assertions. #[derive(Debug, Default, PartialEq, Eq)] pub struct Test { @@ -36,6 +45,8 @@ pub struct Assertion { args: Vec, /// Expected output (stdout or stderr). expected: String, + /// Expected exit status. + exit: ExitStatus, } #[derive(Debug, Default, PartialEq, Eq)] @@ -112,8 +123,12 @@ impl TestFormula { command: cmd.to_owned(), args: args.to_owned(), expected: String::new(), + exit: ExitStatus::Success, }); } else if let Some(test) = test.assertions.last_mut() { + if line.starts_with(ERROR_PREFIX) { + test.exit = ExitStatus::Failure; + } test.expected.push_str(line.as_str()); test.expected.push('\n'); } else { @@ -181,7 +196,14 @@ impl TestFormula { match result { Ok(output) => { let assert = OutputAssert::new(output).with_assert(assert.clone()); - assert.stdout_matches(&assertion.expected).success(); + match assertion.exit { + ExitStatus::Success => { + assert.stdout_matches(&assertion.expected).success(); + } + ExitStatus::Failure => { + assert.stdout_matches(&assertion.expected).failure(); + } + } } Err(err) => { if err.kind() == io::ErrorKind::NotFound { @@ -246,6 +268,7 @@ $ rad sync expected: String::from( "Tracking relationship established for @dave.\nNothing to do.\n\n", ), + exit: ExitStatus::Success, }, Assertion { command: String::from("rad"), @@ -253,6 +276,7 @@ $ rad sync expected: String::from( "Tracking relationship established for @sean.\nNothing to do.\n", ), + exit: ExitStatus::Success, }, ], }, @@ -262,6 +286,7 @@ $ rad sync command: String::from("rad"), args: vec![String::from("sync")], expected: String::new(), + exit: ExitStatus::Success, }], }, ],