use std::{fmt::Debug, time::Instant}; use debug_log::debug_log; use enum_as_inner::EnumAsInner; use tabled::{TableIteratorExt, Tabled}; pub mod recursive; use crate::{array_mut_ref, id::ClientID, log_store::EncodeConfig, LoroCore, VersionVector}; #[derive(arbitrary::Arbitrary, EnumAsInner, Clone, PartialEq, Eq, Debug)] pub enum Action { Ins { content: u16, pos: usize, site: u8 }, Del { pos: usize, len: usize, site: u8 }, Sync { from: u8, to: u8 }, SyncAll, } impl Tabled for Action { const LENGTH: usize = 5; fn fields(&self) -> Vec> { match self { Action::Ins { content, pos, site } => vec![ "ins".into(), site.to_string().into(), pos.to_string().into(), content.to_string().len().to_string().into(), content.to_string().into(), ], Action::Del { pos, len, site } => vec![ "del".into(), site.to_string().into(), pos.to_string().into(), len.to_string().into(), "".into(), ], Action::Sync { from, to } => vec![ "sync".into(), format!("{} to {}", from, to).into(), "".into(), "".into(), "".into(), ], Action::SyncAll => vec![ "sync all".into(), "".into(), "".into(), "".into(), "".into(), ], } } fn headers() -> Vec> { vec![ "type".into(), "site".into(), "pos".into(), "len".into(), "content".into(), ] } } trait Actionable { fn apply_action(&mut self, action: &Action); fn preprocess(&mut self, action: &mut Action); } impl Action { pub fn preprocess(&mut self, max_len: usize, max_users: u8) { match self { Action::Ins { pos, site, .. } => { *pos %= max_len + 1; *site %= max_users; } Action::Del { pos, len, site } => { if max_len == 0 { *pos = 0; *len = 0; } else { *pos %= max_len; *len = (*len).min(max_len - (*pos)); } *site %= max_users; } Action::Sync { from, to } => { *from %= max_users; *to %= max_users; } Action::SyncAll => {} } } } impl Actionable for String { fn apply_action(&mut self, action: &Action) { match action { Action::Ins { content, pos, .. } => { self.insert_str(*pos, &content.to_string()); } &Action::Del { pos, len, .. } => { if self.is_empty() { return; } self.drain(pos..pos + len); } _ => {} } } fn preprocess(&mut self, action: &mut Action) { action.preprocess(self.len(), 1); match action { Action::Ins { pos, .. } => { while !self.is_char_boundary(*pos) { *pos = (*pos + 1) % (self.len() + 1) } } Action::Del { pos, len, .. } => { if self.is_empty() { *len = 0; *pos = 0; return; } while !self.is_char_boundary(*pos) { *pos = (*pos + 1) % self.len(); } *len = (*len).min(self.len() - (*pos)); while !self.is_char_boundary(*pos + *len) { *len += 1; } } _ => {} } } } impl Actionable for Vec { fn apply_action(&mut self, action: &Action) { match action { Action::Ins { content, pos, site } => { let site = &mut self[*site as usize]; let mut text = site.get_text("text"); text.insert(site, *pos, &content.to_string()).unwrap(); } Action::Del { pos, len, site } => { let site = &mut self[*site as usize]; let mut text = site.get_text("text"); text.delete(site, *pos, *len).unwrap(); } Action::Sync { from, to } => { let to_vv = self[*to as usize].vv_cloned(); let from_exported = self[*from as usize].export(to_vv); self[*to as usize].import(from_exported); } Action::SyncAll => { for i in 1..self.len() { let (a, b) = array_mut_ref!(self, [0, i]); a.import(b.export(a.vv_cloned())); } for i in 1..self.len() { let (a, b) = array_mut_ref!(self, [0, i]); b.import(a.export(b.vv_cloned())); } } } } fn preprocess(&mut self, action: &mut Action) { match action { Action::Ins { pos, site, .. } => { *site %= self.len() as u8; let text = self[*site as usize].get_text("text"); change_pos_to_char_boundary(pos, text.len()); } Action::Del { pos, len, site } => { *site %= self.len() as u8; let text = self[*site as usize].get_text("text"); if text.is_empty() { *len = 0; *pos = 0; return; } change_delete_to_char_boundary(pos, len, text.len()); } Action::Sync { from, to } => { *from %= self.len() as u8; *to %= self.len() as u8; } Action::SyncAll => {} } } } pub fn change_delete_to_char_boundary(pos: &mut usize, len: &mut usize, str_len: usize) { *pos %= str_len + 1; *len = (*len).min(str_len - (*pos)); } pub fn change_pos_to_char_boundary(pos: &mut usize, len: usize) { *pos %= len + 1; } fn check_eq(site_a: &mut LoroCore, site_b: &mut LoroCore) { let a = site_a.get_text("text"); let b = site_b.get_text("text"); let value_a = a.get_value(); let value_b = b.get_value(); assert_eq!(value_a.as_string().unwrap(), value_b.as_string().unwrap()); } fn check_synced(sites: &mut [LoroCore]) { for i in 0..sites.len() - 1 { for j in i + 1..sites.len() { debug_log::group!("checking {} with {}", i, j); let (a, b) = array_mut_ref!(sites, [i, j]); { debug_log::group!("Import {}", i); a.decode(&b.encode_from(a.vv_cloned())).unwrap(); debug_log::group_end!(); } { debug_log::group!("Import {}", j); b.decode(&a.encode_from(b.vv_cloned())).unwrap(); debug_log::group_end!(); } check_eq(a, b); debug_log::group_end!(); } } } pub fn test_single_client(mut actions: Vec) { let mut store = LoroCore::new(Default::default(), Some(1)); let mut text_container = store.get_text("haha"); let mut ground_truth = String::new(); let mut applied = Vec::new(); for action in actions .iter_mut() .filter(|x| x.as_del().is_some() || x.as_ins().is_some()) { ground_truth.preprocess(action); applied.push(action.clone()); // println!("{}", (&applied).table()); ground_truth.apply_action(action); match action { Action::Ins { content, pos, .. } => { text_container .insert(&store, *pos, &content.to_string()) .unwrap(); } Action::Del { pos, len, .. } => { if text_container.is_empty() { return; } text_container.delete(&store, *pos, *len).unwrap(); } _ => {} } assert_eq!( ground_truth.as_str(), &**text_container.get_value().as_string().unwrap(), "{}", applied.table() ); } } pub fn test_single_client_encode(mut actions: Vec) { let mut store = LoroCore::new(Default::default(), None); let mut text_container = store.get_text("hello"); let mut ground_truth = String::new(); let mut applied = Vec::new(); for action in actions .iter_mut() .filter(|x| x.as_del().is_some() || x.as_ins().is_some()) { ground_truth.preprocess(action); applied.push(action.clone()); // println!("{}", (&applied).table()); ground_truth.apply_action(action); match action { Action::Ins { content, pos, .. } => { text_container .insert(&store, *pos, &content.to_string()) .unwrap(); } Action::Del { pos, len, .. } => { if text_container.is_empty() { return; } text_container.delete(&store, *pos, *len).unwrap(); } _ => {} } } let encode_bytes = store.encode_with_cfg(EncodeConfig::rle_update(VersionVector::new()).without_compress()); let json1 = store.to_json(); let mut store2 = LoroCore::new(Default::default(), None); store2.decode(&encode_bytes).unwrap(); let _encode_bytes2 = store2.encode_with_cfg(EncodeConfig::rle_update(VersionVector::new()).without_compress()); let json2 = store2.to_json(); // state encode will change mergable range // assert_eq!(encode_bytes, encode_bytes2); assert_eq!(json1, json2); } pub fn minify_error(site_num: u8, actions: Vec, f: F, normalize: N) where F: Fn(u8, &mut [T]), N: Fn(u8, &mut [T]) -> Vec, T: Clone + Debug, { std::panic::set_hook(Box::new(|_info| { // ignore panic output })); let f_ref: *const _ = &f; let f_ref: usize = f_ref as usize; let actions_clone = actions.clone(); let action_ref: usize = (&actions_clone) as *const _ as usize; #[allow(clippy::blocks_in_if_conditions)] if std::panic::catch_unwind(|| { // SAFETY: test let f = unsafe { &*(f_ref as *const F) }; // SAFETY: test let actions_ref = unsafe { &mut *(action_ref as *mut Vec) }; f(site_num, actions_ref); }) .is_ok() { println!("No Error Found"); return; } let mut minified = actions.clone(); let mut candidates = Vec::new(); for i in 0..actions.len() { let mut new = actions.clone(); new.remove(i); candidates.push(new); } println!("Minifying..."); let start = Instant::now(); while let Some(candidate) = candidates.pop() { let f_ref: *const _ = &f; let f_ref: usize = f_ref as usize; let actions_clone = candidate.clone(); let action_ref: usize = (&actions_clone) as *const _ as usize; #[allow(clippy::blocks_in_if_conditions)] if std::panic::catch_unwind(|| { // SAFETY: test let f = unsafe { &*(f_ref as *const F) }; // SAFETY: test let actions_ref = unsafe { &mut *(action_ref as *mut Vec) }; f(site_num, actions_ref); }) .is_err() { for i in 0..candidate.len() { let mut new = candidate.clone(); new.remove(i); candidates.push(new); } if candidate.len() < minified.len() { minified = candidate; println!("New min len={}", minified.len()); } if candidates.len() > 40 { candidates.drain(0..30); } } if start.elapsed().as_secs() > 10 && minified.len() <= 4 { break; } if start.elapsed().as_secs() > 60 { break; } } let minified = normalize(site_num, &mut minified); println!( "Old Length {}, New Length {}", actions.len(), minified.len() ); dbg!(&minified); if actions.len() > minified.len() { minify_error(site_num, minified, f, normalize); } } pub fn normalize(site_num: u8, actions: &mut [Action]) -> Vec { let mut sites = Vec::new(); for i in 0..site_num { sites.push(LoroCore::new(Default::default(), Some(i as ClientID))); } let mut applied = Vec::new(); for action in actions.iter_mut() { sites.preprocess(action); applied.push(action.clone()); let sites_ptr: *mut Vec<_> = &mut sites as *mut _; #[allow(clippy::blocks_in_if_conditions)] if std::panic::catch_unwind(|| { // SAFETY: Test let sites = unsafe { &mut *sites_ptr }; sites.apply_action(&action.clone()); }) .is_err() { break; } } println!("{}", applied.clone().table()); applied } pub fn test_multi_sites(site_num: u8, actions: &mut [Action]) { let mut sites = Vec::new(); for i in 0..site_num { sites.push(LoroCore::new(Default::default(), Some(i as ClientID))); } let mut applied = Vec::new(); for action in actions.iter_mut() { sites.preprocess(action); applied.push(action.clone()); debug_log!("\n{}", (&applied).table()); sites.apply_action(action); } debug_log::group!("CheckSynced"); // println!("{}", actions.table()); check_synced(&mut sites); debug_log::group_end!(); } #[cfg(test)] mod test { use super::Action::*; use super::*; #[test] fn case1() { test_multi_sites( 8, &mut vec![ Ins { content: 35108, pos: 0, site: 2, }, Ins { content: 18218, pos: 0, site: 7, }, Ins { content: 35624, pos: 0, site: 0, }, Ins { content: 38400, pos: 0, site: 6, }, Ins { content: 65280, pos: 2, site: 7, }, Ins { content: 4626, pos: 5, site: 0, }, Ins { content: 60672, pos: 0, site: 1, }, Ins { content: 35072, pos: 1, site: 2, }, Ins { content: 15035, pos: 3, site: 0, }, Ins { content: 65280, pos: 0, site: 7, }, Ins { content: 4626, pos: 0, site: 0, }, Ins { content: 201, pos: 2, site: 2, }, Ins { content: 65377, pos: 3, site: 1, }, Ins { content: 9988, pos: 0, site: 0, }, Ins { content: 4626, pos: 14, site: 0, }, Ins { content: 4626, pos: 11, site: 7, }, Ins { content: 1070, pos: 0, site: 5, }, Ins { content: 27421, pos: 7, site: 1, }, Ins { content: 65121, pos: 22, site: 0, }, Ins { content: 65462, pos: 1, site: 0, }, Ins { content: 4626, pos: 0, site: 4, }, Ins { content: 4626, pos: 16, site: 0, }, Ins { content: 65462, pos: 11, site: 2, }, Ins { content: 48009, pos: 10, site: 0, }, Ins { content: 23277, pos: 7, site: 0, }, Ins { content: 60672, pos: 13, site: 1, }, Ins { content: 4626, pos: 2, site: 7, }, Ins { content: 4626, pos: 2, site: 0, }, Ins { content: 2606, pos: 0, site: 3, }, Ins { content: 65270, pos: 10, site: 0, }, SyncAll, Ins { content: 65462, pos: 107, site: 4, }, SyncAll, Ins { content: 4626, pos: 98, site: 0, }, SyncAll, Ins { content: 0, pos: 0, site: 0, }, Del { pos: 0, len: 147, site: 0, }, Ins { content: 0, pos: 146, site: 4, }, ], ) } #[test] fn case0() { test_multi_sites( 4, &mut [ Ins { content: 31800, pos: 723390690148040714, site: 137, }, Ins { content: 2560, pos: 12826352382887627018, site: 178, }, Sync { from: 178, to: 0 }, Ins { content: 35082, pos: 12876550765177602139, site: 178, }, ], ) } #[test] fn case_new_cache() { test_multi_sites( 3, &mut [ Ins { content: 35108, pos: 0, site: 2, }, Ins { content: 35624, pos: 0, site: 0, }, Del { pos: 0, len: 5, site: 0, }, ], ) } #[test] fn case_two() { test_multi_sites( 3, &mut [ Ins { content: 35108, pos: 0, site: 2, }, Ins { content: 18218, pos: 0, site: 7, }, Ins { content: 65280, pos: 2, site: 7, }, ], ) } #[test] fn mini() { minify_error( 8, vec![ Ins { content: 35108, pos: 0, site: 2, }, Ins { content: 18218, pos: 0, site: 7, }, Ins { content: 35624, pos: 0, site: 0, }, Ins { content: 38400, pos: 0, site: 6, }, Ins { content: 65280, pos: 2, site: 7, }, Ins { content: 4626, pos: 5, site: 0, }, Ins { content: 60672, pos: 0, site: 1, }, Ins { content: 35072, pos: 1, site: 2, }, Ins { content: 15035, pos: 3, site: 0, }, Ins { content: 65280, pos: 0, site: 7, }, Ins { content: 4626, pos: 0, site: 0, }, Ins { content: 201, pos: 2, site: 2, }, Ins { content: 65377, pos: 3, site: 1, }, Ins { content: 9988, pos: 0, site: 0, }, Ins { content: 4626, pos: 14, site: 0, }, Ins { content: 4626, pos: 11, site: 7, }, Ins { content: 1070, pos: 0, site: 5, }, Ins { content: 27421, pos: 7, site: 1, }, Ins { content: 65121, pos: 22, site: 0, }, Ins { content: 65462, pos: 1, site: 0, }, Ins { content: 4626, pos: 0, site: 4, }, Ins { content: 4626, pos: 16, site: 0, }, Ins { content: 65462, pos: 11, site: 2, }, Ins { content: 48009, pos: 10, site: 0, }, Ins { content: 23277, pos: 7, site: 0, }, Ins { content: 60672, pos: 13, site: 1, }, Ins { content: 4626, pos: 2, site: 7, }, Ins { content: 4626, pos: 2, site: 0, }, Ins { content: 2606, pos: 0, site: 3, }, Ins { content: 65270, pos: 10, site: 0, }, SyncAll, Ins { content: 65462, pos: 107, site: 4, }, SyncAll, Ins { content: 4626, pos: 98, site: 0, }, SyncAll, Ins { content: 0, pos: 0, site: 0, }, Del { pos: 0, len: 147, site: 0, }, Ins { content: 0, pos: 146, site: 4, }, ], test_multi_sites, normalize, ) } #[test] fn simplify_checkout() { test_multi_sites( 8, &mut [ Ins { content: 35368, pos: 73184288580830345, site: 16, }, Ins { content: 4, pos: 18446744073693037568, site: 255, }, SyncAll, Del { pos: 18377562991809527818, len: 9955211391596233732, site: 137, }, Ins { content: 1028, pos: 283674009020420, site: 0, }, ], ) } #[test] fn case_encode() { test_single_client_encode(vec![Ins { content: 49087, pos: 4631600097073807295, site: 191, }]) } }