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