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