oxihuman_core/
hot_reload.rs1#[allow(dead_code)]
5#[derive(Clone, PartialEq, Debug)]
6pub enum ChangeKind {
7 Modified,
8 Created,
9 Deleted,
10 Renamed,
11}
12
13#[allow(dead_code)]
14#[derive(Clone)]
15pub struct FileChange {
16 pub path: String,
17 pub kind: ChangeKind,
18 pub timestamp_ms: u64,
19}
20
21#[allow(dead_code)]
22pub struct HotReloadConfig {
23 pub poll_interval_ms: u64,
24 pub debounce_ms: u64,
25 pub watch_extensions: Vec<String>,
26}
27
28#[allow(dead_code)]
29pub struct HotReloadWatcher {
30 pub watched_paths: Vec<String>,
31 pub changes: Vec<FileChange>,
32 pub config: HotReloadConfig,
33 pub enabled: bool,
34 pub simulated_time_ms: u64,
35}
36
37#[allow(dead_code)]
38pub fn default_hot_reload_config() -> HotReloadConfig {
39 HotReloadConfig {
40 poll_interval_ms: 500,
41 debounce_ms: 100,
42 watch_extensions: vec![
43 "obj".to_string(),
44 "mtl".to_string(),
45 "glb".to_string(),
46 "gltf".to_string(),
47 "png".to_string(),
48 "json".to_string(),
49 ],
50 }
51}
52
53#[allow(dead_code)]
54pub fn new_watcher(cfg: HotReloadConfig) -> HotReloadWatcher {
55 HotReloadWatcher {
56 watched_paths: Vec::new(),
57 changes: Vec::new(),
58 config: cfg,
59 enabled: true,
60 simulated_time_ms: 0,
61 }
62}
63
64#[allow(dead_code)]
65pub fn watch_path(watcher: &mut HotReloadWatcher, path: &str) {
66 if !watcher.watched_paths.contains(&path.to_string()) {
67 watcher.watched_paths.push(path.to_string());
68 }
69}
70
71#[allow(dead_code)]
73pub fn unwatch_path(watcher: &mut HotReloadWatcher, path: &str) -> bool {
74 let before = watcher.watched_paths.len();
75 watcher.watched_paths.retain(|p| p != path);
76 watcher.watched_paths.len() < before
77}
78
79#[allow(dead_code)]
81pub fn simulate_file_change(watcher: &mut HotReloadWatcher, path: &str, kind: ChangeKind) {
82 if !watcher.enabled {
83 return;
84 }
85 watcher.changes.push(FileChange {
86 path: path.to_string(),
87 kind,
88 timestamp_ms: watcher.simulated_time_ms,
89 });
90}
91
92#[allow(dead_code)]
93pub fn pending_changes(watcher: &HotReloadWatcher) -> &[FileChange] {
94 &watcher.changes
95}
96
97#[allow(dead_code)]
98pub fn clear_changes(watcher: &mut HotReloadWatcher) {
99 watcher.changes.clear();
100}
101
102#[allow(dead_code)]
103pub fn is_watched(watcher: &HotReloadWatcher, path: &str) -> bool {
104 watcher.watched_paths.contains(&path.to_string())
105}
106
107#[allow(dead_code)]
108pub fn changes_for_path<'a>(watcher: &'a HotReloadWatcher, path: &str) -> Vec<&'a FileChange> {
109 watcher.changes.iter().filter(|c| c.path == path).collect()
110}
111
112#[allow(dead_code)]
114pub fn extension_matches(watcher: &HotReloadWatcher, path: &str) -> bool {
115 let ext = path.rsplit('.').next().unwrap_or("");
116 watcher.config.watch_extensions.iter().any(|e| e == ext)
117}
118
119#[allow(dead_code)]
120pub fn watched_path_count(watcher: &HotReloadWatcher) -> usize {
121 watcher.watched_paths.len()
122}
123
124#[allow(dead_code)]
125pub fn change_count(watcher: &HotReloadWatcher) -> usize {
126 watcher.changes.len()
127}
128
129#[allow(dead_code)]
130pub fn enable_watcher(watcher: &mut HotReloadWatcher) {
131 watcher.enabled = true;
132}
133
134#[allow(dead_code)]
135pub fn disable_watcher(watcher: &mut HotReloadWatcher) {
136 watcher.enabled = false;
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 fn make_watcher() -> HotReloadWatcher {
144 new_watcher(default_hot_reload_config())
145 }
146
147 #[test]
148 fn test_default_config() {
149 let cfg = default_hot_reload_config();
150 assert!(cfg.poll_interval_ms > 0);
151 assert!(!cfg.watch_extensions.is_empty());
152 }
153
154 #[test]
155 fn test_new_watcher() {
156 let w = make_watcher();
157 assert!(w.enabled);
158 assert!(w.watched_paths.is_empty());
159 assert!(w.changes.is_empty());
160 }
161
162 #[test]
163 fn test_watch_path() {
164 let mut w = make_watcher();
165 watch_path(&mut w, "assets/model.glb");
166 assert!(is_watched(&w, "assets/model.glb"));
167 assert_eq!(watched_path_count(&w), 1);
168 }
169
170 #[test]
171 fn test_watch_path_no_duplicates() {
172 let mut w = make_watcher();
173 watch_path(&mut w, "assets/model.glb");
174 watch_path(&mut w, "assets/model.glb");
175 assert_eq!(watched_path_count(&w), 1);
176 }
177
178 #[test]
179 fn test_is_watched_false() {
180 let w = make_watcher();
181 assert!(!is_watched(&w, "missing.obj"));
182 }
183
184 #[test]
185 fn test_unwatch_path() {
186 let mut w = make_watcher();
187 watch_path(&mut w, "assets/tex.png");
188 let removed = unwatch_path(&mut w, "assets/tex.png");
189 assert!(removed);
190 assert!(!is_watched(&w, "assets/tex.png"));
191 }
192
193 #[test]
194 fn test_unwatch_path_missing() {
195 let mut w = make_watcher();
196 let removed = unwatch_path(&mut w, "nonexistent.obj");
197 assert!(!removed);
198 }
199
200 #[test]
201 fn test_simulate_file_change() {
202 let mut w = make_watcher();
203 simulate_file_change(&mut w, "scene.json", ChangeKind::Modified);
204 assert_eq!(change_count(&w), 1);
205 assert_eq!(pending_changes(&w)[0].path, "scene.json");
206 assert_eq!(pending_changes(&w)[0].kind, ChangeKind::Modified);
207 }
208
209 #[test]
210 fn test_simulate_disabled_watcher() {
211 let mut w = make_watcher();
212 disable_watcher(&mut w);
213 simulate_file_change(&mut w, "scene.json", ChangeKind::Modified);
214 assert_eq!(change_count(&w), 0);
215 }
216
217 #[test]
218 fn test_clear_changes() {
219 let mut w = make_watcher();
220 simulate_file_change(&mut w, "a.obj", ChangeKind::Created);
221 simulate_file_change(&mut w, "b.obj", ChangeKind::Deleted);
222 clear_changes(&mut w);
223 assert_eq!(change_count(&w), 0);
224 }
225
226 #[test]
227 fn test_changes_for_path() {
228 let mut w = make_watcher();
229 simulate_file_change(&mut w, "a.obj", ChangeKind::Modified);
230 simulate_file_change(&mut w, "b.obj", ChangeKind::Created);
231 simulate_file_change(&mut w, "a.obj", ChangeKind::Deleted);
232 let changes = changes_for_path(&w, "a.obj");
233 assert_eq!(changes.len(), 2);
234 }
235
236 #[test]
237 fn test_extension_matches() {
238 let w = make_watcher();
239 assert!(extension_matches(&w, "model.glb"));
240 assert!(extension_matches(&w, "texture.png"));
241 assert!(!extension_matches(&w, "script.lua"));
242 assert!(!extension_matches(&w, "data.bin"));
243 }
244
245 #[test]
246 fn test_enable_disable_watcher() {
247 let mut w = make_watcher();
248 disable_watcher(&mut w);
249 assert!(!w.enabled);
250 enable_watcher(&mut w);
251 assert!(w.enabled);
252 }
253
254 #[test]
255 fn test_watched_path_count() {
256 let mut w = make_watcher();
257 assert_eq!(watched_path_count(&w), 0);
258 watch_path(&mut w, "a.obj");
259 watch_path(&mut w, "b.glb");
260 assert_eq!(watched_path_count(&w), 2);
261 }
262
263 #[test]
264 fn test_change_kind_eq() {
265 assert_eq!(ChangeKind::Modified, ChangeKind::Modified);
266 assert_ne!(ChangeKind::Created, ChangeKind::Deleted);
267 }
268}