From b2704bd3d2a07d38ad6cffc298e7602a2c01cbb4 Mon Sep 17 00:00:00 2001 From: daladim Date: Tue, 13 Apr 2021 23:32:07 +0200 Subject: [PATCH] Ability to mock behaviours for tests --- src/cache.rs | 28 +++--- src/calendar/cached_calendar.rs | 28 ++++-- src/lib.rs | 1 + src/mock_behaviour.rs | 155 ++++++++++++++++++++++++++++++++ src/provider.rs | 138 ++++++++++++++++++---------- tests/scenarii.rs | 13 +-- tests/sync.rs | 57 +++++++++--- 7 files changed, 340 insertions(+), 80 deletions(-) create mode 100644 src/mock_behaviour.rs diff --git a/src/cache.rs b/src/cache.rs index 4f53bba..4462aa4 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -4,8 +4,6 @@ use std::path::PathBuf; use std::path::Path; use std::error::Error; use std::collections::HashMap; -use std::collections::HashSet; -use std::hash::Hash; use std::sync::{Arc, Mutex}; use std::ffi::OsStr; @@ -19,6 +17,9 @@ use crate::calendar::cached_calendar::CachedCalendar; use crate::calendar::CalendarId; use crate::calendar::SupportedComponents; +#[cfg(feature = "local_calendar_mocks_remote_calendars")] +use crate::mock_behaviour::MockBehaviour; + const MAIN_FILE: &str = "data.json"; /// A CalDAV source that stores its item in a local folder @@ -27,8 +28,9 @@ pub struct Cache { backing_folder: PathBuf, data: CachedData, + /// In tests, we may add forced errors to this object #[cfg(feature = "local_calendar_mocks_remote_calendars")] - is_mocking_remote_source: bool, + mock_behaviour: Option>>, } #[derive(Default, Debug, Serialize, Deserialize)] @@ -40,8 +42,8 @@ struct CachedData { impl Cache { /// Activate the "mocking remote source" features (i.e. tell its children calendars that they are mocked remote calendars) #[cfg(feature = "local_calendar_mocks_remote_calendars")] - pub fn set_is_mocking_remote_source(&mut self) { - self.is_mocking_remote_source = true; + pub fn set_mock_behaviour(&mut self, mock_behaviour: Option>>) { + self.mock_behaviour = mock_behaviour; } @@ -91,7 +93,7 @@ impl Cache { data, #[cfg(feature = "local_calendar_mocks_remote_calendars")] - is_mocking_remote_source: false, + mock_behaviour: None, }) } @@ -107,7 +109,7 @@ impl Cache { data: CachedData::default(), #[cfg(feature = "local_calendar_mocks_remote_calendars")] - is_mocking_remote_source: false, + mock_behaviour: None, } } @@ -169,6 +171,9 @@ impl Cache { #[async_trait] impl CalDavSource for Cache { async fn get_calendars(&self) -> Result>>, Box> { + #[cfg(feature = "local_calendar_mocks_remote_calendars")] + self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_calendars())?; + Ok(self.data.calendars.iter() .map(|(id, cal)| (id.clone(), cal.clone())) .collect() @@ -181,13 +186,16 @@ impl CalDavSource for Cache { async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents) -> Result>, Box> { log::debug!("Inserting local calendar {}", id); + #[cfg(feature = "local_calendar_mocks_remote_calendars")] + self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_create_calendar())?; + let new_calendar = CachedCalendar::new(name, id.clone(), supported_components); let arc = Arc::new(Mutex::new(new_calendar)); #[cfg(feature = "local_calendar_mocks_remote_calendars")] - if self.is_mocking_remote_source { - arc.lock().unwrap().set_is_mocking_remote_calendar(); - } + if let Some(behaviour) = &self.mock_behaviour { + arc.lock().unwrap().set_mock_behaviour(Some(Arc::clone(behaviour))); + }; match self.data.calendars.insert(id, arc.clone()) { Some(_) => Err("Attempt to insert calendar failed: there is alredy such a calendar.".into()), diff --git a/src/calendar/cached_calendar.rs b/src/calendar/cached_calendar.rs index e180a08..fa8b381 100644 --- a/src/calendar/cached_calendar.rs +++ b/src/calendar/cached_calendar.rs @@ -10,6 +10,11 @@ use crate::calendar::{CalendarId, SupportedComponents}; use crate::Item; use crate::item::ItemId; +#[cfg(feature = "local_calendar_mocks_remote_calendars")] +use std::sync::{Arc, Mutex}; +#[cfg(feature = "local_calendar_mocks_remote_calendars")] +use crate::mock_behaviour::MockBehaviour; + /// A calendar used by the [`cache`](crate::cache) module #[derive(Clone, Debug, Serialize, Deserialize)] @@ -18,7 +23,8 @@ pub struct CachedCalendar { id: CalendarId, supported_components: SupportedComponents, #[cfg(feature = "local_calendar_mocks_remote_calendars")] - is_mocking_remote_calendar: bool, + #[serde(skip)] + mock_behaviour: Option>>, items: HashMap, } @@ -26,8 +32,8 @@ pub struct CachedCalendar { impl CachedCalendar { /// Activate the "mocking remote calendar" feature (i.e. ignore sync statuses, since this is what an actual CalDAV sever would do) #[cfg(feature = "local_calendar_mocks_remote_calendars")] - pub fn set_is_mocking_remote_calendar(&mut self) { - self.is_mocking_remote_calendar = true; + pub fn set_mock_behaviour(&mut self, mock_behaviour: Option>>) { + self.mock_behaviour = mock_behaviour; } /// Add an item @@ -107,7 +113,10 @@ impl BaseCalendar for CachedCalendar { } #[cfg(feature = "local_calendar_mocks_remote_calendars")] async fn add_item(&mut self, item: Item) -> Result> { - if self.is_mocking_remote_calendar { + #[cfg(feature = "local_calendar_mocks_remote_calendars")] + self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_add_item())?; + + if self.mock_behaviour.is_some() { self.add_item_force_synced(item).await } else { self.regular_add_item(item).await @@ -121,7 +130,7 @@ impl CompleteCalendar for CachedCalendar { Self { name, id, supported_components, #[cfg(feature = "local_calendar_mocks_remote_calendars")] - is_mocking_remote_calendar: false, + mock_behaviour: None, items: HashMap::new(), } } @@ -200,6 +209,9 @@ impl DavCalendar for CachedCalendar { } async fn get_item_version_tags(&self) -> Result, Box> { + #[cfg(feature = "local_calendar_mocks_remote_calendars")] + self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_item_version_tags())?; + use crate::item::SyncStatus; let mut result = HashMap::new(); @@ -218,10 +230,16 @@ impl DavCalendar for CachedCalendar { } async fn get_item_by_id(&self, id: &ItemId) -> Result, Box> { + #[cfg(feature = "local_calendar_mocks_remote_calendars")] + self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_get_item_by_id())?; + Ok(self.items.get(id).cloned()) } async fn delete_item(&mut self, item_id: &ItemId) -> Result<(), Box> { + #[cfg(feature = "local_calendar_mocks_remote_calendars")] + self.mock_behaviour.as_ref().map_or(Ok(()), |b| b.lock().unwrap().can_delete_item())?; + self.immediately_delete_item(item_id).await } } diff --git a/src/lib.rs b/src/lib.rs index 1c7ccde..efb6525 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,7 @@ mod event; pub use event::Event; pub mod provider; pub use provider::Provider; +pub mod mock_behaviour; pub mod client; pub use client::Client; diff --git a/src/mock_behaviour.rs b/src/mock_behaviour.rs new file mode 100644 index 0000000..87e8d63 --- /dev/null +++ b/src/mock_behaviour.rs @@ -0,0 +1,155 @@ +//! This module provides ways to tweak mocked calendars, so that they can return errors on some tests + +use std::error::Error; + +/// This stores some behaviour tweaks, that describe how a mocked instance will behave during a given test +/// +/// So that a functions fails _n_ times after _m_ initial successes, set `(m, n)` for the suited parameter +#[derive(Default, Clone, Debug)] +pub struct MockBehaviour { + /// If this is true, every action will be allowed + pub is_suspended: bool, + + // From the CalDavSource trait + pub get_calendars_behaviour: (u32, u32), + //pub get_calendar_behaviour: (u32, u32), + pub create_calendar_behaviour: (u32, u32), + + // From the BaseCalendar trait + pub add_item_behaviour: (u32, u32), + + // From the DavCalendar trait + pub get_item_version_tags_behaviour: (u32, u32), + pub get_item_by_id_behaviour: (u32, u32), + pub delete_item_behaviour: (u32, u32), +} + +impl MockBehaviour { + pub fn new() -> Self { + Self::default() + } + + /// All items will fail at once, for `n_fails` times + pub fn fail_now(n_fails: u32) -> Self { + Self { + is_suspended: false, + get_calendars_behaviour: (0, n_fails), + //get_calendar_behaviour: (0, n_fails), + create_calendar_behaviour: (0, n_fails), + add_item_behaviour: (0, n_fails), + get_item_version_tags_behaviour: (0, n_fails), + get_item_by_id_behaviour: (0, n_fails), + delete_item_behaviour: (0, n_fails), + } + } + + /// Suspend this mock behaviour until you call `resume` + pub fn suspend(&mut self) { + self.is_suspended = true; + } + /// Make this behaviour active again + pub fn resume(&mut self) { + self.is_suspended = false; + } + + pub fn copy_from(&mut self, other: &Self) { + self.get_calendars_behaviour = other.get_calendars_behaviour; + self.create_calendar_behaviour = other.create_calendar_behaviour; + } + + pub fn can_get_calendars(&mut self) -> Result<(), Box> { + if self.is_suspended { return Ok(()) } + decrement(&mut self.get_calendars_behaviour, "get_calendars") + } + // pub fn can_get_calendar(&mut self) -> Result<(), Box> { + // if self.is_suspended { return Ok(()) } + // decrement(&mut self.get_calendar_behaviour, "get_calendar") + // } + pub fn can_create_calendar(&mut self) -> Result<(), Box> { + if self.is_suspended { return Ok(()) } + decrement(&mut self.create_calendar_behaviour, "create_calendar") + } + pub fn can_add_item(&mut self) -> Result<(), Box> { + if self.is_suspended { return Ok(()) } + decrement(&mut self.add_item_behaviour, "add_item") + } + pub fn can_get_item_version_tags(&mut self) -> Result<(), Box> { + if self.is_suspended { return Ok(()) } + decrement(&mut self.get_item_version_tags_behaviour, "get_item_version_tags") + } + pub fn can_get_item_by_id(&mut self) -> Result<(), Box> { + if self.is_suspended { return Ok(()) } + decrement(&mut self.get_item_by_id_behaviour, "get_item_by_id") + } + pub fn can_delete_item(&mut self) -> Result<(), Box> { + if self.is_suspended { return Ok(()) } + decrement(&mut self.delete_item_behaviour, "delete_item") + } +} + + +/// Return Ok(()) in case the value is `(1+, _)` or `(_, 0)`, or return Err and decrement otherwise +fn decrement(value: &mut (u32, u32), descr: &str) -> Result<(), Box> { + let remaining_successes = value.0; + let remaining_failures = value.1; + + if remaining_successes > 0 { + value.0 = value.0 - 1; + log::debug!("Mock behaviour: allowing a {} ({:?})", descr, value); + Ok(()) + } else { + if remaining_failures > 0 { + value.1 = value.1 - 1; + log::debug!("Mock behaviour: failing a {} ({:?})", descr, value); + Err(format!("Mocked behaviour requires this {} to fail this time. ({:?})", descr, value).into()) + } else { + log::debug!("Mock behaviour: allowing a {} ({:?})", descr, value); + Ok(()) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_mock_behaviour() { + let mut ok = MockBehaviour::new(); + assert!(ok.can_get_calendars().is_ok()); + assert!(ok.can_get_calendars().is_ok()); + assert!(ok.can_get_calendars().is_ok()); + assert!(ok.can_get_calendars().is_ok()); + assert!(ok.can_get_calendars().is_ok()); + assert!(ok.can_get_calendars().is_ok()); + assert!(ok.can_get_calendars().is_ok()); + + let mut now = MockBehaviour::fail_now(2); + assert!(now.can_get_calendars().is_err()); + assert!(now.can_create_calendar().is_err()); + assert!(now.can_create_calendar().is_err()); + assert!(now.can_get_calendars().is_err()); + assert!(now.can_get_calendars().is_ok()); + assert!(now.can_get_calendars().is_ok()); + assert!(now.can_create_calendar().is_ok()); + + let mut custom = MockBehaviour{ + get_calendars_behaviour: (0,1), + create_calendar_behaviour: (1,3), + ..MockBehaviour::default() + }; + assert!(custom.can_get_calendars().is_err()); + assert!(custom.can_get_calendars().is_ok()); + assert!(custom.can_get_calendars().is_ok()); + assert!(custom.can_get_calendars().is_ok()); + assert!(custom.can_get_calendars().is_ok()); + assert!(custom.can_get_calendars().is_ok()); + assert!(custom.can_get_calendars().is_ok()); + assert!(custom.can_create_calendar().is_ok()); + assert!(custom.can_create_calendar().is_err()); + assert!(custom.can_create_calendar().is_err()); + assert!(custom.can_create_calendar().is_err()); + assert!(custom.can_create_calendar().is_ok()); + assert!(custom.can_create_calendar().is_ok()); + } +} diff --git a/src/provider.rs b/src/provider.rs index dac2ecc..de8af41 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -10,6 +10,37 @@ use crate::traits::CompleteCalendar; use crate::item::SyncStatus; use crate::calendar::CalendarId; +/// A counter of errors that happen during a sync +struct SyncResult { + n_errors: u32, +} +impl SyncResult { + pub fn new() -> Self { + Self { n_errors: 0 } + } + pub fn is_success(&self) -> bool { + self.n_errors == 0 + } + + pub fn error(&mut self, text: &str) { + log::error!("{}", text); + self.n_errors += 1; + } + pub fn warn(&mut self, text: &str) { + log::warn!("{}", text); + self.n_errors += 1; + } + pub fn info(&mut self, text: &str) { + log::info!("{}", text); + } + pub fn debug(&mut self, text: &str) { + log::debug!("{}", text); + } + pub fn trace(&mut self, text: &str) { + log::trace!("{}", text); + } +} + /// A data source that combines two `CalDavSource`s (usually a server and a local cache), which is able to sync both sources. /// This can be used for integration tests, where the remote source is mocked by a `Cache`. pub struct Provider @@ -54,8 +85,19 @@ where /// /// This bidirectional sync applies additions/deletions made on a source to the other source. /// In case of conflicts (the same item has been modified on both ends since the last sync, `remote` always wins) - pub async fn sync(&mut self) -> Result<(), Box> { - log::info!("Starting a sync."); + /// + /// It returns whether the sync was totally successful (details about errors are logged using the `log::*` macros). + /// In case errors happened, the sync might have been partially executed, and you can safely run this function again, since it has been designed to gracefully recover from errors. + pub async fn sync(&mut self) -> bool { + let mut result = SyncResult::new(); + if let Err(err) = self.run_sync(&mut result).await { + result.error(&format!("Sync terminated because of an error: {}", err)); + } + result.is_success() + } + + async fn run_sync(&mut self, result: &mut SyncResult) -> Result<(), Box> { + result.info("Starting a sync."); let mut handled_calendars = HashSet::new(); @@ -64,14 +106,14 @@ where for (cal_id, cal_remote) in cals_remote { let counterpart = match self.get_or_insert_local_counterpart_calendar(&cal_id, cal_remote.clone()).await { Err(err) => { - log::warn!("Unable to get or insert local counterpart calendar for {} ({}). Skipping this time", cal_id, err); + result.warn(&format!("Unable to get or insert local counterpart calendar for {} ({}). Skipping this time", cal_id, err)); continue; }, Ok(arc) => arc, }; - if let Err(err) = Self::sync_calendar_pair(counterpart, cal_remote).await { - log::warn!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err); + if let Err(err) = Self::sync_calendar_pair(counterpart, cal_remote, result).await { + result.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err)); continue; } handled_calendars.insert(cal_id); @@ -86,14 +128,14 @@ where let counterpart = match self.get_or_insert_remote_counterpart_calendar(&cal_id, cal_local.clone()).await { Err(err) => { - log::warn!("Unable to get or insert remote counterpart calendar for {} ({}). Skipping this time", cal_id, err); + result.warn(&format!("Unable to get or insert remote counterpart calendar for {} ({}). Skipping this time", cal_id, err)); continue; }, Ok(arc) => arc, }; - if let Err(err) = Self::sync_calendar_pair(cal_local, counterpart).await { - log::warn!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err); + if let Err(err) = Self::sync_calendar_pair(cal_local, counterpart, result).await { + result.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err)); continue; } } @@ -110,12 +152,12 @@ where } - async fn sync_calendar_pair(cal_local: Arc>, cal_remote: Arc>) -> Result<(), Box> { + async fn sync_calendar_pair(cal_local: Arc>, cal_remote: Arc>, result: &mut SyncResult) -> Result<(), Box> { let mut cal_remote = cal_remote.lock().unwrap(); let mut cal_local = cal_local.lock().unwrap(); // Step 1 - find the differences - log::debug!("Finding the differences to sync..."); + result.debug("Finding the differences to sync..."); let mut local_del = HashSet::new(); let mut remote_del = HashSet::new(); let mut local_changes = HashSet::new(); @@ -126,49 +168,49 @@ where let remote_items = cal_remote.get_item_version_tags().await?; let mut local_items_to_handle = cal_local.get_item_ids().await?; for (id, remote_tag) in remote_items { - log::trace!("***** Considering remote item {}...", id); + result.trace(&format!("***** Considering remote item {}...", id)); match cal_local.get_item_by_id_ref(&id).await { None => { // This was created on the remote - log::debug!("* {} is a remote addition", id); + result.debug(&format!("* {} is a remote addition", id)); remote_additions.insert(id); }, Some(local_item) => { if local_items_to_handle.remove(&id) == false { - log::error!("Inconsistent state: missing task {} from the local tasks", id); + result.error(&format!("Inconsistent state: missing task {} from the local tasks", id)); } match local_item.sync_status() { SyncStatus::NotSynced => { - log::error!("ID reuse between remote and local sources ({}). Ignoring this item in the sync", id); + result.error(&format!("ID reuse between remote and local sources ({}). Ignoring this item in the sync", id)); continue; }, SyncStatus::Synced(local_tag) => { if &remote_tag != local_tag { // This has been modified on the remote - log::debug!("* {} is a remote change", id); + result.debug(&format!("* {} is a remote change", id)); remote_changes.insert(id); } }, SyncStatus::LocallyModified(local_tag) => { if &remote_tag == local_tag { // This has been changed locally - log::debug!("* {} is a local change", id); + result.debug(&format!("* {} is a local change", id)); local_changes.insert(id); } else { - log::info!("Conflict: task {} has been modified in both sources. Using the remote version.", id); - log::debug!("* {} is considered a remote change", id); + result.info(&format!("Conflict: task {} has been modified in both sources. Using the remote version.", id)); + result.debug(&format!("* {} is considered a remote change", id)); remote_changes.insert(id); } }, SyncStatus::LocallyDeleted(local_tag) => { if &remote_tag == local_tag { // This has been locally deleted - log::debug!("* {} is a local deletion", id); + result.debug(&format!("* {} is a local deletion", id)); local_del.insert(id); } else { - log::info!("Conflict: task {} has been locally deleted and remotely modified. Reverting to the remote version.", id); - log::debug!("* {} is a considered a remote change", id); + result.info(&format!("Conflict: task {} has been locally deleted and remotely modified. Reverting to the remote version.", id)); + result.debug(&format!("* {} is a considered a remote change", id)); remote_changes.insert(id); } }, @@ -179,10 +221,10 @@ where // Also iterate on the local tasks that are not on the remote for id in local_items_to_handle { - log::trace!("##### Considering local item {}...", id); + result.trace(&format!("##### Considering local item {}...", id)); let local_item = match cal_local.get_item_by_id_ref(&id).await { None => { - log::error!("Inconsistent state: missing task {} from the local tasks", id); + result.error(&format!("Inconsistent state: missing task {} from the local tasks", id)); continue; }, Some(item) => item, @@ -191,21 +233,21 @@ where match local_item.sync_status() { SyncStatus::Synced(_) => { // This item has been removed from the remote - log::debug!("# {} is a deletion from the server", id); + result.debug(&format!("# {} is a deletion from the server", id)); remote_del.insert(id); }, SyncStatus::NotSynced => { // This item has just been locally created - log::debug!("# {} has been locally created", id); + result.debug(&format!("# {} has been locally created", id)); local_additions.insert(id); }, SyncStatus::LocallyDeleted(_) => { // This item has been deleted from both sources - log::debug!("# {} has been deleted from both sources", id); + result.debug(&format!("# {} has been deleted from both sources", id)); remote_del.insert(id); }, SyncStatus::LocallyModified(_) => { - log::info!("Conflict: item {} has been deleted from the server and locally modified. Deleting the local copy", id); + result.info(&format!("Conflict: item {} has been deleted from the server and locally modified. Deleting the local copy", id)); remote_del.insert(id); }, } @@ -213,44 +255,44 @@ where // Step 2 - commit changes - log::trace!("Committing changes..."); + result.trace("Committing changes..."); for id_del in local_del { - log::debug!("> Pushing local deletion {} to the server", id_del); + result.debug(&format!("> Pushing local deletion {} to the server", id_del)); match cal_remote.delete_item(&id_del).await { Err(err) => { - log::warn!("Unable to delete remote item {}: {}", id_del, err); + result.warn(&format!("Unable to delete remote item {}: {}", id_del, err)); }, Ok(()) => { // Change the local copy from "marked to deletion" to "actually deleted" if let Err(err) = cal_local.immediately_delete_item(&id_del).await { - log::error!("Unable to permanently delete local item {}: {}", id_del, err); + result.error(&format!("Unable to permanently delete local item {}: {}", id_del, err)); } }, } } for id_del in remote_del { - log::debug!("> Applying remote deletion {} locally", id_del); + result.debug(&format!("> Applying remote deletion {} locally", id_del)); if let Err(err) = cal_local.immediately_delete_item(&id_del).await { - log::warn!("Unable to delete local item {}: {}", id_del, err); + result.warn(&format!("Unable to delete local item {}: {}", id_del, err)); } } for id_add in remote_additions { - log::debug!("> Applying remote addition {} locally", id_add); + result.debug(&format!("> Applying remote addition {} locally", id_add)); match cal_remote.get_item_by_id(&id_add).await { Err(err) => { - log::warn!("Unable to get remote item {}: {}. Skipping it.", id_add, err); + result.warn(&format!("Unable to get remote item {}: {}. Skipping it.", id_add, err)); continue; }, Ok(item) => match item { None => { - log::error!("Inconsistency: new item {} has vanished from the remote end", id_add); + result.error(&format!("Inconsistency: new item {} has vanished from the remote end", id_add)); continue; }, Some(new_item) => { if let Err(err) = cal_local.add_item(new_item.clone()).await { - log::error!("Not able to add item {} to local calendar: {}", id_add, err); + result.error(&format!("Not able to add item {} to local calendar: {}", id_add, err)); } }, }, @@ -258,15 +300,15 @@ where } for id_change in remote_changes { - log::debug!("> Applying remote change {} locally", id_change); + result.debug(&format!("> Applying remote change {} locally", id_change)); match cal_remote.get_item_by_id(&id_change).await { Err(err) => { - log::warn!("Unable to get remote item {}: {}. Skipping it", id_change, err); + result.warn(&format!("Unable to get remote item {}: {}. Skipping it", id_change, err)); continue; }, Ok(item) => match item { None => { - log::error!("Inconsistency: modified item {} has vanished from the remote end", id_change); + result.error(&format!("Inconsistency: modified item {} has vanished from the remote end", id_change)); continue; }, Some(item) => { @@ -277,10 +319,10 @@ where // TODO: implement update_item (maybe only create_item also updates it?) // if let Err(err) = cal_local.immediately_delete_item(&id_change).await { - log::error!("Unable to delete item {} from local calendar: {}", id_change, err); + result.error(&format!("Unable to delete item {} from local calendar: {}", id_change, err)); } if let Err(err) = cal_local.add_item(item.clone()).await { - log::error!("Unable to add item {} to local calendar: {}", id_change, err); + result.error(&format!("Unable to add item {} to local calendar: {}", id_change, err)); } }, } @@ -289,15 +331,15 @@ where for id_add in local_additions { - log::debug!("> Pushing local addition {} to the server", id_add); + result.debug(&format!("> Pushing local addition {} to the server", id_add)); match cal_local.get_item_by_id_mut(&id_add).await { None => { - log::error!("Inconsistency: created item {} has been marked for upload but is locally missing", id_add); + result.error(&format!("Inconsistency: created item {} has been marked for upload but is locally missing", id_add)); continue; }, Some(item) => { match cal_remote.add_item(item.clone()).await { - Err(err) => log::error!("Unable to add item {} to remote calendar: {}", id_add, err), + Err(err) => result.error(&format!("Unable to add item {} to remote calendar: {}", id_add, err)), Ok(new_ss) => { // Update local sync status item.set_sync_status(new_ss); @@ -308,10 +350,10 @@ where } for id_change in local_changes { - log::debug!("> Pushing local change {} to the server", id_change); + result.debug(&format!("> Pushing local change {} to the server", id_change)); match cal_local.get_item_by_id_mut(&id_change).await { None => { - log::error!("Inconsistency: modified item {} has been marked for upload but is locally missing", id_change); + result.error(&format!("Inconsistency: modified item {} has been marked for upload but is locally missing", id_change)); continue; }, Some(item) => { @@ -322,7 +364,7 @@ where // TODO: implement update_item (maybe only create_item also updates it?) // if let Err(err) = cal_remote.delete_item(&id_change).await { - log::error!("Unable to delete item {} from remote calendar: {}", id_change, err); + result.error(&format!("Unable to delete item {} from remote calendar: {}", id_change, err)); } match cal_remote.add_item(item.clone()).await { Err(err) => log::error!("Unable to add item {} to remote calendar: {}", id_change, err), diff --git a/tests/scenarii.rs b/tests/scenarii.rs index f0a5d2e..b08fead 100644 --- a/tests/scenarii.rs +++ b/tests/scenarii.rs @@ -25,6 +25,7 @@ use my_tasks::SyncStatus; use my_tasks::Task; use my_tasks::calendar::cached_calendar::CachedCalendar; use my_tasks::Provider; +use my_tasks::mock_behaviour::MockBehaviour; pub enum LocatedState { /// Item does not exist yet or does not exist anymore @@ -575,21 +576,21 @@ pub fn scenarii_transient_task() -> Vec { /// Build a `Provider` that contains the data (defined in the given scenarii) before sync -pub async fn populate_test_provider_before_sync(scenarii: &[ItemScenario]) -> Provider { - let mut provider = populate_test_provider(scenarii, false).await; +pub async fn populate_test_provider_before_sync(scenarii: &[ItemScenario], mock_behaviour: Arc>) -> Provider { + let mut provider = populate_test_provider(scenarii, mock_behaviour, false).await; apply_changes_on_provider(&mut provider, scenarii).await; provider } /// Build a `Provider` that contains the data (defined in the given scenarii) after sync -pub async fn populate_test_provider_after_sync(scenarii: &[ItemScenario]) -> Provider { - populate_test_provider(scenarii, true).await +pub async fn populate_test_provider_after_sync(scenarii: &[ItemScenario], mock_behaviour: Arc>) -> Provider { + populate_test_provider(scenarii, mock_behaviour, true).await } -async fn populate_test_provider(scenarii: &[ItemScenario], populate_for_final_state: bool) -> Provider { +async fn populate_test_provider(scenarii: &[ItemScenario], mock_behaviour: Arc>, populate_for_final_state: bool) -> Provider { let mut local = Cache::new(&PathBuf::from(String::from("test_cache_local/"))); let mut remote = Cache::new(&PathBuf::from(String::from("test_cache_remote/"))); - remote.set_is_mocking_remote_source(); + remote.set_mock_behaviour(Some(mock_behaviour)); // Create the initial state, as if we synced both sources in a given state for item in scenarii { diff --git a/tests/sync.rs b/tests/sync.rs index d3ae8be..954a8f7 100644 --- a/tests/sync.rs +++ b/tests/sync.rs @@ -1,5 +1,10 @@ mod scenarii; +#[cfg(feature = "local_calendar_mocks_remote_calendars")] +use std::sync::{Arc, Mutex}; + +#[cfg(feature = "local_calendar_mocks_remote_calendars")] +use my_tasks::mock_behaviour::MockBehaviour; @@ -8,6 +13,8 @@ mod scenarii; struct TestFlavour { #[cfg(feature = "local_calendar_mocks_remote_calendars")] scenarii: Vec, + #[cfg(feature = "local_calendar_mocks_remote_calendars")] + mock_behaviour: Arc>, } #[cfg(not(feature = "local_calendar_mocks_remote_calendars"))] @@ -16,8 +23,9 @@ impl TestFlavour { pub fn first_sync_to_local() -> Self { Self{} } pub fn first_sync_to_server() -> Self { Self{} } pub fn transient_task() -> Self { Self{} } + pub fn normal_with_errors() -> Self { Self{} } - pub async fn run(&self) { + pub async fn run(&self, _max_attempts: u32) { println!("WARNING: This test required the \"integration_tests\" Cargo feature"); } } @@ -27,34 +35,54 @@ impl TestFlavour { pub fn normal() -> Self { Self { scenarii: scenarii::scenarii_basic(), + mock_behaviour: Arc::new(Mutex::new(MockBehaviour::new())), } } pub fn first_sync_to_local() -> Self { Self { scenarii: scenarii::scenarii_first_sync_to_local(), + mock_behaviour: Arc::new(Mutex::new(MockBehaviour::new())), } } pub fn first_sync_to_server() -> Self { Self { scenarii: scenarii::scenarii_first_sync_to_server(), + mock_behaviour: Arc::new(Mutex::new(MockBehaviour::new())), } } pub fn transient_task() -> Self { Self { scenarii: scenarii::scenarii_transient_task(), + mock_behaviour: Arc::new(Mutex::new(MockBehaviour::new())), } } - pub async fn run(&self) { - let mut provider = scenarii::populate_test_provider_before_sync(&self.scenarii).await; + pub fn normal_with_errors() -> Self { + Self { + scenarii: scenarii::scenarii_basic(), + mock_behaviour: Arc::new(Mutex::new(MockBehaviour::fail_now(10))), + } + } + + pub async fn run(&self, max_attempts: u32) { + self.mock_behaviour.lock().unwrap().suspend(); + + let mut provider = scenarii::populate_test_provider_before_sync(&self.scenarii, Arc::clone(&self.mock_behaviour)).await; print_provider(&provider, "before sync").await; - println!("\nsyncing...\n"); - provider.sync().await.unwrap(); + self.mock_behaviour.lock().unwrap().resume(); + for attempt in 0..max_attempts { + println!("\nSyncing...\n"); + if provider.sync().await == true { + println!("Sync complete after {} attempts (multiple attempts are due to forced errors in mocked behaviour)", attempt+1); + break + } + } + self.mock_behaviour.lock().unwrap().suspend(); print_provider(&provider, "after sync").await; @@ -62,7 +90,7 @@ impl TestFlavour { assert!(provider.remote().has_same_observable_content_as(provider.local()).await.unwrap()); // But also explicitely check that every item is expected - let expected_provider = scenarii::populate_test_provider_after_sync(&self.scenarii).await; + let expected_provider = scenarii::populate_test_provider_after_sync(&self.scenarii, Arc::clone(&self.mock_behaviour)).await; println!("\n"); print_provider(&expected_provider, "expected after sync").await; @@ -71,7 +99,7 @@ impl TestFlavour { // Perform a second sync, even if no change has happened, just to check println!("Syncing again"); - provider.sync().await.unwrap(); + provider.sync().await; assert!(provider.local() .has_same_observable_content_as(expected_provider.local() ).await.unwrap()); assert!(provider.remote().has_same_observable_content_as(expected_provider.remote()).await.unwrap()); } @@ -85,7 +113,7 @@ async fn test_regular_sync() { let _ = env_logger::builder().is_test(true).try_init(); let flavour = TestFlavour::normal(); - flavour.run().await; + flavour.run(1).await; } #[tokio::test] @@ -93,7 +121,7 @@ async fn test_sync_empty_initial_local() { let _ = env_logger::builder().is_test(true).try_init(); let flavour = TestFlavour::first_sync_to_local(); - flavour.run().await; + flavour.run(1).await; } #[tokio::test] @@ -101,7 +129,7 @@ async fn test_sync_empty_initial_server() { let _ = env_logger::builder().is_test(true).try_init(); let flavour = TestFlavour::first_sync_to_server(); - flavour.run().await; + flavour.run(1).await; } #[tokio::test] @@ -109,9 +137,16 @@ async fn test_sync_transient_task() { let _ = env_logger::builder().is_test(true).try_init(); let flavour = TestFlavour::transient_task(); - flavour.run().await; + flavour.run(1).await; } +#[tokio::test] +async fn test_errors_in_regular_sync() { + let _ = env_logger::builder().is_test(true).try_init(); + + let flavour = TestFlavour::normal_with_errors(); + flavour.run(100).await; +} #[cfg(feature = "integration_tests")] use my_tasks::{traits::CalDavSource,