1use std::collections::HashMap;
48use std::fmt;
49use std::sync::{Arc, RwLock};
50
51#[derive(Debug)]
57pub enum StorageError {
58 Io(std::io::Error),
60 #[cfg(feature = "state-persistence")]
62 Serialization(String),
63 Corruption(String),
65 Unavailable(String),
67}
68
69impl fmt::Display for StorageError {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 match self {
72 StorageError::Io(e) => write!(f, "I/O error: {e}"),
73 #[cfg(feature = "state-persistence")]
74 StorageError::Serialization(msg) => write!(f, "serialization error: {msg}"),
75 StorageError::Corruption(msg) => write!(f, "storage corruption: {msg}"),
76 StorageError::Unavailable(msg) => write!(f, "storage unavailable: {msg}"),
77 }
78 }
79}
80
81impl std::error::Error for StorageError {
82 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
83 match self {
84 StorageError::Io(e) => Some(e),
85 #[cfg(feature = "state-persistence")]
86 StorageError::Serialization(_) => None,
87 StorageError::Corruption(_) => None,
88 StorageError::Unavailable(_) => None,
89 }
90 }
91}
92
93impl From<std::io::Error> for StorageError {
94 fn from(e: std::io::Error) -> Self {
95 StorageError::Io(e)
96 }
97}
98
99pub type StorageResult<T> = Result<T, StorageError>;
101
102#[derive(Clone, Debug)]
111pub struct StoredEntry {
112 pub key: String,
114 pub version: u32,
116 pub data: Vec<u8>,
118}
119
120pub trait StorageBackend: Send + Sync {
131 fn name(&self) -> &str;
133
134 fn load_all(&self) -> StorageResult<HashMap<String, StoredEntry>>;
139
140 fn save_all(&self, entries: &HashMap<String, StoredEntry>) -> StorageResult<()>;
144
145 fn clear(&self) -> StorageResult<()>;
147
148 fn is_available(&self) -> bool {
150 true
151 }
152}
153
154#[derive(Default)]
165pub struct MemoryStorage {
166 data: RwLock<HashMap<String, StoredEntry>>,
167}
168
169impl MemoryStorage {
170 #[must_use]
172 pub fn new() -> Self {
173 Self::default()
174 }
175
176 #[must_use]
178 pub fn with_entries(entries: HashMap<String, StoredEntry>) -> Self {
179 Self {
180 data: RwLock::new(entries),
181 }
182 }
183}
184
185impl StorageBackend for MemoryStorage {
186 fn name(&self) -> &str {
187 "MemoryStorage"
188 }
189
190 fn load_all(&self) -> StorageResult<HashMap<String, StoredEntry>> {
191 let guard = self
192 .data
193 .read()
194 .map_err(|_| StorageError::Corruption("lock poisoned".into()))?;
195 Ok(guard.clone())
196 }
197
198 fn save_all(&self, entries: &HashMap<String, StoredEntry>) -> StorageResult<()> {
199 let mut guard = self
200 .data
201 .write()
202 .map_err(|_| StorageError::Corruption("lock poisoned".into()))?;
203 *guard = entries.clone();
204 Ok(())
205 }
206
207 fn clear(&self) -> StorageResult<()> {
208 let mut guard = self
209 .data
210 .write()
211 .map_err(|_| StorageError::Corruption("lock poisoned".into()))?;
212 guard.clear();
213 Ok(())
214 }
215}
216
217impl fmt::Debug for MemoryStorage {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 let count = self.data.read().map(|g| g.len()).unwrap_or(0);
220 f.debug_struct("MemoryStorage")
221 .field("entries", &count)
222 .finish()
223 }
224}
225
226#[cfg(feature = "state-persistence")]
231mod file_storage {
232 use super::*;
233 use serde::{Deserialize, Serialize};
234 use std::fs::{self, File};
235 use std::io::{BufReader, BufWriter, Write};
236 use std::path::{Path, PathBuf};
237
238 #[derive(Serialize, Deserialize)]
240 struct StateFile {
241 format_version: u32,
243 entries: HashMap<String, FileEntry>,
245 }
246
247 #[derive(Serialize, Deserialize)]
249 struct FileEntry {
250 version: u32,
251 data_base64: String,
253 }
254
255 impl StateFile {
256 const FORMAT_VERSION: u32 = 1;
257
258 fn new() -> Self {
259 Self {
260 format_version: Self::FORMAT_VERSION,
261 entries: HashMap::new(),
262 }
263 }
264 }
265
266 pub struct FileStorage {
292 path: PathBuf,
293 }
294
295 impl FileStorage {
296 #[must_use]
300 pub fn new(path: impl AsRef<Path>) -> Self {
301 Self {
302 path: path.as_ref().to_path_buf(),
303 }
304 }
305
306 #[must_use]
311 pub fn default_for_app(app_name: &str) -> Self {
312 let base = dirs_or_fallback();
313 let path = base.join("ftui").join(app_name).join("state.json");
314 Self { path }
315 }
316
317 fn temp_path(&self) -> PathBuf {
318 let mut tmp = self.path.clone();
319 tmp.set_extension("json.tmp");
320 tmp
321 }
322 }
323
324 fn dirs_or_fallback() -> PathBuf {
326 if let Ok(state_home) = std::env::var("XDG_STATE_HOME") {
328 return PathBuf::from(state_home);
329 }
330 if let Ok(home) = std::env::var("HOME") {
332 return PathBuf::from(home).join(".local").join("state");
333 }
334 PathBuf::from(".")
336 }
337
338 impl StorageBackend for FileStorage {
339 fn name(&self) -> &str {
340 "FileStorage"
341 }
342
343 fn load_all(&self) -> StorageResult<HashMap<String, StoredEntry>> {
344 if !self.path.exists() {
345 return Ok(HashMap::new());
347 }
348
349 let file = File::open(&self.path)?;
350 let reader = BufReader::new(file);
351
352 let state_file: StateFile = serde_json::from_reader(reader).map_err(|e| {
353 StorageError::Serialization(format!("failed to parse state file: {e}"))
354 })?;
355
356 if state_file.format_version != StateFile::FORMAT_VERSION {
358 tracing::warn!(
359 stored = state_file.format_version,
360 expected = StateFile::FORMAT_VERSION,
361 "state file format version mismatch, ignoring stored state"
362 );
363 return Ok(HashMap::new());
364 }
365
366 let mut result = HashMap::new();
368 for (key, entry) in state_file.entries {
369 use base64::Engine;
370 let data = match base64::engine::general_purpose::STANDARD
371 .decode(&entry.data_base64)
372 {
373 Ok(d) => d,
374 Err(e) => {
375 tracing::warn!(key = %key, error = %e, "failed to decode state entry, skipping");
376 continue;
377 }
378 };
379 result.insert(
380 key.clone(),
381 StoredEntry {
382 key,
383 version: entry.version,
384 data,
385 },
386 );
387 }
388
389 Ok(result)
390 }
391
392 fn save_all(&self, entries: &HashMap<String, StoredEntry>) -> StorageResult<()> {
393 use base64::Engine;
394
395 if let Some(parent) = self.path.parent() {
397 fs::create_dir_all(parent)?;
398 }
399
400 let mut state_file = StateFile::new();
402 for (key, entry) in entries {
403 state_file.entries.insert(
404 key.clone(),
405 FileEntry {
406 version: entry.version,
407 data_base64: base64::engine::general_purpose::STANDARD.encode(&entry.data),
408 },
409 );
410 }
411
412 let tmp_path = self.temp_path();
414 {
415 let file = File::create(&tmp_path)?;
416 let mut writer = BufWriter::new(file);
417 serde_json::to_writer_pretty(&mut writer, &state_file).map_err(|e| {
418 StorageError::Serialization(format!("failed to serialize state: {e}"))
419 })?;
420 writer.flush()?;
421 writer.get_ref().sync_all()?;
422 }
423
424 fs::rename(&tmp_path, &self.path)?;
426
427 tracing::debug!(
428 path = %self.path.display(),
429 entries = entries.len(),
430 "saved widget state"
431 );
432
433 Ok(())
434 }
435
436 fn clear(&self) -> StorageResult<()> {
437 if self.path.exists() {
438 fs::remove_file(&self.path)?;
439 }
440 Ok(())
441 }
442
443 fn is_available(&self) -> bool {
444 if let Some(parent) = self.path.parent() {
446 if !parent.exists() {
447 return std::fs::create_dir_all(parent).is_ok();
448 }
449 let test_path = parent.join(".ftui_test_write");
451 if std::fs::write(&test_path, b"test").is_ok() {
452 let _ = std::fs::remove_file(&test_path);
453 return true;
454 }
455 }
456 false
457 }
458 }
459
460 impl fmt::Debug for FileStorage {
461 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
462 f.debug_struct("FileStorage")
463 .field("path", &self.path)
464 .finish()
465 }
466 }
467}
468
469#[cfg(feature = "state-persistence")]
470pub use file_storage::FileStorage;
471
472pub struct StateRegistry {
504 backend: Box<dyn StorageBackend>,
505 cache: RwLock<HashMap<String, StoredEntry>>,
506 dirty: RwLock<bool>,
507}
508
509impl StateRegistry {
510 #[must_use]
514 pub fn new(backend: Box<dyn StorageBackend>) -> Self {
515 Self {
516 backend,
517 cache: RwLock::new(HashMap::new()),
518 dirty: RwLock::new(false),
519 }
520 }
521
522 #[must_use]
524 pub fn in_memory() -> Self {
525 Self::new(Box::new(MemoryStorage::new()))
526 }
527
528 #[cfg(feature = "state-persistence")]
530 #[must_use]
531 pub fn with_file(path: impl AsRef<std::path::Path>) -> Self {
532 Self::new(Box::new(FileStorage::new(path)))
533 }
534
535 pub fn load(&self) -> StorageResult<usize> {
540 let entries = self.backend.load_all()?;
541 let count = entries.len();
542
543 let mut cache = self
544 .cache
545 .write()
546 .map_err(|_| StorageError::Corruption("cache lock poisoned".into()))?;
547 *cache = entries;
548
549 let mut dirty = self
550 .dirty
551 .write()
552 .map_err(|_| StorageError::Corruption("dirty lock poisoned".into()))?;
553 *dirty = false;
554
555 tracing::debug!(backend = %self.backend.name(), count, "loaded widget state");
556 Ok(count)
557 }
558
559 pub fn flush(&self) -> StorageResult<bool> {
564 let dirty = {
565 let guard = self
566 .dirty
567 .read()
568 .map_err(|_| StorageError::Corruption("dirty lock poisoned".into()))?;
569 *guard
570 };
571
572 if !dirty {
573 return Ok(false);
574 }
575
576 let cache = self
577 .cache
578 .read()
579 .map_err(|_| StorageError::Corruption("cache lock poisoned".into()))?;
580
581 self.backend.save_all(&cache)?;
582
583 let mut dirty_guard = self
584 .dirty
585 .write()
586 .map_err(|_| StorageError::Corruption("dirty lock poisoned".into()))?;
587 *dirty_guard = false;
588
589 Ok(true)
590 }
591
592 #[must_use]
596 pub fn get(&self, key: &str) -> Option<StoredEntry> {
597 let cache = self.cache.read().ok()?;
598 cache.get(key).cloned()
599 }
600
601 pub fn set(&self, key: impl Into<String>, version: u32, data: Vec<u8>) {
605 let key = key.into();
606 if let Ok(mut cache) = self.cache.write() {
607 cache.insert(key.clone(), StoredEntry { key, version, data });
608 if let Ok(mut dirty) = self.dirty.write() {
609 *dirty = true;
610 }
611 }
612 }
613
614 pub fn remove(&self, key: &str) -> Option<StoredEntry> {
618 let result = self.cache.write().ok()?.remove(key);
619 if result.is_some()
620 && let Ok(mut dirty) = self.dirty.write()
621 {
622 *dirty = true;
623 }
624 result
625 }
626
627 pub fn clear(&self) -> StorageResult<()> {
629 self.backend.clear()?;
630 if let Ok(mut cache) = self.cache.write() {
631 cache.clear();
632 }
633 if let Ok(mut dirty) = self.dirty.write() {
634 *dirty = false;
635 }
636 Ok(())
637 }
638
639 #[must_use]
641 pub fn len(&self) -> usize {
642 self.cache.read().map(|c| c.len()).unwrap_or(0)
643 }
644
645 #[must_use]
647 pub fn is_empty(&self) -> bool {
648 self.len() == 0
649 }
650
651 #[must_use]
653 pub fn is_dirty(&self) -> bool {
654 self.dirty.read().map(|d| *d).unwrap_or(false)
655 }
656
657 #[must_use]
659 pub fn backend_name(&self) -> &str {
660 self.backend.name()
661 }
662
663 #[must_use]
665 pub fn is_available(&self) -> bool {
666 self.backend.is_available()
667 }
668
669 #[must_use]
671 pub fn keys(&self) -> Vec<String> {
672 self.cache
673 .read()
674 .map(|c| c.keys().cloned().collect())
675 .unwrap_or_default()
676 }
677}
678
679impl fmt::Debug for StateRegistry {
680 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
681 f.debug_struct("StateRegistry")
682 .field("backend", &self.backend.name())
683 .field("entries", &self.len())
684 .field("dirty", &self.is_dirty())
685 .finish()
686 }
687}
688
689impl StateRegistry {
691 #[must_use]
693 pub fn shared(self) -> Arc<Self> {
694 Arc::new(self)
695 }
696}
697
698#[derive(Clone, Debug, Default)]
704pub struct RegistryStats {
705 pub entry_count: usize,
707 pub total_bytes: usize,
709 pub dirty: bool,
711 pub backend: String,
713}
714
715impl StateRegistry {
716 #[must_use]
718 pub fn stats(&self) -> RegistryStats {
719 let (entry_count, total_bytes) = self
720 .cache
721 .read()
722 .map(|c| {
723 let count = c.len();
724 let bytes: usize = c.values().map(|e| e.data.len()).sum();
725 (count, bytes)
726 })
727 .unwrap_or((0, 0));
728
729 RegistryStats {
730 entry_count,
731 total_bytes,
732 dirty: self.is_dirty(),
733 backend: self.backend.name().to_string(),
734 }
735 }
736}
737
738#[cfg(test)]
743mod tests {
744 use super::*;
745
746 #[test]
747 fn memory_storage_basic_operations() {
748 let storage = MemoryStorage::new();
749
750 let entries = storage.load_all().unwrap();
752 assert!(entries.is_empty());
753
754 let mut data = HashMap::new();
756 data.insert(
757 "key1".to_string(),
758 StoredEntry {
759 key: "key1".to_string(),
760 version: 1,
761 data: b"hello".to_vec(),
762 },
763 );
764 storage.save_all(&data).unwrap();
765
766 let loaded = storage.load_all().unwrap();
768 assert_eq!(loaded.len(), 1);
769 assert_eq!(loaded["key1"].data, b"hello");
770
771 storage.clear().unwrap();
773 assert!(storage.load_all().unwrap().is_empty());
774 }
775
776 #[test]
777 fn memory_storage_with_entries() {
778 let mut entries = HashMap::new();
779 entries.insert(
780 "test".to_string(),
781 StoredEntry {
782 key: "test".to_string(),
783 version: 2,
784 data: vec![1, 2, 3],
785 },
786 );
787 let storage = MemoryStorage::with_entries(entries);
788
789 let loaded = storage.load_all().unwrap();
790 assert_eq!(loaded.len(), 1);
791 assert_eq!(loaded["test"].version, 2);
792 }
793
794 #[test]
795 fn registry_basic_operations() {
796 let registry = StateRegistry::in_memory();
797
798 assert!(registry.is_empty());
800 assert!(!registry.is_dirty());
801
802 registry.set("widget::1", 1, b"data".to_vec());
804 assert_eq!(registry.len(), 1);
805 assert!(registry.is_dirty());
806
807 let entry = registry.get("widget::1").unwrap();
809 assert_eq!(entry.version, 1);
810 assert_eq!(entry.data, b"data");
811
812 assert!(registry.get("widget::99").is_none());
814
815 assert!(registry.flush().unwrap());
817 assert!(!registry.is_dirty());
818
819 assert!(!registry.flush().unwrap());
821
822 let removed = registry.remove("widget::1").unwrap();
824 assert_eq!(removed.data, b"data");
825 assert!(registry.is_empty());
826 assert!(registry.is_dirty());
827 }
828
829 #[test]
830 fn registry_load_and_flush() {
831 let storage = MemoryStorage::new();
832 let mut initial = HashMap::new();
833 initial.insert(
834 "pre::existing".to_string(),
835 StoredEntry {
836 key: "pre::existing".to_string(),
837 version: 5,
838 data: b"old".to_vec(),
839 },
840 );
841 storage.save_all(&initial).unwrap();
842
843 let registry = StateRegistry::new(Box::new(storage));
844
845 let count = registry.load().unwrap();
847 assert_eq!(count, 1);
848 assert!(!registry.is_dirty());
849
850 let entry = registry.get("pre::existing").unwrap();
851 assert_eq!(entry.version, 5);
852 }
853
854 #[test]
855 fn registry_clear() {
856 let registry = StateRegistry::in_memory();
857 registry.set("a", 1, vec![]);
858 registry.set("b", 1, vec![]);
859 assert_eq!(registry.len(), 2);
860
861 registry.clear().unwrap();
862 assert!(registry.is_empty());
863 assert!(!registry.is_dirty());
864 }
865
866 #[test]
867 fn registry_keys() {
868 let registry = StateRegistry::in_memory();
869 registry.set("widget::a", 1, vec![]);
870 registry.set("widget::b", 1, vec![]);
871
872 let mut keys = registry.keys();
873 keys.sort();
874 assert_eq!(keys, vec!["widget::a", "widget::b"]);
875 }
876
877 #[test]
878 fn registry_stats() {
879 let registry = StateRegistry::in_memory();
880 registry.set("x", 1, vec![1, 2, 3, 4, 5]);
881 registry.set("y", 1, vec![6, 7, 8]);
882
883 let stats = registry.stats();
884 assert_eq!(stats.entry_count, 2);
885 assert_eq!(stats.total_bytes, 8);
886 assert!(stats.dirty);
887 assert_eq!(stats.backend, "MemoryStorage");
888 }
889
890 #[test]
891 fn registry_shared() {
892 let registry = StateRegistry::in_memory().shared();
893 registry.set("test", 1, vec![42]);
894
895 let registry2 = Arc::clone(®istry);
896 assert_eq!(registry2.get("test").unwrap().data, vec![42]);
897 }
898
899 #[test]
900 fn storage_error_display() {
901 let io_err = StorageError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "missing"));
902 assert!(io_err.to_string().contains("I/O error"));
903
904 let corrupt = StorageError::Corruption("bad data".into());
905 assert!(corrupt.to_string().contains("corruption"));
906
907 let unavail = StorageError::Unavailable("no backend".into());
908 assert!(unavail.to_string().contains("unavailable"));
909 }
910
911 #[test]
914 fn storage_error_source_io() {
915 let err = StorageError::Io(std::io::Error::new(
916 std::io::ErrorKind::BrokenPipe,
917 "broken",
918 ));
919 let source = std::error::Error::source(&err);
920 assert!(source.is_some());
921 }
922
923 #[test]
924 fn storage_error_source_corruption_none() {
925 let err = StorageError::Corruption("test".into());
926 assert!(std::error::Error::source(&err).is_none());
927 }
928
929 #[test]
930 fn storage_error_source_unavailable_none() {
931 let err = StorageError::Unavailable("test".into());
932 assert!(std::error::Error::source(&err).is_none());
933 }
934
935 #[test]
936 fn storage_error_from_io_error() {
937 let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
938 let err: StorageError = io_err.into();
939 match err {
940 StorageError::Io(e) => assert_eq!(e.kind(), std::io::ErrorKind::TimedOut),
941 _ => panic!("expected Io variant"),
942 }
943 }
944
945 #[test]
946 fn storage_error_debug_format() {
947 let err = StorageError::Corruption("test".into());
948 let dbg = format!("{:?}", err);
949 assert!(dbg.contains("Corruption"));
950 }
951
952 #[test]
955 fn memory_storage_name() {
956 let storage = MemoryStorage::new();
957 assert_eq!(storage.name(), "MemoryStorage");
958 }
959
960 #[test]
961 fn memory_storage_is_available() {
962 let storage = MemoryStorage::new();
963 assert!(storage.is_available());
964 }
965
966 #[test]
967 fn memory_storage_debug_format() {
968 let storage = MemoryStorage::new();
969 let dbg = format!("{:?}", storage);
970 assert!(dbg.contains("MemoryStorage"));
971 assert!(dbg.contains("entries"));
972 }
973
974 #[test]
975 fn memory_storage_debug_shows_count() {
976 let mut entries = HashMap::new();
977 entries.insert(
978 "a".to_string(),
979 StoredEntry {
980 key: "a".to_string(),
981 version: 1,
982 data: vec![],
983 },
984 );
985 let storage = MemoryStorage::with_entries(entries);
986 let dbg = format!("{:?}", storage);
987 assert!(dbg.contains("1"));
988 }
989
990 #[test]
991 fn memory_storage_save_replaces_all() {
992 let storage = MemoryStorage::new();
993
994 let mut data1 = HashMap::new();
995 data1.insert(
996 "old".to_string(),
997 StoredEntry {
998 key: "old".to_string(),
999 version: 1,
1000 data: vec![],
1001 },
1002 );
1003 storage.save_all(&data1).unwrap();
1004
1005 let mut data2 = HashMap::new();
1006 data2.insert(
1007 "new".to_string(),
1008 StoredEntry {
1009 key: "new".to_string(),
1010 version: 2,
1011 data: vec![],
1012 },
1013 );
1014 storage.save_all(&data2).unwrap();
1015
1016 let loaded = storage.load_all().unwrap();
1017 assert_eq!(loaded.len(), 1);
1018 assert!(loaded.contains_key("new"));
1019 assert!(!loaded.contains_key("old"));
1020 }
1021
1022 #[test]
1025 fn registry_backend_name() {
1026 let registry = StateRegistry::in_memory();
1027 assert_eq!(registry.backend_name(), "MemoryStorage");
1028 }
1029
1030 #[test]
1031 fn registry_is_available() {
1032 let registry = StateRegistry::in_memory();
1033 assert!(registry.is_available());
1034 }
1035
1036 #[test]
1037 fn registry_debug_format() {
1038 let registry = StateRegistry::in_memory();
1039 registry.set("x", 1, vec![]);
1040 let dbg = format!("{:?}", registry);
1041 assert!(dbg.contains("StateRegistry"));
1042 assert!(dbg.contains("MemoryStorage"));
1043 assert!(dbg.contains("dirty"));
1044 }
1045
1046 #[test]
1047 fn registry_set_overwrites() {
1048 let registry = StateRegistry::in_memory();
1049 registry.set("k", 1, b"first".to_vec());
1050 registry.set("k", 2, b"second".to_vec());
1051
1052 assert_eq!(registry.len(), 1);
1053 let entry = registry.get("k").unwrap();
1054 assert_eq!(entry.version, 2);
1055 assert_eq!(entry.data, b"second");
1056 }
1057
1058 #[test]
1059 fn registry_remove_nonexistent_returns_none() {
1060 let registry = StateRegistry::in_memory();
1061 assert!(registry.remove("nonexistent").is_none());
1062 }
1063
1064 #[test]
1065 fn registry_load_replaces_cache() {
1066 let storage = MemoryStorage::new();
1067 let mut initial = HashMap::new();
1068 initial.insert(
1069 "backend_key".to_string(),
1070 StoredEntry {
1071 key: "backend_key".to_string(),
1072 version: 1,
1073 data: b"from_backend".to_vec(),
1074 },
1075 );
1076 storage.save_all(&initial).unwrap();
1077
1078 let registry = StateRegistry::new(Box::new(storage));
1079 registry.set("local_key", 1, b"local".to_vec());
1080 assert!(registry.get("local_key").is_some());
1081
1082 registry.load().unwrap();
1084 assert!(registry.get("local_key").is_none());
1085 assert!(registry.get("backend_key").is_some());
1086 }
1087
1088 #[test]
1089 fn registry_load_clears_dirty_flag() {
1090 let registry = StateRegistry::in_memory();
1091 registry.set("x", 1, vec![]);
1092 assert!(registry.is_dirty());
1093
1094 registry.load().unwrap();
1095 assert!(!registry.is_dirty());
1096 }
1097
1098 #[test]
1099 fn registry_flush_persists_to_backend() {
1100 let registry = StateRegistry::in_memory();
1101 registry.set("widget::foo", 3, b"bar".to_vec());
1102 registry.flush().unwrap();
1103
1104 let count = registry.load().unwrap();
1106 assert_eq!(count, 1);
1107 let entry = registry.get("widget::foo").unwrap();
1108 assert_eq!(entry.version, 3);
1109 assert_eq!(entry.data, b"bar");
1110 }
1111
1112 #[test]
1113 fn registry_multiple_keys() {
1114 let registry = StateRegistry::in_memory();
1115 registry.set("a", 1, vec![1]);
1116 registry.set("b", 2, vec![2]);
1117 registry.set("c", 3, vec![3]);
1118
1119 assert_eq!(registry.len(), 3);
1120 assert!(!registry.is_empty());
1121
1122 let mut keys = registry.keys();
1123 keys.sort();
1124 assert_eq!(keys, vec!["a", "b", "c"]);
1125 }
1126
1127 #[test]
1128 fn registry_remove_marks_dirty() {
1129 let registry = StateRegistry::in_memory();
1130 registry.set("x", 1, vec![]);
1131 registry.flush().unwrap();
1132 assert!(!registry.is_dirty());
1133
1134 registry.remove("x");
1135 assert!(registry.is_dirty());
1136 }
1137
1138 #[test]
1139 fn registry_clear_after_set_and_flush() {
1140 let registry = StateRegistry::in_memory();
1141 registry.set("a", 1, vec![]);
1142 registry.flush().unwrap();
1143 registry.clear().unwrap();
1144
1145 assert!(registry.is_empty());
1146 assert!(!registry.is_dirty());
1147
1148 let count = registry.load().unwrap();
1150 assert_eq!(count, 0);
1151 }
1152
1153 #[test]
1156 fn registry_stats_default() {
1157 let stats = RegistryStats::default();
1158 assert_eq!(stats.entry_count, 0);
1159 assert_eq!(stats.total_bytes, 0);
1160 assert!(!stats.dirty);
1161 assert_eq!(stats.backend, "");
1162 }
1163
1164 #[test]
1165 fn registry_stats_empty() {
1166 let registry = StateRegistry::in_memory();
1167 let stats = registry.stats();
1168 assert_eq!(stats.entry_count, 0);
1169 assert_eq!(stats.total_bytes, 0);
1170 assert!(!stats.dirty);
1171 }
1172
1173 #[test]
1176 fn stored_entry_clone() {
1177 let entry = StoredEntry {
1178 key: "test".to_string(),
1179 version: 7,
1180 data: vec![1, 2, 3],
1181 };
1182 let cloned = entry.clone();
1183 assert_eq!(cloned.key, "test");
1184 assert_eq!(cloned.version, 7);
1185 assert_eq!(cloned.data, vec![1, 2, 3]);
1186 }
1187
1188 #[test]
1189 fn stored_entry_debug() {
1190 let entry = StoredEntry {
1191 key: "k".to_string(),
1192 version: 1,
1193 data: vec![],
1194 };
1195 let dbg = format!("{:?}", entry);
1196 assert!(dbg.contains("StoredEntry"));
1197 }
1198
1199 #[test]
1202 fn registry_shared_concurrent_access() {
1203 let registry = StateRegistry::in_memory().shared();
1204 let r2 = Arc::clone(®istry);
1205
1206 registry.set("from_1", 1, vec![10]);
1207 r2.set("from_2", 1, vec![20]);
1208
1209 assert_eq!(registry.len(), 2);
1210 assert!(r2.get("from_1").is_some());
1211 assert!(registry.get("from_2").is_some());
1212 }
1213}
1214
1215#[cfg(all(test, feature = "state-persistence"))]
1216mod file_storage_tests {
1217 use super::*;
1218 use std::io::Write;
1219 use tempfile::TempDir;
1220
1221 #[test]
1222 fn file_storage_round_trip() {
1223 let tmp = TempDir::new().unwrap();
1224 let path = tmp.path().join("state.json");
1225 let storage = FileStorage::new(&path);
1226
1227 let mut entries = HashMap::new();
1229 entries.insert(
1230 "widget::test".to_string(),
1231 StoredEntry {
1232 key: "widget::test".to_string(),
1233 version: 3,
1234 data: b"hello world".to_vec(),
1235 },
1236 );
1237 storage.save_all(&entries).unwrap();
1238
1239 assert!(path.exists());
1241
1242 let loaded = storage.load_all().unwrap();
1244 assert_eq!(loaded.len(), 1);
1245 assert_eq!(loaded["widget::test"].version, 3);
1246 assert_eq!(loaded["widget::test"].data, b"hello world");
1247 }
1248
1249 #[test]
1250 fn file_storage_load_nonexistent() {
1251 let tmp = TempDir::new().unwrap();
1252 let path = tmp.path().join("does_not_exist.json");
1253 let storage = FileStorage::new(&path);
1254
1255 let entries = storage.load_all().unwrap();
1256 assert!(entries.is_empty());
1257 }
1258
1259 #[test]
1260 fn file_storage_clear() {
1261 let tmp = TempDir::new().unwrap();
1262 let path = tmp.path().join("state.json");
1263
1264 std::fs::write(&path, "{}").unwrap();
1266 assert!(path.exists());
1267
1268 let storage = FileStorage::new(&path);
1269 storage.clear().unwrap();
1270 assert!(!path.exists());
1271 }
1272
1273 #[test]
1274 fn file_storage_creates_parent_dirs() {
1275 let tmp = TempDir::new().unwrap();
1276 let path = tmp.path().join("nested").join("dirs").join("state.json");
1277 let storage = FileStorage::new(&path);
1278
1279 let mut entries = HashMap::new();
1280 entries.insert(
1281 "k".to_string(),
1282 StoredEntry {
1283 key: "k".to_string(),
1284 version: 1,
1285 data: vec![],
1286 },
1287 );
1288 storage.save_all(&entries).unwrap();
1289 assert!(path.exists());
1290 }
1291
1292 #[test]
1293 fn file_storage_handles_corrupt_entry() {
1294 let tmp = TempDir::new().unwrap();
1295 let path = tmp.path().join("state.json");
1296
1297 let mut f = std::fs::File::create(&path).unwrap();
1299 writeln!(
1300 f,
1301 r#"{{"format_version":1,"entries":{{"bad":{{"version":1,"data_base64":"!!invalid!!"}},"good":{{"version":1,"data_base64":"aGVsbG8="}}}}}}"#
1302 )
1303 .unwrap();
1304
1305 let storage = FileStorage::new(&path);
1306 let loaded = storage.load_all().unwrap();
1307
1308 assert_eq!(loaded.len(), 1);
1310 assert!(loaded.contains_key("good"));
1311 assert_eq!(loaded["good"].data, b"hello");
1312 }
1313}