Ability to mock behaviours for tests
This commit is contained in:
parent
081fc2cbc8
commit
b2704bd3d2
7 changed files with 340 additions and 80 deletions
28
src/cache.rs
28
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<Arc<Mutex<MockBehaviour>>>,
|
||||
}
|
||||
|
||||
#[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<Arc<Mutex<MockBehaviour>>>) {
|
||||
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<CachedCalendar> for Cache {
|
||||
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()
|
||||
.map(|(id, cal)| (id.clone(), cal.clone()))
|
||||
.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>> {
|
||||
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()),
|
||||
|
|
|
@ -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<Arc<Mutex<MockBehaviour>>>,
|
||||
|
||||
items: HashMap<ItemId, Item>,
|
||||
}
|
||||
|
@ -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<Arc<Mutex<MockBehaviour>>>) {
|
||||
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<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
|
||||
} 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<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;
|
||||
|
||||
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>> {
|
||||
#[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<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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
155
src/mock_behaviour.rs
Normal file
155
src/mock_behaviour.rs
Normal 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());
|
||||
}
|
||||
}
|
138
src/provider.rs
138
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<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),
|
||||
|
|
|
@ -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<ItemScenario> {
|
|||
|
||||
|
||||
/// 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> {
|
||||
let mut provider = populate_test_provider(scenarii, false).await;
|
||||
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, 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<Cache, CachedCalendar, Cache, CachedCalendar> {
|
||||
populate_test_provider(scenarii, true).await
|
||||
pub async fn populate_test_provider_after_sync(scenarii: &[ItemScenario], mock_behaviour: Arc<Mutex<MockBehaviour>>) -> Provider<Cache, CachedCalendar, Cache, CachedCalendar> {
|
||||
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 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 {
|
||||
|
|
|
@ -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<scenarii::ItemScenario>,
|
||||
#[cfg(feature = "local_calendar_mocks_remote_calendars")]
|
||||
mock_behaviour: Arc<Mutex<MockBehaviour>>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
|
|
Loading…
Add table
Reference in a new issue