@@ -437,3 +437,294 @@ impl<ObjectID: FsVerityHashValue> FileSystem<ObjectID> {
437
437
}
438
438
}
439
439
}
440
+
441
+ #[ cfg( test) ]
442
+ mod tests {
443
+ use super :: * ;
444
+ use crate :: fsverity:: Sha256HashValue ;
445
+ use std:: cell:: RefCell ;
446
+ use std:: collections:: BTreeMap ;
447
+ use std:: ffi:: { OsStr , OsString } ;
448
+ use std:: rc:: Rc ;
449
+
450
+ // Helper to create a Stat with a specific mtime
451
+ fn stat_with_mtime ( mtime : i64 ) -> Stat {
452
+ Stat {
453
+ st_mode : 0o755 ,
454
+ st_uid : 1000 ,
455
+ st_gid : 1000 ,
456
+ st_mtim_sec : mtime,
457
+ xattrs : RefCell :: new ( BTreeMap :: new ( ) ) ,
458
+ }
459
+ }
460
+
461
+ // Helper to create a simple Leaf (e.g., an empty inline file)
462
+ fn new_leaf_file ( mtime : i64 ) -> Rc < Leaf < Sha256HashValue > > {
463
+ Rc :: new ( Leaf {
464
+ stat : stat_with_mtime ( mtime) ,
465
+ content : LeafContent :: Regular ( RegularFile :: Inline ( Box :: new ( [ ] ) ) ) ,
466
+ } )
467
+ }
468
+
469
+ // Helper to create a simple Leaf (symlink)
470
+ fn new_leaf_symlink ( target : & str , mtime : i64 ) -> Rc < Leaf < Sha256HashValue > > {
471
+ Rc :: new ( Leaf {
472
+ stat : stat_with_mtime ( mtime) ,
473
+ content : LeafContent :: Symlink ( OsString :: from ( target) . into_boxed_os_str ( ) ) ,
474
+ } )
475
+ }
476
+
477
+ // Helper to create an empty Directory Inode with a specific mtime
478
+ fn new_dir_inode ( mtime : i64 ) -> Inode < Sha256HashValue > {
479
+ Inode :: Directory ( Box :: new ( Directory {
480
+ stat : stat_with_mtime ( mtime) ,
481
+ entries : BTreeMap :: new ( ) ,
482
+ } ) )
483
+ }
484
+
485
+ // Helper to create a Directory Inode with specific stat
486
+ fn new_dir_inode_with_stat ( stat : Stat ) -> Inode < Sha256HashValue > {
487
+ Inode :: Directory ( Box :: new ( Directory {
488
+ stat,
489
+ entries : BTreeMap :: new ( ) ,
490
+ } ) )
491
+ }
492
+
493
+ #[ test]
494
+ fn test_directory_default ( ) {
495
+ let dir = Directory :: < Sha256HashValue > :: default ( ) ;
496
+ assert_eq ! ( dir. stat. st_uid, 0 ) ;
497
+ assert_eq ! ( dir. stat. st_gid, 0 ) ;
498
+ assert_eq ! ( dir. stat. st_mode, 0o555 ) ;
499
+ assert_eq ! ( dir. stat. st_mtim_sec, 0 ) ;
500
+ assert ! ( dir. stat. xattrs. borrow( ) . is_empty( ) ) ;
501
+ assert ! ( dir. entries. is_empty( ) ) ;
502
+ }
503
+
504
+ #[ test]
505
+ fn test_directory_new ( ) {
506
+ let stat = stat_with_mtime ( 123 ) ;
507
+ let dir = Directory :: < Sha256HashValue > :: new ( stat) ;
508
+ assert_eq ! ( dir. stat. st_mtim_sec, 123 ) ;
509
+ assert ! ( dir. entries. is_empty( ) ) ;
510
+ }
511
+
512
+ #[ test]
513
+ fn test_insert_and_get_leaf ( ) {
514
+ let mut dir = Directory :: < Sha256HashValue > :: default ( ) ;
515
+ let leaf = new_leaf_file ( 10 ) ;
516
+ dir. insert ( OsStr :: new ( "file.txt" ) , Inode :: Leaf ( Rc :: clone ( & leaf) ) ) ;
517
+ assert_eq ! ( dir. entries. len( ) , 1 ) ;
518
+
519
+ let retrieved_leaf_rc = dir. ref_leaf ( OsStr :: new ( "file.txt" ) ) . unwrap ( ) ;
520
+ assert ! ( Rc :: ptr_eq( & retrieved_leaf_rc, & leaf) ) ;
521
+
522
+ let regular_file_content = dir. get_file ( OsStr :: new ( "file.txt" ) ) . unwrap ( ) ;
523
+ assert ! ( matches!( regular_file_content, RegularFile :: Inline ( _) ) ) ;
524
+ }
525
+
526
+ #[ test]
527
+ fn test_insert_and_get_directory ( ) {
528
+ let mut dir = Directory :: < Sha256HashValue > :: default ( ) ;
529
+ let sub_dir_inode = new_dir_inode ( 20 ) ;
530
+ dir. insert ( OsStr :: new ( "subdir" ) , sub_dir_inode) ;
531
+ assert_eq ! ( dir. entries. len( ) , 1 ) ;
532
+
533
+ let retrieved_subdir = dir. get_directory ( OsStr :: new ( "subdir" ) ) . unwrap ( ) ;
534
+ assert_eq ! ( retrieved_subdir. stat. st_mtim_sec, 20 ) ;
535
+
536
+ let retrieved_subdir_opt = dir
537
+ . get_directory_opt ( OsStr :: new ( "subdir" ) )
538
+ . unwrap ( )
539
+ . unwrap ( ) ;
540
+ assert_eq ! ( retrieved_subdir_opt. stat. st_mtim_sec, 20 ) ;
541
+ }
542
+
543
+ #[ test]
544
+ fn test_get_directory_errors ( ) {
545
+ let mut root = Directory :: < Sha256HashValue > :: default ( ) ;
546
+ root. insert ( OsStr :: new ( "dir1" ) , new_dir_inode ( 10 ) ) ;
547
+ root. insert ( OsStr :: new ( "file1" ) , Inode :: Leaf ( new_leaf_file ( 30 ) ) ) ;
548
+
549
+ match root. get_directory ( OsStr :: new ( "nonexistent" ) ) {
550
+ Err ( ImageError :: NotFound ( name) ) => assert_eq ! ( name. to_str( ) . unwrap( ) , "nonexistent" ) ,
551
+ _ => panic ! ( "Expected NotFound" ) ,
552
+ }
553
+ assert ! ( root
554
+ . get_directory_opt( OsStr :: new( "nonexistent" ) )
555
+ . unwrap( )
556
+ . is_none( ) ) ;
557
+
558
+ match root. get_directory ( OsStr :: new ( "file1" ) ) {
559
+ Err ( ImageError :: NotADirectory ( name) ) => assert_eq ! ( name. to_str( ) . unwrap( ) , "file1" ) ,
560
+ _ => panic ! ( "Expected NotADirectory" ) ,
561
+ }
562
+ }
563
+
564
+ #[ test]
565
+ fn test_get_file_errors ( ) {
566
+ let mut dir = Directory :: < Sha256HashValue > :: default ( ) ;
567
+ dir. insert ( OsStr :: new ( "subdir" ) , new_dir_inode ( 10 ) ) ;
568
+ dir. insert (
569
+ OsStr :: new ( "link.txt" ) ,
570
+ Inode :: Leaf ( new_leaf_symlink ( "target" , 20 ) ) ,
571
+ ) ;
572
+
573
+ match dir. get_file ( OsStr :: new ( "nonexistent.txt" ) ) {
574
+ Err ( ImageError :: NotFound ( name) ) => {
575
+ assert_eq ! ( name. to_str( ) . unwrap( ) , "nonexistent.txt" )
576
+ }
577
+ _ => panic ! ( "Expected NotFound" ) ,
578
+ }
579
+ assert ! ( dir
580
+ . get_file_opt( OsStr :: new( "nonexistent.txt" ) )
581
+ . unwrap( )
582
+ . is_none( ) ) ;
583
+
584
+ match dir. get_file ( OsStr :: new ( "subdir" ) ) {
585
+ Err ( ImageError :: IsADirectory ( name) ) => assert_eq ! ( name. to_str( ) . unwrap( ) , "subdir" ) ,
586
+ _ => panic ! ( "Expected IsADirectory" ) ,
587
+ }
588
+ match dir. get_file ( OsStr :: new ( "link.txt" ) ) {
589
+ Err ( ImageError :: IsNotRegular ( name) ) => assert_eq ! ( name. to_str( ) . unwrap( ) , "link.txt" ) ,
590
+ res => panic ! ( "Expected IsNotRegular, got {:?}" , res) ,
591
+ }
592
+ }
593
+
594
+ #[ test]
595
+ fn test_remove ( ) {
596
+ let mut dir = Directory :: < Sha256HashValue > :: default ( ) ;
597
+ dir. insert ( OsStr :: new ( "file1.txt" ) , Inode :: Leaf ( new_leaf_file ( 10 ) ) ) ;
598
+ dir. insert ( OsStr :: new ( "subdir" ) , new_dir_inode ( 20 ) ) ;
599
+ assert_eq ! ( dir. entries. len( ) , 2 ) ;
600
+
601
+ dir. remove ( OsStr :: new ( "file1.txt" ) ) ;
602
+ assert_eq ! ( dir. entries. len( ) , 1 ) ;
603
+ assert ! ( dir. entries. get( OsStr :: new( "file1.txt" ) ) . is_none( ) ) ;
604
+
605
+ dir. remove ( OsStr :: new ( "nonexistent" ) ) ; // Should be no-op
606
+ assert_eq ! ( dir. entries. len( ) , 1 ) ;
607
+ }
608
+
609
+ #[ test]
610
+ fn test_merge ( ) {
611
+ let mut dir = Directory :: < Sha256HashValue > :: default ( ) ;
612
+
613
+ // Merge Leaf onto empty
614
+ dir. merge ( OsStr :: new ( "item" ) , Inode :: Leaf ( new_leaf_file ( 10 ) ) ) ;
615
+ assert_eq ! (
616
+ dir. entries
617
+ . get( OsStr :: new( "item" ) )
618
+ . unwrap( )
619
+ . stat( )
620
+ . st_mtim_sec,
621
+ 10
622
+ ) ;
623
+
624
+ // Merge Directory onto existing Directory
625
+ let mut existing_dir_inode = new_dir_inode_with_stat ( stat_with_mtime ( 80 ) ) ;
626
+ if let Inode :: Directory ( ref mut ed_box) = existing_dir_inode {
627
+ ed_box. insert ( OsStr :: new ( "inner_file" ) , Inode :: Leaf ( new_leaf_file ( 85 ) ) ) ;
628
+ }
629
+ dir. insert ( OsStr :: new ( "merged_dir" ) , existing_dir_inode) ;
630
+
631
+ let new_merging_dir_inode = new_dir_inode_with_stat ( stat_with_mtime ( 90 ) ) ;
632
+ dir. merge ( OsStr :: new ( "merged_dir" ) , new_merging_dir_inode) ;
633
+
634
+ match dir. entries . get ( OsStr :: new ( "merged_dir" ) ) {
635
+ Some ( Inode :: Directory ( d) ) => {
636
+ assert_eq ! ( d. stat. st_mtim_sec, 90 ) ; // Stat updated
637
+ assert_eq ! ( d. entries. len( ) , 1 ) ; // Inner file preserved
638
+ assert ! ( d. entries. get( OsStr :: new( "inner_file" ) ) . is_some( ) ) ;
639
+ }
640
+ _ => panic ! ( "Expected directory after merge" ) ,
641
+ }
642
+
643
+ // Merge Leaf onto Directory (replaces)
644
+ dir. merge ( OsStr :: new ( "merged_dir" ) , Inode :: Leaf ( new_leaf_file ( 100 ) ) ) ;
645
+ assert ! ( matches!(
646
+ dir. entries. get( OsStr :: new( "merged_dir" ) ) ,
647
+ Some ( Inode :: Leaf ( _) )
648
+ ) ) ;
649
+ assert_eq ! (
650
+ dir. entries
651
+ . get( OsStr :: new( "merged_dir" ) )
652
+ . unwrap( )
653
+ . stat( )
654
+ . st_mtim_sec,
655
+ 100
656
+ ) ;
657
+ }
658
+
659
+ #[ test]
660
+ fn test_clear ( ) {
661
+ let mut dir = Directory :: < Sha256HashValue > :: default ( ) ;
662
+ dir. insert ( OsStr :: new ( "file1" ) , Inode :: Leaf ( new_leaf_file ( 10 ) ) ) ;
663
+ dir. stat . st_mtim_sec = 100 ;
664
+
665
+ dir. clear ( ) ;
666
+ assert ! ( dir. entries. is_empty( ) ) ;
667
+ assert_eq ! ( dir. stat. st_mtim_sec, 100 ) ; // Stat should be unmodified
668
+ }
669
+
670
+ #[ test]
671
+ fn test_newest_file ( ) {
672
+ let mut root = Directory :: < Sha256HashValue > :: new ( stat_with_mtime ( 5 ) ) ;
673
+ assert_eq ! ( root. newest_file( ) , 5 ) ;
674
+
675
+ root. insert ( OsStr :: new ( "file1" ) , Inode :: Leaf ( new_leaf_file ( 10 ) ) ) ;
676
+ assert_eq ! ( root. newest_file( ) , 10 ) ;
677
+
678
+ let subdir_stat = stat_with_mtime ( 15 ) ;
679
+ let mut subdir = Box :: new ( Directory :: new ( subdir_stat) ) ;
680
+ subdir. insert ( OsStr :: new ( "subfile1" ) , Inode :: Leaf ( new_leaf_file ( 12 ) ) ) ;
681
+ root. insert ( OsStr :: new ( "subdir" ) , Inode :: Directory ( subdir) ) ;
682
+ assert_eq ! ( root. newest_file( ) , 15 ) ;
683
+
684
+ if let Some ( Inode :: Directory ( sd) ) = root. entries . get_mut ( OsStr :: new ( "subdir" ) ) {
685
+ sd. insert ( OsStr :: new ( "subfile2" ) , Inode :: Leaf ( new_leaf_file ( 20 ) ) ) ;
686
+ }
687
+ assert_eq ! ( root. newest_file( ) , 20 ) ;
688
+
689
+ root. stat . st_mtim_sec = 25 ;
690
+ assert_eq ! ( root. newest_file( ) , 25 ) ;
691
+ }
692
+
693
+ #[ test]
694
+ fn test_iteration_entries_sorted_inodes ( ) {
695
+ let mut dir = Directory :: < Sha256HashValue > :: default ( ) ;
696
+ dir. insert ( OsStr :: new ( "b_file" ) , Inode :: Leaf ( new_leaf_file ( 10 ) ) ) ;
697
+ dir. insert ( OsStr :: new ( "a_dir" ) , new_dir_inode ( 20 ) ) ;
698
+ dir. insert (
699
+ OsStr :: new ( "c_link" ) ,
700
+ Inode :: Leaf ( new_leaf_symlink ( "target" , 30 ) ) ,
701
+ ) ;
702
+
703
+ let names_from_entries: Vec < & OsStr > = dir. entries ( ) . map ( |( name, _) | name) . collect ( ) ;
704
+ assert_eq ! ( names_from_entries. len( ) , 3 ) ; // BTreeMap iter is sorted
705
+ assert ! ( names_from_entries. contains( & OsStr :: new( "a_dir" ) ) ) ;
706
+ assert ! ( names_from_entries. contains( & OsStr :: new( "b_file" ) ) ) ;
707
+ assert ! ( names_from_entries. contains( & OsStr :: new( "c_link" ) ) ) ;
708
+
709
+ let sorted_names: Vec < & OsStr > = dir. sorted_entries ( ) . map ( |( name, _) | name) . collect ( ) ;
710
+ assert_eq ! (
711
+ sorted_names,
712
+ vec![
713
+ OsStr :: new( "a_dir" ) ,
714
+ OsStr :: new( "b_file" ) ,
715
+ OsStr :: new( "c_link" )
716
+ ]
717
+ ) ;
718
+
719
+ let mut inode_types = vec ! [ ] ;
720
+ for inode in dir. inodes ( ) {
721
+ match inode {
722
+ Inode :: Directory ( _) => inode_types. push ( "dir" ) ,
723
+ Inode :: Leaf ( _) => inode_types. push ( "leaf" ) ,
724
+ }
725
+ }
726
+ assert_eq ! ( inode_types. len( ) , 3 ) ;
727
+ assert_eq ! ( inode_types. iter( ) . filter( |&&t| t == "dir" ) . count( ) , 1 ) ;
728
+ assert_eq ! ( inode_types. iter( ) . filter( |&&t| t == "leaf" ) . count( ) , 2 ) ;
729
+ }
730
+ }
0 commit comments