use docs_core::{fragment_text, snapshot_bytes, text_content}; use yrs::updates::decoder::Decode; use yrs::{Doc, Options, ReadTxn, StateVector, Text, Transact}; fn doc_with(client_id: u64) -> Doc { Doc::with_options(Options::with_guid_and_client_id( "test-guid".into(), client_id, )) } #[test] fn a_snapshot_replayed_into_an_empty_doc_reproduces_the_document() { let source = doc_with(1); let update = std::fs::read("../fixtures/vectors/fragment/simple.bin").unwrap(); source .transact_mut() .apply_update(yrs::Update::decode_v1(&update).unwrap()) .unwrap(); let snap = snapshot_bytes(&source); let rebuilt = doc_with(2); rebuilt .transact_mut() .apply_update(yrs::Update::decode_v1(&snap).unwrap()) .unwrap(); assert_eq!(fragment_text(&rebuilt), fragment_text(&source)); assert!( !fragment_text(&rebuilt).is_empty(), "fixture produced no text" ); } #[test] fn a_snapshot_of_this_document_verifies() { let source = doc_with(1); let update = std::fs::read("../fixtures/vectors/fragment/simple.bin").unwrap(); source .transact_mut() .apply_update(yrs::Update::decode_v1(&update).unwrap()) .unwrap(); let snap = snapshot_bytes(&source); assert!(docs_core::verify_snapshot( &source, &snap, "verify-guid", 99 )); } #[test] fn a_snapshot_of_a_different_document_does_not_verify() { let source = doc_with(1); source .transact_mut() .apply_update( yrs::Update::decode_v1( &std::fs::read("../fixtures/vectors/fragment/simple.bin").unwrap(), ) .unwrap(), ) .unwrap(); let other = doc_with(1); other .transact_mut() .apply_update( yrs::Update::decode_v1( &std::fs::read("../fixtures/vectors/fragment/marks.bin").unwrap(), ) .unwrap(), ) .unwrap(); let wrong = snapshot_bytes(&other); assert!(!docs_core::verify_snapshot( &source, &wrong, "verify-guid", 99 )); } #[test] fn garbage_does_not_verify() { let source = doc_with(1); assert!(!docs_core::verify_snapshot( &source, &[0xff, 0x00, 0x13, 0x37], "verify-guid", 99 )); } /// RULING (Task 2 brief, "beyond the brief" item 1): a test that tells /// `snapshot_bytes`'s correct call, `encode_state_as_update_v1`, apart /// from its dangerous neighbour `encode_diff_v1`. /// /// Both calls render identically whenever nothing is parked, so no test /// that only checks rendered content after a clean replay can catch a /// regression to the wrong call. The two calls diverge only when the /// source document is holding an update it received out of order — one /// whose dependency (an earlier clock range from the same client) has /// not arrived yet, so `apply_update` accepted it but yrs parked it in /// the pending queue instead of integrating it. `encode_state_as_update_v1` /// carries that pending queue along (via yrs's own `merge_pending_v1`); /// `encode_diff_v1` drops it (its own doc comment says so). /// /// To construct that scenario without drawing entropy from the CRDT /// library (every `Doc` below still takes an explicit guid/client id): /// one `origin` document produces two updates in sequence — `dep` /// (its first edit) and `dependent` (its second edit, whose blocks' /// clocks for `origin`'s client id start exactly where `dep`'s left /// off). Handing `dependent` to a fresh document without `dep` first is /// what makes yrs park it — the document has never seen client 1 at /// all, so `dependent`'s blocks fail their dependency check and go into /// the pending queue rather than being rejected outright. #[test] fn a_snapshot_with_a_parked_update_heals_once_its_dependency_arrives() { // Build `dep` and `dependent` as two genuinely sequential updates // from one document, so `dependent` really does depend on `dep`. let origin = doc_with(1); let origin_text = origin.get_or_insert_text("content"); { let mut txn = origin.transact_mut(); origin_text.insert(&mut txn, 0, "hello "); } let dep = origin .transact() .encode_state_as_update_v1(&StateVector::default()); let state_after_dep = origin.transact().state_vector(); { let mut txn = origin.transact_mut(); origin_text.insert(&mut txn, 6, "world"); } let dependent = origin.transact().encode_diff_v1(&state_after_dep); // A document that only ever sees `dependent`: yrs cannot integrate // it (client 1's clock range 0..6 is missing) so it is parked, not // rejected — `apply_update` still returns `Ok`. let parked = doc_with(2); parked .transact_mut() .apply_update(yrs::Update::decode_v1(&dependent).unwrap()) .unwrap(); assert_eq!( text_content(&parked), "", "a parked update must not be visible yet" ); // Snapshot the document holding the pending update and replay that // snapshot into a completely fresh document. Under the correct // `encode_state_as_update_v1` call the pending queue rides along; // under `encode_diff_v1` it would be silently dropped, and // `rebuilt` would never heal below no matter what arrives. let snap = snapshot_bytes(&parked); let rebuilt = doc_with(3); rebuilt .transact_mut() .apply_update(yrs::Update::decode_v1(&snap).unwrap()) .unwrap(); assert_eq!( text_content(&rebuilt), "", "the dependency still hasn't arrived, so this must still be pending, not lost" ); // Deliver the missing dependency to both. If the pending queue // survived the snapshot/replay, both heal to the same content. parked .transact_mut() .apply_update(yrs::Update::decode_v1(&dep).unwrap()) .unwrap(); rebuilt .transact_mut() .apply_update(yrs::Update::decode_v1(&dep).unwrap()) .unwrap(); assert_eq!(text_content(&parked), "hello world"); assert_eq!( text_content(&rebuilt), text_content(&parked), "the snapshot-and-replay document must converge with the original \ once the missing dependency arrives" ); }