1mod storage;
152pub use storage::{Archive, Identifier};
153pub mod translator;
154
155use std::hash::Hash;
156use thiserror::Error;
157
158#[derive(Debug, Error)]
160pub enum Error {
161 #[error("journal error: {0}")]
162 Journal(#[from] crate::journal::Error),
163 #[error("record corrupted")]
164 RecordCorrupted,
165 #[error("already pruned to: {0}")]
166 AlreadyPrunedTo(u64),
167 #[error("record too large")]
168 RecordTooLarge,
169 #[error("compression failed")]
170 CompressionFailed,
171 #[error("decompression failed")]
172 DecompressionFailed,
173}
174
175pub trait Translator: Clone {
181 type Key: Eq + Hash + Send + Sync + Clone;
182
183 fn transform(&self, key: &[u8]) -> Self::Key;
185}
186
187#[derive(Clone)]
189pub struct Config<T: Translator> {
190 pub translator: T,
195
196 pub section_mask: u64,
200
201 pub pending_writes: usize,
205
206 pub replay_concurrency: usize,
208
209 pub compression: Option<u8>,
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use crate::journal::variable::{Config as JConfig, Journal};
217 use crate::journal::Error as JournalError;
218 use bytes::Bytes;
219 use commonware_macros::test_traced;
220 use commonware_runtime::Metrics;
221 use commonware_runtime::{deterministic::Executor, Blob, Runner, Storage};
222 use commonware_utils::array::FixedBytes;
223 use rand::Rng;
224 use std::collections::BTreeMap;
225 use translator::{FourCap, TwoCap};
226
227 const DEFAULT_SECTION_MASK: u64 = 0xffff_ffff_ffff_0000u64;
228
229 fn test_key(key: &str) -> FixedBytes<64> {
230 let mut buf = [0u8; 64];
231 let key = key.as_bytes();
232 assert!(key.len() <= buf.len());
233 buf[..key.len()].copy_from_slice(key);
234 FixedBytes::try_from(&buf[..]).unwrap()
235 }
236
237 fn test_archive_put_get(compression: Option<u8>) {
238 let (executor, context, _) = Executor::default();
240 executor.start(async move {
241 let journal = Journal::init(
243 context.clone(),
244 JConfig {
245 partition: "test_partition".into(),
246 },
247 )
248 .await
249 .expect("Failed to initialize journal");
250
251 let cfg = Config {
253 translator: FourCap,
254 pending_writes: 10,
255 replay_concurrency: 4,
256 compression,
257 section_mask: DEFAULT_SECTION_MASK,
258 };
259 let mut archive = Archive::init(context.clone(), journal, cfg.clone())
260 .await
261 .expect("Failed to initialize archive");
262
263 let index = 1u64;
264 let key = test_key("testkey");
265 let data = Bytes::from("testdata");
266
267 let has = archive
269 .has(Identifier::Index(index))
270 .await
271 .expect("Failed to check key");
272 assert!(!has);
273 let has = archive
274 .has(Identifier::Key(&key))
275 .await
276 .expect("Failed to check key");
277 assert!(!has);
278
279 archive
281 .put(index, key.clone(), data.clone())
282 .await
283 .expect("Failed to put data");
284
285 let has = archive
287 .has(Identifier::Index(index))
288 .await
289 .expect("Failed to check key");
290 assert!(has);
291 let has = archive
292 .has(Identifier::Key(&key))
293 .await
294 .expect("Failed to check key");
295 assert!(has);
296
297 let retrieved = archive
299 .get(Identifier::Index(index))
300 .await
301 .expect("Failed to get data")
302 .expect("Data not found");
303 assert_eq!(retrieved, data);
304 let retrieved = archive
305 .get(Identifier::Key(&key))
306 .await
307 .expect("Failed to get data")
308 .expect("Data not found");
309 assert_eq!(retrieved, data);
310
311 let buffer = context.encode();
313 assert!(buffer.contains("items_tracked 1"));
314 assert!(buffer.contains("unnecessary_reads_total 0"));
315 assert!(buffer.contains("gets_total 2"));
316 assert!(buffer.contains("has_total 4"));
317 assert!(buffer.contains("syncs_total 0"));
318
319 archive.sync().await.expect("Failed to sync data");
321
322 let buffer = context.encode();
324 assert!(buffer.contains("items_tracked 1"));
325 assert!(buffer.contains("unnecessary_reads_total 0"));
326 assert!(buffer.contains("gets_total 2"));
327 assert!(buffer.contains("has_total 4"));
328 assert!(buffer.contains("syncs_total 1"));
329 });
330 }
331
332 #[test_traced]
333 fn test_archive_put_get_no_compression() {
334 test_archive_put_get(None);
335 }
336
337 #[test_traced]
338 fn test_archive_put_get_compression() {
339 test_archive_put_get(Some(3));
340 }
341
342 #[test_traced]
343 fn test_archive_compression_then_none() {
344 let (executor, context, _) = Executor::default();
346 executor.start(async move {
347 let journal = Journal::init(
349 context.clone(),
350 JConfig {
351 partition: "test_partition".into(),
352 },
353 )
354 .await
355 .expect("Failed to initialize journal");
356
357 let cfg = Config {
359 translator: FourCap,
360 pending_writes: 10,
361 replay_concurrency: 4,
362 compression: Some(3),
363 section_mask: DEFAULT_SECTION_MASK,
364 };
365 let mut archive = Archive::init(context.clone(), journal, cfg.clone())
366 .await
367 .expect("Failed to initialize archive");
368
369 let index = 1u64;
371 let key = test_key("testkey");
372 let data = Bytes::from("testdata");
373 archive
374 .put(index, key.clone(), data.clone())
375 .await
376 .expect("Failed to put data");
377
378 archive.close().await.expect("Failed to close archive");
380
381 let journal = Journal::init(
383 context.clone(),
384 JConfig {
385 partition: "test_partition".into(),
386 },
387 )
388 .await
389 .expect("Failed to initialize journal");
390 let cfg = Config {
391 translator: FourCap,
392 pending_writes: 10,
393 replay_concurrency: 4,
394 compression: None,
395 section_mask: DEFAULT_SECTION_MASK,
396 };
397 let archive = Archive::init(context, journal, cfg.clone())
398 .await
399 .expect("Failed to initialize archive");
400
401 let retrieved = archive
403 .get(Identifier::Index(index))
404 .await
405 .expect("Failed to get data")
406 .expect("Data not found");
407 assert_ne!(retrieved, data);
408 let retrieved = archive
409 .get(Identifier::Key(&key))
410 .await
411 .expect("Failed to get data")
412 .expect("Data not found");
413 assert_ne!(retrieved, data);
414 });
415 }
416
417 #[test_traced]
418 fn test_archive_record_corruption() {
419 let (executor, context, _) = Executor::default();
421 executor.start(async move {
422 let journal = Journal::init(
424 context.clone(),
425 JConfig {
426 partition: "test_partition".into(),
427 },
428 )
429 .await
430 .expect("Failed to initialize journal");
431
432 let cfg = Config {
434 translator: FourCap,
435 pending_writes: 10,
436 replay_concurrency: 4,
437 compression: None,
438 section_mask: DEFAULT_SECTION_MASK,
439 };
440 let mut archive = Archive::init(context.clone(), journal, cfg.clone())
441 .await
442 .expect("Failed to initialize archive");
443
444 let index = 1u64;
445 let key = test_key("testkey");
446 let data = Bytes::from("testdata");
447
448 archive
450 .put(index, key.clone(), data.clone())
451 .await
452 .expect("Failed to put data");
453
454 archive.close().await.expect("Failed to close archive");
456
457 let section = index & DEFAULT_SECTION_MASK;
459 let blob = context
460 .open("test_partition", §ion.to_be_bytes())
461 .await
462 .unwrap();
463 let value_location = 4 + 8 + 64 + 4;
464 blob.write_at(b"testdaty", value_location).await.unwrap();
465 blob.close().await.unwrap();
466
467 let journal = Journal::init(
469 context.clone(),
470 JConfig {
471 partition: "test_partition".into(),
472 },
473 )
474 .await
475 .expect("Failed to initialize journal");
476 let archive = Archive::init(
477 context,
478 journal,
479 Config {
480 translator: FourCap,
481 pending_writes: 10,
482 replay_concurrency: 4,
483 compression: None,
484 section_mask: DEFAULT_SECTION_MASK,
485 },
486 )
487 .await
488 .expect("Failed to initialize archive");
489
490 let result = archive.get(Identifier::Key(&key)).await;
492 assert!(matches!(
493 result,
494 Err(Error::Journal(JournalError::ChecksumMismatch(_, _)))
495 ));
496 });
497 }
498
499 #[test_traced]
500 fn test_archive_duplicate_key() {
501 let (executor, context, _) = Executor::default();
503 executor.start(async move {
504 let journal = Journal::init(
506 context.clone(),
507 JConfig {
508 partition: "test_partition".into(),
509 },
510 )
511 .await
512 .expect("Failed to initialize journal");
513
514 let cfg = Config {
516 translator: FourCap,
517 pending_writes: 10,
518 replay_concurrency: 4,
519 compression: None,
520 section_mask: DEFAULT_SECTION_MASK,
521 };
522 let mut archive = Archive::init(context.clone(), journal, cfg.clone())
523 .await
524 .expect("Failed to initialize archive");
525
526 let index = 1u64;
527 let key = test_key("duplicate");
528 let data1 = Bytes::from("data1");
529 let data2 = Bytes::from("data2");
530
531 archive
533 .put(index, key.clone(), data1.clone())
534 .await
535 .expect("Failed to put data");
536
537 archive
539 .put(index, key.clone(), data2.clone())
540 .await
541 .expect("Duplicate put should not fail");
542
543 let retrieved = archive
545 .get(Identifier::Index(index))
546 .await
547 .expect("Failed to get data")
548 .expect("Data not found");
549 assert_eq!(retrieved, data1);
550 let retrieved = archive
551 .get(Identifier::Key(&key))
552 .await
553 .expect("Failed to get data")
554 .expect("Data not found");
555 assert_eq!(retrieved, data1);
556
557 let buffer = context.encode();
559 assert!(buffer.contains("items_tracked 1"));
560 assert!(buffer.contains("unnecessary_reads_total 0"));
561 assert!(buffer.contains("gets_total 2"));
562 });
563 }
564
565 #[test_traced]
566 fn test_archive_get_nonexistent() {
567 let (executor, context, _) = Executor::default();
569 executor.start(async move {
570 let journal = Journal::init(
572 context.clone(),
573 JConfig {
574 partition: "test_partition".into(),
575 },
576 )
577 .await
578 .expect("Failed to initialize journal");
579
580 let cfg = Config {
582 translator: FourCap,
583 pending_writes: 10,
584 replay_concurrency: 4,
585 compression: None,
586 section_mask: DEFAULT_SECTION_MASK,
587 };
588 let archive = Archive::init(context.clone(), journal, cfg.clone())
589 .await
590 .expect("Failed to initialize archive");
591
592 let index = 1u64;
594 let retrieved = archive
595 .get(Identifier::Index(index))
596 .await
597 .expect("Failed to get data");
598 assert!(retrieved.is_none());
599
600 let key = test_key("nonexistent");
602 let retrieved = archive
603 .get(Identifier::Key(&key))
604 .await
605 .expect("Failed to get data");
606 assert!(retrieved.is_none());
607
608 let buffer = context.encode();
610 assert!(buffer.contains("items_tracked 0"));
611 assert!(buffer.contains("unnecessary_reads_total 0"));
612 assert!(buffer.contains("gets_total 2"));
613 });
614 }
615
616 #[test_traced]
617 fn test_archive_overlapping_key() {
618 let (executor, context, _) = Executor::default();
620 executor.start(async move {
621 let journal = Journal::init(
623 context.clone(),
624 JConfig {
625 partition: "test_partition".into(),
626 },
627 )
628 .await
629 .expect("Failed to initialize journal");
630
631 let cfg = Config {
633 translator: FourCap,
634 pending_writes: 10,
635 replay_concurrency: 4,
636 compression: None,
637 section_mask: DEFAULT_SECTION_MASK,
638 };
639 let mut archive = Archive::init(context.clone(), journal, cfg.clone())
640 .await
641 .expect("Failed to initialize archive");
642
643 let index1 = 1u64;
644 let key1 = test_key("keys1");
645 let data1 = Bytes::from("data1");
646 let index2 = 2u64;
647 let key2 = test_key("keys2");
648 let data2 = Bytes::from("data2");
649
650 archive
652 .put(index1, key1.clone(), data1.clone())
653 .await
654 .expect("Failed to put data");
655
656 archive
658 .put(index2, key2.clone(), data2.clone())
659 .await
660 .expect("Failed to put data");
661
662 let retrieved = archive
664 .get(Identifier::Key(&key1))
665 .await
666 .expect("Failed to get data")
667 .expect("Data not found");
668 assert_eq!(retrieved, data1);
669
670 let retrieved = archive
672 .get(Identifier::Key(&key2))
673 .await
674 .expect("Failed to get data")
675 .expect("Data not found");
676 assert_eq!(retrieved, data2);
677
678 let buffer = context.encode();
680 assert!(buffer.contains("items_tracked 2"));
681 assert!(buffer.contains("unnecessary_reads_total 1"));
682 assert!(buffer.contains("gets_total 2"));
683 });
684 }
685
686 #[test_traced]
687 fn test_archive_overlapping_key_multiple_sections() {
688 let (executor, context, _) = Executor::default();
690 executor.start(async move {
691 let journal = Journal::init(
693 context.clone(),
694 JConfig {
695 partition: "test_partition".into(),
696 },
697 )
698 .await
699 .expect("Failed to initialize journal");
700
701 let cfg = Config {
703 translator: FourCap,
704 pending_writes: 10,
705 replay_concurrency: 4,
706 compression: None,
707 section_mask: DEFAULT_SECTION_MASK,
708 };
709 let mut archive = Archive::init(context.clone(), journal, cfg.clone())
710 .await
711 .expect("Failed to initialize archive");
712
713 let index1 = 1u64;
714 let key1 = test_key("keys1");
715 let data1 = Bytes::from("data1");
716 let index2 = 2_000_000u64;
717 let key2 = test_key("keys2");
718 let data2 = Bytes::from("data2");
719
720 archive
722 .put(index1, key1.clone(), data1.clone())
723 .await
724 .expect("Failed to put data");
725
726 archive
728 .put(index2, key2.clone(), data2.clone())
729 .await
730 .expect("Failed to put data");
731
732 let retrieved = archive
734 .get(Identifier::Key(&key1))
735 .await
736 .expect("Failed to get data")
737 .expect("Data not found");
738 assert_eq!(retrieved, data1);
739
740 let retrieved = archive
742 .get(Identifier::Key(&key2))
743 .await
744 .expect("Failed to get data")
745 .expect("Data not found");
746 assert_eq!(retrieved, data2);
747 });
748 }
749
750 #[test_traced]
751 fn test_archive_prune_keys() {
752 let (executor, context, _) = Executor::default();
754 executor.start(async move {
755 let journal = Journal::init(
757 context.clone(),
758 JConfig {
759 partition: "test_partition".into(),
760 },
761 )
762 .await
763 .expect("Failed to initialize journal");
764
765 let cfg = Config {
767 translator: FourCap,
768 pending_writes: 10,
769 replay_concurrency: 4,
770 compression: None,
771 section_mask: 0xffff_ffff_ffff_ffffu64, };
773 let mut archive = Archive::init(context.clone(), journal, cfg.clone())
774 .await
775 .expect("Failed to initialize archive");
776
777 let keys = vec![
779 (1u64, test_key("key1-blah"), Bytes::from("data1")),
780 (2u64, test_key("key2-blah"), Bytes::from("data2")),
781 (3u64, test_key("key3-blah"), Bytes::from("data3")),
782 (4u64, test_key("key3-bleh"), Bytes::from("data3-again")),
783 (5u64, test_key("key4-blah"), Bytes::from("data4")),
784 ];
785
786 for (index, key, data) in &keys {
787 archive
788 .put(*index, key.clone(), data.clone())
789 .await
790 .expect("Failed to put data");
791 }
792
793 let buffer = context.encode();
795 assert!(buffer.contains("items_tracked 5"));
796
797 archive.prune(3).await.expect("Failed to prune");
799
800 for (index, key, data) in keys {
802 let retrieved = archive
803 .get(Identifier::Key(&key))
804 .await
805 .expect("Failed to get data");
806 if index < 3 {
807 assert!(retrieved.is_none());
808 } else {
809 assert_eq!(retrieved.expect("Data not found"), data);
810 }
811 }
812
813 let buffer = context.encode();
815 assert!(buffer.contains("items_tracked 3"));
816 assert!(buffer.contains("indices_pruned_total 2"));
817 assert!(buffer.contains("keys_pruned_total 0")); archive.prune(2).await.expect("Failed to prune");
821
822 archive.prune(3).await.expect("Failed to prune");
824
825 let result = archive
827 .put(1, test_key("key1-blah"), Bytes::from("data1"))
828 .await;
829 assert!(matches!(result, Err(Error::AlreadyPrunedTo(3))));
830
831 archive
833 .put(6, test_key("key2-blfh"), Bytes::from("data2-2"))
834 .await
835 .expect("Failed to put data");
836
837 let buffer = context.encode();
839 assert!(buffer.contains("items_tracked 4")); assert!(buffer.contains("indices_pruned_total 2"));
841 assert!(buffer.contains("keys_pruned_total 1"));
842 });
843 }
844
845 fn test_archive_keys_and_restart(num_keys: usize) -> String {
846 let (executor, mut context, auditor) = Executor::default();
848 executor.start(async move {
849 let journal = Journal::init(
851 context.clone(),
852 JConfig {
853 partition: "test_partition".into(),
854 },
855 )
856 .await
857 .expect("Failed to initialize journal");
858
859 let section_mask = 0xffff_ffff_ffff_ff00u64;
861 let cfg = Config {
862 translator: TwoCap,
863 pending_writes: 10,
864 replay_concurrency: 4,
865 compression: None,
866 section_mask,
867 };
868 let mut archive = Archive::init(context.clone(), journal, cfg.clone())
869 .await
870 .expect("Failed to initialize archive");
871
872 let mut keys = BTreeMap::new();
874 while keys.len() < num_keys {
875 let index = keys.len() as u64;
876 let mut key = [0u8; 64];
877 context.fill(&mut key);
878 let key = FixedBytes::<64>::try_from(&key[..]).unwrap();
879 let mut data = [0u8; 1024];
880 context.fill(&mut data);
881 let data = Bytes::from(data.to_vec());
882 archive
883 .put(index, key.clone(), data.clone())
884 .await
885 .expect("Failed to put data");
886 keys.insert(key, (index, data));
887 }
888
889 for (key, (index, data)) in &keys {
891 let retrieved = archive
892 .get(Identifier::Index(*index))
893 .await
894 .expect("Failed to get data")
895 .expect("Data not found");
896 assert_eq!(retrieved, data);
897 let retrieved = archive
898 .get(Identifier::Key(key))
899 .await
900 .expect("Failed to get data")
901 .expect("Data not found");
902 assert_eq!(retrieved, data);
903 }
904
905 let buffer = context.encode();
907 let tracked = format!("items_tracked {:?}", num_keys);
908 assert!(buffer.contains(&tracked));
909 assert!(buffer.contains("keys_pruned_total 0"));
910
911 archive.close().await.expect("Failed to close archive");
913
914 let journal = Journal::init(
916 context.clone(),
917 JConfig {
918 partition: "test_partition".into(),
919 },
920 )
921 .await
922 .expect("Failed to initialize journal");
923 let cfg = Config {
924 translator: TwoCap,
925 pending_writes: 10,
926 replay_concurrency: 4,
927 compression: None,
928 section_mask,
929 };
930 let mut archive = Archive::init(context.clone(), journal, cfg.clone())
931 .await
932 .expect("Failed to initialize archive");
933
934 for (key, (index, data)) in &keys {
936 let retrieved = archive
937 .get(Identifier::Index(*index))
938 .await
939 .expect("Failed to get data")
940 .expect("Data not found");
941 assert_eq!(retrieved, data);
942 let retrieved = archive
943 .get(Identifier::Key(key))
944 .await
945 .expect("Failed to get data")
946 .expect("Data not found");
947 assert_eq!(retrieved, data);
948 }
949
950 let min = (keys.len() / 2) as u64;
952 archive.prune(min).await.expect("Failed to prune");
953
954 let min = min & section_mask;
956 let mut removed = 0;
957 for (key, (index, data)) in keys {
958 if index >= min {
959 let retrieved = archive
960 .get(Identifier::Key(&key))
961 .await
962 .expect("Failed to get data")
963 .expect("Data not found");
964 assert_eq!(retrieved, data);
965
966 let (current_end, start_next) = archive.next_gap(index);
968 assert_eq!(current_end.unwrap(), num_keys as u64 - 1);
969 assert!(start_next.is_none());
970 } else {
971 let retrieved = archive
972 .get(Identifier::Key(&key))
973 .await
974 .expect("Failed to get data");
975 assert!(retrieved.is_none());
976 removed += 1;
977
978 let (current_end, start_next) = archive.next_gap(index);
980 assert!(current_end.is_none());
981 assert_eq!(start_next.unwrap(), min);
982 }
983 }
984
985 let buffer = context.encode();
987 let tracked = format!("items_tracked {:?}", num_keys - removed);
988 assert!(buffer.contains(&tracked));
989 let pruned = format!("indices_pruned_total {}", removed);
990 assert!(buffer.contains(&pruned));
991 assert!(buffer.contains("keys_pruned_total 0")); });
993 auditor.state()
994 }
995
996 #[test_traced]
997 fn test_archive_many_keys_and_restart() {
998 test_archive_keys_and_restart(100_000); }
1000
1001 #[test_traced]
1002 fn test_determinism() {
1003 let state1 = test_archive_keys_and_restart(5_000); let state2 = test_archive_keys_and_restart(5_000);
1005 assert_eq!(state1, state2);
1006 }
1007
1008 #[test_traced]
1009 fn test_ranges() {
1010 let (executor, context, _) = Executor::default();
1012 executor.start(async move {
1013 let journal = Journal::init(
1015 context.clone(),
1016 JConfig {
1017 partition: "test_partition".into(),
1018 },
1019 )
1020 .await
1021 .expect("Failed to initialize journal");
1022
1023 let cfg = Config {
1025 translator: FourCap,
1026 pending_writes: 10,
1027 replay_concurrency: 4,
1028 compression: None,
1029 section_mask: DEFAULT_SECTION_MASK,
1030 };
1031 let mut archive = Archive::init(context.clone(), journal, cfg.clone())
1032 .await
1033 .expect("Failed to initialize archive");
1034
1035 let keys = vec![
1037 (1u64, test_key("key1-blah"), Bytes::from("data1")),
1038 (10u64, test_key("key2-blah"), Bytes::from("data2")),
1039 (11u64, test_key("key3-blah"), Bytes::from("data3")),
1040 (14u64, test_key("key3-bleh"), Bytes::from("data3-again")),
1041 ];
1042 for (index, key, data) in &keys {
1043 archive
1044 .put(*index, key.clone(), data.clone())
1045 .await
1046 .expect("Failed to put data");
1047 }
1048
1049 let (current_end, start_next) = archive.next_gap(0);
1051 assert!(current_end.is_none());
1052 assert_eq!(start_next.unwrap(), 1);
1053
1054 let (current_end, start_next) = archive.next_gap(1);
1055 assert_eq!(current_end.unwrap(), 1);
1056 assert_eq!(start_next.unwrap(), 10);
1057
1058 let (current_end, start_next) = archive.next_gap(10);
1059 assert_eq!(current_end.unwrap(), 11);
1060 assert_eq!(start_next.unwrap(), 14);
1061
1062 let (current_end, start_next) = archive.next_gap(11);
1063 assert_eq!(current_end.unwrap(), 11);
1064 assert_eq!(start_next.unwrap(), 14);
1065
1066 let (current_end, start_next) = archive.next_gap(12);
1067 assert!(current_end.is_none());
1068 assert_eq!(start_next.unwrap(), 14);
1069
1070 let (current_end, start_next) = archive.next_gap(14);
1071 assert_eq!(current_end.unwrap(), 14);
1072 assert!(start_next.is_none());
1073
1074 archive.close().await.expect("Failed to close archive");
1076
1077 let journal = Journal::init(
1078 context.clone(),
1079 JConfig {
1080 partition: "test_partition".into(),
1081 },
1082 )
1083 .await
1084 .expect("Failed to initialize journal");
1085 let archive = Archive::<_, FixedBytes<64>, _, _>::init(context, journal, cfg.clone())
1086 .await
1087 .expect("Failed to initialize archive");
1088
1089 let (current_end, start_next) = archive.next_gap(0);
1091 assert!(current_end.is_none());
1092 assert_eq!(start_next.unwrap(), 1);
1093
1094 let (current_end, start_next) = archive.next_gap(1);
1095 assert_eq!(current_end.unwrap(), 1);
1096 assert_eq!(start_next.unwrap(), 10);
1097
1098 let (current_end, start_next) = archive.next_gap(10);
1099 assert_eq!(current_end.unwrap(), 11);
1100 assert_eq!(start_next.unwrap(), 14);
1101
1102 let (current_end, start_next) = archive.next_gap(11);
1103 assert_eq!(current_end.unwrap(), 11);
1104 assert_eq!(start_next.unwrap(), 14);
1105
1106 let (current_end, start_next) = archive.next_gap(12);
1107 assert!(current_end.is_none());
1108 assert_eq!(start_next.unwrap(), 14);
1109
1110 let (current_end, start_next) = archive.next_gap(14);
1111 assert_eq!(current_end.unwrap(), 14);
1112 assert!(start_next.is_none());
1113 });
1114 }
1115}