1#[cfg(test)]
17mod tests {
18 use std::collections::VecDeque;
19
20 use proptest::prelude::*;
21 use uuid::Uuid;
22
23 use crate::doc_ng::{
24 block_model::{BlockInit, BlockUpdate, Icon, LabelInit, LabelUpdate},
25 commit::Commit,
26 document::{Document, TitleBlockUpdate},
27 fixtures::{commit, projection},
28 geometry::{FracVal, GridPoint, GridRect, GridSize},
29 id::BlockId,
30 opcode::{Crud, OpCodes},
31 rev::{Confirmed, Rev},
32 session::{ClientSession, Host, Nonce},
33 values::LabelSide,
34 };
35
36 const CLIENTS: usize = 3;
37
38 #[derive(Debug, Clone)]
42 enum Message {
43 Committed { nonce: Nonce, rev: Rev },
44 Rejected { nonce: Nonce },
45 Apply { rev: Rev, commit: Commit },
46 }
47
48 struct Client {
49 session: ClientSession,
50 outbox: VecDeque<(Nonce, Commit)>,
52 inbox: VecDeque<Message>,
54 pending: VecDeque<(Nonce, Commit)>,
57 next_block: u8,
58 }
59
60 struct World {
61 host: Host,
62 clients: Vec<Client>,
63 }
64
65 fn block_of(client: usize, nth: u8) -> BlockId {
67 let mut bytes = [0u8; 16];
68 bytes[0] = u8::try_from(client).expect("few clients");
69 bytes[1] = nth;
70 bytes[2] = 0xb1;
72 BlockId::from_uuid(Uuid::from_bytes(bytes))
73 }
74
75 fn rect(x: i32) -> GridRect {
76 GridRect {
77 top_left: GridPoint { x, y: 0 },
78 size: GridSize { w: 4, h: 4 },
79 }
80 }
81
82 fn label_init(name: &str) -> LabelInit {
83 LabelInit {
84 name: name.into(),
85 side: LabelSide::Bottom,
86 offset: FracVal::from(1.5),
87 hidden: false,
88 }
89 }
90
91 fn block_init(parent: BlockId, name: &str) -> BlockInit {
92 BlockInit {
93 parent,
94 rect: rect(0),
95 locked: false,
96 title: label_init(name),
97 type_label: label_init("kind"),
98 icon: Icon::default(),
99 }
100 }
101
102 fn create_block(id: BlockId, name: &str) -> OpCodes {
103 OpCodes::Block(id, Crud::Create(block_init(BlockId::NULL, name)))
104 }
105
106 fn rename(id: BlockId, name: &str) -> OpCodes {
107 OpCodes::Block(
108 id,
109 Crud::Update(BlockUpdate::Title(LabelUpdate::Name(name.into()))),
110 )
111 }
112
113 fn move_to(id: BlockId, x: i32) -> OpCodes {
114 OpCodes::Block(id, Crud::Update(BlockUpdate::Rect(rect(x))))
115 }
116
117 fn reparent(child: BlockId, parent: BlockId) -> OpCodes {
118 OpCodes::Block(child, Crud::Update(BlockUpdate::Parent(parent)))
119 }
120
121 impl World {
122 fn new() -> Self {
123 Self {
124 host: Host::default(),
125 clients: (0..CLIENTS)
126 .map(|_| Client {
127 session: ClientSession::default(),
128 outbox: VecDeque::new(),
129 inbox: VecDeque::new(),
130 pending: VecDeque::new(),
131 next_block: 0,
132 })
133 .collect(),
134 }
135 }
136
137 fn edit(&mut self, client: usize, commit: Commit) -> bool {
141 let session = &mut self.clients[client].session;
142 let Ok(nonce) = session.submit(commit) else {
143 return false;
144 };
145 let outbound = session
146 .last_submission()
147 .expect("a submission is queued")
148 .1
149 .clone();
150 self.clients[client]
151 .outbox
152 .push_back((nonce, outbound.clone()));
153 self.clients[client].pending.push_back((nonce, outbound));
154 true
155 }
156
157 fn sequence(&mut self, client: usize) {
159 let Some((nonce, commit)) = self.clients[client].outbox.pop_front() else {
160 return;
161 };
162 match self.host.ingest(&commit) {
163 Ok(rev) => {
164 self.clients[client]
165 .inbox
166 .push_back(Message::Committed { nonce, rev });
167 for (ix, other) in self.clients.iter_mut().enumerate() {
168 if ix != client {
169 other.inbox.push_back(Message::Apply {
170 rev,
171 commit: commit.clone(),
172 });
173 }
174 }
175 }
176 Err(_) => self.clients[client]
177 .inbox
178 .push_back(Message::Rejected { nonce }),
179 }
180 }
181
182 fn deliver(&mut self, client: usize, count: usize) {
183 for _ in 0..count {
184 let Some(message) = self.clients[client].inbox.pop_front() else {
185 return;
186 };
187 let client = &mut self.clients[client];
188 match message {
189 Message::Committed { nonce, rev } => {
190 client
191 .session
192 .committed(nonce, rev)
193 .expect("an ack for the front of the queue lands");
194 let (shadow, _) = client.pending.pop_front().expect("a pending entry");
195 assert_eq!(shadow, nonce, "answers arrive in submission order");
196 }
197 Message::Rejected { nonce } => {
198 client.session.rejected(nonce).expect("the rejection lands");
199 let (shadow, _) = client.pending.pop_front().expect("a pending entry");
200 assert_eq!(shadow, nonce);
201 }
202 Message::Apply { rev, commit } => {
203 client
204 .session
205 .apply_foreign(rev, &commit)
206 .expect("a sequenced commit lands");
207 }
208 }
209 }
210 }
211
212 fn quiesce(&mut self) {
213 for client in 0..CLIENTS {
214 while !self.clients[client].outbox.is_empty() {
215 self.sequence(client);
216 }
217 }
218 for client in 0..CLIENTS {
219 let waiting = self.clients[client].inbox.len();
220 self.deliver(client, waiting);
221 }
222 }
223
224 fn assert_oracle(&self) {
228 for (ix, client) in self.clients.iter().enumerate() {
229 let confirmed_count =
230 usize::try_from(client.session.rev().get()).expect("the log fits in memory");
231 let mut replayed = Document::<Confirmed>::default();
232 for commit in &self.host.commits_after(Rev::ZERO)[..confirmed_count] {
233 replayed = replayed
234 .try_apply(commit)
235 .expect("a sequenced commit folds on replay");
236 }
237 let mut predicted = replayed.predict();
238 for (_, commit) in &client.pending {
239 predicted = predicted
240 .try_apply(commit)
241 .expect("the random pool never authors a commit that cannot fold");
242 }
243 assert_eq!(
244 client.session.optimistic().content_hash(),
245 predicted.content_hash(),
246 "client {ix}: optimistic != confirmed-log ⊕ pending",
247 );
248 }
249 }
250
251 fn assert_log_semantics(&self) {
258 let mut title = String::new();
259 let mut blocks: std::collections::BTreeMap<BlockId, (GridRect, String, bool)> =
260 std::collections::BTreeMap::new();
261
262 for commit in self.host.commits_after(Rev::ZERO) {
263 for op in commit.ops() {
264 match op {
265 OpCodes::Document(TitleBlockUpdate::Name(name)) => title = name.clone(),
266 OpCodes::Block(id, Crud::Create(init)) => {
267 blocks.insert(*id, (init.rect, init.title.name.clone(), true));
268 }
269 OpCodes::Block(id, Crud::Update(BlockUpdate::Rect(to))) => {
270 blocks.entry(*id).or_default().0 = *to;
271 }
272 OpCodes::Block(
273 id,
274 Crud::Update(BlockUpdate::Title(LabelUpdate::Name(name))),
275 ) => {
276 blocks.entry(*id).or_default().1 = name.clone();
277 }
278 OpCodes::Block(id, Crud::Delete) => {
279 blocks.entry(*id).or_default().2 = false;
280 }
281 OpCodes::Block(id, Crud::Restore) => {
282 blocks.entry(*id).or_default().2 = true;
283 }
284 _ => panic!("the random pool authored an op this oracle cannot replay"),
285 }
286 }
287 }
288
289 let expected: Vec<_> = blocks
290 .into_iter()
291 .filter(|(_, (_, _, alive))| *alive)
292 .map(|(id, (rect, name, _))| (id, BlockId::NULL, rect, name))
293 .collect();
294 assert_eq!(
295 projection(self.host.state()),
296 expected,
297 "the fold disagrees with last-write-wins over the log",
298 );
299 assert_eq!(
300 self.host.state().title_block().name.as_ref(),
301 &title,
302 "the singleton's register disagrees with the log's last write",
303 );
304 }
305
306 fn assert_converged(&self) {
307 let host = self.host.state().content_hash();
308 for (ix, client) in self.clients.iter().enumerate() {
309 assert!(client.pending.is_empty(), "client {ix} is not quiesced");
310 assert_eq!(
311 client.session.confirmed().content_hash(),
312 host,
313 "client {ix}'s confirmed document differs from the host's",
314 );
315 assert_eq!(
316 client.session.optimistic().content_hash(),
317 host,
318 "client {ix}'s prediction differs from the host's document",
319 );
320 }
321 }
322
323 fn block_ids(&self, client: usize) -> Vec<BlockId> {
324 let mut ids: Vec<_> = self.clients[client]
325 .session
326 .optimistic()
327 .blocks()
328 .map(|(id, _)| id)
329 .collect();
330 ids.sort();
331 ids
332 }
333
334 fn title_of(&self, client: usize, id: BlockId) -> String {
335 self.clients[client]
336 .session
337 .confirmed()
338 .block(&id)
339 .expect("the block is in the document")
340 .as_ref()
341 .title
342 .name
343 .as_ref()
344 .clone()
345 }
346 }
347
348 #[derive(Debug, Clone, Copy)]
349 enum Edit {
350 Create,
351 Move {
352 which: usize,
353 x: i32,
354 },
355 Rename {
356 which: usize,
357 name: u8,
358 },
359 Delete {
360 which: usize,
361 },
362 Restore {
363 which: usize,
364 },
365 Title {
366 name: u8,
367 },
368 MoveTwice {
372 which: usize,
373 first: i32,
374 second: i32,
375 },
376 }
377
378 #[derive(Debug, Clone, Copy)]
379 enum Step {
380 Edit { client: usize, edit: Edit },
381 Sequence { client: usize },
382 Deliver { client: usize, count: usize },
383 }
384
385 impl World {
386 fn apply_edit(&mut self, client: usize, edit: Edit) {
391 let ids = self.block_ids(client);
392 let pick = |which: usize| ids[which % ids.len()];
393 let ops = match edit {
394 Edit::Create => None,
395 _ if ids.is_empty() => None,
396 Edit::Move { which, x } => Some(vec![move_to(pick(which), x)]),
397 Edit::Rename { which, name } => {
398 Some(vec![rename(pick(which), &format!("n{name}"))])
399 }
400 Edit::Delete { which } => Some(vec![OpCodes::Block(pick(which), Crud::Delete)]),
401 Edit::Restore { which } => Some(vec![OpCodes::Block(pick(which), Crud::Restore)]),
402 Edit::Title { name } => Some(vec![OpCodes::Document(TitleBlockUpdate::Name(
403 format!("doc{name}"),
404 ))]),
405 Edit::MoveTwice {
406 which,
407 first,
408 second,
409 } => {
410 let id = pick(which);
411 Some(vec![move_to(id, first), move_to(id, second)])
412 }
413 };
414
415 let ops = ops.unwrap_or_else(|| {
416 let nth = self.clients[client].next_block;
417 self.clients[client].next_block = nth.wrapping_add(1);
418 vec![create_block(block_of(client, nth), "fresh")]
419 });
420 assert!(
421 self.edit(client, commit("edit", ops)),
422 "the random pool must never author an edit the client refuses",
423 );
424 }
425
426 fn step(&mut self, step: Step) {
427 match step {
428 Step::Edit { client, edit } => self.apply_edit(client, edit),
429 Step::Sequence { client } => self.sequence(client),
430 Step::Deliver { client, count } => self.deliver(client, count),
431 }
432 }
433 }
434
435 fn any_edit() -> impl Strategy<Value = Edit> {
436 prop_oneof![
437 2 => Just(Edit::Create),
438 3 => (any::<usize>(), -50i32..50).prop_map(|(which, x)| Edit::Move { which, x }),
439 3 => (any::<usize>(), any::<u8>()).prop_map(|(which, name)| Edit::Rename { which, name }),
440 1 => any::<usize>().prop_map(|which| Edit::Delete { which }),
441 1 => any::<usize>().prop_map(|which| Edit::Restore { which }),
442 1 => any::<u8>().prop_map(|name| Edit::Title { name }),
443 2 => (any::<usize>(), -50i32..50, -50i32..50)
444 .prop_map(|(which, first, second)| Edit::MoveTwice { which, first, second }),
445 ]
446 }
447
448 fn any_step() -> impl Strategy<Value = Step> {
449 prop_oneof![
450 3 => (0..CLIENTS, any_edit()).prop_map(|(client, edit)| Step::Edit { client, edit }),
451 3 => (0..CLIENTS).prop_map(|client| Step::Sequence { client }),
452 3 => (0..CLIENTS, 0usize..4).prop_map(|(client, count)| Step::Deliver { client, count }),
453 ]
454 }
455
456 proptest! {
457 #[test]
462 fn predictions_reconcile_and_everyone_converges(steps in prop::collection::vec(any_step(), 1..80)) {
463 let mut world = World::new();
464 for step in steps {
465 world.step(step);
466 world.assert_oracle();
467 }
468 world.quiesce();
469 world.assert_oracle();
470 world.assert_converged();
471 world.assert_log_semantics();
472 }
473 }
474
475 #[test]
478 fn the_later_commit_in_server_order_wins_the_register_everywhere() {
479 let mut world = World::new();
480 let id = block_of(0, 0);
481 world.edit(0, commit("create", vec![create_block(id, "original")]));
482 world.quiesce();
483
484 world.edit(0, commit("rename", vec![rename(id, "from client 0")]));
487 world.edit(1, commit("rename", vec![rename(id, "from client 1")]));
488 assert_eq!(
489 world.title_of(0, id),
490 "original",
491 "neither rename is confirmed yet",
492 );
493
494 world.sequence(1);
496 world.sequence(0);
497 world.quiesce();
498
499 for client in 0..CLIENTS {
500 assert_eq!(
501 world.title_of(client, id),
502 "from client 0",
503 "client {client} must show the value the later rev wrote",
504 );
505 }
506 world.assert_converged();
507 }
508
509 #[test]
513 fn an_unacked_local_write_outranks_an_incoming_confirmed_one() {
514 let mut world = World::new();
515 let id = block_of(0, 0);
516 world.edit(0, commit("create", vec![create_block(id, "original")]));
517 world.quiesce();
518
519 world.edit(1, commit("rename", vec![rename(id, "local")]));
520 world.edit(0, commit("rename", vec![rename(id, "remote")]));
521 world.sequence(0);
522 world.deliver(1, 1);
523
524 assert_eq!(
525 world.clients[1]
526 .session
527 .optimistic()
528 .block(&id)
529 .expect("the block is present")
530 .as_ref()
531 .title
532 .name
533 .as_ref()
534 .as_str(),
535 "local",
536 "the unacked local edit still suppresses the confirmed one",
537 );
538 assert_eq!(
539 world.title_of(1, id),
540 "remote",
541 "while the confirmed document has already taken the foreign value",
542 );
543
544 world.quiesce();
545 world.assert_converged();
546 }
547
548 #[test]
552 fn a_commit_writing_one_register_twice_ends_on_its_last_write() {
553 let mut world = World::new();
554 let id = block_of(0, 0);
555 world.edit(0, commit("create", vec![create_block(id, "b")]));
556 world.quiesce();
557
558 world.edit(0, commit("drag", vec![move_to(id, 5), move_to(id, 9)]));
559 world.quiesce();
560
561 for client in 0..CLIENTS {
562 assert_eq!(
563 *world.clients[client]
564 .session
565 .confirmed()
566 .block(&id)
567 .expect("the block is present")
568 .as_ref()
569 .rect
570 .as_ref(),
571 rect(9),
572 "client {client} must end on the commit's last write",
573 );
574 }
575 }
576
577 #[test]
586 #[should_panic(expected = "gap in the per-connection rev sequence")]
587 fn re_delivering_a_confirmed_commit_is_refused() {
588 let mut world = World::new();
589 let id = block_of(0, 0);
590 world.edit(0, commit("create", vec![create_block(id, "b")]));
591 world.quiesce();
592
593 let replayed = world.host.commits_after(Rev::ZERO)[0].clone();
594 let stale = world.clients[1].session.rev();
595 let _ = world.clients[1].session.apply_foreign(stale, &replayed);
596 }
597
598 #[test]
602 fn a_commit_the_host_refuses_is_dropped_and_the_client_re_converges() {
603 let mut world = World::new();
604 let (first, second) = (block_of(0, 0), block_of(0, 1));
605 world.edit(
606 0,
607 commit(
608 "two blocks",
609 vec![create_block(first, "a"), create_block(second, "b")],
610 ),
611 );
612 world.quiesce();
613
614 world.edit(0, commit("nest a under b", vec![reparent(first, second)]));
615 world.edit(1, commit("nest b under a", vec![reparent(second, first)]));
616
617 world.sequence(1);
618 let host_after_first = world.host.state().content_hash();
619 world.sequence(0);
620 assert_eq!(
621 world.host.state().content_hash(),
622 host_after_first,
623 "the refused commit must leave the host untouched",
624 );
625 assert_eq!(
626 world.host.rev(),
627 Rev::new(2),
628 "and must consume no rev — the log cannot gap",
629 );
630
631 world.quiesce();
632 world.assert_converged();
633 assert_eq!(
634 *world.clients[0]
635 .session
636 .confirmed()
637 .block(&first)
638 .expect("the block is present")
639 .as_ref()
640 .parent
641 .as_ref(),
642 BlockId::NULL,
643 "client 0's refused reparent is gone from its document",
644 );
645 }
646
647 #[test]
651 fn undo_restores_the_visible_document_through_the_wire() {
652 let id = block_of(0, 0);
653 let created = block_of(0, 1);
654 for edit in [
655 commit("move", vec![move_to(id, 7)]),
656 commit("rename", vec![rename(id, "renamed")]),
657 commit("delete", vec![OpCodes::Block(id, Crud::Delete)]),
658 commit(
659 "create",
660 vec![create_block(created, "new"), move_to(created, 3)],
661 ),
662 commit("drag", vec![move_to(id, 4), move_to(id, 8)]),
663 commit(
664 "multi",
665 vec![
666 move_to(id, 2),
667 rename(id, "multi"),
668 create_block(created, "c"),
669 ],
670 ),
671 ] {
672 let mut world = World::new();
673 world.edit(0, commit("create", vec![create_block(id, "original")]));
674 world.quiesce();
675 let before = projection(world.clients[0].session.confirmed());
676
677 let label = edit.label().to_owned();
678 world.edit(0, edit);
679 world.quiesce();
680 assert_ne!(
681 projection(world.clients[0].session.confirmed()),
682 before,
683 "{label}: the edit must be observable or the round trip proves nothing",
684 );
685
686 let undone = world.clients[0]
687 .session
688 .undo()
689 .expect("something to undo")
690 .expect("the inverse folds");
691 let outbound = world.clients[0]
692 .session
693 .last_submission()
694 .expect("the undo is queued")
695 .1
696 .clone();
697 world.clients[0]
698 .outbox
699 .push_back((undone, outbound.clone()));
700 world.clients[0].pending.push_back((undone, outbound));
701 world.quiesce();
702
703 assert_eq!(
704 projection(world.clients[0].session.confirmed()),
705 before,
706 "{label}: undo must restore the visible document",
707 );
708 world.assert_converged();
709 }
710 }
711
712 #[test]
715 fn a_delete_and_its_restore_travel_to_every_client() {
716 let mut world = World::new();
717 let id = block_of(0, 0);
718 world.edit(0, commit("create", vec![create_block(id, "b")]));
719 world.quiesce();
720
721 world.edit(0, commit("delete", vec![OpCodes::Block(id, Crud::Delete)]));
722 world.quiesce();
723 for client in 0..CLIENTS {
724 assert!(
725 projection(world.clients[client].session.confirmed()).is_empty(),
726 "client {client} must not show a tombstoned block",
727 );
728 }
729
730 world.edit(
731 1,
732 commit("restore", vec![OpCodes::Block(id, Crud::Restore)]),
733 );
734 world.quiesce();
735 for client in 0..CLIENTS {
736 assert_eq!(
737 world.title_of(client, id),
738 "b",
739 "client {client} must see the restored block, inner values intact",
740 );
741 }
742 world.assert_converged();
743 }
744}