diff --git a/coffee_cmd/src/coffee_term/command_show.rs b/coffee_cmd/src/coffee_term/command_show.rs index 20693153..3014b820 100644 --- a/coffee_cmd/src/coffee_term/command_show.rs +++ b/coffee_cmd/src/coffee_term/command_show.rs @@ -84,7 +84,7 @@ pub fn show_nurse_result( Ok(nurse) => { // special case: if the nurse is sane // we print a message and return - if nurse.status[0] == NurseStatus::Sane { + if nurse.is_sane() { term::success!("Coffee configuration is not corrupt! No need to run coffee nurse"); return Ok(()); } @@ -98,14 +98,12 @@ pub fn show_nurse_result( for status in &nurse.status { let action_str = match status { - NurseStatus::Sane => "".to_string(), NurseStatus::RepositoryLocallyRestored(_) => "Restored using Git".to_string(), NurseStatus::RepositoryLocallyRemoved(_) => { "Removed from local storage".to_string() } }; let repos_str = match status { - NurseStatus::Sane => "".to_string(), NurseStatus::RepositoryLocallyRestored(repos) | NurseStatus::RepositoryLocallyRemoved(repos) => repos.join(", "), }; diff --git a/coffee_cmd/src/main.rs b/coffee_cmd/src/main.rs index 1ae25355..30d51cfd 100644 --- a/coffee_cmd/src/main.rs +++ b/coffee_cmd/src/main.rs @@ -160,9 +160,18 @@ async fn main() -> Result<(), CoffeeError> { } Err(err) => Err(err), }, - CoffeeCommand::Nurse {} => { - let nurse_result = coffee.nurse().await; - coffee_term::show_nurse_result(nurse_result) + CoffeeCommand::Nurse { verify } => { + if verify { + let result = coffee.nurse_verify().await?; + term::info!("{}", result); + if !result.is_sane() { + term::info!("Coffee local directory is damaged, please run `coffee nurse` to try to fix it"); + } + Ok(()) + } else { + let nurse_result = coffee.nurse().await; + coffee_term::show_nurse_result(nurse_result) + } } }; diff --git a/coffee_core/src/coffee.rs b/coffee_core/src/coffee.rs index 66b29e2c..6b087799 100644 --- a/coffee_core/src/coffee.rs +++ b/coffee_core/src/coffee.rs @@ -122,6 +122,19 @@ impl CoffeeManager { if let Err(err) = self.coffee_cln_config.parse() { log::error!("{}", err.cause); } + if !self.config.skip_verify { + // Check for the chain of responsibility + let status = self.recovery_strategies.scan(self).await?; + log::debug!("Chain of responsibility status: {:?}", status); + // if any defect is found, we print a warning message (we don't take action) + if !status.defects.is_empty() { + return Err( + error!("Coffee found some defects in the configuration. Please run `coffee nurse` to fix them. + If you are want to skip the verification, please add the `--skip-verify ` flag to the command.") + ); + }; + } + self.load_cln_conf().await?; log::debug!("cln conf {:?}", self.coffee_cln_config); log::debug!("finish plugin manager inventory"); @@ -453,6 +466,10 @@ impl PluginManager for CoffeeManager { Err(err) } + async fn nurse_verify(&self) -> Result { + self.recovery_strategies.scan(self).await + } + async fn nurse(&mut self) -> Result { let status = self.recovery_strategies.scan(self).await?; let mut nurse_actions: Vec = vec![]; @@ -465,14 +482,11 @@ impl PluginManager for CoffeeManager { } } } - - // If there was no actions taken by nurse, we return a sane status. - if nurse_actions.is_empty() { - nurse_actions.push(NurseStatus::Sane); - } - Ok(CoffeeNurse { + let mut nurse = CoffeeNurse { status: nurse_actions, - }) + }; + nurse.organize(); + Ok(nurse) } async fn patch_repository_locally_absent( @@ -503,6 +517,16 @@ impl PluginManager for CoffeeManager { } Err(err) => { log::debug!("error while recovering repository {repo_name}: {err}"); + // We make sure that the repository folder is removed + // from local storage. + // Maybe when trying to recover the repository, + // we have created the folder but we were not able + // to clone the repository. + let repo_path = repo.url().path_string; + // This shouldn't return an error if the repository + // is not present locally. + let _ = fs::remove_dir_all(repo_path).await; + log::info!("removing repository {}", repo_name.clone()); self.repos.remove(repo_name); log::debug!("remote removed: {}", repo_name); diff --git a/coffee_core/src/lib.rs b/coffee_core/src/lib.rs index 6e5588f1..da3a6cb6 100644 --- a/coffee_core/src/lib.rs +++ b/coffee_core/src/lib.rs @@ -21,7 +21,7 @@ pub enum CoffeeOperation { Show(String), /// Search(plugin name) Search(String), - Nurse, + Nurse(bool), } #[derive(Clone, Debug)] @@ -40,4 +40,6 @@ pub trait CoffeeArgs: Send + Sync { fn network(&self) -> Option; /// return the data dir fn data_dir(&self) -> Option; + /// return the skip verify flag + fn skip_verify(&self) -> bool; } diff --git a/coffee_lib/src/plugin_manager.rs b/coffee_lib/src/plugin_manager.rs index 9d42626e..7ae5fcc2 100644 --- a/coffee_lib/src/plugin_manager.rs +++ b/coffee_lib/src/plugin_manager.rs @@ -52,6 +52,9 @@ pub trait PluginManager { /// clean up storage information about the remote repositories of the plugin manager. async fn nurse(&mut self) -> Result; + /// verify that coffee configuration is sane without taking any action. + async fn nurse_verify(&self) -> Result; + /// patch coffee configuration in the case that a repository is present in the coffee /// configuration but is absent from the local storage. async fn patch_repository_locally_absent( diff --git a/coffee_lib/src/types/mod.rs b/coffee_lib/src/types/mod.rs index 19ead44b..2c73f2d2 100644 --- a/coffee_lib/src/types/mod.rs +++ b/coffee_lib/src/types/mod.rs @@ -116,7 +116,7 @@ pub mod response { /// This struct is used to represent a defect /// that can be patched by the nurse. - #[derive(Clone, Debug, Serialize, Deserialize)] + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum Defect { // A patch operation when a git repository is present in the coffee configuration // but is absent from the local storage. @@ -129,12 +129,38 @@ pub mod response { pub defects: Vec, } + impl ChainOfResponsibilityStatus { + pub fn is_sane(&self) -> bool { + self.defects.is_empty() + } + } + + impl fmt::Display for ChainOfResponsibilityStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.defects.is_empty() { + write!(f, "Coffee is sane") + } else { + writeln!(f, "Coffee has the following defects:")?; + for (i, defect) in self.defects.iter().enumerate() { + match defect { + Defect::RepositoryLocallyAbsent(repos) => { + write!(f, "{}. Repository missing locally: ", i + 1)?; + for repo in repos { + write!(f, " {}", repo)?; + } + } + } + } + Ok(()) + } + } + } + /// This struct is used to represent the status of nurse, /// either sane or not. /// If not sane, return the action that nurse has taken. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum NurseStatus { - Sane, RepositoryLocallyRestored(Vec), RepositoryLocallyRemoved(Vec), } @@ -144,13 +170,44 @@ pub mod response { pub status: Vec, } + impl CoffeeNurse { + pub fn is_sane(&self) -> bool { + self.status.is_empty() + } + + pub fn organize(&mut self) { + // For every action taken by the nurse, we want to + // have 1 entry with the list of repositories affected. + let mut new_status: Vec = vec![]; + let mut repositories_locally_removed: Vec = vec![]; + let mut repositories_locally_restored: Vec = vec![]; + for repo in self.status.iter() { + match repo { + NurseStatus::RepositoryLocallyRemoved(repos) => { + repositories_locally_removed.append(&mut repos.clone()) + } + NurseStatus::RepositoryLocallyRestored(repos) => { + repositories_locally_restored.append(&mut repos.clone()) + } + } + } + if !repositories_locally_removed.is_empty() { + new_status.push(NurseStatus::RepositoryLocallyRemoved( + repositories_locally_removed, + )); + } + if !repositories_locally_restored.is_empty() { + new_status.push(NurseStatus::RepositoryLocallyRestored( + repositories_locally_restored, + )); + } + self.status = new_status; + } + } + impl fmt::Display for NurseStatus { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - NurseStatus::Sane => write!( - f, - "coffee configuration is not corrupt! No need to run coffee nurse" - ), NurseStatus::RepositoryLocallyRestored(val) => { write!(f, "Repositories restored locally: {}", val.join(" ")) } diff --git a/coffee_plugin/src/plugin/state.rs b/coffee_plugin/src/plugin/state.rs index 8cbfbe34..6c9dd39b 100644 --- a/coffee_plugin/src/plugin/state.rs +++ b/coffee_plugin/src/plugin/state.rs @@ -75,6 +75,11 @@ impl CoffeeArgs for PluginArgs { fn network(&self) -> Option { Some(self.network.clone()) } + + // we don't need to verify the nurse + fn skip_verify(&self) -> bool { + true + } } impl From for PluginArgs { diff --git a/tests/src/coffee_integration_tests.rs b/tests/src/coffee_integration_tests.rs index 3d7c6b41..126af133 100644 --- a/tests/src/coffee_integration_tests.rs +++ b/tests/src/coffee_integration_tests.rs @@ -5,7 +5,7 @@ use tokio::fs; use serde_json::json; use coffee_lib::plugin_manager::PluginManager; -use coffee_lib::types::response::NurseStatus; +use coffee_lib::types::response::{Defect, NurseStatus}; use coffee_testing::cln::Node; use coffee_testing::prelude::tempfile; use coffee_testing::{CoffeeTesting, CoffeeTestingArgs}; @@ -634,10 +634,8 @@ pub async fn test_nurse_repository_missing_on_disk() { let result = manager.coffee().nurse().await; assert!(result.is_ok(), "{:?}", result); let result = result.unwrap(); - // Assert result has only 1 value - assert_eq!(result.status.len(), 1, "{:?}", result); // Assert that the value is Sane - assert_eq!(result.status[0], NurseStatus::Sane, "{:?}", result); + assert!(result.is_sane()); // Remove folgore repository (we militate that the repository is missing on disk) let result = fs::remove_dir_all(&folgore_path).await; @@ -650,6 +648,20 @@ pub async fn test_nurse_repository_missing_on_disk() { folgore_path ); + // Assert that nurse --verify returns that coffee is corrupt + let result = manager.coffee().nurse_verify().await; + assert!(result.is_ok(), "{:?}", result); + let result = result.unwrap(); + // Assert that the value is Corrupt + let defects = result.defects; + assert_eq!(defects.len(), 1, "{:?}", defects); + assert_eq!( + defects[0], + Defect::RepositoryLocallyAbsent(vec!["folgore".to_string()]), + "{:?}", + defects + ); + // Run nurse again // Assert that nurse returns that coffee isn't Sane let result = manager.coffee().nurse().await; @@ -679,5 +691,12 @@ pub async fn test_nurse_repository_missing_on_disk() { folgore_readme_path ); + // Assert that nurse --verify returns that coffee is Sane + let result = manager.coffee().nurse_verify().await; + assert!(result.is_ok(), "{:?}", result); + let result = result.unwrap(); + // Assert that the value is Sane + assert!(result.is_sane()); + cln.stop().await.unwrap(); }