diff --git a/src/cache.rs b/src/cache.rs index 94bf697..32e2e30 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -17,6 +17,7 @@ use crate::traits::BaseCalendar; use crate::traits::CompleteCalendar; use crate::calendar::cached_calendar::CachedCalendar; use crate::calendar::CalendarId; +use crate::calendar::SupportedComponents; const MAIN_FILE: &str = "data.json"; @@ -25,6 +26,9 @@ const MAIN_FILE: &str = "data.json"; pub struct Cache { backing_folder: PathBuf, data: CachedData, + + #[cfg(feature = "local_calendar_mocks_remote_calendars")] + is_mocking_remote_source: bool, } #[derive(Default, Debug, Serialize, Deserialize)] @@ -34,6 +38,13 @@ 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; + } + + /// Get the path to the cache folder pub fn cache_folder() -> PathBuf { return PathBuf::from(String::from("~/.config/my-tasks/cache/")) @@ -78,6 +89,9 @@ impl Cache { Ok(Self{ backing_folder: PathBuf::from(folder), data, + + #[cfg(feature = "local_calendar_mocks_remote_calendars")] + is_mocking_remote_source: false, }) } @@ -91,6 +105,9 @@ impl Cache { Self{ backing_folder: PathBuf::from(folder_path), data: CachedData::default(), + + #[cfg(feature = "local_calendar_mocks_remote_calendars")] + is_mocking_remote_source: false, } } @@ -198,13 +215,19 @@ impl CalDavSource for Cache { self.data.calendars.get(id).map(|arc| arc.clone()) } - async fn insert_calendar(&mut self, new_calendar: CachedCalendar) -> Result>, Box> { - let id = new_calendar.id().clone(); + async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents) -> Result>, Box> { log::debug!("Inserting local calendar {}", id); + 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(); + } + match self.data.calendars.insert(id, arc.clone()) { Some(_) => Err("Attempt to insert calendar failed: there is alredy such a calendar.".into()), - None => Ok(arc) , + None => Ok(arc), } } } @@ -224,10 +247,11 @@ mod tests { let mut cache = Cache::new(&cache_path); - let cal1 = CachedCalendar::new("shopping list".to_string(), - Url::parse("https://caldav.com/shopping").unwrap(), - SupportedComponents::TODO); - cache.insert_calendar(cal1).await.unwrap(); + let _ = cache.create_calendar( + Url::parse("https://caldav.com/shopping").unwrap(), + "shopping list".to_string(), + SupportedComponents::TODO, + ).await.unwrap(); cache.save_to_folder().unwrap(); diff --git a/src/calendar/cached_calendar.rs b/src/calendar/cached_calendar.rs index 583a884..6c0d67c 100644 --- a/src/calendar/cached_calendar.rs +++ b/src/calendar/cached_calendar.rs @@ -156,11 +156,16 @@ impl CompleteCalendar for CachedCalendar { #[cfg(feature = "local_calendar_mocks_remote_calendars")] use crate::{item::VersionTag, - traits::DavCalendar}; + traits::DavCalendar, + resource::Resource}; #[cfg(feature = "local_calendar_mocks_remote_calendars")] #[async_trait] impl DavCalendar for CachedCalendar { + fn new(name: String, resource: Resource, supported_components: SupportedComponents) -> Self { + crate::traits::CompleteCalendar::new(name, resource.url().clone(), supported_components) + } + async fn get_item_version_tags(&self) -> Result, Box> { use crate::item::SyncStatus; diff --git a/src/calendar/remote_calendar.rs b/src/calendar/remote_calendar.rs index 720a4a2..a4864f7 100644 --- a/src/calendar/remote_calendar.rs +++ b/src/calendar/remote_calendar.rs @@ -40,15 +40,6 @@ pub struct RemoteCalendar { cached_version_tags: Mutex>>, } -impl RemoteCalendar { - pub fn new(name: String, resource: Resource, supported_components: SupportedComponents) -> Self { - Self { - name, resource, supported_components, - cached_version_tags: Mutex::new(None), - } - } -} - #[async_trait] impl BaseCalendar for RemoteCalendar { fn name(&self) -> &str { &self.name } @@ -65,6 +56,14 @@ impl BaseCalendar for RemoteCalendar { #[async_trait] impl DavCalendar for RemoteCalendar { + fn new(name: String, resource: Resource, supported_components: SupportedComponents) -> Self { + Self { + name, resource, supported_components, + cached_version_tags: Mutex::new(None), + } + } + + async fn get_item_version_tags(&self) -> Result, Box> { if let Some(map) = &*self.cached_version_tags.lock().unwrap() { log::debug!("Version tags are already cached."); diff --git a/src/client.rs b/src/client.rs index 59ca993..8e1a1ce 100644 --- a/src/client.rs +++ b/src/client.rs @@ -14,8 +14,10 @@ use crate::resource::Resource; use crate::utils::{find_elem, find_elems}; use crate::calendar::remote_calendar::RemoteCalendar; use crate::calendar::CalendarId; +use crate::calendar::SupportedComponents; use crate::traits::CalDavSource; use crate::traits::BaseCalendar; +use crate::traits::DavCalendar; static DAVCLIENT_BODY: &str = r#" @@ -222,17 +224,17 @@ impl CalDavSource for Client { }; } - async fn get_calendar(&self, id: &CalendarId) -> Option>> { self.cached_replies.lock().unwrap() .calendars .as_ref() .and_then(|cals| cals.get(id)) .map(|cal| cal.clone()) - } + } - async fn insert_calendar(&mut self, _new_calendar: RemoteCalendar) -> Result>, Box> { + async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents) -> Result>, Box> { todo!(); } + } diff --git a/src/provider.rs b/src/provider.rs index d2698b2..646a2f6 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -9,6 +9,7 @@ use crate::traits::{CalDavSource, DavCalendar}; use crate::traits::CompleteCalendar; use crate::item::SyncStatus; use crate::calendar::SupportedComponents; +use crate::calendar::CalendarId; /// 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`. @@ -57,22 +58,27 @@ where pub async fn sync(&mut self) -> Result<(), Box> { log::info!("Starting a sync."); + let mut handled_calendars = HashSet::new(); + + // Sync every remote calendar let cals_remote = self.remote.get_calendars().await?; for (cal_id, cal_remote) in cals_remote { - let cal_local = loop { - if let Some(cal) = self.local.get_calendar(&cal_id).await { - break cal; - } + let cal_local = self.get_or_insert_local_counterpart_calendar(&cal_id).await; - // This calendar does not exist locally yet, let's add it - log::error!("TODO: what name/SP should we choose?"); - let new_calendar = T::new(String::from("new calendar"), cal_id.clone(), SupportedComponents::TODO); + if let Err(err) = Self::sync_calendar_pair(cal_local, cal_remote).await { + log::warn!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err); + } + handled_calendars.insert(cal_id); + } - if let Err(err) = self.local.insert_calendar(new_calendar).await { - log::warn!("Unable to create local calendar {}: {}. Skipping it.", cal_id, err); - continue; - } - }; + // Sync every local calendar that would not be in the remote yet + let cals_local = self.local.get_calendars().await?; + for (cal_id, cal_local) in cals_local { + if handled_calendars.contains(&cal_id) { + continue; + } + + let cal_remote = self.get_or_insert_remote_counterpart_calendar(&cal_id).await; if let Err(err) = Self::sync_calendar_pair(cal_local, cal_remote).await { log::warn!("Unable to sync calendar {}: {}, skipping this time.", cal_id, err); @@ -83,6 +89,46 @@ where } + async fn get_or_insert_local_counterpart_calendar(&mut self, cal_id: &CalendarId) -> Arc> { + loop { + if let Some(cal) = self.local.get_calendar(&cal_id).await { + break cal; + } + + // This calendar does not exist locally yet, let's add it + log::debug!("Adding a local calendar {}", cal_id); + if let Err(err) = self.local.create_calendar( + cal_id.clone(), + String::from("new calendar"), + SupportedComponents::TODO, + ).await { + log::warn!("Unable to create local calendar {}: {}. Skipping it.", cal_id, err); + continue; + } + } + } + + async fn get_or_insert_remote_counterpart_calendar(&mut self, cal_id: &CalendarId) -> Arc> { + loop { + if let Some(cal) = self.remote.get_calendar(&cal_id).await { + break cal; + } + + // This calendar does not exist in the remote yet, let's add it + log::debug!("Adding a remote calendar {}", cal_id); + if let Err(err) = self.remote.create_calendar( + cal_id.clone(), + String::from("new calendar"), + SupportedComponents::TODO, + ).await { + log::warn!("Unable to create remote calendar {}: {}. Skipping it.", cal_id, err); + continue; + } + } + } + + + async fn sync_calendar_pair(cal_local: Arc>, cal_remote: Arc>) -> Result<(), Box> { let mut cal_remote = cal_remote.lock().unwrap(); let mut cal_local = cal_local.lock().unwrap(); diff --git a/src/traits.rs b/src/traits.rs index 503f819..9def058 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -10,6 +10,7 @@ use crate::item::ItemId; use crate::item::VersionTag; use crate::calendar::CalendarId; use crate::calendar::SupportedComponents; +use crate::resource::Resource; /// This trait must be implemented by data sources (either local caches or remote CalDAV clients) #[async_trait] @@ -19,8 +20,9 @@ pub trait CalDavSource { async fn get_calendars(&self) -> Result>>, Box>; /// Returns the calendar matching the ID async fn get_calendar(&self, id: &CalendarId) -> Option>>; - /// Insert a calendar if it did not exist, and return it - async fn insert_calendar(&mut self, new_calendar: T) -> Result>, Box>; + /// Create a calendar if it did not exist, and return it + async fn create_calendar(&mut self, id: CalendarId, name: String, supported_components: SupportedComponents) + -> Result>, Box>; } /// This trait contains functions that are common to all calendars @@ -55,6 +57,9 @@ pub trait BaseCalendar { /// Functions availabe for calendars that are backed by a CalDAV server #[async_trait] pub trait DavCalendar : BaseCalendar { + /// Create a new calendar + fn new(name: String, resource: Resource, supported_components: SupportedComponents) -> Self; + /// Get the IDs and the version tags of every item in this calendar async fn get_item_version_tags(&self) -> Result, Box>; diff --git a/src/utils.rs b/src/utils.rs index d35cc33..0dd0a5b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -99,19 +99,14 @@ where pub fn print_task(item: &Item) { match item { Item::Task(task) => { - let mut status = String::new(); - if task.completed() { - status += "✓"; - } else { - status += " "; - } - match task.sync_status() { - SyncStatus::NotSynced => { status += " "; }, - SyncStatus::Synced(_) => { status += "="; }, - SyncStatus::LocallyModified(_) => { status += "~"; }, - SyncStatus::LocallyDeleted(_) => { status += "x"; }, - } - println!(" {} {}\t{}", status, task.name(), task.id()); + let completion = if task.completed() { "✓" } else { " " }; + let sync = match task.sync_status() { + SyncStatus::NotSynced => ".", + SyncStatus::Synced(_) => "=", + SyncStatus::LocallyModified(_) => "~", + SyncStatus::LocallyDeleted(_) => "x", + }; + println!(" {}{} {}\t{}", completion, sync, task.name(), task.id()); }, _ => return, } diff --git a/tests/scenarii.rs b/tests/scenarii.rs index 71bde54..0557664 100644 --- a/tests/scenarii.rs +++ b/tests/scenarii.rs @@ -83,6 +83,13 @@ pub fn scenarii_basic() -> Vec { let main_cal = CalendarId::from("https://some.calend.ar/main/".parse().unwrap()); + // + // + // + // + // TODO: add new calendars, with or without taks in them + // + tasks.push( ItemScenario { id: ItemId::random(), @@ -440,6 +447,71 @@ pub fn scenarii_first_sync_to_local() -> Vec { tasks } +/// This scenario basically checks a first sync to an empty server +pub fn scenarii_first_sync_to_server() -> Vec { + let mut tasks = Vec::new(); + + let cal3 = CalendarId::from("https://some.calend.ar/third/".parse().unwrap()); + let cal4 = CalendarId::from("https://some.calend.ar/fourth/".parse().unwrap()); + + tasks.push( + ItemScenario { + id: ItemId::random(), + initial_state: LocatedState::Local( ItemState{ + calendar: cal3.clone(), + name: String::from("Task A3"), + completed: false, + }), + local_changes_to_apply: Vec::new(), + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::BothSynced( ItemState{ + calendar: cal3.clone(), + name: String::from("Task A3"), + completed: false, + }), + } + ); + + tasks.push( + ItemScenario { + id: ItemId::random(), + initial_state: LocatedState::Local( ItemState{ + calendar: cal4.clone(), + name: String::from("Task A4"), + completed: false, + }), + local_changes_to_apply: Vec::new(), + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::BothSynced( ItemState{ + calendar: cal4.clone(), + name: String::from("Task A4"), + completed: false, + }), + } + ); + + tasks.push( + ItemScenario { + id: ItemId::random(), + initial_state: LocatedState::Local( ItemState{ + calendar: cal3.clone(), + name: String::from("Task B3"), + completed: false, + }), + local_changes_to_apply: Vec::new(), + remote_changes_to_apply: Vec::new(), + after_sync: LocatedState::BothSynced( ItemState{ + calendar: cal3.clone(), + name: String::from("Task B3"), + completed: false, + }), + } + ); + + tasks +} + + /// Build a `Provider` that contains the data (defined in the given scenarii) before sync pub async fn populate_test_provider_before_sync(scenarii: &[ItemScenario]) -> Provider { let mut provider = populate_test_provider(scenarii, false).await; @@ -453,8 +525,9 @@ pub async fn populate_test_provider_after_sync(scenarii: &[ItemScenario]) -> Pro } async fn populate_test_provider(scenarii: &[ItemScenario], populate_for_final_state: bool) -> Provider { - let mut remote = Cache::new(&PathBuf::from(String::from("test_cache_remote/"))); 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(); // Create the initial state, as if we synced both sources in a given state for item in scenarii { @@ -483,14 +556,14 @@ async fn populate_test_provider(scenarii: &[ItemScenario], populate_for_final_st match required_state { LocatedState::None => panic!("Should not happen, we've continued already"), LocatedState::Local(s) => { - get_or_insert_calendar(&mut local, &s.calendar, false).await.unwrap().lock().unwrap().add_item(new_item).await.unwrap(); + get_or_insert_calendar(&mut local, &s.calendar).await.unwrap().lock().unwrap().add_item(new_item).await.unwrap(); }, LocatedState::Remote(s) => { - get_or_insert_calendar(&mut remote, &s.calendar, true).await.unwrap().lock().unwrap().add_item(new_item).await.unwrap(); + get_or_insert_calendar(&mut remote, &s.calendar).await.unwrap().lock().unwrap().add_item(new_item).await.unwrap(); }, LocatedState::BothSynced(s) => { - get_or_insert_calendar(&mut local, &s.calendar, false).await.unwrap().lock().unwrap().add_item(new_item.clone()).await.unwrap(); - get_or_insert_calendar(&mut remote, &s.calendar, true).await.unwrap().lock().unwrap().add_item(new_item).await.unwrap(); + get_or_insert_calendar(&mut local, &s.calendar).await.unwrap().lock().unwrap().add_item(new_item.clone()).await.unwrap(); + get_or_insert_calendar(&mut remote, &s.calendar).await.unwrap().lock().unwrap().add_item(new_item).await.unwrap(); }, } } @@ -518,7 +591,7 @@ async fn apply_changes_on_provider(provider: &mut Provider Result>, Box> { match source.get_calendar(id).await { @@ -526,11 +599,12 @@ async fn get_or_insert_calendar(source: &mut Cache, id: &CalendarId, should_mock None => { let new_name = format!("Calendar for ID {}", id); let supported_components = SupportedComponents::TODO; - let mut cal = CachedCalendar::new(new_name.to_string(), id.clone(), supported_components); - if should_mock_remote_calendar { - cal.set_is_mocking_remote_calendar(); - } - source.insert_calendar(cal).await + + source.create_calendar( + id.clone(), + new_name.to_string(), + supported_components, + ).await } } } diff --git a/tests/sync.rs b/tests/sync.rs index f9137a8..e49da24 100644 --- a/tests/sync.rs +++ b/tests/sync.rs @@ -1,9 +1,5 @@ mod scenarii; -use my_tasks::traits::CalDavSource; -use my_tasks::Provider; -use my_tasks::cache::Cache; -use my_tasks::calendar::cached_calendar::CachedCalendar; @@ -19,6 +15,8 @@ impl TestFlavour { pub fn normal() -> Self { Self{} } #[cfg(not(feature = "local_calendar_mocks_remote_calendars"))] pub fn first_sync_to_local() -> Self { Self{} } + #[cfg(not(feature = "local_calendar_mocks_remote_calendars"))] + pub fn first_sync_to_server() -> Self { Self{} } #[cfg(feature = "local_calendar_mocks_remote_calendars")] pub fn normal() -> Self { @@ -34,6 +32,13 @@ impl TestFlavour { } } + #[cfg(feature = "local_calendar_mocks_remote_calendars")] + pub fn first_sync_to_server() -> Self { + Self { + scenarii: scenarii::scenarii_first_sync_to_server(), + } + } + #[cfg(not(feature = "local_calendar_mocks_remote_calendars"))] pub async fn run(&self) { @@ -83,6 +88,22 @@ async fn test_sync_empty_initial_local() { flavour.run().await; } +#[tokio::test] +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; +} + + +#[cfg(feature = "integration_tests")] +use my_tasks::{traits::CalDavSource, + Provider, + cache::Cache, + calendar::cached_calendar::CachedCalendar, +}; + /// Print the contents of the provider. This is usually used for debugging #[allow(dead_code)] #[cfg(feature = "integration_tests")]