1use dampen_core::binding::UiBindable;
7use dampen_core::parser::error::ParseError;
8use dampen_core::state::AppState;
9use serde::{Serialize, de::DeserializeOwned};
10use std::collections::HashMap;
11use std::marker::PhantomData;
12use std::sync::Arc;
13use std::sync::atomic::{AtomicUsize, Ordering};
14use std::time::{Duration, Instant};
15
16#[derive(Clone)]
18struct ParsedDocumentCache {
19 document: dampen_core::ir::DampenDocument,
21
22 cached_at: Instant,
24}
25
26pub struct HotReloadContext<M> {
28 last_model_snapshot: Option<String>,
30
31 last_reload_timestamp: Instant,
33
34 reload_count: usize,
36
37 error: Option<String>,
39
40 parse_cache: HashMap<u64, ParsedDocumentCache>,
43
44 max_cache_size: usize,
46
47 cache_hits: AtomicUsize,
49
50 cache_misses: AtomicUsize,
52
53 _marker: PhantomData<M>,
54}
55
56impl<M: UiBindable> HotReloadContext<M> {
57 pub fn new() -> Self {
59 Self {
60 last_model_snapshot: None,
61 last_reload_timestamp: Instant::now(),
62 reload_count: 0,
63 error: None,
64 parse_cache: HashMap::new(),
65 max_cache_size: 10,
66 cache_hits: AtomicUsize::new(0),
67 cache_misses: AtomicUsize::new(0),
68 _marker: PhantomData,
69 }
70 }
71
72 pub fn with_cache_size(cache_size: usize) -> Self {
74 Self {
75 last_model_snapshot: None,
76 last_reload_timestamp: Instant::now(),
77 reload_count: 0,
78 error: None,
79 parse_cache: HashMap::new(),
80 max_cache_size: cache_size,
81 cache_hits: AtomicUsize::new(0),
82 cache_misses: AtomicUsize::new(0),
83 _marker: PhantomData,
84 }
85 }
86
87 fn compute_content_hash(xml_source: &str) -> u64 {
89 use std::collections::hash_map::DefaultHasher;
90 use std::hash::{Hash, Hasher};
91
92 let mut hasher = DefaultHasher::new();
93 xml_source.hash(&mut hasher);
94 hasher.finish()
95 }
96
97 fn get_cached_document(&self, xml_source: &str) -> Option<dampen_core::ir::DampenDocument> {
99 let content_hash = Self::compute_content_hash(xml_source);
100
101 self.parse_cache
102 .get(&content_hash)
103 .map(|entry| entry.document.clone())
104 .inspect(|_| {
105 self.cache_hits.fetch_add(1, Ordering::Relaxed);
106 })
107 .or_else(|| {
108 self.cache_misses.fetch_add(1, Ordering::Relaxed);
109 None
110 })
111 }
112
113 fn cache_document(&mut self, xml_source: &str, document: dampen_core::ir::DampenDocument) {
115 if self.parse_cache.len() >= self.max_cache_size
117 && let Some(oldest_key) = self
118 .parse_cache
119 .iter()
120 .min_by_key(|(_, entry)| entry.cached_at)
121 .map(|(key, _)| *key)
122 {
123 self.parse_cache.remove(&oldest_key);
124 }
125
126 let content_hash = Self::compute_content_hash(xml_source);
127
128 self.parse_cache.insert(
129 content_hash,
130 ParsedDocumentCache {
131 document,
132 cached_at: Instant::now(),
133 },
134 );
135 }
136
137 pub fn clear_cache(&mut self) {
139 self.parse_cache.clear();
140 }
141
142 pub fn cache_stats(&self) -> (usize, usize) {
144 (self.parse_cache.len(), self.max_cache_size)
145 }
146
147 pub fn performance_metrics(&self) -> ReloadPerformanceMetrics {
149 ReloadPerformanceMetrics {
150 reload_count: self.reload_count,
151 last_reload_latency: self.last_reload_latency(),
152 cache_hit_rate: self.calculate_cache_hit_rate(),
153 cache_size: self.parse_cache.len(),
154 }
155 }
156
157 fn calculate_cache_hit_rate(&self) -> f64 {
159 let hits = self.cache_hits.load(Ordering::Relaxed);
160 let misses = self.cache_misses.load(Ordering::Relaxed);
161 let total = hits.saturating_add(misses);
162
163 if total == 0 {
164 0.0
165 } else {
166 hits as f64 / total as f64
167 }
168 }
169
170 pub fn snapshot_model(&mut self, model: &M) -> Result<(), String>
172 where
173 M: Serialize,
174 {
175 match serde_json::to_string(model) {
176 Ok(json) => {
177 self.last_model_snapshot = Some(json);
178 Ok(())
179 }
180 Err(e) => Err(format!("Failed to serialize model: {}", e)),
181 }
182 }
183
184 pub fn restore_model(&self) -> Result<M, String>
186 where
187 M: DeserializeOwned,
188 {
189 match &self.last_model_snapshot {
190 Some(json) => serde_json::from_str(json)
191 .map_err(|e| format!("Failed to deserialize model: {}", e)),
192 None => Err("No model snapshot available".to_string()),
193 }
194 }
195
196 pub fn record_reload(&mut self, success: bool) {
198 self.reload_count += 1;
199 self.last_reload_timestamp = Instant::now();
200 if !success {
201 self.error = Some("Reload failed".to_string());
202 } else {
203 self.error = None;
204 }
205 }
206
207 pub fn record_reload_with_timing(&mut self, success: bool, elapsed: Duration) {
209 self.reload_count += 1;
210 self.last_reload_timestamp = Instant::now();
211 if !success {
212 self.error = Some("Reload failed".to_string());
213 } else {
214 self.error = None;
215 }
216
217 if success && elapsed.as_millis() > 300 {
219 eprintln!(
220 "Warning: Hot-reload took {}ms (target: <300ms)",
221 elapsed.as_millis()
222 );
223 }
224 }
225
226 pub fn last_reload_latency(&self) -> Duration {
228 self.last_reload_timestamp.elapsed()
229 }
230}
231
232impl<M: UiBindable> Default for HotReloadContext<M> {
233 fn default() -> Self {
234 Self::new()
235 }
236}
237
238#[derive(Debug, Clone, Copy)]
240pub struct ReloadPerformanceMetrics {
241 pub reload_count: usize,
243
244 pub last_reload_latency: Duration,
246
247 pub cache_hit_rate: f64,
249
250 pub cache_size: usize,
252}
253
254impl ReloadPerformanceMetrics {
255 pub fn meets_target(&self) -> bool {
257 self.last_reload_latency.as_millis() < 300
258 }
259
260 pub fn latency_ms(&self) -> u128 {
262 self.last_reload_latency.as_millis()
263 }
264}
265
266#[derive(Debug)]
268pub enum ReloadResult<M: UiBindable> {
269 Success(AppState<M>),
271
272 ParseError(ParseError),
274
275 ValidationError(Vec<String>),
277
278 StateRestoreWarning(AppState<M>, String),
280}
281
282pub fn attempt_hot_reload<M, F>(
359 xml_source: &str,
360 current_state: &AppState<M>,
361 context: &mut HotReloadContext<M>,
362 create_handlers: F,
363) -> ReloadResult<M>
364where
365 M: UiBindable + Serialize + DeserializeOwned + Default,
366 F: FnOnce() -> dampen_core::handler::HandlerRegistry,
367{
368 let reload_start = Instant::now();
369
370 if let Err(e) = context.snapshot_model(¤t_state.model) {
372 eprintln!("Warning: Failed to snapshot model: {}", e);
374 }
375
376 let new_document = if let Some(cached_doc) = context.get_cached_document(xml_source) {
378 cached_doc
380 } else {
381 match dampen_core::parser::parse(xml_source) {
383 Ok(doc) => {
384 context.cache_document(xml_source, doc.clone());
385 doc
386 }
387 Err(err) => {
388 context.record_reload(false);
389 return ReloadResult::ParseError(err);
390 }
391 }
392 };
393
394 let new_handlers = create_handlers();
396
397 if let Err(missing_handlers) = validate_handlers(&new_document, &new_handlers) {
399 context.record_reload(false);
400 let error_messages: Vec<String> = missing_handlers
401 .iter()
402 .map(|h| format!("Handler '{}' is referenced but not registered", h))
403 .collect();
404 return ReloadResult::ValidationError(error_messages);
405 }
406
407 let restored_model = match context.restore_model() {
409 Ok(model) => {
410 model
412 }
413 Err(e) => {
414 eprintln!("Warning: Failed to restore model ({}), using default", e);
416
417 let new_state = AppState::with_all(new_document, M::default(), new_handlers);
419
420 context.record_reload(true);
421 return ReloadResult::StateRestoreWarning(new_state, e);
422 }
423 };
424
425 let new_state = AppState::with_all(new_document, restored_model, new_handlers);
427
428 let elapsed = reload_start.elapsed();
429 context.record_reload_with_timing(true, elapsed);
430 ReloadResult::Success(new_state)
431}
432
433pub async fn attempt_hot_reload_async<M, F>(
496 xml_source: Arc<String>,
497 current_state: &AppState<M>,
498 context: &mut HotReloadContext<M>,
499 create_handlers: F,
500) -> ReloadResult<M>
501where
502 M: UiBindable + Serialize + DeserializeOwned + Default + Send + 'static,
503 F: FnOnce() -> dampen_core::handler::HandlerRegistry + Send + 'static,
504{
505 let reload_start = Instant::now();
506
507 if let Err(e) = context.snapshot_model(¤t_state.model) {
509 eprintln!("Warning: Failed to snapshot model: {}", e);
510 }
511
512 let model_snapshot = context.last_model_snapshot.clone();
514
515 let new_document = if let Some(cached_doc) = context.get_cached_document(&xml_source) {
517 cached_doc
519 } else {
520 let xml_for_parse = Arc::clone(&xml_source);
522 let parse_result =
523 tokio::task::spawn_blocking(move || dampen_core::parser::parse(&xml_for_parse)).await;
524
525 match parse_result {
526 Ok(Ok(doc)) => {
527 context.cache_document(&xml_source, doc.clone());
528 doc
529 }
530 Ok(Err(err)) => {
531 context.record_reload(false);
532 return ReloadResult::ParseError(err);
533 }
534 Err(join_err) => {
535 context.record_reload(false);
536 let error = ParseError {
537 kind: dampen_core::parser::error::ParseErrorKind::XmlSyntax,
538 span: dampen_core::ir::span::Span::default(),
539 message: format!("Async parsing failed: {}", join_err),
540 suggestion: Some(
541 "Check if the XML file is accessible and not corrupted".to_string(),
542 ),
543 };
544 return ReloadResult::ParseError(error);
545 }
546 }
547 };
548
549 let new_handlers = create_handlers();
551
552 if let Err(missing_handlers) = validate_handlers(&new_document, &new_handlers) {
554 context.record_reload(false);
555 let error_messages: Vec<String> = missing_handlers
556 .iter()
557 .map(|h| format!("Handler '{}' is referenced but not registered", h))
558 .collect();
559 return ReloadResult::ValidationError(error_messages);
560 }
561
562 let restored_model = match model_snapshot {
564 Some(json) => match serde_json::from_str::<M>(&json) {
565 Ok(model) => model,
566 Err(e) => {
567 eprintln!("Warning: Failed to restore model ({}), using default", e);
568 let new_state = AppState::with_all(new_document, M::default(), new_handlers);
569 context.record_reload(true);
570 return ReloadResult::StateRestoreWarning(
571 new_state,
572 format!("Failed to deserialize model: {}", e),
573 );
574 }
575 },
576 None => {
577 eprintln!("Warning: No model snapshot available, using default");
578 let new_state = AppState::with_all(new_document, M::default(), new_handlers);
579 context.record_reload(true);
580 return ReloadResult::StateRestoreWarning(
581 new_state,
582 "No model snapshot available".to_string(),
583 );
584 }
585 };
586
587 let new_state = AppState::with_all(new_document, restored_model, new_handlers);
589
590 let elapsed = reload_start.elapsed();
591 context.record_reload_with_timing(true, elapsed);
592 ReloadResult::Success(new_state)
593}
594
595fn collect_handler_names(document: &dampen_core::ir::DampenDocument) -> Vec<String> {
608 use std::collections::HashSet;
609
610 let mut handlers = HashSet::new();
611 collect_handlers_from_node(&document.root, &mut handlers);
612 handlers.into_iter().collect()
613}
614
615fn collect_handlers_from_node(
617 node: &dampen_core::ir::node::WidgetNode,
618 handlers: &mut std::collections::HashSet<String>,
619) {
620 for event in &node.events {
622 handlers.insert(event.handler.clone());
623 }
624
625 for child in &node.children {
627 collect_handlers_from_node(child, handlers);
628 }
629}
630
631fn validate_handlers(
642 document: &dampen_core::ir::DampenDocument,
643 registry: &dampen_core::handler::HandlerRegistry,
644) -> Result<(), Vec<String>> {
645 let referenced_handlers = collect_handler_names(document);
646 let mut missing_handlers = Vec::new();
647
648 for handler_name in referenced_handlers {
649 if registry.get(&handler_name).is_none() {
650 missing_handlers.push(handler_name);
651 }
652 }
653
654 if missing_handlers.is_empty() {
655 Ok(())
656 } else {
657 Err(missing_handlers)
658 }
659}
660
661#[derive(Debug)]
663pub enum ThemeReloadResult {
664 Success,
666
667 ParseError(String),
669
670 ValidationError(String),
672
673 NoThemeContext,
675
676 FileNotFound,
678}
679
680pub fn attempt_theme_hot_reload(
725 theme_path: &std::path::Path,
726 theme_context: &mut Option<dampen_core::state::ThemeContext>,
727) -> ThemeReloadResult {
728 let theme_context = match theme_context {
729 Some(ctx) => ctx,
730 None => return ThemeReloadResult::NoThemeContext,
731 };
732
733 let content = match std::fs::read_to_string(theme_path) {
734 Ok(c) => c,
735 Err(e) => return ThemeReloadResult::ParseError(format!("Failed to read file: {}", e)),
736 };
737
738 let new_doc = match dampen_core::parser::theme_parser::parse_theme_document(&content) {
739 Ok(doc) => doc,
740 Err(e) => {
741 return ThemeReloadResult::ParseError(format!(
742 "Failed to parse theme document: {}",
743 e.message
744 ));
745 }
746 };
747
748 if let Err(e) = new_doc.validate() {
749 return ThemeReloadResult::ValidationError(e.to_string());
750 }
751
752 theme_context.reload(new_doc);
753 ThemeReloadResult::Success
754}
755
756pub fn is_theme_file_path(path: &std::path::Path) -> bool {
758 path.file_name()
759 .and_then(|n| n.to_str())
760 .map(|n| n == "theme.dampen")
761 .unwrap_or(false)
762}
763
764pub fn get_theme_dir_from_path(path: &std::path::Path) -> Option<std::path::PathBuf> {
769 let path = std::fs::canonicalize(path).ok()?;
770 let theme_file_name = path.file_name()?;
771
772 if theme_file_name == "theme.dampen" {
773 return Some(path.parent()?.to_path_buf());
774 }
775
776 None
777}
778
779#[cfg(test)]
780mod tests {
781 use super::*;
782 use serde::{Deserialize, Serialize};
783
784 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
785 struct TestModel {
786 count: i32,
787 name: String,
788 }
789
790 impl UiBindable for TestModel {
791 fn get_field(&self, _path: &[&str]) -> Option<dampen_core::binding::BindingValue> {
792 None
793 }
794
795 fn available_fields() -> Vec<String> {
796 vec![]
797 }
798 }
799
800 impl Default for TestModel {
801 fn default() -> Self {
802 Self {
803 count: 0,
804 name: "default".to_string(),
805 }
806 }
807 }
808
809 #[test]
810 fn test_snapshot_model_success() {
811 let mut context = HotReloadContext::<TestModel>::new();
812 let model = TestModel {
813 count: 42,
814 name: "Alice".to_string(),
815 };
816
817 let result = context.snapshot_model(&model);
818 assert!(result.is_ok());
819 assert!(context.last_model_snapshot.is_some());
820 }
821
822 #[test]
823 fn test_restore_model_success() {
824 let mut context = HotReloadContext::<TestModel>::new();
825 let original = TestModel {
826 count: 42,
827 name: "Alice".to_string(),
828 };
829
830 context.snapshot_model(&original).unwrap();
832
833 let restored = context.restore_model().unwrap();
835 assert_eq!(restored, original);
836 }
837
838 #[test]
839 fn test_restore_model_no_snapshot() {
840 let context = HotReloadContext::<TestModel>::new();
841
842 let result = context.restore_model();
844 assert!(result.is_err());
845 assert!(result.unwrap_err().contains("No model snapshot"));
846 }
847
848 #[test]
849 fn test_snapshot_restore_round_trip() {
850 let mut context = HotReloadContext::<TestModel>::new();
851 let original = TestModel {
852 count: 999,
853 name: "Bob".to_string(),
854 };
855
856 context.snapshot_model(&original).unwrap();
858
859 let mut modified = original.clone();
860 modified.count = 0;
861 modified.name = "Changed".to_string();
862
863 let restored = context.restore_model().unwrap();
865 assert_eq!(restored, original);
866 assert_ne!(restored, modified);
867 }
868
869 #[test]
870 fn test_multiple_snapshots() {
871 let mut context = HotReloadContext::<TestModel>::new();
872
873 let model1 = TestModel {
875 count: 1,
876 name: "First".to_string(),
877 };
878 context.snapshot_model(&model1).unwrap();
879
880 let model2 = TestModel {
882 count: 2,
883 name: "Second".to_string(),
884 };
885 context.snapshot_model(&model2).unwrap();
886
887 let restored = context.restore_model().unwrap();
889 assert_eq!(restored, model2);
890 assert_ne!(restored, model1);
891 }
892
893 #[test]
894 fn test_record_reload() {
895 let mut context = HotReloadContext::<TestModel>::new();
896
897 assert_eq!(context.reload_count, 0);
898 assert!(context.error.is_none());
899
900 context.record_reload(true);
902 assert_eq!(context.reload_count, 1);
903 assert!(context.error.is_none());
904
905 context.record_reload(false);
907 assert_eq!(context.reload_count, 2);
908 assert!(context.error.is_some());
909
910 context.record_reload(true);
912 assert_eq!(context.reload_count, 3);
913 assert!(context.error.is_none());
914 }
915
916 #[test]
917 fn test_cache_hit_rate_calculated_correctly() {
918 use std::sync::atomic::Ordering;
919
920 let mut context = HotReloadContext::<TestModel>::new();
921
922 assert_eq!(context.calculate_cache_hit_rate(), 0.0);
924
925 context.cache_hits.store(3, Ordering::Relaxed);
927 context.cache_misses.store(2, Ordering::Relaxed);
928
929 assert_eq!(context.calculate_cache_hit_rate(), 0.6);
931 }
932
933 #[test]
934 fn test_cache_hit_rate_zero_division() {
935 let context = HotReloadContext::<TestModel>::new();
936 assert_eq!(context.calculate_cache_hit_rate(), 0.0);
938 }
939
940 #[test]
941 fn test_cache_hit_rate_full_misses() {
942 use std::sync::atomic::Ordering;
943
944 let mut context = HotReloadContext::<TestModel>::new();
945 context.cache_hits.store(0, Ordering::Relaxed);
946 context.cache_misses.store(5, Ordering::Relaxed);
947 assert_eq!(context.calculate_cache_hit_rate(), 0.0);
948 }
949
950 #[test]
951 fn test_cache_hit_rate_full_hits() {
952 use std::sync::atomic::Ordering;
953
954 let mut context = HotReloadContext::<TestModel>::new();
955 context.cache_hits.store(5, Ordering::Relaxed);
956 context.cache_misses.store(0, Ordering::Relaxed);
957 assert_eq!(context.calculate_cache_hit_rate(), 1.0);
958 }
959
960 #[test]
961 fn test_attempt_hot_reload_success() {
962 use dampen_core::handler::HandlerRegistry;
963 use dampen_core::parser;
964
965 let xml_v1 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="Version 1" /></column></dampen>"#;
967 let doc_v1 = parser::parse(xml_v1).unwrap();
968 let model_v1 = TestModel {
969 count: 42,
970 name: "Alice".to_string(),
971 };
972 let registry_v1 = HandlerRegistry::new();
973 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
974
975 let mut context = HotReloadContext::<TestModel>::new();
977
978 let xml_v2 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="Version 2" /></column></dampen>"#;
980
981 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
983
984 match result {
986 ReloadResult::Success(new_state) => {
987 assert_eq!(new_state.model.count, 42);
988 assert_eq!(new_state.model.name, "Alice");
989 assert_eq!(context.reload_count, 1);
990 }
991 _ => panic!("Expected Success, got {:?}", result),
992 }
993 }
994
995 #[test]
996 fn test_attempt_hot_reload_parse_error() {
997 use dampen_core::handler::HandlerRegistry;
998 use dampen_core::parser;
999
1000 let xml_v1 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="Version 1" /></column></dampen>"#;
1002 let doc_v1 = parser::parse(xml_v1).unwrap();
1003 let model_v1 = TestModel {
1004 count: 10,
1005 name: "Bob".to_string(),
1006 };
1007 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1008
1009 let mut context = HotReloadContext::<TestModel>::new();
1010
1011 let xml_invalid = r#"<dampen version="1.1" encoding="utf-8"><column><text value="Broken"#;
1013
1014 let result = attempt_hot_reload(xml_invalid, &state_v1, &mut context, || {
1016 HandlerRegistry::new()
1017 });
1018
1019 match result {
1021 ReloadResult::ParseError(_err) => {
1022 assert_eq!(context.reload_count, 1); }
1025 _ => panic!("Expected ParseError, got {:?}", result),
1026 }
1027 }
1028
1029 #[test]
1030 fn test_attempt_hot_reload_model_restore_failure() {
1031 use dampen_core::handler::HandlerRegistry;
1032 use dampen_core::parser;
1033
1034 let xml_v1 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="Version 1" /></column></dampen>"#;
1036 let doc_v1 = parser::parse(xml_v1).unwrap();
1037 let model_v1 = TestModel {
1038 count: 99,
1039 name: "Charlie".to_string(),
1040 };
1041 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1042
1043 let mut context = HotReloadContext::<TestModel>::new();
1045 context.last_model_snapshot = Some("{ invalid json }".to_string()); let xml_v2 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="Version 2" /></column></dampen>"#;
1049
1050 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
1052
1053 match result {
1060 ReloadResult::Success(new_state) => {
1061 assert_eq!(new_state.model.count, 99);
1063 assert_eq!(new_state.model.name, "Charlie");
1064 assert_eq!(context.reload_count, 1);
1065 }
1066 _ => panic!("Expected Success, got {:?}", result),
1067 }
1068 }
1069
1070 #[test]
1071 fn test_attempt_hot_reload_preserves_model_across_multiple_reloads() {
1072 use dampen_core::handler::HandlerRegistry;
1073 use dampen_core::parser;
1074
1075 let xml_v1 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="V1" /></column></dampen>"#;
1077 let doc_v1 = parser::parse(xml_v1).unwrap();
1078 let model_v1 = TestModel {
1079 count: 100,
1080 name: "Dave".to_string(),
1081 };
1082 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1083
1084 let mut context = HotReloadContext::<TestModel>::new();
1085
1086 let xml_v2 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="V2" /></column></dampen>"#;
1088 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
1089
1090 let state_v2 = match result {
1091 ReloadResult::Success(s) => s,
1092 _ => panic!("First reload failed"),
1093 };
1094
1095 assert_eq!(state_v2.model.count, 100);
1096 assert_eq!(state_v2.model.name, "Dave");
1097
1098 let xml_v3 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="V3" /></column></dampen>"#;
1100 let result = attempt_hot_reload(xml_v3, &state_v2, &mut context, || HandlerRegistry::new());
1101
1102 let state_v3 = match result {
1103 ReloadResult::Success(s) => s,
1104 _ => panic!("Second reload failed"),
1105 };
1106
1107 assert_eq!(state_v3.model.count, 100);
1109 assert_eq!(state_v3.model.name, "Dave");
1110 assert_eq!(context.reload_count, 2);
1111 }
1112
1113 #[test]
1114 fn test_attempt_hot_reload_with_handler_registry() {
1115 use dampen_core::handler::HandlerRegistry;
1116 use dampen_core::parser;
1117
1118 let xml_v1 = r#"<dampen version="1.1" encoding="utf-8"><column><button label="Click" on_click="test" /></column></dampen>"#;
1120 let doc_v1 = parser::parse(xml_v1).unwrap();
1121 let model_v1 = TestModel {
1122 count: 5,
1123 name: "Eve".to_string(),
1124 };
1125
1126 let registry_v1 = HandlerRegistry::new();
1127 registry_v1.register_simple("test", |_model| {
1128 });
1130
1131 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
1132
1133 let mut context = HotReloadContext::<TestModel>::new();
1134
1135 let xml_v2 = r#"<dampen version="1.1" encoding="utf-8"><column><button label="Click Me" on_click="test2" /></column></dampen>"#;
1137
1138 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1140 let registry = HandlerRegistry::new();
1141 registry.register_simple("test2", |_model| {
1142 });
1144 registry
1145 });
1146
1147 match result {
1149 ReloadResult::Success(new_state) => {
1150 assert_eq!(new_state.model.count, 5);
1152 assert_eq!(new_state.model.name, "Eve");
1153
1154 assert!(new_state.handler_registry.get("test2").is_some());
1156 }
1157 _ => panic!("Expected Success, got {:?}", result),
1158 }
1159 }
1160
1161 #[test]
1162 fn test_collect_handler_names() {
1163 use dampen_core::parser;
1164
1165 let xml = r#"
1167 <dampen version="1.1" encoding="utf-8">
1168 <column>
1169 <button label="Click" on_click="handle_click" />
1170 <text_input placeholder="Type" on_input="handle_input" />
1171 <button label="Submit" on_click="handle_submit" />
1172 </column>
1173 </dampen>
1174 "#;
1175
1176 let doc = parser::parse(xml).unwrap();
1177 let handlers = collect_handler_names(&doc);
1178
1179 assert_eq!(handlers.len(), 3);
1181 assert!(handlers.contains(&"handle_click".to_string()));
1182 assert!(handlers.contains(&"handle_input".to_string()));
1183 assert!(handlers.contains(&"handle_submit".to_string()));
1184 }
1185
1186 #[test]
1187 fn test_collect_handler_names_nested() {
1188 use dampen_core::parser;
1189
1190 let xml = r#"
1192 <dampen version="1.1" encoding="utf-8">
1193 <column>
1194 <row>
1195 <button label="A" on_click="handler_a" />
1196 </row>
1197 <row>
1198 <button label="B" on_click="handler_b" />
1199 <column>
1200 <button label="C" on_click="handler_c" />
1201 </column>
1202 </row>
1203 </column>
1204 </dampen>
1205 "#;
1206
1207 let doc = parser::parse(xml).unwrap();
1208 let handlers = collect_handler_names(&doc);
1209
1210 assert_eq!(handlers.len(), 3);
1212 assert!(handlers.contains(&"handler_a".to_string()));
1213 assert!(handlers.contains(&"handler_b".to_string()));
1214 assert!(handlers.contains(&"handler_c".to_string()));
1215 }
1216
1217 #[test]
1218 fn test_collect_handler_names_duplicates() {
1219 use dampen_core::parser;
1220
1221 let xml = r#"
1223 <dampen version="1.1" encoding="utf-8">
1224 <column>
1225 <button label="1" on_click="same_handler" />
1226 <button label="2" on_click="same_handler" />
1227 <button label="3" on_click="same_handler" />
1228 </column>
1229 </dampen>
1230 "#;
1231
1232 let doc = parser::parse(xml).unwrap();
1233 let handlers = collect_handler_names(&doc);
1234
1235 assert_eq!(handlers.len(), 1);
1237 assert!(handlers.contains(&"same_handler".to_string()));
1238 }
1239
1240 #[test]
1241 fn test_validate_handlers_all_present() {
1242 use dampen_core::handler::HandlerRegistry;
1243 use dampen_core::parser;
1244
1245 let xml = r#"
1246 <dampen version="1.1" encoding="utf-8">
1247 <column>
1248 <button label="Click" on_click="test_handler" />
1249 </column>
1250 </dampen>
1251 "#;
1252
1253 let doc = parser::parse(xml).unwrap();
1254 let registry = HandlerRegistry::new();
1255 registry.register_simple("test_handler", |_model| {});
1256
1257 let result = validate_handlers(&doc, ®istry);
1258 assert!(result.is_ok());
1259 }
1260
1261 #[test]
1262 fn test_validate_handlers_missing() {
1263 use dampen_core::handler::HandlerRegistry;
1264 use dampen_core::parser;
1265
1266 let xml = r#"
1267 <dampen version="1.1" encoding="utf-8">
1268 <column>
1269 <button label="Click" on_click="missing_handler" />
1270 </column>
1271 </dampen>
1272 "#;
1273
1274 let doc = parser::parse(xml).unwrap();
1275 let registry = HandlerRegistry::new();
1276 let result = validate_handlers(&doc, ®istry);
1279 assert!(result.is_err());
1280
1281 let missing = result.unwrap_err();
1282 assert_eq!(missing.len(), 1);
1283 assert_eq!(missing[0], "missing_handler");
1284 }
1285
1286 #[test]
1287 fn test_validate_handlers_multiple_missing() {
1288 use dampen_core::handler::HandlerRegistry;
1289 use dampen_core::parser;
1290
1291 let xml = r#"
1292 <dampen version="1.1" encoding="utf-8">
1293 <column>
1294 <button label="A" on_click="handler_a" />
1295 <button label="B" on_click="handler_b" />
1296 <button label="C" on_click="handler_c" />
1297 </column>
1298 </dampen>
1299 "#;
1300
1301 let doc = parser::parse(xml).unwrap();
1302 let registry = HandlerRegistry::new();
1303 registry.register_simple("handler_b", |_model| {});
1305
1306 let result = validate_handlers(&doc, ®istry);
1307 assert!(result.is_err());
1308
1309 let missing = result.unwrap_err();
1310 assert_eq!(missing.len(), 2);
1311 assert!(missing.contains(&"handler_a".to_string()));
1312 assert!(missing.contains(&"handler_c".to_string()));
1313 }
1314
1315 #[test]
1316 fn test_attempt_hot_reload_validation_error() {
1317 use dampen_core::handler::HandlerRegistry;
1318 use dampen_core::parser;
1319
1320 let xml_v1 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="V1" /></column></dampen>"#;
1322 let doc_v1 = parser::parse(xml_v1).unwrap();
1323 let model_v1 = TestModel {
1324 count: 10,
1325 name: "Test".to_string(),
1326 };
1327 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1328
1329 let mut context = HotReloadContext::<TestModel>::new();
1330
1331 let xml_v2 = r#"
1333 <dampen version="1.1" encoding="utf-8">
1334 <column>
1335 <button label="Click" on_click="unregistered_handler" />
1336 </column>
1337 </dampen>
1338 "#;
1339
1340 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1342 HandlerRegistry::new() });
1344
1345 match result {
1347 ReloadResult::ValidationError(errors) => {
1348 assert!(!errors.is_empty());
1349 assert!(errors[0].contains("unregistered_handler"));
1350 assert_eq!(context.reload_count, 1); }
1352 _ => panic!("Expected ValidationError, got {:?}", result),
1353 }
1354 }
1355
1356 #[test]
1357 fn test_attempt_hot_reload_validation_success() {
1358 use dampen_core::handler::HandlerRegistry;
1359 use dampen_core::parser;
1360
1361 let xml_v1 = r#"<dampen version="1.1" encoding="utf-8"><column><text value="V1" /></column></dampen>"#;
1363 let doc_v1 = parser::parse(xml_v1).unwrap();
1364 let model_v1 = TestModel {
1365 count: 20,
1366 name: "Valid".to_string(),
1367 };
1368 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1369
1370 let mut context = HotReloadContext::<TestModel>::new();
1371
1372 let xml_v2 = r#"
1374 <dampen version="1.1" encoding="utf-8">
1375 <column>
1376 <button label="Click" on_click="registered_handler" />
1377 </column>
1378 </dampen>
1379 "#;
1380
1381 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1383 let registry = HandlerRegistry::new();
1384 registry.register_simple("registered_handler", |_model| {});
1385 registry
1386 });
1387
1388 match result {
1390 ReloadResult::Success(new_state) => {
1391 assert_eq!(new_state.model.count, 20);
1392 assert_eq!(new_state.model.name, "Valid");
1393 assert_eq!(context.reload_count, 1);
1394 }
1395 _ => panic!("Expected Success, got {:?}", result),
1396 }
1397 }
1398
1399 #[test]
1400 fn test_handler_registry_complete_replacement() {
1401 use dampen_core::handler::HandlerRegistry;
1402 use dampen_core::parser;
1403
1404 let xml_v1 = r#"
1406 <dampen version="1.1" encoding="utf-8">
1407 <column>
1408 <button label="Old" on_click="old_handler" />
1409 </column>
1410 </dampen>
1411 "#;
1412 let doc_v1 = parser::parse(xml_v1).unwrap();
1413 let model_v1 = TestModel {
1414 count: 1,
1415 name: "Initial".to_string(),
1416 };
1417
1418 let registry_v1 = HandlerRegistry::new();
1419 registry_v1.register_simple("old_handler", |_model| {});
1420
1421 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
1422
1423 assert!(state_v1.handler_registry.get("old_handler").is_some());
1425
1426 let mut context = HotReloadContext::<TestModel>::new();
1427
1428 let xml_v2 = r#"
1430 <dampen version="1.1" encoding="utf-8">
1431 <column>
1432 <button label="New" on_click="new_handler" />
1433 <button label="Another" on_click="another_handler" />
1434 </column>
1435 </dampen>
1436 "#;
1437
1438 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1440 let registry = HandlerRegistry::new();
1441 registry.register_simple("new_handler", |_model| {});
1442 registry.register_simple("another_handler", |_model| {});
1443 registry
1444 });
1445
1446 match result {
1448 ReloadResult::Success(new_state) => {
1449 assert_eq!(new_state.model.count, 1);
1451 assert_eq!(new_state.model.name, "Initial");
1452
1453 assert!(new_state.handler_registry.get("old_handler").is_none());
1455
1456 assert!(new_state.handler_registry.get("new_handler").is_some());
1458 assert!(new_state.handler_registry.get("another_handler").is_some());
1459 }
1460 _ => panic!("Expected Success, got {:?}", result),
1461 }
1462 }
1463
1464 #[test]
1465 fn test_handler_registry_rebuild_before_validation() {
1466 use dampen_core::handler::HandlerRegistry;
1467 use dampen_core::parser;
1468
1469 let xml_v1 = r#"<dampen version="1.1" encoding="utf-8"><column><button on_click="handler_a" label="A" /></column></dampen>"#;
1474 let doc_v1 = parser::parse(xml_v1).unwrap();
1475 let model_v1 = TestModel {
1476 count: 100,
1477 name: "Test".to_string(),
1478 };
1479
1480 let registry_v1 = HandlerRegistry::new();
1481 registry_v1.register_simple("handler_a", |_model| {});
1482
1483 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
1484
1485 let mut context = HotReloadContext::<TestModel>::new();
1486
1487 let xml_v2 = r#"<dampen version="1.1" encoding="utf-8"><column><button on_click="handler_b" label="B" /></column></dampen>"#;
1489
1490 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1492 let registry = HandlerRegistry::new();
1493 registry.register_simple("handler_b", |_model| {}); registry
1495 });
1496
1497 match result {
1499 ReloadResult::Success(new_state) => {
1500 assert_eq!(new_state.model.count, 100);
1501 assert!(new_state.handler_registry.get("handler_b").is_some());
1503 assert!(new_state.handler_registry.get("handler_a").is_none());
1505 }
1506 _ => panic!(
1507 "Expected Success (registry rebuilt before validation), got {:?}",
1508 result
1509 ),
1510 }
1511 }
1512}
1513
1514#[cfg(test)]
1515mod theme_reload_tests {
1516 use super::*;
1517 use dampen_core::ir::style::Color;
1518 use dampen_core::ir::theme::{SpacingScale, Theme, ThemeDocument, ThemePalette, Typography};
1519 use tempfile::TempDir;
1520
1521 fn create_test_palette(primary: &str) -> ThemePalette {
1522 ThemePalette {
1523 primary: Some(Color::from_hex(primary).unwrap()),
1524 secondary: Some(Color::from_hex("#2ecc71").unwrap()),
1525 success: Some(Color::from_hex("#27ae60").unwrap()),
1526 warning: Some(Color::from_hex("#f39c12").unwrap()),
1527 danger: Some(Color::from_hex("#e74c3c").unwrap()),
1528 background: Some(Color::from_hex("#ecf0f1").unwrap()),
1529 surface: Some(Color::from_hex("#ffffff").unwrap()),
1530 text: Some(Color::from_hex("#2c3e50").unwrap()),
1531 text_secondary: Some(Color::from_hex("#7f8c8d").unwrap()),
1532 }
1533 }
1534
1535 fn create_test_theme(name: &str, primary: &str) -> Theme {
1536 Theme {
1537 name: name.to_string(),
1538 palette: create_test_palette(primary),
1539 typography: Typography {
1540 font_family: Some("sans-serif".to_string()),
1541 font_size_base: Some(16.0),
1542 font_size_small: Some(12.0),
1543 font_size_large: Some(24.0),
1544 font_weight: dampen_core::ir::theme::FontWeight::Normal,
1545 line_height: Some(1.5),
1546 },
1547 spacing: SpacingScale { unit: Some(8.0) },
1548 base_styles: std::collections::HashMap::new(),
1549 extends: None,
1550 }
1551 }
1552
1553 fn create_test_document() -> ThemeDocument {
1554 ThemeDocument {
1555 themes: std::collections::HashMap::from([
1556 ("light".to_string(), create_test_theme("light", "#3498db")),
1557 ("dark".to_string(), create_test_theme("dark", "#5dade2")),
1558 ]),
1559 default_theme: Some("light".to_string()),
1560 follow_system: true,
1561 }
1562 }
1563
1564 fn create_test_theme_context() -> dampen_core::state::ThemeContext {
1565 let doc = create_test_document();
1566 dampen_core::state::ThemeContext::from_document(doc, None).unwrap()
1567 }
1568
1569 #[test]
1570 fn test_attempt_theme_hot_reload_success() {
1571 let temp_dir = TempDir::new().unwrap();
1572 let theme_dir = temp_dir.path().join("src/ui/theme");
1573 std::fs::create_dir_all(&theme_dir).unwrap();
1574
1575 let theme_content = r##"
1576 <dampen version="1.1" encoding="utf-8">
1577 <themes>
1578 <theme name="light">
1579 <palette
1580 primary="#111111"
1581 secondary="#2ecc71"
1582 success="#27ae60"
1583 warning="#f39c12"
1584 danger="#e74c3c"
1585 background="#ecf0f1"
1586 surface="#ffffff"
1587 text="#2c3e50"
1588 text_secondary="#7f8c8d" />
1589 </theme>
1590 </themes>
1591 <default_theme name="light" />
1592 </dampen>
1593 "##;
1594
1595 let theme_path = theme_dir.join("theme.dampen");
1596 std::fs::write(&theme_path, theme_content).unwrap();
1597
1598 let mut theme_ctx = Some(create_test_theme_context());
1599 let result = attempt_theme_hot_reload(&theme_path, &mut theme_ctx);
1600
1601 match result {
1602 ThemeReloadResult::Success => {
1603 let ctx = theme_ctx.unwrap();
1604 assert_eq!(ctx.active_name(), "light");
1605 assert_eq!(
1606 ctx.active().palette.primary,
1607 Some(Color::from_hex("#111111").unwrap())
1608 );
1609 }
1610 _ => panic!("Expected Success, got {:?}", result),
1611 }
1612 }
1613
1614 #[test]
1615 fn test_attempt_theme_hot_reload_parse_error() {
1616 let temp_dir = TempDir::new().unwrap();
1617 let theme_dir = temp_dir.path().join("src/ui/theme");
1618 std::fs::create_dir_all(&theme_dir).unwrap();
1619
1620 let theme_path = theme_dir.join("theme.dampen");
1621 std::fs::write(&theme_path, "invalid xml").unwrap();
1622
1623 let mut theme_ctx = Some(create_test_theme_context());
1624 let result = attempt_theme_hot_reload(&theme_path, &mut theme_ctx);
1625
1626 match result {
1627 ThemeReloadResult::ParseError(_) => {}
1628 _ => panic!("Expected ParseError, got {:?}", result),
1629 }
1630 }
1631
1632 #[test]
1633 fn test_attempt_theme_hot_reload_no_theme_context() {
1634 let temp_dir = TempDir::new().unwrap();
1635 let theme_dir = temp_dir.path().join("src/ui/theme");
1636 std::fs::create_dir_all(&theme_dir).unwrap();
1637
1638 let theme_path = theme_dir.join("theme.dampen");
1639 std::fs::write(
1640 &theme_path,
1641 r#"<dampen version="1.1" encoding="utf-8"><themes></themes></dampen>"#,
1642 )
1643 .unwrap();
1644
1645 let mut theme_ctx: Option<dampen_core::state::ThemeContext> = None;
1646 let result = attempt_theme_hot_reload(&theme_path, &mut theme_ctx);
1647
1648 match result {
1649 ThemeReloadResult::NoThemeContext => {}
1650 _ => panic!("Expected NoThemeContext, got {:?}", result),
1651 }
1652 }
1653
1654 #[test]
1655 fn test_attempt_theme_hot_reload_preserves_active_theme() {
1656 let temp_dir = TempDir::new().unwrap();
1657 let theme_dir = temp_dir.path().join("src/ui/theme");
1658 std::fs::create_dir_all(&theme_dir).unwrap();
1659
1660 let theme_content = r##"
1661 <dampen version="1.1" encoding="utf-8">
1662 <themes>
1663 <theme name="new_theme">
1664 <palette
1665 primary="#ff0000"
1666 secondary="#2ecc71"
1667 success="#27ae60"
1668 warning="#f39c12"
1669 danger="#e74c3c"
1670 background="#ecf0f1"
1671 surface="#ffffff"
1672 text="#2c3e50"
1673 text_secondary="#7f8c8d" />
1674 </theme>
1675 <theme name="dark">
1676 <palette
1677 primary="#5dade2"
1678 secondary="#52be80"
1679 success="#27ae60"
1680 warning="#f39c12"
1681 danger="#ec7063"
1682 background="#2c3e50"
1683 surface="#34495e"
1684 text="#ecf0f1"
1685 text_secondary="#95a5a6" />
1686 </theme>
1687 </themes>
1688 <default_theme name="new_theme" />
1689 </dampen>
1690 "##;
1691
1692 let theme_path = theme_dir.join("theme.dampen");
1693 std::fs::write(&theme_path, theme_content).unwrap();
1694
1695 let mut theme_ctx = Some(create_test_theme_context());
1696 theme_ctx.as_mut().unwrap().set_theme("dark").unwrap();
1697 assert_eq!(theme_ctx.as_ref().unwrap().active_name(), "dark");
1698
1699 let result = attempt_theme_hot_reload(&theme_path, &mut theme_ctx);
1700
1701 match result {
1702 ThemeReloadResult::Success => {
1703 let ctx = theme_ctx.unwrap();
1704 assert_eq!(ctx.active_name(), "dark");
1705 }
1706 _ => panic!("Expected Success, got {:?}", result),
1707 }
1708 }
1709
1710 #[test]
1711 fn test_is_theme_file_path() {
1712 assert!(is_theme_file_path(&std::path::PathBuf::from(
1713 "src/ui/theme/theme.dampen"
1714 )));
1715 assert!(!is_theme_file_path(&std::path::PathBuf::from(
1716 "src/ui/window.dampen"
1717 )));
1718 assert!(is_theme_file_path(&std::path::PathBuf::from(
1719 "/some/path/theme.dampen"
1720 )));
1721 }
1722
1723 #[test]
1724 fn test_get_theme_dir_from_path() {
1725 let temp_dir = TempDir::new().unwrap();
1726 let theme_dir = temp_dir.path().join("src/ui/theme");
1727 std::fs::create_dir_all(&theme_dir).unwrap();
1728 let theme_path = theme_dir.join("theme.dampen");
1729 std::fs::write(
1730 &theme_path,
1731 "<dampen version=\"1.0\"><themes></themes></dampen>",
1732 )
1733 .unwrap();
1734
1735 let result = get_theme_dir_from_path(&theme_path);
1736 assert!(result.is_some());
1737 assert_eq!(result.unwrap(), theme_dir);
1738 }
1739}