loro/crates/loro-internal/tests/test.rs

1242 lines
38 KiB
Rust
Raw Normal View History

use std::sync::{atomic::AtomicBool, Arc, Mutex};
feat: movable tree support (#120) * feat: tree state * feat: tree value * feat: tree handler * fix: tree diff * test: fuzz tree * feat: tree snapshot * fix: tree default value * fix: test new node * fix: tree diff * fix: tree unresolved value * fix: tree fuzz * fix: tree fuzz move * fix: sort by tree id * fix: tree diff sorted by lamport * fix: sort roots before tree converted to string * fix: rebase main * fix: tree fuzz * fix: delete undo * fix: tree to json children sorted * fix: diff calculate * fix: diff cycle move * fix: tree old parent cache * feat: cache * fix: local op add tree cache * fix: don't add same tree move to cache * fix: need update cache * feat: new cache * bench: add checkout bench * chore: clean * fix: apply node uncheck * perf: lamport bound * fix: calc old parent * feat: tree wasm * fix: change tree diff * fix: tree diff retreat * fix: tree diff should not apply when add node * feat: new tree loro value * chore: typo * fix: tree deep value * fix: snapshot tree index -1 * fix: decode tree snapshot use state * fix: release state lock when emit event * fix: tree node meta container * fix: need set map container when covert to local tree op * fix: tree value add deleted * fix: more then one op in a change * fix: tree fuzz deleted equal * fix: tree calc min lamport * feat: tree encoding v2 * doc: movable tree * fix: test tree meta * test: remove import bytes check * refactor: diff of text and map * refactor: del span * perf: tree state use deleted cache * fix: some details * fix: loro js tree create * feat: add un exist tree node * bench: tree depth * fix: check out should emit event * refactor: event * fix: fuzz err * fix: pass all tests * fix: fuzz err * fix: list child cache err * chore: rm debug code * fix: encode enhanced err * fix: encode enchanced * fix: fix several richtext issue * fix: richtext anchor err * chore: rm debug code * fix: richtext fuzz err * feat: speedup text snapshot decode * perf: optimize snapshot encoding * perf: speed up decode & insert * fix: fugue span merge err * perf: speedup delete & id cursor map * fix: fugue merge err * chore: update utils * fix: fix merge * fix: return err apply op * fix: fix merge * fix: get map container as tree meta
2023-10-30 03:13:52 +00:00
use fxhash::FxHashMap;
use loro_common::{ContainerID, ContainerType, LoroResult, LoroValue, ID};
use loro_internal::{
delta::ResolvedMapValue,
event::{Diff, EventTriggerKind},
handler::{Handler, TextDelta, ValueOrHandler},
version::Frontiers,
ApplyDiff, HandlerTrait, ListHandler, LoroDoc, MapHandler, TextHandler, ToJson, TreeHandler,
};
use serde_json::json;
#[test]
fn issue_502() -> LoroResult<()> {
let doc = LoroDoc::new_auto_commit();
doc.get_map("map").insert("stringA", "Original data")?;
doc.commit_then_renew();
doc.get_map("map").insert("stringA", "Updated data")?;
doc.attach();
doc.get_map("map").insert("stringB", "Something else")?;
doc.commit_then_renew();
Ok(())
}
#[test]
fn issue_225() -> LoroResult<()> {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert(0, "123")?;
text.mark(0, 3, "bold", true.into())?;
// when apply_delta, the attributes of insert should override the current styles
text.apply_delta(&[
TextDelta::Retain {
retain: 3,
attributes: None,
},
TextDelta::Insert {
insert: "new".into(),
attributes: None,
},
])?;
assert_eq!(
text.get_richtext_value().to_json_value(),
json!([{ "insert": "123", "attributes": { "bold": true } }, { "insert": "new" }])
);
Ok(())
}
#[test]
fn issue_211() -> LoroResult<()> {
let doc1 = LoroDoc::new_auto_commit();
let doc2 = LoroDoc::new_auto_commit();
doc1.get_text("text").insert(0, "T")?;
doc2.merge(&doc1)?;
let v0 = doc1.oplog_frontiers();
doc1.get_text("text").insert(1, "A")?;
doc2.get_text("text").insert(1, "B")?;
doc1.checkout(&v0)?;
doc2.checkout(&v0)?;
doc1.checkout_to_latest();
doc2.checkout_to_latest();
// let v1_of_doc1 = doc1.oplog_frontiers();
let v1_of_doc2 = doc2.oplog_frontiers();
doc2.get_text("text").insert(2, "B")?;
doc2.checkout(&v1_of_doc2)?;
doc2.checkout(&v0)?;
assert_eq!(
doc2.get_deep_value().to_json_value(),
json!({
"text": "T"
})
);
Ok(())
}
#[test]
fn mark_with_the_same_key_value_should_be_skipped() {
let a = LoroDoc::new_auto_commit();
let text = a.get_text("text");
text.insert(0, "Hello world!").unwrap();
text.mark(0, 11, "bold", "value".into()).unwrap();
a.commit_then_renew();
let v = a.oplog_vv();
text.mark(0, 5, "bold", "value".into()).unwrap();
a.commit_then_renew();
let new_v = a.oplog_vv();
// new mark should be ignored, so vv should be the same
assert_eq!(v, new_v);
}
#[test]
fn event_from_checkout() {
let a = LoroDoc::new_auto_commit();
let sub_id = a.subscribe_root(Arc::new(|event| {
assert!(matches!(
event.event_meta.by,
EventTriggerKind::Checkout | EventTriggerKind::Local
));
}));
2023-11-28 08:22:43 +00:00
a.get_text("text").insert(0, "hello").unwrap();
a.commit_then_renew();
let version = a.oplog_frontiers();
2023-11-28 08:22:43 +00:00
a.get_text("text").insert(0, "hello").unwrap();
a.commit_then_renew();
a.unsubscribe(sub_id);
let ran = Arc::new(AtomicBool::new(false));
let ran_cloned = ran.clone();
a.subscribe_root(Arc::new(move |event| {
assert!(event.event_meta.by.is_checkout());
ran.store(true, std::sync::atomic::Ordering::Relaxed);
}));
a.checkout(&version).unwrap();
assert!(ran_cloned.load(std::sync::atomic::Ordering::Relaxed));
}
#[test]
fn handler_in_event() {
let doc = LoroDoc::new_auto_commit();
doc.subscribe_root(Arc::new(|e| {
dbg!(&e);
let value = e.events[0]
.diff
.as_list()
.unwrap()
.iter()
.next()
.unwrap()
.as_replace()
.unwrap()
.0
.iter()
.next()
.unwrap();
assert!(matches!(value, ValueOrHandler::Handler(Handler::Text(_))));
}));
let list = doc.get_list("list");
list.insert_container(0, TextHandler::new_detached())
.unwrap();
doc.commit_then_renew();
}
2023-11-07 15:32:07 +00:00
#[test]
fn out_of_bound_test() {
let a = LoroDoc::new_auto_commit();
2023-11-28 08:22:43 +00:00
a.get_text("text").insert(0, "Hello").unwrap();
a.get_list("list").insert(0, "Hello").unwrap();
a.get_list("list").insert(1, "Hello").unwrap();
2023-11-07 15:32:07 +00:00
// expect out of bound err
2023-11-28 08:22:43 +00:00
let err = a.get_text("text").insert(6, "Hello").unwrap_err();
2023-11-07 15:32:07 +00:00
assert!(matches!(err, loro_common::LoroError::OutOfBound { .. }));
2023-11-28 08:22:43 +00:00
let err = a.get_text("text").delete(3, 5).unwrap_err();
2023-11-07 15:32:07 +00:00
assert!(matches!(err, loro_common::LoroError::OutOfBound { .. }));
let err = a.get_text("text").mark(0, 8, "h", 5.into()).unwrap_err();
2023-11-07 15:32:07 +00:00
assert!(matches!(err, loro_common::LoroError::OutOfBound { .. }));
let _err = a.get_text("text").mark(3, 0, "h", 5.into()).unwrap_err();
2023-11-28 08:22:43 +00:00
let err = a.get_list("list").insert(6, "Hello").unwrap_err();
2023-11-07 15:32:07 +00:00
assert!(matches!(err, loro_common::LoroError::OutOfBound { .. }));
2023-11-28 08:22:43 +00:00
let err = a.get_list("list").delete(3, 2).unwrap_err();
2023-11-07 15:32:07 +00:00
assert!(matches!(err, loro_common::LoroError::OutOfBound { .. }));
let err = a
.get_list("list")
.insert_container(3, MapHandler::new_detached())
2023-11-07 15:32:07 +00:00
.unwrap_err();
assert!(matches!(err, loro_common::LoroError::OutOfBound { .. }));
}
2023-11-07 11:48:16 +00:00
#[test]
fn list() {
let a = LoroDoc::new_auto_commit();
2023-11-28 08:22:43 +00:00
a.get_list("list").insert(0, "Hello").unwrap();
2023-11-07 11:48:16 +00:00
assert_eq!(a.get_list("list").get(0).unwrap(), LoroValue::from("Hello"));
let map = a
.get_list("list")
.insert_container(1, MapHandler::new_detached())
2023-11-07 11:48:16 +00:00
.unwrap();
2023-11-28 08:22:43 +00:00
map.insert("Hello", LoroValue::from("u")).unwrap();
let pos = map
.insert_container("pos", MapHandler::new_detached())
.unwrap();
2023-11-28 08:22:43 +00:00
pos.insert("x", 0).unwrap();
pos.insert("y", 100).unwrap();
2023-11-07 11:48:16 +00:00
let cid = map.id();
let id = a.get_list("list").get(1);
assert_eq!(id.as_ref().unwrap().as_container().unwrap(), &cid);
let map = a.get_map(id.unwrap().into_container().unwrap());
let new_pos = a.get_map(map.get("pos").unwrap().into_container().unwrap());
assert_eq!(
new_pos.get_deep_value().to_json_value(),
json!({
"x": 0,
"y": 100,
})
);
2023-11-07 11:48:16 +00:00
}
#[test]
fn richtext_mark_event() {
let a = LoroDoc::new_auto_commit();
a.subscribe(
&a.get_text("text").id(),
Arc::new(|e| {
let delta = e.events[0].diff.as_text().unwrap();
assert_eq!(
delta.to_json_value(),
json!([
{"insert": "He", "attributes": {"bold": true}},
{"insert": "ll", "attributes": {"bold": null}},
{"insert": "o", "attributes": {"bold": true}}
])
)
}),
);
2023-11-28 08:22:43 +00:00
a.get_text("text").insert(0, "Hello").unwrap();
a.get_text("text").mark(0, 5, "bold", true.into()).unwrap();
a.get_text("text")
.mark(2, 4, "bold", LoroValue::Null)
.unwrap();
a.commit_then_stop();
let b = LoroDoc::new_auto_commit();
b.subscribe(
&a.get_text("text").id(),
Arc::new(|e| {
let delta = e.events[0].diff.as_text().unwrap();
assert_eq!(
delta.to_json_value(),
json!([
{"insert": "He", "attributes": {"bold": true}},
{"insert": "ll", "attributes": {"bold": null}},
{"insert": "o", "attributes": {"bold": true}}
])
)
}),
);
b.merge(&a).unwrap();
}
#[test]
fn concurrent_richtext_mark_event() {
let a = LoroDoc::new_auto_commit();
let b = LoroDoc::new_auto_commit();
let c = LoroDoc::new_auto_commit();
2023-11-28 08:22:43 +00:00
a.get_text("text").insert(0, "Hello").unwrap();
b.merge(&a).unwrap();
c.merge(&a).unwrap();
b.get_text("text").mark(0, 3, "bold", true.into()).unwrap();
c.get_text("text").mark(1, 4, "link", true.into()).unwrap();
b.merge(&c).unwrap();
let sub_id = a.subscribe(
&a.get_text("text").id(),
Arc::new(|e| {
let delta = e.events[0].diff.as_text().unwrap();
assert_eq!(
delta.to_json_value(),
json!([
{"retain": 1, "attributes": {"bold": true, }},
{"retain": 2, "attributes": {"bold": true, "link": true}},
{"retain": 1, "attributes": {"link": true}},
])
)
}),
);
a.merge(&b).unwrap();
a.unsubscribe(sub_id);
let sub_id = a.subscribe(
&a.get_text("text").id(),
Arc::new(|e| {
let delta = e.events[0].diff.as_text().unwrap();
assert_eq!(
delta.to_json_value(),
json!([
{
"retain": 2,
},
{
"retain": 1,
"attributes": {"bold": null, "link": true}
}
])
)
}),
);
b.get_text("text")
.mark(2, 3, "bold", LoroValue::Null)
.unwrap();
a.merge(&b).unwrap();
a.unsubscribe(sub_id);
a.subscribe(
&a.get_text("text").id(),
Arc::new(|e| {
for container_diff in e.events {
let delta = container_diff.diff.as_text().unwrap();
assert_eq!(
delta.to_json_value(),
json!([
{
"retain": 2,
},
{
"insert": "A",
"attributes": {"bold": true, "link": true}
}
])
)
}
}),
);
2023-11-28 08:22:43 +00:00
a.get_text("text").insert(2, "A").unwrap();
a.commit_then_stop();
}
#[test]
fn insert_richtext_event() {
let a = LoroDoc::new_auto_commit();
2023-11-28 08:22:43 +00:00
a.get_text("text").insert(0, "Hello").unwrap();
a.get_text("text").mark(0, 5, "bold", true.into()).unwrap();
a.commit_then_renew();
let text = a.get_text("text");
a.subscribe(
&text.id(),
Arc::new(|e| {
let delta = e.events[0].diff.as_text().unwrap();
assert_eq!(
delta.to_json_value(),
json!([
{"retain": 5,},
{"insert": " World!", "attributes": {"bold": true}}
])
)
}),
);
2023-11-28 08:22:43 +00:00
text.insert(5, " World!").unwrap();
}
#[test]
fn import_after_init_handlers() {
let a = LoroDoc::new_auto_commit();
a.subscribe(
&ContainerID::new_root("text", ContainerType::Text),
Arc::new(|event| {
assert!(matches!(
event.events[0].diff,
loro_internal::event::Diff::Text(_)
))
}),
);
a.subscribe(
&ContainerID::new_root("map", ContainerType::Map),
Arc::new(|event| {
assert!(matches!(
event.events[0].diff,
loro_internal::event::Diff::Map(_)
))
}),
);
a.subscribe(
&ContainerID::new_root("list", ContainerType::List),
Arc::new(|event| {
assert!(matches!(
event.events[0].diff,
loro_internal::event::Diff::List(_)
))
}),
);
let b = LoroDoc::new_auto_commit();
2023-11-28 08:22:43 +00:00
b.get_list("list").insert(0, "list").unwrap();
b.get_list("list_a").insert(0, "list_a").unwrap();
b.get_text("text").insert(0, "text").unwrap();
b.get_map("map").insert("m", "map").unwrap();
a.import(&b.export_snapshot()).unwrap();
a.commit_then_renew();
}
#[test]
2023-10-31 11:02:52 +00:00
fn test_from_snapshot() {
let a = LoroDoc::new_auto_commit();
2023-11-28 08:22:43 +00:00
a.get_text("text").insert(0, "0").unwrap();
2023-10-31 11:02:52 +00:00
let snapshot = a.export_snapshot();
let c = LoroDoc::from_snapshot(&snapshot).unwrap();
assert_eq!(a.get_deep_value(), c.get_deep_value());
assert_eq!(a.oplog_frontiers(), c.oplog_frontiers());
assert_eq!(a.state_frontiers(), c.state_frontiers());
let updates = a.export_from(&Default::default());
let d = match LoroDoc::from_snapshot(&updates) {
Ok(_) => panic!(),
Err(e) => e,
};
assert!(matches!(d, loro_common::LoroError::DecodeError(..)));
}
#[test]
fn test_pending() {
let a = LoroDoc::new_auto_commit();
a.set_peer_id(0).unwrap();
2023-11-28 08:22:43 +00:00
a.get_text("text").insert(0, "0").unwrap();
let b = LoroDoc::new_auto_commit();
b.set_peer_id(1).unwrap();
b.import(&a.export_from(&Default::default())).unwrap();
2023-11-28 08:22:43 +00:00
b.get_text("text").insert(0, "1").unwrap();
let c = LoroDoc::new_auto_commit();
b.set_peer_id(2).unwrap();
c.import(&b.export_from(&Default::default())).unwrap();
2023-11-28 08:22:43 +00:00
c.get_text("text").insert(0, "2").unwrap();
// c creates a pending change for a, insert "2" cannot be merged into a yet
a.import(&c.export_from(&b.oplog_vv())).unwrap();
assert_eq!(a.get_deep_value().to_json_value(), json!({"text": "0"}));
// b does not has c's change
a.import(&b.export_from(&a.oplog_vv())).unwrap();
dbg!(&a.oplog().lock().unwrap());
assert_eq!(a.get_deep_value().to_json_value(), json!({"text": "210"}));
}
#[test]
fn test_checkout() {
let doc_0 = LoroDoc::new();
doc_0.set_peer_id(0).unwrap();
let doc_1 = LoroDoc::new();
doc_1.set_peer_id(1).unwrap();
let value: Arc<Mutex<LoroValue>> = Arc::new(Mutex::new(LoroValue::Map(Default::default())));
let root_value = value.clone();
doc_0.subscribe_root(Arc::new(move |event| {
dbg!(&event);
let mut root_value = root_value.lock().unwrap();
for container_diff in event.events {
root_value.apply(
&container_diff.path.iter().map(|x| x.1.clone()).collect(),
&[container_diff.diff.clone()],
);
}
}));
let map = doc_0.get_map("map");
doc_0
.with_txn(|txn| {
let handler =
map.insert_container_with_txn(txn, "text", TextHandler::new_detached())?;
let text = handler;
2023-11-28 08:22:43 +00:00
text.insert_with_txn(txn, 0, "123")
})
.unwrap();
let map = doc_1.get_map("map");
doc_1
2023-11-28 08:22:43 +00:00
.with_txn(|txn| map.insert_with_txn(txn, "text", LoroValue::Double(1.0)))
.unwrap();
doc_0
.import(&doc_1.export_from(&Default::default()))
.unwrap();
doc_0
.checkout(&Frontiers::from(vec![ID::new(0, 2)]))
.unwrap();
assert_eq!(&doc_0.get_deep_value(), &*value.lock().unwrap());
assert_eq!(
value.lock().unwrap().to_json_value(),
json!({
"map": {
"text": "12"
}
})
);
}
2022-11-11 03:13:35 +00:00
2023-08-30 08:41:04 +00:00
#[test]
fn import() {
let doc = LoroDoc::new();
doc.import(&[
108, 111, 114, 111, 0, 0, 10, 10, 255, 255, 68, 255, 255, 4, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 255, 255, 108, 111, 114, 111, 255, 255, 0, 255, 207, 207, 255, 255, 255, 255,
255,
])
.unwrap_or_default();
}
#[test]
fn test_timestamp() {
let doc = LoroDoc::new();
doc.set_record_timestamp(true);
let text = doc.get_text("text");
let mut txn = doc.txn().unwrap();
2023-11-28 08:22:43 +00:00
text.insert_with_txn(&mut txn, 0, "123").unwrap();
txn.commit().unwrap();
2023-08-29 02:38:48 +00:00
let op_log = &doc.oplog().lock().unwrap();
let change = op_log.get_change_at(ID::new(doc.peer_id(), 0)).unwrap();
assert!(change.timestamp() > 1690966970);
}
#[test]
fn test_text_checkout() {
let doc = LoroDoc::new();
doc.set_peer_id(1).unwrap();
let text = doc.get_text("text");
let mut txn = doc.txn().unwrap();
2023-11-28 08:22:43 +00:00
text.insert_with_txn(&mut txn, 0, "你界").unwrap();
text.insert_with_txn(&mut txn, 1, "好世").unwrap();
txn.commit().unwrap();
{
doc.checkout(&Frontiers::from([ID::new(doc.peer_id(), 0)].as_slice()))
.unwrap();
assert_eq!(text.get_value().as_string().unwrap().as_str(), "");
}
{
doc.checkout(&Frontiers::from([ID::new(doc.peer_id(), 1)].as_slice()))
.unwrap();
assert_eq!(text.get_value().as_string().unwrap().as_str(), "你界");
}
{
doc.checkout(&Frontiers::from([ID::new(doc.peer_id(), 2)].as_slice()))
.unwrap();
assert_eq!(text.get_value().as_string().unwrap().as_str(), "你好界");
}
{
doc.checkout(&Frontiers::from([ID::new(doc.peer_id(), 3)].as_slice()))
.unwrap();
assert_eq!(text.get_value().as_string().unwrap().as_str(), "你好世界");
}
assert_eq!(text.len_unicode(), 4);
assert_eq!(text.len_utf8(), 12);
assert_eq!(text.len_unicode(), 4);
doc.checkout_to_latest();
2023-11-28 08:22:43 +00:00
doc.with_txn(|txn| text.delete_with_txn(txn, 3, 1)).unwrap();
assert_eq!(text.get_value().as_string().unwrap().as_str(), "你好世");
2023-11-28 08:22:43 +00:00
doc.with_txn(|txn| text.delete_with_txn(txn, 2, 1)).unwrap();
assert_eq!(text.get_value().as_string().unwrap().as_str(), "你好");
doc.checkout(&Frontiers::from([ID::new(doc.peer_id(), 3)].as_slice()))
.unwrap();
assert_eq!(text.get_value().as_string().unwrap().as_str(), "你好世界");
doc.checkout(&Frontiers::from([ID::new(doc.peer_id(), 4)].as_slice()))
.unwrap();
assert_eq!(text.get_value().as_string().unwrap().as_str(), "你好世");
doc.checkout(&Frontiers::from([ID::new(doc.peer_id(), 5)].as_slice()))
.unwrap();
assert_eq!(text.get_value().as_string().unwrap().as_str(), "你好");
{
doc.checkout(&Frontiers::from([ID::new(doc.peer_id(), 0)].as_slice()))
.unwrap();
assert_eq!(text.get_value().as_string().unwrap().as_str(), "");
}
{
doc.checkout(&Frontiers::from([ID::new(doc.peer_id(), 1)].as_slice()))
.unwrap();
assert_eq!(text.get_value().as_string().unwrap().as_str(), "你界");
}
{
doc.checkout(&Frontiers::from([ID::new(doc.peer_id(), 2)].as_slice()))
.unwrap();
assert_eq!(text.get_value().as_string().unwrap().as_str(), "你好界");
}
{
doc.checkout(&Frontiers::from([ID::new(doc.peer_id(), 3)].as_slice()))
.unwrap();
assert_eq!(text.get_value().as_string().unwrap().as_str(), "你好世界");
}
}
2023-08-04 14:41:02 +00:00
#[test]
fn map_checkout() {
let doc = LoroDoc::new();
2023-08-04 14:41:02 +00:00
let meta = doc.get_map("meta");
let v_empty = doc.oplog_frontiers();
doc.with_txn(|txn| {
2023-11-28 08:22:43 +00:00
meta.insert_with_txn(txn, "key", 0.into()).unwrap();
Ok(())
2023-08-04 14:41:02 +00:00
})
.unwrap();
let v0 = doc.oplog_frontiers();
doc.with_txn(|txn| {
2023-11-28 08:22:43 +00:00
meta.insert_with_txn(txn, "key", 1.into()).unwrap();
Ok(())
2023-08-04 14:41:02 +00:00
})
.unwrap();
let v1 = doc.oplog_frontiers();
assert_eq!(meta.get_deep_value().to_json(), r#"{"key":1}"#);
doc.checkout(&v0).unwrap();
2023-08-04 14:41:02 +00:00
assert_eq!(meta.get_deep_value().to_json(), r#"{"key":0}"#);
doc.checkout(&v_empty).unwrap();
2023-08-04 14:41:02 +00:00
assert_eq!(meta.get_deep_value().to_json(), r#"{}"#);
doc.checkout(&v1).unwrap();
2023-08-04 14:41:02 +00:00
assert_eq!(meta.get_deep_value().to_json(), r#"{"key":1}"#);
}
#[test]
fn a_list_of_map_checkout() {
let doc = LoroDoc::new();
let entry = doc.get_map("entry");
let (list, sub) = doc
.with_txn(|txn| {
let list = entry.insert_container_with_txn(txn, "list", ListHandler::new_detached())?;
let sub_map = list.insert_container_with_txn(txn, 0, MapHandler::new_detached())?;
2023-11-28 08:22:43 +00:00
sub_map.insert_with_txn(txn, "x", 100.into())?;
sub_map.insert_with_txn(txn, "y", 1000.into())?;
Ok((list, sub_map))
})
.unwrap();
let v0 = doc.oplog_frontiers();
let d0 = doc.get_deep_value().to_json();
doc.with_txn(|txn| {
2023-11-28 08:22:43 +00:00
list.insert_with_txn(txn, 0, 3.into())?;
list.push_with_txn(txn, 4.into())?;
list.insert_container_with_txn(txn, 2, MapHandler::new_detached())?;
list.insert_container_with_txn(txn, 3, TextHandler::new_detached())?;
Ok(())
})
.unwrap();
doc.with_txn(|txn| {
2023-11-28 08:22:43 +00:00
list.delete_with_txn(txn, 2, 1)?;
Ok(())
})
.unwrap();
doc.with_txn(|txn| {
2023-11-28 08:22:43 +00:00
sub.insert_with_txn(txn, "x", 9.into())?;
sub.insert_with_txn(txn, "y", 9.into())?;
Ok(())
})
.unwrap();
doc.with_txn(|txn| {
2023-11-28 08:22:43 +00:00
sub.insert_with_txn(txn, "z", 9.into())?;
Ok(())
})
.unwrap();
let v1 = doc.oplog_frontiers();
let d1 = doc.get_deep_value().to_json();
doc.with_txn(|txn| {
2023-11-28 08:22:43 +00:00
sub.insert_with_txn(txn, "x", 77.into())?;
Ok(())
})
.unwrap();
doc.with_txn(|txn| {
2023-11-28 08:22:43 +00:00
sub.insert_with_txn(txn, "y", 88.into())?;
Ok(())
})
.unwrap();
doc.with_txn(|txn| {
2023-11-28 08:22:43 +00:00
list.delete_with_txn(txn, 0, 1)?;
list.insert_with_txn(txn, 0, 123.into())?;
list.push_with_txn(txn, 99.into())?;
Ok(())
})
.unwrap();
let v2 = doc.oplog_frontiers();
let d2 = doc.get_deep_value().to_json();
doc.checkout(&v0).unwrap();
assert_eq!(doc.get_deep_value().to_json(), d0);
doc.checkout(&v1).unwrap();
assert_eq!(doc.get_deep_value().to_json(), d1);
doc.checkout(&v2).unwrap();
println!("{}", doc.get_deep_value_with_id().to_json_pretty());
assert_eq!(doc.get_deep_value().to_json(), d2);
doc.checkout(&v1).unwrap();
println!("{}", doc.get_deep_value_with_id().to_json_pretty());
assert_eq!(doc.get_deep_value().to_json(), d1);
doc.checkout(&v0).unwrap();
assert_eq!(doc.get_deep_value().to_json(), d0);
}
2023-08-04 14:41:02 +00:00
#[test]
fn map_concurrent_checkout() {
let doc_a = LoroDoc::new();
2023-08-04 14:41:02 +00:00
let meta_a = doc_a.get_map("meta");
let doc_b = LoroDoc::new();
let meta_b = doc_b.get_map("meta");
doc_a
.with_txn(|txn| {
2023-11-28 08:22:43 +00:00
meta_a.insert_with_txn(txn, "key", 0.into()).unwrap();
Ok(())
2023-08-04 14:41:02 +00:00
})
.unwrap();
let va = doc_a.oplog_frontiers();
doc_b
.with_txn(|txn| {
2023-11-28 08:22:43 +00:00
meta_b.insert_with_txn(txn, "s", 1.into()).unwrap();
Ok(())
2023-08-04 14:41:02 +00:00
})
.unwrap();
let vb_0 = doc_b.oplog_frontiers();
doc_b
.with_txn(|txn| {
2023-11-28 08:22:43 +00:00
meta_b.insert_with_txn(txn, "key", 1.into()).unwrap();
Ok(())
2023-08-04 14:41:02 +00:00
})
.unwrap();
let vb_1 = doc_b.oplog_frontiers();
doc_a.import(&doc_b.export_snapshot()).unwrap();
doc_a
.with_txn(|txn| {
2023-11-28 08:22:43 +00:00
meta_a.insert_with_txn(txn, "key", 2.into()).unwrap();
Ok(())
2023-08-04 14:41:02 +00:00
})
.unwrap();
let v_merged = doc_a.oplog_frontiers();
doc_a.checkout(&va).unwrap();
2023-08-04 14:41:02 +00:00
assert_eq!(meta_a.get_deep_value().to_json(), r#"{"key":0}"#);
doc_a.checkout(&vb_0).unwrap();
2023-08-04 14:41:02 +00:00
assert_eq!(meta_a.get_deep_value().to_json(), r#"{"s":1}"#);
doc_a.checkout(&vb_1).unwrap();
2023-08-04 14:41:02 +00:00
assert_eq!(meta_a.get_deep_value().to_json(), r#"{"s":1,"key":1}"#);
doc_a.checkout(&v_merged).unwrap();
2023-08-04 14:41:02 +00:00
assert_eq!(meta_a.get_deep_value().to_json(), r#"{"s":1,"key":2}"#);
}
feat: movable tree support (#120) * feat: tree state * feat: tree value * feat: tree handler * fix: tree diff * test: fuzz tree * feat: tree snapshot * fix: tree default value * fix: test new node * fix: tree diff * fix: tree unresolved value * fix: tree fuzz * fix: tree fuzz move * fix: sort by tree id * fix: tree diff sorted by lamport * fix: sort roots before tree converted to string * fix: rebase main * fix: tree fuzz * fix: delete undo * fix: tree to json children sorted * fix: diff calculate * fix: diff cycle move * fix: tree old parent cache * feat: cache * fix: local op add tree cache * fix: don't add same tree move to cache * fix: need update cache * feat: new cache * bench: add checkout bench * chore: clean * fix: apply node uncheck * perf: lamport bound * fix: calc old parent * feat: tree wasm * fix: change tree diff * fix: tree diff retreat * fix: tree diff should not apply when add node * feat: new tree loro value * chore: typo * fix: tree deep value * fix: snapshot tree index -1 * fix: decode tree snapshot use state * fix: release state lock when emit event * fix: tree node meta container * fix: need set map container when covert to local tree op * fix: tree value add deleted * fix: more then one op in a change * fix: tree fuzz deleted equal * fix: tree calc min lamport * feat: tree encoding v2 * doc: movable tree * fix: test tree meta * test: remove import bytes check * refactor: diff of text and map * refactor: del span * perf: tree state use deleted cache * fix: some details * fix: loro js tree create * feat: add un exist tree node * bench: tree depth * fix: check out should emit event * refactor: event * fix: fuzz err * fix: pass all tests * fix: fuzz err * fix: list child cache err * chore: rm debug code * fix: encode enhanced err * fix: encode enchanced * fix: fix several richtext issue * fix: richtext anchor err * chore: rm debug code * fix: richtext fuzz err * feat: speedup text snapshot decode * perf: optimize snapshot encoding * perf: speed up decode & insert * fix: fugue span merge err * perf: speedup delete & id cursor map * fix: fugue merge err * chore: update utils * fix: fix merge * fix: return err apply op * fix: fix merge * fix: get map container as tree meta
2023-10-30 03:13:52 +00:00
#[test]
fn tree_checkout() {
let doc_a = LoroDoc::new_auto_commit();
doc_a.subscribe_root(Arc::new(|_e| {}));
doc_a.set_peer_id(1).unwrap();
feat: movable tree support (#120) * feat: tree state * feat: tree value * feat: tree handler * fix: tree diff * test: fuzz tree * feat: tree snapshot * fix: tree default value * fix: test new node * fix: tree diff * fix: tree unresolved value * fix: tree fuzz * fix: tree fuzz move * fix: sort by tree id * fix: tree diff sorted by lamport * fix: sort roots before tree converted to string * fix: rebase main * fix: tree fuzz * fix: delete undo * fix: tree to json children sorted * fix: diff calculate * fix: diff cycle move * fix: tree old parent cache * feat: cache * fix: local op add tree cache * fix: don't add same tree move to cache * fix: need update cache * feat: new cache * bench: add checkout bench * chore: clean * fix: apply node uncheck * perf: lamport bound * fix: calc old parent * feat: tree wasm * fix: change tree diff * fix: tree diff retreat * fix: tree diff should not apply when add node * feat: new tree loro value * chore: typo * fix: tree deep value * fix: snapshot tree index -1 * fix: decode tree snapshot use state * fix: release state lock when emit event * fix: tree node meta container * fix: need set map container when covert to local tree op * fix: tree value add deleted * fix: more then one op in a change * fix: tree fuzz deleted equal * fix: tree calc min lamport * feat: tree encoding v2 * doc: movable tree * fix: test tree meta * test: remove import bytes check * refactor: diff of text and map * refactor: del span * perf: tree state use deleted cache * fix: some details * fix: loro js tree create * feat: add un exist tree node * bench: tree depth * fix: check out should emit event * refactor: event * fix: fuzz err * fix: pass all tests * fix: fuzz err * fix: list child cache err * chore: rm debug code * fix: encode enhanced err * fix: encode enchanced * fix: fix several richtext issue * fix: richtext anchor err * chore: rm debug code * fix: richtext fuzz err * feat: speedup text snapshot decode * perf: optimize snapshot encoding * perf: speed up decode & insert * fix: fugue span merge err * perf: speedup delete & id cursor map * fix: fugue merge err * chore: update utils * fix: fix merge * fix: return err apply op * fix: fix merge * fix: get map container as tree meta
2023-10-30 03:13:52 +00:00
let tree = doc_a.get_tree("root");
let id1 = tree.create(None).unwrap();
let id2 = tree.create(id1).unwrap();
feat: movable tree support (#120) * feat: tree state * feat: tree value * feat: tree handler * fix: tree diff * test: fuzz tree * feat: tree snapshot * fix: tree default value * fix: test new node * fix: tree diff * fix: tree unresolved value * fix: tree fuzz * fix: tree fuzz move * fix: sort by tree id * fix: tree diff sorted by lamport * fix: sort roots before tree converted to string * fix: rebase main * fix: tree fuzz * fix: delete undo * fix: tree to json children sorted * fix: diff calculate * fix: diff cycle move * fix: tree old parent cache * feat: cache * fix: local op add tree cache * fix: don't add same tree move to cache * fix: need update cache * feat: new cache * bench: add checkout bench * chore: clean * fix: apply node uncheck * perf: lamport bound * fix: calc old parent * feat: tree wasm * fix: change tree diff * fix: tree diff retreat * fix: tree diff should not apply when add node * feat: new tree loro value * chore: typo * fix: tree deep value * fix: snapshot tree index -1 * fix: decode tree snapshot use state * fix: release state lock when emit event * fix: tree node meta container * fix: need set map container when covert to local tree op * fix: tree value add deleted * fix: more then one op in a change * fix: tree fuzz deleted equal * fix: tree calc min lamport * feat: tree encoding v2 * doc: movable tree * fix: test tree meta * test: remove import bytes check * refactor: diff of text and map * refactor: del span * perf: tree state use deleted cache * fix: some details * fix: loro js tree create * feat: add un exist tree node * bench: tree depth * fix: check out should emit event * refactor: event * fix: fuzz err * fix: pass all tests * fix: fuzz err * fix: list child cache err * chore: rm debug code * fix: encode enhanced err * fix: encode enchanced * fix: fix several richtext issue * fix: richtext anchor err * chore: rm debug code * fix: richtext fuzz err * feat: speedup text snapshot decode * perf: optimize snapshot encoding * perf: speed up decode & insert * fix: fugue span merge err * perf: speedup delete & id cursor map * fix: fugue merge err * chore: update utils * fix: fix merge * fix: return err apply op * fix: fix merge * fix: get map container as tree meta
2023-10-30 03:13:52 +00:00
let v1_state = tree.get_deep_value();
let v1 = doc_a.oplog_frontiers();
let _id3 = tree.create(id2).unwrap();
feat: movable tree support (#120) * feat: tree state * feat: tree value * feat: tree handler * fix: tree diff * test: fuzz tree * feat: tree snapshot * fix: tree default value * fix: test new node * fix: tree diff * fix: tree unresolved value * fix: tree fuzz * fix: tree fuzz move * fix: sort by tree id * fix: tree diff sorted by lamport * fix: sort roots before tree converted to string * fix: rebase main * fix: tree fuzz * fix: delete undo * fix: tree to json children sorted * fix: diff calculate * fix: diff cycle move * fix: tree old parent cache * feat: cache * fix: local op add tree cache * fix: don't add same tree move to cache * fix: need update cache * feat: new cache * bench: add checkout bench * chore: clean * fix: apply node uncheck * perf: lamport bound * fix: calc old parent * feat: tree wasm * fix: change tree diff * fix: tree diff retreat * fix: tree diff should not apply when add node * feat: new tree loro value * chore: typo * fix: tree deep value * fix: snapshot tree index -1 * fix: decode tree snapshot use state * fix: release state lock when emit event * fix: tree node meta container * fix: need set map container when covert to local tree op * fix: tree value add deleted * fix: more then one op in a change * fix: tree fuzz deleted equal * fix: tree calc min lamport * feat: tree encoding v2 * doc: movable tree * fix: test tree meta * test: remove import bytes check * refactor: diff of text and map * refactor: del span * perf: tree state use deleted cache * fix: some details * fix: loro js tree create * feat: add un exist tree node * bench: tree depth * fix: check out should emit event * refactor: event * fix: fuzz err * fix: pass all tests * fix: fuzz err * fix: list child cache err * chore: rm debug code * fix: encode enhanced err * fix: encode enchanced * fix: fix several richtext issue * fix: richtext anchor err * chore: rm debug code * fix: richtext fuzz err * feat: speedup text snapshot decode * perf: optimize snapshot encoding * perf: speed up decode & insert * fix: fugue span merge err * perf: speedup delete & id cursor map * fix: fugue merge err * chore: update utils * fix: fix merge * fix: return err apply op * fix: fix merge * fix: get map container as tree meta
2023-10-30 03:13:52 +00:00
let v2_state = tree.get_deep_value();
let v2 = doc_a.oplog_frontiers();
tree.delete(id2).unwrap();
feat: movable tree support (#120) * feat: tree state * feat: tree value * feat: tree handler * fix: tree diff * test: fuzz tree * feat: tree snapshot * fix: tree default value * fix: test new node * fix: tree diff * fix: tree unresolved value * fix: tree fuzz * fix: tree fuzz move * fix: sort by tree id * fix: tree diff sorted by lamport * fix: sort roots before tree converted to string * fix: rebase main * fix: tree fuzz * fix: delete undo * fix: tree to json children sorted * fix: diff calculate * fix: diff cycle move * fix: tree old parent cache * feat: cache * fix: local op add tree cache * fix: don't add same tree move to cache * fix: need update cache * feat: new cache * bench: add checkout bench * chore: clean * fix: apply node uncheck * perf: lamport bound * fix: calc old parent * feat: tree wasm * fix: change tree diff * fix: tree diff retreat * fix: tree diff should not apply when add node * feat: new tree loro value * chore: typo * fix: tree deep value * fix: snapshot tree index -1 * fix: decode tree snapshot use state * fix: release state lock when emit event * fix: tree node meta container * fix: need set map container when covert to local tree op * fix: tree value add deleted * fix: more then one op in a change * fix: tree fuzz deleted equal * fix: tree calc min lamport * feat: tree encoding v2 * doc: movable tree * fix: test tree meta * test: remove import bytes check * refactor: diff of text and map * refactor: del span * perf: tree state use deleted cache * fix: some details * fix: loro js tree create * feat: add un exist tree node * bench: tree depth * fix: check out should emit event * refactor: event * fix: fuzz err * fix: pass all tests * fix: fuzz err * fix: list child cache err * chore: rm debug code * fix: encode enhanced err * fix: encode enchanced * fix: fix several richtext issue * fix: richtext anchor err * chore: rm debug code * fix: richtext fuzz err * feat: speedup text snapshot decode * perf: optimize snapshot encoding * perf: speed up decode & insert * fix: fugue span merge err * perf: speedup delete & id cursor map * fix: fugue merge err * chore: update utils * fix: fix merge * fix: return err apply op * fix: fix merge * fix: get map container as tree meta
2023-10-30 03:13:52 +00:00
let v3_state = tree.get_deep_value();
let v3 = doc_a.oplog_frontiers();
doc_a.checkout(&v1).unwrap();
assert_eq!(
serde_json::to_value(tree.get_deep_value())
.unwrap()
.get("roots"),
serde_json::to_value(v1_state).unwrap().get("roots")
);
doc_a.checkout(&v2).unwrap();
assert_eq!(
serde_json::to_value(tree.get_deep_value())
.unwrap()
.get("roots"),
serde_json::to_value(v2_state).unwrap().get("roots")
);
doc_a.checkout(&v3).unwrap();
assert_eq!(
serde_json::to_value(tree.get_deep_value())
.unwrap()
.get("roots"),
serde_json::to_value(v3_state).unwrap().get("roots")
);
doc_a.attach();
tree.create(None).unwrap();
feat: movable tree support (#120) * feat: tree state * feat: tree value * feat: tree handler * fix: tree diff * test: fuzz tree * feat: tree snapshot * fix: tree default value * fix: test new node * fix: tree diff * fix: tree unresolved value * fix: tree fuzz * fix: tree fuzz move * fix: sort by tree id * fix: tree diff sorted by lamport * fix: sort roots before tree converted to string * fix: rebase main * fix: tree fuzz * fix: delete undo * fix: tree to json children sorted * fix: diff calculate * fix: diff cycle move * fix: tree old parent cache * feat: cache * fix: local op add tree cache * fix: don't add same tree move to cache * fix: need update cache * feat: new cache * bench: add checkout bench * chore: clean * fix: apply node uncheck * perf: lamport bound * fix: calc old parent * feat: tree wasm * fix: change tree diff * fix: tree diff retreat * fix: tree diff should not apply when add node * feat: new tree loro value * chore: typo * fix: tree deep value * fix: snapshot tree index -1 * fix: decode tree snapshot use state * fix: release state lock when emit event * fix: tree node meta container * fix: need set map container when covert to local tree op * fix: tree value add deleted * fix: more then one op in a change * fix: tree fuzz deleted equal * fix: tree calc min lamport * feat: tree encoding v2 * doc: movable tree * fix: test tree meta * test: remove import bytes check * refactor: diff of text and map * refactor: del span * perf: tree state use deleted cache * fix: some details * fix: loro js tree create * feat: add un exist tree node * bench: tree depth * fix: check out should emit event * refactor: event * fix: fuzz err * fix: pass all tests * fix: fuzz err * fix: list child cache err * chore: rm debug code * fix: encode enhanced err * fix: encode enchanced * fix: fix several richtext issue * fix: richtext anchor err * chore: rm debug code * fix: richtext fuzz err * feat: speedup text snapshot decode * perf: optimize snapshot encoding * perf: speed up decode & insert * fix: fugue span merge err * perf: speedup delete & id cursor map * fix: fugue merge err * chore: update utils * fix: fix merge * fix: return err apply op * fix: fix merge * fix: get map container as tree meta
2023-10-30 03:13:52 +00:00
}
2024-02-17 02:50:00 +00:00
#[test]
fn issue_batch_import_snapshot() {
let doc = LoroDoc::new_auto_commit();
doc.set_peer_id(123).unwrap();
let doc2 = LoroDoc::new_auto_commit();
doc2.set_peer_id(456).unwrap();
doc.get_map("map").insert("s", "hello world!").unwrap();
doc2.get_map("map").insert("s", "hello?").unwrap();
let data1 = doc.export_snapshot();
let data2 = doc2.export_snapshot();
let doc3 = LoroDoc::new();
doc3.import_batch(&[data1, data2]).unwrap();
}
#[test]
fn state_may_deadlock_when_import() {
// helper function ref: https://github.com/rust-lang/rfcs/issues/2798#issuecomment-552949300
use std::time::Duration;
use std::{sync::mpsc, thread};
fn panic_after<T, F>(d: Duration, f: F) -> T
where
T: Send + 'static,
F: FnOnce() -> T,
F: Send + 'static,
{
let (done_tx, done_rx) = mpsc::channel();
let handle = thread::spawn(move || {
let val = f();
done_tx.send(()).expect("Unable to send completion signal");
val
});
match done_rx.recv_timeout(d) {
Ok(_) => handle.join().expect("Thread panicked"),
Err(_) => panic!("Thread took too long"),
}
}
panic_after(Duration::from_millis(100), || {
let doc = LoroDoc::new_auto_commit();
let map = doc.get_map("map");
doc.subscribe_root(Arc::new(move |_e| {
map.id();
}));
let doc2 = LoroDoc::new_auto_commit();
doc2.get_map("map").insert("foo", 123).unwrap();
doc.import(&doc.export_snapshot()).unwrap();
})
}
Movable List (#293) * bk: add move op content * bk: add inner_movable_list diff and related stuff * perf: high perf state * fix: update old list item cache * fix: should use id in del * feat: two kinds of len for movable list state * bk: add op index to movable list * bk: make basic handler test pass * refactor: add move_from to list event * fix: make all existing tests pass * bk: list move event hint into event * bk: convert inner event into user event Co-authored-by: Leon <leeeon233@gmail.com> * fix: convert issue when inserting new value * feat: add op group for movable list * feat: diff calc * feat: add mov support to tracker * fix: when applying diff, state should be force update * feat: encoded op * feat: snapshot encode * fix: pass basic sync * fix: snapshot encode/decode * fix: warnings * feat: expose mov list to loro crate * test: fuzz movable list * test: fix fuzz integration * fix: movable list basic move sync * fix: movable list events * fix: movable event err * fix: register child container on movable list * fix: should not return child index if the value is already overwritten * fix: local event err in movable list * fix: get elem at pos * refactor: extract mut op that could break invariants * fix: event err * fix: child container to elem err * fix: bringback event issue * fix: event err * fix: event emit * fix: id to cursor iter issue * chore: fix a few warnings * fix: warnings * fix: fix move in tracker * test: add consistency check * test: fix tracker * refactor: simplify event conversion in docstate * refactor: refine move event * refactor: simplify the maintain of parent child links * fix: revive err * fix: warnings * fix: it's possible that pos change but cannot find the respective list item * fix: elem may be dropped after snapshot * fix: warnings * fix: richtext time travel issue * fix: move op used wrong delete id on tracker * fix: handle events created by concurrent moves correctly * fix: event hint error, used op index for list event * fix: move_from flag err * fix: id to cursor get err * test: add mov fuzz target * fix: the pos of inserting new container * fix: used wrong event hint index * fix: del event hint * fix: warnings * fix: internal diff to event err * fix: event's move flag error This "move" flag does not actually mean that the insertion is caused by the move op. 就算是 move 造成的它不一定就能是 true 它得是下游真的能在“前一个版本的 array 里找到“,才能是 true * fix: remove redundant elements from the movable list The Movable List is currently flawed; an element may not exist on the movable list state, yet there are operations that revive its corresponding list item. In such cases, the diff calculation does not send back the corresponding element state (this occurs when tracing back, which fuzz testing currently does not cover. It might only be exposed by randomly switching to a version and then checking for consistency; otherwise, as long as all elements are in memory, this problem does not arise). Moreover, there is no need to store elements in the state that do not have a corresponding list item. They will be deleted during the Snapshot, and relying on "them still being in the state" is incorrect behavior. Such adjustments also eliminate the need to maintain the `pending_elements` field. By allowing the opgroup to record the mapping from pos id to state id, we can ensure that the events sent to the movable list state will include the corresponding state. Movable List 现在是有错的,elem 可能不存在 movable list state 上,但是又有操作把它对应的 list item 复活了,此时 diff calc 不会把对应 element 状态发送回来(往前回溯的时候会出现,fuzz 现在没覆盖到。得有随意切换一个版本然后 check consistency 才可能会暴露;否则现在大家 elements 都在内存,就没这个问题) 而且我们没有必要在状态中存储没有对应 list item 的 element。在 Snapshot 的时候它们都会被删掉,如果依赖了“它们还会在 state 内”就是错误的行为。这样的调整也让我们不需要去维护 pending_elements 这个 field 了 通过让 opgroup 记录了 pos id → state id 的映射,可以保证发给 movable list state 的事件中会带上对应的 state * test: make fuzzer stricter * test: test expectation error * refactor: rename stable pos to cursor * tests: chore list bench init * test: add bench * bench: add mov & set bench * feat(wasm): movable list js api * fix: make movablelist able to attach even if it's already attached & refine the type of subscribe * fix: remove the loro doc param in .unsub * refactor: refine ts types and export setContainer api * chore: fix warnings * chore: rm debug logs * perf: reduce mem usage of opgroup * bench: add list criterion bench --------- Co-authored-by: Leon <leeeon233@gmail.com>
2024-04-26 04:08:53 +00:00
#[ctor::ctor]
fn init() {
dev_utils::setup_test_log();
}
#[test]
fn missing_event_when_checkout() {
let doc = LoroDoc::new_auto_commit();
doc.checkout(&doc.oplog_frontiers()).unwrap();
let value = Arc::new(Mutex::new(FxHashMap::default()));
let map = value.clone();
doc.subscribe(
&ContainerID::new_root("tree", ContainerType::Tree),
Arc::new(move |e| {
let mut v = map.lock().unwrap();
for container_diff in e.events.iter() {
let from_children =
container_diff.id != ContainerID::new_root("tree", ContainerType::Tree);
if from_children {
if let Diff::Map(map) = &container_diff.diff {
for (k, ResolvedMapValue { value, .. }) in map.updated.iter() {
match value {
Some(value) => {
v.insert(
k.to_string(),
*value.as_value().unwrap().as_i64().unwrap(),
);
}
None => {
v.remove(&k.to_string());
}
}
}
}
}
}
}),
);
let doc2 = LoroDoc::new_auto_commit();
let tree = doc2.get_tree("tree");
let node = tree.create_at(None, 0).unwrap();
let _ = tree.create_at(None, 0).unwrap();
let meta = tree.get_meta(node).unwrap();
meta.insert("a", 0).unwrap();
doc.import(&doc2.export_from(&doc.oplog_vv())).unwrap();
doc.attach();
meta.insert("b", 1).unwrap();
doc.checkout(&doc.oplog_frontiers()).unwrap();
doc.import(&doc2.export_from(&doc.oplog_vv())).unwrap();
// checkout use the same diff_calculator, the depth of calculator is not updated
doc.attach();
assert!(value.lock().unwrap().contains_key("b"));
}
2024-03-14 12:46:18 +00:00
#[test]
fn empty_event() {
let doc = LoroDoc::new_auto_commit();
doc.get_map("map").insert("key", 123).unwrap();
doc.commit_then_renew();
let fire = Arc::new(AtomicBool::new(false));
let fire_clone = Arc::clone(&fire);
doc.subscribe_root(Arc::new(move |_e| {
fire_clone.store(true, std::sync::atomic::Ordering::Relaxed);
}));
doc.import(&doc.export_snapshot()).unwrap();
assert!(!fire.load(std::sync::atomic::Ordering::Relaxed));
}
#[test]
fn insert_attach_container() -> LoroResult<()> {
let doc = LoroDoc::new_auto_commit();
let list = doc.get_list("list");
list.insert_container(0, MapHandler::new_detached())?
.insert("key", 1)?;
list.insert_container(1, MapHandler::new_detached())?
.insert("key", 2)?;
let elem = list.insert_container(2, MapHandler::new_detached())?;
elem.insert("key", 3)?;
let new_map = list.insert_container(0, elem)?;
assert_eq!(new_map.get_value().to_json_value(), json!({"key": 3}));
list.delete(3, 1)?;
let elem = list.insert_container(0, TextHandler::new_detached())?;
elem.insert(0, "abc")?;
elem.mark(0, 2, "bold", true.into())?;
let new_text = list.insert_container(0, elem)?;
assert_eq!(
new_text.get_richtext_value().to_json_value(),
json!([{"insert":"ab", "attributes": {"bold": true}}, {"insert":"c"}])
);
let elem = list.insert_container(0, ListHandler::new_detached())?;
elem.insert(0, "list")?;
let new_list = list.insert_container(0, elem)?;
new_list.insert(0, "new_list")?;
assert_eq!(
new_list.get_value().to_json_value(),
json!(["new_list", "list"])
);
// let elem = list.insert_container(2, TreeHandler::new_detached())?;
// let p = elem.create(None)?;
// elem.create(p)?;
// list.insert_container(0, elem)?;
assert_eq!(
doc.get_deep_value().to_json_value(),
json!({
"list": [["new_list", "list"], ["list"],"abc", "abc", {"key": 3}, {"key": 1}, {"key": 2}]
})
);
Ok(())
}
#[test]
fn tree_attach() {
let tree = TreeHandler::new_detached();
let id = tree.create(None).unwrap();
tree.get_meta(id).unwrap().insert("key", "value").unwrap();
let doc = LoroDoc::new_auto_commit();
doc.get_list("list").insert_container(0, tree).unwrap();
let v = doc.get_deep_value();
assert_eq!(
v.as_map().unwrap().get("list").unwrap().as_list().unwrap()[0]
.as_list()
.unwrap()[0]
.as_map()
.unwrap()
.get("meta")
.unwrap()
.to_json_value(),
json!({"key":"value"})
)
}
#[test]
#[cfg(feature = "counter")]
fn counter() {
let doc = LoroDoc::new_auto_commit();
let counter = doc.get_counter("counter");
counter.increment(1.).unwrap();
counter.increment(2.).unwrap();
counter.decrement(1.).unwrap();
2024-06-13 07:11:29 +00:00
let json = doc.export_json_updates(&Default::default(), &doc.oplog_vv());
let doc2 = LoroDoc::new_auto_commit();
doc2.import_json_updates(json).unwrap();
}
#[test]
fn test_insert_utf8() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert_utf8(0, "Hello ").unwrap();
text.insert_utf8(6, "World").unwrap();
assert_eq!(
text.get_richtext_value().to_json_value(),
json!([{"insert":"Hello World"}])
)
}
#[test]
fn test_insert_utf8_cross_unicode_1() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert_utf8(0, "你好").unwrap();
text.insert_utf8(3, "World").unwrap();
assert_eq!(
text.get_richtext_value().to_json_value(),
json!([{"insert":"你World好"}])
)
}
#[test]
fn test_insert_utf8_cross_unicode_2() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert_utf8(0, "你好").unwrap();
text.insert_utf8(6, "World").unwrap();
assert_eq!(
text.get_richtext_value().to_json_value(),
json!([{"insert":"你好World"}])
)
}
#[test]
fn test_insert_utf8_detached() {
let text = TextHandler::new_detached();
text.insert_utf8(0, "Hello ").unwrap();
text.insert_utf8(6, "World").unwrap();
assert_eq!(
text.get_richtext_value().to_json_value(),
json!([{"insert":"Hello World"}])
)
}
#[test]
#[should_panic]
fn test_insert_utf8_panic_cross_unicode() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert_utf8(0, "你好").unwrap();
text.insert_utf8(1, "World").unwrap();
}
#[test]
#[should_panic]
fn test_insert_utf8_panic_out_bound() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert_utf8(0, "Hello ").unwrap();
text.insert_utf8(7, "World").unwrap();
}
// println!("{}", text.get_richtext_value().to_json_value().to_string());
#[test]
fn test_delete_utf8() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert_utf8(0, "Hello").unwrap();
text.delete_utf8(1, 3).unwrap();
assert_eq!(
text.get_richtext_value().to_json_value(),
json!([{"insert":"Ho"}])
)
}
#[test]
fn test_delete_utf8_with_zero_len() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert_utf8(0, "Hello").unwrap();
text.delete_utf8(1, 0).unwrap();
assert_eq!(
text.get_richtext_value().to_json_value(),
json!([{"insert":"Hello"}])
)
}
#[test]
fn test_delete_utf8_cross_unicode() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert_utf8(0, "你好").unwrap();
text.delete_utf8(0, 3).unwrap();
assert_eq!(
text.get_richtext_value().to_json_value(),
json!([{"insert":""}])
)
}
#[test]
fn test_delete_utf8_detached() {
let text = TextHandler::new_detached();
text.insert_utf8(0, "Hello").unwrap();
text.delete_utf8(1, 3).unwrap();
assert_eq!(
text.get_richtext_value().to_json_value(),
json!([{"insert":"Ho"}])
)
}
// WARNING:
// Due to the current inability to report an error on
// get_offset_and_found on BTree, this test won't be ok.
// #[test]
// #[should_panic]
// fn test_delete_utf8_panic_cross_unicode() {
// let doc = LoroDoc::new_auto_commit();
// let text = doc.get_text("text");
// text.insert_utf8(0, "你好").unwrap();
// text.delete_utf8(0, 2).unwrap();
// }
#[test]
#[should_panic]
fn test_delete_utf8_panic_out_bound_pos() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert(0, "Hello").unwrap();
text.delete_utf8(10, 1).unwrap();
}
#[test]
#[should_panic]
fn test_delete_utf8_panic_out_bound_len() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert(0, "Hello").unwrap();
text.delete_utf8(1, 10).unwrap();
}
#[test]
fn test_char_at() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert(0, "Herld").unwrap();
text.insert(2, "llo Wo").unwrap();
assert_eq!(text.char_at(0).unwrap(), 'H');
assert_eq!(text.char_at(1).unwrap(), 'e');
assert_eq!(text.char_at(2).unwrap(), 'l');
assert_eq!(text.char_at(3).unwrap(), 'l');
let err = text.char_at(15).unwrap_err();
assert!(matches!(err, loro_common::LoroError::OutOfBound { .. }))
}
#[test]
fn test_char_at_detached() {
let text = TextHandler::new_detached();
text.insert(0, "Herld").unwrap();
text.insert(2, "llo Wo").unwrap();
assert_eq!(text.char_at(0).unwrap(), 'H');
assert_eq!(text.char_at(1).unwrap(), 'e');
assert_eq!(text.char_at(2).unwrap(), 'l');
assert_eq!(text.char_at(3).unwrap(), 'l');
let err = text.char_at(15).unwrap_err();
assert!(matches!(err, loro_common::LoroError::OutOfBound { .. }))
}
#[test]
fn test_char_at_wchar() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert(0, "你好").unwrap();
text.insert(1, "世界").unwrap();
assert_eq!(text.char_at(0).unwrap(), '你');
assert_eq!(text.char_at(1).unwrap(), '世');
assert_eq!(text.char_at(2).unwrap(), '界');
assert_eq!(text.char_at(3).unwrap(), '好');
let err = text.char_at(5).unwrap_err();
assert!(matches!(err, loro_common::LoroError::OutOfBound { .. }))
}
#[test]
fn test_text_slice() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert(0, "Hello").unwrap();
text.insert(1, "World").unwrap();
assert_eq!(text.slice(0, 4).unwrap(), "HWor");
assert_eq!(text.slice(0, 1).unwrap(), "H");
}
#[test]
fn test_text_slice_detached() {
let text = TextHandler::new_detached();
text.insert(0, "Herld").unwrap();
text.insert(2, "llo Wo").unwrap();
assert_eq!(text.slice(0, 4).unwrap(), "Hell");
assert_eq!(text.slice(0, 1).unwrap(), "H");
}
#[test]
fn test_text_slice_wchar() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert(0, "你好").unwrap();
text.insert(1, "世界").unwrap();
assert_eq!(text.slice(0, 3).unwrap(), "你世界");
}
#[test]
#[should_panic]
fn test_text_slice_end_index_less_than_start() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert(0, "你好").unwrap();
text.insert(1, "世界").unwrap();
text.slice(2, 1).unwrap();
}
#[test]
#[should_panic]
fn test_text_slice_out_of_bound() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert(0, "你好").unwrap();
text.insert(1, "世界").unwrap();
text.slice(1, 10).unwrap();
}
#[test]
fn test_text_splice() {
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert(0, "你好").unwrap();
assert_eq!(text.splice(1, 1, "世界").unwrap(), "");
assert_eq!(text.to_string(), "你世界");
}
fn test_text_iter() {
let mut str = String::new();
let doc = LoroDoc::new_auto_commit();
let text = doc.get_text("text");
text.insert(0, "Hello").unwrap();
text.insert(1, "Hello").unwrap();
text.iter(|s| {
str.push_str(s);
2024-07-17 04:15:16 +00:00
true
});
assert_eq!(str, "HHelloello");
str = String::new();
let mut i = 0;
text.iter(|s| {
if i == 1 {
return false;
}
str.push_str(s);
2024-07-17 04:15:16 +00:00
i += 1;
true
});
assert_eq!(str, "H");
}
#[test]
fn test_text_iter_detached() {
let mut str = String::new();
let text = TextHandler::new_detached();
text.insert(0, "Hello").unwrap();
text.insert(1, "Hello").unwrap();
text.iter(|s| {
str.push_str(s);
2024-07-17 04:15:16 +00:00
true
});
assert_eq!(str, "HHelloello");
}