aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/document/changes.rs11
-rw-r--r--src/document/file.rs7
-rw-r--r--src/document/mod.rs2
-rw-r--r--src/document/store.rs70
-rw-r--r--src/document/sync.rs19
-rw-r--r--src/document/testing.rs29
-rw-r--r--src/remake/catchup.rs12
-rw-r--r--src/remake/delta.rs117
-rw-r--r--src/remake/kind.rs34
-rw-r--r--src/remake/records.rs91
-rw-r--r--src/remake/snapshot.rs166
-rw-r--r--src/swarm/members.rs17
12 files changed, 325 insertions, 250 deletions
diff --git a/src/document/changes.rs b/src/document/changes.rs
index 655063b..33029fc 100644
--- a/src/document/changes.rs
+++ b/src/document/changes.rs
@@ -101,12 +101,13 @@ pub(crate) fn load_delta(doc: &mut Document, bytes: &[u8]) -> Result<()> {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::document::testing::change;
use automerge::{ActorId, ROOT, transaction::Transactable};
fn random_write(rng: &mut crate::testrand::Rng, doc: &mut Document) {
let key = format!("k{}", rng.below(6));
- doc.put(ROOT, key, rng.below(100) as i64).unwrap();
- doc.commit();
+ let value = rng.below(100) as i64;
+ change(doc, |doc| doc.put(ROOT, key, value).unwrap());
}
fn sorted(mut heads: Vec<ChangeHash>) -> Vec<ChangeHash> {
@@ -158,8 +159,7 @@ mod tests {
#[test]
fn no_delta_without_its_heads() {
let mut doc = Document::new();
- doc.put(ROOT, "k", 1_i64).unwrap();
- doc.commit();
+ change(&mut doc, |doc| doc.put(ROOT, "k", 1_i64).unwrap());
assert!(delta_bytes(&mut doc, &[], &[ChangeHash([7; 32])]).is_none());
}
@@ -167,8 +167,7 @@ mod tests {
#[test]
fn a_change_is_checked_against_its_hash() {
let mut doc = Document::new();
- doc.put(ROOT, "k", 1_i64).unwrap();
- doc.commit();
+ change(&mut doc, |doc| doc.put(ROOT, "k", 1_i64).unwrap());
let hash = doc.get_heads()[0];
let bytes = change_bytes(&mut doc, &hash).unwrap();
assert_eq!(change_from_bytes(bytes.clone(), hash).unwrap().hash(), hash);
diff --git a/src/document/file.rs b/src/document/file.rs
index e03c2e6..6439e27 100644
--- a/src/document/file.rs
+++ b/src/document/file.rs
@@ -322,19 +322,18 @@ impl DocumentFile {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::document::testing::change;
use automerge::{ROOT, ReadDoc, transaction::Transactable};
#[test]
fn whole_then_increments() {
let mut doc = AutoCommit::new();
- doc.put(ROOT, "a", 1_i64).unwrap();
- doc.commit();
+ change(&mut doc, |doc| doc.put(ROOT, "a", 1_i64).unwrap());
let actor = doc.get_actor().clone();
let heads = doc.get_heads();
let mut bytes = whole(std::slice::from_ref(&actor), &doc.save()).unwrap();
- doc.put(ROOT, "b", 2_i64).unwrap();
- doc.commit();
+ change(&mut doc, |doc| doc.put(ROOT, "b", 2_i64).unwrap());
bytes.extend(record(&doc.save_after(&heads)));
let (loaded, dropped) = parse(&bytes).unwrap();
diff --git a/src/document/mod.rs b/src/document/mod.rs
index f11fef0..a348774 100644
--- a/src/document/mod.rs
+++ b/src/document/mod.rs
@@ -37,6 +37,8 @@ pub(crate) mod meta;
pub(crate) mod replica;
pub(crate) mod store;
pub(crate) mod sync;
+#[cfg(test)]
+pub(crate) mod testing;
#[cfg(test)]
mod tests {
diff --git a/src/document/store.rs b/src/document/store.rs
index dff0cfb..c6c428c 100644
--- a/src/document/store.rs
+++ b/src/document/store.rs
@@ -1071,6 +1071,7 @@ fn nested<T>(
#[cfg(test)]
mod tests {
use super::*;
+ use crate::document::testing::try_change;
use serde_json::json;
fn path(at: &str) -> Vec<String> {
@@ -1080,7 +1081,10 @@ mod tests {
#[test]
fn paths_go_through_lists() {
let mut doc = AutoCommit::new();
- write_path(&mut doc, &path("list"), &json!(["b", {"k": 1}, "d"])).unwrap();
+ try_change(&mut doc, |doc| {
+ write_path(doc, &path("list"), &json!(["b", {"k": 1}, "d"]))
+ })
+ .unwrap();
assert_eq!(read_path(&doc, &path("list/0")).unwrap(), Some(json!("b")));
assert_eq!(read_path(&doc, &path("list/1/k")).unwrap(), Some(json!(1)));
// Not an index, or past the end: nothing.
@@ -1088,18 +1092,27 @@ mod tests {
assert_eq!(read_path(&doc, &path(missing)).unwrap(), None, "{missing}");
}
// Replaced, and appended at the end; past it, nothing.
- write_path(&mut doc, &path("list/0"), &json!("a")).unwrap();
- write_path(&mut doc, &path("list/3"), &json!("e")).unwrap();
- write_path(&mut doc, &path("list/1/k"), &json!(2)).unwrap();
+ try_change(&mut doc, |doc| {
+ write_path(doc, &path("list/0"), &json!("a"))
+ })
+ .unwrap();
+ try_change(&mut doc, |doc| {
+ write_path(doc, &path("list/3"), &json!("e"))
+ })
+ .unwrap();
+ try_change(&mut doc, |doc| {
+ write_path(doc, &path("list/1/k"), &json!(2))
+ })
+ .unwrap();
assert!(matches!(
- write_path(&mut doc, &path("list/9"), &json!(0)),
+ try_change(&mut doc, |doc| write_path(doc, &path("list/9"), &json!(0))),
Err(StoreError::NotFound)
));
assert_eq!(
read_path(&doc, &path("list")).unwrap(),
Some(json!(["a", {"k": 2}, "d", "e"]))
);
- delete_path(&mut doc, &path("list/2"), false).unwrap();
+ try_change(&mut doc, |doc| delete_path(doc, &path("list/2"), false)).unwrap();
assert_eq!(
read_path(&doc, &path("list")).unwrap(),
Some(json!(["a", {"k": 2}, "e"]))
@@ -1113,8 +1126,14 @@ mod tests {
#[test]
fn negative_indexes_count_from_the_end() {
let mut doc = AutoCommit::new();
- write_path(&mut doc, &path("list"), &json!(["a", "b"])).unwrap();
- write_path(&mut doc, &path("map"), &json!({"-1": "a key"})).unwrap();
+ try_change(&mut doc, |doc| {
+ write_path(doc, &path("list"), &json!(["a", "b"]))
+ })
+ .unwrap();
+ try_change(&mut doc, |doc| {
+ write_path(doc, &path("map"), &json!({"-1": "a key"}))
+ })
+ .unwrap();
assert_eq!(
positive_path(&doc, &path("list/-1"), false).unwrap(),
Some(path("list/1"))
@@ -1135,28 +1154,47 @@ mod tests {
#[test]
fn inserts_into_lists() {
let mut doc = AutoCommit::new();
- write_path(&mut doc, &path("list"), &json!(["b", "d"])).unwrap();
- insert_path(&mut doc, &path("list/0"), &json!("a")).unwrap();
- insert_path(&mut doc, &path("list/2"), &json!("c")).unwrap();
- insert_path(&mut doc, &path("list/4"), &json!({"e": 5})).unwrap();
+ try_change(&mut doc, |doc| {
+ write_path(doc, &path("list"), &json!(["b", "d"]))
+ })
+ .unwrap();
+ try_change(&mut doc, |doc| {
+ insert_path(doc, &path("list/0"), &json!("a"))
+ })
+ .unwrap();
+ try_change(&mut doc, |doc| {
+ insert_path(doc, &path("list/2"), &json!("c"))
+ })
+ .unwrap();
+ try_change(&mut doc, |doc| {
+ insert_path(doc, &path("list/4"), &json!({"e": 5}))
+ })
+ .unwrap();
assert_eq!(
read_path(&doc, &path("list")).unwrap(),
Some(json!(["a", "b", "c", "d", {"e": 5}]))
);
assert!(matches!(
- insert_path(&mut doc, &path("list/6"), &json!(1)),
+ try_change(&mut doc, |doc| insert_path(doc, &path("list/6"), &json!(1))),
Err(StoreError::PastTheEnd {
index: 6,
length: 5
})
));
- write_path(&mut doc, &path("map"), &json!({"k": 1})).unwrap();
+ try_change(&mut doc, |doc| {
+ write_path(doc, &path("map"), &json!({"k": 1}))
+ })
+ .unwrap();
assert!(matches!(
- insert_path(&mut doc, &path("map/0"), &json!(1)),
+ try_change(&mut doc, |doc| insert_path(doc, &path("map/0"), &json!(1))),
Err(StoreError::NotAList)
));
assert!(matches!(
- insert_path(&mut doc, &path("nothing/0"), &json!(1)),
+ try_change(&mut doc, |doc| insert_path(
+ doc,
+ &path("nothing/0"),
+ &json!(1)
+ )),
Err(StoreError::NotFound)
));
}
diff --git a/src/document/sync.rs b/src/document/sync.rs
index dc286fa..625d805 100644
--- a/src/document/sync.rs
+++ b/src/document/sync.rs
@@ -384,11 +384,12 @@ mod tests {
/// trip (`answer_for_everything_since_shared`).
#[test]
fn missing_predecessors_go_at_once() {
+ use crate::document::testing::change;
use automerge::transaction::Transactable;
let mut a = Document::new();
let mut b = Document::new();
let (mut at_a, mut at_b) = (sync::State::new(), sync::State::new());
- a.put(automerge::ROOT, "early", 1).unwrap();
+ change(&mut a, |a| a.put(automerge::ROOT, "early", 1).unwrap());
// Until in step.
for _ in 0..10 {
let to_b = a.sync().generate_sync_message(&mut at_a);
@@ -406,12 +407,12 @@ mod tests {
assert_eq!(a.get_heads(), b.get_heads());
// Five changes, whose message is lost; then a sixth, which arrives alone.
for i in 0..5 {
- a.put(automerge::ROOT, format!("lost{i}"), i).unwrap();
- a.commit();
+ change(&mut a, |a| {
+ a.put(automerge::ROOT, format!("lost{i}"), i).unwrap()
+ });
}
let _lost = a.sync().generate_sync_message(&mut at_a).unwrap();
- a.put(automerge::ROOT, "last", 6).unwrap();
- a.commit();
+ change(&mut a, |a| a.put(automerge::ROOT, "last", 6).unwrap());
at_a.in_flight = false; // As after our reply timeout.
let last = a.sync().generate_sync_message(&mut at_a).unwrap();
assert_eq!(last.changes.len(), 1);
@@ -467,6 +468,7 @@ mod tests {
/// overdue answer, gets the same changes in the next message.
#[test]
fn withdrawing_for_a_delta_keeps_the_changes_for_later() {
+ use crate::document::testing::change;
use automerge::{ROOT, transaction::Transactable};
// 16 KiB of hex, about 8 KiB of information: Automerge compresses what it sends
@@ -476,8 +478,7 @@ mod tests {
.map(|_| format!("{:016x}", rng.next_u64()))
.collect();
let mut ours = Document::new();
- ours.put(ROOT, "big", big).unwrap();
- ours.commit();
+ change(&mut ours, |ours| ours.put(ROOT, "big", big).unwrap());
let mut theirs = Document::new();
let (mut a, mut b) = (session(), session());
let now = Instant::now();
@@ -515,6 +516,7 @@ mod tests {
/// has everything (`have_all_theirs`).
#[test]
fn after_a_delta_the_peer_learns_our_heads() {
+ use crate::document::testing::change;
use automerge::{ROOT, transaction::Transactable};
let mut rng = crate::testrand::Rng::new(2);
@@ -522,8 +524,7 @@ mod tests {
.map(|_| format!("{:016x}", rng.next_u64()))
.collect();
let mut ours = Document::new();
- ours.put(ROOT, "big", big).unwrap();
- ours.commit();
+ change(&mut ours, |ours| ours.put(ROOT, "big", big).unwrap());
let mut theirs = Document::new();
let (mut a, mut b) = (session(), session());
let now = Instant::now();
diff --git a/src/document/testing.rs b/src/document/testing.rs
new file mode 100644
index 0000000..b34d85f
--- /dev/null
+++ b/src/document/testing.rs
@@ -0,0 +1,29 @@
+//! For tests: writing to documents the way the node does, one change at a time, so
+//! that tests don't depend on how the engine groups writes into changes.
+
+use super::Document;
+
+/// The writes `f` makes to `doc`, as one change; what `f` returns.
+pub(crate) fn change<T>(doc: &mut Document, f: impl FnOnce(&mut Document) -> T) -> T {
+ let out = f(doc);
+ doc.commit();
+ out
+}
+
+/// The writes `f` makes to `doc`, as one change if it succeeds, dropped if not; what
+/// `f` returns. Committed at time 0: the same writes make the same change.
+pub(crate) fn try_change<T, E>(
+ doc: &mut Document,
+ f: impl FnOnce(&mut Document) -> Result<T, E>,
+) -> Result<T, E> {
+ let out = f(doc);
+ match &out {
+ Ok(_) => {
+ doc.commit_with(automerge::transaction::CommitOptions::default().with_time(0));
+ }
+ Err(_) => {
+ doc.rollback();
+ }
+ }
+ out
+}
diff --git a/src/remake/catchup.rs b/src/remake/catchup.rs
index 0be7dfd..e265d0b 100644
--- a/src/remake/catchup.rs
+++ b/src/remake/catchup.rs
@@ -555,6 +555,7 @@ async fn abandon<H: Host>(host: &H, rejoin: Rejoin, reason: String) {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::document::testing::change;
use crate::remake::snapshot::{SnapshotParams, build};
use automerge::{ActorId, ROOT, transaction::Transactable};
use serde_json::{Value as JsonValue, json};
@@ -563,8 +564,7 @@ mod tests {
/// document of the next epoch (snapshot plus one foreign change).
fn scenario(policy: &JsonValue) -> (AutoCommit, AutoCommit, Epoch) {
let mut old = AutoCommit::new().with_actor(ActorId::from(&b"late-node"[..]));
- old.put(ROOT, "base", 1_i64).unwrap();
- old.commit();
+ change(&mut old, |old| old.put(ROOT, "base", 1_i64).unwrap());
let base = old.get_heads();
let snapshot = build(
@@ -584,11 +584,11 @@ mod tests {
let target = snapshot.epoch;
let mut swarm = snapshot.doc.with_actor(ActorId::from(&b"swarm-node"[..]));
- swarm.put(ROOT, "theirs", 1_i64).unwrap();
- swarm.commit();
+ change(&mut swarm, |swarm| {
+ swarm.put(ROOT, "theirs", 1_i64).unwrap()
+ });
- old.put(ROOT, "mine", 1_i64).unwrap();
- old.commit();
+ change(&mut old, |old| old.put(ROOT, "mine", 1_i64).unwrap());
(old, swarm, target)
}
diff --git a/src/remake/delta.rs b/src/remake/delta.rs
index aebd048..21c07b7 100644
--- a/src/remake/delta.rs
+++ b/src/remake/delta.rs
@@ -251,6 +251,7 @@ pub(crate) fn changed_paths(
#[cfg(test)]
mod tests {
use super::*;
+ use crate::document::testing::{change, try_change};
use automerge::{ObjType, ROOT, ReadDoc, ScalarValue, transaction::Transactable};
use serde_json::json;
@@ -265,8 +266,8 @@ mod tests {
const KEYS: [&str; 3] = ["a", "b", "c"];
let depth = rng.between(1, 3);
let path: Vec<String> = (0..depth).map(|_| rng.pick(&KEYS).to_string()).collect();
- let result = if rng.chance(0.2) {
- store::delete_path(doc, &path, false)
+ let _ = if rng.chance(0.2) {
+ try_change(doc, |doc| store::delete_path(doc, &path, false))
} else {
let value = match rng.below(4) {
0 => json!(rng.below(100)),
@@ -278,16 +279,8 @@ mod tests {
serde_json::Value::Object(map)
}
};
- store::write_path(doc, &path, &value)
+ try_change(doc, |doc| store::write_path(doc, &path, &value))
};
- match result {
- Ok(()) => {
- doc.commit_with(automerge::transaction::CommitOptions::default().with_time(0));
- }
- Err(_) => {
- doc.rollback();
- }
- }
}
/// Compaction's promise: a snapshot of the base plus everything replayed since gives
@@ -302,8 +295,9 @@ mod tests {
}
let base = old.get_heads();
let mut new = AutoCommit::new().with_actor(actor(100));
- store::materialize_into(&old, &mut new, &|_| false).unwrap();
- new.commit();
+ change(&mut new, |new| {
+ store::materialize_into(&old, new, &|_| false).unwrap()
+ });
let mut nodes: Vec<AutoCommit> = (1..=rng.between(1, 3))
.map(|i| old.fork().with_actor(actor(i as u8)))
@@ -395,19 +389,21 @@ mod tests {
#[test]
fn minimal_paths() {
let mut doc = AutoCommit::new();
- doc.put(ROOT, "untouched", 1_i64).unwrap();
+ change(&mut doc, |doc| doc.put(ROOT, "untouched", 1_i64).unwrap());
let before = doc.get_heads();
- let m = doc.put_object(ROOT, "m", ObjType::Map).unwrap();
- doc.put(&m, "a", 1_i64).unwrap();
- doc.put(ROOT, "x", 1_i64).unwrap();
- doc.put(ROOT, "c", ScalarValue::counter(0)).unwrap();
- doc.commit();
+ change(&mut doc, |doc| {
+ let m = doc.put_object(ROOT, "m", ObjType::Map).unwrap();
+ doc.put(&m, "a", 1_i64).unwrap();
+ doc.put(ROOT, "x", 1_i64).unwrap();
+ doc.put(ROOT, "c", ScalarValue::counter(0)).unwrap();
+ });
let middle = doc.get_heads();
- doc.increment(ROOT, "c", 2).unwrap();
- let list = doc.put_object(ROOT, "l", ObjType::List).unwrap();
- doc.insert(&list, 0, "e").unwrap();
- doc.commit();
+ change(&mut doc, |doc| {
+ doc.increment(ROOT, "c", 2).unwrap();
+ let list = doc.put_object(ROOT, "l", ObjType::List).unwrap();
+ doc.insert(&list, 0, "e").unwrap();
+ });
let after = doc.get_heads();
let all = changed_paths(&mut doc, &before, &after);
@@ -429,26 +425,30 @@ mod tests {
fn replays_state_per_key() {
// Base shared by the old document and the snapshot.
let mut old = AutoCommit::new();
- let m = old.put_object(ROOT, "m", ObjType::Map).unwrap();
- old.put(&m, "keep", "base").unwrap();
- old.put(&m, "change", "base").unwrap();
- old.put(ROOT, "gone", true).unwrap();
- old.commit();
+ let m = change(&mut old, |old| {
+ let m = old.put_object(ROOT, "m", ObjType::Map).unwrap();
+ old.put(&m, "keep", "base").unwrap();
+ old.put(&m, "change", "base").unwrap();
+ old.put(ROOT, "gone", true).unwrap();
+ m
+ });
let base = old.get_heads();
let mut new = AutoCommit::new();
- store::materialize_into(&old, &mut new, &|_| false).unwrap();
- // Something the new epoch already holds that the delta must not clobber.
- let Some((_, m_new)) = new.get(ROOT, "m").unwrap() else {
- panic!("m missing");
- };
- new.put(&m_new, "keep", "new epoch").unwrap();
- new.commit();
+ change(&mut new, |new| {
+ store::materialize_into(&old, new, &|_| false).unwrap();
+ // Something the new epoch already holds that the delta must not clobber.
+ let Some((_, m_new)) = new.get(ROOT, "m").unwrap() else {
+ panic!("m missing");
+ };
+ new.put(&m_new, "keep", "new epoch").unwrap();
+ });
// The delta: one key changed, one key deleted.
- old.put(&m, "change", "delta").unwrap();
- old.delete(ROOT, "gone").unwrap();
- old.commit();
+ change(&mut old, |old| {
+ old.put(&m, "change", "delta").unwrap();
+ old.delete(ROOT, "gone").unwrap();
+ });
let after = old.get_heads();
let stats = replay(&mut old, &base, &after, &mut new, &Translator::default());
@@ -465,8 +465,7 @@ mod tests {
let other = ActorId::from(&b"other-actor"[..]);
let mut doc = AutoCommit::new().with_actor(own.clone());
- doc.put(ROOT, "base", 1_i64).unwrap();
- doc.commit();
+ change(&mut doc, |doc| doc.put(ROOT, "base", 1_i64).unwrap());
let base = doc.get_heads();
// The snapshot contains the base only.
@@ -474,14 +473,14 @@ mod tests {
// A foreign change after the base (replayed by its own node) ...
let mut foreign = doc.fork().with_actor(other);
- foreign.put(ROOT, "theirs", 1_i64).unwrap();
- foreign.commit();
+ change(&mut foreign, |foreign| {
+ foreign.put(ROOT, "theirs", 1_i64).unwrap()
+ });
doc.merge(&mut foreign).unwrap();
let with_foreign = doc.get_heads();
// ... and one of ours.
- doc.put(ROOT, "mine", 1_i64).unwrap();
- doc.commit();
+ change(&mut doc, |doc| doc.put(ROOT, "mine", 1_i64).unwrap());
let after = doc.get_heads();
let missing = missing_from_snapshot(&mut doc, &clock, std::slice::from_ref(&own));
@@ -505,28 +504,24 @@ mod tests {
#[test]
fn protocol_metadata_is_never_replayed() {
let mut old = AutoCommit::new();
- old.put(ROOT, "x", 1_i64).unwrap();
- old.commit();
+ change(&mut old, |old| old.put(ROOT, "x", 1_i64).unwrap());
let base = old.get_heads();
let mut new = AutoCommit::new();
- store::write_path(
- &mut new,
- &meta::path(&[EPOCHS, "1"]),
- &json!({ "generation": 1 }),
- )
- .unwrap();
- new.commit();
+ change(&mut new, |new| {
+ store::write_path(
+ new,
+ &meta::path(&[EPOCHS, "1"]),
+ &json!({ "generation": 1 }),
+ )
+ .unwrap()
+ });
// The old epoch replaces `_continuum` wholesale and writes a remake record.
- store::write_path(
- &mut old,
- &meta::path(&[]),
- &json!({ "forgotten": { "pk": 1 } }),
- )
- .unwrap();
- store::write_path(&mut old, &meta::path(&[REMAKES, "c"]), &json!({ "x": 1 })).unwrap();
- old.commit();
+ change(&mut old, |old| {
+ store::write_path(old, &meta::path(&[]), &json!({ "forgotten": { "pk": 1 } })).unwrap();
+ store::write_path(old, &meta::path(&[REMAKES, "c"]), &json!({ "x": 1 })).unwrap();
+ });
let after = old.get_heads();
replay(&mut old, &base, &after, &mut new, &Translator::default());
diff --git a/src/remake/kind.rs b/src/remake/kind.rs
index ca16de0..1df03cb 100644
--- a/src/remake/kind.rs
+++ b/src/remake/kind.rs
@@ -591,17 +591,20 @@ mod tests {
#[test]
fn import_keeps_types_and_drops_protocol_data() {
+ use crate::document::testing::{change, try_change};
use automerge::{ObjType, ROOT, ReadDoc, transaction::Transactable};
use serde_json::json;
let mut source = AutoCommit::new();
- let text = source.put_object(ROOT, "notes", ObjType::Text).unwrap();
- source.splice_text(&text, 0, 0, "hello").unwrap();
- source
- .put(ROOT, "hits", automerge::ScalarValue::counter(3))
- .unwrap();
- let meta = source.put_object(ROOT, meta::ROOT, ObjType::Map).unwrap();
- source.put(&meta, "members", "elsewhere").unwrap();
+ change(&mut source, |source| {
+ let text = source.put_object(ROOT, "notes", ObjType::Text).unwrap();
+ source.splice_text(&text, 0, 0, "hello").unwrap();
+ source
+ .put(ROOT, "hits", automerge::ScalarValue::counter(3))
+ .unwrap();
+ let meta = source.put_object(ROOT, meta::ROOT, ObjType::Map).unwrap();
+ source.put(&meta, "members", "elsewhere").unwrap();
+ });
let encoded = BASE64.encode(source.save());
assert!(
@@ -620,10 +623,10 @@ mod tests {
.unwrap();
let build = || {
let mut target = AutoCommit::new().with_actor(automerge::ActorId::from(&b"fixed"[..]));
- Import
- .build(&AutoCommit::new(), prepared.content.as_deref(), &mut target)
- .unwrap();
- target.commit_with(automerge::transaction::CommitOptions::default().with_time(0));
+ try_change(&mut target, |target| {
+ Import.build(&AutoCommit::new(), prepared.content.as_deref(), target)
+ })
+ .unwrap();
target
};
let mut a = build();
@@ -648,14 +651,15 @@ mod tests {
#[test]
fn replace_builds_the_document_deterministically() {
+ use crate::document::testing::try_change;
use serde_json::json;
let build = |document: serde_json::Value| {
let prepared = replace(json!({ "document": document })).unwrap();
let mut target = AutoCommit::new().with_actor(automerge::ActorId::from(&b"fixed"[..]));
- Replace
- .build(&AutoCommit::new(), prepared.content.as_deref(), &mut target)
- .unwrap();
- target.commit_with(automerge::transaction::CommitOptions::default().with_time(0));
+ try_change(&mut target, |target| {
+ Replace.build(&AutoCommit::new(), prepared.content.as_deref(), target)
+ })
+ .unwrap();
(
target.get_heads(),
store::read_path(&target, &[]).unwrap().unwrap(),
diff --git a/src/remake/records.rs b/src/remake/records.rs
index f22b1e5..40b563a 100644
--- a/src/remake/records.rs
+++ b/src/remake/records.rs
@@ -499,6 +499,7 @@ pub(crate) fn decode_hex32(hex: &str) -> Option<[u8; 32]> {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::document::testing::change;
#[test]
fn retention_covers_until_every_limit_passed() {
@@ -570,16 +571,6 @@ mod tests {
fn round_trip_through_the_document() {
let mut doc = Document::new();
let p = proposal(10_000);
- write_proposal(&mut doc, "c-1", &p).unwrap();
- write_abort_request(
- &mut doc,
- "c-1",
- &AbortRequest {
- by: "x".into(),
- at: UnixMillis(5),
- },
- )
- .unwrap();
let decision = Decision::Committed {
to_epoch: EpochRecord {
generation: 1,
@@ -589,14 +580,25 @@ mod tests {
snapshot_bytes: 123,
decided_at: UnixMillis(11_000),
};
- write_decision(&mut doc, "c-1", &decision).unwrap();
let vote = |remake: &str| Vote {
remake: remake.into(),
at: UnixMillis(7),
};
- write_vote(&mut doc, "node-b", &vote("c-1")).unwrap();
- write_vote(&mut doc, "node-c", &vote("other")).unwrap();
- doc.commit();
+ change(&mut doc, |doc| {
+ write_proposal(doc, "c-1", &p).unwrap();
+ write_abort_request(
+ doc,
+ "c-1",
+ &AbortRequest {
+ by: "x".into(),
+ at: UnixMillis(5),
+ },
+ )
+ .unwrap();
+ write_decision(doc, "c-1", &decision).unwrap();
+ write_vote(doc, "node-b", &vote("c-1")).unwrap();
+ write_vote(doc, "node-c", &vote("other")).unwrap();
+ });
let all = load_all(&doc);
assert_eq!(all.len(), 1);
@@ -617,14 +619,11 @@ mod tests {
remake: remake.into(),
at: UnixMillis(at),
};
- write_vote(&mut a, "node-a", &vote("x", 1)).unwrap();
- a.commit();
+ change(&mut a, |a| write_vote(a, "node-a", &vote("x", 1)).unwrap());
let mut b = a.fork();
// node-b votes in both, never seeing its other vote (a lost vote).
- write_vote(&mut a, "node-b", &vote("x", 2)).unwrap();
- write_vote(&mut b, "node-b", &vote("y", 3)).unwrap();
- a.commit();
- b.commit();
+ change(&mut a, |a| write_vote(a, "node-b", &vote("x", 2)).unwrap());
+ change(&mut b, |b| write_vote(b, "node-b", &vote("y", 3)).unwrap());
a.merge(&mut b).unwrap();
let register = load_register(&a);
@@ -634,8 +633,7 @@ mod tests {
assert_eq!(twice, [vote("x", 2), vote("y", 3)]);
// Seen, a vote is replaced.
- write_vote(&mut a, "node-b", &vote("z", 4)).unwrap();
- a.commit();
+ change(&mut a, |a| write_vote(a, "node-b", &vote("z", 4)).unwrap());
assert_eq!(load_register(&a)["node-b"], [vote("z", 4)]);
}
@@ -686,8 +684,9 @@ mod tests {
#[test]
fn disagreeing_decisions_are_contested() {
let mut base = Document::new();
- write_proposal(&mut base, "c-1", &proposal(10_000)).unwrap();
- base.commit();
+ change(&mut base, |base| {
+ write_proposal(base, "c-1", &proposal(10_000)).unwrap()
+ });
let (mut a, mut b, mut c) = (base.fork(), base.fork(), base.fork());
let committed = Decision::Committed {
to_epoch: EpochRecord {
@@ -698,21 +697,20 @@ mod tests {
snapshot_bytes: 1,
decided_at: UnixMillis(2),
};
- write_decision(&mut a, "c-1", &committed).unwrap();
- write_decision(
- &mut b,
- "c-1",
- &Decision::Aborted {
- reason: "test".into(),
- decided_at: UnixMillis(3),
- },
- )
- .unwrap();
+ change(&mut a, |a| write_decision(a, "c-1", &committed).unwrap());
+ change(&mut b, |b| {
+ write_decision(
+ b,
+ "c-1",
+ &Decision::Aborted {
+ reason: "test".into(),
+ decided_at: UnixMillis(3),
+ },
+ )
+ .unwrap()
+ });
// The same decision, concurrently: they agree.
- write_decision(&mut c, "c-1", &committed).unwrap();
- a.commit();
- b.commit();
- c.commit();
+ change(&mut c, |c| write_decision(c, "c-1", &committed).unwrap());
let mut agreeing = a.fork();
agreeing.merge(&mut c).unwrap();
assert!(!load(&agreeing, "c-1").unwrap().contested);
@@ -726,14 +724,15 @@ mod tests {
#[test]
fn unreadable_records_are_skipped() {
let mut doc = Document::new();
- store::write_path(
- &mut doc,
- &record_path("broken", ""),
- &serde_json::json!({ "proposal": "not an object" }),
- )
- .unwrap();
- write_proposal(&mut doc, "fine", &proposal(1)).unwrap();
- doc.commit();
+ change(&mut doc, |doc| {
+ store::write_path(
+ doc,
+ &record_path("broken", ""),
+ &serde_json::json!({ "proposal": "not an object" }),
+ )
+ .unwrap();
+ write_proposal(doc, "fine", &proposal(1)).unwrap();
+ });
let all = load_all(&doc);
assert_eq!(all.len(), 1);
assert_eq!(all[0].0, "fine");
diff --git a/src/remake/snapshot.rs b/src/remake/snapshot.rs
index b071b74..c818af6 100644
--- a/src/remake/snapshot.rs
+++ b/src/remake/snapshot.rs
@@ -376,6 +376,7 @@ pub(super) fn canonical(value: JsonValue) -> JsonValue {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::document::testing::change;
use automerge::{ObjType, ROOT, ReadDoc, ScalarValue, Value, transaction::Transactable};
fn doc(actor: &[u8]) -> AutoCommit {
@@ -414,28 +415,31 @@ mod tests {
fn same_view_different_history_same_snapshot() {
// One history: overwrites, increments, one-shot text.
let mut a = doc(b"actor-a");
- a.put(ROOT, "x", 1_i64).unwrap();
- a.commit();
- a.put(ROOT, "x", 2_i64).unwrap();
- a.put(ROOT, "c", ScalarValue::counter(0)).unwrap();
- a.increment(ROOT, "c", 5).unwrap();
- let text = a.put_object(ROOT, "t", ObjType::Text).unwrap();
- a.splice_text(&text, 0, 0, "hello").unwrap();
- let m = a.put_object(ROOT, "m", ObjType::Map).unwrap();
- a.put(&m, "k", "v").unwrap();
- a.commit();
+ change(&mut a, |a| a.put(ROOT, "x", 1_i64).unwrap());
+ change(&mut a, |a| {
+ a.put(ROOT, "x", 2_i64).unwrap();
+ a.put(ROOT, "c", ScalarValue::counter(0)).unwrap();
+ a.increment(ROOT, "c", 5).unwrap();
+ let text = a.put_object(ROOT, "t", ObjType::Text).unwrap();
+ a.splice_text(&text, 0, 0, "hello").unwrap();
+ let m = a.put_object(ROOT, "m", ObjType::Map).unwrap();
+ a.put(&m, "k", "v").unwrap();
+ });
// Another actor, other order, other steps, same result.
let mut b = doc(b"actor-b");
- let m = b.put_object(ROOT, "m", ObjType::Map).unwrap();
- b.put(&m, "k", "v").unwrap();
- let text = b.put_object(ROOT, "t", ObjType::Text).unwrap();
- b.splice_text(&text, 0, 0, "hel").unwrap();
- b.commit();
- b.splice_text(&text, 3, 0, "lo").unwrap();
- b.put(ROOT, "c", ScalarValue::counter(5)).unwrap();
- b.put(ROOT, "x", 2_i64).unwrap();
- b.commit();
+ let text = change(&mut b, |b| {
+ let m = b.put_object(ROOT, "m", ObjType::Map).unwrap();
+ b.put(&m, "k", "v").unwrap();
+ let text = b.put_object(ROOT, "t", ObjType::Text).unwrap();
+ b.splice_text(&text, 0, 0, "hel").unwrap();
+ text
+ });
+ change(&mut b, |b| {
+ b.splice_text(&text, 3, 0, "lo").unwrap();
+ b.put(ROOT, "c", ScalarValue::counter(5)).unwrap();
+ b.put(ROOT, "x", 2_i64).unwrap();
+ });
assert_eq!(user_view(&a), user_view(&b));
@@ -452,12 +456,13 @@ mod tests {
#[test]
fn rebuild_is_deterministic() {
let mut a = doc(b"actor-a");
- a.put(ROOT, "x", 1_i64).unwrap();
- let list = a.put_object(ROOT, "l", ObjType::List).unwrap();
- a.insert(&list, 0, "first").unwrap();
- let inner = a.insert_object(&list, 1, ObjType::Map).unwrap();
- a.put(&inner, "k", true).unwrap();
- a.commit();
+ change(&mut a, |a| {
+ a.put(ROOT, "x", 1_i64).unwrap();
+ let list = a.put_object(ROOT, "l", ObjType::List).unwrap();
+ a.insert(&list, 0, "first").unwrap();
+ let inner = a.insert_object(&list, 1, ObjType::Map).unwrap();
+ a.put(&inner, "k", true).unwrap();
+ });
// A second node holding the same changes, received in a different way.
let mut b = doc(b"actor-b");
@@ -475,14 +480,16 @@ mod tests {
#[test]
fn conflicts_collapse_into_the_merged_view() {
let mut a = doc(b"actor-a");
- let cfg = a.put_object(ROOT, "cfg", ObjType::Map).unwrap();
- a.put(&cfg, "x", 1_i64).unwrap();
- a.commit();
+ change(&mut a, |a| {
+ let cfg = a.put_object(ROOT, "cfg", ObjType::Map).unwrap();
+ a.put(&cfg, "x", 1_i64).unwrap();
+ });
let mut b = doc(b"actor-b");
- let cfg = b.put_object(ROOT, "cfg", ObjType::Map).unwrap();
- b.put(&cfg, "y", 2_i64).unwrap();
- b.commit();
+ change(&mut b, |b| {
+ let cfg = b.put_object(ROOT, "cfg", ObjType::Map).unwrap();
+ b.put(&cfg, "y", 2_i64).unwrap();
+ });
a.merge(&mut b).unwrap();
let (_, conflicts) = store::read_path_with_conflicts(&a, &[]).unwrap().unwrap();
@@ -507,22 +514,19 @@ mod tests {
fn base_heads_bound_content_and_clock() {
let mut a = doc(b"actor-a");
for i in 0..3_i64 {
- a.put(ROOT, format!("a{i}"), i).unwrap();
- a.commit();
+ change(&mut a, |a| a.put(ROOT, format!("a{i}"), i).unwrap());
}
let mut b = doc(b"actor-b");
for i in 0..2_i64 {
- b.put(ROOT, format!("b{i}"), i).unwrap();
- b.commit();
+ change(&mut b, |b| b.put(ROOT, format!("b{i}"), i).unwrap());
}
a.merge(&mut b).unwrap();
let base = a.get_heads();
// After the base: must not appear in the snapshot or its clock.
- a.put(ROOT, "late", true).unwrap();
- a.commit();
+ change(&mut a, |a| a.put(ROOT, "late", true).unwrap());
let policy = json!({});
let snap = build(&mut a, &params(&base, &policy)).unwrap();
@@ -541,8 +545,9 @@ mod tests {
#[test]
fn epoch_of_a_backup() {
let mut a = doc(b"actor-a");
- store::write_path(&mut a, &["k".into()], &json!(1)).unwrap();
- a.commit();
+ change(&mut a, |a| {
+ store::write_path(a, &["k".into()], &json!(1)).unwrap()
+ });
assert_eq!(epoch_of(&mut a), Ok(Epoch::GENESIS));
let heads = a.get_heads();
@@ -553,8 +558,9 @@ mod tests {
.doc
.fork()
.with_actor(ActorId::from(b"actor-b".as_slice()));
- store::write_path(&mut later, &["k".into()], &json!(2)).unwrap();
- later.commit();
+ change(&mut later, |later| {
+ store::write_path(later, &["k".into()], &json!(2)).unwrap()
+ });
let mut backup = AutoCommit::load(&later.save()).unwrap();
assert_eq!(epoch_of(&mut backup), Ok(snap.epoch));
}
@@ -562,25 +568,26 @@ mod tests {
#[test]
fn metadata_written_and_remake_records_dropped() {
let mut a = doc(b"actor-a");
- store::write_path(
- &mut a,
- &meta::path(&[REMAKES, "c-1"]),
- &json!({ "state": "committed" }),
- )
- .unwrap();
- store::write_path(
- &mut a,
- &meta::path(&[VOTES, "pk"]),
- &json!({ "remake": "c-1", "at": 1 }),
- )
- .unwrap();
- store::write_path(
- &mut a,
- &meta::path(&[meta::FORGOTTEN, "pk"]),
- &json!({ "at": 1 }),
- )
- .unwrap();
- a.commit();
+ change(&mut a, |a| {
+ store::write_path(
+ a,
+ &meta::path(&[REMAKES, "c-1"]),
+ &json!({ "state": "committed" }),
+ )
+ .unwrap();
+ store::write_path(
+ a,
+ &meta::path(&[VOTES, "pk"]),
+ &json!({ "remake": "c-1", "at": 1 }),
+ )
+ .unwrap();
+ store::write_path(
+ a,
+ &meta::path(&[meta::FORGOTTEN, "pk"]),
+ &json!({ "at": 1 }),
+ )
+ .unwrap();
+ });
let heads = a.get_heads();
let policy = json!({ "replayWindow": { "epochs": 1 } });
@@ -605,16 +612,17 @@ mod tests {
#[test]
fn automerge_types_survive() {
let mut a = doc(b"actor-a");
- a.put(ROOT, "c", ScalarValue::counter(3)).unwrap();
- a.increment(ROOT, "c", 4).unwrap();
- let text = a.put_object(ROOT, "t", ObjType::Text).unwrap();
- a.splice_text(&text, 0, 0, "abc").unwrap();
- a.put(ROOT, "bytes", ScalarValue::Bytes(vec![0, 1, 255]))
- .unwrap();
- a.put(ROOT, "ts", ScalarValue::Timestamp(1_700_000_000_000))
- .unwrap();
- a.put(ROOT, "neg_zero", -0.0_f64).unwrap();
- a.commit();
+ change(&mut a, |a| {
+ a.put(ROOT, "c", ScalarValue::counter(3)).unwrap();
+ a.increment(ROOT, "c", 4).unwrap();
+ let text = a.put_object(ROOT, "t", ObjType::Text).unwrap();
+ a.splice_text(&text, 0, 0, "abc").unwrap();
+ a.put(ROOT, "bytes", ScalarValue::Bytes(vec![0, 1, 255]))
+ .unwrap();
+ a.put(ROOT, "ts", ScalarValue::Timestamp(1_700_000_000_000))
+ .unwrap();
+ a.put(ROOT, "neg_zero", -0.0_f64).unwrap();
+ });
let snap = snapshot_of(&mut a);
let d = &snap.doc;
@@ -655,8 +663,7 @@ mod tests {
#[test]
fn snapshot_is_a_single_change_with_the_epoch_id() {
let mut a = doc(b"actor-a");
- a.put(ROOT, "x", 1_i64).unwrap();
- a.commit();
+ change(&mut a, |a| a.put(ROOT, "x", 1_i64).unwrap());
let mut snap = snapshot_of(&mut a);
let heads = snap.doc.get_heads();
@@ -668,12 +675,10 @@ mod tests {
#[test]
fn missing_base_is_rejected() {
let mut a = doc(b"actor-a");
- a.put(ROOT, "x", 1_i64).unwrap();
- a.commit();
+ change(&mut a, |a| a.put(ROOT, "x", 1_i64).unwrap());
let mut other = doc(b"actor-b");
- other.put(ROOT, "y", 1_i64).unwrap();
- other.commit();
+ change(&mut other, |other| other.put(ROOT, "y", 1_i64).unwrap());
let foreign = other.get_heads();
let policy = json!({});
@@ -698,8 +703,7 @@ mod tests {
// Old epoch: the base, then a late write by this node.
let mut late = doc(b"late-node");
- late.put(ROOT, "shared", "base").unwrap();
- late.commit();
+ change(&mut late, |late| late.put(ROOT, "shared", "base").unwrap());
let base = late.get_heads();
let policy = json!({});
let mut snap = build(&mut late, &params(&base, &policy)).unwrap();
@@ -709,12 +713,10 @@ mod tests {
.doc
.fork()
.with_actor(ActorId::from(&b"swarm-node"[..]));
- swarm.put(ROOT, "new", 1_i64).unwrap();
- swarm.commit();
+ change(&mut swarm, |swarm| swarm.put(ROOT, "new", 1_i64).unwrap());
let bytes = swarm.save();
- late.put(ROOT, "mine", "late").unwrap();
- late.commit();
+ change(&mut late, |late| late.put(ROOT, "mine", "late").unwrap());
let mut fetched = AutoCommit::load(&bytes).unwrap();
let metadata = verify_document(&mut fetched, snap.epoch).unwrap();
diff --git a/src/swarm/members.rs b/src/swarm/members.rs
index 0e27681..8d7f909 100644
--- a/src/swarm/members.rs
+++ b/src/swarm/members.rs
@@ -324,6 +324,7 @@ pub(crate) fn unregister(doc: &mut Document, pk: &str) -> Result<(), StoreError>
#[cfg(test)]
mod tests {
use super::*;
+ use crate::document::testing::change;
#[test]
fn electorate_by_alive_stamp() {
@@ -336,9 +337,11 @@ mod tests {
("new", json!({ "since": now - day })),
("old", json!({ "since": 1 })),
];
- for (pk, entry) in entries {
- store::write_path(&mut doc, &member_path(pk), &entry).unwrap();
- }
+ change(&mut doc, |doc| {
+ for (pk, entry) in entries {
+ store::write_path(doc, &member_path(pk), &entry).unwrap();
+ }
+ });
assert_eq!(
electorate(&doc, now, Duration::from_secs(7 * 24 * 3600)),
["fresh", "new"]
@@ -350,9 +353,13 @@ mod tests {
let mut doc = Document::new();
assert!(relays(&doc).is_empty());
let relay = "libp2p:/ip6/fd00::1/tcp/9661";
- store::write_path(&mut doc, &meta::path(&[meta::RELAYS]), &json!([relay, 7])).unwrap();
+ change(&mut doc, |doc| {
+ store::write_path(doc, &meta::path(&[meta::RELAYS]), &json!([relay, 7])).unwrap()
+ });
assert_eq!(relays(&doc), [relay]); // Anything but text is ignored.
- store::write_path(&mut doc, &meta::path(&[meta::RELAYS]), &json!(relay)).unwrap();
+ change(&mut doc, |doc| {
+ store::write_path(doc, &meta::path(&[meta::RELAYS]), &json!(relay)).unwrap()
+ });
assert!(relays(&doc).is_empty()); // Not a list.
}