1use std::collections::{BTreeMap, BTreeSet};
14use std::io::{Read, Write};
15use std::path::{Component, Path, PathBuf};
16
17use anyhow::anyhow;
18
19use crate::core::{FsBackend, Result};
20use crate::{DirEntry, DirEntryType};
21
22pub struct DirFS {
54 root: PathBuf, cwd: PathBuf, entries: BTreeMap<PathBuf, DirEntry>, created_root_parents: Vec<PathBuf>, is_auto_clean: bool,
59}
60
61impl DirFS {
62 pub fn new<P: AsRef<Path>>(root: P) -> Result<Self> {
67 let root = root.as_ref();
68
69 if root.as_os_str().is_empty() {
70 return Err(anyhow!("invalid root path: empty"));
71 }
72 if root.is_relative() {
73 return Err(anyhow!("the root path must be absolute"));
74 }
75 if root.exists() && !root.is_dir() {
76 return Err(anyhow!("{:?} is not a directory", root));
77 }
78
79 let root = Self::normalize(root);
80
81 let mut created_root_parents = Vec::new();
82 if !std::fs::exists(&root)? {
83 created_root_parents.extend(Self::mkdir_all(&root)?);
84 }
85
86 if !Self::check_permissions(&root) {
88 return Err(anyhow!("Access denied: {:?}", root));
89 }
90
91 let inner_root = PathBuf::from("/");
92 let mut entries = BTreeMap::new();
93 entries.insert(
94 inner_root.clone(),
95 DirEntry::new(&inner_root, DirEntryType::Directory),
96 );
97
98 Ok(Self {
99 root,
100 cwd: inner_root,
101 entries,
102 created_root_parents,
103 is_auto_clean: true,
104 })
105 }
106
107 pub fn set_auto_clean(&mut self, clean: bool) {
111 self.is_auto_clean = clean;
112 }
113
114 pub fn add<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
120 let inner = self.to_inner(&path);
121 let host = self.to_host(&inner);
122 if !host.exists() {
123 return Err(anyhow!(
124 "No such file or directory: {}",
125 path.as_ref().display()
126 ));
127 }
128 self.add_recursive(&inner, &host)
129 }
130
131 pub fn forget<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
195 let inner = self.to_inner(&path);
196 if !self.exists(&inner) {
197 return Err(anyhow!("{:?} path is not tracked by VFS", path.as_ref()));
198 }
199 if Self::is_inner_root(&inner) {
200 return Err(anyhow!("cannot forget root directory"));
201 }
202
203 if let Some(entry) = self.entries.remove(&inner) {
204 if entry.is_dir() {
205 let childs: Vec<_> = self
206 .entries
207 .iter()
208 .map(|(path, _)| path)
209 .filter(|&path| path.starts_with(&inner))
210 .cloned()
211 .collect();
212
213 for child in childs {
214 self.entries.remove(&child);
215 }
216 }
217 }
218
219 Ok(())
220 }
221
222 fn normalize<P: AsRef<Path>>(path: P) -> PathBuf {
225 let mut result = PathBuf::new();
226 for component in path.as_ref().components() {
227 match component {
228 Component::CurDir => {}
229 Component::ParentDir => {
230 result.pop();
231 }
232 _ => {
233 result.push(component);
234 }
235 }
236 }
237 if result != PathBuf::from("/") && result.ends_with("/") {
239 result.pop();
240 }
241 result
242 }
243
244 fn to_host<P: AsRef<Path>>(&self, inner_path: P) -> PathBuf {
245 let inner = self.to_inner(inner_path);
246 self.root.join(inner.strip_prefix("/").unwrap())
247 }
248
249 fn to_inner<P: AsRef<Path>>(&self, inner_path: P) -> PathBuf {
250 Self::normalize(self.cwd.join(inner_path))
251 }
252
253 fn is_inner_root<P: AsRef<Path>>(path: P) -> bool {
254 let components: Vec<_> = path.as_ref().components().collect();
255 components.len() == 1 && components[0] == Component::RootDir
256 }
257
258 fn mkdir_all<P: AsRef<Path>>(path: P) -> Result<Vec<PathBuf>> {
262 let host_path = path.as_ref().to_path_buf();
263
264 let mut existed_part = host_path.clone();
266 while let Some(parent) = existed_part.parent() {
267 let parent_buf = parent.to_path_buf();
268 if std::fs::exists(parent)? {
269 existed_part = parent_buf;
270 break;
271 }
272 existed_part = parent_buf;
273 }
274
275 let need_to_create: Vec<_> = host_path
277 .strip_prefix(&existed_part)?
278 .components()
279 .collect();
280
281 let mut created = Vec::new();
282
283 let mut built = PathBuf::from(&existed_part);
284 for component in need_to_create {
285 built.push(component);
286 if !std::fs::exists(&built)? {
287 std::fs::create_dir(&built)?;
288 created.push(built.clone());
289 }
290 }
291
292 Ok(created)
293 }
294
295 fn rm_host_artifact<P: AsRef<Path>>(host_path: P) -> Result<()> {
296 let host_path = host_path.as_ref();
297 if host_path.is_dir() {
298 std::fs::remove_dir_all(host_path)?
299 } else {
300 std::fs::remove_file(host_path)?
301 }
302 Ok(())
303 }
304
305 fn check_permissions<P: AsRef<Path>>(path: P) -> bool {
306 let path = path.as_ref();
307 let filename = path.join(".access");
308 if let Err(_) = std::fs::write(&filename, b"check") {
309 return false;
310 }
311 if let Err(_) = std::fs::remove_file(filename) {
312 return false;
313 }
314 true
315 }
316
317 fn add_recursive(&mut self, inner_path: &Path, host_path: &Path) -> Result<()> {
319 let entry_type = if host_path.is_dir() {
320 DirEntryType::Directory
321 } else {
322 DirEntryType::File
323 };
324 let entry = DirEntry::new(inner_path, entry_type);
325 self.entries.insert(inner_path.to_path_buf(), entry);
326
327 if host_path.is_dir() {
328 for entry in std::fs::read_dir(host_path)? {
329 let entry = entry?;
330 let host_child = entry.path();
331 let inner_child = inner_path.join(entry.file_name());
332
333 self.add_recursive(&inner_child, &host_child)?;
334 }
335 }
336
337 Ok(())
338 }
339
340 fn inner_or_cwd<P: AsRef<Path>>(&self, path: Option<P>) -> Result<PathBuf> {
341 Ok(match path {
342 Some(p) => {
343 let inner = self.to_inner(&p);
344 if !self.exists(&inner) {
345 return Err(anyhow!("{} does not exist", p.as_ref().display()));
346 }
347 inner
348 }
349 None => self.cwd().to_path_buf(),
350 })
351 }
352}
353
354impl FsBackend for DirFS {
355 fn root(&self) -> &Path {
357 self.root.as_path()
358 }
359
360 fn cwd(&self) -> &Path {
362 self.cwd.as_path()
363 }
364
365 fn cd<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
369 let target = self.to_inner(path);
370 if !self.exists(&target) {
371 return Err(anyhow!("{} does not exist", target.display()));
372 }
373 self.cwd = target;
374 Ok(())
375 }
376
377 fn exists<P: AsRef<Path>>(&self, path: P) -> bool {
383 let inner = self.to_inner(path);
384 self.entries.contains_key(&inner)
385 }
386
387 fn ls<P: AsRef<Path>>(&self, path: Option<P>) -> Result<impl Iterator<Item = DirEntry>> {
439 let inner_path = self.inner_or_cwd(path)?;
440 let component_count = inner_path.components().count() + 1;
441 Ok(self
442 .entries
443 .iter()
444 .map(|(_, entry)| entry.clone())
445 .filter(move |entry| {
446 entry.path().starts_with(&inner_path)
447 && entry.path() != inner_path
448 && entry.path().components().count() == component_count
449 }))
450 }
451
452 fn tree<P: AsRef<Path>>(&self, path: Option<P>) -> Result<impl Iterator<Item = DirEntry>> {
507 let path = self.inner_or_cwd(path)?;
508 Ok(self
509 .entries
510 .iter()
511 .map(|(_, entry)| entry.clone())
512 .filter(move |entry| entry.path().starts_with(&path) && entry.path() != path))
513 }
514
515 fn mkdir<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
518 if path.as_ref().as_os_str().is_empty() {
519 return Err(anyhow!("invalid path: empty"));
520 }
521
522 let inner_path = self.to_inner(path);
523
524 if self.exists(&inner_path) {
525 return Err(anyhow!("path already exists: {}", inner_path.display()));
526 }
527
528 let mut existed_parent = inner_path.clone();
530 while let Some(parent) = existed_parent.parent() {
531 let parent_buf = parent.to_path_buf();
532 if self.exists(parent) {
533 existed_parent = parent_buf;
534 break;
535 }
536 existed_parent = parent_buf;
537 }
538
539 let need_to_create: Vec<_> = inner_path
541 .strip_prefix(&existed_parent)?
542 .components()
543 .collect();
544
545 let mut built = PathBuf::from(&existed_parent);
546 for component in need_to_create {
547 built.push(component);
548 if !self.exists(&built) {
549 let host = self.to_host(&built);
550 std::fs::create_dir(&host)?;
551 self.entries.insert(
552 built.clone(),
553 DirEntry::new(&built, DirEntryType::Directory),
554 );
555 }
556 }
557
558 Ok(())
559 }
560
561 fn mkfile<P: AsRef<Path>>(&mut self, file_path: P, content: Option<&[u8]>) -> Result<()> {
566 let file_path = self.to_inner(file_path);
567 if let Some(parent) = file_path.parent() {
568 if let Err(e) = std::fs::exists(parent) {
569 return Err(anyhow!("{:?}: {}", parent, e));
570 }
571 }
572 let host = self.to_host(&file_path);
573 let mut fd = std::fs::File::create(host)?;
574 self.entries.insert(
575 file_path.clone(),
576 DirEntry::new(&file_path, DirEntryType::File),
577 );
578 if let Some(content) = content {
579 fd.write_all(content)?;
580 }
581 Ok(())
582 }
583
584 fn read<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
599 let inner = self.to_inner(&path);
600 if !self.exists(&inner) {
601 return Err(anyhow!("file does not exist: {}", path.as_ref().display()));
602 }
603 if let Some(entry) = self.entries.get(&inner) {
604 if entry.is_dir() {
605 return Err(anyhow!("{} is a directory", inner.display()));
606 }
607 }
608
609 let mut content = Vec::new();
610 let host = self.to_host(&inner);
611 std::fs::File::open(&host)?.read_to_end(&mut content)?;
612
613 Ok(content)
614 }
615
616 fn write<P: AsRef<Path>>(&self, path: P, content: &[u8]) -> Result<()> {
634 let inner = self.to_inner(&path);
635 if !self.exists(&inner) {
636 return Err(anyhow!("file does not exist: {}", path.as_ref().display()));
637 }
638 if let Some(entry) = self.entries.get(&inner) {
639 if entry.is_dir() {
640 return Err(anyhow!("{} is a directory", inner.display()));
641 }
642 }
643
644 let host = self.to_host(&inner);
645 std::fs::write(&host, content)?;
646
647 Ok(())
648 }
649
650 fn append<P: AsRef<Path>>(&self, path: P, content: &[u8]) -> Result<()> {
670 let inner = self.to_inner(&path);
671 if !self.exists(&inner) {
672 return Err(anyhow!("file does not exist: {}", path.as_ref().display()));
673 }
674 if let Some(entry) = self.entries.get(&inner) {
675 if entry.is_dir() {
676 return Err(anyhow!("{} is a directory", inner.display()));
677 }
678 }
679
680 use std::fs::OpenOptions;
682 let host = self.to_host(&inner);
683 let mut file = OpenOptions::new().write(true).append(true).open(&host)?;
684
685 file.write_all(content)?;
686
687 Ok(())
688 }
689
690 fn rm<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
703 if path.as_ref().as_os_str().is_empty() {
704 return Err(anyhow!("invalid path: empty"));
705 }
706 if Self::is_inner_root(&path) {
707 return Err(anyhow!("invalid path: the root cannot be removed"));
708 }
709
710 let inner_path = self.to_inner(path); let host_path = self.to_host(&inner_path); if !self.exists(&inner_path) {
715 return Err(anyhow!("{} does not exist", inner_path.display()));
716 }
717
718 Self::rm_host_artifact(host_path)?;
720
721 let removed: Vec<PathBuf> = self
723 .entries
724 .iter()
725 .map(|(entry_path, _)| entry_path)
726 .filter(|&p| p.starts_with(&inner_path)) .cloned()
728 .collect();
729
730 for p in &removed {
732 self.entries.remove(p);
733 }
734
735 Ok(())
736 }
737
738 fn cleanup(&mut self) -> bool {
740 let mut is_ok = true;
741
742 let mut sorted_paths_to_remove: BTreeSet<PathBuf> = BTreeSet::new();
744 for (path, entry) in &self.entries {
745 if !entry.is_root() {
746 sorted_paths_to_remove.insert(path.clone());
747 }
748 }
749
750 for entry in sorted_paths_to_remove.iter().rev() {
751 let host = self.to_host(entry);
752 let result = Self::rm_host_artifact(&host);
753 if result.is_ok() {
754 self.entries.remove(entry);
755 } else {
756 is_ok = false;
757 eprintln!("Unable to remove: {}", host.display());
758 }
759 }
760
761 is_ok
762 }
763}
764
765impl Drop for DirFS {
766 fn drop(&mut self) {
767 if !self.is_auto_clean {
768 return;
769 }
770
771 if self.cleanup() {
772 self.entries.clear();
773 }
774
775 let errors: Vec<_> = self
776 .created_root_parents
777 .iter()
778 .rev()
779 .filter_map(|p| Self::rm_host_artifact(p).err())
780 .collect();
781 if !errors.is_empty() {
782 eprintln!("Failed to remove parents: {:?}", errors);
783 }
784
785 self.created_root_parents.clear();
786 }
787}
788
789#[cfg(test)]
790mod tests {
791 use super::*;
792 use tempdir::TempDir;
793
794 mod creations {
795 use super::*;
796
797 #[test]
798 fn test_new_absolute_path_existing() {
799 let temp_dir = setup_test_env();
800 let root = temp_dir.path().to_path_buf();
801
802 let fs = DirFS::new(&root).unwrap();
803
804 assert_eq!(fs.root, root);
805 assert_eq!(fs.cwd, PathBuf::from("/"));
806 assert!(fs.entries.contains_key(&PathBuf::from("/")));
807 assert!(fs.created_root_parents.is_empty());
808 assert!(fs.is_auto_clean);
809 }
810
811 #[test]
812 fn test_new_nonexistent_path_created() {
813 let temp_dir = setup_test_env();
814 let nonexistent = temp_dir.path().join("new_root");
815
816 let fs = DirFS::new(&nonexistent).unwrap();
817
818 assert_eq!(fs.root, nonexistent);
819 assert!(!fs.created_root_parents.is_empty()); assert!(nonexistent.exists()); }
822
823 #[test]
824 fn test_new_nested_nonexistent_path() {
825 let temp_dir = setup_test_env();
826 let nested = temp_dir.path().join("a/b/c");
827
828 let fs = DirFS::new(&nested).unwrap();
829
830 assert_eq!(fs.root, nested);
831 assert_eq!(fs.created_root_parents.len(), 3); assert!(nested.exists());
833 }
834
835 #[test]
836 fn test_new_permission_denied() {
837 #[cfg(unix)]
839 {
840 use std::os::unix::fs::PermissionsExt;
841
842 let temp_dir = setup_test_env();
843 let protected = temp_dir.path().join("protected");
844 let protected_root = protected.join("root");
845 std::fs::create_dir_all(&protected_root).unwrap();
846 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o000)).unwrap(); let result = DirFS::new(&protected_root);
849 assert!(result.is_err());
850
851 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o755)).unwrap(); }
853 }
854
855 #[test]
856 fn test_new_normalize_path() {
857 let temp_dir = setup_test_env();
858 let messy_path = temp_dir.path().join("././subdir/../subdir");
859
860 let fs = DirFS::new(&messy_path).unwrap();
861 let canonical = DirFS::normalize(temp_dir.path().join("subdir"));
862
863 assert_eq!(fs.root, canonical);
864 }
865
866 #[test]
867 fn test_new_root_is_file() {
868 let temp_dir = setup_test_env();
869 let file_path = temp_dir.path().join("file.txt");
870 std::fs::write(&file_path, "content").unwrap();
871
872 let result = DirFS::new(&file_path);
873 assert!(result.is_err()); }
875
876 #[test]
877 fn test_new_empty_path() {
878 let result = DirFS::new("");
879 assert!(result.is_err());
880 }
881
882 #[test]
883 fn test_new_special_characters() {
884 let temp_dir = setup_test_env();
885 let special = temp_dir.path().join("папка с пробелами и юникод!");
886
887 let fs = DirFS::new(&special).unwrap();
888
889 assert_eq!(fs.root, special);
890 assert!(special.exists());
891 }
892
893 #[test]
894 fn test_new_is_auto_clean_default() {
895 let temp_dir = setup_test_env();
896 let fs = DirFS::new(temp_dir.path()).unwrap();
897 assert!(fs.is_auto_clean); }
899
900 #[test]
901 fn test_root_returns_correct_path() {
902 let temp_dir = setup_test_env();
903
904 let vfs_root = temp_dir.path().join("vfs-root");
905 let fs = DirFS::new(&vfs_root).unwrap();
906 assert_eq!(fs.root(), vfs_root);
907 }
908
909 #[test]
910 fn test_cwd_defaults_to_root() {
911 let temp_dir = setup_test_env();
912 let fs = DirFS::new(temp_dir).unwrap();
913 assert_eq!(fs.cwd(), Path::new("/"));
914 }
915 }
916
917 mod normalize {
918 use super::*;
919
920 #[test]
921 fn test_normalize_path() {
922 assert_eq!(DirFS::normalize("/a/b/c/"), PathBuf::from("/a/b/c"));
923 assert_eq!(DirFS::normalize("/a/b/./c"), PathBuf::from("/a/b/c"));
924 assert_eq!(DirFS::normalize("/a/b/../c"), PathBuf::from("/a/c"));
925 assert_eq!(DirFS::normalize("/"), PathBuf::from("/"));
926 assert_eq!(DirFS::normalize("/.."), PathBuf::from("/"));
927 assert_eq!(DirFS::normalize(".."), PathBuf::from(""));
928 assert_eq!(DirFS::normalize(""), PathBuf::from(""));
929 assert_eq!(DirFS::normalize("../a"), PathBuf::from("a"));
930 assert_eq!(DirFS::normalize("./a"), PathBuf::from("a"));
931 }
932 }
933
934 mod cd {
935 use super::*;
936
937 #[test]
938 fn test_cd_to_absolute_path() {
939 let temp_dir = setup_test_env();
940 let mut fs = DirFS::new(&temp_dir).unwrap();
941 fs.mkdir("/projects").unwrap();
942 fs.cd("/projects").unwrap();
943 assert_eq!(fs.cwd(), Path::new("/projects"));
944 }
945
946 #[test]
947 fn test_cd_with_relative_path() {
948 let temp_dir = setup_test_env();
949 let mut fs = DirFS::new(&temp_dir).unwrap();
950 fs.mkdir("/home/user").unwrap();
951 fs.cwd = PathBuf::from("/home");
952 fs.cd("user").unwrap();
953 assert_eq!(fs.cwd(), Path::new("/home/user"));
954 }
955
956 #[test]
957 fn test_cd_extreme_cases() {
958 let temp_dir = setup_test_env();
959 let mut fs = DirFS::new(&temp_dir).unwrap();
960
961 fs.cd("..").unwrap(); assert_eq!(fs.cwd(), Path::new("/"));
963
964 fs.cd(".").unwrap(); assert_eq!(fs.cwd(), Path::new("/"));
966
967 fs.cwd = PathBuf::from("/home");
968 assert_eq!(fs.cwd(), Path::new("/home"));
969 fs.mkdir("/other").unwrap();
970 fs.cd("../other").unwrap();
971 assert_eq!(fs.cwd(), Path::new("/other"));
972
973 fs.cwd = PathBuf::from("/home");
974 assert_eq!(fs.cwd(), Path::new("/home"));
975 fs.mkdir("/home/other").unwrap();
976 fs.cd("./other").unwrap();
977 assert_eq!(fs.cwd(), Path::new("/home/other"));
978 }
979 }
980
981 mod mkdir {
982 use super::*;
983
984 #[test]
985 fn test_mkdir_create_single_dir() {
986 let temp_dir = setup_test_env();
987 let mut fs = DirFS::new(&temp_dir).unwrap();
988 fs.mkdir("/projects").unwrap();
989 assert!(fs.exists("/projects"));
990 }
991
992 #[test]
993 fn test_mkdir_relative_path() {
994 let temp_dir = setup_test_env();
995 let mut fs = DirFS::new(&temp_dir).unwrap();
996 fs.mkdir("home").unwrap();
997 fs.cd("/home").unwrap();
998 fs.mkdir("user").unwrap();
999 assert!(fs.exists("/home/user"));
1000 }
1001
1002 #[test]
1003 fn test_mkdir_nested_path() {
1004 let temp_dir = setup_test_env();
1005 let mut fs = DirFS::new(&temp_dir).unwrap();
1006 fs.mkdir("/a/b/c").unwrap();
1007 assert!(fs.exists("/a"));
1008 assert!(fs.exists("/a/b"));
1009 assert!(fs.exists("/a/b/c"));
1010 }
1011
1012 #[test]
1013 fn test_mkdir_already_exists() {
1014 let temp_dir = setup_test_env();
1015 let mut fs = DirFS::new(&temp_dir).unwrap();
1016 fs.mkdir("/data").unwrap();
1017 let result = fs.mkdir("/data");
1018 assert!(result.is_err());
1019 }
1020
1021 #[test]
1022 fn test_mkdir_invalid_path() {
1023 let temp_dir = setup_test_env();
1024 let mut fs = DirFS::new(&temp_dir).unwrap();
1025 let result = fs.mkdir("");
1026 assert!(result.is_err());
1027 }
1028 }
1029
1030 mod exists {
1031 use super::*;
1032
1033 #[test]
1034 fn test_exists_root() {
1035 let temp_dir = setup_test_env();
1036 let fs = DirFS::new(&temp_dir).unwrap();
1037 assert!(fs.exists("/"));
1038 }
1039
1040 #[test]
1041 fn test_exists_cwd() {
1042 let temp_dir = setup_test_env();
1043 let mut fs = DirFS::new(&temp_dir).unwrap();
1044 fs.mkdir("/projects").unwrap();
1045 fs.cd("/projects").unwrap();
1046 assert!(fs.exists("."));
1047 assert!(fs.exists("./"));
1048 assert!(fs.exists("/projects"));
1049 }
1050 }
1051
1052 mod ls {
1053 use super::*;
1054
1055 #[test]
1056 fn test_ls_empty_cwd() -> Result<()> {
1057 let temp_dir = setup_test_env();
1058 let fs = DirFS::new(temp_dir.path())?;
1059
1060 let entries: Vec<_> = fs.ls::<&Path>(None)?.collect();
1061 assert!(entries.is_empty(), "CWD should have no entries");
1062
1063 Ok(())
1064 }
1065
1066 #[test]
1067 fn test_ls_single_file_in_cwd() -> Result<()> {
1068 let temp_dir = setup_test_env();
1069 let mut fs = DirFS::new(temp_dir.path())?;
1070
1071 fs.mkfile("/file.txt", Some(b"Hello"))?;
1072
1073 let entries: Vec<_> = fs.ls::<&Path>(None)?.collect();
1074 assert_eq!(entries.len(), 1, "Should return exactly one file");
1075 assert_eq!(
1076 entries[0].path(),
1077 Path::new("/file.txt"),
1078 "File path should match"
1079 );
1080
1081 Ok(())
1082 }
1083
1084 #[test]
1085 fn test_ls_multiple_items_in_directory() -> Result<()> {
1086 let temp_dir = setup_test_env();
1087 let mut fs = DirFS::new(temp_dir.path())?;
1088
1089 fs.mkdir("/docs")?;
1090 fs.mkfile("/docs/readme.txt", None)?;
1091 fs.mkfile("/docs/todo.txt", None)?;
1092
1093 let entries: Vec<_> = fs.ls(Some("/docs"))?.collect();
1094
1095 assert_eq!(entries.len(), 2, "Should list both files in directory");
1096 assert!(entries.contains(&DirEntry::new("/docs/readme.txt", DirEntryType::File)));
1097 assert!(entries.contains(&DirEntry::new("/docs/todo.txt", DirEntryType::File)));
1098
1099 Ok(())
1100 }
1101
1102 #[test]
1103 fn test_ls_nested_files_excluded() -> Result<()> {
1104 let temp_dir = setup_test_env();
1105 let mut fs = DirFS::new(temp_dir.path())?;
1106
1107 fs.mkdir("/project/src")?;
1108 fs.mkfile("/project/main.rs", None)?;
1109 fs.mkfile("/project/src/lib.rs", None)?; let entries: Vec<_> = fs.ls(Some("/project"))?.collect();
1112
1113 assert_eq!(entries.len(), 2, "Only immediate children should be listed");
1114 assert!(entries.contains(&DirEntry::new("/project/main.rs", DirEntryType::File)));
1115 assert!(
1116 !entries
1117 .iter()
1118 .any(|p| p == &DirEntry::new("/project/src/lib.rs", DirEntryType::File)),
1119 "Nested file should not be included"
1120 );
1121
1122 Ok(())
1123 }
1124
1125 #[test]
1126 fn test_ls_directories_and_files_mixed() -> Result<()> {
1127 let temp_dir = setup_test_env();
1128 let mut fs = DirFS::new(temp_dir.path())?;
1129
1130 fs.mkdir("/mix")?;
1131 fs.mkfile("/mix/file1.txt", None)?;
1132 fs.mkdir("/mix/subdir")?; fs.mkfile("/mix/subdir/deep.txt", None)?; let entries: Vec<_> = fs.ls(Some("/mix"))?.collect();
1136
1137 assert_eq!(
1138 entries.len(),
1139 2,
1140 "Both file and subdirectory should be listed"
1141 );
1142 assert!(entries.contains(&DirEntry::new("/mix/file1.txt", DirEntryType::File)));
1143 assert!(entries.contains(&DirEntry::new("/mix/subdir", DirEntryType::Directory)));
1144 assert!(
1145 !entries
1146 .iter()
1147 .any(|p| p.path().to_str().unwrap().contains("deep.txt")),
1148 "Deeper nested file should be excluded"
1149 );
1150
1151 Ok(())
1152 }
1153
1154 #[test]
1155 fn test_ls_nonexistent_path_returns_error() -> Result<()> {
1156 let temp_dir = setup_test_env();
1157 let fs = DirFS::new(temp_dir.path())?;
1158
1159 let result: Result<Vec<_>> = fs
1160 .tree(Some("/nonexistent/path"))
1161 .map(|iter| iter.collect());
1162
1163 assert!(result.is_err(), "Should return error for nonexistent path");
1164 assert!(
1165 result.unwrap_err().to_string().contains("does not exist"),
1166 "Error message should indicate path does not exist"
1167 );
1168
1169 Ok(())
1170 }
1171
1172 #[test]
1173 fn test_ls_relative_path_resolution() -> Result<()> {
1174 let temp_dir = setup_test_env();
1175 let mut fs = DirFS::new(temp_dir.path())?;
1176
1177 fs.mkdir("/base")?;
1178 fs.cd("/base")?;
1179 fs.mkdir("sub")?;
1180 fs.mkfile("sub/file.txt", None)?;
1181 fs.mkfile("note.txt", None)?;
1182
1183 let sub_entries: Vec<_> = fs.ls(Some("sub"))?.collect();
1185 assert_eq!(
1186 sub_entries.len(),
1187 1,
1188 "Current directory should list one item"
1189 );
1190
1191 let base_entries: Vec<_> = fs.ls(Some("."))?.collect();
1193 assert_eq!(
1194 base_entries.len(),
1195 2,
1196 "Current directory should list two items"
1197 );
1198 assert!(base_entries.contains(&DirEntry::new("/base/sub", DirEntryType::Directory)));
1199 assert!(base_entries.contains(&DirEntry::new("/base/note.txt", DirEntryType::File)));
1200
1201 Ok(())
1202 }
1203
1204 #[test]
1205 fn test_ls_unicode_path_support() -> Result<()> {
1206 let temp_dir = setup_test_env();
1207 let mut fs = DirFS::new(temp_dir.path())?;
1208
1209 fs.mkdir("/проект")?;
1210 fs.mkfile("/проект/документ.txt", Some(b"Content"))?;
1211 fs.mkdir("/проект/подпапка")?;
1212 fs.mkfile("/проект/подпапка/файл.txt", Some(b"Nested"))?; let entries: Vec<_> = fs.ls(Some("/проект"))?.collect();
1215
1216 assert_eq!(
1217 entries.len(),
1218 2,
1219 "Should include both file and subdir at level"
1220 );
1221 assert!(entries.contains(&DirEntry::new("/проект/документ.txt", DirEntryType::File)));
1222 assert!(entries.contains(&DirEntry::new("/проект/подпапка", DirEntryType::Directory)));
1223 assert!(
1224 !entries
1225 .iter()
1226 .any(|p| p.path().to_str().unwrap().contains("файл.txt")),
1227 "Nested unicode file should be excluded"
1228 );
1229
1230 Ok(())
1231 }
1232
1233 #[test]
1234 fn test_ls_root_directory_listing() -> Result<()> {
1235 let temp_dir = setup_test_env();
1236 let mut fs = DirFS::new(temp_dir.path())?;
1237
1238 fs.mkfile("/a.txt", None)?;
1239 fs.mkdir("/sub")?;
1240 fs.mkfile("/sub/inner.txt", None)?; let entries: Vec<_> = fs.ls(Some("/"))?.collect();
1243
1244 assert_eq!(
1245 entries.len(),
1246 2,
1247 "Root should list immediate files and dirs"
1248 );
1249 assert!(entries.contains(&DirEntry::new("/a.txt", DirEntryType::File)));
1250 assert!(entries.contains(&DirEntry::new("/sub", DirEntryType::Directory)));
1251 assert!(
1252 !entries
1253 .iter()
1254 .any(|p| p.path().to_str().unwrap().contains("inner.txt")),
1255 "Nested file in sub should be excluded"
1256 );
1257
1258 Ok(())
1259 }
1260
1261 #[test]
1262 fn test_ls_empty_directory_returns_empty() -> Result<()> {
1263 let temp_dir = setup_test_env();
1264 let mut fs = DirFS::new(temp_dir.path())?;
1265
1266 fs.mkdir("/empty")?;
1267
1268 let entries: Vec<_> = fs.ls(Some("/empty"))?.collect();
1269 assert!(
1270 entries.is_empty(),
1271 "Empty directory should return no entries"
1272 );
1273
1274 Ok(())
1275 }
1276 }
1277
1278 mod tree {
1279 use super::*;
1280
1281 #[test]
1282 fn test_tree_current_directory_empty() -> Result<()> {
1283 let temp_dir = setup_test_env();
1284 let fs = DirFS::new(temp_dir.path())?;
1285
1286 let entries: Vec<_> = fs.tree::<&Path>(None)?.collect();
1287 assert!(entries.is_empty());
1288
1289 Ok(())
1290 }
1291
1292 #[test]
1293 fn test_tree_specific_directory_empty() -> Result<()> {
1294 let temp_dir = setup_test_env();
1295 let mut fs = DirFS::new(temp_dir.path())?;
1296
1297 fs.mkdir("/empty_dir")?;
1298
1299 let entries: Vec<_> = fs.tree(Some("/empty_dir"))?.collect();
1300 assert!(entries.is_empty());
1301
1302 Ok(())
1303 }
1304
1305 #[test]
1306 fn test_tree_single_file_in_cwd() -> Result<()> {
1307 let temp_dir = setup_test_env();
1308 let mut fs = DirFS::new(temp_dir.path())?;
1309
1310 fs.mkfile("/file.txt", Some(b"Content"))?;
1311
1312 let entries: Vec<_> = fs.tree::<&Path>(None)?.collect();
1313 assert_eq!(entries.len(), 1);
1314 assert_eq!(entries[0], DirEntry::new("/file.txt", DirEntryType::File));
1315
1316 Ok(())
1317 }
1318
1319 #[test]
1320 fn test_tree_file_in_subdirectory() -> Result<()> {
1321 let temp_dir = setup_test_env();
1322 let mut fs = DirFS::new(temp_dir.path())?;
1323
1324 fs.mkdir("/docs")?;
1325 fs.mkfile("/docs/readme.txt", Some(b"Docs"))?;
1326
1327 let entries: Vec<_> = fs.tree(Some("/docs"))?.collect();
1328 assert_eq!(entries.len(), 1);
1329 assert_eq!(
1330 entries[0],
1331 DirEntry::new("/docs/readme.txt", DirEntryType::File)
1332 );
1333
1334 Ok(())
1335 }
1336
1337 #[test]
1338 fn test_tree_nested_structure() -> Result<()> {
1339 let temp_dir = setup_test_env();
1340 let mut fs = DirFS::new(temp_dir.path())?;
1341
1342 fs.mkdir("/project")?;
1344 fs.mkdir("/project/src")?;
1345 fs.mkdir("/project/tests")?;
1346 fs.mkfile("/project/main.rs", Some(b"fn main() {}"))?;
1347 fs.mkfile("/project/src/lib.rs", Some(b"mod utils;"))?;
1348 fs.mkfile("/project/tests/test.rs", Some(b"#[test] fn it_works() {}"))?;
1349
1350 let root_entries: Vec<_> = fs.tree::<&Path>(None)?.collect();
1352 assert_eq!(root_entries.len(), 6); let project_entries: Vec<_> = fs.tree(Some("/project"))?.collect();
1356 assert_eq!(project_entries.len(), 5); Ok(())
1359 }
1360
1361 #[test]
1362 fn test_tree_nonexistent_path_error() -> Result<()> {
1363 let temp_dir = setup_test_env();
1364 let fs = DirFS::new(temp_dir.path())?;
1365
1366 let result: Result<Vec<_>> = fs.tree(Some("/nonexistent")).map(|iter| iter.collect());
1367 assert!(result.is_err());
1368 assert!(result.unwrap_err().to_string().contains("does not exist"));
1369
1370 Ok(())
1371 }
1372
1373 #[test]
1374 fn test_tree_relative_path() -> Result<()> {
1375 let temp_dir = setup_test_env();
1376 let mut fs = DirFS::new(temp_dir.path())?;
1377
1378 fs.mkdir("/docs")?;
1379 fs.cd("/docs")?;
1380 fs.mkdir("sub")?;
1381 fs.mkfile("sub/file.txt", Some(b"Relative"))?;
1382
1383 let entries: Vec<_> = fs.tree(Some("sub"))?.collect();
1384 assert_eq!(entries.len(), 1);
1385 assert_eq!(
1386 entries[0],
1387 DirEntry::new("/docs/sub/file.txt", DirEntryType::File)
1388 );
1389
1390 Ok(())
1391 }
1392
1393 #[test]
1394 fn test_tree_unicode_paths() -> Result<()> {
1395 let temp_dir = setup_test_env();
1396 let mut fs = DirFS::new(temp_dir.path())?;
1397
1398 fs.mkdir("/проект")?;
1399 fs.mkfile("/проект/документ.txt", Some(b"Unicode"))?;
1400 fs.mkdir("/проект/подпапка")?;
1401 fs.mkfile("/проект/подпапка/файл.txt", Some(b"Nested unicode"))?;
1402
1403 let entries: Vec<_> = fs.tree(Some("/проект"))?.collect();
1404
1405 assert_eq!(entries.len(), 3);
1406 assert!(entries.contains(&DirEntry::new("/проект/документ.txt", DirEntryType::File)));
1407 assert!(entries.contains(&DirEntry::new("/проект/подпапка", DirEntryType::Directory)));
1408 assert!(entries.contains(&DirEntry::new(
1409 "/проект/подпапка/файл.txt",
1410 DirEntryType::File
1411 )));
1412
1413 Ok(())
1414 }
1415
1416 #[test]
1417 fn test_tree_no_root_inclusion() -> Result<()> {
1418 let temp_dir = setup_test_env();
1419 let mut fs = DirFS::new(temp_dir.path())?;
1420
1421 fs.mkdir("/parent")?;
1422 fs.mkfile("/parent/child.txt", Some(b"Child"))?;
1423
1424 let entries: Vec<_> = fs.tree(Some("/parent"))?.collect();
1425
1426 assert!(
1428 !entries
1429 .iter()
1430 .any(|p| p == &DirEntry::new("/parent", DirEntryType::Directory))
1431 );
1432 assert!(
1433 entries
1434 .iter()
1435 .any(|p| p == &DirEntry::new("/parent/child.txt", DirEntryType::File))
1436 );
1437
1438 Ok(())
1439 }
1440
1441 #[test]
1442 fn test_tree_order_independence() -> Result<()> {
1443 let temp_dir = setup_test_env();
1444 let mut fs = DirFS::new(temp_dir.path())?;
1445
1446 fs.mkdir("/order_test")?;
1447 fs.mkfile("/order_test/a.txt", None)?;
1448 fs.mkfile("/order_test/b.txt", None)?;
1449 fs.mkfile("/order_test/c.txt", None)?;
1450
1451 let entries: Vec<_> = fs.tree(Some("/order_test"))?.collect();
1452
1453 assert_eq!(entries.len(), 3);
1454
1455 Ok(())
1456 }
1457 }
1458
1459 mod mkdir_all {
1460 use super::*;
1461 use std::fs;
1462 use std::path::PathBuf;
1463
1464 #[test]
1465 fn test_mkdir_all_simple_creation() {
1466 let temp_dir = setup_test_env();
1467 let target = temp_dir.path().join("a/b/c");
1468
1469 let created = DirFS::mkdir_all(&target).unwrap();
1470
1471 assert_eq!(created.len(), 3);
1472 assert!(created.contains(&temp_dir.path().join("a")));
1473 assert!(created.contains(&temp_dir.path().join("a/b")));
1474 assert!(created.contains(&temp_dir.path().join("a/b/c")));
1475
1476 assert!(temp_dir.path().join("a").is_dir());
1478 assert!(temp_dir.path().join("a/b").is_dir());
1479 assert!(temp_dir.path().join("a/b/c").is_dir());
1480 }
1481
1482 #[test]
1483 fn test_mkdir_all_existing_parent() {
1484 let temp_dir = setup_test_env();
1485 fs::create_dir_all(temp_dir.path().join("a")).unwrap(); let target = temp_dir.path().join("a/b/c");
1488 let created = DirFS::mkdir_all(&target).unwrap();
1489
1490 assert_eq!(created.len(), 2); assert!(created.contains(&temp_dir.path().join("a/b")));
1492 assert!(created.contains(&temp_dir.path().join("a/b/c")));
1493 }
1494
1495 #[test]
1496 fn test_mkdir_all_target_exists() {
1497 let temp_dir = setup_test_env();
1498 fs::create_dir_all(temp_dir.path().join("x/y")).unwrap();
1499
1500 let target = temp_dir.path().join("x/y");
1501 let created = DirFS::mkdir_all(&target).unwrap();
1502
1503 assert!(created.is_empty()); }
1505
1506 #[test]
1507 fn test_mkdir_all_root_path() {
1508 let result = DirFS::mkdir_all("/");
1510 assert!(result.is_ok());
1511 assert!(result.unwrap().is_empty());
1512 }
1513
1514 #[test]
1515 fn test_mkdir_all_single_dir() {
1516 let temp_dir = setup_test_env();
1517 let target = temp_dir.path().join("single");
1518
1519 let created = DirFS::mkdir_all(&target).unwrap();
1520
1521 assert_eq!(created.len(), 1);
1522 assert!(created.contains(&target));
1523 assert!(target.is_dir());
1524 }
1525
1526 #[test]
1527 fn test_mkdir_all_absolute_vs_relative() {
1528 let temp_dir = setup_test_env();
1529
1530 let abs_target = temp_dir.path().join("abs/a/b");
1532 let abs_created = DirFS::mkdir_all(&abs_target).unwrap();
1533
1534 assert!(!abs_created.is_empty());
1535 }
1536
1537 #[test]
1538 fn test_mkdir_all_nested_existing() {
1539 let temp_dir = setup_test_env();
1540 fs::create_dir_all(temp_dir.path().join("deep/a")).unwrap();
1541
1542 let target = temp_dir.path().join("deep/a/b/c/d");
1543 let created = DirFS::mkdir_all(&target).unwrap();
1544
1545 assert_eq!(created.len(), 3); }
1547
1548 #[test]
1549 fn test_mkdir_all_invalid_path() {
1550 #[cfg(unix)]
1552 {
1553 let invalid_path = PathBuf::from("/nonexistent/parent/child");
1554
1555 let result = DirFS::mkdir_all(&invalid_path);
1557 assert!(result.is_err());
1558 }
1559 }
1560
1561 #[test]
1562 fn test_mkdir_all_file_in_path() {
1563 let temp_dir = setup_test_env();
1564 let file_path = temp_dir.path().join("file.txt");
1565 fs::write(&file_path, "content").unwrap(); let target = file_path.join("subdir"); let result = DirFS::mkdir_all(&target);
1570 assert!(result.is_err()); }
1572
1573 #[test]
1574 fn test_mkdir_all_trailing_slash() {
1575 let temp_dir = setup_test_env();
1576 let target = temp_dir.path().join("trailing/");
1577
1578 let created = DirFS::mkdir_all(&target).unwrap();
1579 assert!(!created.is_empty());
1580 assert!(temp_dir.path().join("trailing").is_dir());
1581 }
1582
1583 #[test]
1584 fn test_mkdir_all_unicode_paths() {
1585 let temp_dir = setup_test_env();
1586 let target = temp_dir.path().join("папка/файл");
1587
1588 let created = DirFS::mkdir_all(&target).unwrap();
1589
1590 assert_eq!(created.len(), 2);
1591 assert!(temp_dir.path().join("папка").is_dir());
1592 assert!(temp_dir.path().join("папка/файл").is_dir());
1593 }
1594
1595 #[test]
1596 fn test_mkdir_all_permissions_error() {
1597 #[cfg(unix)]
1600 {
1601 use std::os::unix::fs::PermissionsExt;
1602
1603 let temp_dir = setup_test_env();
1604 fs::set_permissions(&temp_dir, PermissionsExt::from_mode(0o444)).unwrap(); let target = temp_dir.path().join("protected/dir");
1607 let result = DirFS::mkdir_all(&target);
1608
1609 assert!(result.is_err());
1610 }
1611 }
1612 }
1613
1614 mod drop {
1615 use super::*;
1616
1617 #[test]
1618 fn test_drop_removes_created_directories() {
1619 let temp_dir = setup_test_env();
1620 let root = temp_dir.path().join("to_remove");
1621
1622 let fs = DirFS::new(&root).unwrap();
1624 assert!(root.exists());
1625
1626 drop(fs);
1628
1629 assert!(!root.exists());
1631 }
1632
1633 #[test]
1634 fn test_drop_only_removes_created_parents() {
1635 let temp_dir = setup_test_env();
1636 let parent = temp_dir.path().join("parent");
1637 let child = parent.join("child");
1638
1639 std::fs::create_dir_all(&parent).unwrap(); let fs = DirFS::new(&child).unwrap();
1641
1642 assert!(parent.exists()); assert!(child.exists());
1644
1645 drop(fs);
1646
1647 assert!(parent.exists()); assert!(!child.exists()); }
1650
1651 #[test]
1652 fn test_drop_with_is_auto_clean_false() {
1653 let temp_dir = setup_test_env();
1654 let root = temp_dir.path().join("keep");
1655
1656 let mut fs = DirFS::new(&root).unwrap();
1657 fs.is_auto_clean = false; drop(fs);
1660
1661 assert!(root.exists()); }
1663
1664 #[test]
1665 fn test_drop_empty_created_root_parents() {
1666 let temp_dir = setup_test_env();
1667 let existing = temp_dir.path().join("existing");
1668 std::fs::create_dir(&existing).unwrap();
1669
1670 let fs = DirFS::new(&existing).unwrap(); drop(fs);
1673
1674 assert!(existing.exists()); }
1676
1677 #[test]
1678 fn test_drop_nested_directories_removed() {
1679 let temp_dir = setup_test_env();
1680 let nested = temp_dir.path().join("a/b/c");
1681
1682 let fs = DirFS::new(&nested).unwrap();
1683 assert!(nested.exists());
1684
1685 drop(fs);
1686
1687 assert!(!temp_dir.path().join("a").exists());
1689 assert!(!temp_dir.path().join("a/b").exists());
1690 assert!(!nested.exists());
1691 }
1692
1693 #[test]
1696 fn test_drop_removes_entries_created_by_mkdir() {
1697 let temp_dir = setup_test_env();
1698 let root = temp_dir.path().join("test_root");
1699
1700 let mut fs = DirFS::new(&root).unwrap();
1701 fs.mkdir("/subdir").unwrap();
1702 assert!(root.join("subdir").exists());
1703
1704 drop(fs);
1705
1706 assert!(!root.exists()); assert!(!root.join("subdir").exists()); }
1709
1710 #[test]
1711 fn test_drop_removes_entries_created_by_mkfile() {
1712 let temp_dir = setup_test_env();
1713 let root = temp_dir.path().join("test_root");
1714
1715 let mut fs = DirFS::new(&root).unwrap();
1716 fs.mkfile("/file.txt", None).unwrap();
1717 assert!(root.join("file.txt").exists());
1718
1719 drop(fs);
1720
1721 assert!(!root.exists());
1722 assert!(!root.join("file.txt").exists());
1723 }
1724
1725 #[test]
1726 fn test_drop_handles_nested_entries() {
1727 let temp_dir = setup_test_env();
1728 let root = temp_dir.path().join("test_root");
1729
1730 let mut fs = DirFS::new(&root).unwrap();
1731 fs.mkdir("/a/b/c").unwrap();
1732 fs.mkfile("/a/file.txt", None).unwrap();
1733
1734 assert!(root.join("a/b/c").exists());
1735 assert!(root.join("a/file.txt").exists());
1736
1737 drop(fs);
1738
1739 assert!(!root.exists());
1740 }
1741
1742 #[test]
1743 fn test_drop_ignores_non_entries() {
1744 let temp_dir = setup_test_env();
1745 let root = temp_dir.path().join("test_root");
1746 let external = temp_dir.path().join("external_file.txt");
1747
1748 std::fs::write(&external, "content").unwrap(); let fs = DirFS::new(&root).unwrap();
1751 drop(fs);
1752
1753 assert!(!root.exists());
1754 assert!(external.exists()); }
1756
1757 #[test]
1758 fn test_drop_with_empty_entries() {
1759 let temp_dir = setup_test_env();
1760 let root = temp_dir.path().join("empty_root");
1761
1762 let fs = DirFS::new(&root).unwrap();
1763 drop(fs);
1766
1767 assert!(!root.exists());
1768 }
1769 }
1770
1771 mod mkfile {
1772 use super::*;
1773
1774 #[test]
1775 fn test_mkfile_simple_creation() {
1776 let temp_dir = setup_test_env();
1777 let root = temp_dir.path();
1778
1779 let mut fs = DirFS::new(root).unwrap();
1780 fs.mkfile("/file.txt", None).unwrap();
1781
1782 assert!(fs.exists("/file.txt"));
1783 assert!(root.join("file.txt").exists());
1784 assert_eq!(fs.entries.contains_key(&PathBuf::from("/file.txt")), true);
1785 }
1786
1787 #[test]
1788 fn test_mkfile_with_content() {
1789 let temp_dir = setup_test_env();
1790 let root = temp_dir.path();
1791
1792 let mut fs = DirFS::new(root).unwrap();
1793 let content = b"Hello, VFS!";
1794 fs.mkfile("/data.bin", Some(content)).unwrap();
1795
1796 assert!(fs.exists("/data.bin"));
1797 let file_content = std::fs::read(root.join("data.bin")).unwrap();
1798 assert_eq!(&file_content, content);
1799 }
1800
1801 #[test]
1802 fn test_mkfile_in_subdirectory() {
1803 let temp_dir = setup_test_env();
1804 let root = temp_dir.path();
1805
1806 let mut fs = DirFS::new(root).unwrap();
1807 fs.mkdir("/subdir").unwrap();
1808 fs.mkfile("/subdir/file.txt", None).unwrap();
1809
1810 assert!(fs.exists("/subdir/file.txt"));
1811 assert!(root.join("subdir/file.txt").exists());
1812 }
1813
1814 #[test]
1815 fn test_mkfile_parent_does_not_exist() {
1816 let temp_dir = setup_test_env();
1817 let root = temp_dir.path();
1818
1819 let mut fs = DirFS::new(root).unwrap();
1820
1821 let result = fs.mkfile("/nonexistent/file.txt", None);
1822 assert!(result.is_err());
1823 }
1824
1825 #[test]
1826 fn test_mkfile_file_already_exists() {
1827 let temp_dir = setup_test_env();
1828 let root = temp_dir.path();
1829
1830 let mut fs = DirFS::new(root).unwrap();
1831 fs.mkfile("/existing.txt", None).unwrap();
1832
1833 let result = fs.mkfile("/existing.txt", None);
1835 assert!(result.is_ok()); assert!(fs.exists("/existing.txt"));
1837 }
1838
1839 #[test]
1840 fn test_mkfile_empty_content() {
1841 let temp_dir = setup_test_env();
1842 let root = temp_dir.path();
1843
1844 let mut fs = DirFS::new(root).unwrap();
1845 fs.mkfile("/empty.txt", Some(&[])).unwrap(); assert!(fs.exists("/empty.txt"));
1848 let file_size = std::fs::metadata(root.join("empty.txt")).unwrap().len();
1849 assert_eq!(file_size, 0);
1850 }
1851
1852 #[test]
1853 fn test_mkfile_relative_path() {
1854 let temp_dir = setup_test_env();
1855 let root = temp_dir.path();
1856
1857 let mut fs = DirFS::new(root).unwrap();
1858 fs.mkdir("/sub").unwrap();
1859 fs.cd("/sub").unwrap(); fs.mkfile("relative.txt", None).unwrap(); assert!(fs.exists("/sub/relative.txt"));
1864 assert!(root.join("sub/relative.txt").exists());
1865 }
1866
1867 #[test]
1868 fn test_mkfile_normalize_path() {
1869 let temp_dir = setup_test_env();
1870 let root = temp_dir.path();
1871
1872 let mut fs = DirFS::new(root).unwrap();
1873 fs.mkdir("/normalized").unwrap();
1874
1875 fs.mkfile("/./normalized/../normalized/file.txt", None)
1876 .unwrap();
1877
1878 assert!(fs.exists("/normalized/file.txt"));
1879 assert!(root.join("normalized/file.txt").exists());
1880 }
1881
1882 #[test]
1883 fn test_mkfile_invalid_path_components() {
1884 let temp_dir = setup_test_env();
1885 let root = temp_dir.path();
1886
1887 let mut fs = DirFS::new(root).unwrap();
1888
1889 #[cfg(unix)]
1891 {
1892 let result = fs.mkfile("/invalid\0name.txt", None);
1893 assert!(result.is_err()); }
1895 }
1896
1897 #[test]
1898 fn test_mkfile_permission_denied() {
1899 #[cfg(unix)]
1900 {
1901 use std::os::unix::fs::PermissionsExt;
1902
1903 let temp_dir = setup_test_env();
1904 let root = temp_dir.path();
1905 let protected = root.join("protected");
1906 std::fs::create_dir(&protected).unwrap();
1907 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o000)).unwrap(); let mut fs = DirFS::new(root).unwrap();
1910 let result = fs.mkfile("/protected/file.txt", None);
1911
1912 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o755)).unwrap(); assert!(result.is_err());
1915 assert!(
1916 result
1917 .unwrap_err()
1918 .to_string()
1919 .contains("Permission denied")
1920 );
1921 }
1922 }
1923
1924 #[test]
1925 fn test_mkfile_root_directory() {
1926 let temp_dir = setup_test_env();
1927 let root = temp_dir.path();
1928
1929 let mut fs = DirFS::new(root).unwrap();
1930
1931 let result = fs.mkfile("/", None);
1933 assert!(result.is_err());
1934 }
1935
1936 #[test]
1937 fn test_mkfile_unicode_filename() {
1938 let temp_dir = setup_test_env();
1939 let root = temp_dir.path();
1940
1941 let mut fs = DirFS::new(root).unwrap();
1942 fs.mkfile("/тест.txt", Some(b"Content")).unwrap();
1943
1944 assert!(fs.exists("/тест.txt"));
1945 assert!(root.join("тест.txt").exists());
1946 let content = std::fs::read_to_string(root.join("тест.txt")).unwrap();
1947 assert_eq!(content, "Content");
1948 }
1949 }
1950
1951 mod read {
1952 use super::*;
1953
1954 #[test]
1955 fn test_read_existing_file() -> Result<()> {
1956 let temp_dir = setup_test_env();
1957 let mut fs = DirFS::new(&temp_dir)?;
1958
1959 fs.mkfile("/test.txt", Some(b"Hello, VFS!"))?;
1961
1962 let content = fs.read("/test.txt")?;
1964 assert_eq!(content, b"Hello, VFS!");
1965
1966 Ok(())
1967 }
1968
1969 #[test]
1970 fn test_read_nonexistent_file() -> Result<()> {
1971 let temp_dir = setup_test_env();
1972 let fs = DirFS::new(temp_dir.path())?;
1973
1974 let result = fs.read("/not/found.txt");
1975 assert!(result.is_err());
1976 assert!(
1977 result
1978 .unwrap_err()
1979 .to_string()
1980 .contains("file does not exist: /not/found.txt")
1981 );
1982
1983 Ok(())
1984 }
1985
1986 #[test]
1987 fn test_read_directory_as_file() -> Result<()> {
1988 let temp_dir = setup_test_env();
1989 let mut fs = DirFS::new(temp_dir.path())?;
1990
1991 fs.mkdir("/empty_dir")?;
1992
1993 let result = fs.read("/empty_dir");
1994 assert!(result.is_err());
1995 assert!(result.unwrap_err().to_string().contains("is a directory"));
1997
1998 Ok(())
1999 }
2000
2001 #[test]
2002 fn test_read_empty_file() -> Result<()> {
2003 let temp_dir = setup_test_env();
2004 let mut fs = DirFS::new(temp_dir.path())?;
2005
2006 fs.mkfile("/empty.txt", None)?; let content = fs.read("/empty.txt")?;
2009 assert_eq!(content.len(), 0);
2010
2011 Ok(())
2012 }
2013
2014 #[test]
2015 fn test_read_relative_path() -> Result<()> {
2016 let temp_dir = setup_test_env();
2017 let mut fs = DirFS::new(temp_dir.path())?;
2018
2019 fs.cd("/")?;
2020 fs.mkdir("/parent")?;
2021 fs.cd("/parent")?;
2022 fs.mkfile("child.txt", Some(b"Content"))?;
2023
2024 let content = fs.read("child.txt")?;
2026 assert_eq!(content, b"Content");
2027
2028 Ok(())
2029 }
2030
2031 #[test]
2032 fn test_read_unicode_path() -> Result<()> {
2033 let temp_dir = setup_test_env();
2034 let mut fs = DirFS::new(temp_dir.path())?;
2035
2036 fs.mkdir("/папка")?;
2037 fs.mkfile("/папка/файл.txt", Some(b"Unicode content"))?;
2038
2039 let content = fs.read("/папка/файл.txt")?;
2040 assert_eq!(content, b"Unicode content");
2041
2042 Ok(())
2043 }
2044
2045 #[test]
2046 fn test_read_permission_denied() -> Result<()> {
2047 #[cfg(unix)]
2048 {
2049 use std::os::unix::fs::PermissionsExt;
2050
2051 let temp_dir = setup_test_env();
2052 let mut fs = DirFS::new(temp_dir.path())?;
2053
2054 fs.mkfile("/protected.txt", Some(b"Secret"))?;
2056 let host_path = temp_dir.path().join("protected.txt");
2057 std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o000))?;
2058
2059 let result = fs.read("/protected.txt");
2061 assert!(result.is_err());
2062 assert!(
2063 result
2064 .unwrap_err()
2065 .to_string()
2066 .contains("Permission denied")
2067 );
2068
2069 std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o644))?;
2071 }
2072 Ok(())
2073 }
2074
2075 #[test]
2076 fn test_read_root_file() -> Result<()> {
2077 let temp_dir = setup_test_env();
2078 let mut fs = DirFS::new(temp_dir.path())?;
2079
2080 fs.mkfile("/root_file.txt", Some(b"At root"))?;
2081 let content = fs.read("/root_file.txt")?;
2082 assert_eq!(content, b"At root");
2083
2084 Ok(())
2085 }
2086 }
2087
2088 mod write {
2089 use super::*;
2090
2091 #[test]
2092 fn test_write_new_file() -> Result<()> {
2093 let temp_dir = setup_test_env();
2094 let mut fs = DirFS::new(temp_dir.path())?;
2095
2096 fs.mkfile("/new.txt", None)?;
2097 let content = b"Hello, VFS!";
2098 fs.write("/new.txt", content)?;
2099
2100 assert!(fs.exists("/new.txt"));
2102 let read_back = fs.read("/new.txt")?;
2103 assert_eq!(read_back, content);
2104
2105 Ok(())
2106 }
2107
2108 #[test]
2109 fn test_write_existing_file_overwrite() -> Result<()> {
2110 let temp_dir = setup_test_env();
2111 let mut fs = DirFS::new(temp_dir.path())?;
2112
2113 fs.mkfile("/exist.txt", Some(b"Old content"))?;
2114
2115 let new_content = b"New content";
2116 fs.write("/exist.txt", new_content)?;
2117
2118 let read_back = fs.read("/exist.txt")?;
2119 assert_eq!(read_back, new_content);
2120
2121 Ok(())
2122 }
2123
2124 #[test]
2125 fn test_write_to_directory_path() -> Result<()> {
2126 let temp_dir = setup_test_env();
2127 let mut fs = DirFS::new(temp_dir.path())?;
2128
2129 fs.mkdir("/dir")?;
2130
2131 let result = fs.write("/dir", b"Content");
2132 assert!(result.is_err());
2133 assert!(result.unwrap_err().to_string().contains("is a directory"));
2134
2135 Ok(())
2136 }
2137
2138 #[test]
2139 fn test_write_to_nonexistent_file() -> Result<()> {
2140 let temp_dir = setup_test_env();
2141 let fs = DirFS::new(temp_dir.path())?;
2142
2143 let result = fs.write("/parent/child.txt", b"Content");
2144 assert!(result.is_err());
2145 assert!(
2146 result
2147 .unwrap_err()
2148 .to_string()
2149 .contains("file does not exist")
2150 );
2151
2152 Ok(())
2153 }
2154
2155 #[test]
2156 fn test_write_empty_content() -> Result<()> {
2157 let temp_dir = setup_test_env();
2158 let mut fs = DirFS::new(temp_dir.path())?;
2159
2160 fs.mkfile("/empty.txt", None)?;
2161 fs.write("/empty.txt", &[])?;
2162
2163 let read_back = fs.read("/empty.txt")?;
2164 assert!(read_back.is_empty());
2165
2166 Ok(())
2167 }
2168
2169 #[test]
2170 fn test_write_relative_path() -> Result<()> {
2171 let temp_dir = setup_test_env();
2172 let mut fs = DirFS::new(temp_dir.path())?;
2173
2174 fs.mkdir("/docs")?;
2175 fs.cd("docs")?;
2176
2177 fs.mkfile("file.txt", None)?;
2178 let content = b"Relative write";
2179 fs.write("file.txt", content)?;
2180
2181 let read_back = fs.read("/docs/file.txt")?;
2182 assert_eq!(read_back, content);
2183
2184 Ok(())
2185 }
2186 }
2187
2188 mod append {
2189 use super::*;
2190
2191 #[test]
2192 fn test_append_to_existing_file() -> Result<()> {
2193 let temp_dir = setup_test_env();
2194 let mut fs = DirFS::new(temp_dir.path())?;
2195
2196 fs.mkfile("/log.txt", Some(b"Initial content\n"))?;
2198
2199 fs.append("/log.txt", b"Appended line 1\n")?;
2201 fs.append("/log.txt", b"Appended line 2\n")?;
2202
2203 let content = fs.read("/log.txt")?;
2205 assert_eq!(
2206 content,
2207 b"Initial content\nAppended line 1\nAppended line 2\n"
2208 );
2209
2210 Ok(())
2211 }
2212
2213 #[test]
2214 fn test_append_to_empty_file() -> Result<()> {
2215 let temp_dir = setup_test_env();
2216 let mut fs = DirFS::new(temp_dir.path())?;
2217
2218 fs.mkfile("/empty.txt", Some(&[]))?;
2220
2221 fs.append("/empty.txt", b"First append\n")?;
2223 fs.append("/empty.txt", b"Second append\n")?;
2224
2225 let content = fs.read("/empty.txt")?;
2226 assert_eq!(content, b"First append\nSecond append\n");
2227
2228 Ok(())
2229 }
2230
2231 #[test]
2232 fn test_append_nonexistent_file() -> Result<()> {
2233 let temp_dir = setup_test_env();
2234 let fs = DirFS::new(temp_dir.path())?;
2235
2236 let result = fs.append("/not_found.txt", b"Content");
2237 assert!(result.is_err());
2238 assert!(
2239 result
2240 .unwrap_err()
2241 .to_string()
2242 .contains("file does not exist: /not_found.txt")
2243 );
2244
2245 Ok(())
2246 }
2247
2248 #[test]
2249 fn test_append_to_directory() -> Result<()> {
2250 let temp_dir = setup_test_env();
2251 let mut fs = DirFS::new(temp_dir.path())?;
2252
2253 fs.mkdir("/mydir")?;
2254
2255 let result = fs.append("/mydir", b"Content");
2256 assert!(result.is_err());
2257 assert!(result.unwrap_err().to_string().contains("is a directory"));
2258
2259 Ok(())
2260 }
2261
2262 #[test]
2263 fn test_append_empty_content() -> Result<()> {
2264 let temp_dir = setup_test_env();
2265 let mut fs = DirFS::new(temp_dir.path())?;
2266
2267 fs.mkfile("/test.txt", Some(b"Existing\n"))?;
2268
2269 fs.append("/test.txt", &[])?;
2271
2272 let content = fs.read("/test.txt")?;
2274 assert_eq!(content, b"Existing\n");
2275
2276 Ok(())
2277 }
2278
2279 #[test]
2280 fn test_append_relative_path() -> Result<()> {
2281 let temp_dir = setup_test_env();
2282 let mut fs = DirFS::new(temp_dir.path())?;
2283
2284 fs.mkdir("/docs")?;
2285 fs.cd("/docs")?;
2286 fs.mkfile("log.txt", Some(b"Start\n"))?; fs.append("log.txt", b"Added\n")?;
2289
2290 let content = fs.read("/docs/log.txt")?;
2291 assert_eq!(content, b"Start\nAdded\n");
2292
2293 Ok(())
2294 }
2295
2296 #[test]
2297 fn test_append_unicode_path() -> Result<()> {
2298 let temp_dir = setup_test_env();
2299 let mut fs = DirFS::new(temp_dir.path())?;
2300
2301 let first = Vec::from("Начало\n");
2302 let second = Vec::from("Продолжение\n");
2303
2304 fs.mkdir("/папка")?;
2305 fs.mkfile("/папка/файл.txt", Some(first.as_slice()))?;
2306 fs.append("/папка/файл.txt", second.as_slice())?;
2307
2308 let content = fs.read("/папка/файл.txt")?;
2309
2310 let mut expected = Vec::from(first);
2311 expected.extend(second);
2312
2313 assert_eq!(content, expected);
2314
2315 Ok(())
2316 }
2317
2318 #[test]
2319 fn test_concurrent_append_safety() -> Result<()> {
2320 let temp_dir = setup_test_env();
2321 let mut fs = DirFS::new(temp_dir.path())?;
2322
2323 fs.mkfile("/concurrent.txt", Some(b""))?;
2324
2325 for i in 1..=3 {
2327 fs.append("/concurrent.txt", format!("Line {}\n", i).as_bytes())?;
2328 }
2329
2330 let content = fs.read("/concurrent.txt")?;
2331 assert_eq!(content, b"Line 1\nLine 2\nLine 3\n");
2332
2333 Ok(())
2334 }
2335
2336 #[test]
2337 fn test_append_permission_denied() -> Result<()> {
2338 #[cfg(unix)]
2339 {
2340 use std::os::unix::fs::PermissionsExt;
2341
2342 let temp_dir = setup_test_env();
2343 let mut fs = DirFS::new(temp_dir.path())?;
2344
2345 fs.mkfile("/protected.txt", Some(b"Content"))?;
2347 let host_path = temp_dir.path().join("protected.txt");
2348 std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o000))?;
2349
2350 let result = fs.append("/protected.txt", b"New content");
2352 assert!(result.is_err());
2353 assert!(
2354 result
2355 .unwrap_err()
2356 .to_string()
2357 .contains("Permission denied")
2358 );
2359
2360 std::fs::set_permissions(&host_path, PermissionsExt::from_mode(0o644))?;
2362 }
2363 Ok(())
2364 }
2365 }
2366
2367 mod add {
2368 use super::*;
2369
2370 #[test]
2371 fn test_add_existing_file() -> Result<()> {
2372 let temp_dir = setup_test_env();
2373 let mut fs = DirFS::new(temp_dir.path())?;
2374
2375 let host_file = temp_dir.path().join("external.txt");
2377 std::fs::write(&host_file, b"Content from host")?;
2378
2379 fs.add("external.txt")?;
2381
2382 assert!(fs.exists("/external.txt"));
2384 let content = fs.read("/external.txt")?;
2385 assert_eq!(content, b"Content from host");
2386
2387 Ok(())
2388 }
2389
2390 #[test]
2391 fn test_add_existing_directory() -> Result<()> {
2392 let temp_dir = setup_test_env();
2393 let mut fs = DirFS::new(temp_dir.path())?;
2394
2395 let host_dir = temp_dir.path().join("external_dir");
2397 std::fs::create_dir_all(&host_dir)?;
2398
2399 fs.add("external_dir")?;
2401
2402 assert!(fs.exists("/external_dir"));
2404
2405 Ok(())
2406 }
2407
2408 #[test]
2409 fn test_add_nonexistent_path() -> Result<()> {
2410 let temp_dir = setup_test_env();
2411 let mut fs = DirFS::new(temp_dir.path())?;
2412
2413 let result = fs.add("/nonexistent.txt");
2414 assert!(result.is_err());
2415 assert!(
2416 result
2417 .unwrap_err()
2418 .to_string()
2419 .contains("No such file or directory")
2420 );
2421
2422 Ok(())
2423 }
2424
2425 #[test]
2426 fn test_add_relative_path() -> Result<()> {
2427 let temp_dir = setup_test_env();
2428 let mut fs = DirFS::new(temp_dir.path())?;
2429
2430 let subdir = temp_dir.path().join("sub");
2432 std::fs::create_dir_all(&subdir)?;
2433 std::fs::write(subdir.join("file.txt"), b"Relative content")?;
2434
2435 fs.add("/sub")?;
2436 fs.cd("/sub")?;
2437
2438 fs.add("file.txt")?;
2440
2441 assert!(fs.exists("/sub/file.txt"));
2442 let content = fs.read("/sub/file.txt")?;
2443 assert_eq!(content, b"Relative content");
2444
2445 Ok(())
2446 }
2447
2448 #[test]
2449 fn test_add_already_tracked_path() -> Result<()> {
2450 let temp_dir = setup_test_env();
2451 let mut fs = DirFS::new(temp_dir.path())?;
2452
2453 let host_file = temp_dir.path().join("duplicate.txt");
2455 std::fs::write(&host_file, b"Original")?;
2456 fs.add("duplicate.txt")?;
2457
2458 let result = fs.add("duplicate.txt");
2460 assert!(result.is_ok());
2462
2463 let content = fs.read("/duplicate.txt")?;
2465 assert_eq!(content, b"Original");
2466
2467 Ok(())
2468 }
2469
2470 #[test]
2471 fn test_add_unicode_path() -> Result<()> {
2472 let temp_dir = setup_test_env();
2473 let mut fs = DirFS::new(temp_dir.path())?;
2474
2475 let unicode_file = temp_dir.path().join("файл.txt");
2477 std::fs::write(&unicode_file, b"Unicode content")?;
2478
2479 fs.add("файл.txt")?;
2480
2481 assert!(fs.exists("/файл.txt"));
2482 let content = fs.read("/файл.txt")?;
2483 assert_eq!(content, b"Unicode content");
2484
2485 Ok(())
2486 }
2487
2488 #[test]
2489 fn test_add_and_auto_cleanup() -> Result<()> {
2490 let temp_dir = setup_test_env();
2491 let mut fs = DirFS::new(temp_dir.path())?;
2492
2493 let host_file = temp_dir.path().join("cleanup.txt");
2495 std::fs::write(&host_file, b"To be cleaned up")?;
2496 fs.add("cleanup.txt")?;
2497
2498 assert!(host_file.exists());
2499
2500 drop(fs);
2502
2503 assert!(!host_file.exists());
2506
2507 Ok(())
2508 }
2509
2510 #[test]
2511 fn test_add_single_file_no_recursion() -> Result<()> {
2512 let temp_dir = setup_test_env();
2513 let mut fs = DirFS::new(temp_dir.path())?;
2514
2515 let host_file = temp_dir.path().join("file.txt");
2516 std::fs::write(&host_file, b"Content")?;
2517
2518 fs.add("file.txt")?;
2519
2520 assert!(fs.exists("/file.txt"));
2521 assert_eq!(fs.read("/file.txt")?, b"Content");
2522
2523 Ok(())
2524 }
2525
2526 #[test]
2527 fn test_add_empty_directory() -> Result<()> {
2528 let temp_dir = setup_test_env();
2529 let mut fs = DirFS::new(temp_dir.path())?;
2530
2531 let host_dir = temp_dir.path().join("empty_dir");
2532 std::fs::create_dir_all(&host_dir)?;
2533
2534 fs.add("empty_dir")?;
2535
2536 assert!(fs.exists("/empty_dir"));
2537
2538 Ok(())
2539 }
2540
2541 #[test]
2542 fn test_add_directory_with_files() -> Result<()> {
2543 let temp_dir = setup_test_env();
2544 let mut fs = DirFS::new(temp_dir.path())?;
2545
2546 let data_dir = temp_dir.path().join("data");
2547 std::fs::create_dir_all(&data_dir)?;
2548 std::fs::write(data_dir.join("file1.txt"), b"First")?;
2549 std::fs::write(data_dir.join("file2.txt"), b"Second")?;
2550
2551 fs.add("data")?;
2552
2553 assert!(fs.exists("/data"));
2554 assert!(fs.exists("/data/file1.txt"));
2555 assert!(fs.exists("/data/file2.txt"));
2556 assert_eq!(fs.read("/data/file1.txt")?, b"First");
2557 assert_eq!(fs.read("/data/file2.txt")?, b"Second");
2558
2559 Ok(())
2560 }
2561
2562 #[test]
2563 fn test_add_nested_directories() -> Result<()> {
2564 let temp_dir = setup_test_env();
2565 let mut fs = DirFS::new(temp_dir.path())?;
2566
2567 let project = temp_dir.path().join("project");
2568 std::fs::create_dir_all(project.join("src"))?;
2569 std::fs::create_dir_all(project.join("docs"))?;
2570
2571 std::fs::write(project.join("src").join("main.rs"), b"fn main() {}")?;
2572 std::fs::write(project.join("docs").join("README.md"), b"Project docs")?;
2573
2574 std::fs::write(project.join("config.toml"), b"[config]")?;
2575
2576 fs.add("project")?;
2577
2578 assert!(fs.exists("/project"));
2579 assert!(fs.exists("/project/src"));
2580 assert!(fs.exists("/project/docs"));
2581 assert!(fs.exists("/project/src/main.rs"));
2582 assert!(fs.exists("/project/docs/README.md"));
2583 assert!(fs.exists("/project/config.toml"));
2584
2585 assert_eq!(fs.read("/project/src/main.rs")?, b"fn main() {}");
2586 assert_eq!(fs.read("/project/docs/README.md")?, b"Project docs");
2587 assert_eq!(fs.read("/project/config.toml")?, b"[config]");
2588
2589 Ok(())
2590 }
2591 }
2592
2593 mod forget {
2594 use super::*;
2595
2596 #[test]
2597 fn test_forget_existing_file() -> Result<()> {
2598 let temp_dir = setup_test_env();
2599 let mut fs = DirFS::new(temp_dir.path())?;
2600
2601 fs.mkfile("/note.txt", Some(b"Hello"))?;
2602 assert!(fs.exists("/note.txt"));
2603
2604 fs.forget("/note.txt")?;
2605
2606 assert!(!fs.exists("/note.txt"));
2607 assert!(std::fs::exists(fs.root().join("note.txt")).unwrap());
2608
2609 Ok(())
2610 }
2611
2612 #[test]
2613 fn test_forget_existing_directory() -> Result<()> {
2614 let temp_dir = setup_test_env();
2615 let mut fs = DirFS::new(temp_dir.path())?;
2616
2617 fs.mkdir("/temp")?;
2618 assert!(fs.exists("/temp"));
2619
2620 fs.forget("/temp")?;
2621
2622 assert!(!fs.exists("/temp"));
2623 assert!(std::fs::exists(fs.root().join("temp")).unwrap());
2624
2625 Ok(())
2626 }
2627
2628 #[test]
2629 fn test_forget_nested_path() -> Result<()> {
2630 let temp_dir = setup_test_env();
2631 let mut fs = DirFS::new(temp_dir.path())?;
2632
2633 fs.mkdir("/a")?;
2634 fs.mkdir("/a/b")?;
2635 fs.mkfile("/a/b/file.txt", Some(b"Data"))?;
2636
2637 assert!(fs.exists("/a/b/file.txt"));
2638
2639 fs.forget("/a/b")?;
2640
2641 assert!(!fs.exists("/a/b"));
2642 assert!(!fs.exists("/a/b/file.txt"));
2643 assert!(fs.exists("/a"));
2644
2645 Ok(())
2646 }
2647
2648 #[test]
2649 fn test_forget_nonexistent_path() -> Result<()> {
2650 let temp_dir = setup_test_env();
2651 let mut fs = DirFS::new(temp_dir.path())?;
2652
2653 let result = fs.forget("/not/found.txt");
2654 assert!(result.is_err());
2655 assert!(
2656 result
2657 .unwrap_err()
2658 .to_string()
2659 .contains("path is not tracked by VFS")
2660 );
2661
2662 Ok(())
2663 }
2664
2665 #[test]
2666 fn test_forget_relative_path() -> Result<()> {
2667 let temp_dir = setup_test_env();
2668 let mut fs = DirFS::new(temp_dir.path())?;
2669
2670 fs.mkdir("/docs")?;
2671 fs.cd("/docs")?;
2672 fs.mkdir("sub")?;
2673 fs.mkfile("sub/file.txt", Some(b"Content"))?;
2674
2675 assert!(fs.exists("/docs/sub/file.txt"));
2676
2677 fs.forget("sub/file.txt")?;
2678
2679 assert!(!fs.exists("/docs/sub/file.txt"));
2680 assert!(fs.exists("/docs/sub"));
2681
2682 Ok(())
2683 }
2684
2685 #[test]
2686 fn test_forget_root_directory() -> Result<()> {
2687 let temp_dir = setup_test_env();
2688 let mut fs = DirFS::new(temp_dir.path())?;
2689
2690 let result = fs.forget("/");
2691 assert!(result.is_err());
2692 assert!(
2693 result
2694 .unwrap_err()
2695 .to_string()
2696 .contains("cannot forget root directory")
2697 );
2698
2699 assert!(fs.exists("/"));
2700
2701 Ok(())
2702 }
2703
2704 #[test]
2705 fn test_forget_parent_after_child() -> Result<()> {
2706 let temp_dir = setup_test_env();
2707 let mut fs = DirFS::new(temp_dir.path())?;
2708
2709 fs.mkdir("/parent")?;
2710 fs.mkfile("/parent/child.txt", Some(b"Child content"))?;
2711
2712 fs.forget("/parent/child.txt")?;
2713 assert!(!fs.exists("/parent/child.txt"));
2714
2715 fs.forget("/parent")?;
2716 assert!(!fs.exists("/parent"));
2717
2718 Ok(())
2719 }
2720
2721 #[test]
2722 fn test_forget_unicode_path() -> Result<()> {
2723 let temp_dir = setup_test_env();
2724 let mut fs = DirFS::new(temp_dir.path())?;
2725
2726 fs.mkdir("/папка")?;
2727 fs.mkfile("/папка/файл.txt", Some(b"Unicode"))?;
2728 assert!(fs.exists("/папка/файл.txt"));
2729
2730 fs.forget("/папка/файл.txt")?;
2731
2732 assert!(!fs.exists("/папка/файл.txt"));
2733 assert!(fs.exists("/папка"));
2734
2735 Ok(())
2736 }
2737
2738 #[test]
2739 fn test_forget_case_sensitivity_unix() -> Result<()> {
2740 #[cfg(unix)]
2741 {
2742 let temp_dir = setup_test_env();
2743 let mut fs = DirFS::new(temp_dir.path())?;
2744
2745 fs.mkfile("/File.TXT", Some(b"Case test"))?;
2746 assert!(fs.exists("/File.TXT"));
2747
2748 let result = fs.forget("/file.txt");
2749 assert!(result.is_err());
2750 assert!(fs.exists("/File.TXT"));
2751
2752 fs.forget("/File.TXT")?;
2753 assert!(!fs.exists("/File.TXT"));
2754 }
2755 Ok(())
2756 }
2757
2758 #[test]
2759 fn test_forget_after_add_and_remove() -> Result<()> {
2760 let temp_dir = setup_test_env();
2761 let mut fs = DirFS::new(temp_dir.path())?;
2762
2763 let host_file = temp_dir.path().join("external.txt");
2764 std::fs::write(&host_file, b"External")?;
2765
2766 fs.add("external.txt")?;
2767 assert!(fs.exists("/external.txt"));
2768
2769 std::fs::remove_file(&host_file)?;
2770 assert!(!host_file.exists());
2771
2772 fs.forget("external.txt")?;
2773 assert!(!fs.exists("/external.txt"));
2774
2775 Ok(())
2776 }
2777 }
2778
2779 mod rm {
2780 use super::*;
2781
2782 #[test]
2783 fn test_rm_file_success() {
2784 let temp_dir = setup_test_env();
2785 let mut fs = DirFS::new(temp_dir.path()).unwrap();
2786
2787 fs.mkfile("/test.txt", Some(b"hello")).unwrap();
2789 assert!(fs.exists("/test.txt"));
2790 assert!(temp_dir.path().join("test.txt").exists());
2791
2792 fs.rm("/test.txt").unwrap();
2794
2795 assert!(!fs.exists("/test.txt"));
2797 assert!(!temp_dir.path().join("test.txt").exists());
2798 }
2799
2800 #[test]
2801 fn test_rm_directory_recursive() {
2802 let temp_dir = setup_test_env();
2803 let mut fs = DirFS::new(temp_dir.path()).unwrap();
2804
2805 fs.mkdir("/a/b/c").unwrap();
2807 fs.mkfile("/a/file1.txt", None).unwrap();
2808 fs.mkfile("/a/b/file2.txt", None).unwrap();
2809
2810 assert!(fs.exists("/a/b/c"));
2811 assert!(fs.exists("/a/file1.txt"));
2812 assert!(fs.exists("/a/b/file2.txt"));
2813
2814 fs.rm("/a").unwrap();
2816
2817 assert!(!fs.exists("/a"));
2819 assert!(!fs.exists("/a/b"));
2820 assert!(!fs.exists("/a/b/c"));
2821 assert!(!fs.exists("/a/file1.txt"));
2822 assert!(!fs.exists("/a/b/file2.txt"));
2823
2824 assert!(!temp_dir.path().join("a").exists());
2825 }
2826
2827 #[test]
2828 fn test_rm_nonexistent_path() {
2829 #[cfg(unix)]
2830 {
2831 let temp_dir = setup_test_env();
2832 let mut fs = DirFS::new(temp_dir.path()).unwrap();
2833
2834 let result = fs.rm("/not/found");
2835 assert!(result.is_err());
2836 assert_eq!(result.unwrap_err().to_string(), "/not/found does not exist");
2837 }
2838 }
2839
2840 #[test]
2841 fn test_rm_relative_path() {
2842 let temp_dir = setup_test_env();
2843 let mut fs = DirFS::new(temp_dir.path()).unwrap();
2844
2845 fs.mkdir("/parent").unwrap();
2846 fs.cd("/parent").unwrap();
2847 fs.mkfile("child.txt", None).unwrap();
2848
2849 assert!(fs.exists("/parent/child.txt"));
2850
2851 fs.rm("child.txt").unwrap();
2853
2854 assert!(!fs.exists("/parent/child.txt"));
2855 assert!(!temp_dir.path().join("parent/child.txt").exists());
2856 }
2857
2858 #[test]
2859 fn test_rm_empty_string_path() {
2860 let temp_dir = setup_test_env();
2861 let mut fs = DirFS::new(temp_dir.path()).unwrap();
2862
2863 let result = fs.rm("");
2864 assert!(result.is_err());
2865 assert_eq!(result.unwrap_err().to_string(), "invalid path: empty");
2866 }
2867
2868 #[test]
2869 fn test_rm_root_directory() {
2870 let temp_dir = setup_test_env();
2871 let mut fs = DirFS::new(temp_dir.path()).unwrap();
2872
2873 let result = fs.rm("/");
2875 assert!(result.is_err());
2876 assert_eq!(
2877 result.unwrap_err().to_string(),
2878 "invalid path: the root cannot be removed"
2879 );
2880
2881 assert!(fs.exists("/"));
2883 assert!(temp_dir.path().exists());
2884 }
2885
2886 #[test]
2887 fn test_rm_trailing_slash() {
2888 let temp_dir = setup_test_env();
2889 let mut fs = DirFS::new(temp_dir.path()).unwrap();
2890
2891 fs.mkdir("/dir/").unwrap(); fs.mkfile("/dir/file.txt", None).unwrap();
2893
2894 fs.rm("/dir/").unwrap();
2896
2897 assert!(!fs.exists("/dir"));
2898 assert!(!temp_dir.path().join("dir").exists());
2899 }
2900
2901 #[test]
2902 fn test_rm_unicode_path() {
2903 let temp_dir = setup_test_env();
2904 let mut fs = DirFS::new(temp_dir.path()).unwrap();
2905
2906 let unicode_path = "/папка/файл.txt";
2907 fs.mkdir("/папка").unwrap();
2908 fs.mkfile(unicode_path, None).unwrap();
2909
2910 assert!(fs.exists(unicode_path));
2911
2912 fs.rm(unicode_path).unwrap();
2913
2914 assert!(!fs.exists(unicode_path));
2915 assert!(!temp_dir.path().join("папка/файл.txt").exists());
2916 }
2917
2918 #[test]
2919 fn test_rm_permission_denied() {
2920 #[cfg(unix)]
2921 {
2922 use std::os::unix::fs::PermissionsExt;
2923
2924 let temp_dir = setup_test_env();
2925 let mut fs = DirFS::new(temp_dir.path()).unwrap();
2926 fs.mkdir("/protected").unwrap();
2927
2928 let protected = fs.root().join("protected");
2930 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o000)).unwrap();
2931
2932 let result = fs.rm("/protected");
2934 assert!(result.is_err());
2935 assert!(
2936 result
2937 .unwrap_err()
2938 .to_string()
2939 .contains("Permission denied")
2940 );
2941
2942 std::fs::set_permissions(&protected, PermissionsExt::from_mode(0o755)).unwrap();
2944 }
2945 }
2946
2947 #[test]
2948 fn test_rm_symlink_file() {
2949 #[cfg(unix)]
2950 {
2951 use std::os::unix::fs::symlink;
2952
2953 let temp_dir = setup_test_env();
2954 let mut fs = DirFS::new(temp_dir.path()).unwrap();
2955
2956 std::fs::write(temp_dir.path().join("real.txt"), "content").unwrap();
2958 symlink("real.txt", temp_dir.path().join("link.txt")).unwrap();
2959
2960 fs.mkfile("/link.txt", None).unwrap(); assert!(fs.exists("/link.txt"));
2962
2963 fs.rm("/link.txt").unwrap();
2965
2966 assert!(!fs.exists("/link.txt"));
2967 assert!(!temp_dir.path().join("link.txt").exists()); assert!(temp_dir.path().join("real.txt").exists()); }
2970 }
2971
2972 #[test]
2973 fn test_rm_after_cd() {
2974 let temp_dir = setup_test_env();
2975 let mut fs = DirFS::new(temp_dir.path()).unwrap();
2976
2977 fs.mkdir("/projects").unwrap();
2978 fs.cd("/projects").unwrap();
2979 fs.mkfile("notes.txt", None).unwrap();
2980
2981 assert!(fs.exists("/projects/notes.txt"));
2982
2983 fs.rm("notes.txt").unwrap();
2985
2986 assert!(!fs.exists("/projects/notes.txt"));
2987 assert!(!temp_dir.path().join("projects/notes.txt").exists());
2988 }
2989 }
2990
2991 mod cleanup {
2992 use super::*;
2993
2994 #[test]
2995 fn test_cleanup_ignores_is_auto_clean() {
2996 let temp_dir = setup_test_env();
2997 let root = temp_dir.path();
2998
2999 let mut fs = DirFS::new(root).unwrap();
3000 fs.is_auto_clean = false; fs.mkfile("/temp.txt", None).unwrap();
3002
3003 fs.cleanup(); assert!(!fs.exists("/temp.txt"));
3006 assert!(!root.join("temp.txt").exists());
3007 }
3008
3009 #[test]
3010 fn test_cleanup_preserves_root_and_parents() {
3011 let temp_dir = setup_test_env();
3012 let root = temp_dir.path().join("preserve_root");
3013
3014 let mut fs = DirFS::new(&root).unwrap();
3015 fs.mkdir("/subdir").unwrap();
3016 fs.mkfile("/subdir/file.txt", None).unwrap();
3017
3018 assert!(!fs.created_root_parents.is_empty());
3020
3021 fs.cleanup();
3022
3023 assert!(root.exists());
3025 for parent in &fs.created_root_parents {
3026 assert!(parent.exists());
3027 }
3028
3029 assert_eq!(fs.entries.len(), 1);
3031 assert!(fs.entries.contains_key(&PathBuf::from("/")));
3032 }
3033
3034 #[test]
3035 fn test_cleanup_empty_entries() {
3036 let temp_dir = setup_test_env();
3037 let root = temp_dir.path();
3038
3039 let mut fs = DirFS::new(root).unwrap();
3040 assert_eq!(fs.entries.len(), 1);
3042
3043 fs.cleanup();
3044
3045 assert_eq!(fs.entries.len(), 1); assert!(fs.entries.contains_key(&PathBuf::from("/")));
3047 assert!(root.exists()); }
3049 }
3050
3051 fn setup_test_env() -> TempDir {
3053 TempDir::new("dirfs_test").unwrap()
3054 }
3055}