diff options
| -rw-r--r-- | src/document/changes.rs | 11 | ||||
| -rw-r--r-- | src/document/file.rs | 7 | ||||
| -rw-r--r-- | src/document/mod.rs | 2 | ||||
| -rw-r--r-- | src/document/store.rs | 70 | ||||
| -rw-r--r-- | src/document/sync.rs | 19 | ||||
| -rw-r--r-- | src/document/testing.rs | 29 | ||||
| -rw-r--r-- | src/remake/catchup.rs | 12 | ||||
| -rw-r--r-- | src/remake/delta.rs | 117 | ||||
| -rw-r--r-- | src/remake/kind.rs | 34 | ||||
| -rw-r--r-- | src/remake/records.rs | 91 | ||||
| -rw-r--r-- | src/remake/snapshot.rs | 166 | ||||
| -rw-r--r-- | src/swarm/members.rs | 17 |
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, ¶ms(&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, ¶ms(&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. } |
