automerge/automerge-frontend/tests/test_apply_patch.rs
Andrew Jeffery fc1b8f87fb
Use SmolStr in place of String (#182)
Most of the strings are small and so fit nicely in a SmolStr. When they
don't it just reverts to using a normal String.
2021-06-19 16:28:51 +01:00

1047 lines
34 KiB
Rust

use std::{collections::HashMap, convert::TryInto};
use amp::RootDiff;
use automerge_frontend::{Frontend, Path, Primitive, Value};
use automerge_protocol as amp;
use maplit::hashmap;
use unicode_segmentation::UnicodeSegmentation;
#[test]
fn set_object_root_properties() {
let actor = amp::ActorId::random();
let patch = amp::Patch {
actor: None,
seq: None,
max_op: 1,
pending_changes: 0,
deps: Vec::new(),
clock: hashmap! {
actor.clone() => 1,
},
diffs: RootDiff {
props: hashmap! {
"bird".into() => hashmap!{
actor.op_id_at(1) => "magpie".into()
}
},
},
};
let mut frontend = Frontend::new();
frontend.apply_patch(patch).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(hashmap! {"bird" => "magpie"})
);
}
#[test]
fn set_bytes_value() {
let actor = amp::ActorId::random();
let patch = amp::Patch {
actor: None,
seq: None,
max_op: 1,
pending_changes: 0,
deps: Vec::new(),
clock: hashmap! {
actor.clone() => 1,
},
diffs: amp::RootDiff {
props: hashmap! {
"bird".into() => hashmap!{
actor.op_id_at(1) => amp::Diff::Value(amp::ScalarValue::Bytes(vec![1, 2, 3])),
}
},
},
};
let mut frontend = Frontend::new();
frontend.apply_patch(patch).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(hashmap! {"bird" => Primitive::Bytes(vec![1, 2, 3])})
);
}
#[test]
fn reveal_conflicts_on_root_properties() {
// We don't just use random actor IDs because we need to have a specific
// ordering (actor1 > actor2)
let actor1 = amp::ActorId::from_bytes(
uuid::Uuid::parse_str("02ef21f3-c9eb-4087-880e-bedd7c4bbe43")
.unwrap()
.as_bytes(),
);
let actor2 = amp::ActorId::from_bytes(
uuid::Uuid::parse_str("2a1d376b-24f7-4400-8d4a-f58252d644dd")
.unwrap()
.as_bytes(),
);
let patch = amp::Patch {
actor: None,
seq: None,
max_op: 2,
pending_changes: 0,
clock: hashmap! {
actor1.clone() => 1,
actor2.clone() => 2,
},
deps: Vec::new(),
diffs: RootDiff {
props: hashmap! {
"favouriteBird".into() => hashmap!{
actor1.op_id_at(1) => amp::Diff::Value("robin".into()),
actor2.op_id_at(1) => amp::Diff::Value("wagtail".into()),
}
},
},
};
let mut doc = Frontend::new();
doc.apply_patch(patch).unwrap();
assert_eq!(
doc.state(),
&Into::<Value>::into(hashmap! {"favouriteBird" => "wagtail"})
);
let conflicts = doc.get_conflicts(&Path::root().key("favouriteBird"));
assert_eq!(
conflicts,
Some(hashmap! {
actor1.op_id_at(1) => "robin".into(),
actor2.op_id_at(1) => "wagtail".into(),
})
)
}
#[test]
fn create_nested_maps() {
let actor = amp::ActorId::random();
let patch = amp::Patch {
actor: None,
seq: None,
max_op: 3,
pending_changes: 0,
deps: Vec::new(),
clock: hashmap! {
actor.clone() => 1,
},
diffs: RootDiff {
props: hashmap! {
"birds".into() => hashmap!{
actor.op_id_at(1) => amp::Diff::Map(amp::MapDiff{
object_id: actor.op_id_at(2).into(),
props: hashmap!{
"wrens".into() => hashmap!{
actor.op_id_at(2) => amp::Diff::Value(amp::ScalarValue::Int(3))
}
}
})
}
},
},
};
let mut frontend = Frontend::new();
frontend.apply_patch(patch).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(hashmap! {"birds" => hashmap!{"wrens" => Primitive::Int(3)}})
);
}
#[test]
fn apply_updates_inside_nested_maps() {
let actor = amp::ActorId::random();
let patch1 = amp::Patch {
actor: None,
seq: None,
max_op: 2,
pending_changes: 0,
deps: Vec::new(),
clock: hashmap! {
actor.clone() => 1,
},
diffs: RootDiff {
props: hashmap! {
"birds".into() => hashmap!{
actor.op_id_at(1) => amp::Diff::Map(amp::MapDiff{
object_id: actor.op_id_at(2).into(),
props: hashmap!{
"wrens".into() => hashmap!{
actor.op_id_at(2) => amp::Diff::Value(amp::ScalarValue::Int(3))
}
}
})
}
},
},
};
let mut frontend = Frontend::new();
frontend.apply_patch(patch1).unwrap();
let birds_id = frontend.get_object_id(&Path::root().key("birds")).unwrap();
let patch2 = amp::Patch {
actor: None,
seq: None,
max_op: 3,
pending_changes: 0,
deps: Vec::new(),
clock: hashmap! {
actor.clone() => 2,
},
diffs: RootDiff {
props: hashmap! {
"birds".into() => hashmap!{
actor.op_id_at(1) => amp::Diff::Map(amp::MapDiff{
object_id: birds_id,
props: hashmap!{
"sparrows".into() => hashmap!{
actor.op_id_at(3) => amp::Diff::Value(amp::ScalarValue::Int(15))
}
}
})
}
},
},
};
frontend.apply_patch(patch2).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(
hashmap! {"birds" => hashmap!{"wrens" => Primitive::Int(3), "sparrows" => Primitive::Int(15)}}
)
);
}
#[test]
fn apply_updates_inside_map_conflicts() {
// We don't just use random actor IDs because we need to have a specific
// ordering (actor1 < actor2)
let actor1 = amp::ActorId::from_bytes(
uuid::Uuid::parse_str("02ef21f3-c9eb-4087-880e-bedd7c4bbe43")
.unwrap()
.as_bytes(),
);
let actor2 = amp::ActorId::from_bytes(
uuid::Uuid::parse_str("2a1d376b-24f7-4400-8d4a-f58252d644dd")
.unwrap()
.as_bytes(),
);
let patch1 = amp::Patch {
actor: None,
seq: None,
max_op: 2,
pending_changes: 0,
deps: Vec::new(),
clock: hashmap! {
actor1.clone() => 1,
actor2.clone() => 1,
},
diffs: RootDiff {
props: hashmap! {
"favouriteBirds".into() => hashmap!{
actor1.op_id_at(1) => amp::Diff::Map(amp::MapDiff{
object_id: actor1.op_id_at(1).into(),
props: hashmap!{
"blackbirds".into() => hashmap!{
actor1.op_id_at(2) => amp::Diff::Value(amp::ScalarValue::Int(1)),
}
},
}),
actor2.op_id_at(1) => amp::Diff::Map(amp::MapDiff{
object_id: actor2.op_id_at(1).into(),
props: hashmap!{
"wrens".into() => hashmap!{
actor2.op_id_at(2) => amp::Diff::Value(amp::ScalarValue::Int(3)),
}
},
})
}
},
},
};
let mut frontend = Frontend::new();
frontend.apply_patch(patch1).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(hashmap! {"favouriteBirds" => hashmap!{"wrens" => Primitive::Int(3)}})
);
assert_eq!(
frontend
.get_conflicts(&Path::root().key("favouriteBirds"))
.unwrap(),
hashmap! {
actor1.op_id_at(1) => hashmap!{"blackbirds" => Primitive::Int(1)}.into(),
actor2.op_id_at(1) => hashmap!{"wrens" => Primitive::Int(3)}.into(),
}
);
let patch2 = amp::Patch {
actor: None,
seq: None,
max_op: 1,
pending_changes: 0,
deps: Vec::new(),
clock: hashmap! {
actor1.clone() => 2,
actor2.clone() => 1,
},
diffs: RootDiff {
props: hashmap! {
"favouriteBirds".into() => hashmap!{
actor1.op_id_at(1) => amp::Diff::Map(amp::MapDiff{
object_id: actor1.op_id_at(1).into(),
props: hashmap!{
"blackbirds".into() => hashmap!{
actor1.op_id_at(3) => amp::Diff::Value(amp::ScalarValue::Int(2)),
}
},
}),
actor2.op_id_at(1) => amp::Diff::Map(amp::MapDiff{
object_id: actor2.op_id_at(1).into(),
props: HashMap::new(),
})
}
},
},
};
frontend.apply_patch(patch2).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(hashmap! {"favouriteBirds" => hashmap!{"wrens" => Primitive::Int(3)}})
);
assert_eq!(
frontend
.get_conflicts(&Path::root().key("favouriteBirds"))
.unwrap(),
hashmap! {
actor1.op_id_at(1) => hashmap!{"blackbirds" => Primitive::Int(2)}.into(),
actor2.op_id_at(1) => hashmap!{"wrens" => Primitive::Int(3)}.into(),
}
);
}
#[test]
fn delete_keys_in_maps() {
let actor = amp::ActorId::random();
let mut frontend = Frontend::new();
let patch1 = amp::Patch {
actor: None,
max_op: 2,
pending_changes: 0,
seq: None,
deps: Vec::new(),
clock: hashmap! {
actor.clone() => 1,
},
diffs: RootDiff {
props: hashmap! {
"magpies".into() => hashmap!{
actor.op_id_at(1) => amp::Diff::Value(amp::ScalarValue::Int(2))
},
"sparrows".into() => hashmap!{
actor.op_id_at(2) => amp::Diff::Value(amp::ScalarValue::Int(15))
}
},
},
};
frontend.apply_patch(patch1).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(
hashmap! {"magpies" => Primitive::Int(2), "sparrows" => Primitive::Int(15)}
)
);
let patch2 = amp::Patch {
actor: None,
seq: None,
max_op: 3,
pending_changes: 0,
deps: Vec::new(),
clock: hashmap! {
actor => 2,
},
diffs: RootDiff {
props: hashmap! {
"magpies".into() => hashmap!{}
},
},
};
frontend.apply_patch(patch2).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(hashmap! {"sparrows" => Primitive::Int(15)})
);
}
#[test]
fn create_lists() {
let actor = amp::ActorId::random();
let mut frontend = Frontend::new();
let patch = amp::Patch {
actor: None,
seq: None,
max_op: 2,
pending_changes: 0,
deps: Vec::new(),
clock: hashmap! {
actor.clone() => 2,
},
diffs: RootDiff {
props: hashmap! {
"birds".into() => hashmap!{
actor.op_id_at(1) => amp::Diff::List(amp::ListDiff{
object_id: actor.op_id_at(1).into(),
edits: vec![amp::DiffEdit::SingleElementInsert {
index: 0,
elem_id: actor.op_id_at(2).into(),
op_id: actor.op_id_at(2),
value: amp::Diff::Value("chaffinch".into()),
}],
})
}
},
},
};
frontend.apply_patch(patch).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(hashmap! {"birds" => vec!["chaffinch"]})
)
}
#[test]
fn apply_updates_inside_lists() {
let actor = amp::ActorId::random();
let mut frontend = Frontend::new();
let patch = amp::Patch {
actor: None,
seq: None,
max_op: 1,
pending_changes: 0,
deps: Vec::new(),
clock: hashmap! {
actor.clone() => 1,
},
diffs: RootDiff {
props: hashmap! {
"birds".into() => hashmap!{
actor.op_id_at(1) => amp::Diff::List(amp::ListDiff{
object_id: actor.op_id_at(1).into(),
edits: vec![amp::DiffEdit::SingleElementInsert {
index: 0,
elem_id: actor.op_id_at(2).into(),
op_id: actor.op_id_at(2),
value: amp::Diff::Value("chaffinch".into()),
}],
})
}
},
},
};
frontend.apply_patch(patch).unwrap();
let patch2 = amp::Patch {
actor: None,
seq: None,
max_op: 3,
pending_changes: 0,
deps: Vec::new(),
clock: hashmap! {
actor.clone() => 2,
},
diffs: RootDiff {
props: hashmap! {
"birds".into() => hashmap!{
actor.op_id_at(1) => amp::Diff::List(amp::ListDiff{
object_id: actor.op_id_at(1).into(),
edits: vec![amp::DiffEdit::Update{
index: 0,
op_id: actor.op_id_at(3),
value: amp::Diff::Value("greenfinch".into()),
}],
})
}
},
},
};
frontend.apply_patch(patch2).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(hashmap! {"birds" => vec!["greenfinch"]})
)
}
#[test]
fn apply_multi_insert_updates_inside_lists() {
let actor = amp::ActorId::random();
let mut frontend = Frontend::new();
let patch = amp::Patch {
actor: None,
seq: None,
max_op: 1,
deps: Vec::new(),
clock: hashmap! {
actor.clone() => 1,
},
diffs: RootDiff {
props: hashmap! {
"birds".into() => hashmap!{
actor.op_id_at(1) => amp::Diff::List(amp::ListDiff{
object_id: actor.op_id_at(1).into(),
edits: vec![amp::DiffEdit::MultiElementInsert {
index: 0,
elem_id: actor.op_id_at(2).into(),
values: vec![
"greenfinch".into(),
"bullfinch".into(),
]
}],
})
}
},
},
pending_changes: 0,
};
frontend.apply_patch(patch).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(hashmap! {"birds" => vec!["greenfinch", "bullfinch"]})
)
}
#[test]
fn apply_updates_inside_list_conflicts() {
// We don't just use random actor IDs because we need to have a specific
// ordering (actor1 < actor2)
let actor1 = amp::ActorId::from_bytes(
uuid::Uuid::parse_str("02ef21f3-c9eb-4087-880e-bedd7c4bbe43")
.unwrap()
.as_bytes(),
);
let actor2 = amp::ActorId::from_bytes(
uuid::Uuid::parse_str("2a1d376b-24f7-4400-8d4a-f58252d644dd")
.unwrap()
.as_bytes(),
);
let other_actor = amp::ActorId::random();
// A patch to create conflicting value at index 0
let patch1 = amp::Patch {
actor: None,
seq: None,
max_op: 2,
pending_changes: 0,
deps: Vec::new(),
clock: hashmap! {
other_actor.clone() => 1,
actor1.clone() => 1,
actor2.clone() => 1,
},
diffs: RootDiff {
props: hashmap! {
"birds".into() => hashmap!{
other_actor.op_id_at(1) => amp::Diff::List(amp::ListDiff{
object_id: other_actor.op_id_at(1).into(),
edits: vec![
amp::DiffEdit::SingleElementInsert{
index: 0,
elem_id: actor1.op_id_at(2).into(),
op_id: actor1.op_id_at(2),
value: amp::Diff::Map(amp::MapDiff{
object_id: actor1.op_id_at(2).into(),
props: hashmap!{
"species".into() => hashmap!{
actor1.op_id_at(3) => amp::Diff::Value("woodpecker".into()),
},
"numSeen".into() => hashmap!{
actor1.op_id_at(4) => amp::Diff::Value(amp::ScalarValue::Int(1)),
},
}
}),
},
amp::DiffEdit::Update{
index: 0,
op_id: actor2.op_id_at(2),
value: amp::Diff::Map(amp::MapDiff{
object_id: actor2.op_id_at(2).into(),
props: hashmap!{
"species".into() => hashmap!{
actor2.op_id_at(3) => amp::Diff::Value("lapwing".into()),
},
"numSeen".into() => hashmap!{
actor2.op_id_at(4) => amp::Diff::Value(amp::ScalarValue::Int(2)),
},
}
}),
},
],
})
}
},
},
};
let mut frontend = Frontend::new();
frontend.apply_patch(patch1).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(
hashmap! {"birds" => vec![hashmap!{"species" => Primitive::Str("lapwing".into()), "numSeen" => Primitive::Int(2)}]}
)
);
assert_eq!(
frontend
.get_conflicts(&Path::root().key("birds").index(0))
.unwrap(),
hashmap! {
actor1.op_id_at(2) => hashmap!{
"species" => Primitive::Str("woodpecker".into()),
"numSeen" => Primitive::Int(1),
}.into(),
actor2.op_id_at(2) => hashmap!{
"species" => Primitive::Str("lapwing".into()),
"numSeen" => Primitive::Int(2),
}.into(),
}
);
// Update the conflicting values
let patch2 = amp::Patch {
actor: None,
seq: None,
max_op: 5,
pending_changes: 0,
deps: Vec::new(),
clock: hashmap! {
actor1.clone() => 2,
actor2.clone() => 1,
},
diffs: RootDiff {
props: hashmap! {
"birds".into() => hashmap!{
other_actor.op_id_at(1) => amp::Diff::List(amp::ListDiff{
object_id: other_actor.op_id_at(1).into(),
edits: vec![
amp::DiffEdit::Update{
index: 0,
op_id: actor1.op_id_at(2),
value: amp::Diff::Map(amp::MapDiff{
object_id: actor1.op_id_at(2).into(),
props: hashmap!{
"numSeen".into() => hashmap!{
actor1.op_id_at(5) => amp::Diff::Value(amp::ScalarValue::Int(2)),
},
}
})
},
amp::DiffEdit::Update{
index: 0,
op_id: actor2.op_id_at(2),
value: amp::Diff::Map(amp::MapDiff{
object_id: actor2.op_id_at(2).into(),
props: HashMap::new(),
})
}
],
})
}
},
},
};
frontend.apply_patch(patch2).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(
hashmap! {"birds" => vec![hashmap!{"species" => Primitive::Str("lapwing".into()), "numSeen" => Primitive::Int(2)}]}
)
);
assert_eq!(
frontend
.get_conflicts(&Path::root().key("birds").index(0))
.unwrap(),
hashmap! {
actor1.op_id_at(2) => hashmap!{
"species" => Primitive::Str("woodpecker".into()),
"numSeen" => Primitive::Int(2),
}.into(),
actor2.op_id_at(2) => hashmap!{
"species" => Primitive::Str("lapwing".into()),
"numSeen" => Primitive::Int(2),
}.into(),
}
);
// Remove one of the conflicting values
let patch3 = amp::Patch {
actor: None,
seq: None,
max_op: 5,
deps: Vec::new(),
clock: hashmap! {
actor1 => 2,
actor2.clone() => 1,
},
diffs: RootDiff {
props: hashmap! {
"birds".into() => hashmap!{
other_actor.op_id_at(1) => amp::Diff::List(amp::ListDiff{
object_id: other_actor.op_id_at(1).into(),
edits: vec![
amp::DiffEdit::Update{
index: 0,
op_id: actor2.op_id_at(2),
value: amp::Diff::Map(amp::MapDiff{
object_id: actor2.op_id_at(2).into(),
props: HashMap::new(),
})
}
],
})
}
},
},
pending_changes: 0,
};
frontend.apply_patch(patch3).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(
hashmap! {"birds" => vec![hashmap!{"species" => Primitive::Str("lapwing".into()), "numSeen" => Primitive::Int(2)}]}
)
);
assert_eq!(
frontend
.get_conflicts(&Path::root().key("birds").index(0))
.unwrap(),
hashmap! {
actor2.op_id_at(2) => hashmap!{
"species" => Primitive::Str("lapwing".into()),
"numSeen" => Primitive::Int(2),
}.into(),
}
);
}
#[test]
fn delete_list_elements() {
let actor = amp::ActorId::random();
let mut frontend = Frontend::new();
let patch = amp::Patch {
actor: None,
seq: None,
max_op: 3,
pending_changes: 0,
deps: Vec::new(),
clock: hashmap! {
actor.clone() => 1,
},
diffs: RootDiff {
props: hashmap! {
"birds".into() => hashmap!{
actor.op_id_at(1) => amp::Diff::List(amp::ListDiff{
object_id: actor.op_id_at(1).into(),
edits: vec![
amp::DiffEdit::SingleElementInsert {
index: 0,
elem_id: actor.op_id_at(2).into(),
op_id: actor.op_id_at(2),
value: amp::Diff::Value("chaffinch".into())
},
amp::DiffEdit::SingleElementInsert {
index: 1,
elem_id: actor.op_id_at(3).into(),
op_id: actor.op_id_at(3),
value: amp::Diff::Value("goldfinch".into()),
},
],
})
}
},
},
};
frontend.apply_patch(patch).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(hashmap! {"birds" => vec!["chaffinch", "goldfinch"]})
);
let patch2 = amp::Patch {
actor: None,
seq: None,
max_op: 4,
pending_changes: 0,
deps: Vec::new(),
clock: hashmap! {
actor.clone() => 2,
},
diffs: RootDiff {
props: hashmap! {
"birds".into() => hashmap!{
actor.op_id_at(1) => amp::Diff::List(amp::ListDiff{
object_id: actor.op_id_at(1).into(),
edits: vec![amp::DiffEdit::Remove{ index: 0, count: 1 }],
})
}
},
},
};
frontend.apply_patch(patch2).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(hashmap! {"birds" => vec!["goldfinch"]})
);
}
#[test]
fn apply_updates_at_different_levels_of_object_tree() {
let actor = amp::ActorId::random();
let patch1 = amp::Patch {
clock: hashmap! {actor.clone() => 1},
seq: None,
max_op: 6,
pending_changes: 0,
actor: None,
deps: Vec::new(),
diffs: RootDiff {
props: hashmap! {
"counts".into() => hashmap!{
actor.op_id_at(1) => amp::Diff::Map(amp::MapDiff{
object_id: actor.op_id_at(1).into(),
props: hashmap!{
"magpie".into() => hashmap!{
actor.op_id_at(2) => amp::Diff::Value(amp::ScalarValue::Int(2))
}
}
})
},
"details".into() => hashmap!{
actor.op_id_at(3) => amp::Diff::List(amp::ListDiff{
object_id: actor.op_id_at(3).into(),
edits: vec![amp::DiffEdit::SingleElementInsert{
index: 0,
elem_id: actor.op_id_at(4).into(),
op_id: actor.op_id_at(4),
value: amp::Diff::Map(amp::MapDiff{
object_id: actor.op_id_at(4).into(),
props: hashmap!{
"species".into() => hashmap!{
actor.op_id_at(5) => amp::Diff::Value("magpie".into())
},
"family".into() => hashmap!{
actor.op_id_at(6) => amp::Diff::Value("Corvidae".into())
}
}
})
}],
})
},
},
},
};
let mut frontend = Frontend::new();
frontend.apply_patch(patch1).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(hashmap! {
"counts" => Into::<Value>::into(hashmap!{"magpie".to_string() => Primitive::Int(2)}),
"details" => vec![Into::<Value>::into(hashmap!{
"species" => "magpie",
"family" => "Corvidae",
})].into()
})
);
let patch2 = amp::Patch {
clock: hashmap! {actor.clone() => 2},
seq: None,
max_op: 7,
pending_changes: 0,
actor: None,
deps: Vec::new(),
diffs: RootDiff {
props: hashmap! {
"counts".into() => hashmap!{
actor.op_id_at(1) => amp::Diff::Map(amp::MapDiff{
object_id: actor.op_id_at(1).into(),
props: hashmap!{
"magpie".into() => hashmap!{
actor.op_id_at(7) => amp::Diff::Value(amp::ScalarValue::Int(3))
}
}
})
},
"details".into() => hashmap!{
actor.op_id_at(3) => amp::Diff::List(amp::ListDiff{
object_id: actor.op_id_at(3).into(),
edits: vec![amp::DiffEdit::Update{
index: 0,
op_id: actor.op_id_at(4),
value: amp::Diff::Map(amp::MapDiff{
object_id: actor.op_id_at(4).into(),
props: hashmap!{
"species".into() => hashmap!{
actor.op_id_at(8) => amp::Diff::Value("Eurasian magpie".into())
},
}
}),
}],
})
},
},
},
};
frontend.apply_patch(patch2).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(hashmap! {
"counts" => Into::<Value>::into(hashmap!{"magpie".to_string() => Primitive::Int(3)}),
"details" => vec![Into::<Value>::into(hashmap!{
"species" => "Eurasian magpie",
"family" => "Corvidae",
})].into()
})
);
}
#[test]
fn test_text_objects() {
let actor = amp::ActorId::random();
let mut frontend = Frontend::new();
let patch = amp::Patch {
actor: None,
seq: None,
max_op: 4,
pending_changes: 0,
deps: Vec::new(),
clock: hashmap! {
actor.clone() => 2,
},
diffs: RootDiff {
props: hashmap! {
"name".into() => hashmap!{
actor.op_id_at(1) => amp::Diff::Text(amp::TextDiff{
object_id: actor.op_id_at(1).into(),
edits: vec![
amp::DiffEdit::SingleElementInsert {
index: 0,
elem_id: actor.op_id_at(2).into(),
op_id: actor.op_id_at(2),
value: amp::Diff::Value("b".into()),
},
amp::DiffEdit::SingleElementInsert {
index: 1,
elem_id: actor.op_id_at(3).into(),
op_id: actor.op_id_at(3),
value: amp::Diff::Value("e".into()),
},
amp::DiffEdit::SingleElementInsert {
index: 2,
elem_id: actor.op_id_at(4).into(),
op_id: actor.op_id_at(4),
value: amp::Diff::Value("n".into()),
},
],
})
}
},
},
};
frontend.apply_patch(patch).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(
hashmap! {"name" => Value::Text("ben".graphemes(true).map(|s|s.into()).collect())}
)
);
let patch2 = amp::Patch {
actor: None,
seq: None,
max_op: 5,
pending_changes: 0,
deps: Vec::new(),
clock: hashmap! {
actor.clone() => 3,
},
diffs: RootDiff {
props: hashmap! {
"name".into() => hashmap!{
actor.op_id_at(1) => amp::Diff::Text(amp::TextDiff{
object_id: actor.op_id_at(1).into(),
edits: vec![
amp::DiffEdit::Remove { index: 1, count: 1 },
amp::DiffEdit::Update{
index: 1,
op_id: actor.op_id_at(5),
value: amp::Diff::Value(amp::ScalarValue::Str("i".into())),
}
],
})
}
},
},
};
frontend.apply_patch(patch2).unwrap();
assert_eq!(
frontend.state(),
&Into::<Value>::into(
hashmap! {"name" => Value::Text("bi".graphemes(true).map(|s|s.into()).collect())}
)
);
}
#[test]
fn test_unchanged_diff_creates_empty_objects() {
let mut doc = Frontend::new();
let patch = amp::Patch {
actor: Some(doc.actor_id.clone()),
seq: Some(1),
clock: hashmap! {doc.actor_id.clone() => 1},
deps: Vec::new(),
max_op: 1,
pending_changes: 0,
diffs: RootDiff {
props: hashmap! {
"text".into() => hashmap!{
"1@cfe5fefb771f4c15a716d488012cbf40".try_into().unwrap() => amp::Diff::Text(amp::TextDiff{
object_id: "1@cfe5fefb771f4c15a716d488012cbf40".try_into().unwrap(),
edits: Vec::new(),
})
}
},
},
};
doc.apply_patch(patch).unwrap();
assert_eq!(
doc.state(),
&Value::Map(hashmap! {"text".into() => Value::Text(Vec::new())},),
);
}