11use crate :: ctx:: DreContext ;
22use ic_canisters:: IcAgentCanisterClient ;
33use ic_canisters:: governance:: GovernanceCanisterWrapper ;
4+ use ic_registry_common_proto:: pb:: local_store:: v1:: ChangelogEntry ;
45use ic_management_backend:: health:: HealthStatusQuerier ;
56use ic_management_backend:: lazy_registry:: LazyRegistry ;
67use ic_management_types:: { HealthStatus , Network } ;
@@ -20,6 +21,7 @@ use prost::Message;
2021use serde:: Serialize ;
2122use std:: {
2223 collections:: { BTreeMap , HashMap } ,
24+ ffi:: OsStr ,
2325 net:: Ipv6Addr ,
2426 str:: FromStr ,
2527 sync:: Arc ,
@@ -259,7 +261,7 @@ async fn get_node_operators(local_registry: &Arc<dyn LazyRegistry>, network: &Ne
259261
260262pub ( crate ) async fn get_sorted_versions_from_local (
261263 ctx : & DreContext ,
262- ) -> anyhow:: Result < ( Vec < u64 > , Vec < ( u64 , ic_registry_common_proto :: pb :: local_store :: v1 :: ChangelogEntry ) > ) > {
264+ ) -> anyhow:: Result < ( Vec < u64 > , Vec < ( u64 , ChangelogEntry ) > ) > {
263265 let base_dirs = get_dirs_from_ctx ( ctx) ?;
264266
265267 let entries = load_first_available_entries ( & base_dirs) ?;
@@ -522,18 +524,15 @@ async fn _get_nodes(
522524
523525pub ( crate ) fn load_first_available_entries (
524526 base_dirs : & [ std:: path:: PathBuf ] ,
525- ) -> anyhow:: Result < Vec < ( u64 , ic_registry_common_proto:: pb:: local_store:: v1:: ChangelogEntry ) > > {
526- use ic_registry_common_proto:: pb:: local_store:: v1:: ChangelogEntry as PbChangelogEntry ;
527- use std:: ffi:: OsStr ;
528-
529- let mut entries: Vec < ( u64 , PbChangelogEntry ) > = Vec :: new ( ) ;
527+ ) -> anyhow:: Result < Vec < ( u64 , ChangelogEntry ) > > {
528+ let mut entries: Vec < ( u64 , ChangelogEntry ) > = Vec :: new ( ) ;
530529 for base_dir in base_dirs. iter ( ) {
531- let mut local: Vec < ( u64 , PbChangelogEntry ) > = Vec :: new ( ) ;
530+ let mut local: Vec < ( u64 , ChangelogEntry ) > = Vec :: new ( ) ;
532531 collect_pb_files ( base_dir, & mut |path| {
533532 if path. extension ( ) == Some ( OsStr :: new ( "pb" ) ) {
534533 if let Some ( v) = extract_version_from_registry_path ( base_dir, path) {
535534 let bytes = std:: fs:: read ( path) . unwrap_or_else ( |_| panic ! ( "Failed reading {}" , path. display( ) ) ) ;
536- let entry = PbChangelogEntry :: decode ( bytes. as_slice ( ) ) . unwrap_or_else ( |_| panic ! ( "Failed decoding {}" , path. display( ) ) ) ;
535+ let entry = ChangelogEntry :: decode ( bytes. as_slice ( ) ) . unwrap_or_else ( |_| panic ! ( "Failed decoding {}" , path. display( ) ) ) ;
537536 local. push ( ( v, entry) ) ;
538537 }
539538 }
@@ -589,8 +588,327 @@ fn extract_version_from_registry_path(base_dir: &std::path::Path, full_path: &st
589588
590589#[ cfg( test) ]
591590mod test {
592- use super :: extract_version_from_registry_path ;
591+ use super :: * ;
593592 use std:: path:: PathBuf ;
593+ use std:: collections:: HashSet ;
594+ use std:: time:: { SystemTime , UNIX_EPOCH } ;
595+ use ic_registry_common_proto:: pb:: local_store:: v1:: { ChangelogEntry , KeyMutation , MutationType } ;
596+ use prost:: Message ;
597+
598+ #[ test]
599+ fn test_load_first_available_entries ( ) {
600+ struct TestCase {
601+ description : String ,
602+ setup : Box < dyn Fn ( & PathBuf ) -> Vec < PathBuf > > ,
603+ expected_result : Result < usize , bool > , // Ok(count) or Err(should_error)
604+ }
605+
606+ // Generate unique test directory name
607+ let test_id = SystemTime :: now ( )
608+ . duration_since ( UNIX_EPOCH )
609+ . unwrap ( )
610+ . as_nanos ( ) ;
611+ let base_test_dir = PathBuf :: from ( format ! ( "/tmp/dre_test_load_entries_{}" , test_id) ) ;
612+
613+ let test_cases = vec ! [
614+ TestCase {
615+ description: "all directories empty" . to_string( ) ,
616+ setup: Box :: new( |base| {
617+ let dir1 = base. join( "dir1" ) ;
618+ let dir2 = base. join( "dir2" ) ;
619+ fs_err:: create_dir_all( & dir1) . unwrap( ) ;
620+ fs_err:: create_dir_all( & dir2) . unwrap( ) ;
621+ vec![ dir1, dir2]
622+ } ) ,
623+ expected_result: Err ( true ) , // Should error
624+ } ,
625+ TestCase {
626+ description: "multiple directories, first empty, second has entries" . to_string( ) ,
627+ setup: Box :: new( |base| {
628+ let dir1 = base. join( "dir1" ) ;
629+ let dir2 = base. join( "dir2" ) ;
630+ fs_err:: create_dir_all( & dir1) . unwrap( ) ;
631+ fs_err:: create_dir_all( & dir2) . unwrap( ) ;
632+ let entry = ChangelogEntry {
633+ key_mutations: vec![ KeyMutation {
634+ key: "test_key" . to_string( ) ,
635+ value: b"test_value" . to_vec( ) ,
636+ mutation_type: MutationType :: Set as i32 ,
637+ } ] ,
638+ } ;
639+ let hex_str = format!( "{:019x}" , 1 ) ;
640+ let dir_path = dir2
641+ . join( & hex_str[ 0 ..10 ] )
642+ . join( & hex_str[ 10 ..12 ] )
643+ . join( & hex_str[ 12 ..14 ] ) ;
644+ fs_err:: create_dir_all( & dir_path) . unwrap( ) ;
645+ let file_path = dir_path. join( format!( "{}.pb" , & hex_str[ 14 ..] ) ) ;
646+ fs_err:: write( & file_path, entry. encode_to_vec( ) ) . unwrap( ) ;
647+ vec![ dir1, dir2]
648+ } ) ,
649+ expected_result: Ok ( 1 ) ,
650+ } ,
651+ TestCase {
652+ description: "multiple directories, first has entries, second ignored" . to_string( ) ,
653+ setup: Box :: new( |base| {
654+ let dir1 = base. join( "dir1" ) ;
655+ let dir2 = base. join( "dir2" ) ;
656+ fs_err:: create_dir_all( & dir1) . unwrap( ) ;
657+ fs_err:: create_dir_all( & dir2) . unwrap( ) ;
658+ let entry1 = ChangelogEntry {
659+ key_mutations: vec![ KeyMutation {
660+ key: "test_key1" . to_string( ) ,
661+ value: b"test_value1" . to_vec( ) ,
662+ mutation_type: MutationType :: Set as i32 ,
663+ } ] ,
664+ } ;
665+ let entry2 = ChangelogEntry {
666+ key_mutations: vec![ KeyMutation {
667+ key: "test_key2" . to_string( ) ,
668+ value: b"test_value2" . to_vec( ) ,
669+ mutation_type: MutationType :: Set as i32 ,
670+ } ] ,
671+ } ;
672+ // Create entry in dir1
673+ let hex_str1 = format!( "{:019x}" , 1 ) ;
674+ let dir_path1 = dir1
675+ . join( & hex_str1[ 0 ..10 ] )
676+ . join( & hex_str1[ 10 ..12 ] )
677+ . join( & hex_str1[ 12 ..14 ] ) ;
678+ fs_err:: create_dir_all( & dir_path1) . unwrap( ) ;
679+ let file_path1 = dir_path1. join( format!( "{}.pb" , & hex_str1[ 14 ..] ) ) ;
680+ fs_err:: write( & file_path1, entry1. encode_to_vec( ) ) . unwrap( ) ;
681+ // Create entry in dir2 (should be ignored)
682+ let hex_str2 = format!( "{:019x}" , 2 ) ;
683+ let dir_path2 = dir2
684+ . join( & hex_str2[ 0 ..10 ] )
685+ . join( & hex_str2[ 10 ..12 ] )
686+ . join( & hex_str2[ 12 ..14 ] ) ;
687+ fs_err:: create_dir_all( & dir_path2) . unwrap( ) ;
688+ let file_path2 = dir_path2. join( format!( "{}.pb" , & hex_str2[ 14 ..] ) ) ;
689+ fs_err:: write( & file_path2, entry2. encode_to_vec( ) ) . unwrap( ) ;
690+ vec![ dir1, dir2]
691+ } ) ,
692+ expected_result: Ok ( 1 ) , // Should only return entries from dir1
693+ } ,
694+ ] ;
695+
696+ // Cleanup function
697+ let cleanup = |path : & PathBuf | {
698+ if path. exists ( ) {
699+ let _ = fs_err:: remove_dir_all ( path) ;
700+ }
701+ } ;
702+
703+ for test_case in test_cases {
704+ let base_dirs = ( test_case. setup ) ( & base_test_dir) ;
705+
706+ let result = load_first_available_entries ( & base_dirs) ;
707+
708+ match test_case. expected_result {
709+ Ok ( expected_count) => {
710+ assert ! (
711+ result. is_ok( ) ,
712+ "{}: load_first_available_entries should succeed" ,
713+ test_case. description
714+ ) ;
715+ let entries = result. unwrap ( ) ;
716+ assert_eq ! (
717+ entries. len( ) ,
718+ expected_count,
719+ "{}: should load {} entries, got {}" ,
720+ test_case. description,
721+ expected_count,
722+ entries. len( )
723+ ) ;
724+ }
725+ Err ( should_error) => {
726+ if should_error {
727+ assert ! (
728+ result. is_err( ) ,
729+ "{}: load_first_available_entries should return error" ,
730+ test_case. description
731+ ) ;
732+ } else {
733+ assert ! (
734+ result. is_ok( ) ,
735+ "{}: load_first_available_entries should succeed" ,
736+ test_case. description
737+ ) ;
738+ }
739+ }
740+ }
741+
742+ // Cleanup after each test case
743+ for dir in & base_dirs {
744+ cleanup ( dir) ;
745+ }
746+ }
747+
748+ // Final cleanup of base test directory
749+ cleanup ( & base_test_dir) ;
750+ }
751+
752+ #[ test]
753+ fn test_collect_pb_files ( ) {
754+ use std:: time:: { SystemTime , UNIX_EPOCH } ;
755+
756+ struct TestCase {
757+ description : String ,
758+ setup : Box < dyn Fn ( & PathBuf ) -> ( PathBuf , HashSet < PathBuf > ) > ,
759+ expected_count : usize ,
760+ }
761+
762+ // Generate unique test directory name
763+ let test_id = SystemTime :: now ( )
764+ . duration_since ( UNIX_EPOCH )
765+ . unwrap ( )
766+ . as_nanos ( ) ;
767+ let base_test_dir = PathBuf :: from ( format ! ( "/tmp/dre_test_collect_pb_{}" , test_id) ) ;
768+
769+ let test_cases = vec ! [
770+ TestCase {
771+ description: "empty directory" . to_string( ) ,
772+ setup: Box :: new( |base| {
773+ let test_dir = base. join( "empty" ) ;
774+ fs_err:: create_dir_all( & test_dir) . unwrap( ) ;
775+ ( test_dir, HashSet :: new( ) )
776+ } ) ,
777+ expected_count: 0 ,
778+ } ,
779+ TestCase {
780+ description: "single file in root" . to_string( ) ,
781+ setup: Box :: new( |base| {
782+ let test_dir = base. join( "single_file" ) ;
783+ fs_err:: create_dir_all( & test_dir) . unwrap( ) ;
784+ let file1 = test_dir. join( "file1.pb" ) ;
785+ fs_err:: write( & file1, b"test" ) . unwrap( ) ;
786+ let mut expected = HashSet :: new( ) ;
787+ expected. insert( file1. clone( ) ) ;
788+ ( test_dir, expected)
789+ } ) ,
790+ expected_count: 1 ,
791+ } ,
792+ TestCase {
793+ description: "multiple files in root" . to_string( ) ,
794+ setup: Box :: new( |base| {
795+ let test_dir = base. join( "multiple_files" ) ;
796+ fs_err:: create_dir_all( & test_dir) . unwrap( ) ;
797+ let file1 = test_dir. join( "file1.pb" ) ;
798+ let file2 = test_dir. join( "file2.pb" ) ;
799+ let file3 = test_dir. join( "file3.txt" ) ;
800+ fs_err:: write( & file1, b"test1" ) . unwrap( ) ;
801+ fs_err:: write( & file2, b"test2" ) . unwrap( ) ;
802+ fs_err:: write( & file3, b"test3" ) . unwrap( ) ;
803+ let mut expected = HashSet :: new( ) ;
804+ expected. insert( file1. clone( ) ) ;
805+ expected. insert( file2. clone( ) ) ;
806+ expected. insert( file3. clone( ) ) ;
807+ ( test_dir, expected)
808+ } ) ,
809+ expected_count: 3 ,
810+ } ,
811+ TestCase {
812+ description: "nested directory structure" . to_string( ) ,
813+ setup: Box :: new( |base| {
814+ let test_dir = base. join( "nested" ) ;
815+ let subdir = test_dir. join( "subdir" ) ;
816+ fs_err:: create_dir_all( & subdir) . unwrap( ) ;
817+ let file1 = test_dir. join( "file1.pb" ) ;
818+ let file2 = subdir. join( "file2.pb" ) ;
819+ let file3 = subdir. join( "file3.pb" ) ;
820+ fs_err:: write( & file1, b"test1" ) . unwrap( ) ;
821+ fs_err:: write( & file2, b"test2" ) . unwrap( ) ;
822+ fs_err:: write( & file3, b"test3" ) . unwrap( ) ;
823+ let mut expected = HashSet :: new( ) ;
824+ expected. insert( file1. clone( ) ) ;
825+ expected. insert( file2. clone( ) ) ;
826+ expected. insert( file3. clone( ) ) ;
827+ ( test_dir, expected)
828+ } ) ,
829+ expected_count: 3 ,
830+ } ,
831+ TestCase {
832+ description: "deeply nested directory structure" . to_string( ) ,
833+ setup: Box :: new( |base| {
834+ let test_dir = base. join( "deeply_nested" ) ;
835+ let deep_dir = test_dir. join( "level1" ) . join( "level2" ) . join( "level3" ) ;
836+ fs_err:: create_dir_all( & deep_dir) . unwrap( ) ;
837+ let file1 = test_dir. join( "file1.pb" ) ;
838+ let file2 = deep_dir. join( "file2.pb" ) ;
839+ fs_err:: write( & file1, b"test1" ) . unwrap( ) ;
840+ fs_err:: write( & file2, b"test2" ) . unwrap( ) ;
841+ let mut expected = HashSet :: new( ) ;
842+ expected. insert( file1. clone( ) ) ;
843+ expected. insert( file2. clone( ) ) ;
844+ ( test_dir, expected)
845+ } ) ,
846+ expected_count: 2 ,
847+ } ,
848+ TestCase {
849+ description: "non-existent directory" . to_string( ) ,
850+ setup: Box :: new( |base| {
851+ let test_dir = base. join( "non_existent" ) ;
852+ ( test_dir, HashSet :: new( ) )
853+ } ) ,
854+ expected_count: 0 ,
855+ } ,
856+ TestCase {
857+ description: "directory with only subdirectories (no files)" . to_string( ) ,
858+ setup: Box :: new( |base| {
859+ let test_dir = base. join( "only_subdirs" ) ;
860+ let subdir1 = test_dir. join( "subdir1" ) ;
861+ let subdir2 = test_dir. join( "subdir2" ) ;
862+ fs_err:: create_dir_all( & subdir1) . unwrap( ) ;
863+ fs_err:: create_dir_all( & subdir2) . unwrap( ) ;
864+ ( test_dir, HashSet :: new( ) )
865+ } ) ,
866+ expected_count: 0 ,
867+ } ,
868+ ] ;
869+
870+ // Cleanup function
871+ let cleanup = |path : & PathBuf | {
872+ if path. exists ( ) {
873+ let _ = fs_err:: remove_dir_all ( path) ;
874+ }
875+ } ;
876+
877+ for test_case in test_cases {
878+ let ( test_dir, expected_files) = ( test_case. setup ) ( & base_test_dir) ;
879+ let base_path = test_dir. clone ( ) ;
880+
881+ let mut collected_files = HashSet :: new ( ) ;
882+ let result = collect_pb_files ( & base_path, & mut |path| {
883+ collected_files. insert ( path. to_path_buf ( ) ) ;
884+ } ) ;
885+
886+ assert ! ( result. is_ok( ) , "{}: collect_pb_files should succeed" , test_case. description) ;
887+ assert_eq ! (
888+ collected_files. len( ) ,
889+ test_case. expected_count,
890+ "{}: should collect {} files, got {}" ,
891+ test_case. description,
892+ test_case. expected_count,
893+ collected_files. len( )
894+ ) ;
895+
896+ for expected_file in & expected_files {
897+ assert ! (
898+ collected_files. contains( expected_file) ,
899+ "{}: should collect {:?}" ,
900+ test_case. description,
901+ expected_file
902+ ) ;
903+ }
904+
905+ // Cleanup after each test case
906+ cleanup ( & test_dir) ;
907+ }
908+
909+ // Final cleanup of base test directory
910+ cleanup ( & base_test_dir) ;
911+ }
594912
595913 #[ test]
596914 fn test_extract_version_from_registry_path ( ) {
0 commit comments