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

@ -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<L, T, R, U>
@ -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<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();
@ -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<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_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),