1use crate::runtime::RuntimeState;
65use crate::runtime::state::{
66 IdSnapshot, ObligationStateSnapshot, RegionStateSnapshot, RuntimeSnapshot, TaskSnapshot,
67 TaskStateSnapshot,
68};
69use crate::types::Time;
70use serde::{Deserialize, Serialize};
71use std::collections::{HashMap, HashSet};
72use std::fmt;
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum RestoreError {
77 OrphanTask {
79 task_id: u32,
81 region_id: u32,
83 },
84 OrphanObligation {
86 obligation_id: u32,
88 task_id: u32,
90 },
91 OrphanObligationRegion {
93 obligation_id: u32,
95 region_id: u32,
97 },
98 ObligationRegionMismatch {
100 obligation_id: u32,
102 task_id: u32,
104 holder_region_id: u32,
106 owning_region_id: u32,
108 },
109 InvalidParent {
111 region_id: u32,
113 parent_id: u32,
115 },
116 CyclicRegionTree {
118 cycle: Vec<u32>,
120 },
121 NonQuiescentClosure {
123 region_id: u32,
125 live_children: Vec<u32>,
127 live_tasks: Vec<u32>,
129 },
130 InvalidTimestamp {
132 snapshot_time: u64,
134 entity_time: u64,
136 entity: String,
138 },
139 DuplicateId {
141 kind: &'static str,
143 id: u32,
145 },
146}
147
148impl fmt::Display for RestoreError {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 match self {
151 Self::OrphanTask { task_id, region_id } => {
152 write!(
153 f,
154 "task {task_id} references non-existent region {region_id}"
155 )
156 }
157 Self::OrphanObligation {
158 obligation_id,
159 task_id,
160 } => {
161 write!(
162 f,
163 "obligation {obligation_id} references non-existent task {task_id}"
164 )
165 }
166 Self::OrphanObligationRegion {
167 obligation_id,
168 region_id,
169 } => {
170 write!(
171 f,
172 "obligation {obligation_id} references non-existent owning region {region_id}"
173 )
174 }
175 Self::ObligationRegionMismatch {
176 obligation_id,
177 task_id,
178 holder_region_id,
179 owning_region_id,
180 } => {
181 write!(
182 f,
183 "obligation {obligation_id} held by task {task_id} is in region \
184 {holder_region_id}, but records owning region {owning_region_id}"
185 )
186 }
187 Self::InvalidParent {
188 region_id,
189 parent_id,
190 } => {
191 write!(
192 f,
193 "region {region_id} references non-existent parent {parent_id}"
194 )
195 }
196 Self::CyclicRegionTree { cycle } => {
197 write!(f, "region tree contains cycle: {cycle:?}")
198 }
199 Self::NonQuiescentClosure {
200 region_id,
201 live_children,
202 live_tasks,
203 } => {
204 write!(
205 f,
206 "closed region {region_id} has {} live children and {} live tasks",
207 live_children.len(),
208 live_tasks.len()
209 )
210 }
211 Self::InvalidTimestamp {
212 snapshot_time,
213 entity_time,
214 entity,
215 } => {
216 write!(
217 f,
218 "timestamp inconsistency: snapshot={snapshot_time}, {entity}={entity_time}"
219 )
220 }
221 Self::DuplicateId { kind, id } => {
222 write!(f, "duplicate {kind} ID: {id}")
223 }
224 }
225 }
226}
227
228impl std::error::Error for RestoreError {}
229
230#[derive(Debug, Clone)]
232pub struct ValidationResult {
233 pub is_valid: bool,
235 pub errors: Vec<RestoreError>,
237 pub stats: SnapshotStats,
239}
240
241#[derive(Debug, Clone, Default)]
243pub struct SnapshotStats {
244 pub region_count: usize,
246 pub task_count: usize,
248 pub obligation_count: usize,
250 pub max_depth: usize,
252 pub terminal_task_count: usize,
254 pub resolved_obligation_count: usize,
256 pub closed_region_count: usize,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct RestorableSnapshot {
265 pub snapshot: RuntimeSnapshot,
267 pub schema_version: u32,
269 pub content_hash: u64,
271}
272
273impl RestorableSnapshot {
274 pub const SCHEMA_VERSION: u32 = 1;
276
277 #[must_use]
279 pub fn new(snapshot: RuntimeSnapshot) -> Self {
280 let schema_version = Self::SCHEMA_VERSION;
281 let content_hash = Self::compute_hash(schema_version, &snapshot);
282 Self {
283 snapshot,
284 schema_version,
285 content_hash,
286 }
287 }
288
289 fn compute_hash(schema_version: u32, snapshot: &RuntimeSnapshot) -> u64 {
291 const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
293 const FNV_PRIME: u64 = 0x0100_0000_01b3;
294
295 let mut hash = FNV_OFFSET;
296 for byte in schema_version.to_le_bytes() {
297 hash ^= u64::from(byte);
298 hash = hash.wrapping_mul(FNV_PRIME);
299 }
300 if let Ok(encoded) = serde_json::to_vec(snapshot) {
304 for byte in encoded {
305 hash ^= u64::from(byte);
306 hash = hash.wrapping_mul(FNV_PRIME);
307 }
308 } else {
309 for byte in b"snapshot-hash-serialization-error" {
311 hash ^= u64::from(*byte);
312 hash = hash.wrapping_mul(FNV_PRIME);
313 }
314 }
315
316 hash
317 }
318
319 #[must_use]
328 #[allow(clippy::too_many_lines)]
329 pub fn validate(&self) -> ValidationResult {
330 let mut errors = Vec::new();
331 let mut stats = SnapshotStats::default();
332
333 let region_ids: HashSet<SnapshotIdKey> = self
335 .snapshot
336 .regions
337 .iter()
338 .map(|region| snapshot_id_key(region.id))
339 .collect();
340 let task_ids: HashSet<SnapshotIdKey> = self
341 .snapshot
342 .tasks
343 .iter()
344 .map(|task| snapshot_id_key(task.id))
345 .collect();
346 let task_regions: HashMap<SnapshotIdKey, SnapshotIdKey> = self
347 .snapshot
348 .tasks
349 .iter()
350 .map(|task| (snapshot_id_key(task.id), snapshot_id_key(task.region_id)))
351 .collect();
352 let region_slots: HashSet<u32> = self
353 .snapshot
354 .regions
355 .iter()
356 .map(|region| region.id.index)
357 .collect();
358 let task_slots: HashSet<u32> = self
359 .snapshot
360 .tasks
361 .iter()
362 .map(|task| task.id.index)
363 .collect();
364 let obligation_slots: HashSet<u32> = self
365 .snapshot
366 .obligations
367 .iter()
368 .map(|obligation| obligation.id.index)
369 .collect();
370
371 stats.region_count = self.snapshot.regions.len();
372 stats.task_count = self.snapshot.tasks.len();
373 stats.obligation_count = self.snapshot.obligations.len();
374 let snapshot_time = self.snapshot.timestamp;
375
376 if region_slots.len() != self.snapshot.regions.len() {
378 let mut seen = HashSet::new();
380 for region in &self.snapshot.regions {
381 if !seen.insert(region.id.index) {
382 errors.push(RestoreError::DuplicateId {
383 kind: "region",
384 id: region.id.index,
385 });
386 }
387 }
388 }
389
390 if task_slots.len() != self.snapshot.tasks.len() {
392 let mut seen = HashSet::new();
393 for task in &self.snapshot.tasks {
394 if !seen.insert(task.id.index) {
395 errors.push(RestoreError::DuplicateId {
396 kind: "task",
397 id: task.id.index,
398 });
399 }
400 }
401 }
402
403 if obligation_slots.len() != self.snapshot.obligations.len() {
405 let mut seen = HashSet::new();
406 for obligation in &self.snapshot.obligations {
407 if !seen.insert(obligation.id.index) {
408 errors.push(RestoreError::DuplicateId {
409 kind: "obligation",
410 id: obligation.id.index,
411 });
412 }
413 }
414 }
415
416 for task in &self.snapshot.tasks {
418 if task.created_at > snapshot_time {
419 errors.push(RestoreError::InvalidTimestamp {
420 snapshot_time,
421 entity_time: task.created_at,
422 entity: format!("task {} created_at", task.id.index),
423 });
424 }
425 if !region_ids.contains(&snapshot_id_key(task.region_id)) {
426 errors.push(RestoreError::OrphanTask {
427 task_id: task.id.index,
428 region_id: task.region_id.index,
429 });
430 }
431 if is_task_terminal(&task.state) {
432 stats.terminal_task_count += 1;
433 }
434 }
435
436 for obligation in &self.snapshot.obligations {
438 if obligation.created_at > snapshot_time {
439 errors.push(RestoreError::InvalidTimestamp {
440 snapshot_time,
441 entity_time: obligation.created_at,
442 entity: format!("obligation {} created_at", obligation.id.index),
443 });
444 }
445 if !task_ids.contains(&snapshot_id_key(obligation.holder_task)) {
446 errors.push(RestoreError::OrphanObligation {
447 obligation_id: obligation.id.index,
448 task_id: obligation.holder_task.index,
449 });
450 }
451 if !region_ids.contains(&snapshot_id_key(obligation.owning_region)) {
452 errors.push(RestoreError::OrphanObligationRegion {
453 obligation_id: obligation.id.index,
454 region_id: obligation.owning_region.index,
455 });
456 } else if let Some(holder_region_id) =
457 task_regions.get(&snapshot_id_key(obligation.holder_task))
458 {
459 if *holder_region_id != snapshot_id_key(obligation.owning_region) {
460 errors.push(RestoreError::ObligationRegionMismatch {
461 obligation_id: obligation.id.index,
462 task_id: obligation.holder_task.index,
463 holder_region_id: holder_region_id.0,
464 owning_region_id: obligation.owning_region.index,
465 });
466 }
467 }
468 if is_obligation_resolved(&obligation.state) {
469 stats.resolved_obligation_count += 1;
470 }
471 }
472
473 let mut parent_map: HashMap<SnapshotIdKey, Option<SnapshotIdKey>> = HashMap::new();
475 for region in &self.snapshot.regions {
476 parent_map.insert(
477 snapshot_id_key(region.id),
478 region.parent_id.map(snapshot_id_key),
479 );
480 if let Some(parent_id) = ®ion.parent_id {
481 if !region_ids.contains(&snapshot_id_key(*parent_id)) {
482 errors.push(RestoreError::InvalidParent {
483 region_id: region.id.index,
484 parent_id: parent_id.index,
485 });
486 }
487 }
488 if is_region_closed(®ion.state) {
489 stats.closed_region_count += 1;
490 }
491 }
492
493 if let Some(cycle) = detect_cycle(&parent_map) {
495 errors.push(RestoreError::CyclicRegionTree { cycle });
496 }
497
498 stats.max_depth = compute_max_depth(&parent_map);
500
501 let mut region_tasks: HashMap<SnapshotIdKey, Vec<&TaskSnapshot>> = HashMap::new();
503 for task in &self.snapshot.tasks {
504 region_tasks
505 .entry(snapshot_id_key(task.region_id))
506 .or_default()
507 .push(task);
508 }
509
510 let mut region_children: HashMap<SnapshotIdKey, Vec<SnapshotIdKey>> = HashMap::new();
511 let mut closed_regions: HashSet<SnapshotIdKey> = HashSet::new();
512 for region in &self.snapshot.regions {
513 if is_region_closed(®ion.state) {
514 closed_regions.insert(snapshot_id_key(region.id));
515 }
516 if let Some(parent_id) = region.parent_id {
517 region_children
518 .entry(snapshot_id_key(parent_id))
519 .or_default()
520 .push(snapshot_id_key(region.id));
521 }
522 }
523
524 for region in &self.snapshot.regions {
526 if is_region_closed(®ion.state) {
527 let region_id = snapshot_id_key(region.id);
528 let live_children: Vec<u32> = region_children
529 .get(®ion_id)
530 .map(|children| {
531 children
532 .iter()
533 .filter(|&&child_id| !closed_regions.contains(&child_id))
534 .map(|&(child_index, _)| child_index)
535 .collect()
536 })
537 .unwrap_or_default();
538
539 let live_tasks: Vec<u32> = region_tasks
540 .get(®ion_id)
541 .map(|tasks| {
542 tasks
543 .iter()
544 .filter(|t| !is_task_terminal(&t.state))
545 .map(|t| t.id.index)
546 .collect()
547 })
548 .unwrap_or_default();
549
550 if !live_children.is_empty() || !live_tasks.is_empty() {
551 errors.push(RestoreError::NonQuiescentClosure {
552 region_id: region.id.index,
553 live_children,
554 live_tasks,
555 });
556 }
557 }
558 }
559
560 ValidationResult {
561 is_valid: errors.is_empty(),
562 errors,
563 stats,
564 }
565 }
566
567 #[must_use]
569 pub fn verify_integrity(&self) -> bool {
570 Self::compute_hash(self.schema_version, &self.snapshot) == self.content_hash
571 }
572
573 #[must_use]
575 pub fn timestamp(&self) -> Time {
576 Time::from_nanos(self.snapshot.timestamp)
577 }
578}
579
580fn is_task_terminal(state: &TaskStateSnapshot) -> bool {
582 matches!(state, TaskStateSnapshot::Completed { .. })
583}
584
585fn is_obligation_resolved(state: &ObligationStateSnapshot) -> bool {
587 matches!(
588 state,
589 ObligationStateSnapshot::Committed
590 | ObligationStateSnapshot::Aborted
591 | ObligationStateSnapshot::Leaked
592 )
593}
594
595fn is_region_closed(state: &RegionStateSnapshot) -> bool {
597 matches!(state, RegionStateSnapshot::Closed)
598}
599
600type SnapshotIdKey = (u32, u32);
601
602fn snapshot_id_key(id: IdSnapshot) -> SnapshotIdKey {
603 (id.index, id.generation)
604}
605
606fn detect_cycle(parent_map: &HashMap<SnapshotIdKey, Option<SnapshotIdKey>>) -> Option<Vec<u32>> {
608 for &start in parent_map.keys() {
609 let mut visited = HashSet::new();
610 let mut path = Vec::new();
611 let mut current = Some(start);
612
613 while let Some(node) = current {
614 if visited.contains(&node) {
615 if let Some(pos) = path.iter().position(|&key| key == node) {
617 return Some(path[pos..].iter().map(|(index, _)| *index).collect());
618 }
619 }
620 visited.insert(node);
621 path.push(node);
622 current = parent_map.get(&node).copied().flatten();
623 }
624 }
625 None
626}
627
628fn compute_max_depth(parent_map: &HashMap<SnapshotIdKey, Option<SnapshotIdKey>>) -> usize {
630 let mut max_depth = 0;
631 for &start in parent_map.keys() {
632 let mut depth = 0;
633 let mut current = Some(start);
634 let mut visited = HashSet::new();
635 while let Some(node) = current {
636 if !visited.insert(node) {
637 break;
639 }
640 depth += 1;
641 current = parent_map.get(&node).copied().flatten();
642 }
643 max_depth = max_depth.max(depth);
644 }
645 max_depth
646}
647
648pub trait SnapshotRestore {
650 fn restorable_snapshot(&self) -> RestorableSnapshot;
652}
653
654impl SnapshotRestore for RuntimeState {
655 fn restorable_snapshot(&self) -> RestorableSnapshot {
656 RestorableSnapshot::new(self.snapshot())
657 }
658}
659
660#[cfg(test)]
663mod tests {
664 use super::*;
665 use crate::runtime::state::IdSnapshot;
666 use crate::runtime::state::{
667 BudgetSnapshot, ObligationKindSnapshot, ObligationSnapshot, RegionSnapshot,
668 };
669
670 fn init_test(name: &str) {
671 crate::test_utils::init_test_logging();
672 crate::test_phase!(name);
673 }
674
675 fn snap_id(index: u32, generation: u32) -> IdSnapshot {
676 IdSnapshot { index, generation }
677 }
678
679 fn make_region(id: u32, parent: Option<u32>, state: RegionStateSnapshot) -> RegionSnapshot {
680 RegionSnapshot {
681 id: snap_id(id, 0),
682 parent_id: parent.map(|p| snap_id(p, 0)),
683 state,
684 budget: BudgetSnapshot {
685 deadline: None,
686 poll_quota: 1000,
687 cost_quota: None,
688 priority: 100,
689 },
690 child_count: 0,
691 task_count: 0,
692 name: None,
693 }
694 }
695
696 fn make_task(id: u32, region_id: u32, state: TaskStateSnapshot) -> TaskSnapshot {
697 TaskSnapshot {
698 id: snap_id(id, 0),
699 region_id: snap_id(region_id, 0),
700 state,
701 name: None,
702 poll_count: 0,
703 created_at: 0,
704 obligations: Vec::new(),
705 }
706 }
707
708 fn make_obligation(
709 id: u32,
710 task_id: u32,
711 state: ObligationStateSnapshot,
712 ) -> ObligationSnapshot {
713 make_obligation_in_region(id, task_id, 0, state)
714 }
715
716 fn make_obligation_in_region(
717 id: u32,
718 task_id: u32,
719 owning_region: u32,
720 state: ObligationStateSnapshot,
721 ) -> ObligationSnapshot {
722 ObligationSnapshot {
723 id: snap_id(id, 0),
724 kind: ObligationKindSnapshot::SendPermit,
725 state,
726 holder_task: snap_id(task_id, 0),
727 owning_region: snap_id(owning_region, 0),
728 created_at: 0,
729 }
730 }
731
732 fn make_snapshot(
733 regions: Vec<RegionSnapshot>,
734 tasks: Vec<TaskSnapshot>,
735 obligations: Vec<ObligationSnapshot>,
736 ) -> RestorableSnapshot {
737 RestorableSnapshot::new(RuntimeSnapshot {
738 timestamp: 1000,
739 regions,
740 tasks,
741 obligations,
742 recent_events: Vec::new(),
743 })
744 }
745
746 #[test]
747 fn empty_snapshot_is_valid() {
748 init_test("empty_snapshot_is_valid");
749 let snapshot = make_snapshot(Vec::new(), Vec::new(), Vec::new());
750 let result = snapshot.validate();
751
752 crate::assert_with_log!(result.is_valid, "is_valid", true, result.is_valid);
753 let errors_empty = result.errors.is_empty();
754 crate::assert_with_log!(errors_empty, "errors empty", true, errors_empty);
755 crate::test_complete!("empty_snapshot_is_valid");
756 }
757
758 #[test]
759 fn single_region_is_valid() {
760 init_test("single_region_is_valid");
761 let snapshot = make_snapshot(
762 vec![make_region(0, None, RegionStateSnapshot::Open)],
763 Vec::new(),
764 Vec::new(),
765 );
766 let result = snapshot.validate();
767
768 crate::assert_with_log!(result.is_valid, "is_valid", true, result.is_valid);
769 crate::assert_with_log!(
770 result.stats.region_count == 1,
771 "region_count",
772 1,
773 result.stats.region_count
774 );
775 crate::test_complete!("single_region_is_valid");
776 }
777
778 #[test]
779 fn task_with_valid_region_is_valid() {
780 init_test("task_with_valid_region_is_valid");
781 let snapshot = make_snapshot(
782 vec![make_region(0, None, RegionStateSnapshot::Open)],
783 vec![make_task(0, 0, TaskStateSnapshot::Running)],
784 Vec::new(),
785 );
786 let result = snapshot.validate();
787
788 crate::assert_with_log!(result.is_valid, "is_valid", true, result.is_valid);
789 crate::assert_with_log!(
790 result.stats.task_count == 1,
791 "task_count",
792 1,
793 result.stats.task_count
794 );
795 crate::test_complete!("task_with_valid_region_is_valid");
796 }
797
798 #[test]
799 fn orphan_task_detected() {
800 init_test("orphan_task_detected");
801 let snapshot = make_snapshot(
802 vec![make_region(0, None, RegionStateSnapshot::Open)],
803 vec![make_task(0, 99, TaskStateSnapshot::Running)], Vec::new(),
805 );
806 let result = snapshot.validate();
807
808 let not_valid = !result.is_valid;
809 crate::assert_with_log!(not_valid, "not valid", true, not_valid);
810 let has_error = result
811 .errors
812 .iter()
813 .any(|e| matches!(e, RestoreError::OrphanTask { .. }));
814 crate::assert_with_log!(has_error, "has OrphanTask error", true, has_error);
815 crate::test_complete!("orphan_task_detected");
816 }
817
818 #[test]
819 fn task_with_stale_region_generation_is_orphaned() {
820 init_test("task_with_stale_region_generation_is_orphaned");
821 let mut snapshot = make_snapshot(
822 vec![make_region(7, None, RegionStateSnapshot::Open)],
823 vec![make_task(0, 7, TaskStateSnapshot::Running)],
824 Vec::new(),
825 );
826 snapshot.snapshot.regions[0].id = snap_id(7, 1);
827
828 let result = snapshot.validate();
829
830 let not_valid = !result.is_valid;
831 crate::assert_with_log!(not_valid, "not valid", true, not_valid);
832 let has_error = result.errors.iter().any(|e| {
833 matches!(
834 e,
835 RestoreError::OrphanTask {
836 task_id: 0,
837 region_id: 7,
838 }
839 )
840 });
841 crate::assert_with_log!(
842 has_error,
843 "generation mismatch yields OrphanTask",
844 true,
845 has_error
846 );
847 crate::test_complete!("task_with_stale_region_generation_is_orphaned");
848 }
849
850 #[test]
851 fn orphan_obligation_detected() {
852 init_test("orphan_obligation_detected");
853 let snapshot = make_snapshot(
854 vec![make_region(0, None, RegionStateSnapshot::Open)],
855 vec![make_task(0, 0, TaskStateSnapshot::Running)],
856 vec![make_obligation(0, 99, ObligationStateSnapshot::Reserved)], );
858 let result = snapshot.validate();
859
860 let not_valid = !result.is_valid;
861 crate::assert_with_log!(not_valid, "not valid", true, not_valid);
862 let has_error = result
863 .errors
864 .iter()
865 .any(|e| matches!(e, RestoreError::OrphanObligation { .. }));
866 crate::assert_with_log!(has_error, "has OrphanObligation error", true, has_error);
867 crate::test_complete!("orphan_obligation_detected");
868 }
869
870 #[test]
871 fn obligation_with_stale_holder_generation_is_orphaned() {
872 init_test("obligation_with_stale_holder_generation_is_orphaned");
873 let mut snapshot = make_snapshot(
874 vec![make_region(0, None, RegionStateSnapshot::Open)],
875 vec![make_task(5, 0, TaskStateSnapshot::Running)],
876 vec![make_obligation(0, 5, ObligationStateSnapshot::Reserved)],
877 );
878 snapshot.snapshot.tasks[0].id = snap_id(5, 1);
879
880 let result = snapshot.validate();
881
882 let not_valid = !result.is_valid;
883 crate::assert_with_log!(not_valid, "not valid", true, not_valid);
884 let has_error = result.errors.iter().any(|e| {
885 matches!(
886 e,
887 RestoreError::OrphanObligation {
888 obligation_id: 0,
889 task_id: 5,
890 }
891 )
892 });
893 crate::assert_with_log!(
894 has_error,
895 "generation mismatch yields OrphanObligation",
896 true,
897 has_error
898 );
899 crate::test_complete!("obligation_with_stale_holder_generation_is_orphaned");
900 }
901
902 #[test]
903 fn orphan_obligation_region_detected() {
904 init_test("orphan_obligation_region_detected");
905 let snapshot = make_snapshot(
906 vec![make_region(0, None, RegionStateSnapshot::Open)],
907 vec![make_task(0, 0, TaskStateSnapshot::Running)],
908 vec![make_obligation_in_region(
909 0,
910 0,
911 99,
912 ObligationStateSnapshot::Reserved,
913 )],
914 );
915 let result = snapshot.validate();
916
917 let not_valid = !result.is_valid;
918 crate::assert_with_log!(not_valid, "not valid", true, not_valid);
919 let has_error = result
920 .errors
921 .iter()
922 .any(|e| matches!(e, RestoreError::OrphanObligationRegion { .. }));
923 crate::assert_with_log!(
924 has_error,
925 "has OrphanObligationRegion error",
926 true,
927 has_error
928 );
929 crate::test_complete!("orphan_obligation_region_detected");
930 }
931
932 #[test]
933 fn obligation_with_stale_owning_region_generation_is_orphaned() {
934 init_test("obligation_with_stale_owning_region_generation_is_orphaned");
935 let mut snapshot = make_snapshot(
936 vec![make_region(3, None, RegionStateSnapshot::Open)],
937 vec![make_task(0, 3, TaskStateSnapshot::Running)],
938 vec![make_obligation_in_region(
939 0,
940 0,
941 3,
942 ObligationStateSnapshot::Reserved,
943 )],
944 );
945 snapshot.snapshot.regions[0].id = snap_id(3, 1);
946 snapshot.snapshot.tasks[0].region_id = snap_id(3, 1);
947
948 let result = snapshot.validate();
949
950 let not_valid = !result.is_valid;
951 crate::assert_with_log!(not_valid, "not valid", true, not_valid);
952 let has_error = result.errors.iter().any(|e| {
953 matches!(
954 e,
955 RestoreError::OrphanObligationRegion {
956 obligation_id: 0,
957 region_id: 3,
958 }
959 )
960 });
961 crate::assert_with_log!(
962 has_error,
963 "generation mismatch yields OrphanObligationRegion",
964 true,
965 has_error
966 );
967 crate::test_complete!("obligation_with_stale_owning_region_generation_is_orphaned");
968 }
969
970 #[test]
971 fn obligation_region_mismatch_detected() {
972 init_test("obligation_region_mismatch_detected");
973 let snapshot = make_snapshot(
974 vec![
975 make_region(0, None, RegionStateSnapshot::Open),
976 make_region(1, None, RegionStateSnapshot::Open),
977 ],
978 vec![make_task(0, 0, TaskStateSnapshot::Running)],
979 vec![make_obligation_in_region(
980 0,
981 0,
982 1,
983 ObligationStateSnapshot::Reserved,
984 )],
985 );
986 let result = snapshot.validate();
987
988 let not_valid = !result.is_valid;
989 crate::assert_with_log!(not_valid, "not valid", true, not_valid);
990 let has_error = result
991 .errors
992 .iter()
993 .any(|e| matches!(e, RestoreError::ObligationRegionMismatch { .. }));
994 crate::assert_with_log!(
995 has_error,
996 "has ObligationRegionMismatch error",
997 true,
998 has_error
999 );
1000 crate::test_complete!("obligation_region_mismatch_detected");
1001 }
1002
1003 #[test]
1004 fn invalid_parent_detected() {
1005 init_test("invalid_parent_detected");
1006 let snapshot = make_snapshot(
1007 vec![
1008 make_region(0, None, RegionStateSnapshot::Open),
1009 make_region(1, Some(99), RegionStateSnapshot::Open), ],
1011 Vec::new(),
1012 Vec::new(),
1013 );
1014 let result = snapshot.validate();
1015
1016 let not_valid = !result.is_valid;
1017 crate::assert_with_log!(not_valid, "not valid", true, not_valid);
1018 let has_error = result
1019 .errors
1020 .iter()
1021 .any(|e| matches!(e, RestoreError::InvalidParent { .. }));
1022 crate::assert_with_log!(has_error, "has InvalidParent error", true, has_error);
1023 crate::test_complete!("invalid_parent_detected");
1024 }
1025
1026 #[test]
1027 fn parent_generation_mismatch_detected() {
1028 init_test("parent_generation_mismatch_detected");
1029 let mut snapshot = make_snapshot(
1030 vec![
1031 make_region(0, None, RegionStateSnapshot::Open),
1032 make_region(1, Some(0), RegionStateSnapshot::Open),
1033 ],
1034 Vec::new(),
1035 Vec::new(),
1036 );
1037 snapshot.snapshot.regions[0].id = snap_id(0, 1);
1038
1039 let result = snapshot.validate();
1040
1041 let not_valid = !result.is_valid;
1042 crate::assert_with_log!(not_valid, "not valid", true, not_valid);
1043 let has_error = result.errors.iter().any(|e| {
1044 matches!(
1045 e,
1046 RestoreError::InvalidParent {
1047 region_id: 1,
1048 parent_id: 0,
1049 }
1050 )
1051 });
1052 crate::assert_with_log!(
1053 has_error,
1054 "generation mismatch yields InvalidParent",
1055 true,
1056 has_error
1057 );
1058 crate::test_complete!("parent_generation_mismatch_detected");
1059 }
1060
1061 #[test]
1062 fn closed_region_with_live_task_detected() {
1063 init_test("closed_region_with_live_task_detected");
1064 let snapshot = make_snapshot(
1065 vec![make_region(0, None, RegionStateSnapshot::Closed)],
1066 vec![make_task(0, 0, TaskStateSnapshot::Running)], Vec::new(),
1068 );
1069 let result = snapshot.validate();
1070
1071 let not_valid = !result.is_valid;
1072 crate::assert_with_log!(not_valid, "not valid", true, not_valid);
1073 let has_error = result
1074 .errors
1075 .iter()
1076 .any(|e| matches!(e, RestoreError::NonQuiescentClosure { .. }));
1077 crate::assert_with_log!(has_error, "has NonQuiescentClosure error", true, has_error);
1078 crate::test_complete!("closed_region_with_live_task_detected");
1079 }
1080
1081 #[test]
1082 fn nested_regions_valid() {
1083 init_test("nested_regions_valid");
1084 let snapshot = make_snapshot(
1085 vec![
1086 make_region(0, None, RegionStateSnapshot::Open),
1087 make_region(1, Some(0), RegionStateSnapshot::Open),
1088 make_region(2, Some(1), RegionStateSnapshot::Open),
1089 ],
1090 Vec::new(),
1091 Vec::new(),
1092 );
1093 let result = snapshot.validate();
1094
1095 crate::assert_with_log!(result.is_valid, "is_valid", true, result.is_valid);
1096 crate::assert_with_log!(
1097 result.stats.max_depth == 3,
1098 "max_depth",
1099 3,
1100 result.stats.max_depth
1101 );
1102 crate::test_complete!("nested_regions_valid");
1103 }
1104
1105 #[test]
1106 fn terminal_task_stats_computed() {
1107 init_test("terminal_task_stats_computed");
1108 let snapshot = make_snapshot(
1109 vec![make_region(0, None, RegionStateSnapshot::Open)],
1110 vec![
1111 make_task(0, 0, TaskStateSnapshot::Running),
1112 make_task(
1113 1,
1114 0,
1115 TaskStateSnapshot::Completed {
1116 outcome: crate::runtime::state::OutcomeSnapshot::Ok,
1117 },
1118 ),
1119 ],
1120 Vec::new(),
1121 );
1122 let result = snapshot.validate();
1123
1124 crate::assert_with_log!(result.is_valid, "is_valid", true, result.is_valid);
1125 crate::assert_with_log!(
1126 result.stats.terminal_task_count == 1,
1127 "terminal_task_count",
1128 1,
1129 result.stats.terminal_task_count
1130 );
1131 crate::test_complete!("terminal_task_stats_computed");
1132 }
1133
1134 #[test]
1135 fn content_hash_deterministic() {
1136 init_test("content_hash_deterministic");
1137 let snapshot1 = make_snapshot(
1138 vec![make_region(0, None, RegionStateSnapshot::Open)],
1139 vec![make_task(0, 0, TaskStateSnapshot::Running)],
1140 Vec::new(),
1141 );
1142 let snapshot2 = make_snapshot(
1143 vec![make_region(0, None, RegionStateSnapshot::Open)],
1144 vec![make_task(0, 0, TaskStateSnapshot::Running)],
1145 Vec::new(),
1146 );
1147
1148 crate::assert_with_log!(
1149 snapshot1.content_hash == snapshot2.content_hash,
1150 "hashes equal",
1151 snapshot1.content_hash,
1152 snapshot2.content_hash
1153 );
1154 crate::test_complete!("content_hash_deterministic");
1155 }
1156
1157 #[test]
1158 fn integrity_verification_works() {
1159 init_test("integrity_verification_works");
1160 let snapshot = make_snapshot(
1161 vec![make_region(0, None, RegionStateSnapshot::Open)],
1162 Vec::new(),
1163 Vec::new(),
1164 );
1165
1166 let valid = snapshot.verify_integrity();
1167 crate::assert_with_log!(valid, "integrity valid", true, valid);
1168
1169 let mut tampered = snapshot;
1171 tampered.content_hash ^= 1;
1172 let invalid = !tampered.verify_integrity();
1173 crate::assert_with_log!(invalid, "tampered invalid", true, invalid);
1174
1175 crate::test_complete!("integrity_verification_works");
1176 }
1177
1178 #[test]
1179 fn integrity_verification_detects_semantic_tampering() {
1180 init_test("integrity_verification_detects_semantic_tampering");
1181 let snapshot = make_snapshot(
1182 vec![make_region(0, None, RegionStateSnapshot::Open)],
1183 vec![make_task(0, 0, TaskStateSnapshot::Running)],
1184 vec![make_obligation(0, 0, ObligationStateSnapshot::Reserved)],
1185 );
1186
1187 let mut tampered = snapshot;
1188 tampered.snapshot.tasks[0].state = TaskStateSnapshot::Completed {
1189 outcome: crate::runtime::state::OutcomeSnapshot::Ok,
1190 };
1191
1192 let invalid = !tampered.verify_integrity();
1193 crate::assert_with_log!(invalid, "semantic tamper invalid", true, invalid);
1194
1195 crate::test_complete!("integrity_verification_detects_semantic_tampering");
1196 }
1197
1198 #[test]
1199 fn integrity_verification_detects_schema_version_tampering() {
1200 init_test("integrity_verification_detects_schema_version_tampering");
1201 let snapshot = make_snapshot(
1202 vec![make_region(0, None, RegionStateSnapshot::Open)],
1203 vec![make_task(0, 0, TaskStateSnapshot::Running)],
1204 Vec::new(),
1205 );
1206
1207 let mut tampered = snapshot;
1208 tampered.schema_version = tampered.schema_version.saturating_add(1);
1209
1210 let invalid = !tampered.verify_integrity();
1211 crate::assert_with_log!(invalid, "schema version tamper invalid", true, invalid);
1212
1213 crate::test_complete!("integrity_verification_detects_schema_version_tampering");
1214 }
1215
1216 #[test]
1217 fn duplicate_region_id_detected() {
1218 init_test("duplicate_region_id_detected");
1219 let snapshot = make_snapshot(
1220 vec![
1221 make_region(0, None, RegionStateSnapshot::Open),
1222 make_region(0, None, RegionStateSnapshot::Open), ],
1224 Vec::new(),
1225 Vec::new(),
1226 );
1227 let result = snapshot.validate();
1228
1229 let not_valid = !result.is_valid;
1230 crate::assert_with_log!(not_valid, "not valid", true, not_valid);
1231 let has_error = result
1232 .errors
1233 .iter()
1234 .any(|e| matches!(e, RestoreError::DuplicateId { kind: "region", .. }));
1235 crate::assert_with_log!(has_error, "has DuplicateId error", true, has_error);
1236 crate::test_complete!("duplicate_region_id_detected");
1237 }
1238
1239 #[test]
1240 fn duplicate_obligation_id_detected() {
1241 init_test("duplicate_obligation_id_detected");
1242 let snapshot = make_snapshot(
1243 vec![make_region(0, None, RegionStateSnapshot::Open)],
1244 vec![make_task(0, 0, TaskStateSnapshot::Running)],
1245 vec![
1246 make_obligation(7, 0, ObligationStateSnapshot::Reserved),
1247 make_obligation(7, 0, ObligationStateSnapshot::Committed), ],
1249 );
1250 let result = snapshot.validate();
1251
1252 let not_valid = !result.is_valid;
1253 crate::assert_with_log!(not_valid, "not valid", true, not_valid);
1254 let has_error = result.errors.iter().any(|e| {
1255 matches!(
1256 e,
1257 RestoreError::DuplicateId {
1258 kind: "obligation",
1259 ..
1260 }
1261 )
1262 });
1263 crate::assert_with_log!(
1264 has_error,
1265 "has obligation DuplicateId error",
1266 true,
1267 has_error
1268 );
1269 crate::test_complete!("duplicate_obligation_id_detected");
1270 }
1271
1272 #[test]
1273 fn cyclic_region_tree_detected_without_depth_hang() {
1274 init_test("cyclic_region_tree_detected_without_depth_hang");
1275 let snapshot = make_snapshot(
1276 vec![
1277 make_region(0, Some(1), RegionStateSnapshot::Open),
1278 make_region(1, Some(0), RegionStateSnapshot::Open),
1279 ],
1280 Vec::new(),
1281 Vec::new(),
1282 );
1283 let result = snapshot.validate();
1284
1285 let not_valid = !result.is_valid;
1286 crate::assert_with_log!(not_valid, "not valid", true, not_valid);
1287 let has_cycle = result
1288 .errors
1289 .iter()
1290 .any(|e| matches!(e, RestoreError::CyclicRegionTree { .. }));
1291 crate::assert_with_log!(has_cycle, "has CyclicRegionTree error", true, has_cycle);
1292 crate::assert_with_log!(
1293 result.stats.max_depth == 2,
1294 "max_depth bounded with cycle",
1295 2,
1296 result.stats.max_depth
1297 );
1298 crate::test_complete!("cyclic_region_tree_detected_without_depth_hang");
1299 }
1300
1301 #[test]
1302 fn resolved_obligation_stats_computed() {
1303 init_test("resolved_obligation_stats_computed");
1304 let snapshot = make_snapshot(
1305 vec![make_region(0, None, RegionStateSnapshot::Open)],
1306 vec![make_task(0, 0, TaskStateSnapshot::Running)],
1307 vec![
1308 make_obligation(0, 0, ObligationStateSnapshot::Reserved),
1309 make_obligation(1, 0, ObligationStateSnapshot::Committed),
1310 make_obligation(2, 0, ObligationStateSnapshot::Aborted),
1311 ],
1312 );
1313 let result = snapshot.validate();
1314
1315 crate::assert_with_log!(result.is_valid, "is_valid", true, result.is_valid);
1316 crate::assert_with_log!(
1317 result.stats.resolved_obligation_count == 2,
1318 "resolved_obligation_count",
1319 2,
1320 result.stats.resolved_obligation_count
1321 );
1322 crate::test_complete!("resolved_obligation_stats_computed");
1323 }
1324
1325 #[test]
1326 fn task_timestamp_after_snapshot_detected() {
1327 init_test("task_timestamp_after_snapshot_detected");
1328 let mut snapshot = make_snapshot(
1329 vec![make_region(0, None, RegionStateSnapshot::Open)],
1330 vec![make_task(0, 0, TaskStateSnapshot::Running)],
1331 Vec::new(),
1332 );
1333 snapshot.snapshot.tasks[0].created_at = snapshot.snapshot.timestamp + 1;
1334
1335 let result = snapshot.validate();
1336 let has_error = result.errors.iter().any(|e| {
1337 matches!(
1338 e,
1339 RestoreError::InvalidTimestamp {
1340 entity, ..
1341 } if entity.contains("task 0 created_at")
1342 )
1343 });
1344 crate::assert_with_log!(
1345 has_error,
1346 "task invalid timestamp detected",
1347 true,
1348 has_error
1349 );
1350 crate::test_complete!("task_timestamp_after_snapshot_detected");
1351 }
1352
1353 #[test]
1354 fn obligation_timestamp_after_snapshot_detected() {
1355 init_test("obligation_timestamp_after_snapshot_detected");
1356 let mut snapshot = make_snapshot(
1357 vec![make_region(0, None, RegionStateSnapshot::Open)],
1358 vec![make_task(0, 0, TaskStateSnapshot::Running)],
1359 vec![make_obligation(0, 0, ObligationStateSnapshot::Reserved)],
1360 );
1361 snapshot.snapshot.obligations[0].created_at = snapshot.snapshot.timestamp + 1;
1362
1363 let result = snapshot.validate();
1364 let has_error = result.errors.iter().any(|e| {
1365 matches!(
1366 e,
1367 RestoreError::InvalidTimestamp {
1368 entity, ..
1369 } if entity.contains("obligation 0 created_at")
1370 )
1371 });
1372 crate::assert_with_log!(
1373 has_error,
1374 "obligation invalid timestamp detected",
1375 true,
1376 has_error
1377 );
1378 crate::test_complete!("obligation_timestamp_after_snapshot_detected");
1379 }
1380
1381 #[test]
1384 fn restore_error_debug_clone_eq() {
1385 let e1 = RestoreError::OrphanTask {
1386 task_id: 5,
1387 region_id: 99,
1388 };
1389 let e2 = e1.clone();
1390 assert_eq!(e1, e2);
1391 let dbg = format!("{e1:?}");
1392 assert!(dbg.contains("OrphanTask"));
1393
1394 let e3 = RestoreError::CyclicRegionTree {
1395 cycle: vec![1, 2, 3],
1396 };
1397 let e4 = e3.clone();
1398 assert_eq!(e3, e4);
1399 assert_ne!(e1, e3);
1400 }
1401
1402 #[test]
1403 fn snapshot_stats_debug_clone_default() {
1404 let s = SnapshotStats::default();
1405 assert_eq!(s.region_count, 0);
1406 assert_eq!(s.task_count, 0);
1407 assert_eq!(s.obligation_count, 0);
1408 assert_eq!(s.max_depth, 0);
1409 assert_eq!(s.terminal_task_count, 0);
1410 assert_eq!(s.resolved_obligation_count, 0);
1411 assert_eq!(s.closed_region_count, 0);
1412
1413 let s2 = s;
1414 let dbg = format!("{s2:?}");
1415 assert!(dbg.contains("SnapshotStats"));
1416 }
1417
1418 #[test]
1419 fn validation_result_debug_clone() {
1420 let vr = ValidationResult {
1421 is_valid: true,
1422 errors: vec![],
1423 stats: SnapshotStats::default(),
1424 };
1425 let vr2 = vr;
1426 assert!(vr2.is_valid);
1427 assert!(vr2.errors.is_empty());
1428 let dbg = format!("{vr2:?}");
1429 assert!(dbg.contains("ValidationResult"));
1430 }
1431}