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