| 1 | use docs_core::{fragment_text, snapshot_bytes, text_content}; |
| 2 | use yrs::updates::decoder::Decode; |
| 3 | use yrs::{Doc, Options, ReadTxn, StateVector, Text, Transact}; |
| 4 | |
| 5 | fn doc_with(client_id: u64) -> Doc { |
| 6 | Doc::with_options(Options::with_guid_and_client_id( |
| 7 | "test-guid".into(), |
| 8 | client_id, |
| 9 | )) |
| 10 | } |
| 11 | |
| 12 | #[test] |
| 13 | fn a_snapshot_replayed_into_an_empty_doc_reproduces_the_document() { |
| 14 | let source = doc_with(1); |
| 15 | let update = std::fs::read("../fixtures/vectors/fragment/simple.bin").unwrap(); |
| 16 | source |
| 17 | .transact_mut() |
| 18 | .apply_update(yrs::Update::decode_v1(&update).unwrap()) |
| 19 | .unwrap(); |
| 20 | |
| 21 | let snap = snapshot_bytes(&source); |
| 22 | |
| 23 | let rebuilt = doc_with(2); |
| 24 | rebuilt |
| 25 | .transact_mut() |
| 26 | .apply_update(yrs::Update::decode_v1(&snap).unwrap()) |
| 27 | .unwrap(); |
| 28 | |
| 29 | assert_eq!(fragment_text(&rebuilt), fragment_text(&source)); |
| 30 | assert!( |
| 31 | !fragment_text(&rebuilt).is_empty(), |
| 32 | "fixture produced no text" |
| 33 | ); |
| 34 | } |
| 35 | |
| 36 | #[test] |
| 37 | fn a_snapshot_of_this_document_verifies() { |
| 38 | let source = doc_with(1); |
| 39 | let update = std::fs::read("../fixtures/vectors/fragment/simple.bin").unwrap(); |
| 40 | source |
| 41 | .transact_mut() |
| 42 | .apply_update(yrs::Update::decode_v1(&update).unwrap()) |
| 43 | .unwrap(); |
| 44 | let snap = snapshot_bytes(&source); |
| 45 | assert!(docs_core::verify_snapshot( |
| 46 | &source, |
| 47 | &snap, |
| 48 | "verify-guid", |
| 49 | 99 |
| 50 | )); |
| 51 | } |
| 52 | |
| 53 | #[test] |
| 54 | fn a_snapshot_of_a_different_document_does_not_verify() { |
| 55 | let source = doc_with(1); |
| 56 | source |
| 57 | .transact_mut() |
| 58 | .apply_update( |
| 59 | yrs::Update::decode_v1( |
| 60 | &std::fs::read("../fixtures/vectors/fragment/simple.bin").unwrap(), |
| 61 | ) |
| 62 | .unwrap(), |
| 63 | ) |
| 64 | .unwrap(); |
| 65 | |
| 66 | let other = doc_with(1); |
| 67 | other |
| 68 | .transact_mut() |
| 69 | .apply_update( |
| 70 | yrs::Update::decode_v1( |
| 71 | &std::fs::read("../fixtures/vectors/fragment/marks.bin").unwrap(), |
| 72 | ) |
| 73 | .unwrap(), |
| 74 | ) |
| 75 | .unwrap(); |
| 76 | let wrong = snapshot_bytes(&other); |
| 77 | |
| 78 | assert!(!docs_core::verify_snapshot( |
| 79 | &source, |
| 80 | &wrong, |
| 81 | "verify-guid", |
| 82 | 99 |
| 83 | )); |
| 84 | } |
| 85 | |
| 86 | #[test] |
| 87 | fn garbage_does_not_verify() { |
| 88 | let source = doc_with(1); |
| 89 | assert!(!docs_core::verify_snapshot( |
| 90 | &source, |
| 91 | &[0xff, 0x00, 0x13, 0x37], |
| 92 | "verify-guid", |
| 93 | 99 |
| 94 | )); |
| 95 | } |
| 96 | |
| 97 | /// RULING (Task 2 brief, "beyond the brief" item 1): a test that tells |
| 98 | /// `snapshot_bytes`'s correct call, `encode_state_as_update_v1`, apart |
| 99 | /// from its dangerous neighbour `encode_diff_v1`. |
| 100 | /// |
| 101 | /// Both calls render identically whenever nothing is parked, so no test |
| 102 | /// that only checks rendered content after a clean replay can catch a |
| 103 | /// regression to the wrong call. The two calls diverge only when the |
| 104 | /// source document is holding an update it received out of order — one |
| 105 | /// whose dependency (an earlier clock range from the same client) has |
| 106 | /// not arrived yet, so `apply_update` accepted it but yrs parked it in |
| 107 | /// the pending queue instead of integrating it. `encode_state_as_update_v1` |
| 108 | /// carries that pending queue along (via yrs's own `merge_pending_v1`); |
| 109 | /// `encode_diff_v1` drops it (its own doc comment says so). |
| 110 | /// |
| 111 | /// To construct that scenario without drawing entropy from the CRDT |
| 112 | /// library (every `Doc` below still takes an explicit guid/client id): |
| 113 | /// one `origin` document produces two updates in sequence — `dep` |
| 114 | /// (its first edit) and `dependent` (its second edit, whose blocks' |
| 115 | /// clocks for `origin`'s client id start exactly where `dep`'s left |
| 116 | /// off). Handing `dependent` to a fresh document without `dep` first is |
| 117 | /// what makes yrs park it — the document has never seen client 1 at |
| 118 | /// all, so `dependent`'s blocks fail their dependency check and go into |
| 119 | /// the pending queue rather than being rejected outright. |
| 120 | #[test] |
| 121 | fn a_snapshot_with_a_parked_update_heals_once_its_dependency_arrives() { |
| 122 | // Build `dep` and `dependent` as two genuinely sequential updates |
| 123 | // from one document, so `dependent` really does depend on `dep`. |
| 124 | let origin = doc_with(1); |
| 125 | let origin_text = origin.get_or_insert_text("content"); |
| 126 | { |
| 127 | let mut txn = origin.transact_mut(); |
| 128 | origin_text.insert(&mut txn, 0, "hello "); |
| 129 | } |
| 130 | let dep = origin |
| 131 | .transact() |
| 132 | .encode_state_as_update_v1(&StateVector::default()); |
| 133 | let state_after_dep = origin.transact().state_vector(); |
| 134 | { |
| 135 | let mut txn = origin.transact_mut(); |
| 136 | origin_text.insert(&mut txn, 6, "world"); |
| 137 | } |
| 138 | let dependent = origin.transact().encode_diff_v1(&state_after_dep); |
| 139 | |
| 140 | // A document that only ever sees `dependent`: yrs cannot integrate |
| 141 | // it (client 1's clock range 0..6 is missing) so it is parked, not |
| 142 | // rejected — `apply_update` still returns `Ok`. |
| 143 | let parked = doc_with(2); |
| 144 | parked |
| 145 | .transact_mut() |
| 146 | .apply_update(yrs::Update::decode_v1(&dependent).unwrap()) |
| 147 | .unwrap(); |
| 148 | assert_eq!( |
| 149 | text_content(&parked), |
| 150 | "", |
| 151 | "a parked update must not be visible yet" |
| 152 | ); |
| 153 | |
| 154 | // Snapshot the document holding the pending update and replay that |
| 155 | // snapshot into a completely fresh document. Under the correct |
| 156 | // `encode_state_as_update_v1` call the pending queue rides along; |
| 157 | // under `encode_diff_v1` it would be silently dropped, and |
| 158 | // `rebuilt` would never heal below no matter what arrives. |
| 159 | let snap = snapshot_bytes(&parked); |
| 160 | let rebuilt = doc_with(3); |
| 161 | rebuilt |
| 162 | .transact_mut() |
| 163 | .apply_update(yrs::Update::decode_v1(&snap).unwrap()) |
| 164 | .unwrap(); |
| 165 | assert_eq!( |
| 166 | text_content(&rebuilt), |
| 167 | "", |
| 168 | "the dependency still hasn't arrived, so this must still be pending, not lost" |
| 169 | ); |
| 170 | |
| 171 | // Deliver the missing dependency to both. If the pending queue |
| 172 | // survived the snapshot/replay, both heal to the same content. |
| 173 | parked |
| 174 | .transact_mut() |
| 175 | .apply_update(yrs::Update::decode_v1(&dep).unwrap()) |
| 176 | .unwrap(); |
| 177 | rebuilt |
| 178 | .transact_mut() |
| 179 | .apply_update(yrs::Update::decode_v1(&dep).unwrap()) |
| 180 | .unwrap(); |
| 181 | |
| 182 | assert_eq!(text_content(&parked), "hello world"); |
| 183 | assert_eq!( |
| 184 | text_content(&rebuilt), |
| 185 | text_content(&parked), |
| 186 | "the snapshot-and-replay document must converge with the original \ |
| 187 | once the missing dependency arrives" |
| 188 | ); |
| 189 | } |
| 190 | |