636 lines
19 KiB
Rust
636 lines
19 KiB
Rust
extern crate automerge_backend;
|
|
use std::{collections::HashSet, convert::TryInto};
|
|
|
|
use automerge_backend::{Backend, Change};
|
|
use automerge_protocol as protocol;
|
|
use automerge_protocol::{
|
|
ActorId, ChangeHash, Diff, DiffEdit, ElementId, MapDiff, MapType, ObjType, ObjectId, Op,
|
|
OpType, Patch, SeqDiff, SequenceType, UncompressedChange,
|
|
};
|
|
use maplit::hashmap;
|
|
use protocol::{Key, ScalarValue};
|
|
|
|
#[test]
|
|
fn test_apply_local_change() {
|
|
let actor: ActorId = "eb738e04ef8848ce8b77309b6c7f7e39".try_into().unwrap();
|
|
let change_request = UncompressedChange {
|
|
actor_id: actor.clone(),
|
|
time: 0,
|
|
message: None,
|
|
hash: None,
|
|
seq: 1,
|
|
deps: Vec::new(),
|
|
start_op: 1,
|
|
operations: vec![Op {
|
|
action: protocol::OpType::Set("magpie".into()),
|
|
key: "bird".into(),
|
|
obj: ObjectId::Root,
|
|
insert: false,
|
|
pred: Vec::new(),
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
};
|
|
|
|
let mut backend = Backend::new();
|
|
let patch = backend.apply_local_change(change_request).unwrap().0;
|
|
|
|
let changes = backend.get_changes(&[]);
|
|
let expected_change = UncompressedChange {
|
|
actor_id: actor.clone(),
|
|
seq: 1,
|
|
start_op: 1,
|
|
time: changes[0].time,
|
|
message: None,
|
|
hash: None,
|
|
deps: Vec::new(),
|
|
operations: vec![Op {
|
|
action: OpType::Set("magpie".into()),
|
|
obj: ObjectId::Root,
|
|
key: "bird".into(),
|
|
pred: Vec::new(),
|
|
insert: false,
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
}
|
|
.try_into()
|
|
.unwrap();
|
|
assert_eq!(changes[0], &expected_change);
|
|
|
|
let expected_patch = Patch {
|
|
actor: Some(actor.clone()),
|
|
max_op: 1,
|
|
pending_changes: 0,
|
|
seq: Some(1),
|
|
clock: hashmap! {
|
|
actor => 1,
|
|
},
|
|
deps: Vec::new(),
|
|
diffs: Some(Diff::Map(MapDiff {
|
|
object_id: ObjectId::Root,
|
|
obj_type: MapType::Map,
|
|
props: hashmap! {
|
|
"bird".into() => hashmap!{
|
|
"1@eb738e04ef8848ce8b77309b6c7f7e39".try_into().unwrap() => Diff::Value("magpie".into())
|
|
}
|
|
},
|
|
})),
|
|
};
|
|
assert_eq!(patch, expected_patch);
|
|
}
|
|
|
|
#[test]
|
|
fn test_error_on_duplicate_requests() {
|
|
let actor: ActorId = "37704788917a499cb0206fa8519ac4d9".try_into().unwrap();
|
|
let change_request1 = UncompressedChange {
|
|
actor_id: actor.clone(),
|
|
seq: 1,
|
|
message: None,
|
|
hash: None,
|
|
time: 0,
|
|
deps: Vec::new(),
|
|
start_op: 1,
|
|
operations: vec![Op {
|
|
action: protocol::OpType::Set("magpie".into()),
|
|
obj: ObjectId::Root,
|
|
key: "bird".into(),
|
|
insert: false,
|
|
pred: Vec::new(),
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
};
|
|
|
|
let change_request2 = UncompressedChange {
|
|
actor_id: actor,
|
|
seq: 2,
|
|
message: None,
|
|
hash: None,
|
|
time: 0,
|
|
deps: Vec::new(),
|
|
start_op: 2,
|
|
operations: vec![Op {
|
|
action: protocol::OpType::Set("jay".into()),
|
|
obj: ObjectId::Root,
|
|
key: "bird".into(),
|
|
insert: false,
|
|
pred: Vec::new(),
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
};
|
|
let mut backend = Backend::new();
|
|
backend.apply_local_change(change_request1.clone()).unwrap();
|
|
backend.apply_local_change(change_request2.clone()).unwrap();
|
|
assert!(backend.apply_local_change(change_request1).is_err());
|
|
assert!(backend.apply_local_change(change_request2).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_handle_concurrent_frontend_and_backend_changes() {
|
|
let actor: ActorId = "cb55260e9d7e457886a4fc73fd949202".try_into().unwrap();
|
|
let local1 = UncompressedChange {
|
|
actor_id: actor.clone(),
|
|
seq: 1,
|
|
time: 0,
|
|
deps: Vec::new(),
|
|
message: None,
|
|
hash: None,
|
|
start_op: 1,
|
|
operations: vec![Op {
|
|
action: protocol::OpType::Set("magpie".into()),
|
|
obj: ObjectId::Root,
|
|
key: "bird".into(),
|
|
insert: false,
|
|
pred: Vec::new(),
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
};
|
|
|
|
let local2 = UncompressedChange {
|
|
actor_id: actor.clone(),
|
|
seq: 2,
|
|
start_op: 2,
|
|
time: 0,
|
|
deps: Vec::new(),
|
|
message: None,
|
|
hash: None,
|
|
operations: vec![Op {
|
|
action: protocol::OpType::Set("jay".into()),
|
|
obj: ObjectId::Root,
|
|
key: "bird".into(),
|
|
insert: false,
|
|
pred: vec![actor.op_id_at(1)],
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
};
|
|
let remote_actor: ActorId = "6d48a01318644eed90455d2cb68ac657".try_into().unwrap();
|
|
let remote1 = UncompressedChange {
|
|
actor_id: remote_actor.clone(),
|
|
seq: 1,
|
|
start_op: 1,
|
|
time: 0,
|
|
deps: Vec::new(),
|
|
message: None,
|
|
hash: None,
|
|
operations: vec![Op {
|
|
action: protocol::OpType::Set("goldfish".into()),
|
|
obj: ObjectId::Root,
|
|
key: "fish".into(),
|
|
pred: Vec::new(),
|
|
insert: false,
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
}
|
|
.try_into()
|
|
.unwrap();
|
|
|
|
let mut expected_change1 = UncompressedChange {
|
|
actor_id: actor.clone(),
|
|
seq: 1,
|
|
start_op: 1,
|
|
time: 0,
|
|
message: None,
|
|
hash: None,
|
|
deps: Vec::new(),
|
|
operations: vec![Op {
|
|
action: protocol::OpType::Set("magpie".into()),
|
|
obj: ObjectId::Root,
|
|
key: "bird".into(),
|
|
pred: Vec::new(),
|
|
insert: false,
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
};
|
|
|
|
let mut expected_change2 = UncompressedChange {
|
|
actor_id: remote_actor,
|
|
seq: 1,
|
|
start_op: 1,
|
|
time: 0,
|
|
message: None,
|
|
hash: None,
|
|
deps: Vec::new(),
|
|
operations: vec![Op {
|
|
action: protocol::OpType::Set("goldfish".into()),
|
|
key: "fish".into(),
|
|
obj: ObjectId::Root,
|
|
pred: Vec::new(),
|
|
insert: false,
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
};
|
|
|
|
let mut expected_change3 = UncompressedChange {
|
|
actor_id: actor.clone(),
|
|
seq: 2,
|
|
start_op: 2,
|
|
time: 0,
|
|
message: None,
|
|
hash: None,
|
|
deps: Vec::new(),
|
|
operations: vec![Op {
|
|
action: protocol::OpType::Set("jay".into()),
|
|
obj: ObjectId::Root,
|
|
key: "bird".into(),
|
|
pred: vec![actor.op_id_at(1)],
|
|
insert: false,
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
};
|
|
let mut backend = Backend::new();
|
|
backend.apply_local_change(local1).unwrap();
|
|
let backend_after_first = backend.clone();
|
|
let changes1 = backend_after_first.get_changes(&[]);
|
|
let change01 = changes1.get(0).unwrap();
|
|
|
|
backend.apply_changes(vec![remote1]).unwrap();
|
|
let backend_after_second = backend.clone();
|
|
let changes2 = backend_after_second.get_changes(&[change01.hash]);
|
|
let change12 = *changes2.get(0).unwrap();
|
|
|
|
backend.apply_local_change(local2).unwrap();
|
|
let changes3 = backend.get_changes(&[change01.hash, change12.hash]);
|
|
let change23 = changes3.get(0).unwrap();
|
|
|
|
expected_change1.time = change01.time;
|
|
expected_change2.time = change12.time;
|
|
expected_change3.time = change23.time;
|
|
expected_change3.deps = vec![change01.hash];
|
|
|
|
assert_eq!(change01, &&expected_change1.try_into().unwrap());
|
|
assert_eq!(change12, &expected_change2.try_into().unwrap());
|
|
assert_changes_equal(change23.decode(), expected_change3.clone());
|
|
assert_eq!(change23, &&expected_change3.try_into().unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_transform_list_indexes_into_element_ids() {
|
|
let actor: ActorId = "8f389df8fecb4ddc989102321af3578e".try_into().unwrap();
|
|
let remote_actor: ActorId = "9ba21574dc44411b8ce37bc6037a9687".try_into().unwrap();
|
|
let remote1: Change = UncompressedChange {
|
|
actor_id: remote_actor.clone(),
|
|
seq: 1,
|
|
start_op: 1,
|
|
time: 0,
|
|
message: None,
|
|
hash: None,
|
|
deps: Vec::new(),
|
|
operations: vec![Op {
|
|
action: protocol::OpType::Make(ObjType::list()),
|
|
key: "birds".into(),
|
|
obj: ObjectId::Root,
|
|
pred: Vec::new(),
|
|
insert: false,
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
}
|
|
.try_into()
|
|
.unwrap();
|
|
|
|
let remote2: Change = UncompressedChange {
|
|
actor_id: remote_actor.clone(),
|
|
seq: 2,
|
|
start_op: 2,
|
|
time: 0,
|
|
message: None,
|
|
hash: None,
|
|
deps: vec![remote1.hash],
|
|
operations: vec![Op {
|
|
action: protocol::OpType::Set("magpie".into()),
|
|
obj: ObjectId::from(remote_actor.op_id_at(1)),
|
|
key: ElementId::Head.into(),
|
|
insert: true,
|
|
pred: Vec::new(),
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
}
|
|
.try_into()
|
|
.unwrap();
|
|
|
|
let local1 = UncompressedChange {
|
|
actor_id: actor.clone(),
|
|
seq: 1,
|
|
message: None,
|
|
hash: None,
|
|
time: 0,
|
|
deps: vec![remote1.hash],
|
|
start_op: 2,
|
|
operations: vec![Op {
|
|
obj: ObjectId::from(remote_actor.op_id_at(1)),
|
|
action: protocol::OpType::Set("goldfinch".into()),
|
|
key: ElementId::Head.into(),
|
|
insert: true,
|
|
pred: Vec::new(),
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
};
|
|
let local2 = UncompressedChange {
|
|
actor_id: actor.clone(),
|
|
seq: 2,
|
|
message: None,
|
|
hash: None,
|
|
deps: Vec::new(),
|
|
time: 0,
|
|
start_op: 3,
|
|
operations: vec![Op {
|
|
obj: ObjectId::from(remote_actor.op_id_at(1)),
|
|
action: protocol::OpType::Set("wagtail".into()),
|
|
key: actor.op_id_at(2).into(),
|
|
insert: true,
|
|
pred: Vec::new(),
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
};
|
|
|
|
let local3 = UncompressedChange {
|
|
actor_id: actor.clone(),
|
|
seq: 3,
|
|
message: None,
|
|
hash: None,
|
|
deps: vec![remote2.hash],
|
|
time: 0,
|
|
start_op: 4,
|
|
operations: vec![
|
|
Op {
|
|
obj: ObjectId::from(remote_actor.op_id_at(1)),
|
|
action: protocol::OpType::Set("Magpie".into()),
|
|
key: remote_actor.op_id_at(2).into(),
|
|
insert: false,
|
|
pred: vec![remote_actor.op_id_at(2)],
|
|
},
|
|
Op {
|
|
obj: ObjectId::from(remote_actor.op_id_at(1)),
|
|
action: protocol::OpType::Set("Goldfinch".into()),
|
|
key: actor.op_id_at(2).into(),
|
|
insert: false,
|
|
pred: vec![actor.op_id_at(2)],
|
|
},
|
|
],
|
|
extra_bytes: Vec::new(),
|
|
};
|
|
|
|
let mut expected_change1 = UncompressedChange {
|
|
actor_id: actor.clone(),
|
|
seq: 1,
|
|
start_op: 2,
|
|
time: 0,
|
|
message: None,
|
|
hash: None,
|
|
deps: vec![remote1.hash],
|
|
operations: vec![Op {
|
|
obj: ObjectId::from(remote_actor.op_id_at(1)),
|
|
action: protocol::OpType::Set("goldfinch".into()),
|
|
key: ElementId::Head.into(),
|
|
insert: true,
|
|
pred: Vec::new(),
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
};
|
|
let mut expected_change2 = UncompressedChange {
|
|
actor_id: actor.clone(),
|
|
seq: 2,
|
|
start_op: 3,
|
|
time: 0,
|
|
message: None,
|
|
hash: None,
|
|
deps: Vec::new(),
|
|
operations: vec![Op {
|
|
obj: ObjectId::from(remote_actor.op_id_at(1)),
|
|
action: protocol::OpType::Set("wagtail".into()),
|
|
key: actor.op_id_at(2).into(),
|
|
insert: true,
|
|
pred: Vec::new(),
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
};
|
|
let mut expected_change3 = UncompressedChange {
|
|
actor_id: actor.clone(),
|
|
seq: 3,
|
|
start_op: 4,
|
|
time: 0,
|
|
message: None,
|
|
hash: None,
|
|
deps: Vec::new(),
|
|
operations: vec![
|
|
Op {
|
|
obj: ObjectId::from(remote_actor.op_id_at(1)),
|
|
action: protocol::OpType::Set("Magpie".into()),
|
|
key: remote_actor.op_id_at(2).into(),
|
|
pred: vec![remote_actor.op_id_at(2)],
|
|
insert: false,
|
|
},
|
|
Op {
|
|
obj: ObjectId::from(remote_actor.op_id_at(1)),
|
|
action: protocol::OpType::Set("Goldfinch".into()),
|
|
key: actor.op_id_at(2).into(),
|
|
pred: vec![actor.op_id_at(2)],
|
|
insert: false,
|
|
},
|
|
],
|
|
extra_bytes: Vec::new(),
|
|
};
|
|
|
|
let mut backend = Backend::new();
|
|
backend.apply_changes(vec![remote1.clone()]).unwrap();
|
|
backend.apply_local_change(local1).unwrap();
|
|
let backend_after_first = backend.clone();
|
|
let changes1 = backend_after_first.get_changes(&[remote1.hash]);
|
|
let change12 = *changes1.get(0).unwrap();
|
|
|
|
backend.apply_changes(vec![remote2.clone()]).unwrap();
|
|
backend.apply_local_change(local2).unwrap();
|
|
let backend_after_second = backend.clone();
|
|
let changes2 = backend_after_second.get_changes(&[remote2.hash, change12.hash]);
|
|
let change23 = *changes2.get(0).unwrap();
|
|
|
|
backend.apply_local_change(local3).unwrap();
|
|
let changes3 = backend.get_changes(&[remote2.hash, change23.hash]);
|
|
let change34 = changes3.get(0).unwrap().decode();
|
|
|
|
expected_change1.time = change12.time;
|
|
expected_change2.time = change23.time;
|
|
expected_change2.deps = vec![change12.hash];
|
|
expected_change3.time = change34.time;
|
|
expected_change3.deps = vec![remote2.hash, change23.hash];
|
|
|
|
assert_changes_equal(change34, expected_change3);
|
|
assert_eq!(change12, &expected_change1.try_into().unwrap());
|
|
assert_changes_equal(change23.decode(), expected_change2.clone());
|
|
assert_eq!(change23, &expected_change2.try_into().unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_handle_list_insertion_and_deletion_in_same_change() {
|
|
let actor: ActorId = "0723d2a1940744868ffd6b294ada813f".try_into().unwrap();
|
|
let local1 = UncompressedChange {
|
|
actor_id: actor.clone(),
|
|
seq: 1,
|
|
message: None,
|
|
hash: None,
|
|
time: 0,
|
|
deps: Vec::new(),
|
|
start_op: 1,
|
|
operations: vec![Op {
|
|
obj: ObjectId::Root,
|
|
action: protocol::OpType::Make(ObjType::list()),
|
|
key: "birds".into(),
|
|
insert: false,
|
|
pred: Vec::new(),
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
};
|
|
|
|
let local2 = UncompressedChange {
|
|
actor_id: actor.clone(),
|
|
seq: 2,
|
|
message: None,
|
|
hash: None,
|
|
time: 0,
|
|
deps: Vec::new(),
|
|
start_op: 2,
|
|
operations: vec![
|
|
Op {
|
|
obj: ObjectId::from(actor.op_id_at(1)),
|
|
action: protocol::OpType::Set("magpie".into()),
|
|
key: ElementId::Head.into(),
|
|
insert: true,
|
|
pred: Vec::new(),
|
|
},
|
|
Op {
|
|
obj: ObjectId::from(actor.op_id_at(1)),
|
|
action: protocol::OpType::Del,
|
|
key: actor.op_id_at(2).into(),
|
|
insert: false,
|
|
pred: vec![actor.op_id_at(2)],
|
|
},
|
|
],
|
|
extra_bytes: Vec::new(),
|
|
};
|
|
|
|
let mut expected_patch = Patch {
|
|
actor: Some(actor.clone()),
|
|
seq: Some(2),
|
|
max_op: 3,
|
|
pending_changes: 0,
|
|
clock: hashmap! {
|
|
actor.clone() => 2
|
|
},
|
|
deps: Vec::new(),
|
|
diffs: Some(Diff::Map(MapDiff {
|
|
object_id: ObjectId::Root,
|
|
obj_type: MapType::Map,
|
|
props: hashmap! {
|
|
"birds".into() => hashmap!{
|
|
actor.op_id_at(1) => Diff::Seq(SeqDiff{
|
|
object_id: ObjectId::from(actor.op_id_at(1)),
|
|
obj_type: SequenceType::List,
|
|
edits: vec![
|
|
DiffEdit::Insert{index: 0, elem_id: actor.op_id_at(2).into()},
|
|
DiffEdit::Remove{index: 0},
|
|
],
|
|
props: hashmap!{},
|
|
})
|
|
}
|
|
},
|
|
})),
|
|
};
|
|
|
|
let mut backend = Backend::new();
|
|
backend.apply_local_change(local1).unwrap();
|
|
let patch = backend.apply_local_change(local2).unwrap().0;
|
|
expected_patch.deps = patch.deps.clone();
|
|
assert_eq!(patch, expected_patch);
|
|
|
|
let changes = backend.get_changes(&[]);
|
|
assert_eq!(changes.len(), 2);
|
|
let change1 = changes[0].clone();
|
|
let change2 = changes[1].clone();
|
|
|
|
let expected_change1 = UncompressedChange {
|
|
actor_id: actor.clone(),
|
|
seq: 1,
|
|
start_op: 1,
|
|
time: change1.time,
|
|
message: None,
|
|
hash: None,
|
|
deps: Vec::new(),
|
|
operations: vec![Op {
|
|
obj: ObjectId::Root,
|
|
action: protocol::OpType::Make(ObjType::list()),
|
|
key: "birds".into(),
|
|
insert: false,
|
|
pred: Vec::new(),
|
|
}],
|
|
extra_bytes: Vec::new(),
|
|
}
|
|
.try_into()
|
|
.unwrap();
|
|
|
|
let expected_change2 = UncompressedChange {
|
|
actor_id: actor.clone(),
|
|
seq: 2,
|
|
start_op: 2,
|
|
time: change2.time,
|
|
message: None,
|
|
hash: None,
|
|
deps: vec![change1.hash],
|
|
operations: vec![
|
|
Op {
|
|
obj: ObjectId::from(actor.op_id_at(1)),
|
|
action: protocol::OpType::Set("magpie".into()),
|
|
key: ElementId::Head.into(),
|
|
insert: true,
|
|
pred: Vec::new(),
|
|
},
|
|
Op {
|
|
obj: ObjectId::from(actor.op_id_at(1)),
|
|
action: protocol::OpType::Del,
|
|
key: actor.op_id_at(2).into(),
|
|
pred: vec![actor.op_id_at(2)],
|
|
insert: false,
|
|
},
|
|
],
|
|
extra_bytes: Vec::new(),
|
|
}
|
|
.try_into()
|
|
.unwrap();
|
|
|
|
assert_eq!(change1, expected_change1);
|
|
assert_eq!(change2, expected_change2);
|
|
}
|
|
|
|
/// Asserts that the changes are equal without respect to order of the hashes
|
|
/// in the change dependencies
|
|
fn assert_changes_equal(mut change1: UncompressedChange, change2: UncompressedChange) {
|
|
let change2_clone = change2.clone();
|
|
let deps1: HashSet<&ChangeHash> = change1.deps.iter().collect();
|
|
let deps2: HashSet<&ChangeHash> = change2.deps.iter().collect();
|
|
assert_eq!(
|
|
deps1, deps2,
|
|
"The two changes did not have equal dependencies, left: {:?}, right: {:?}",
|
|
deps1, deps2
|
|
);
|
|
change1.deps = change2.deps;
|
|
assert_eq!(change1, change2_clone)
|
|
}
|
|
|
|
#[test]
|
|
fn test_random_change_start_op_overflow() {
|
|
let change = UncompressedChange {
|
|
operations: vec![Op {
|
|
action: OpType::Set(ScalarValue::Int(-2512681860335064791)),
|
|
obj: ObjectId::Root,
|
|
key: Key::Map("".to_owned()),
|
|
pred: vec![],
|
|
insert: false,
|
|
}],
|
|
actor_id: ActorId::random(),
|
|
hash: None,
|
|
seq: 1,
|
|
start_op: 18446744073709551615,
|
|
time: 10766414268858367,
|
|
message: None,
|
|
deps: vec![],
|
|
extra_bytes: vec![65, 41, 1, 67, 0, 0, 0, 0, 0, 7, 210, 214, 194, 2, 0],
|
|
};
|
|
let mut b = Backend::new();
|
|
let _ = b.apply_local_change(change);
|
|
}
|