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, PartialEq, Eq)]
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_snapshot = {
577 let cache_guard = self
578 .cache
579 .read()
580 .map_err(|_| StorageError::Corruption("cache lock poisoned".into()))?;
581 cache_guard.clone()
582 };
583
584 self.backend.save_all(&cache_snapshot)?;
585
586 let cache_guard = self
587 .cache
588 .read()
589 .map_err(|_| StorageError::Corruption("cache lock poisoned".into()))?;
590 let mut dirty_guard = self
591 .dirty
592 .write()
593 .map_err(|_| StorageError::Corruption("dirty lock poisoned".into()))?;
594 *dirty_guard = *cache_guard != cache_snapshot;
595
596 Ok(true)
597 }
598
599 #[must_use]
603 pub fn get(&self, key: &str) -> Option<StoredEntry> {
604 let cache = self.cache.read().ok()?;
605 cache.get(key).cloned()
606 }
607
608 pub fn set(&self, key: impl Into<String>, version: u32, data: Vec<u8>) {
612 let key = key.into();
613 if let Ok(mut cache) = self.cache.write() {
614 cache.insert(key.clone(), StoredEntry { key, version, data });
615 if let Ok(mut dirty) = self.dirty.write() {
616 *dirty = true;
617 }
618 }
619 }
620
621 pub fn remove(&self, key: &str) -> Option<StoredEntry> {
625 let result = self.cache.write().ok()?.remove(key);
626 if result.is_some()
627 && let Ok(mut dirty) = self.dirty.write()
628 {
629 *dirty = true;
630 }
631 result
632 }
633
634 pub fn clear(&self) -> StorageResult<()> {
636 self.backend.clear()?;
637 if let Ok(mut cache) = self.cache.write() {
638 cache.clear();
639 }
640 if let Ok(mut dirty) = self.dirty.write() {
641 *dirty = false;
642 }
643 Ok(())
644 }
645
646 #[must_use]
648 pub fn len(&self) -> usize {
649 self.cache.read().map(|c| c.len()).unwrap_or(0)
650 }
651
652 #[must_use]
654 pub fn is_empty(&self) -> bool {
655 self.len() == 0
656 }
657
658 #[must_use]
660 pub fn is_dirty(&self) -> bool {
661 self.dirty.read().map(|d| *d).unwrap_or(false)
662 }
663
664 #[must_use]
666 pub fn backend_name(&self) -> &str {
667 self.backend.name()
668 }
669
670 #[must_use]
672 pub fn is_available(&self) -> bool {
673 self.backend.is_available()
674 }
675
676 #[must_use]
678 pub fn keys(&self) -> Vec<String> {
679 self.cache
680 .read()
681 .map(|c| c.keys().cloned().collect())
682 .unwrap_or_default()
683 }
684}
685
686impl fmt::Debug for StateRegistry {
687 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
688 f.debug_struct("StateRegistry")
689 .field("backend", &self.backend.name())
690 .field("entries", &self.len())
691 .field("dirty", &self.is_dirty())
692 .finish()
693 }
694}
695
696impl StateRegistry {
698 #[must_use]
700 pub fn shared(self) -> Arc<Self> {
701 Arc::new(self)
702 }
703}
704
705#[derive(Clone, Debug, Default)]
711pub struct RegistryStats {
712 pub entry_count: usize,
714 pub total_bytes: usize,
716 pub dirty: bool,
718 pub backend: String,
720}
721
722impl StateRegistry {
723 #[must_use]
725 pub fn stats(&self) -> RegistryStats {
726 let (entry_count, total_bytes) = self
727 .cache
728 .read()
729 .map(|c| {
730 let count = c.len();
731 let bytes: usize = c.values().map(|e| e.data.len()).sum();
732 (count, bytes)
733 })
734 .unwrap_or((0, 0));
735
736 RegistryStats {
737 entry_count,
738 total_bytes,
739 dirty: self.is_dirty(),
740 backend: self.backend.name().to_string(),
741 }
742 }
743}
744
745#[cfg(test)]
750mod tests {
751 use super::*;
752 use std::sync::atomic::{AtomicBool, Ordering};
753 use std::sync::{Mutex, Weak};
754 use std::thread;
755 use web_time::Duration;
756
757 #[derive(Default)]
758 struct ReentrantFlushBackendState {
759 registry: Mutex<Option<Weak<StateRegistry>>>,
760 injected_during_save: AtomicBool,
761 saved_entries: RwLock<HashMap<String, StoredEntry>>,
762 }
763
764 impl ReentrantFlushBackendState {
765 fn bind_registry(&self, registry: &Arc<StateRegistry>) {
766 *self.registry.lock().unwrap_or_else(|e| e.into_inner()) =
767 Some(Arc::downgrade(registry));
768 }
769
770 fn saved_entries(&self) -> HashMap<String, StoredEntry> {
771 self.saved_entries
772 .read()
773 .unwrap_or_else(|e| e.into_inner())
774 .clone()
775 }
776 }
777
778 #[derive(Clone)]
779 struct ReentrantFlushBackend {
780 state: Arc<ReentrantFlushBackendState>,
781 }
782
783 impl StorageBackend for ReentrantFlushBackend {
784 fn name(&self) -> &str {
785 "ReentrantFlushBackend"
786 }
787
788 fn load_all(&self) -> StorageResult<HashMap<String, StoredEntry>> {
789 Ok(self.state.saved_entries())
790 }
791
792 fn save_all(&self, entries: &HashMap<String, StoredEntry>) -> StorageResult<()> {
793 *self
794 .state
795 .saved_entries
796 .write()
797 .map_err(|_| StorageError::Corruption("saved entries lock poisoned".into()))? =
798 entries.clone();
799
800 if !self.state.injected_during_save.swap(true, Ordering::SeqCst)
801 && let Some(registry) = self
802 .state
803 .registry
804 .lock()
805 .unwrap_or_else(|e| e.into_inner())
806 .as_ref()
807 .and_then(Weak::upgrade)
808 {
809 registry.set("backend::late", 2, b"late".to_vec());
810 }
811
812 Ok(())
813 }
814
815 fn clear(&self) -> StorageResult<()> {
816 self.state
817 .saved_entries
818 .write()
819 .map_err(|_| StorageError::Corruption("saved entries lock poisoned".into()))?
820 .clear();
821 Ok(())
822 }
823 }
824
825 #[test]
826 fn memory_storage_basic_operations() {
827 let storage = MemoryStorage::new();
828
829 let entries = storage.load_all().unwrap();
831 assert!(entries.is_empty());
832
833 let mut data = HashMap::new();
835 data.insert(
836 "key1".to_string(),
837 StoredEntry {
838 key: "key1".to_string(),
839 version: 1,
840 data: b"hello".to_vec(),
841 },
842 );
843 storage.save_all(&data).unwrap();
844
845 let loaded = storage.load_all().unwrap();
847 assert_eq!(loaded.len(), 1);
848 assert_eq!(loaded["key1"].data, b"hello");
849
850 storage.clear().unwrap();
852 assert!(storage.load_all().unwrap().is_empty());
853 }
854
855 #[test]
856 fn memory_storage_with_entries() {
857 let mut entries = HashMap::new();
858 entries.insert(
859 "test".to_string(),
860 StoredEntry {
861 key: "test".to_string(),
862 version: 2,
863 data: vec![1, 2, 3],
864 },
865 );
866 let storage = MemoryStorage::with_entries(entries);
867
868 let loaded = storage.load_all().unwrap();
869 assert_eq!(loaded.len(), 1);
870 assert_eq!(loaded["test"].version, 2);
871 }
872
873 #[test]
874 fn registry_basic_operations() {
875 let registry = StateRegistry::in_memory();
876
877 assert!(registry.is_empty());
879 assert!(!registry.is_dirty());
880
881 registry.set("widget::1", 1, b"data".to_vec());
883 assert_eq!(registry.len(), 1);
884 assert!(registry.is_dirty());
885
886 let entry = registry.get("widget::1").unwrap();
888 assert_eq!(entry.version, 1);
889 assert_eq!(entry.data, b"data");
890
891 assert!(registry.get("widget::99").is_none());
893
894 assert!(registry.flush().unwrap());
896 assert!(!registry.is_dirty());
897
898 assert!(!registry.flush().unwrap());
900
901 let removed = registry.remove("widget::1").unwrap();
903 assert_eq!(removed.data, b"data");
904 assert!(registry.is_empty());
905 assert!(registry.is_dirty());
906 }
907
908 #[test]
909 fn registry_load_and_flush() {
910 let storage = MemoryStorage::new();
911 let mut initial = HashMap::new();
912 initial.insert(
913 "pre::existing".to_string(),
914 StoredEntry {
915 key: "pre::existing".to_string(),
916 version: 5,
917 data: b"old".to_vec(),
918 },
919 );
920 storage.save_all(&initial).unwrap();
921
922 let registry = StateRegistry::new(Box::new(storage));
923
924 let count = registry.load().unwrap();
926 assert_eq!(count, 1);
927 assert!(!registry.is_dirty());
928
929 let entry = registry.get("pre::existing").unwrap();
930 assert_eq!(entry.version, 5);
931 }
932
933 #[test]
934 fn registry_clear() {
935 let registry = StateRegistry::in_memory();
936 registry.set("a", 1, vec![]);
937 registry.set("b", 1, vec![]);
938 assert_eq!(registry.len(), 2);
939
940 registry.clear().unwrap();
941 assert!(registry.is_empty());
942 assert!(!registry.is_dirty());
943 }
944
945 #[test]
946 fn registry_keys() {
947 let registry = StateRegistry::in_memory();
948 registry.set("widget::a", 1, vec![]);
949 registry.set("widget::b", 1, vec![]);
950
951 let mut keys = registry.keys();
952 keys.sort();
953 assert_eq!(keys, vec!["widget::a", "widget::b"]);
954 }
955
956 #[test]
957 fn registry_stats() {
958 let registry = StateRegistry::in_memory();
959 registry.set("x", 1, vec![1, 2, 3, 4, 5]);
960 registry.set("y", 1, vec![6, 7, 8]);
961
962 let stats = registry.stats();
963 assert_eq!(stats.entry_count, 2);
964 assert_eq!(stats.total_bytes, 8);
965 assert!(stats.dirty);
966 assert_eq!(stats.backend, "MemoryStorage");
967 }
968
969 #[test]
970 fn registry_shared() {
971 let registry = StateRegistry::in_memory().shared();
972 registry.set("test", 1, vec![42]);
973
974 let registry2 = Arc::clone(®istry);
975 assert_eq!(registry2.get("test").unwrap().data, vec![42]);
976 }
977
978 #[test]
979 fn storage_error_display() {
980 let io_err = StorageError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "missing"));
981 assert!(io_err.to_string().contains("I/O error"));
982
983 let corrupt = StorageError::Corruption("bad data".into());
984 assert!(corrupt.to_string().contains("corruption"));
985
986 let unavail = StorageError::Unavailable("no backend".into());
987 assert!(unavail.to_string().contains("unavailable"));
988 }
989
990 #[test]
993 fn storage_error_source_io() {
994 let err = StorageError::Io(std::io::Error::new(
995 std::io::ErrorKind::BrokenPipe,
996 "broken",
997 ));
998 let source = std::error::Error::source(&err);
999 assert!(source.is_some());
1000 }
1001
1002 #[test]
1003 fn storage_error_source_corruption_none() {
1004 let err = StorageError::Corruption("test".into());
1005 assert!(std::error::Error::source(&err).is_none());
1006 }
1007
1008 #[test]
1009 fn storage_error_source_unavailable_none() {
1010 let err = StorageError::Unavailable("test".into());
1011 assert!(std::error::Error::source(&err).is_none());
1012 }
1013
1014 #[test]
1015 fn storage_error_from_io_error() {
1016 let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
1017 let err: StorageError = io_err.into();
1018 match err {
1019 StorageError::Io(e) => assert_eq!(e.kind(), std::io::ErrorKind::TimedOut),
1020 _ => panic!("expected Io variant"),
1021 }
1022 }
1023
1024 #[test]
1025 fn storage_error_debug_format() {
1026 let err = StorageError::Corruption("test".into());
1027 let dbg = format!("{:?}", err);
1028 assert!(dbg.contains("Corruption"));
1029 }
1030
1031 #[test]
1034 fn memory_storage_name() {
1035 let storage = MemoryStorage::new();
1036 assert_eq!(storage.name(), "MemoryStorage");
1037 }
1038
1039 #[test]
1040 fn memory_storage_is_available() {
1041 let storage = MemoryStorage::new();
1042 assert!(storage.is_available());
1043 }
1044
1045 #[test]
1046 fn memory_storage_debug_format() {
1047 let storage = MemoryStorage::new();
1048 let dbg = format!("{:?}", storage);
1049 assert!(dbg.contains("MemoryStorage"));
1050 assert!(dbg.contains("entries"));
1051 }
1052
1053 #[test]
1054 fn memory_storage_debug_shows_count() {
1055 let mut entries = HashMap::new();
1056 entries.insert(
1057 "a".to_string(),
1058 StoredEntry {
1059 key: "a".to_string(),
1060 version: 1,
1061 data: vec![],
1062 },
1063 );
1064 let storage = MemoryStorage::with_entries(entries);
1065 let dbg = format!("{:?}", storage);
1066 assert!(dbg.contains("1"));
1067 }
1068
1069 #[test]
1070 fn memory_storage_save_replaces_all() {
1071 let storage = MemoryStorage::new();
1072
1073 let mut data1 = HashMap::new();
1074 data1.insert(
1075 "old".to_string(),
1076 StoredEntry {
1077 key: "old".to_string(),
1078 version: 1,
1079 data: vec![],
1080 },
1081 );
1082 storage.save_all(&data1).unwrap();
1083
1084 let mut data2 = HashMap::new();
1085 data2.insert(
1086 "new".to_string(),
1087 StoredEntry {
1088 key: "new".to_string(),
1089 version: 2,
1090 data: vec![],
1091 },
1092 );
1093 storage.save_all(&data2).unwrap();
1094
1095 let loaded = storage.load_all().unwrap();
1096 assert_eq!(loaded.len(), 1);
1097 assert!(loaded.contains_key("new"));
1098 assert!(!loaded.contains_key("old"));
1099 }
1100
1101 #[test]
1104 fn registry_backend_name() {
1105 let registry = StateRegistry::in_memory();
1106 assert_eq!(registry.backend_name(), "MemoryStorage");
1107 }
1108
1109 #[test]
1110 fn registry_is_available() {
1111 let registry = StateRegistry::in_memory();
1112 assert!(registry.is_available());
1113 }
1114
1115 #[test]
1116 fn registry_debug_format() {
1117 let registry = StateRegistry::in_memory();
1118 registry.set("x", 1, vec![]);
1119 let dbg = format!("{:?}", registry);
1120 assert!(dbg.contains("StateRegistry"));
1121 assert!(dbg.contains("MemoryStorage"));
1122 assert!(dbg.contains("dirty"));
1123 }
1124
1125 #[test]
1126 fn registry_set_overwrites() {
1127 let registry = StateRegistry::in_memory();
1128 registry.set("k", 1, b"first".to_vec());
1129 registry.set("k", 2, b"second".to_vec());
1130
1131 assert_eq!(registry.len(), 1);
1132 let entry = registry.get("k").unwrap();
1133 assert_eq!(entry.version, 2);
1134 assert_eq!(entry.data, b"second");
1135 }
1136
1137 #[test]
1138 fn registry_remove_nonexistent_returns_none() {
1139 let registry = StateRegistry::in_memory();
1140 assert!(registry.remove("nonexistent").is_none());
1141 }
1142
1143 #[test]
1144 fn registry_load_replaces_cache() {
1145 let storage = MemoryStorage::new();
1146 let mut initial = HashMap::new();
1147 initial.insert(
1148 "backend_key".to_string(),
1149 StoredEntry {
1150 key: "backend_key".to_string(),
1151 version: 1,
1152 data: b"from_backend".to_vec(),
1153 },
1154 );
1155 storage.save_all(&initial).unwrap();
1156
1157 let registry = StateRegistry::new(Box::new(storage));
1158 registry.set("local_key", 1, b"local".to_vec());
1159 assert!(registry.get("local_key").is_some());
1160
1161 registry.load().unwrap();
1163 assert!(registry.get("local_key").is_none());
1164 assert!(registry.get("backend_key").is_some());
1165 }
1166
1167 #[test]
1168 fn registry_load_clears_dirty_flag() {
1169 let registry = StateRegistry::in_memory();
1170 registry.set("x", 1, vec![]);
1171 assert!(registry.is_dirty());
1172
1173 registry.load().unwrap();
1174 assert!(!registry.is_dirty());
1175 }
1176
1177 #[test]
1178 fn registry_flush_persists_to_backend() {
1179 let registry = StateRegistry::in_memory();
1180 registry.set("widget::foo", 3, b"bar".to_vec());
1181 registry.flush().unwrap();
1182
1183 let count = registry.load().unwrap();
1185 assert_eq!(count, 1);
1186 let entry = registry.get("widget::foo").unwrap();
1187 assert_eq!(entry.version, 3);
1188 assert_eq!(entry.data, b"bar");
1189 }
1190
1191 #[test]
1192 fn registry_flush_drops_cache_lock_before_backend_save() {
1193 let backend_state = Arc::new(ReentrantFlushBackendState::default());
1194 let registry = Arc::new(StateRegistry::new(Box::new(ReentrantFlushBackend {
1195 state: Arc::clone(&backend_state),
1196 })));
1197 backend_state.bind_registry(®istry);
1198
1199 registry.set("widget::foo", 1, b"bar".to_vec());
1200
1201 let (done_tx, done_rx) = std::sync::mpsc::channel();
1202 let registry_for_thread = Arc::clone(®istry);
1203 let handle = thread::spawn(move || {
1204 let result = registry_for_thread.flush();
1205 done_tx.send(result).expect("flush result");
1206 });
1207
1208 done_rx
1209 .recv_timeout(Duration::from_secs(1))
1210 .expect("flush should complete without deadlocking")
1211 .expect("flush succeeds");
1212 handle.join().expect("flush thread");
1213
1214 let saved_entries = backend_state.saved_entries();
1215 assert!(saved_entries.contains_key("widget::foo"));
1216 }
1217
1218 #[test]
1219 fn registry_flush_preserves_dirty_when_backend_mutates_registry() {
1220 let backend_state = Arc::new(ReentrantFlushBackendState::default());
1221 let registry = Arc::new(StateRegistry::new(Box::new(ReentrantFlushBackend {
1222 state: Arc::clone(&backend_state),
1223 })));
1224 backend_state.bind_registry(®istry);
1225
1226 registry.set("widget::foo", 1, b"bar".to_vec());
1227 assert!(registry.flush().unwrap());
1228
1229 let first_saved = backend_state.saved_entries();
1230 assert!(first_saved.contains_key("widget::foo"));
1231 assert!(!first_saved.contains_key("backend::late"));
1232 assert!(registry.is_dirty());
1233 assert_eq!(registry.get("backend::late").unwrap().data, b"late");
1234
1235 assert!(registry.flush().unwrap());
1236
1237 let second_saved = backend_state.saved_entries();
1238 assert!(second_saved.contains_key("backend::late"));
1239 assert!(!registry.is_dirty());
1240 }
1241
1242 #[test]
1243 fn registry_multiple_keys() {
1244 let registry = StateRegistry::in_memory();
1245 registry.set("a", 1, vec![1]);
1246 registry.set("b", 2, vec![2]);
1247 registry.set("c", 3, vec![3]);
1248
1249 assert_eq!(registry.len(), 3);
1250 assert!(!registry.is_empty());
1251
1252 let mut keys = registry.keys();
1253 keys.sort();
1254 assert_eq!(keys, vec!["a", "b", "c"]);
1255 }
1256
1257 #[test]
1258 fn registry_remove_marks_dirty() {
1259 let registry = StateRegistry::in_memory();
1260 registry.set("x", 1, vec![]);
1261 registry.flush().unwrap();
1262 assert!(!registry.is_dirty());
1263
1264 registry.remove("x");
1265 assert!(registry.is_dirty());
1266 }
1267
1268 #[test]
1269 fn registry_clear_after_set_and_flush() {
1270 let registry = StateRegistry::in_memory();
1271 registry.set("a", 1, vec![]);
1272 registry.flush().unwrap();
1273 registry.clear().unwrap();
1274
1275 assert!(registry.is_empty());
1276 assert!(!registry.is_dirty());
1277
1278 let count = registry.load().unwrap();
1280 assert_eq!(count, 0);
1281 }
1282
1283 #[test]
1286 fn registry_stats_default() {
1287 let stats = RegistryStats::default();
1288 assert_eq!(stats.entry_count, 0);
1289 assert_eq!(stats.total_bytes, 0);
1290 assert!(!stats.dirty);
1291 assert_eq!(stats.backend, "");
1292 }
1293
1294 #[test]
1295 fn registry_stats_empty() {
1296 let registry = StateRegistry::in_memory();
1297 let stats = registry.stats();
1298 assert_eq!(stats.entry_count, 0);
1299 assert_eq!(stats.total_bytes, 0);
1300 assert!(!stats.dirty);
1301 }
1302
1303 #[test]
1306 fn stored_entry_clone() {
1307 let entry = StoredEntry {
1308 key: "test".to_string(),
1309 version: 7,
1310 data: vec![1, 2, 3],
1311 };
1312 let cloned = entry.clone();
1313 assert_eq!(cloned.key, "test");
1314 assert_eq!(cloned.version, 7);
1315 assert_eq!(cloned.data, vec![1, 2, 3]);
1316 }
1317
1318 #[test]
1319 fn stored_entry_debug() {
1320 let entry = StoredEntry {
1321 key: "k".to_string(),
1322 version: 1,
1323 data: vec![],
1324 };
1325 let dbg = format!("{:?}", entry);
1326 assert!(dbg.contains("StoredEntry"));
1327 }
1328
1329 #[test]
1332 fn registry_shared_concurrent_access() {
1333 let registry = StateRegistry::in_memory().shared();
1334 let r2 = Arc::clone(®istry);
1335
1336 registry.set("from_1", 1, vec![10]);
1337 r2.set("from_2", 1, vec![20]);
1338
1339 assert_eq!(registry.len(), 2);
1340 assert!(r2.get("from_1").is_some());
1341 assert!(registry.get("from_2").is_some());
1342 }
1343}
1344
1345#[cfg(all(test, feature = "state-persistence"))]
1346mod file_storage_tests {
1347 use super::*;
1348 use std::io::Write;
1349 use tempfile::TempDir;
1350
1351 #[test]
1352 fn file_storage_round_trip() {
1353 let tmp = TempDir::new().unwrap();
1354 let path = tmp.path().join("state.json");
1355 let storage = FileStorage::new(&path);
1356
1357 let mut entries = HashMap::new();
1359 entries.insert(
1360 "widget::test".to_string(),
1361 StoredEntry {
1362 key: "widget::test".to_string(),
1363 version: 3,
1364 data: b"hello world".to_vec(),
1365 },
1366 );
1367 storage.save_all(&entries).unwrap();
1368
1369 assert!(path.exists());
1371
1372 let loaded = storage.load_all().unwrap();
1374 assert_eq!(loaded.len(), 1);
1375 assert_eq!(loaded["widget::test"].version, 3);
1376 assert_eq!(loaded["widget::test"].data, b"hello world");
1377 }
1378
1379 #[test]
1380 fn file_storage_load_nonexistent() {
1381 let tmp = TempDir::new().unwrap();
1382 let path = tmp.path().join("does_not_exist.json");
1383 let storage = FileStorage::new(&path);
1384
1385 let entries = storage.load_all().unwrap();
1386 assert!(entries.is_empty());
1387 }
1388
1389 #[test]
1390 fn file_storage_clear() {
1391 let tmp = TempDir::new().unwrap();
1392 let path = tmp.path().join("state.json");
1393
1394 std::fs::write(&path, "{}").unwrap();
1396 assert!(path.exists());
1397
1398 let storage = FileStorage::new(&path);
1399 storage.clear().unwrap();
1400 assert!(!path.exists());
1401 }
1402
1403 #[test]
1404 fn file_storage_creates_parent_dirs() {
1405 let tmp = TempDir::new().unwrap();
1406 let path = tmp.path().join("nested").join("dirs").join("state.json");
1407 let storage = FileStorage::new(&path);
1408
1409 let mut entries = HashMap::new();
1410 entries.insert(
1411 "k".to_string(),
1412 StoredEntry {
1413 key: "k".to_string(),
1414 version: 1,
1415 data: vec![],
1416 },
1417 );
1418 storage.save_all(&entries).unwrap();
1419 assert!(path.exists());
1420 }
1421
1422 #[test]
1423 fn file_storage_handles_corrupt_entry() {
1424 let tmp = TempDir::new().unwrap();
1425 let path = tmp.path().join("state.json");
1426
1427 let mut f = std::fs::File::create(&path).unwrap();
1429 writeln!(
1430 f,
1431 r#"{{"format_version":1,"entries":{{"bad":{{"version":1,"data_base64":"!!invalid!!"}},"good":{{"version":1,"data_base64":"aGVsbG8="}}}}}}"#
1432 )
1433 .unwrap();
1434
1435 let storage = FileStorage::new(&path);
1436 let loaded = storage.load_all().unwrap();
1437
1438 assert_eq!(loaded.len(), 1);
1440 assert!(loaded.contains_key("good"));
1441 assert_eq!(loaded["good"].data, b"hello");
1442 }
1443}