migration.rs (5730B)
1 use std::path::PathBuf; 2 3 pub const RADROOTS_MIGRATION_POSTURE: &str = "explicit_operator_import_required"; 4 pub const RADROOTS_MIGRATION_COMPATIBILITY_WINDOW: &str = "detect_and_report_only"; 5 6 #[derive(Debug, Clone, PartialEq, Eq)] 7 pub struct RadrootsLegacyPathCandidate { 8 pub id: String, 9 pub description: String, 10 pub path: PathBuf, 11 pub destination: Option<PathBuf>, 12 pub import_hint: String, 13 } 14 15 #[derive(Debug, Clone, PartialEq, Eq)] 16 pub struct RadrootsLegacyPathDetection { 17 pub id: String, 18 pub description: String, 19 pub path: PathBuf, 20 pub destination: Option<PathBuf>, 21 pub import_hint: String, 22 } 23 24 #[derive(Debug, Clone, PartialEq, Eq)] 25 pub struct RadrootsMigrationReport { 26 pub posture: &'static str, 27 pub state: &'static str, 28 pub silent_startup_relocation: bool, 29 pub compatibility_window: &'static str, 30 pub detected_legacy_paths: Vec<RadrootsLegacyPathDetection>, 31 } 32 33 impl RadrootsLegacyPathCandidate { 34 #[must_use] 35 pub fn new( 36 id: impl Into<String>, 37 description: impl Into<String>, 38 path: impl Into<PathBuf>, 39 destination: Option<PathBuf>, 40 import_hint: impl Into<String>, 41 ) -> Self { 42 Self { 43 id: id.into(), 44 description: description.into(), 45 path: path.into(), 46 destination, 47 import_hint: import_hint.into(), 48 } 49 } 50 51 fn into_detection(self) -> RadrootsLegacyPathDetection { 52 RadrootsLegacyPathDetection { 53 id: self.id, 54 description: self.description, 55 path: self.path, 56 destination: self.destination, 57 import_hint: self.import_hint, 58 } 59 } 60 } 61 62 impl RadrootsMigrationReport { 63 #[must_use] 64 pub fn empty() -> Self { 65 Self::from_detected_legacy_paths(Vec::new()) 66 } 67 68 #[must_use] 69 pub fn from_detected_legacy_paths( 70 detected_legacy_paths: Vec<RadrootsLegacyPathDetection>, 71 ) -> Self { 72 let state = if detected_legacy_paths.is_empty() { 73 "ready" 74 } else { 75 "legacy_state_detected" 76 }; 77 Self { 78 posture: RADROOTS_MIGRATION_POSTURE, 79 state, 80 silent_startup_relocation: false, 81 compatibility_window: RADROOTS_MIGRATION_COMPATIBILITY_WINDOW, 82 detected_legacy_paths, 83 } 84 } 85 } 86 87 #[must_use] 88 pub fn inspect_legacy_paths( 89 candidates: impl IntoIterator<Item = RadrootsLegacyPathCandidate>, 90 ) -> RadrootsMigrationReport { 91 let detected = candidates 92 .into_iter() 93 .filter(|candidate| candidate.path.exists()) 94 .map(RadrootsLegacyPathCandidate::into_detection) 95 .collect(); 96 RadrootsMigrationReport::from_detected_legacy_paths(detected) 97 } 98 99 #[cfg(test)] 100 mod tests { 101 use std::path::PathBuf; 102 103 use super::{ 104 RADROOTS_MIGRATION_COMPATIBILITY_WINDOW, RADROOTS_MIGRATION_POSTURE, 105 RadrootsLegacyPathCandidate, RadrootsMigrationReport, inspect_legacy_paths, 106 }; 107 108 fn unique_test_dir() -> PathBuf { 109 let nanos = std::time::SystemTime::now() 110 .duration_since(std::time::UNIX_EPOCH) 111 .expect("clock") 112 .as_nanos(); 113 let path = std::env::temp_dir().join(format!("radroots_runtime_paths-test-{nanos}")); 114 std::fs::create_dir_all(&path).expect("create temp test dir"); 115 path 116 } 117 118 #[test] 119 fn inspect_legacy_paths_reports_only_paths_that_exist() { 120 let temp = unique_test_dir(); 121 let existing = temp.join("old-state"); 122 let missing = temp.join("missing-state"); 123 std::fs::write(&existing, "legacy").expect("write legacy marker"); 124 125 let report = inspect_legacy_paths([ 126 RadrootsLegacyPathCandidate::new( 127 "old-state", 128 "old state", 129 &existing, 130 Some(temp.join("new-state")), 131 "run the explicit importer", 132 ), 133 RadrootsLegacyPathCandidate::new( 134 "missing-state", 135 "missing state", 136 &missing, 137 None, 138 "nothing to do", 139 ), 140 ]); 141 142 assert_eq!(report.posture, RADROOTS_MIGRATION_POSTURE); 143 assert_eq!(report.state, "legacy_state_detected"); 144 assert!(!report.silent_startup_relocation); 145 assert_eq!( 146 report.compatibility_window, 147 RADROOTS_MIGRATION_COMPATIBILITY_WINDOW 148 ); 149 assert_eq!(report.detected_legacy_paths.len(), 1); 150 assert_eq!(report.detected_legacy_paths[0].id, "old-state"); 151 assert_eq!(report.detected_legacy_paths[0].path, existing); 152 std::fs::remove_dir_all(temp).expect("remove temp test dir"); 153 } 154 155 #[test] 156 fn inspect_legacy_paths_is_ready_when_no_candidate_exists() { 157 let temp = unique_test_dir(); 158 159 let report = inspect_legacy_paths([RadrootsLegacyPathCandidate::new( 160 "missing-state", 161 "missing state", 162 temp.join("missing-state"), 163 None, 164 "nothing to do", 165 )]); 166 167 assert_eq!(report.state, "ready"); 168 assert!(report.detected_legacy_paths.is_empty()); 169 std::fs::remove_dir_all(temp).expect("remove temp test dir"); 170 } 171 172 #[test] 173 fn empty_report_matches_ready_state() { 174 let report = RadrootsMigrationReport::empty(); 175 assert_eq!(report.posture, RADROOTS_MIGRATION_POSTURE); 176 assert_eq!(report.state, "ready"); 177 assert!(!report.silent_startup_relocation); 178 assert_eq!( 179 report.compatibility_window, 180 RADROOTS_MIGRATION_COMPATIBILITY_WINDOW 181 ); 182 assert!(report.detected_legacy_paths.is_empty()); 183 } 184 }