[tests] Adding support for reproducible scenarii
This commit is contained in:
parent
f18b1acc0f
commit
9b3afd3001
2 changed files with 255 additions and 31 deletions
225
tests/scenarii.rs
Normal file
225
tests/scenarii.rs
Normal file
|
@ -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<ChangeToApply>,
|
||||||
|
remote_changes_to_apply: Vec<ChangeToApply>,
|
||||||
|
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<ItemScenario> {
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
tasks
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn populate_test_provider(scenarii: &[ItemScenario]) -> Provider<Cache, CachedCalendar, Cache, CachedCalendar> {
|
||||||
|
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<S, C>(source: &mut S, id: &CalendarId) -> Result<Arc<Mutex<C>>, Box<dyn Error>>
|
||||||
|
where
|
||||||
|
S: CalDavSource<C>,
|
||||||
|
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<S, C>(source: &S, calendar_id: Option<&CalendarId>, item_id: &ItemId, change: &ChangeToApply, is_remote: bool)
|
||||||
|
where
|
||||||
|
S: CalDavSource<C>,
|
||||||
|
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<S, C>(source: &S, calendar_id: &CalendarId, item_id: &ItemId, change: &ChangeToApply, is_remote: bool)
|
||||||
|
where
|
||||||
|
S: CalDavSource<C>,
|
||||||
|
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<S, C>(source: &S, change: &ChangeToApply)
|
||||||
|
where
|
||||||
|
S: CalDavSource<C>,
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
#![cfg(feature = "integration_tests")]
|
#![cfg(feature = "integration_tests")]
|
||||||
|
mod scenarii;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
@ -17,10 +18,20 @@ 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;
|
||||||
|
|
||||||
|
|
||||||
|
#[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]
|
#[tokio::test]
|
||||||
/// This test simulates a synchronisation between a local cache and a server
|
/// This test simulates a synchronisation between a local cache and a server
|
||||||
/// To "mock" a server, let's use a second cache
|
/// 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 _ = env_logger::builder().is_test(true).try_init();
|
||||||
|
|
||||||
let mut provider = populate_test_provider().await;
|
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<Cache, CachedCalendar, Cache, CachedCalendar> {
|
async fn populate_test_provider() -> Provider<Cache, CachedCalendar, Cache, CachedCalendar> {
|
||||||
let mut server = Cache::new(&PathBuf::from(String::from("server.json")));
|
let mut server = Cache::new(&PathBuf::from(String::from("server.json")));
|
||||||
let mut local = Cache::new(&PathBuf::from(String::from("local.json")));
|
let mut local = Cache::new(&PathBuf::from(String::from("local.json")));
|
||||||
|
|
||||||
let cal_id = Url::parse("http://todo.list/cal").unwrap();
|
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_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()));
|
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()));
|
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()));
|
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()));
|
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()));
|
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()));
|
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()));
|
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()));
|
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()));
|
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()));
|
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()));
|
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()));
|
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_b_id = task_b.id().clone();
|
||||||
let task_c_id = task_c.id().clone();
|
let task_c_id = task_c.id().clone();
|
||||||
|
@ -107,8 +106,8 @@ async fn populate_test_provider() -> Provider<Cache, CachedCalendar, Cache, Cach
|
||||||
calendar.add_item(task_l).await.unwrap();
|
calendar.add_item(task_l).await.unwrap();
|
||||||
calendar.add_item(task_m).await.unwrap();
|
calendar.add_item(task_m).await.unwrap();
|
||||||
|
|
||||||
server.add_calendar(Arc::new(Mutex::new(calendar.clone())));
|
server.insert_calendar(calendar.clone());
|
||||||
local.add_calendar(Arc::new(Mutex::new(calendar.clone())));
|
local.insert_calendar(calendar.clone());
|
||||||
|
|
||||||
// Step 2
|
// Step 2
|
||||||
// Edit the server calendar
|
// Edit the server calendar
|
||||||
|
@ -136,7 +135,7 @@ async fn populate_test_provider() -> Provider<Cache, CachedCalendar, Cache, Cach
|
||||||
|
|
||||||
cal_server.delete_item(&task_l_id).await.unwrap();
|
cal_server.delete_item(&task_l_id).await.unwrap();
|
||||||
|
|
||||||
let task_n = Item::Task(Task::new("task N (new from server)".into(), ItemId::random(), SyncStatus::random_synced()));
|
let task_n = Item::Task(Task::new("task N (new from server)".into(), ItemId::random(), SyncStatus::random_synced(), false));
|
||||||
cal_server.add_item(task_n).await.unwrap();
|
cal_server.add_item(task_n).await.unwrap();
|
||||||
|
|
||||||
|
|
||||||
|
@ -165,7 +164,7 @@ async fn populate_test_provider() -> Provider<Cache, CachedCalendar, Cache, Cach
|
||||||
cal_local.mark_for_deletion(&task_k_id).await.unwrap();
|
cal_local.mark_for_deletion(&task_k_id).await.unwrap();
|
||||||
cal_local.mark_for_deletion(&task_l_id).await.unwrap();
|
cal_local.mark_for_deletion(&task_l_id).await.unwrap();
|
||||||
|
|
||||||
let task_o = Item::Task(Task::new("task O (new from local)".into(), ItemId::random(), SyncStatus::NotSynced));
|
let task_o = Item::Task(Task::new("task O (new from local)".into(), ItemId::random(), SyncStatus::NotSynced, false));
|
||||||
cal_local.add_item(task_o).await.unwrap();
|
cal_local.add_item(task_o).await.unwrap();
|
||||||
|
|
||||||
Provider::new(server, local)
|
Provider::new(server, local)
|
||||||
|
|
Loading…
Add table
Reference in a new issue