1pub(crate) mod claim;
2mod error;
3mod integrity;
4mod record;
5
6use crate::{
7 declaration::AllocationDeclaration,
8 physical::{CommitRecoveryError, DualCommitStore},
9 session::ValidatedAllocations,
10};
11use claim::{ClaimConflict, ClaimOutcome, validate_declaration_claim, validate_reservation_claim};
12use serde::{Deserialize, Serialize};
13
14pub use error::{
15 AllocationReservationError, AllocationRetirementError, AllocationStageError, LedgerCommitError,
16 LedgerCompatibilityError, LedgerIntegrityError,
17};
18pub use record::{
19 AllocationHistory, AllocationLedger, AllocationRecord, AllocationRetirement, AllocationState,
20 CURRENT_LEDGER_SCHEMA_VERSION, CURRENT_PHYSICAL_FORMAT_ID, GenerationRecord,
21 LedgerCompatibility, SchemaMetadataRecord,
22};
23
24pub trait LedgerCodec {
34 type Error;
36
37 fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error>;
39
40 fn decode(&self, bytes: &[u8]) -> Result<AllocationLedger, Self::Error>;
42}
43
44#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
59pub struct LedgerCommitStore {
60 physical: DualCommitStore,
62}
63
64impl LedgerCommitStore {
65 #[must_use]
67 pub const fn physical(&self) -> &DualCommitStore {
68 &self.physical
69 }
70
71 #[cfg(test)]
72 pub(crate) const fn physical_mut(&mut self) -> &mut DualCommitStore {
73 &mut self.physical
74 }
75
76 pub fn recover<C: LedgerCodec>(
78 &self,
79 codec: &C,
80 ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
81 self.recover_with_compatibility(codec, LedgerCompatibility::current())
82 }
83
84 pub fn recover_with_compatibility<C: LedgerCodec>(
86 &self,
87 codec: &C,
88 compatibility: LedgerCompatibility,
89 ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
90 let committed = self
91 .physical
92 .authoritative()
93 .map_err(LedgerCommitError::Recovery)?;
94 let ledger = codec
95 .decode(committed.payload())
96 .map_err(LedgerCommitError::Codec)?;
97 if committed.generation() != ledger.current_generation {
98 return Err(LedgerCommitError::PhysicalLogicalGenerationMismatch {
99 physical_generation: committed.generation(),
100 logical_generation: ledger.current_generation,
101 });
102 }
103 compatibility
104 .validate(&ledger)
105 .map_err(LedgerCommitError::Compatibility)?;
106 ledger
107 .validate_committed_integrity()
108 .map_err(LedgerCommitError::Integrity)?;
109 Ok(ledger)
110 }
111
112 pub fn recover_or_initialize<C: LedgerCodec>(
118 &mut self,
119 codec: &C,
120 genesis: &AllocationLedger,
121 ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
122 self.recover_or_initialize_with_compatibility(
123 codec,
124 genesis,
125 LedgerCompatibility::current(),
126 )
127 }
128
129 pub fn recover_or_initialize_with_compatibility<C: LedgerCodec>(
131 &mut self,
132 codec: &C,
133 genesis: &AllocationLedger,
134 compatibility: LedgerCompatibility,
135 ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
136 match self.recover_with_compatibility(codec, compatibility) {
137 Ok(ledger) => Ok(ledger),
138 Err(LedgerCommitError::Recovery(CommitRecoveryError::NoValidGeneration))
139 if self.physical.is_uninitialized() =>
140 {
141 self.commit_with_compatibility(genesis, codec, compatibility)
142 }
143 Err(err) => Err(err),
144 }
145 }
146
147 pub fn commit<C: LedgerCodec>(
149 &mut self,
150 ledger: &AllocationLedger,
151 codec: &C,
152 ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
153 self.commit_with_compatibility(ledger, codec, LedgerCompatibility::current())
154 }
155
156 pub fn commit_with_compatibility<C: LedgerCodec>(
158 &mut self,
159 ledger: &AllocationLedger,
160 codec: &C,
161 compatibility: LedgerCompatibility,
162 ) -> Result<AllocationLedger, LedgerCommitError<C::Error>> {
163 compatibility
164 .validate(ledger)
165 .map_err(LedgerCommitError::Compatibility)?;
166 ledger
167 .validate_committed_integrity()
168 .map_err(LedgerCommitError::Integrity)?;
169 let payload = codec.encode(ledger).map_err(LedgerCommitError::Codec)?;
170 self.physical
171 .commit_payload_at_generation(ledger.current_generation, payload)
172 .map_err(LedgerCommitError::Recovery)?;
173 self.recover_with_compatibility(codec, compatibility)
174 }
175
176 #[cfg(test)]
178 pub fn write_corrupt_inactive_ledger<C: LedgerCodec>(
179 &mut self,
180 ledger: &AllocationLedger,
181 codec: &C,
182 ) -> Result<(), LedgerCommitError<C::Error>> {
183 let payload = codec.encode(ledger).map_err(LedgerCommitError::Codec)?;
184 self.physical
185 .write_corrupt_inactive_slot(ledger.current_generation, payload);
186 Ok(())
187 }
188}
189
190impl AllocationLedger {
191 pub fn stage_validated_generation(
197 &self,
198 validated: &ValidatedAllocations,
199 committed_at: Option<u64>,
200 ) -> Result<Self, AllocationStageError> {
201 if validated.generation() != self.current_generation {
202 return Err(AllocationStageError::StaleValidatedAllocations {
203 validated_generation: validated.generation(),
204 ledger_generation: self.current_generation,
205 });
206 }
207 let next_generation = checked_next_generation(self.current_generation)
208 .map_err(|generation| AllocationStageError::GenerationOverflow { generation })?;
209 let mut next = self.clone();
210 next.current_generation = next_generation;
211 let declaration_count = u32::try_from(validated.declarations().len()).unwrap_or(u32::MAX);
212
213 for declaration in validated.declarations() {
214 declaration.schema.validate().map_err(|error| {
215 AllocationStageError::InvalidSchemaMetadata {
216 stable_key: declaration.stable_key.clone(),
217 error,
218 }
219 })?;
220 record_declaration(&mut next, next_generation, declaration)?;
221 }
222
223 next.allocation_history.push_generation(GenerationRecord {
224 generation: next_generation,
225 parent_generation: Some(self.current_generation),
226 runtime_fingerprint: validated.runtime_fingerprint().map(str::to_string),
227 declaration_count,
228 committed_at,
229 });
230
231 Ok(next)
232 }
233
234 pub fn stage_reservation_generation(
239 &self,
240 reservations: &[AllocationDeclaration],
241 committed_at: Option<u64>,
242 ) -> Result<Self, AllocationReservationError> {
243 let next_generation = checked_next_generation(self.current_generation)
244 .map_err(|generation| AllocationReservationError::GenerationOverflow { generation })?;
245 let mut next = self.clone();
246 next.current_generation = next_generation;
247
248 for reservation in reservations {
249 reservation.schema.validate().map_err(|error| {
250 AllocationReservationError::InvalidSchemaMetadata {
251 stable_key: reservation.stable_key.clone(),
252 error,
253 }
254 })?;
255 record_reservation(&mut next, next_generation, reservation)?;
256 }
257
258 next.allocation_history.push_generation(GenerationRecord {
259 generation: next_generation,
260 parent_generation: Some(self.current_generation),
261 runtime_fingerprint: None,
262 declaration_count: u32::try_from(reservations.len()).unwrap_or(u32::MAX),
263 committed_at,
264 });
265
266 Ok(next)
267 }
268
269 pub fn stage_retirement_generation(
271 &self,
272 retirement: &AllocationRetirement,
273 committed_at: Option<u64>,
274 ) -> Result<Self, AllocationRetirementError> {
275 let next_generation = checked_next_generation(self.current_generation)
276 .map_err(|generation| AllocationRetirementError::GenerationOverflow { generation })?;
277 let mut next = self.clone();
278 let record = next
279 .allocation_history
280 .records_mut()
281 .iter_mut()
282 .find(|record| record.stable_key == retirement.stable_key)
283 .ok_or_else(|| {
284 AllocationRetirementError::UnknownStableKey(retirement.stable_key.clone())
285 })?;
286
287 if record.slot != retirement.slot {
288 return Err(AllocationRetirementError::SlotMismatch {
289 stable_key: retirement.stable_key.clone(),
290 historical_slot: Box::new(record.slot.clone()),
291 retired_slot: Box::new(retirement.slot.clone()),
292 });
293 }
294 if record.state == AllocationState::Retired {
295 return Err(AllocationRetirementError::AlreadyRetired {
296 stable_key: retirement.stable_key.clone(),
297 slot: Box::new(record.slot.clone()),
298 });
299 }
300
301 record.state = AllocationState::Retired;
302 record.retired_generation = Some(next_generation);
303 next.current_generation = next_generation;
304 next.allocation_history.push_generation(GenerationRecord {
305 generation: next_generation,
306 parent_generation: Some(self.current_generation),
307 runtime_fingerprint: None,
308 declaration_count: 0,
309 committed_at,
310 });
311
312 Ok(next)
313 }
314}
315
316fn record_declaration(
317 ledger: &mut AllocationLedger,
318 generation: u64,
319 declaration: &AllocationDeclaration,
320) -> Result<(), AllocationStageError> {
321 match validate_declaration_claim(ledger, declaration) {
322 Ok(ClaimOutcome::Existing { record_index }) => {
323 ledger.allocation_history.records_mut()[record_index]
324 .observe_declaration(generation, declaration);
325 Ok(())
326 }
327 Ok(ClaimOutcome::New) => {
328 ledger
329 .allocation_history
330 .push_record(AllocationRecord::from_declaration(
331 generation,
332 declaration.clone(),
333 AllocationState::Active,
334 ));
335 Ok(())
336 }
337 Err(conflict) => Err(map_declaration_stage_conflict(
338 ledger,
339 declaration,
340 conflict,
341 )),
342 }
343}
344
345const fn checked_next_generation(current_generation: u64) -> Result<u64, u64> {
346 match current_generation.checked_add(1) {
347 Some(next_generation) => Ok(next_generation),
348 None => Err(current_generation),
349 }
350}
351
352fn record_reservation(
353 ledger: &mut AllocationLedger,
354 generation: u64,
355 reservation: &AllocationDeclaration,
356) -> Result<(), AllocationReservationError> {
357 match validate_reservation_claim(ledger, reservation) {
358 Ok(ClaimOutcome::Existing { record_index }) => {
359 ledger.allocation_history.records_mut()[record_index]
360 .observe_reservation(generation, reservation);
361 Ok(())
362 }
363 Ok(ClaimOutcome::New) => {
364 ledger
365 .allocation_history
366 .push_record(AllocationRecord::reserved(generation, reservation.clone()));
367 Ok(())
368 }
369 Err(conflict) => Err(map_reservation_stage_conflict(
370 ledger,
371 reservation,
372 conflict,
373 )),
374 }
375}
376
377fn map_declaration_stage_conflict(
378 ledger: &AllocationLedger,
379 declaration: &AllocationDeclaration,
380 conflict: ClaimConflict,
381) -> AllocationStageError {
382 match conflict {
383 ClaimConflict::StableKeyMoved { record_index } => {
384 let record = &ledger.allocation_history.records()[record_index];
385 AllocationStageError::StableKeySlotConflict {
386 stable_key: declaration.stable_key.clone(),
387 historical_slot: Box::new(record.slot.clone()),
388 declared_slot: Box::new(declaration.slot.clone()),
389 }
390 }
391 ClaimConflict::SlotReused { record_index } => {
392 let record = &ledger.allocation_history.records()[record_index];
393 AllocationStageError::SlotStableKeyConflict {
394 slot: Box::new(declaration.slot.clone()),
395 historical_key: record.stable_key.clone(),
396 declared_key: declaration.stable_key.clone(),
397 }
398 }
399 ClaimConflict::Tombstoned { record_index } => {
400 let record = &ledger.allocation_history.records()[record_index];
401 AllocationStageError::RetiredAllocation {
402 stable_key: declaration.stable_key.clone(),
403 slot: Box::new(record.slot.clone()),
404 }
405 }
406 ClaimConflict::ActiveAllocation { .. } => {
407 unreachable!("active allocation conflicts are reservation-only")
408 }
409 }
410}
411
412fn map_reservation_stage_conflict(
413 ledger: &AllocationLedger,
414 reservation: &AllocationDeclaration,
415 conflict: ClaimConflict,
416) -> AllocationReservationError {
417 match conflict {
418 ClaimConflict::StableKeyMoved { record_index } => {
419 let record = &ledger.allocation_history.records()[record_index];
420 AllocationReservationError::StableKeySlotConflict {
421 stable_key: reservation.stable_key.clone(),
422 historical_slot: Box::new(record.slot.clone()),
423 reserved_slot: Box::new(reservation.slot.clone()),
424 }
425 }
426 ClaimConflict::SlotReused { record_index } => {
427 let record = &ledger.allocation_history.records()[record_index];
428 AllocationReservationError::SlotStableKeyConflict {
429 slot: Box::new(reservation.slot.clone()),
430 historical_key: record.stable_key.clone(),
431 reserved_key: reservation.stable_key.clone(),
432 }
433 }
434 ClaimConflict::Tombstoned { record_index } => {
435 let record = &ledger.allocation_history.records()[record_index];
436 AllocationReservationError::RetiredAllocation {
437 stable_key: reservation.stable_key.clone(),
438 slot: Box::new(record.slot.clone()),
439 }
440 }
441 ClaimConflict::ActiveAllocation { record_index } => {
442 let record = &ledger.allocation_history.records()[record_index];
443 AllocationReservationError::ActiveAllocation {
444 stable_key: reservation.stable_key.clone(),
445 slot: Box::new(record.slot.clone()),
446 }
447 }
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454 use crate::{
455 declaration::{DeclarationSnapshot, DeclarationSnapshotError},
456 key::StableKey,
457 physical::CommittedGenerationBytes,
458 schema::{SchemaMetadata, SchemaMetadataError},
459 slot::AllocationSlotDescriptor,
460 };
461 use std::cell::RefCell;
462
463 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
464 struct TestCodec;
465
466 impl LedgerCodec for TestCodec {
467 type Error = &'static str;
468
469 fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error> {
470 let mut bytes = Vec::with_capacity(16);
471 bytes.extend_from_slice(&ledger.ledger_schema_version.to_le_bytes());
472 bytes.extend_from_slice(&ledger.physical_format_id.to_le_bytes());
473 bytes.extend_from_slice(&ledger.current_generation.to_le_bytes());
474 Ok(bytes)
475 }
476
477 fn decode(&self, bytes: &[u8]) -> Result<AllocationLedger, Self::Error> {
478 let bytes = <[u8; 16]>::try_from(bytes).map_err(|_| "invalid ledger")?;
479 let ledger_schema_version =
480 u32::from_le_bytes(bytes[0..4].try_into().map_err(|_| "invalid schema")?);
481 let physical_format_id =
482 u32::from_le_bytes(bytes[4..8].try_into().map_err(|_| "invalid format")?);
483 let current_generation =
484 u64::from_le_bytes(bytes[8..16].try_into().map_err(|_| "invalid generation")?);
485 let mut ledger = committed_ledger(current_generation);
486 ledger.ledger_schema_version = ledger_schema_version;
487 ledger.physical_format_id = physical_format_id;
488 Ok(ledger)
489 }
490 }
491
492 #[derive(Debug, Default)]
493 struct FullLedgerCodec {
494 ledgers: RefCell<Vec<AllocationLedger>>,
495 }
496
497 impl LedgerCodec for FullLedgerCodec {
498 type Error = &'static str;
499
500 fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error> {
501 let mut ledgers = self.ledgers.borrow_mut();
502 let index = u64::try_from(ledgers.len()).map_err(|_| "too many ledgers")?;
503 ledgers.push(ledger.clone());
504 Ok(index.to_le_bytes().to_vec())
505 }
506
507 fn decode(&self, bytes: &[u8]) -> Result<AllocationLedger, Self::Error> {
508 let bytes = <[u8; 8]>::try_from(bytes).map_err(|_| "invalid ledger index")?;
509 let index =
510 usize::try_from(u64::from_le_bytes(bytes)).map_err(|_| "invalid ledger index")?;
511 self.ledgers
512 .borrow()
513 .get(index)
514 .cloned()
515 .ok_or("unknown ledger index")
516 }
517 }
518
519 fn declaration(key: &str, id: u8, schema_version: Option<u32>) -> AllocationDeclaration {
520 AllocationDeclaration::new(
521 key,
522 AllocationSlotDescriptor::memory_manager(id).expect("usable slot"),
523 None,
524 SchemaMetadata {
525 schema_version,
526 schema_fingerprint: None,
527 },
528 )
529 .expect("declaration")
530 }
531
532 fn invalid_schema_metadata() -> SchemaMetadata {
533 SchemaMetadata {
534 schema_version: Some(0),
535 schema_fingerprint: None,
536 }
537 }
538
539 fn declaration_with_invalid_schema(key: &str, id: u8) -> AllocationDeclaration {
540 let mut declaration = declaration(key, id, Some(1));
541 declaration.schema = invalid_schema_metadata();
542 declaration
543 }
544
545 fn ledger() -> AllocationLedger {
546 AllocationLedger {
547 ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION,
548 physical_format_id: CURRENT_PHYSICAL_FORMAT_ID,
549 current_generation: 3,
550 allocation_history: AllocationHistory::default(),
551 }
552 }
553
554 fn committed_ledger(current_generation: u64) -> AllocationLedger {
555 AllocationLedger {
556 ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION,
557 physical_format_id: CURRENT_PHYSICAL_FORMAT_ID,
558 current_generation,
559 allocation_history: AllocationHistory::from_parts(
560 Vec::new(),
561 (1..=current_generation)
562 .map(|generation| GenerationRecord {
563 generation,
564 parent_generation: if generation == 1 {
565 Some(0)
566 } else {
567 Some(generation - 1)
568 },
569 runtime_fingerprint: None,
570 declaration_count: 0,
571 committed_at: None,
572 })
573 .collect(),
574 ),
575 }
576 }
577
578 fn active_record(key: &str, id: u8) -> AllocationRecord {
579 AllocationRecord::from_declaration(1, declaration(key, id, None), AllocationState::Active)
580 }
581
582 fn validated(
583 generation: u64,
584 declarations: Vec<AllocationDeclaration>,
585 ) -> crate::session::ValidatedAllocations {
586 crate::session::ValidatedAllocations::new(generation, declarations, None)
587 }
588
589 fn record<'ledger>(ledger: &'ledger AllocationLedger, key: &str) -> &'ledger AllocationRecord {
590 ledger
591 .allocation_history
592 .records()
593 .iter()
594 .find(|record| record.stable_key.as_str() == key)
595 .expect("allocation record")
596 }
597
598 #[test]
599 fn allocation_history_accessors_expose_read_only_views() {
600 let history = AllocationHistory::from_parts(
601 vec![active_record("app.users.v1", 100)],
602 vec![GenerationRecord::new(1, Some(0), None, 1, Some(42)).expect("generation record")],
603 );
604
605 assert!(!history.is_empty());
606 assert_eq!(history.records().len(), 1);
607 assert_eq!(history.generations().len(), 1);
608 assert_eq!(history.generations()[0].committed_at(), Some(42));
609 }
610
611 #[test]
612 fn record_constructors_validate_metadata() {
613 let schema_err = SchemaMetadataRecord::new(1, invalid_schema_metadata())
614 .expect_err("invalid schema must fail");
615 assert_eq!(schema_err, SchemaMetadataError::InvalidVersion);
616
617 let generation_err = GenerationRecord::new(1, Some(0), Some(String::new()), 0, None)
618 .expect_err("empty fingerprint must fail");
619 assert_eq!(
620 generation_err,
621 DeclarationSnapshotError::EmptyRuntimeFingerprint
622 );
623 }
624
625 #[test]
626 fn stage_validated_generation_records_new_allocations() {
627 let declarations = vec![declaration("app.users.v1", 100, Some(1))];
628 let validated = validated(3, declarations);
629
630 let staged = ledger()
631 .stage_validated_generation(&validated, Some(42))
632 .expect("staged generation");
633
634 assert_eq!(staged.current_generation, 4);
635 assert_eq!(staged.allocation_history.records().len(), 1);
636 assert_eq!(staged.allocation_history.records()[0].first_generation, 4);
637 assert_eq!(staged.allocation_history.generations()[0].generation, 4);
638 assert_eq!(
639 staged.allocation_history.generations()[0].committed_at,
640 Some(42)
641 );
642 }
643
644 #[test]
645 fn stage_validated_generation_rejects_stale_validated_allocations() {
646 let validated = validated(2, vec![declaration("app.users.v1", 100, Some(1))]);
647
648 let err = ledger()
649 .stage_validated_generation(&validated, None)
650 .expect_err("stale validated allocations");
651
652 assert_eq!(
653 err,
654 AllocationStageError::StaleValidatedAllocations {
655 validated_generation: 2,
656 ledger_generation: 3
657 }
658 );
659 }
660
661 #[test]
662 fn stage_validated_generation_rejects_invalid_schema_metadata() {
663 let validated = crate::session::ValidatedAllocations::new(
664 3,
665 vec![declaration_with_invalid_schema("app.users.v1", 100)],
666 None,
667 );
668
669 let err = ledger()
670 .stage_validated_generation(&validated, None)
671 .expect_err("invalid schema metadata");
672
673 assert_eq!(
674 err,
675 AllocationStageError::InvalidSchemaMetadata {
676 stable_key: StableKey::parse("app.users.v1").expect("stable key"),
677 error: SchemaMetadataError::InvalidVersion,
678 }
679 );
680 }
681
682 #[test]
683 fn stage_validated_generation_rejects_generation_overflow() {
684 let ledger = AllocationLedger {
685 current_generation: u64::MAX,
686 ..ledger()
687 };
688 let validated = validated(u64::MAX, vec![declaration("app.users.v1", 100, Some(1))]);
689
690 let err = ledger
691 .stage_validated_generation(&validated, None)
692 .expect_err("overflow must fail");
693
694 assert_eq!(
695 err,
696 AllocationStageError::GenerationOverflow {
697 generation: u64::MAX
698 }
699 );
700 }
701
702 #[test]
703 fn stage_validated_generation_rejects_same_key_different_slot() {
704 let mut ledger = ledger();
705 *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
706 let validated = validated(3, vec![declaration("app.users.v1", 101, None)]);
707
708 let err = ledger
709 .stage_validated_generation(&validated, None)
710 .expect_err("stable key cannot move slots");
711
712 assert!(matches!(
713 err,
714 AllocationStageError::StableKeySlotConflict { .. }
715 ));
716 }
717
718 #[test]
719 fn stage_validated_generation_rejects_same_slot_different_key() {
720 let mut ledger = ledger();
721 *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
722 let validated = validated(3, vec![declaration("app.orders.v1", 100, None)]);
723
724 let err = ledger
725 .stage_validated_generation(&validated, None)
726 .expect_err("slot cannot be reused by another key");
727
728 assert!(matches!(
729 err,
730 AllocationStageError::SlotStableKeyConflict { .. }
731 ));
732 }
733
734 #[test]
735 fn stage_validated_generation_rejects_retired_redeclaration() {
736 let mut ledger = ledger();
737 let mut record = active_record("app.users.v1", 100);
738 record.state = AllocationState::Retired;
739 record.retired_generation = Some(3);
740 *ledger.allocation_history.records_mut() = vec![record];
741 let validated = validated(3, vec![declaration("app.users.v1", 100, None)]);
742
743 let err = ledger
744 .stage_validated_generation(&validated, None)
745 .expect_err("retired allocation cannot be redeclared");
746
747 assert!(matches!(
748 err,
749 AllocationStageError::RetiredAllocation { .. }
750 ));
751 }
752
753 #[test]
754 fn stage_validated_generation_preserves_omitted_records() {
755 let first = validated(
756 3,
757 vec![
758 declaration("app.users.v1", 100, Some(1)),
759 declaration("app.orders.v1", 101, Some(1)),
760 ],
761 );
762 let second = validated(4, vec![declaration("app.users.v1", 100, Some(1))]);
763
764 let staged = ledger()
765 .stage_validated_generation(&first, None)
766 .expect("first generation");
767 let staged = staged
768 .stage_validated_generation(&second, None)
769 .expect("second generation");
770
771 assert_eq!(staged.current_generation, 5);
772 assert_eq!(staged.allocation_history.records().len(), 2);
773 let omitted = staged
774 .allocation_history
775 .records()
776 .iter()
777 .find(|record| record.stable_key.as_str() == "app.orders.v1")
778 .expect("omitted record");
779 assert_eq!(omitted.state, AllocationState::Active);
780 assert_eq!(omitted.last_seen_generation, 4);
781 }
782
783 #[test]
784 fn stage_validated_generation_records_schema_metadata_history() {
785 let first = validated(3, vec![declaration("app.users.v1", 100, Some(1))]);
786 let second = validated(4, vec![declaration("app.users.v1", 100, Some(2))]);
787
788 let staged = ledger()
789 .stage_validated_generation(&first, None)
790 .expect("first generation");
791 let staged = staged
792 .stage_validated_generation(&second, None)
793 .expect("second generation");
794 let record = &staged.allocation_history.records()[0];
795
796 assert_eq!(record.schema_history.len(), 2);
797 assert_eq!(record.schema_history[0].generation, 4);
798 assert_eq!(record.schema_history[1].generation, 5);
799 }
800
801 #[test]
802 fn stage_reservation_generation_records_reserved_allocations() {
803 let reservations = vec![declaration("ic_memory.generation_log.v1", 1, None)];
804
805 let staged = ledger()
806 .stage_reservation_generation(&reservations, Some(42))
807 .expect("reserved generation");
808
809 assert_eq!(staged.current_generation, 4);
810 assert_eq!(staged.allocation_history.records().len(), 1);
811 assert_eq!(
812 staged.allocation_history.records()[0].state,
813 AllocationState::Reserved
814 );
815 assert_eq!(
816 staged.allocation_history.generations()[0].declaration_count,
817 1
818 );
819 }
820
821 #[test]
822 fn stage_reservation_generation_refreshes_existing_reserved_allocation() {
823 let first = vec![declaration("app.future_store.v1", 100, Some(1))];
824 let staged = ledger()
825 .stage_reservation_generation(&first, Some(42))
826 .expect("first reservation generation");
827
828 let second = vec![declaration("app.future_store.v1", 100, Some(2))];
829 let staged = staged
830 .stage_reservation_generation(&second, Some(43))
831 .expect("reservation refresh");
832 let record = record(&staged, "app.future_store.v1");
833
834 assert_eq!(record.state(), AllocationState::Reserved);
835 assert_eq!(record.first_generation(), 4);
836 assert_eq!(record.last_seen_generation(), 5);
837 assert_eq!(record.schema_history().len(), 2);
838 assert_eq!(record.schema_history()[1].generation(), 5);
839 assert_eq!(
840 staged.allocation_history.generations()[1].declaration_count(),
841 1
842 );
843 }
844
845 #[test]
846 fn stage_reservation_generation_rejects_generation_overflow() {
847 let ledger = AllocationLedger {
848 current_generation: u64::MAX,
849 ..ledger()
850 };
851 let reservations = vec![declaration("ic_memory.generation_log.v1", 1, None)];
852
853 let err = ledger
854 .stage_reservation_generation(&reservations, None)
855 .expect_err("overflow must fail");
856
857 assert_eq!(
858 err,
859 AllocationReservationError::GenerationOverflow {
860 generation: u64::MAX
861 }
862 );
863 }
864
865 #[test]
866 fn stage_reservation_generation_rejects_invalid_schema_metadata() {
867 let reservations = vec![declaration_with_invalid_schema(
868 "ic_memory.generation_log.v1",
869 1,
870 )];
871
872 let err = ledger()
873 .stage_reservation_generation(&reservations, None)
874 .expect_err("invalid reservation schema metadata");
875
876 assert_eq!(
877 err,
878 AllocationReservationError::InvalidSchemaMetadata {
879 stable_key: StableKey::parse("ic_memory.generation_log.v1").expect("stable key"),
880 error: SchemaMetadataError::InvalidVersion,
881 }
882 );
883 }
884
885 #[test]
886 fn stage_reservation_generation_rejects_same_key_different_slot() {
887 let mut ledger = ledger();
888 *ledger.allocation_history.records_mut() = vec![AllocationRecord::reserved(
889 3,
890 declaration("app.future_store.v1", 100, None),
891 )];
892 let reservations = vec![declaration("app.future_store.v1", 101, None)];
893
894 let err = ledger
895 .stage_reservation_generation(&reservations, None)
896 .expect_err("reservation key cannot move slots");
897
898 assert!(matches!(
899 err,
900 AllocationReservationError::StableKeySlotConflict { .. }
901 ));
902 }
903
904 #[test]
905 fn stage_reservation_generation_rejects_same_slot_different_key() {
906 let mut ledger = ledger();
907 *ledger.allocation_history.records_mut() = vec![AllocationRecord::reserved(
908 3,
909 declaration("app.future_store.v1", 100, None),
910 )];
911 let reservations = vec![declaration("app.other_future_store.v1", 100, None)];
912
913 let err = ledger
914 .stage_reservation_generation(&reservations, None)
915 .expect_err("reservation slot cannot be reused by another key");
916
917 assert!(matches!(
918 err,
919 AllocationReservationError::SlotStableKeyConflict { .. }
920 ));
921 }
922
923 #[test]
924 fn stage_reservation_generation_rejects_active_allocation() {
925 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
926 let staged = ledger()
927 .stage_validated_generation(&active, None)
928 .expect("active generation");
929 let reservations = vec![declaration("app.users.v1", 100, None)];
930
931 let err = staged
932 .stage_reservation_generation(&reservations, None)
933 .expect_err("active cannot become reserved");
934
935 assert!(matches!(
936 err,
937 AllocationReservationError::ActiveAllocation { .. }
938 ));
939 }
940
941 #[test]
942 fn stage_reservation_generation_rejects_retired_allocation() {
943 let mut ledger = ledger();
944 let mut record = active_record("app.users.v1", 100);
945 record.state = AllocationState::Retired;
946 record.retired_generation = Some(3);
947 *ledger.allocation_history.records_mut() = vec![record];
948 let reservations = vec![declaration("app.users.v1", 100, None)];
949
950 let err = ledger
951 .stage_reservation_generation(&reservations, None)
952 .expect_err("retired cannot revive");
953
954 assert!(matches!(
955 err,
956 AllocationReservationError::RetiredAllocation { .. }
957 ));
958 }
959
960 #[test]
961 fn stage_validated_generation_activates_reserved_record() {
962 let reservations = vec![declaration("app.future_store.v1", 100, Some(1))];
963 let staged = ledger()
964 .stage_reservation_generation(&reservations, None)
965 .expect("reserved generation");
966 let active = validated(4, vec![declaration("app.future_store.v1", 100, Some(2))]);
967
968 let staged = staged
969 .stage_validated_generation(&active, None)
970 .expect("active generation");
971 let record = &staged.allocation_history.records()[0];
972
973 assert_eq!(record.state, AllocationState::Active);
974 assert_eq!(record.first_generation, 4);
975 assert_eq!(record.last_seen_generation, 5);
976 assert_eq!(record.schema_history.len(), 2);
977 }
978
979 #[test]
980 fn stage_retirement_generation_tombstones_named_allocation() {
981 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
982 let staged = ledger()
983 .stage_validated_generation(&active, None)
984 .expect("active generation");
985 let retirement = AllocationRetirement::new(
986 "app.users.v1",
987 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
988 )
989 .expect("retirement");
990
991 let staged = staged
992 .stage_retirement_generation(&retirement, Some(42))
993 .expect("retired generation");
994 let record = &staged.allocation_history.records()[0];
995
996 assert_eq!(staged.current_generation, 5);
997 assert_eq!(record.state, AllocationState::Retired);
998 assert_eq!(record.retired_generation, Some(5));
999 assert_eq!(
1000 staged.allocation_history.generations()[1].declaration_count,
1001 0
1002 );
1003 }
1004
1005 #[test]
1006 fn stage_retirement_generation_rejects_generation_overflow() {
1007 let mut ledger = ledger();
1008 ledger.current_generation = u64::MAX;
1009 *ledger.allocation_history.records_mut() = vec![active_record("app.users.v1", 100)];
1010 let retirement = AllocationRetirement::new(
1011 "app.users.v1",
1012 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1013 )
1014 .expect("retirement");
1015
1016 let err = ledger
1017 .stage_retirement_generation(&retirement, None)
1018 .expect_err("overflow must fail");
1019
1020 assert_eq!(
1021 err,
1022 AllocationRetirementError::GenerationOverflow {
1023 generation: u64::MAX
1024 }
1025 );
1026 }
1027
1028 #[test]
1029 fn stage_retirement_generation_requires_matching_slot() {
1030 let active = validated(3, vec![declaration("app.users.v1", 100, None)]);
1031 let staged = ledger()
1032 .stage_validated_generation(&active, None)
1033 .expect("active generation");
1034 let retirement = AllocationRetirement::new(
1035 "app.users.v1",
1036 AllocationSlotDescriptor::memory_manager(101).expect("usable slot"),
1037 )
1038 .expect("retirement");
1039
1040 let err = staged
1041 .stage_retirement_generation(&retirement, None)
1042 .expect_err("slot mismatch");
1043
1044 assert!(matches!(
1045 err,
1046 AllocationRetirementError::SlotMismatch { .. }
1047 ));
1048 }
1049
1050 #[test]
1051 fn snapshot_can_feed_validated_generation() {
1052 let snapshot = DeclarationSnapshot::new(vec![declaration("app.users.v1", 100, None)])
1053 .expect("snapshot");
1054 let (declarations, runtime_fingerprint) = snapshot.into_parts();
1055 let validated =
1056 crate::session::ValidatedAllocations::new(3, declarations, runtime_fingerprint);
1057
1058 let staged = ledger()
1059 .stage_validated_generation(&validated, None)
1060 .expect("validated generation");
1061
1062 assert_eq!(staged.allocation_history.records().len(), 1);
1063 }
1064
1065 #[test]
1066 fn stage_validated_generation_records_runtime_fingerprint() {
1067 let validated = crate::session::ValidatedAllocations::new(
1068 3,
1069 vec![declaration("app.users.v1", 100, None)],
1070 Some("wasm:abc123".to_string()),
1071 );
1072
1073 let staged = ledger()
1074 .stage_validated_generation(&validated, None)
1075 .expect("validated generation");
1076
1077 assert_eq!(
1078 staged.allocation_history.generations()[0].runtime_fingerprint,
1079 Some("wasm:abc123".to_string())
1080 );
1081 }
1082
1083 #[test]
1084 fn strict_committed_integrity_accepts_full_lifecycle() {
1085 let mut ledger = committed_ledger(0);
1086 ledger
1087 .validate_committed_integrity()
1088 .expect("genesis ledger with no history");
1089
1090 ledger = ledger
1091 .stage_validated_generation(
1092 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1093 Some(1),
1094 )
1095 .expect("first real commit after genesis");
1096 ledger
1097 .validate_committed_integrity()
1098 .expect("first real commit");
1099
1100 ledger = ledger
1101 .stage_validated_generation(
1102 &validated(1, vec![declaration("app.users.v1", 100, Some(1))]),
1103 Some(2),
1104 )
1105 .expect("repeated active declaration");
1106 ledger
1107 .validate_committed_integrity()
1108 .expect("repeated active declaration");
1109 assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 1);
1110
1111 ledger = ledger
1112 .stage_validated_generation(
1113 &validated(2, vec![declaration("app.users.v1", 100, Some(2))]),
1114 Some(3),
1115 )
1116 .expect("schema drift");
1117 ledger
1118 .validate_committed_integrity()
1119 .expect("schema metadata drift");
1120 assert_eq!(record(&ledger, "app.users.v1").schema_history.len(), 2);
1121
1122 ledger = ledger
1123 .stage_reservation_generation(
1124 &[declaration("app.future_store.v1", 101, Some(1))],
1125 Some(4),
1126 )
1127 .expect("reservation-only generation");
1128 ledger
1129 .validate_committed_integrity()
1130 .expect("reservation-only generation");
1131 assert_eq!(
1132 record(&ledger, "app.future_store.v1").state,
1133 AllocationState::Reserved
1134 );
1135
1136 ledger = ledger
1137 .stage_validated_generation(
1138 &validated(4, vec![declaration("app.future_store.v1", 101, Some(2))]),
1139 Some(5),
1140 )
1141 .expect("reservation activation");
1142 ledger
1143 .validate_committed_integrity()
1144 .expect("reservation activation");
1145 assert_eq!(
1146 record(&ledger, "app.future_store.v1").state,
1147 AllocationState::Active
1148 );
1149
1150 let retirement = AllocationRetirement::new(
1151 "app.users.v1",
1152 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
1153 )
1154 .expect("retirement");
1155 ledger = ledger
1156 .stage_retirement_generation(&retirement, Some(6))
1157 .expect("retirement generation");
1158 ledger
1159 .validate_committed_integrity()
1160 .expect("retirement generation");
1161 assert_eq!(ledger.current_generation, 6);
1162 assert_eq!(
1163 record(&ledger, "app.users.v1").state,
1164 AllocationState::Retired
1165 );
1166 assert_eq!(
1167 record(&ledger, "app.future_store.v1").last_seen_generation,
1168 5
1169 );
1170 }
1171
1172 #[test]
1173 fn new_committed_requires_strict_generation_history() {
1174 let structurally_valid = AllocationLedger::new(
1175 CURRENT_LEDGER_SCHEMA_VERSION,
1176 CURRENT_PHYSICAL_FORMAT_ID,
1177 3,
1178 AllocationHistory::default(),
1179 )
1180 .expect("structurally valid DTO");
1181
1182 assert_eq!(structurally_valid.current_generation, 3);
1183
1184 let err = AllocationLedger::new_committed(
1185 CURRENT_LEDGER_SCHEMA_VERSION,
1186 CURRENT_PHYSICAL_FORMAT_ID,
1187 3,
1188 AllocationHistory::default(),
1189 )
1190 .expect_err("committed ledger needs generation history");
1191
1192 assert_eq!(
1193 err,
1194 LedgerIntegrityError::MissingCurrentGenerationRecord {
1195 current_generation: 3
1196 }
1197 );
1198 }
1199
1200 #[test]
1201 fn validate_integrity_rejects_duplicate_stable_keys() {
1202 let mut ledger = ledger();
1203 *ledger.allocation_history.records_mut() = vec![
1204 active_record("app.users.v1", 100),
1205 active_record("app.users.v1", 101),
1206 ];
1207
1208 let err = ledger.validate_integrity().expect_err("duplicate key");
1209
1210 assert!(matches!(
1211 err,
1212 LedgerIntegrityError::DuplicateStableKey { .. }
1213 ));
1214 }
1215
1216 #[test]
1217 fn validate_integrity_rejects_duplicate_slots() {
1218 let mut ledger = ledger();
1219 *ledger.allocation_history.records_mut() = vec![
1220 active_record("app.users.v1", 100),
1221 active_record("app.orders.v1", 100),
1222 ];
1223
1224 let err = ledger.validate_integrity().expect_err("duplicate slot");
1225
1226 assert!(matches!(err, LedgerIntegrityError::DuplicateSlot { .. }));
1227 }
1228
1229 #[test]
1230 fn validate_integrity_rejects_retired_record_without_retired_generation() {
1231 let mut ledger = ledger();
1232 let mut record = active_record("app.users.v1", 100);
1233 record.state = AllocationState::Retired;
1234 *ledger.allocation_history.records_mut() = vec![record];
1235
1236 let err = ledger
1237 .validate_integrity()
1238 .expect_err("missing retired generation");
1239
1240 assert!(matches!(
1241 err,
1242 LedgerIntegrityError::MissingRetiredGeneration { .. }
1243 ));
1244 }
1245
1246 #[test]
1247 fn validate_integrity_rejects_non_retired_record_with_retired_generation() {
1248 let mut ledger = ledger();
1249 let mut record = active_record("app.users.v1", 100);
1250 record.retired_generation = Some(2);
1251 *ledger.allocation_history.records_mut() = vec![record];
1252
1253 let err = ledger
1254 .validate_integrity()
1255 .expect_err("unexpected retired generation");
1256
1257 assert!(matches!(
1258 err,
1259 LedgerIntegrityError::UnexpectedRetiredGeneration { .. }
1260 ));
1261 }
1262
1263 #[test]
1264 fn validate_integrity_rejects_non_increasing_schema_history() {
1265 let mut ledger = ledger();
1266 let mut record = active_record("app.users.v1", 100);
1267 record.schema_history.push(SchemaMetadataRecord {
1268 generation: 1,
1269 schema: SchemaMetadata::default(),
1270 });
1271 *ledger.allocation_history.records_mut() = vec![record];
1272
1273 let err = ledger
1274 .validate_integrity()
1275 .expect_err("non-increasing schema history");
1276
1277 assert!(matches!(
1278 err,
1279 LedgerIntegrityError::NonIncreasingSchemaHistory { .. }
1280 ));
1281 }
1282
1283 #[test]
1284 fn validate_integrity_rejects_invalid_schema_metadata_history() {
1285 let mut ledger = committed_ledger(1);
1286 let mut record = active_record("app.users.v1", 100);
1287 record.schema_history[0].schema = invalid_schema_metadata();
1288 *ledger.allocation_history.records_mut() = vec![record];
1289
1290 let err = ledger
1291 .validate_committed_integrity()
1292 .expect_err("invalid committed schema metadata");
1293
1294 assert_eq!(
1295 err,
1296 LedgerIntegrityError::InvalidSchemaMetadata {
1297 stable_key: StableKey::parse("app.users.v1").expect("stable key"),
1298 generation: 1,
1299 error: SchemaMetadataError::InvalidVersion,
1300 }
1301 );
1302 }
1303
1304 #[test]
1305 fn validate_committed_integrity_requires_current_generation_record() {
1306 let err = ledger()
1307 .validate_committed_integrity()
1308 .expect_err("missing current generation");
1309
1310 assert_eq!(
1311 err,
1312 LedgerIntegrityError::MissingCurrentGenerationRecord {
1313 current_generation: 3
1314 }
1315 );
1316 }
1317
1318 #[test]
1319 fn validate_committed_integrity_rejects_generation_history_gaps() {
1320 let mut ledger = committed_ledger(3);
1321 ledger.allocation_history.generations_mut().remove(1);
1322
1323 let err = ledger
1324 .validate_committed_integrity()
1325 .expect_err("generation history gap");
1326
1327 assert!(matches!(
1328 err,
1329 LedgerIntegrityError::NonIncreasingGenerationRecords { .. }
1330 ));
1331 }
1332
1333 #[test]
1334 fn ledger_commit_store_rejects_invalid_ledger_before_write() {
1335 let mut store = LedgerCommitStore::default();
1336 let codec = TestCodec;
1337 let mut invalid = ledger();
1338 *invalid.allocation_history.records_mut() = vec![
1339 active_record("app.users.v1", 100),
1340 active_record("app.orders.v1", 100),
1341 ];
1342
1343 let err = store.commit(&invalid, &codec).expect_err("invalid ledger");
1344
1345 assert!(matches!(
1346 err,
1347 LedgerCommitError::Integrity(LedgerIntegrityError::DuplicateSlot { .. })
1348 ));
1349 assert!(store.physical().is_uninitialized());
1350 }
1351
1352 #[test]
1353 fn ledger_commit_store_recovers_latest_committed_ledger() {
1354 let mut store = LedgerCommitStore::default();
1355 let codec = TestCodec;
1356 let first = committed_ledger(1);
1357 let second = committed_ledger(2);
1358
1359 store.commit(&first, &codec).expect("first commit");
1360 store.commit(&second, &codec).expect("second commit");
1361 let recovered = store.recover(&codec).expect("recovered ledger");
1362
1363 assert_eq!(recovered.current_generation, 2);
1364 }
1365
1366 #[test]
1367 fn ledger_commit_store_recovers_compatible_genesis_and_first_real_commit() {
1368 let mut store = LedgerCommitStore::default();
1369 let codec = FullLedgerCodec::default();
1370 let genesis = committed_ledger(0);
1371
1372 let recovered = store
1373 .recover_or_initialize(&codec, &genesis)
1374 .expect("compatible genesis ledger");
1375 assert_eq!(recovered.current_generation, 0);
1376 assert!(recovered.allocation_history.generations().is_empty());
1377
1378 let first = recovered
1379 .stage_validated_generation(
1380 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1381 None,
1382 )
1383 .expect("first real generation");
1384 let recovered = store.commit(&first, &codec).expect("first commit");
1385
1386 assert_eq!(recovered.current_generation, 1);
1387 assert_eq!(recovered.allocation_history.generations()[0].generation, 1);
1388 assert_eq!(record(&recovered, "app.users.v1").first_generation, 1);
1389 }
1390
1391 #[test]
1392 fn ledger_commit_store_recovers_full_payload_after_corrupt_latest_slot() {
1393 let mut store = LedgerCommitStore::default();
1394 let codec = FullLedgerCodec::default();
1395 let genesis = committed_ledger(0);
1396 store.commit(&genesis, &codec).expect("genesis commit");
1397 let first = genesis
1398 .stage_validated_generation(
1399 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1400 None,
1401 )
1402 .expect("first generation");
1403 let first = store.commit(&first, &codec).expect("first commit");
1404 let second = first
1405 .stage_validated_generation(
1406 &validated(1, vec![declaration("app.users.v1", 100, Some(2))]),
1407 None,
1408 )
1409 .expect("second generation");
1410
1411 store
1412 .write_corrupt_inactive_ledger(&second, &codec)
1413 .expect("corrupt latest");
1414 let recovered = store.recover(&codec).expect("recover prior generation");
1415
1416 assert_eq!(recovered.current_generation, 1);
1417 assert_eq!(record(&recovered, "app.users.v1").schema_history.len(), 1);
1418 }
1419
1420 #[test]
1421 fn ledger_commit_store_recovers_identical_duplicate_slots() {
1422 let codec = FullLedgerCodec::default();
1423 let ledger = committed_ledger(0)
1424 .stage_validated_generation(
1425 &validated(0, vec![declaration("app.users.v1", 100, Some(1))]),
1426 None,
1427 )
1428 .expect("first generation");
1429 let payload = codec.encode(&ledger).expect("payload");
1430 let committed = CommittedGenerationBytes::new(ledger.current_generation, payload);
1431 let store = LedgerCommitStore {
1432 physical: DualCommitStore {
1433 slot0: Some(committed.clone()),
1434 slot1: Some(committed),
1435 },
1436 };
1437
1438 let recovered = store.recover(&codec).expect("recovered");
1439
1440 assert_eq!(recovered, ledger);
1441 }
1442
1443 #[test]
1444 fn ledger_commit_store_ignores_corrupt_inactive_ledger() {
1445 let mut store = LedgerCommitStore::default();
1446 let codec = TestCodec;
1447 let first = committed_ledger(1);
1448 let second = committed_ledger(2);
1449
1450 store.commit(&first, &codec).expect("first commit");
1451 store
1452 .write_corrupt_inactive_ledger(&second, &codec)
1453 .expect("corrupt write");
1454 let recovered = store.recover(&codec).expect("recovered ledger");
1455
1456 assert_eq!(recovered.current_generation, 1);
1457 }
1458
1459 #[test]
1460 fn ledger_commit_store_rejects_physical_logical_generation_mismatch() {
1461 let store = LedgerCommitStore {
1462 physical: DualCommitStore {
1463 slot0: Some(CommittedGenerationBytes::new(
1464 7,
1465 TestCodec.encode(&committed_ledger(6)).expect("payload"),
1466 )),
1467 slot1: None,
1468 },
1469 };
1470 let codec = TestCodec;
1471
1472 let err = store.recover(&codec).expect_err("mismatch");
1473
1474 assert_eq!(
1475 err,
1476 LedgerCommitError::PhysicalLogicalGenerationMismatch {
1477 physical_generation: 7,
1478 logical_generation: 6
1479 }
1480 );
1481 }
1482
1483 #[test]
1484 fn ledger_commit_store_rejects_non_next_logical_generation() {
1485 let mut store = LedgerCommitStore::default();
1486 let codec = TestCodec;
1487 store
1488 .commit(&committed_ledger(1), &codec)
1489 .expect("first commit");
1490
1491 let err = store
1492 .commit(&committed_ledger(3), &codec)
1493 .expect_err("skipped generation");
1494
1495 assert_eq!(
1496 err,
1497 LedgerCommitError::Recovery(CommitRecoveryError::UnexpectedGeneration {
1498 expected: 2,
1499 actual: 3
1500 })
1501 );
1502 }
1503
1504 #[test]
1505 fn ledger_commit_store_initializes_empty_store_explicitly() {
1506 let mut store = LedgerCommitStore::default();
1507 let codec = TestCodec;
1508 let genesis = committed_ledger(3);
1509
1510 let recovered = store
1511 .recover_or_initialize(&codec, &genesis)
1512 .expect("initialized ledger");
1513
1514 assert_eq!(recovered.current_generation, 3);
1515 assert!(!store.physical().is_uninitialized());
1516 }
1517
1518 #[test]
1519 fn ledger_commit_store_rejects_corrupt_store_even_with_genesis() {
1520 let mut store = LedgerCommitStore::default();
1521 let codec = TestCodec;
1522 store
1523 .write_corrupt_inactive_ledger(&ledger(), &codec)
1524 .expect("corrupt write");
1525
1526 let err = store
1527 .recover_or_initialize(&codec, &ledger())
1528 .expect_err("corrupt state");
1529
1530 assert!(matches!(
1531 err,
1532 LedgerCommitError::Recovery(CommitRecoveryError::NoValidGeneration)
1533 ));
1534 }
1535
1536 #[test]
1537 fn ledger_commit_store_rejects_incompatible_schema_before_write() {
1538 let mut store = LedgerCommitStore::default();
1539 let codec = TestCodec;
1540 let incompatible = AllocationLedger {
1541 ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION + 1,
1542 ..committed_ledger(0)
1543 };
1544
1545 let err = store
1546 .commit(&incompatible, &codec)
1547 .expect_err("incompatible schema");
1548
1549 assert!(matches!(
1550 err,
1551 LedgerCommitError::Compatibility(
1552 LedgerCompatibilityError::UnsupportedLedgerSchemaVersion { .. }
1553 )
1554 ));
1555 assert!(store.physical().is_uninitialized());
1556 }
1557
1558 #[test]
1559 fn ledger_commit_store_rejects_incompatible_schema_on_recovery() {
1560 let mut store = LedgerCommitStore::default();
1561 let codec = TestCodec;
1562 let incompatible = AllocationLedger {
1563 ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION + 1,
1564 ..committed_ledger(3)
1565 };
1566 let payload = codec.encode(&incompatible).expect("payload");
1567 store
1568 .physical_mut()
1569 .commit_payload_at_generation(incompatible.current_generation, payload)
1570 .expect("physical commit");
1571
1572 let err = store.recover(&codec).expect_err("incompatible schema");
1573
1574 assert!(matches!(
1575 err,
1576 LedgerCommitError::Compatibility(
1577 LedgerCompatibilityError::UnsupportedLedgerSchemaVersion { .. }
1578 )
1579 ));
1580 }
1581
1582 #[test]
1583 fn ledger_commit_store_rejects_incompatible_physical_format() {
1584 let mut store = LedgerCommitStore::default();
1585 let codec = TestCodec;
1586 let incompatible = AllocationLedger {
1587 physical_format_id: CURRENT_PHYSICAL_FORMAT_ID + 1,
1588 ..committed_ledger(0)
1589 };
1590
1591 let err = store
1592 .recover_or_initialize(&codec, &incompatible)
1593 .expect_err("incompatible format");
1594
1595 assert!(matches!(
1596 err,
1597 LedgerCommitError::Compatibility(
1598 LedgerCompatibilityError::UnsupportedPhysicalFormat { .. }
1599 )
1600 ));
1601 assert!(store.physical().is_uninitialized());
1602 }
1603}