Ability to mock behaviours for tests

This commit is contained in:
daladim 2021-04-13 23:32:07 +02:00
parent 081fc2cbc8
commit b2704bd3d2
7 changed files with 340 additions and 80 deletions

View file

@ -4,8 +4,6 @@ use std::path::PathBuf;
use std::path::Path; use std::path::Path;
use std::error::Error; use std::error::Error;
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::HashSet;
use std::hash::Hash;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::ffi::OsStr; use std::ffi::OsStr;
@ -19,6 +17,9 @@ use crate::calendar::cached_calendar::CachedCalendar;
use crate::calendar::CalendarId; use crate::calendar::CalendarId;
use crate::calendar::SupportedComponents; use crate::calendar::SupportedComponents;
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
use crate::mock_behaviour::MockBehaviour;
const MAIN_FILE: &str = "data.json"; const MAIN_FILE: &str = "data.json";
/// A CalDAV source that stores its item in a local folder /// A CalDAV source that stores its item in a local folder
@ -27,8 +28,9 @@ pub struct Cache {
backing_folder: PathBuf, backing_folder: PathBuf,
data: CachedData, data: CachedData,
/// In tests, we may add forced errors to this object
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
is_mocking_remote_source: bool, mock_behaviour: Option<Arc<Mutex<MockBehaviour>>>,
} }
#[derive(Default, Debug, Serialize, Deserialize)] #[derive(Default, Debug, Serialize, Deserialize)]
@ -40,8 +42,8 @@ struct CachedData {
impl Cache { impl Cache {
/// Activate the "mocking remote source" features (i.e. tell its children calendars that they are mocked remote calendars) /// 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")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
pub fn set_is_mocking_remote_source(&mut self) { pub fn set_mock_behaviour(&mut self, mock_behaviour: Option<Arc<Mutex<MockBehaviour>>>) {
self.is_mocking_remote_source = true; self.mock_behaviour = mock_behaviour;
} }
@ -91,7 +93,7 @@ impl Cache {
data, data,
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
is_mocking_remote_source: false, mock_behaviour: None,
}) })
} }
@ -107,7 +109,7 @@ impl Cache {
data: CachedData::default(), data: CachedData::default(),
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
is_mocking_remote_source: false, mock_behaviour: None,
} }
} }
@ -169,6 +171,9 @@ impl Cache {
#[async_trait] #[async_trait]
impl CalDavSource<CachedCalendar> for Cache { impl CalDavSource<CachedCalendar> for Cache {
async fn get_calendars(&self) -> Result<HashMap<CalendarId, Arc<Mutex<CachedCalendar>>>, Box<dyn Error>> { async fn get_calendars(&self) -> Result<HashMap<CalendarId, Arc<Mutex<CachedCalendar>>>, Box<dyn Error>> {
#[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() Ok(self.data.calendars.iter()
.map(|(id, cal)| (id.clone(), cal.clone())) .map(|(id, cal)| (id.clone(), cal.clone()))
.collect() .collect()
@ -181,13 +186,16 @@ impl CalDavSource<CachedCalendar> for Cache {
async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents) -> Result<Arc<Mutex<CachedCalendar>>, Box<dyn Error>> { async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents) -> Result<Arc<Mutex<CachedCalendar>>, Box<dyn Error>> {
log::debug!("Inserting local calendar {}", id); 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 new_calendar = CachedCalendar::new(name, id.clone(), supported_components);
let arc = Arc::new(Mutex::new(new_calendar)); let arc = Arc::new(Mutex::new(new_calendar));
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
if self.is_mocking_remote_source { if let Some(behaviour) = &self.mock_behaviour {
arc.lock().unwrap().set_is_mocking_remote_calendar(); arc.lock().unwrap().set_mock_behaviour(Some(Arc::clone(behaviour)));
} };
match self.data.calendars.insert(id, arc.clone()) { match self.data.calendars.insert(id, arc.clone()) {
Some(_) => Err("Attempt to insert calendar failed: there is alredy such a calendar.".into()), Some(_) => Err("Attempt to insert calendar failed: there is alredy such a calendar.".into()),

View file

@ -10,6 +10,11 @@ use crate::calendar::{CalendarId, SupportedComponents};
use crate::Item; use crate::Item;
use crate::item::ItemId; 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 /// A calendar used by the [`cache`](crate::cache) module
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
@ -18,7 +23,8 @@ pub struct CachedCalendar {
id: CalendarId, id: CalendarId,
supported_components: SupportedComponents, supported_components: SupportedComponents,
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
is_mocking_remote_calendar: bool, #[serde(skip)]
mock_behaviour: Option<Arc<Mutex<MockBehaviour>>>,
items: HashMap<ItemId, Item>, items: HashMap<ItemId, Item>,
} }
@ -26,8 +32,8 @@ pub struct CachedCalendar {
impl CachedCalendar { impl CachedCalendar {
/// Activate the "mocking remote calendar" feature (i.e. ignore sync statuses, since this is what an actual CalDAV sever would do) /// 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")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
pub fn set_is_mocking_remote_calendar(&mut self) { pub fn set_mock_behaviour(&mut self, mock_behaviour: Option<Arc<Mutex<MockBehaviour>>>) {
self.is_mocking_remote_calendar = true; self.mock_behaviour = mock_behaviour;
} }
/// Add an item /// Add an item
@ -107,7 +113,10 @@ impl BaseCalendar for CachedCalendar {
} }
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
async fn add_item(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> { async fn add_item(&mut self, item: Item) -> Result<SyncStatus, Box<dyn Error>> {
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 self.add_item_force_synced(item).await
} else { } else {
self.regular_add_item(item).await self.regular_add_item(item).await
@ -121,7 +130,7 @@ impl CompleteCalendar for CachedCalendar {
Self { Self {
name, id, supported_components, name, id, supported_components,
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
is_mocking_remote_calendar: false, mock_behaviour: None,
items: HashMap::new(), items: HashMap::new(),
} }
} }
@ -200,6 +209,9 @@ impl DavCalendar for CachedCalendar {
} }
async fn get_item_version_tags(&self) -> Result<HashMap<ItemId, VersionTag>, Box<dyn Error>> { async fn get_item_version_tags(&self) -> Result<HashMap<ItemId, VersionTag>, Box<dyn Error>> {
#[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; use crate::item::SyncStatus;
let mut result = HashMap::new(); let mut result = HashMap::new();
@ -218,10 +230,16 @@ impl DavCalendar for CachedCalendar {
} }
async fn get_item_by_id(&self, id: &ItemId) -> Result<Option<Item>, Box<dyn Error>> { async fn get_item_by_id(&self, id: &ItemId) -> Result<Option<Item>, Box<dyn Error>> {
#[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()) Ok(self.items.get(id).cloned())
} }
async fn delete_item(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> { async fn delete_item(&mut self, item_id: &ItemId) -> Result<(), Box<dyn Error>> {
#[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 self.immediately_delete_item(item_id).await
} }
} }

View file

@ -26,6 +26,7 @@ mod event;
pub use event::Event; pub use event::Event;
pub mod provider; pub mod provider;
pub use provider::Provider; pub use provider::Provider;
pub mod mock_behaviour;
pub mod client; pub mod client;
pub use client::Client; pub use client::Client;

155
src/mock_behaviour.rs Normal file
View file

@ -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<dyn Error>> {
if self.is_suspended { return Ok(()) }
decrement(&mut self.get_calendars_behaviour, "get_calendars")
}
// pub fn can_get_calendar(&mut self) -> Result<(), Box<dyn Error>> {
// if self.is_suspended { return Ok(()) }
// decrement(&mut self.get_calendar_behaviour, "get_calendar")
// }
pub fn can_create_calendar(&mut self) -> Result<(), Box<dyn Error>> {
if self.is_suspended { return Ok(()) }
decrement(&mut self.create_calendar_behaviour, "create_calendar")
}
pub fn can_add_item(&mut self) -> Result<(), Box<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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());
}
}

View file

@ -10,6 +10,37 @@ use crate::traits::CompleteCalendar;
use crate::item::SyncStatus; use crate::item::SyncStatus;
use crate::calendar::CalendarId; 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. /// 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`. /// This can be used for integration tests, where the remote source is mocked by a `Cache`.
pub struct Provider<L, T, R, U> pub struct Provider<L, T, R, U>
@ -54,8 +85,19 @@ where
/// ///
/// This bidirectional sync applies additions/deletions made on a source to the other source. /// 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) /// 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<dyn Error>> { ///
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<dyn Error>> {
result.info("Starting a sync.");
let mut handled_calendars = HashSet::new(); let mut handled_calendars = HashSet::new();
@ -64,14 +106,14 @@ where
for (cal_id, cal_remote) in cals_remote { for (cal_id, cal_remote) in cals_remote {
let counterpart = match self.get_or_insert_local_counterpart_calendar(&cal_id, cal_remote.clone()).await { let counterpart = match self.get_or_insert_local_counterpart_calendar(&cal_id, cal_remote.clone()).await {
Err(err) => { 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; continue;
}, },
Ok(arc) => arc, Ok(arc) => arc,
}; };
if let Err(err) = Self::sync_calendar_pair(counterpart, cal_remote).await { if let Err(err) = Self::sync_calendar_pair(counterpart, cal_remote, result).await {
log::warn!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err); result.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err));
continue; continue;
} }
handled_calendars.insert(cal_id); 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 { let counterpart = match self.get_or_insert_remote_counterpart_calendar(&cal_id, cal_local.clone()).await {
Err(err) => { 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; continue;
}, },
Ok(arc) => arc, Ok(arc) => arc,
}; };
if let Err(err) = Self::sync_calendar_pair(cal_local, counterpart).await { if let Err(err) = Self::sync_calendar_pair(cal_local, counterpart, result).await {
log::warn!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err); result.warn(&format!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err));
continue; continue;
} }
} }
@ -110,12 +152,12 @@ where
} }
async fn sync_calendar_pair(cal_local: Arc<Mutex<T>>, cal_remote: Arc<Mutex<U>>) -> Result<(), Box<dyn Error>> { async fn sync_calendar_pair(cal_local: Arc<Mutex<T>>, cal_remote: Arc<Mutex<U>>, result: &mut SyncResult) -> Result<(), Box<dyn Error>> {
let mut cal_remote = cal_remote.lock().unwrap(); let mut cal_remote = cal_remote.lock().unwrap();
let mut cal_local = cal_local.lock().unwrap(); let mut cal_local = cal_local.lock().unwrap();
// Step 1 - find the differences // 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 local_del = HashSet::new();
let mut remote_del = HashSet::new(); let mut remote_del = HashSet::new();
let mut local_changes = HashSet::new(); let mut local_changes = HashSet::new();
@ -126,49 +168,49 @@ where
let remote_items = cal_remote.get_item_version_tags().await?; let remote_items = cal_remote.get_item_version_tags().await?;
let mut local_items_to_handle = cal_local.get_item_ids().await?; let mut local_items_to_handle = cal_local.get_item_ids().await?;
for (id, remote_tag) in remote_items { 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 { match cal_local.get_item_by_id_ref(&id).await {
None => { None => {
// This was created on the remote // 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); remote_additions.insert(id);
}, },
Some(local_item) => { Some(local_item) => {
if local_items_to_handle.remove(&id) == false { 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() { match local_item.sync_status() {
SyncStatus::NotSynced => { 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; continue;
}, },
SyncStatus::Synced(local_tag) => { SyncStatus::Synced(local_tag) => {
if &remote_tag != local_tag { if &remote_tag != local_tag {
// This has been modified on the remote // 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); remote_changes.insert(id);
} }
}, },
SyncStatus::LocallyModified(local_tag) => { SyncStatus::LocallyModified(local_tag) => {
if &remote_tag == local_tag { if &remote_tag == local_tag {
// This has been changed locally // This has been changed locally
log::debug!("* {} is a local change", id); result.debug(&format!("* {} is a local change", id));
local_changes.insert(id); local_changes.insert(id);
} else { } else {
log::info!("Conflict: task {} has been modified in both sources. Using the remote version.", id); result.info(&format!("Conflict: task {} has been modified in both sources. Using the remote version.", id));
log::debug!("* {} is considered a remote change", id); result.debug(&format!("* {} is considered a remote change", id));
remote_changes.insert(id); remote_changes.insert(id);
} }
}, },
SyncStatus::LocallyDeleted(local_tag) => { SyncStatus::LocallyDeleted(local_tag) => {
if &remote_tag == local_tag { if &remote_tag == local_tag {
// This has been locally deleted // This has been locally deleted
log::debug!("* {} is a local deletion", id); result.debug(&format!("* {} is a local deletion", id));
local_del.insert(id); local_del.insert(id);
} else { } else {
log::info!("Conflict: task {} has been locally deleted and remotely modified. Reverting to the remote version.", id); result.info(&format!("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.debug(&format!("* {} is a considered a remote change", id));
remote_changes.insert(id); remote_changes.insert(id);
} }
}, },
@ -179,10 +221,10 @@ where
// Also iterate on the local tasks that are not on the remote // Also iterate on the local tasks that are not on the remote
for id in local_items_to_handle { 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 { let local_item = match cal_local.get_item_by_id_ref(&id).await {
None => { 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; continue;
}, },
Some(item) => item, Some(item) => item,
@ -191,21 +233,21 @@ where
match local_item.sync_status() { match local_item.sync_status() {
SyncStatus::Synced(_) => { SyncStatus::Synced(_) => {
// This item has been removed from the remote // 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); remote_del.insert(id);
}, },
SyncStatus::NotSynced => { SyncStatus::NotSynced => {
// This item has just been locally created // 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); local_additions.insert(id);
}, },
SyncStatus::LocallyDeleted(_) => { SyncStatus::LocallyDeleted(_) => {
// This item has been deleted from both sources // 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); remote_del.insert(id);
}, },
SyncStatus::LocallyModified(_) => { 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); remote_del.insert(id);
}, },
} }
@ -213,44 +255,44 @@ where
// Step 2 - commit changes // Step 2 - commit changes
log::trace!("Committing changes..."); result.trace("Committing changes...");
for id_del in local_del { 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 { match cal_remote.delete_item(&id_del).await {
Err(err) => { Err(err) => {
log::warn!("Unable to delete remote item {}: {}", id_del, err); result.warn(&format!("Unable to delete remote item {}: {}", id_del, err));
}, },
Ok(()) => { Ok(()) => {
// Change the local copy from "marked to deletion" to "actually deleted" // Change the local copy from "marked to deletion" to "actually deleted"
if let Err(err) = cal_local.immediately_delete_item(&id_del).await { 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 { 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 { 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 { 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 { match cal_remote.get_item_by_id(&id_add).await {
Err(err) => { 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; continue;
}, },
Ok(item) => match item { Ok(item) => match item {
None => { 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; continue;
}, },
Some(new_item) => { Some(new_item) => {
if let Err(err) = cal_local.add_item(new_item.clone()).await { 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 { 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 { match cal_remote.get_item_by_id(&id_change).await {
Err(err) => { 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; continue;
}, },
Ok(item) => match item { Ok(item) => match item {
None => { 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; continue;
}, },
Some(item) => { Some(item) => {
@ -277,10 +319,10 @@ where
// TODO: implement update_item (maybe only create_item also updates it?) // TODO: implement update_item (maybe only create_item also updates it?)
// //
if let Err(err) = cal_local.immediately_delete_item(&id_change).await { 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 { 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 { 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 { match cal_local.get_item_by_id_mut(&id_add).await {
None => { 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; continue;
}, },
Some(item) => { Some(item) => {
match cal_remote.add_item(item.clone()).await { 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) => { Ok(new_ss) => {
// Update local sync status // Update local sync status
item.set_sync_status(new_ss); item.set_sync_status(new_ss);
@ -308,10 +350,10 @@ where
} }
for id_change in local_changes { 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 { match cal_local.get_item_by_id_mut(&id_change).await {
None => { 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; continue;
}, },
Some(item) => { Some(item) => {
@ -322,7 +364,7 @@ where
// TODO: implement update_item (maybe only create_item also updates it?) // TODO: implement update_item (maybe only create_item also updates it?)
// //
if let Err(err) = cal_remote.delete_item(&id_change).await { 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 { match cal_remote.add_item(item.clone()).await {
Err(err) => log::error!("Unable to add item {} to remote calendar: {}", id_change, err), Err(err) => log::error!("Unable to add item {} to remote calendar: {}", id_change, err),

View file

@ -25,6 +25,7 @@ use my_tasks::SyncStatus;
use my_tasks::Task; use my_tasks::Task;
use my_tasks::calendar::cached_calendar::CachedCalendar; use my_tasks::calendar::cached_calendar::CachedCalendar;
use my_tasks::Provider; use my_tasks::Provider;
use my_tasks::mock_behaviour::MockBehaviour;
pub enum LocatedState { pub enum LocatedState {
/// Item does not exist yet or does not exist anymore /// Item does not exist yet or does not exist anymore
@ -575,21 +576,21 @@ pub fn scenarii_transient_task() -> Vec<ItemScenario> {
/// Build a `Provider` that contains the data (defined in the given scenarii) before sync /// 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<Cache, CachedCalendar, Cache, CachedCalendar> { pub async fn populate_test_provider_before_sync(scenarii: &[ItemScenario], mock_behaviour: Arc<Mutex<MockBehaviour>>) -> Provider<Cache, CachedCalendar, Cache, CachedCalendar> {
let mut provider = populate_test_provider(scenarii, false).await; let mut provider = populate_test_provider(scenarii, mock_behaviour, false).await;
apply_changes_on_provider(&mut provider, scenarii).await; apply_changes_on_provider(&mut provider, scenarii).await;
provider provider
} }
/// Build a `Provider` that contains the data (defined in the given scenarii) after sync /// 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<Cache, CachedCalendar, Cache, CachedCalendar> { pub async fn populate_test_provider_after_sync(scenarii: &[ItemScenario], mock_behaviour: Arc<Mutex<MockBehaviour>>) -> Provider<Cache, CachedCalendar, Cache, CachedCalendar> {
populate_test_provider(scenarii, true).await populate_test_provider(scenarii, mock_behaviour, true).await
} }
async fn populate_test_provider(scenarii: &[ItemScenario], populate_for_final_state: bool) -> Provider<Cache, CachedCalendar, Cache, CachedCalendar> { async fn populate_test_provider(scenarii: &[ItemScenario], mock_behaviour: Arc<Mutex<MockBehaviour>>, populate_for_final_state: bool) -> Provider<Cache, CachedCalendar, Cache, CachedCalendar> {
let mut local = Cache::new(&PathBuf::from(String::from("test_cache_local/"))); let mut local = Cache::new(&PathBuf::from(String::from("test_cache_local/")));
let mut remote = Cache::new(&PathBuf::from(String::from("test_cache_remote/"))); 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 // Create the initial state, as if we synced both sources in a given state
for item in scenarii { for item in scenarii {

View file

@ -1,5 +1,10 @@
mod scenarii; 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 { struct TestFlavour {
#[cfg(feature = "local_calendar_mocks_remote_calendars")] #[cfg(feature = "local_calendar_mocks_remote_calendars")]
scenarii: Vec<scenarii::ItemScenario>, scenarii: Vec<scenarii::ItemScenario>,
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
mock_behaviour: Arc<Mutex<MockBehaviour>>,
} }
#[cfg(not(feature = "local_calendar_mocks_remote_calendars"))] #[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_local() -> Self { Self{} }
pub fn first_sync_to_server() -> Self { Self{} } pub fn first_sync_to_server() -> Self { Self{} }
pub fn transient_task() -> 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"); println!("WARNING: This test required the \"integration_tests\" Cargo feature");
} }
} }
@ -27,34 +35,54 @@ impl TestFlavour {
pub fn normal() -> Self { pub fn normal() -> Self {
Self { Self {
scenarii: scenarii::scenarii_basic(), scenarii: scenarii::scenarii_basic(),
mock_behaviour: Arc::new(Mutex::new(MockBehaviour::new())),
} }
} }
pub fn first_sync_to_local() -> Self { pub fn first_sync_to_local() -> Self {
Self { Self {
scenarii: scenarii::scenarii_first_sync_to_local(), scenarii: scenarii::scenarii_first_sync_to_local(),
mock_behaviour: Arc::new(Mutex::new(MockBehaviour::new())),
} }
} }
pub fn first_sync_to_server() -> Self { pub fn first_sync_to_server() -> Self {
Self { Self {
scenarii: scenarii::scenarii_first_sync_to_server(), scenarii: scenarii::scenarii_first_sync_to_server(),
mock_behaviour: Arc::new(Mutex::new(MockBehaviour::new())),
} }
} }
pub fn transient_task() -> Self { pub fn transient_task() -> Self {
Self { Self {
scenarii: scenarii::scenarii_transient_task(), scenarii: scenarii::scenarii_transient_task(),
mock_behaviour: Arc::new(Mutex::new(MockBehaviour::new())),
} }
} }
pub async fn run(&self) { pub fn normal_with_errors() -> Self {
let mut provider = scenarii::populate_test_provider_before_sync(&self.scenarii).await; 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; print_provider(&provider, "before sync").await;
println!("\nsyncing...\n"); self.mock_behaviour.lock().unwrap().resume();
provider.sync().await.unwrap(); 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; print_provider(&provider, "after sync").await;
@ -62,7 +90,7 @@ impl TestFlavour {
assert!(provider.remote().has_same_observable_content_as(provider.local()).await.unwrap()); assert!(provider.remote().has_same_observable_content_as(provider.local()).await.unwrap());
// But also explicitely check that every item is expected // 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"); println!("\n");
print_provider(&expected_provider, "expected after sync").await; 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 // Perform a second sync, even if no change has happened, just to check
println!("Syncing again"); 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.local() .has_same_observable_content_as(expected_provider.local() ).await.unwrap());
assert!(provider.remote().has_same_observable_content_as(expected_provider.remote()).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 _ = env_logger::builder().is_test(true).try_init();
let flavour = TestFlavour::normal(); let flavour = TestFlavour::normal();
flavour.run().await; flavour.run(1).await;
} }
#[tokio::test] #[tokio::test]
@ -93,7 +121,7 @@ async fn test_sync_empty_initial_local() {
let _ = env_logger::builder().is_test(true).try_init(); let _ = env_logger::builder().is_test(true).try_init();
let flavour = TestFlavour::first_sync_to_local(); let flavour = TestFlavour::first_sync_to_local();
flavour.run().await; flavour.run(1).await;
} }
#[tokio::test] #[tokio::test]
@ -101,7 +129,7 @@ async fn test_sync_empty_initial_server() {
let _ = env_logger::builder().is_test(true).try_init(); let _ = env_logger::builder().is_test(true).try_init();
let flavour = TestFlavour::first_sync_to_server(); let flavour = TestFlavour::first_sync_to_server();
flavour.run().await; flavour.run(1).await;
} }
#[tokio::test] #[tokio::test]
@ -109,9 +137,16 @@ async fn test_sync_transient_task() {
let _ = env_logger::builder().is_test(true).try_init(); let _ = env_logger::builder().is_test(true).try_init();
let flavour = TestFlavour::transient_task(); 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")] #[cfg(feature = "integration_tests")]
use my_tasks::{traits::CalDavSource, use my_tasks::{traits::CalDavSource,