1use std::collections::{HashMap, HashSet};
19use std::sync::Arc;
20
21use serde_json::{json, Value};
22
23use crate::html::Html;
24
25pub type Path = Vec<u32>;
27
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub enum Op {
30 Replace {
32 path: Path,
33 html: Html,
34 },
35 SetText {
36 path: Path,
37 text: String,
38 },
39 SetAttr {
40 path: Path,
41 name: String,
42 value: String,
43 },
44 RemoveAttr {
45 path: Path,
46 name: String,
47 },
48 Insert {
50 path: Path,
51 index: u32,
52 html: Html,
53 },
54 Remove {
55 path: Path,
56 index: u32,
57 },
58 Move {
60 path: Path,
61 from: u32,
62 to: u32,
63 },
64}
65
66impl Op {
67 pub fn to_wire(&self) -> Value {
69 match self {
70 Op::Replace { path, html } => json!([0, path, html.to_wire()]),
71 Op::SetText { path, text } => json!([1, path, text]),
72 Op::SetAttr { path, name, value } => json!([2, path, name, value]),
73 Op::RemoveAttr { path, name } => json!([3, path, name]),
74 Op::Insert { path, index, html } => json!([4, path, index, html.to_wire()]),
75 Op::Remove { path, index } => json!([5, path, index]),
76 Op::Move { path, from, to } => json!([6, path, from, to]),
77 }
78 }
79}
80
81pub fn diff(old: &Html, new: &Html) -> Vec<Op> {
83 let mut ops = Vec::new();
84 let mut path = Vec::new();
85 diff_node(old, new, &mut path, &mut ops);
86 ops
87}
88
89fn diff_node(old: &Html, new: &Html, path: &mut Path, ops: &mut Vec<Op>) {
90 if old.hash() == new.hash() {
91 return; }
93 match (old, new) {
94 (Html::Text { .. }, Html::Text { text, .. }) => ops.push(Op::SetText {
95 path: path.clone(),
96 text: text.clone(),
97 }),
98 (
99 Html::Element {
100 tag: old_tag,
101 key: old_key,
102 attrs: old_attrs,
103 children: old_children,
104 ..
105 },
106 Html::Element {
107 tag: new_tag,
108 key: new_key,
109 attrs: new_attrs,
110 children: new_children,
111 ..
112 },
113 ) if old_tag == new_tag && old_key == new_key => {
114 diff_attrs(old_attrs, new_attrs, path, ops);
115 diff_children(old_children, new_children, path, ops);
116 }
117 _ => ops.push(Op::Replace {
118 path: path.clone(),
119 html: new.clone(),
120 }),
121 }
122}
123
124fn diff_attrs(old: &[(String, String)], new: &[(String, String)], path: &Path, ops: &mut Vec<Op>) {
125 for (name, value) in new {
126 match old.iter().find(|(k, _)| k == name) {
127 Some((_, old_value)) if old_value == value => {}
128 _ => ops.push(Op::SetAttr {
129 path: path.clone(),
130 name: name.clone(),
131 value: value.clone(),
132 }),
133 }
134 }
135 for (name, _) in old {
136 if !new.iter().any(|(k, _)| k == name) {
137 ops.push(Op::RemoveAttr {
138 path: path.clone(),
139 name: name.clone(),
140 });
141 }
142 }
143}
144
145fn diff_children(old: &[Arc<Html>], new: &[Arc<Html>], path: &mut Path, ops: &mut Vec<Op>) {
146 let (head, tail) = shared_ends(old, new);
147 if both_keyed(old, new, head, tail) {
148 diff_keyed_from(
149 &old[head..old.len() - tail],
150 &new[head..new.len() - tail],
151 head as u32,
152 path,
153 ops,
154 );
155 } else {
156 diff_positional(old, new, path, ops);
161 }
162}
163
164fn shared_ends(old: &[Arc<Html>], new: &[Arc<Html>]) -> (usize, usize) {
170 let mut head = 0;
171 while head < old.len() && head < new.len() && Arc::ptr_eq(&old[head], &new[head]) {
172 head += 1;
173 }
174 let mut tail = 0;
175 while head + tail < old.len()
176 && head + tail < new.len()
177 && Arc::ptr_eq(&old[old.len() - 1 - tail], &new[new.len() - 1 - tail])
178 {
179 tail += 1;
180 }
181 (head, tail)
182}
183
184fn both_keyed(old: &[Arc<Html>], new: &[Arc<Html>], head: usize, tail: usize) -> bool {
198 if old.is_empty() || new.is_empty() {
201 return false;
202 }
203 let mut seen: HashSet<&str> = HashSet::with_capacity(old.len());
204 let shared = old[..head]
206 .iter()
207 .chain(&old[old.len() - tail..])
208 .map(|c| c.key_of());
209 for key in shared {
210 match key {
211 Some(k) if seen.insert(k) => {}
212 _ => return false,
213 }
214 }
215 let old_window = &old[head..old.len() - tail];
218 let new_window = &new[head..new.len() - tail];
219 for window in [old_window, new_window] {
220 for child in window {
221 match child.key_of() {
222 Some(k) if seen.insert(k) => {}
223 _ => return false,
224 }
225 }
226 for child in window {
227 seen.remove(child.key_of().expect("just inserted"));
228 }
229 }
230 true
231}
232
233fn diff_positional(old: &[Arc<Html>], new: &[Arc<Html>], path: &mut Path, ops: &mut Vec<Op>) {
234 let common = old.len().min(new.len());
235 for i in 0..common {
236 path.push(i as u32);
237 diff_node(&old[i], &new[i], path, ops);
238 path.pop();
239 }
240 for i in (common..old.len()).rev() {
241 ops.push(Op::Remove {
242 path: path.clone(),
243 index: i as u32,
244 });
245 }
246 for (i, node) in new.iter().enumerate().skip(common) {
247 ops.push(Op::Insert {
248 path: path.clone(),
249 index: i as u32,
250 html: (**node).clone(),
251 });
252 }
253}
254
255fn diff_keyed_from(
261 old: &[Arc<Html>],
262 new: &[Arc<Html>],
263 base: u32,
264 path: &mut Path,
265 ops: &mut Vec<Op>,
266) {
267 let wanted: HashSet<&str> = new.iter().filter_map(|c| c.key_of()).collect();
268
269 let mut kept: Vec<&Html> = Vec::with_capacity(old.len().min(new.len()));
274 for c in old {
275 if c.key_of().is_some_and(|k| wanted.contains(k)) {
276 kept.push(c);
277 } else {
278 ops.push(Op::Remove {
279 path: path.clone(),
280 index: base + kept.len() as u32,
281 });
282 }
283 }
284
285 let mut origin: HashMap<&str, usize> = HashMap::with_capacity(kept.len());
290 for (p, c) in kept.iter().enumerate() {
291 if let Some(k) = c.key_of() {
292 origin.insert(k, p);
293 }
294 }
295
296 let mut unclaimed = Unclaimed::new(kept.len());
297 for (j, want) in new.iter().enumerate() {
298 let node = match want.key_of().and_then(|k| origin.remove(k)) {
299 Some(p) => {
300 let offset = unclaimed.ahead_of(p);
304 if offset > 0 {
305 ops.push(Op::Move {
306 path: path.clone(),
307 from: base + (j + offset) as u32,
308 to: base + j as u32,
309 });
310 }
311 unclaimed.claim(p);
312 kept[p]
313 }
314 None => {
315 ops.push(Op::Insert {
316 path: path.clone(),
317 index: base + j as u32,
318 html: (**want).clone(),
319 });
320 continue; }
322 };
323 path.push(base + j as u32);
324 diff_node(node, want, path, ops);
325 path.pop();
326 }
327}
328
329struct Unclaimed {
339 tree: Vec<u32>,
342}
343
344impl Unclaimed {
345 fn new(n: usize) -> Self {
348 let mut tree = vec![0u32; n + 1];
349 for i in 1..=n {
350 tree[i] += 1;
351 let parent = i + (i & i.wrapping_neg());
352 if parent <= n {
353 let carried = tree[i];
354 tree[parent] += carried;
355 }
356 }
357 Self { tree }
358 }
359
360 fn ahead_of(&self, p: usize) -> usize {
362 let mut i = p;
363 let mut sum = 0u32;
364 while i > 0 {
365 sum += self.tree[i];
366 i -= i & i.wrapping_neg();
367 }
368 sum as usize
369 }
370
371 fn claim(&mut self, p: usize) {
373 let mut i = p + 1;
374 while i < self.tree.len() {
375 self.tree[i] -= 1;
376 i += i & i.wrapping_neg();
377 }
378 }
379}
380
381pub fn apply(root: &Html, ops: &[Op]) -> Html {
387 let mut root = root.clone();
388 for op in ops {
389 match op {
390 Op::Replace { path, html } => set_node(&mut root, path, html.clone()),
391 Op::SetText { path, text } => set_node(&mut root, path, Html::text(text.clone())),
392 Op::SetAttr { path, name, value } => {
393 let target = node_mut(&mut root, path);
394 *target = with_attr(target.clone(), name, Some(value));
395 }
396 Op::RemoveAttr { path, name } => {
397 let target = node_mut(&mut root, path);
398 *target = with_attr(target.clone(), name, None);
399 }
400 Op::Insert { path, index, html } => {
401 let parent = node_mut(&mut root, path);
402 *parent = rebuild(parent.clone(), |cs| {
403 cs.insert(*index as usize, Arc::new(html.clone()));
404 });
405 }
406 Op::Remove { path, index } => {
407 let parent = node_mut(&mut root, path);
408 *parent = rebuild(parent.clone(), |cs| {
409 cs.remove(*index as usize);
410 });
411 }
412 Op::Move { path, from, to } => {
413 let parent = node_mut(&mut root, path);
414 *parent = rebuild(parent.clone(), |cs| {
415 let node = cs.remove(*from as usize);
416 cs.insert(*to as usize, node);
417 });
418 }
419 }
420 }
421 root.rehash()
424}
425
426fn node_mut<'a>(root: &'a mut Html, path: &[u32]) -> &'a mut Html {
427 let mut node = root;
428 for step in path {
429 node = match node {
430 Html::Element { children, .. } => Arc::make_mut(&mut children[*step as usize]),
434 Html::Text { .. } => panic!("patch path descends into a text node"),
435 };
436 }
437 node
438}
439
440fn set_node(root: &mut Html, path: &[u32], value: Html) {
441 *node_mut(root, path) = value;
442}
443
444fn rebuild(node: Html, f: impl FnOnce(&mut Vec<Arc<Html>>)) -> Html {
446 match node {
447 Html::Element {
448 tag,
449 attrs,
450 key,
451 mut children,
452 ..
453 } => {
454 f(&mut children);
455 let mut el = Html::el(tag);
456 for (k, v) in attrs {
457 el = el.attr(k, v);
458 }
459 if let Some(k) = key {
460 el = el.key(k);
461 }
462 el.children_shared(children)
463 }
464 text => text,
465 }
466}
467
468fn with_attr(node: Html, name: &str, value: Option<&str>) -> Html {
469 match node {
470 Html::Element {
471 tag,
472 attrs,
473 key,
474 children,
475 ..
476 } => {
477 let mut el = Html::el(tag);
478 let mut replaced = false;
479 for (k, v) in attrs {
480 if k == name {
481 replaced = true;
482 match value {
483 Some(new) => el = el.attr(k, new),
484 None => continue,
485 }
486 } else {
487 el = el.attr(k, v);
488 }
489 }
490 if !replaced {
491 if let Some(new) = value {
492 el = el.attr(name, new);
493 }
494 }
495 if let Some(k) = key {
496 el = el.key(k);
497 }
498 el.children_shared(children)
499 }
500 text => text,
501 }
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507
508 fn li(key: &str, text: &str, done: bool) -> Html {
509 Html::el("li")
510 .key(key)
511 .attr_if(done, "class", "done")
512 .child(Html::text(text))
513 }
514
515 fn list(items: Vec<Html>) -> Html {
516 Html::el("main")
517 .child(Html::el("h1").child(Html::text("todos")))
518 .child(Html::el("ul").children(items))
519 }
520
521 #[test]
522 fn identical_trees_produce_no_ops() {
523 let a = list(vec![li("1", "a", false), li("2", "b", false)]);
524 let b = list(vec![li("1", "a", false), li("2", "b", false)]);
525 assert!(diff(&a, &b).is_empty());
526 }
527
528 #[test]
529 fn a_toggle_touches_one_attribute_and_nothing_else() {
530 let a = list(vec![li("1", "a", false), li("2", "b", false)]);
531 let b = list(vec![li("1", "a", true), li("2", "b", false)]);
532 let ops = diff(&a, &b);
533 assert_eq!(
534 ops,
535 vec![Op::SetAttr {
536 path: vec![1, 0],
537 name: "class".into(),
538 value: "done".into(),
539 }]
540 );
541 assert_eq!(apply(&a, &ops), b);
542 }
543
544 #[test]
545 fn reordering_moves_rather_than_rebuilds() {
546 let a = list(vec![
547 li("1", "a", false),
548 li("2", "b", false),
549 li("3", "c", false),
550 ]);
551 let b = list(vec![
552 li("3", "c", false),
553 li("1", "a", false),
554 li("2", "b", false),
555 ]);
556 let ops = diff(&a, &b);
557 assert_eq!(
558 ops,
559 vec![Op::Move {
560 path: vec![1],
561 from: 2,
562 to: 0
563 }]
564 );
565 assert_eq!(apply(&a, &ops), b);
566 }
567
568 #[test]
569 fn insert_remove_and_edit_compose() {
570 let a = list(vec![
571 li("1", "a", false),
572 li("2", "b", false),
573 li("3", "c", false),
574 ]);
575 let b = list(vec![
576 li("2", "b!", true),
577 li("4", "d", false),
578 li("1", "a", false),
579 ]);
580 let ops = diff(&a, &b);
581 assert_eq!(apply(&a, &ops), b, "ops: {ops:#?}");
582 }
583
584 #[test]
585 fn unkeyed_children_fall_back_to_positional() {
586 let a = Html::el("footer").child(Html::text("2 remaining"));
587 let b = Html::el("footer").child(Html::text("1 remaining"));
588 let ops = diff(&a, &b);
589 assert_eq!(
590 ops,
591 vec![Op::SetText {
592 path: vec![0],
593 text: "1 remaining".into()
594 }]
595 );
596 assert_eq!(apply(&a, &ops), b);
597 }
598
599 #[test]
600 fn tag_change_replaces() {
601 let a = Html::el("main").child(Html::el("p").child(Html::text("x")));
602 let b = Html::el("main").child(Html::el("div").child(Html::text("x")));
603 let ops = diff(&a, &b);
604 assert!(matches!(ops.as_slice(), [Op::Replace { .. }]));
605 assert_eq!(apply(&a, &ops), b);
606 }
607
608 #[test]
621 fn a_shared_page_and_a_copied_one_produce_the_same_ops() {
622 fn shared(items: &[Arc<Html>]) -> Html {
623 let mut el = Html::el("ul");
624 for i in items {
625 el = el.child_shared(i.clone());
626 }
627 el
628 }
629 let pool: Vec<Arc<Html>> = (0..24)
630 .map(|i| Arc::new(li(&format!("k{i}"), &format!("item {i}"), i % 3 == 0)))
631 .collect();
632
633 let mut seed = 0x5eedu64;
636 let mut rand = move |n: usize| {
637 seed = seed
638 .wrapping_mul(6364136223846793005)
639 .wrapping_add(1442695040888963407);
640 (seed >> 33) as usize % n.max(1)
641 };
642 let mut exercised = 0usize;
643 for case in 0..200 {
644 let take = 4 + rand(16);
645 let old_rows: Vec<Arc<Html>> = pool.iter().take(take).cloned().collect();
646 let mut new_rows = old_rows.clone();
647 match case % 6 {
648 0 => new_rows.insert(0, pool[23].clone()),
649 1 => new_rows.push(pool[23].clone()),
650 2 => {
651 if !new_rows.is_empty() {
652 new_rows.remove(rand(new_rows.len()));
653 }
654 }
655 3 => new_rows.reverse(),
656 4 => {
657 if new_rows.len() > 2 {
658 let n = new_rows.len();
659 new_rows.swap(1, n - 1);
660 }
661 }
662 _ => {
663 let at = rand(new_rows.len());
666 new_rows[at] = Arc::new(li(&format!("k{at}"), "edited", true));
667 }
668 }
669 let (old, new) = (shared(&old_rows), shared(&new_rows));
670 let (old_copy, new_copy) = (old.rehash(), new.rehash());
672 assert_eq!(old, old_copy, "rehash must preserve the value, case {case}");
673
674 let trimmed = diff(&old, &new);
675 let full = diff(&old_copy, &new_copy);
676 assert_eq!(
677 trimmed, full,
678 "case {case}: trimming the shared ends changed the ops"
679 );
680 assert_eq!(
681 apply(&old, &trimmed),
682 new.rehash(),
683 "case {case}: round trip"
684 );
685 if old_rows
686 .first()
687 .zip(new_rows.first())
688 .is_some_and(|(a, b)| Arc::ptr_eq(a, b))
689 || old_rows
690 .last()
691 .zip(new_rows.last())
692 .is_some_and(|(a, b)| Arc::ptr_eq(a, b))
693 {
694 exercised += 1;
695 }
696 }
697 assert!(
700 exercised > 100,
701 "only {exercised} of 200 cases shared an end, so the trim was barely exercised"
702 );
703 }
704
705 #[test]
718 fn a_repeated_key_in_the_part_two_pages_share_forces_the_positional_path() {
719 let dup_a = Arc::new(li("dup", "first", false));
722 let dup_b = Arc::new(li("dup", "second", false));
723 let x = Arc::new(li("x", "x", false));
724 let y = Arc::new(li("y", "y", false));
725 let end = Arc::new(li("end", "end", false));
726
727 let old = Html::el("ul").children_shared([
728 Arc::clone(&dup_a),
729 Arc::clone(&dup_b),
730 Arc::clone(&x),
731 Arc::clone(&y),
732 Arc::clone(&end),
733 ]);
734 let new = Html::el("ul").children_shared([
735 Arc::clone(&dup_a),
736 Arc::clone(&dup_b),
737 Arc::clone(&y),
738 Arc::clone(&x),
739 Arc::clone(&end),
740 ]);
741
742 let ops = diff(&old, &new);
743 assert!(
744 !ops.iter().any(|o| matches!(o, Op::Move { .. })),
745 "`dup` is carried by two children, so this list cannot reconcile by key and the \
746 positional path is the honest answer — but the ops moved something: {ops:?}"
747 );
748 assert_eq!(
749 apply(&old, &ops),
750 new.rehash(),
751 "and it still has to round trip"
752 );
753
754 let un_dup = Arc::new(li("dup2", "second", false));
757 let keyed_old = Html::el("ul").children_shared([
758 Arc::clone(&dup_a),
759 Arc::clone(&un_dup),
760 Arc::clone(&x),
761 Arc::clone(&y),
762 Arc::clone(&end),
763 ]);
764 let keyed_new = Html::el("ul").children_shared([
765 Arc::clone(&dup_a),
766 Arc::clone(&un_dup),
767 Arc::clone(&y),
768 Arc::clone(&x),
769 Arc::clone(&end),
770 ]);
771 let keyed_ops = diff(&keyed_old, &keyed_new);
772 assert!(
773 keyed_ops.iter().any(|o| matches!(o, Op::Move { .. })),
774 "with distinct keys the same reorder should move rather than rebuild: {keyed_ops:?}"
775 );
776 assert_eq!(apply(&keyed_old, &keyed_ops), keyed_new.rehash());
777 }
778
779 fn scan_keyed_from(
782 old: &[Arc<Html>],
783 new: &[Arc<Html>],
784 base: u32,
785 path: &mut Path,
786 ops: &mut Vec<Op>,
787 ) {
788 let wanted: HashSet<&str> = new.iter().filter_map(|c| c.key_of()).collect();
789 let mut cursor: Vec<&Html> = old.iter().map(|c| &**c).collect();
790 let mut i = 0;
791 while i < cursor.len() {
792 if cursor[i].key_of().is_some_and(|k| wanted.contains(k)) {
793 i += 1;
794 } else {
795 ops.push(Op::Remove {
796 path: path.clone(),
797 index: base + i as u32,
798 });
799 cursor.remove(i);
800 }
801 }
802 for (j, want) in new.iter().enumerate() {
803 let key = want.key_of();
804 let found = cursor[j..].iter().position(|c| c.key_of() == key);
805 match found {
806 Some(0) => {}
807 Some(offset) => {
808 ops.push(Op::Move {
809 path: path.clone(),
810 from: base + (j + offset) as u32,
811 to: base + j as u32,
812 });
813 let node = cursor.remove(j + offset);
814 cursor.insert(j, node);
815 }
816 None => {
817 ops.push(Op::Insert {
818 path: path.clone(),
819 index: base + j as u32,
820 html: (**want).clone(),
821 });
822 cursor.insert(j, want);
823 continue;
824 }
825 }
826 path.push(base + j as u32);
827 diff_node(cursor[j], want, path, ops);
828 path.pop();
829 }
830 }
831
832 #[test]
845 fn the_rank_structure_and_the_scan_it_replaced_emit_the_same_ops() {
846 let mut seed = 0xd1ffu64;
847 let mut rand = move |n: usize| {
848 seed = seed
849 .wrapping_mul(6364136223846793005)
850 .wrapping_add(1442695040888963407);
851 (seed >> 33) as usize % n.max(1)
852 };
853 let (mut moved, mut inserted, mut removed, mut in_place) = (0usize, 0, 0, 0);
854
855 for case in 0..300 {
856 let n = rand(24);
857 let old: Vec<Arc<Html>> = (0..n)
858 .map(|i| Arc::new(li(&format!("k{i}"), &format!("row {i}"), false)))
859 .collect();
860
861 let mut kept: Vec<Arc<Html>> = Vec::new();
864 for c in &old {
865 if rand(4) == 0 {
866 continue;
867 }
868 if rand(3) == 0 {
869 let key = c.key_of().expect("a keyed child").to_string();
870 let done = rand(2) == 0;
871 kept.push(Arc::new(li(&key, "edited", done)));
872 } else {
873 kept.push(Arc::clone(c));
874 }
875 }
876 for _ in 0..rand(6) {
877 if !kept.is_empty() {
878 let from = rand(kept.len());
879 let to = rand(kept.len());
880 let node = kept.remove(from);
881 kept.insert(to, node);
882 }
883 }
884 for fresh in 0..rand(4) {
885 let at = rand(kept.len() + 1);
886 kept.insert(at, Arc::new(li(&format!("fresh{fresh}"), "new", false)));
887 }
888 let new = kept;
889
890 let mut fast = Vec::new();
891 let mut scan = Vec::new();
892 diff_keyed_from(&old, &new, 0, &mut Path::default(), &mut fast);
893 scan_keyed_from(&old, &new, 0, &mut Path::default(), &mut scan);
894 assert_eq!(
895 fast,
896 scan,
897 "case {case}: the rank structure diverged from the scan\n old {:?}\n new {:?}",
898 old.iter().map(|c| c.key_of()).collect::<Vec<_>>(),
899 new.iter().map(|c| c.key_of()).collect::<Vec<_>>(),
900 );
901
902 for op in &fast {
903 match op {
904 Op::Move { .. } => moved += 1,
905 Op::Insert { .. } => inserted += 1,
906 Op::Remove { .. } => removed += 1,
907 _ => {}
908 }
909 }
910 in_place += new.len().saturating_sub(
911 fast.iter()
912 .filter(|o| matches!(o, Op::Move { .. } | Op::Insert { .. }))
913 .count(),
914 );
915 }
916 assert!(
917 moved > 100 && inserted > 100 && removed > 100 && in_place > 100,
918 "the generator stopped covering an outcome: {moved} moves, {inserted} inserts, \
919 {removed} removes, {in_place} left in place"
920 );
921 }
922
923 #[test]
924 fn round_trips_over_a_long_random_walk() {
925 let mut seed = 0x243f_6a88_85a3_08d3u64;
927 let mut rand = move || {
928 seed ^= seed << 13;
929 seed ^= seed >> 7;
930 seed ^= seed << 17;
931 seed
932 };
933
934 let mut items: Vec<(u32, String, bool)> = Vec::new();
935 let mut next_key = 0u32;
936 let mut current = list(vec![]);
937
938 for _ in 0..400 {
939 match rand() % 4 {
940 0 => {
941 next_key += 1;
942 let at = if items.is_empty() {
943 0
944 } else {
945 (rand() as usize) % (items.len() + 1)
946 };
947 items.insert(at, (next_key, format!("todo {next_key}"), false));
948 }
949 1 if !items.is_empty() => {
950 let at = (rand() as usize) % items.len();
951 items.remove(at);
952 }
953 2 if !items.is_empty() => {
954 let at = (rand() as usize) % items.len();
955 items[at].2 = !items[at].2;
956 }
957 3 if items.len() > 1 => {
958 let from = (rand() as usize) % items.len();
959 let to = (rand() as usize) % items.len();
960 let item = items.remove(from);
961 items.insert(to, item);
962 }
963 _ => {}
964 }
965
966 let next = list(
967 items
968 .iter()
969 .map(|(k, t, d)| li(&k.to_string(), t, *d))
970 .collect(),
971 );
972 let ops = diff(¤t, &next);
973 assert_eq!(apply(¤t, &ops), next, "ops: {ops:#?}");
974 current = next;
975 }
976 }
977}