diff --git a/tests/scenarii.rs b/tests/scenarii.rs new file mode 100644 index 0000000..2b12d32 --- /dev/null +++ b/tests/scenarii.rs @@ -0,0 +1,225 @@ +//! Multiple scenarios that are performed to test sync operations correctly work +#![cfg(feature = "integration_tests")] + +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use std::error::Error; + +use my_tasks::calendar::CalendarId; +use my_tasks::calendar::SupportedComponents; +use my_tasks::traits::CalDavSource; +use my_tasks::traits::BaseCalendar; +use my_tasks::traits::CompleteCalendar; +use my_tasks::traits::DavCalendar; +use my_tasks::cache::Cache; +use my_tasks::Item; +use my_tasks::ItemId; +use my_tasks::SyncStatus; +use my_tasks::Task; +use my_tasks::calendar::cached_calendar::CachedCalendar; +use my_tasks::Provider; + +pub enum LocatedState { + /// Item does not exist yet or does not exist anymore + None, + /// Item is only in the local source + Local(ItemState), + /// Item is only in the remote source + Remote(ItemState), + /// Item is synced at both locations, + BothSynced(ItemState), +} + +pub struct ItemState { + // TODO: if/when this crate supports Events as well, we could add such events here + /// The calendar it is in + calendar: CalendarId, + /// Its name + name: String, + /// Its completion status + completed: bool, +} + +pub enum ChangeToApply { + Rename(String), + SetCompletion(bool), + ChangeCalendar(CalendarId), + Create(CalendarId, Item), + /// "remove" means "mark for deletion" in the local calendar, or "immediately delete" on the remote calendar + Remove, +} + + +pub struct ItemScenario { + id: ItemId, + before_sync: LocatedState, + local_changes_to_apply: Vec, + remote_changes_to_apply: Vec, + after_sync: LocatedState, +} + +/// Populate sources with the following: +/// * At the last sync: both sources had A, B, C, D, E, F, G, H, I, J, K, L, M✓, N✓, O✓ at last sync +/// * Before the newer sync, this will be the content of the sources: +/// * cache: A, B, D', E, F'', G , H✓, I✓, J✓, M, N✓, O, P, +/// * server: A, C, D, E', F', G✓, H , I', K✓, M✓, N , O, Q +/// +/// Hence, here is the expected result after the sync: +/// * both: A, D', E', F', G✓, H✓, I', K✓, M, N, O, P, Q +/// +/// Notes: +/// * X': name has been modified since the last sync +/// * F'/F'': name conflict +/// * G✓: task has been marked as completed +pub fn basic_scenarii() -> Vec { + let mut tasks = Vec::new(); + + + + tasks +} + +pub async fn populate_test_provider(scenarii: &[ItemScenario]) -> 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/"))); + + // Create the initial state, as if we synced both sources in a given state + for item in scenarii { + let (state, sync_status) = match &item.before_sync { + LocatedState::None => continue, + LocatedState::Local(s) => (s, SyncStatus::NotSynced), + LocatedState::Remote(s) => (s, SyncStatus::random_synced()), + LocatedState::BothSynced(s) => (s, SyncStatus::random_synced()), + }; + + let new_item = Item::Task( + Task::new( + state.name.clone(), + item.id.clone(), + sync_status, + state.completed, + )); + + match &item.before_sync { + LocatedState::None => panic!("Should not happen, we've continued already"), + LocatedState::Local(s) => { + 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).await.unwrap().lock().unwrap().add_item(new_item).await.unwrap(); + }, + LocatedState::BothSynced(s) => { + 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(); + }, + } + } + let provider = Provider::new(remote, local); + + + // Apply changes to each item + for item in scenarii { + let initial_calendar_id = match &item.before_sync { + LocatedState::None => None, + LocatedState::Local(state) => Some(&state.calendar), + LocatedState::Remote(state) => Some(&state.calendar), + LocatedState::BothSynced(state) => Some(&state.calendar), + }; + + for local_change in &item.local_changes_to_apply { + apply_change(provider.local(), initial_calendar_id, &item.id, local_change, false).await; + } + + for remote_change in &item.remote_changes_to_apply { + apply_change(provider.remote(), initial_calendar_id, &item.id, remote_change, true).await; + } + } + + provider +} + +async fn get_or_insert_calendar(source: &mut S, id: &CalendarId) -> Result>, Box> +where + S: CalDavSource, + C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds +{ + match source.get_calendar(id).await { + Some(cal) => Ok(cal), + None => { + let new_name = format!("Calendar for ID {}", id); + let supported_components = SupportedComponents::TODO; + let cal = C::new(new_name.to_string(), id.clone(), supported_components); + source.insert_calendar(cal).await + } + } +} + +/// Apply a single change on a given source +async fn apply_change(source: &S, calendar_id: Option<&CalendarId>, item_id: &ItemId, change: &ChangeToApply, is_remote: bool) +where + S: CalDavSource, + C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds +{ + match calendar_id { + Some(cal) => apply_changes_on_an_existing_item(source, cal, item_id, change, is_remote).await, + None => create_test_item(source, change).await, + } +} + +async fn apply_changes_on_an_existing_item(source: &S, calendar_id: &CalendarId, item_id: &ItemId, change: &ChangeToApply, is_remote: bool) +where + S: CalDavSource, + C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds +{ + let cal = source.get_calendar(calendar_id).await.unwrap(); + let mut cal = cal.lock().unwrap(); + let task = cal.get_item_by_id_mut(item_id).await.unwrap().unwrap_task_mut(); + + match change { + ChangeToApply::Rename(new_name) => { + if is_remote { + task.mock_remote_calendar_set_name(new_name.clone()); + } else { + task.set_name(new_name.clone()); + } + }, + ChangeToApply::SetCompletion(new_status) => { + if is_remote { + task.mock_remote_calendar_set_completed(new_status.clone()); + } else { + task.set_completed(new_status.clone()); + } + }, + ChangeToApply::ChangeCalendar(_) => { + panic!("Not implemented yet"); + }, + ChangeToApply::Remove => { + match is_remote { + false => cal.mark_for_deletion(item_id).await.unwrap(), + true => cal.delete_item(item_id).await.unwrap(), + }; + }, + ChangeToApply::Create(_calendar_id, _item) => { + panic!("This function only handles already existing items"); + }, + } +} + +async fn create_test_item(source: &S, change: &ChangeToApply) +where + S: CalDavSource, + C: CompleteCalendar + DavCalendar, // in this test, we're using a calendar that mocks both kinds +{ + match change { + ChangeToApply::Rename(_) | + ChangeToApply::SetCompletion(_) | + ChangeToApply::ChangeCalendar(_) | + ChangeToApply::Remove => { + panic!("This function only creates items that do not exist yet"); + } + ChangeToApply::Create(calendar_id, item) => { + let cal = source.get_calendar(calendar_id).await.unwrap(); + cal.lock().unwrap().add_item(item.clone()).await.unwrap(); + }, + } +} diff --git a/tests/sync.rs b/tests/sync.rs index b7fa3cc..51de084 100644 --- a/tests/sync.rs +++ b/tests/sync.rs @@ -1,4 +1,5 @@ #![cfg(feature = "integration_tests")] +mod scenarii; use std::path::PathBuf; use std::sync::{Arc, Mutex}; @@ -17,10 +18,20 @@ use my_tasks::Task; use my_tasks::calendar::cached_calendar::CachedCalendar; use my_tasks::Provider; + +#[tokio::test] +/// This test simulates a regular synchronisation between a local cache and a server. +/// Note that this uses a second cache to "mock" a server. +async fn test_regular_sync() { + let scenarii = scenarii::basic_scenarii(); + let provider = scenarii::populate_test_provider(&scenarii).await; +} + + #[tokio::test] /// This test simulates a synchronisation between a local cache and a server /// To "mock" a server, let's use a second cache -async fn test_regular_sync() { +async fn legacy_test() { let _ = env_logger::builder().is_test(true).try_init(); let mut provider = populate_test_provider().await; @@ -45,38 +56,26 @@ async fn test_regular_sync() { } -/// Populate sources with the following: -/// * At the last sync: both sources had A, B, C, D, E, F, G, H, I, J, K, L, M at last sync -/// * Before the newer sync, this will be the content of the sources: -/// * server: A, C, D, E', F', G✓, H , I', K✓, M, N -/// * cache: A, B, D', E, F'', G , H✓, I✓, J✓, M, O -/// -/// Hence, here is the expected result after the sync: -/// * both: A, D', E', F', G✓, H✓, I', K✓, M, N, O -/// -/// Notes: -/// * X': name has been modified since the last sync -/// * F'/F'': name conflict -/// * G✓: task has been marked as completed + async fn populate_test_provider() -> Provider { let mut server = Cache::new(&PathBuf::from(String::from("server.json"))); let mut local = Cache::new(&PathBuf::from(String::from("local.json"))); let cal_id = Url::parse("http://todo.list/cal").unwrap(); - let task_a = Item::Task(Task::new("task A".into(), ItemId::random(), SyncStatus::random_synced())); - let task_b = Item::Task(Task::new("task B".into(), ItemId::random(), SyncStatus::random_synced())); - let task_c = Item::Task(Task::new("task C".into(), ItemId::random(), SyncStatus::random_synced())); - let task_d = Item::Task(Task::new("task D".into(), ItemId::random(), SyncStatus::random_synced())); - let task_e = Item::Task(Task::new("task E".into(), ItemId::random(), SyncStatus::random_synced())); - let task_f = Item::Task(Task::new("task F".into(), ItemId::random(), SyncStatus::random_synced())); - let task_g = Item::Task(Task::new("task G".into(), ItemId::random(), SyncStatus::random_synced())); - let task_h = Item::Task(Task::new("task H".into(), ItemId::random(), SyncStatus::random_synced())); - let task_i = Item::Task(Task::new("task I".into(), ItemId::random(), SyncStatus::random_synced())); - let task_j = Item::Task(Task::new("task J".into(), ItemId::random(), SyncStatus::random_synced())); - let task_k = Item::Task(Task::new("task K".into(), ItemId::random(), SyncStatus::random_synced())); - let task_l = Item::Task(Task::new("task L".into(), ItemId::random(), SyncStatus::random_synced())); - let task_m = Item::Task(Task::new("task M".into(), ItemId::random(), SyncStatus::random_synced())); + let task_a = Item::Task(Task::new("task A".into(), ItemId::random(), SyncStatus::random_synced(), false)); + let task_b = Item::Task(Task::new("task B".into(), ItemId::random(), SyncStatus::random_synced(), false)); + let task_c = Item::Task(Task::new("task C".into(), ItemId::random(), SyncStatus::random_synced(), false)); + let task_d = Item::Task(Task::new("task D".into(), ItemId::random(), SyncStatus::random_synced(), false)); + let task_e = Item::Task(Task::new("task E".into(), ItemId::random(), SyncStatus::random_synced(), false)); + let task_f = Item::Task(Task::new("task F".into(), ItemId::random(), SyncStatus::random_synced(), false)); + let task_g = Item::Task(Task::new("task G".into(), ItemId::random(), SyncStatus::random_synced(), false)); + let task_h = Item::Task(Task::new("task H".into(), ItemId::random(), SyncStatus::random_synced(), false)); + let task_i = Item::Task(Task::new("task I".into(), ItemId::random(), SyncStatus::random_synced(), false)); + let task_j = Item::Task(Task::new("task J".into(), ItemId::random(), SyncStatus::random_synced(), false)); + let task_k = Item::Task(Task::new("task K".into(), ItemId::random(), SyncStatus::random_synced(), false)); + let task_l = Item::Task(Task::new("task L".into(), ItemId::random(), SyncStatus::random_synced(), false)); + let task_m = Item::Task(Task::new("task M".into(), ItemId::random(), SyncStatus::random_synced(), false)); let task_b_id = task_b.id().clone(); let task_c_id = task_c.id().clone(); @@ -107,8 +106,8 @@ async fn populate_test_provider() -> Provider Provider Provider