automerge/rust/automerge/src/automerge/current_state.rs
Conrad Irwin 44fa7ac416
Don't panic on missing deps of change chunks (#538)
* Fix doubly-reported ops in load of change chunks

Since c3c04128f5, observers have been
called twice when calling Automerge::load() with change chunks.

* Better handle change chunks with missing deps

Before this change Automerge::load would panic if you passed a change
chunk that was missing a dependency, or multiple change chunks not in
strict dependency order. After this change these cases will error
instead.
2023-02-27 20:12:09 +00:00

915 lines
30 KiB
Rust

use std::{borrow::Cow, collections::HashSet, iter::Peekable};
use itertools::Itertools;
use crate::{
types::{ElemId, Key, ListEncoding, ObjId, Op, OpId},
ObjType, OpObserver, OpType, ScalarValue, Value,
};
/// Traverse the "current" state of the document, notifying `observer`
///
/// The "current" state of the document is the set of visible operations. This function will
/// traverse that set of operations and call the corresponding methods on the `observer` as it
/// encounters values. The `observer` methods will be called in the order in which they appear in
/// the document. That is to say that the observer will be notified of parent objects before the
/// objects they contain and elements of a sequence will be notified in the order they occur.
///
/// Due to only notifying of visible operations the observer will only be called with `put`,
/// `insert`, and `splice`, operations.
pub(super) fn observe_current_state<O: OpObserver>(doc: &crate::Automerge, observer: &mut O) {
// The OpSet already exposes operations in the order they appear in the document.
// `OpSet::iter_objs` iterates over the objects in causal order, this means that parent objects
// will always appear before their children. Furthermore, the operations within each object are
// ordered by key (which means by their position in a sequence for sequences).
//
// Effectively then we iterate over each object, then we group the operations in the object by
// key and for each key find the visible operations for that key. Then we notify the observer
// for each of those visible operations.
let mut visible_objs = HashSet::new();
visible_objs.insert(ObjId::root());
for (obj, typ, ops) in doc.ops().iter_objs() {
if !visible_objs.contains(obj) {
continue;
}
let ops_by_key = ops.group_by(|o| o.key);
let actions = ops_by_key
.into_iter()
.flat_map(|(key, key_ops)| key_actions(key, key_ops));
if typ == ObjType::Text && !observer.text_as_seq() {
track_new_objs_and_notify(
&mut visible_objs,
doc,
obj,
typ,
observer,
text_actions(actions),
)
} else if typ == ObjType::List {
track_new_objs_and_notify(
&mut visible_objs,
doc,
obj,
typ,
observer,
list_actions(actions),
)
} else {
track_new_objs_and_notify(&mut visible_objs, doc, obj, typ, observer, actions)
}
}
}
fn track_new_objs_and_notify<N: Action, I: Iterator<Item = N>, O: OpObserver>(
visible_objs: &mut HashSet<ObjId>,
doc: &crate::Automerge,
obj: &ObjId,
typ: ObjType,
observer: &mut O,
actions: I,
) {
let exid = doc.id_to_exid(obj.0);
for action in actions {
if let Some(obj) = action.made_object() {
visible_objs.insert(obj);
}
action.notify_observer(doc, &exid, obj, typ, observer);
}
}
trait Action {
/// Notify an observer of whatever this action does
fn notify_observer<O: OpObserver>(
self,
doc: &crate::Automerge,
exid: &crate::ObjId,
obj: &ObjId,
typ: ObjType,
observer: &mut O,
);
/// If this action created an object, return the ID of that object
fn made_object(&self) -> Option<ObjId>;
}
fn key_actions<'a, I: Iterator<Item = &'a Op>>(
key: Key,
key_ops: I,
) -> impl Iterator<Item = SimpleAction<'a>> {
#[derive(Clone)]
enum CurrentOp<'a> {
Put {
value: Value<'a>,
id: OpId,
conflicted: bool,
},
Insert(Value<'a>, OpId),
}
let current_ops = key_ops
.filter(|o| o.visible())
.filter_map(|o| match o.action {
OpType::Make(obj_type) => {
let value = Value::Object(obj_type);
if o.insert {
Some(CurrentOp::Insert(value, o.id))
} else {
Some(CurrentOp::Put {
value,
id: o.id,
conflicted: false,
})
}
}
OpType::Put(ref value) => {
let value = Value::Scalar(Cow::Borrowed(value));
if o.insert {
Some(CurrentOp::Insert(value, o.id))
} else {
Some(CurrentOp::Put {
value,
id: o.id,
conflicted: false,
})
}
}
_ => None,
});
current_ops
.coalesce(|previous, current| match (previous, current) {
(CurrentOp::Put { .. }, CurrentOp::Put { value, id, .. }) => Ok(CurrentOp::Put {
value,
id,
conflicted: true,
}),
(previous, current) => Err((previous, current)),
})
.map(move |op| match op {
CurrentOp::Put {
value,
id,
conflicted,
} => SimpleAction::Put {
prop: key,
tagged_value: (value, id),
conflict: conflicted,
},
CurrentOp::Insert(val, id) => SimpleAction::Insert {
elem_id: ElemId(id),
tagged_value: (val, id),
},
})
}
/// Either a "put" or "insert" action. i.e. not splicing for text values
enum SimpleAction<'a> {
Put {
prop: Key,
tagged_value: (Value<'a>, OpId),
conflict: bool,
},
Insert {
elem_id: ElemId,
tagged_value: (Value<'a>, OpId),
},
}
impl<'a> Action for SimpleAction<'a> {
fn notify_observer<O: OpObserver>(
self,
doc: &crate::Automerge,
exid: &crate::ObjId,
obj: &ObjId,
typ: ObjType,
observer: &mut O,
) {
let encoding = match typ {
ObjType::Text => ListEncoding::Text(doc.text_encoding()),
_ => ListEncoding::List,
};
match self {
Self::Put {
prop,
tagged_value,
conflict,
} => {
let tagged_value = (tagged_value.0, doc.id_to_exid(tagged_value.1));
let prop = doc.ops().export_key(*obj, prop, encoding).unwrap();
observer.put(doc, exid.clone(), prop, tagged_value, conflict);
}
Self::Insert {
elem_id,
tagged_value: (value, opid),
} => {
let index = doc
.ops()
.search(obj, crate::query::ElemIdPos::new(elem_id, encoding))
.index()
.unwrap();
let tagged_value = (value, doc.id_to_exid(opid));
observer.insert(doc, doc.id_to_exid(obj.0), index, tagged_value);
}
}
}
fn made_object(&self) -> Option<ObjId> {
match self {
Self::Put {
tagged_value: (Value::Object(_), id),
..
} => Some((*id).into()),
Self::Insert {
tagged_value: (Value::Object(_), id),
..
} => Some((*id).into()),
_ => None,
}
}
}
/// An `Action` which splices for text values
enum TextAction<'a> {
Action(SimpleAction<'a>),
Splice { start: ElemId, chars: String },
}
impl<'a> Action for TextAction<'a> {
fn notify_observer<O: OpObserver>(
self,
doc: &crate::Automerge,
exid: &crate::ObjId,
obj: &ObjId,
typ: ObjType,
observer: &mut O,
) {
match self {
Self::Action(action) => action.notify_observer(doc, exid, obj, typ, observer),
Self::Splice { start, chars } => {
let index = doc
.ops()
.search(
obj,
crate::query::ElemIdPos::new(
start,
ListEncoding::Text(doc.text_encoding()),
),
)
.index()
.unwrap();
observer.splice_text(doc, doc.id_to_exid(obj.0), index, chars.as_str());
}
}
}
fn made_object(&self) -> Option<ObjId> {
match self {
Self::Action(action) => action.made_object(),
_ => None,
}
}
}
fn list_actions<'a, I: Iterator<Item = SimpleAction<'a>>>(
actions: I,
) -> impl Iterator<Item = SimpleAction<'a>> {
actions.map(|a| match a {
SimpleAction::Put {
prop: Key::Seq(elem_id),
tagged_value,
..
} => SimpleAction::Insert {
elem_id,
tagged_value,
},
a => a,
})
}
/// Condense consecutive `SimpleAction::Insert` actions into one `TextAction::Splice`
fn text_actions<'a, I>(actions: I) -> impl Iterator<Item = TextAction<'a>>
where
I: Iterator<Item = SimpleAction<'a>>,
{
TextActions {
ops: actions.peekable(),
}
}
struct TextActions<'a, I: Iterator<Item = SimpleAction<'a>>> {
ops: Peekable<I>,
}
impl<'a, I: Iterator<Item = SimpleAction<'a>>> Iterator for TextActions<'a, I> {
type Item = TextAction<'a>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(SimpleAction::Insert { .. }) = self.ops.peek() {
let (start, value) = match self.ops.next() {
Some(SimpleAction::Insert {
tagged_value: (value, opid),
..
}) => (opid, value),
_ => unreachable!(),
};
let mut chars = match value {
Value::Scalar(Cow::Borrowed(ScalarValue::Str(s))) => s.to_string(),
_ => "\u{fffc}".to_string(),
};
while let Some(SimpleAction::Insert { .. }) = self.ops.peek() {
if let Some(SimpleAction::Insert {
tagged_value: (value, _),
..
}) = self.ops.next()
{
match value {
Value::Scalar(Cow::Borrowed(ScalarValue::Str(s))) => chars.push_str(s),
_ => chars.push('\u{fffc}'),
}
}
}
Some(TextAction::Splice {
start: ElemId(start),
chars,
})
} else {
self.ops.next().map(TextAction::Action)
}
}
}
#[cfg(test)]
mod tests {
use std::{borrow::Cow, fs};
use crate::{transaction::Transactable, Automerge, ObjType, OpObserver, Prop, ReadDoc, Value};
// Observer ops often carry a "tagged value", which is a value and the OpID of the op which
// created that value. For a lot of values (i.e. any scalar value) we don't care about the
// opid. This type implements `PartialEq` for the `Untagged` variant by ignoring the tag, which
// allows us to express tests which don't care about the tag.
#[derive(Clone, Debug)]
enum ObservedValue {
Tagged(crate::Value<'static>, crate::ObjId),
Untagged(crate::Value<'static>),
}
impl<'a> From<(Value<'a>, crate::ObjId)> for ObservedValue {
fn from(value: (Value<'a>, crate::ObjId)) -> Self {
Self::Tagged(value.0.into_owned(), value.1)
}
}
impl PartialEq<ObservedValue> for ObservedValue {
fn eq(&self, other: &ObservedValue) -> bool {
match (self, other) {
(Self::Tagged(v1, o1), Self::Tagged(v2, o2)) => equal_vals(v1, v2) && o1 == o2,
(Self::Untagged(v1), Self::Untagged(v2)) => equal_vals(v1, v2),
(Self::Tagged(v1, _), Self::Untagged(v2)) => equal_vals(v1, v2),
(Self::Untagged(v1), Self::Tagged(v2, _)) => equal_vals(v1, v2),
}
}
}
/// Consider counters equal if they have the same current value
fn equal_vals(v1: &Value<'_>, v2: &Value<'_>) -> bool {
match (v1, v2) {
(Value::Scalar(v1), Value::Scalar(v2)) => match (v1.as_ref(), v2.as_ref()) {
(crate::ScalarValue::Counter(c1), crate::ScalarValue::Counter(c2)) => {
c1.current == c2.current
}
_ => v1 == v2,
},
_ => v1 == v2,
}
}
#[derive(Debug, Clone, PartialEq)]
enum ObserverCall {
Put {
obj: crate::ObjId,
prop: Prop,
value: ObservedValue,
conflict: bool,
},
Insert {
obj: crate::ObjId,
index: usize,
value: ObservedValue,
},
SpliceText {
obj: crate::ObjId,
index: usize,
chars: String,
},
}
// A Vec<ObserverCall> is pretty hard to look at in a test failure. This wrapper prints the
// calls out in a nice table so it's easier to see what's different
#[derive(Clone, PartialEq)]
struct Calls(Vec<ObserverCall>);
impl std::fmt::Debug for Calls {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut table = prettytable::Table::new();
table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
table.set_titles(prettytable::row![
"Op", "Object", "Property", "Value", "Conflict"
]);
for call in &self.0 {
match call {
ObserverCall::Put {
obj,
prop,
value,
conflict,
} => {
table.add_row(prettytable::row![
"Put",
format!("{}", obj),
prop,
match value {
ObservedValue::Tagged(v, o) => format!("{} ({})", v, o),
ObservedValue::Untagged(v) => format!("{}", v),
},
conflict
]);
}
ObserverCall::Insert { obj, index, value } => {
table.add_row(prettytable::row![
"Insert",
format!("{}", obj),
index,
match value {
ObservedValue::Tagged(v, o) => format!("{} ({})", v, o),
ObservedValue::Untagged(v) => format!("{}", v),
},
""
]);
}
ObserverCall::SpliceText { obj, index, chars } => {
table.add_row(prettytable::row![
"SpliceText",
format!("{}", obj),
index,
chars,
""
]);
}
}
}
let mut out = Vec::new();
table.print(&mut out).unwrap();
write!(f, "\n{}\n", String::from_utf8(out).unwrap())
}
}
struct ObserverStub {
ops: Vec<ObserverCall>,
text_as_seq: bool,
}
impl ObserverStub {
fn new() -> Self {
Self {
ops: Vec::new(),
text_as_seq: true,
}
}
fn new_text_v2() -> Self {
Self {
ops: Vec::new(),
text_as_seq: false,
}
}
}
impl OpObserver for ObserverStub {
fn insert<R: ReadDoc>(
&mut self,
_doc: &R,
objid: crate::ObjId,
index: usize,
tagged_value: (crate::Value<'_>, crate::ObjId),
) {
self.ops.push(ObserverCall::Insert {
obj: objid,
index,
value: tagged_value.into(),
});
}
fn splice_text<R: ReadDoc>(
&mut self,
_doc: &R,
objid: crate::ObjId,
index: usize,
value: &str,
) {
self.ops.push(ObserverCall::SpliceText {
obj: objid,
index,
chars: value.to_string(),
});
}
fn put<R: ReadDoc>(
&mut self,
_doc: &R,
objid: crate::ObjId,
prop: crate::Prop,
tagged_value: (crate::Value<'_>, crate::ObjId),
conflict: bool,
) {
self.ops.push(ObserverCall::Put {
obj: objid,
prop,
value: tagged_value.into(),
conflict,
});
}
fn expose<R: ReadDoc>(
&mut self,
_doc: &R,
_objid: crate::ObjId,
_prop: crate::Prop,
_tagged_value: (crate::Value<'_>, crate::ObjId),
_conflict: bool,
) {
panic!("expose not expected");
}
fn increment<R: ReadDoc>(
&mut self,
_doc: &R,
_objid: crate::ObjId,
_prop: crate::Prop,
_tagged_value: (i64, crate::ObjId),
) {
panic!("increment not expected");
}
fn delete_map<R: ReadDoc>(&mut self, _doc: &R, _objid: crate::ObjId, _key: &str) {
panic!("delete not expected");
}
fn delete_seq<R: ReadDoc>(
&mut self,
_doc: &R,
_objid: crate::ObjId,
_index: usize,
_num: usize,
) {
panic!("delete not expected");
}
fn text_as_seq(&self) -> bool {
self.text_as_seq
}
}
#[test]
fn basic_test() {
let mut doc = crate::AutoCommit::new();
doc.put(crate::ROOT, "key", "value").unwrap();
let map = doc.put_object(crate::ROOT, "map", ObjType::Map).unwrap();
doc.put(&map, "nested_key", "value").unwrap();
let list = doc.put_object(crate::ROOT, "list", ObjType::List).unwrap();
doc.insert(&list, 0, "value").unwrap();
let text = doc.put_object(crate::ROOT, "text", ObjType::Text).unwrap();
doc.insert(&text, 0, "a").unwrap();
let mut obs = ObserverStub::new();
super::observe_current_state(doc.document(), &mut obs);
assert_eq!(
Calls(obs.ops),
Calls(vec![
ObserverCall::Put {
obj: crate::ROOT,
prop: "key".into(),
value: ObservedValue::Untagged("value".into()),
conflict: false,
},
ObserverCall::Put {
obj: crate::ROOT,
prop: "list".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::List), list.clone()),
conflict: false,
},
ObserverCall::Put {
obj: crate::ROOT,
prop: "map".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::Map), map.clone()),
conflict: false,
},
ObserverCall::Put {
obj: crate::ROOT,
prop: "text".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::Text), text.clone()),
conflict: false,
},
ObserverCall::Put {
obj: map.clone(),
prop: "nested_key".into(),
value: ObservedValue::Untagged("value".into()),
conflict: false,
},
ObserverCall::Insert {
obj: list,
index: 0,
value: ObservedValue::Untagged("value".into()),
},
ObserverCall::Insert {
obj: text,
index: 0,
value: ObservedValue::Untagged("a".into()),
},
])
);
}
#[test]
fn test_deleted_ops_omitted() {
let mut doc = crate::AutoCommit::new();
doc.put(crate::ROOT, "key", "value").unwrap();
doc.delete(crate::ROOT, "key").unwrap();
let map = doc.put_object(crate::ROOT, "map", ObjType::Map).unwrap();
doc.put(&map, "nested_key", "value").unwrap();
doc.delete(&map, "nested_key").unwrap();
let list = doc.put_object(crate::ROOT, "list", ObjType::List).unwrap();
doc.insert(&list, 0, "value").unwrap();
doc.delete(&list, 0).unwrap();
let text = doc.put_object(crate::ROOT, "text", ObjType::Text).unwrap();
doc.insert(&text, 0, "a").unwrap();
doc.delete(&text, 0).unwrap();
doc.put_object(crate::ROOT, "deleted_map", ObjType::Map)
.unwrap();
doc.delete(crate::ROOT, "deleted_map").unwrap();
doc.put_object(crate::ROOT, "deleted_list", ObjType::List)
.unwrap();
doc.delete(crate::ROOT, "deleted_list").unwrap();
doc.put_object(crate::ROOT, "deleted_text", ObjType::Text)
.unwrap();
doc.delete(crate::ROOT, "deleted_text").unwrap();
let mut obs = ObserverStub::new();
super::observe_current_state(doc.document(), &mut obs);
assert_eq!(
Calls(obs.ops),
Calls(vec![
ObserverCall::Put {
obj: crate::ROOT,
prop: "list".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::List), list.clone()),
conflict: false,
},
ObserverCall::Put {
obj: crate::ROOT,
prop: "map".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::Map), map.clone()),
conflict: false,
},
ObserverCall::Put {
obj: crate::ROOT,
prop: "text".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::Text), text.clone()),
conflict: false,
},
])
);
}
#[test]
fn test_text_spliced() {
let mut doc = crate::AutoCommit::new();
let text = doc.put_object(crate::ROOT, "text", ObjType::Text).unwrap();
doc.insert(&text, 0, "a").unwrap();
doc.splice_text(&text, 1, 0, "bcdef").unwrap();
doc.splice_text(&text, 2, 2, "g").unwrap();
let mut obs = ObserverStub::new_text_v2();
super::observe_current_state(doc.document(), &mut obs);
assert_eq!(
Calls(obs.ops),
Calls(vec![
ObserverCall::Put {
obj: crate::ROOT,
prop: "text".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::Text), text.clone()),
conflict: false,
},
ObserverCall::SpliceText {
obj: text,
index: 0,
chars: "abgef".to_string()
}
])
);
}
#[test]
fn test_counters() {
let actor1 = crate::ActorId::from("aa".as_bytes());
let actor2 = crate::ActorId::from("bb".as_bytes());
let mut doc = crate::AutoCommit::new().with_actor(actor2);
let mut doc2 = doc.fork().with_actor(actor1);
doc2.put(crate::ROOT, "key", "someval").unwrap();
doc.put(crate::ROOT, "key", crate::ScalarValue::Counter(1.into()))
.unwrap();
doc.increment(crate::ROOT, "key", 2).unwrap();
doc.increment(crate::ROOT, "key", 3).unwrap();
doc.merge(&mut doc2).unwrap();
let mut obs = ObserverStub::new_text_v2();
super::observe_current_state(doc.document(), &mut obs);
assert_eq!(
Calls(obs.ops),
Calls(vec![ObserverCall::Put {
obj: crate::ROOT,
prop: "key".into(),
value: ObservedValue::Untagged(Value::Scalar(Cow::Owned(
crate::ScalarValue::Counter(6.into())
))),
conflict: true,
},])
);
}
#[test]
fn test_multiple_list_insertions() {
let mut doc = crate::AutoCommit::new();
let list = doc.put_object(crate::ROOT, "list", ObjType::List).unwrap();
doc.insert(&list, 0, 1).unwrap();
doc.insert(&list, 1, 2).unwrap();
let mut obs = ObserverStub::new_text_v2();
super::observe_current_state(doc.document(), &mut obs);
assert_eq!(
Calls(obs.ops),
Calls(vec![
ObserverCall::Put {
obj: crate::ROOT,
prop: "list".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::List), list.clone()),
conflict: false,
},
ObserverCall::Insert {
obj: list.clone(),
index: 0,
value: ObservedValue::Untagged(1.into()),
},
ObserverCall::Insert {
obj: list,
index: 1,
value: ObservedValue::Untagged(2.into()),
},
])
);
}
#[test]
fn test_concurrent_insertions_at_same_index() {
let mut doc = crate::AutoCommit::new().with_actor(crate::ActorId::from("aa".as_bytes()));
let list = doc.put_object(crate::ROOT, "list", ObjType::List).unwrap();
let mut doc2 = doc.fork().with_actor(crate::ActorId::from("bb".as_bytes()));
doc.insert(&list, 0, 1).unwrap();
doc2.insert(&list, 0, 2).unwrap();
doc.merge(&mut doc2).unwrap();
let mut obs = ObserverStub::new_text_v2();
super::observe_current_state(doc.document(), &mut obs);
assert_eq!(
Calls(obs.ops),
Calls(vec![
ObserverCall::Put {
obj: crate::ROOT,
prop: "list".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::List), list.clone()),
conflict: false,
},
ObserverCall::Insert {
obj: list.clone(),
index: 0,
value: ObservedValue::Untagged(2.into()),
},
ObserverCall::Insert {
obj: list,
index: 1,
value: ObservedValue::Untagged(1.into()),
},
])
);
}
#[test]
fn test_insert_objects() {
let mut doc = crate::AutoCommit::new().with_actor(crate::ActorId::from("aa".as_bytes()));
let list = doc.put_object(crate::ROOT, "list", ObjType::List).unwrap();
let map = doc.insert_object(&list, 0, ObjType::Map).unwrap();
doc.put(&map, "key", "value").unwrap();
let mut obs = ObserverStub::new_text_v2();
super::observe_current_state(doc.document(), &mut obs);
assert_eq!(
Calls(obs.ops),
Calls(vec![
ObserverCall::Put {
obj: crate::ROOT,
prop: "list".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::List), list.clone()),
conflict: false,
},
ObserverCall::Insert {
obj: list.clone(),
index: 0,
value: ObservedValue::Tagged(Value::Object(ObjType::Map), map.clone()),
},
ObserverCall::Put {
obj: map,
prop: "key".into(),
value: ObservedValue::Untagged("value".into()),
conflict: false
},
])
);
}
#[test]
fn test_insert_and_update() {
let mut doc = crate::AutoCommit::new();
let list = doc.put_object(crate::ROOT, "list", ObjType::List).unwrap();
doc.insert(&list, 0, "one").unwrap();
doc.insert(&list, 1, "two").unwrap();
doc.put(&list, 0, "three").unwrap();
doc.put(&list, 1, "four").unwrap();
let mut obs = ObserverStub::new_text_v2();
super::observe_current_state(doc.document(), &mut obs);
assert_eq!(
Calls(obs.ops),
Calls(vec![
ObserverCall::Put {
obj: crate::ROOT,
prop: "list".into(),
value: ObservedValue::Tagged(Value::Object(ObjType::List), list.clone()),
conflict: false,
},
ObserverCall::Insert {
obj: list.clone(),
index: 0,
value: ObservedValue::Untagged("three".into()),
},
ObserverCall::Insert {
obj: list.clone(),
index: 1,
value: ObservedValue::Untagged("four".into()),
},
])
);
}
#[test]
fn test_load_changes() {
fn fixture(name: &str) -> Vec<u8> {
fs::read("./tests/fixtures/".to_owned() + name).unwrap()
}
let mut obs = ObserverStub::new();
let _doc = Automerge::load_with(
&fixture("counter_value_is_ok.automerge"),
crate::OnPartialLoad::Error,
crate::storage::VerificationMode::Check,
Some(&mut obs),
);
assert_eq!(
Calls(obs.ops),
Calls(vec![ObserverCall::Put {
obj: crate::ROOT,
prop: "a".into(),
value: ObservedValue::Untagged(crate::ScalarValue::Counter(2000.into()).into()),
conflict: false,
},])
);
}
}