Skip to content

Commit

Permalink
Feat: allow editing on detached mode (#473)
Browse files Browse the repository at this point in the history
* refactor: add detached editing config and prepare the architecture for editing detached doc

* feat: subscribe for peer id change

* fix: undo after checkout & add tests for detached editing

* test: add fuzzer for detached editing

* feat: expose detached editing configure to wasm

* test: add wasm test for detached editing
  • Loading branch information
zxch3n authored Sep 24, 2024
1 parent 88e9ec9 commit bef39ce
Show file tree
Hide file tree
Showing 34 changed files with 1,264 additions and 280 deletions.
5 changes: 3 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
},
"rust-analyzer.cargo.features": [
"jsonpath",
"counter"
"counter",
"test_utils"
],
"editor.defaultFormatter": "rust-lang.rust-analyzer",
"rust-analyzer.server.extraEnv": {
Expand All @@ -80,7 +81,7 @@
"cortex-debug.variableUseNaturalFormat": true,
"[markdown]": {},
"[typescript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"vitest.enable": true,
"[shellscript]": {
Expand Down
6 changes: 6 additions & 0 deletions crates/fuzz/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ path = "fuzz_targets/gc_fuzz.rs"
test = false
doc = false

[[bin]]
name = "one_doc_fuzz"
path = "fuzz_targets/one_doc_fuzz.rs"
test = false
doc = false

[[bin]]
name = "mov"
path = "fuzz_targets/mov.rs"
Expand Down
9 changes: 9 additions & 0 deletions crates/fuzz/fuzz/fuzz_targets/one_doc_fuzz.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#![no_main]

use libfuzzer_sys::fuzz_target;

use fuzz::{test_multi_sites_on_one_doc, Action, FuzzTarget};

fuzz_target!(|actions: Vec<Action>| {
test_multi_sites_on_one_doc(5, &mut actions.clone());
});
2 changes: 1 addition & 1 deletion crates/fuzz/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::fmt::Debug;
use crate::container::CounterAction;
pub use crate::container::MovableListAction;

use super::{
pub use super::{
actor::ActionExecutor,
container::{ListAction, MapAction, TextAction, TreeAction},
crdt_fuzzer::FuzzValue,
Expand Down
12 changes: 6 additions & 6 deletions crates/fuzz/src/container/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
mod counter;
mod list;
mod map;
mod movable_list;
mod text;
mod tree;
pub mod counter;
pub mod list;
pub mod map;
pub mod movable_list;
pub mod text;
pub mod tree;
pub use counter::*;
pub use list::*;
use loro::{LoroError, LoroResult};
Expand Down
2 changes: 2 additions & 0 deletions crates/fuzz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ pub mod actor;
pub mod container;
pub mod crdt_fuzzer;
mod macros;
pub mod one_doc_fuzzer;
mod value;
pub use crdt_fuzzer::{test_multi_sites, test_multi_sites_with_gc, Action, FuzzTarget};
mod mem_kv_fuzzer;
pub use mem_kv_fuzzer::{
minify_simple as kv_minify_simple, test_mem_kv_fuzzer, test_random_bytes_import,
Action as KVAction,
};
pub use one_doc_fuzzer::test_multi_sites_on_one_doc;
282 changes: 282 additions & 0 deletions crates/fuzz/src/one_doc_fuzzer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
use loro::{ContainerType, Frontiers, LoroDoc};
use tabled::TableIteratorExt;
use tracing::{info, info_span};

use crate::{actions::ActionWrapper, crdt_fuzzer::FuzzValue, Action};

#[derive(Default)]
struct Branch {
frontiers: Frontiers,
}

struct OneDocFuzzer {
doc: LoroDoc,
branches: Vec<Branch>,
}

impl OneDocFuzzer {
pub fn new(site_num: usize) -> Self {
let doc = LoroDoc::new();
doc.set_detached_editing(true);
Self {
doc,
branches: (0..site_num).map(|_| Branch::default()).collect(),
}
}

fn site_num(&self) -> usize {
self.branches.len()
}

fn pre_process(&mut self, action: &mut Action) {
let max_users = self.site_num() as u8;
match action {
Action::Sync { from, to } => {
*from %= max_users;
*to %= max_users;
if to == from {
*to = (*to + 1) % max_users;
}
}
Action::SyncAll => {}
Action::Checkout { site, to } => {}
Action::Handle {
site,
target,
container,
action,
} => {
if matches!(action, ActionWrapper::Action(_)) {
return;
}
*site %= max_users;
let branch = &mut self.branches[*site as usize];
let valid_targets = [
ContainerType::Text,
ContainerType::List,
ContainerType::Map,
ContainerType::MovableList,
];
*target %= valid_targets.len() as u8;
action.convert_to_inner(&valid_targets[*target as usize]);
self.doc.checkout(&branch.frontiers).unwrap();
if let Some(action) = action.as_action_mut() {
match action {
crate::actions::ActionInner::Map(map_action) => {}
crate::actions::ActionInner::List(list_action) => match list_action {
crate::container::list::ListAction::Insert { pos, value } => {
let len = self.doc.get_list("list").len();
*pos %= (len as u8).saturating_add(1);
}
crate::container::list::ListAction::Delete { pos, len } => {
let length = self.doc.get_list("list").len();
if length == 0 {
*pos = 0;
*len = 0;
} else {
*pos %= length as u8;
let mut end = pos.saturating_add(*len);
end = end % (length as u8) + 1;
if *pos > end {
*pos = end - 1;
}
*len = end - *pos;
}
}
},
crate::actions::ActionInner::MovableList(movable_list_action) => {
match movable_list_action {
crate::actions::MovableListAction::Insert { pos, value } => {
let len = self.doc.get_movable_list("movable_list").len();
*pos %= (len as u8).saturating_add(1);
}
crate::actions::MovableListAction::Delete { pos, len } => {
let length = self.doc.get_movable_list("movable_list").len();
if length == 0 {
*pos = 0;
*len = 0;
} else {
*pos %= length as u8;
let mut end = pos.saturating_add(*len);
end = end % (length as u8) + 1;
if *pos > end {
*pos = end - 1;
}
*len = end - *pos;
}
}
crate::actions::MovableListAction::Move { from, to } => {
let len = self.doc.get_movable_list("movable_list").len();
if len == 0 {
*movable_list_action =
crate::actions::MovableListAction::Insert {
pos: 0,
value: FuzzValue::I32(0),
};
} else {
*from %= len as u8;
*to %= len as u8;
}
}
crate::actions::MovableListAction::Set { pos, value } => {
let len = self.doc.get_movable_list("movable_list").len();
if len == 0 {
*movable_list_action =
crate::actions::MovableListAction::Insert {
pos: 0,
value: *value,
};
} else {
*pos %= len as u8;
}
}
}
}
crate::actions::ActionInner::Text(text_action) => {
match text_action.action {
crate::container::TextActionInner::Insert => {
let len = self.doc.get_text("text").len_unicode();
text_action.pos %= len.saturating_add(1);
}
crate::container::TextActionInner::Delete => {
let len = self.doc.get_text("text").len_unicode();
if len == 0 {
text_action.action =
crate::container::TextActionInner::Insert;
}
text_action.pos %= len.saturating_add(1);
let mut end = text_action.pos.wrapping_add(text_action.len);
if end > len {
end %= len + 1;
}
if end < text_action.pos {
end = len;
}
text_action.len = end - text_action.pos;
}
crate::container::TextActionInner::Mark(_) => {}
}
}
_ => {}
}
}
}
Action::Undo { site, op_len } => {}
Action::SyncAllUndo { site, op_len } => {}
}
}

fn apply_action(&mut self, action: &mut Action) {
match action {
Action::Handle {
site,
target,
container,
action,
} => {
let doc = &mut self.doc;
let branch = &mut self.branches[*site as usize];
doc.checkout(&branch.frontiers).unwrap();
match action {
ActionWrapper::Action(action_inner) => match action_inner {
crate::actions::ActionInner::Map(map_action) => match map_action {
crate::actions::MapAction::Insert { key, value } => {
let map = doc.get_map("map");
map.insert(&key.to_string(), value.to_string()).unwrap();
}
crate::actions::MapAction::Delete { key } => {
let map = doc.get_map("map");
map.delete(&key.to_string()).unwrap();
}
},
crate::actions::ActionInner::List(list_action) => match list_action {
crate::actions::ListAction::Insert { pos, value } => {
let list = doc.get_list("list");
list.insert(*pos as usize, value.to_string()).unwrap();
}
crate::actions::ListAction::Delete { pos, len } => {
let list = doc.get_list("list");
list.delete(*pos as usize, *len as usize).unwrap();
}
},
crate::actions::ActionInner::MovableList(movable_list_action) => {
match movable_list_action {
crate::actions::MovableListAction::Insert { pos, value } => {
let list = doc.get_movable_list("movable_list");
list.insert(*pos as usize, value.to_string()).unwrap();
}
crate::actions::MovableListAction::Delete { pos, len } => {
let list = doc.get_movable_list("movable_list");
list.delete(*pos as usize, *len as usize).unwrap();
}
crate::actions::MovableListAction::Move { from, to } => {
let list = doc.get_movable_list("movable_list");
list.mov(*from as usize, *to as usize).unwrap();
}
crate::actions::MovableListAction::Set { pos, value } => {
let list = doc.get_movable_list("movable_list");
list.set(*pos as usize, value.to_string()).unwrap();
}
}
}
crate::actions::ActionInner::Text(text_action) => {
let text = doc.get_text("text");
match text_action.action {
crate::container::TextActionInner::Insert => {
text.insert(text_action.pos, &text_action.len.to_string())
.unwrap();
}
crate::container::TextActionInner::Delete => {
text.delete(text_action.pos as usize, text_action.len)
.unwrap();
}
crate::container::TextActionInner::Mark(_) => {}
}
}
_ => unimplemented!(),
},
_ => unreachable!(),
}
}
Action::Sync { from, to } => {
let a = self.branches[*from as usize].frontiers.clone();
self.branches[*to as usize].frontiers.extend_from_slice(&a);
}
Action::SyncAll => {
let f = self.doc.oplog_frontiers();
for b in self.branches.iter_mut() {
b.frontiers = f.clone();
}
}
_ => {}
}
}

fn check_sync(&self) {
self.doc.checkout_to_latest();
self.doc.check_state_correctness_slow();
for b in self.branches.iter() {
self.doc.checkout(&b.frontiers).unwrap();
self.doc.check_state_correctness_slow();
}
}
}

pub fn test_multi_sites_on_one_doc(site_num: u8, actions: &mut [Action]) {
let mut fuzzer = OneDocFuzzer::new(site_num as usize);
let mut applied = Vec::new();
for action in actions.iter_mut() {
fuzzer.pre_process(action);
info_span!("ApplyAction", ?action).in_scope(|| {
applied.push(action.clone());
info!("OptionsTable \n{}", (&applied).table());
// info!("Apply Action {:?}", applied);
fuzzer.apply_action(action);
});
}

// println!("OpTable \n{}", (&applied).table());
info_span!("check synced").in_scope(|| {
fuzzer.check_sync();
});
}
Loading

0 comments on commit bef39ce

Please sign in to comment.