Skip to main content

plain_ds/tree/file_tree/
impl_tree.rs

1//! This module contains file-tree implementation.
2
3use std::path::{Component, Path, PathBuf};
4
5use crate::core::utils;
6use super::node::DirNode;
7use crate::{DSError, Result};
8
9/// `FileTree` is a specialized data structure for compactly storing in memory hierarchical
10/// structure of files and directories. It also provides fast search and access to data.
11///
12/// **Implementation Features** <br>
13/// If all the file paths you plan to store in `FileTree` begin with the same long prefix,
14/// it's better to store this prefix separately, outside of this structure.
15///
16/// For example, you have several file paths:
17///```plain text
18/// /very/long/prefix/to/my/files/file.01
19///
20/// /very/long/prefix/to/my/files/alfa/file.02
21///
22/// /very/long/prefix/to/my/files/beta/gamma/file.03
23///```
24///
25/// Common prefix is: `/very/long/prefix/to/my/files` - store it separately.
26///
27/// And in `FileTree` store short paths: `/file.01`, `/alfa/file.02` and `/beta/gamma/file.03`.
28///
29/// In this case, `FileTree` will store the following hierarchy:
30///```plain text
31///                      /
32///        +-------------+--------------+
33///     file.01         alfa           beta
34///                      /              /
35///                   file.02         gamma
36///                                     /
37///                                  file.03
38///```
39/// All paths in `FileTree` must be absolute (i.e., start with `/`). <br>
40/// Do not include any prefixes into paths (for example, like in Windows - `C:`).
41pub struct FileTree {
42    root: DirNode,
43}
44
45impl FileTree {
46    /// Creates new file-tree and initialize root as `/`.
47    pub fn new() -> Self {
48        Self {
49            root: DirNode::new(),
50        }
51    }
52
53    /// Checks if the tree is empty.
54    ///
55    /// **Efficiency**: O(1)
56    pub fn is_empty(&self) -> bool {
57        self.root.is_empty()
58    }
59
60    /// Checks if `path` is contained in the tree as file.
61    ///
62    /// **Efficiency**: O(n), where `n` is a path length (in components).
63    pub fn contains_file<P: AsRef<Path>>(&self, path: P) -> Result<bool> {
64        let path = path.as_ref();
65        if path.as_os_str() == "" {
66            return Err(DSError::EmptyPath);
67        }
68        if !path.is_absolute() {
69            return Err(DSError::NotAbsolutePath {
70                path: path.to_owned(),
71            });
72        }
73        if path.as_os_str() == "/" {
74            return Err(DSError::NotFile {
75                path: path.to_owned(),
76            });
77        }
78
79        let (dir, file) = utils::split_path(path);
80        let dir = dir.ok_or(DSError::WrongPath { path: path.to_owned() })?;
81
82        Ok(self.check_path(dir, file))
83    }
84
85    /// Checks if `path` is contained in the tree as directory.
86    ///
87    /// **Efficiency**: O(n), where `n` is a path length (in components).
88    pub fn contains_dir<P: AsRef<Path>>(&self, path: P) -> Result<bool> {
89        let path = path.as_ref();
90        if path.as_os_str() == "" {
91            return Err(DSError::EmptyPath);
92        }
93        if !path.is_absolute() {
94            return Err(DSError::NotAbsolutePath {
95                path: path.to_owned(),
96            });
97        }
98        if path.as_os_str() == "/" {
99            return Ok(true);
100        }
101
102        Ok(self.check_path(path, None))
103    }
104
105    /// Add directory into tree.
106    ///
107    /// `path` must be absolute (i.e., start with `/`) and not contain prefixes
108    /// (for example, like in Windows - `C:`).
109    ///
110    /// **Efficiency**: O(n), where `n` is a path length (in components).
111    pub fn add_dir<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
112        let path = path.as_ref();
113        if path.as_os_str() == "" {
114            return Err(DSError::EmptyPath);
115        }
116        if !path.is_absolute() {
117            return Err(DSError::NotAbsolutePath {
118                path: path.to_owned(),
119            });
120        }
121        if path.as_os_str() == "/" {
122            return Ok(());
123        }
124
125        // Create all necessary directories
126        let _ = self.ensure_dirs(path);
127
128        Ok(())
129    }
130
131    /// Add file into tree.
132    ///
133    /// `path` must be absolute (i.e., start with `/`) and not contain prefixes
134    /// (for example, like in Windows - `C:`).
135    ///
136    /// **Efficiency**: O(n), where `n` is a path length (in components).
137    pub fn add_file<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
138        let path = path.as_ref();
139        if path.as_os_str() == "" {
140            return Err(DSError::EmptyPath);
141        }
142        if !path.is_absolute() {
143            return Err(DSError::NotAbsolutePath {
144                path: path.to_owned(),
145            });
146        }
147        if path.as_os_str() == "/" {
148            return Err(DSError::NotFile {
149                path: path.to_owned(),
150            });
151        }
152        
153        let (dir, file) = utils::split_path(path);
154
155        // First pass: create all necessary directories
156        if let Some(dir) = dir {
157            let parent_dir = self.ensure_dirs(dir);
158
159            // Second pass: add the file to the last directory
160            if let Some(file) = file {
161                parent_dir.insert_file(file);
162            }
163        }
164
165        Ok(())
166    }
167
168    /// Removes a file from the tree.
169    pub fn remove_file<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
170        let path = path.as_ref();
171        if path.as_os_str() == "" {
172            return Err(DSError::EmptyPath);
173        }
174        if !path.is_absolute() {
175            return Err(DSError::NotAbsolutePath {
176                path: path.to_owned(),
177            });
178        }
179        if path.as_os_str() == "/" {
180            return Err(DSError::NotFile {
181                path: path.to_owned(),
182            });
183        }
184
185        let (dir, file) = utils::split_path(path);
186        let dir = dir.ok_or(DSError::WrongPath { path: path.to_owned() })?;
187        let file_name = file.ok_or(DSError::NotFile { path: path.to_owned() })?;
188
189        // First pass: find the parent directory
190        let parent = self.find_dir(dir)?;
191
192        // Second pass: remove the file from the parent directory
193        if !parent.files_contains(&file_name) {
194            // File doesn't exist — return error
195            return Err(DSError::PathNotFound {
196                path: path.to_owned(),
197            });
198        }
199        parent.remove_file(&file_name);
200
201        Ok(())
202    }
203
204    /// Removes a directory (with all its entries) from the tree.
205    pub fn remove_dir<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
206        let path = path.as_ref();
207        if path.as_os_str() == "" {
208            return Err(DSError::EmptyPath);
209        }
210        if !path.is_absolute() {
211            return Err(DSError::NotAbsolutePath {
212                path: path.to_owned(),
213            });
214        }
215        if path.as_os_str() == "/" {
216            return Err(DSError::NotFile {
217                path: path.to_owned(),
218            });
219        }
220
221        let (parent, child) = utils::split_path(path);
222        let parent = parent.ok_or(DSError::WrongPath { path: path.to_owned() })?;
223        let child = child.ok_or(DSError::NotDirectory { path: path.to_owned() })?;
224
225        // First pass: find the parent directory
226        let parent = self.find_dir(parent)?;
227
228        // Second pass: remove the directory from the parent
229        if !parent.dirs_contains(child) {
230            // Directory doesn't exist — return error
231            return Err(DSError::PathNotFound {
232                path: path.to_owned(),
233            });
234        }
235        parent.remove_dir(child);
236
237        Ok(())
238    }
239
240    /// Clears all tree contents.
241    ///
242    /// **Efficiency**: O(1)
243    pub fn clear(&mut self) {
244        self.root.clear();
245    }
246
247    /// Visits all leaf elements in the tree and performs a `visitor` for each of them.
248    pub fn visit(&self, mut visitor: impl FnMut(&Path)) {
249        fn visit_recursive(parent: &Path, current: &DirNode, visitor: &mut impl FnMut(&Path)) {
250            for name in current.files_iter() {
251                visitor(parent.join(Path::new(name)).as_path())
252            }
253            for (name, sub_childs) in current.dirs_iter() {
254                let parent = parent.join(Path::new(name));
255                if sub_childs.is_empty() {
256                    visitor(&parent);
257                } else {
258                    visit_recursive(&parent, sub_childs, visitor);
259                }
260            }
261        }
262
263        let parent = Path::new("/");
264
265        visit_recursive(parent, &self.root, &mut visitor);
266    }
267
268    fn check_path(
269        &self,
270        dir_path: &Path,
271        file_name: Option<&str>,
272    ) -> bool {
273        if self.is_empty() {
274            return false;
275        }
276
277        let mut current = &self.root;
278        let mut is_found = true;
279
280        // First pass: checks all parent directories
281        // Skip RootDir
282        let components = dir_path.components().skip(1);
283        for component in components {
284            let name = utils::path_comp_to_str(&component);
285            if let Some(next) = current.get_dir(name) {
286                current = next;
287            } else {
288                is_found = false;
289                break;
290            }
291        }
292
293        // Second pass: checks the file in the last directory
294        if let Some(file_name) = file_name && is_found {
295            if !current.files_contains(file_name) {
296                is_found = false;
297            }
298        }
299
300        is_found
301    }
302
303    fn ensure_dirs(&mut self, path: &Path) -> &mut DirNode {
304        let mut current = &mut self.root;
305
306        // Skip RootDir
307        let components = path.components().skip(1);
308        for component in components {
309            let name = utils::path_comp_to_str(&component);
310            current.insert_dir(name);
311            current =  current.get_dir_mut(name).unwrap();  // safe unwrap
312        }
313
314        current
315    }
316
317    /// Helper method to find a directory node by path components.
318    /// Returns error if any component in the path doesn't exist.
319    fn find_dir(&mut self, path: &Path) -> Result<&mut DirNode> {
320        let mut current = &mut self.root;
321
322        // Skip RootDir
323        let components = path.components().skip(1);
324        for component in components {
325            let name = utils::path_comp_to_str(&component);
326
327            if let Some(dir) = current.get_dir_mut(name) {
328                current = dir;
329            } else {
330                return Err(DSError::PathNotFound { path: path.to_owned() });
331            }
332        }
333
334        Ok(current)
335    }
336
337    /// Helper method to build a string path from components for error reporting.
338    fn build_path(&self, components: &[Component<'_>]) -> PathBuf {
339        let mut pb = PathBuf::from("/");
340        for component in components {
341            pb.push(component);
342        }
343        pb
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    mod add {
352        use super::*;
353
354        /// Test adding a simple directory at the root level.
355        #[test]
356        fn test_add_simple_dir() {
357            let mut tree = FileTree::new();
358
359            // Add /home directory
360            assert!(tree.add_dir("/home").is_ok());
361
362            // Verify that /home directory exists
363            assert!(tree.root.dirs_contains("home"));
364        }
365
366        /// Test adding nested directories.
367        #[test]
368        fn test_add_nested_dirs() {
369            let mut tree = FileTree::new();
370
371            // Add nested directories: /home/user/documents
372            assert!(tree.add_dir(Path::new("/home/user/documents")).is_ok());
373
374            // Verify the full path exists
375            let home = tree.root.get_dir("home").unwrap();
376            let user = home.get_dir("user").unwrap();
377            assert!(user.dirs_contains("documents"));
378        }
379
380        /// Test adding directory with root path (should succeed without changes).
381        #[test]
382        fn test_add_root_dir() {
383            let mut tree = FileTree::new();
384
385            // Adding root directory should be a no‑op
386            assert!(tree.add_dir(Path::new("/")).is_ok());
387
388            // Root should still exist and have no children
389            assert!(tree.root.is_empty());
390        }
391
392        /// Test adding file to an existing directory structure.
393        #[test]
394        fn test_add_file_to_existing_dirs() {
395            let mut tree = FileTree::new();
396
397            // First create directories
398            assert!(tree.add_dir(Path::new("/home/user")).is_ok());
399            // Then add a file
400            assert!(tree.add_file(Path::new("/home/user/document.txt")).is_ok());
401
402            // Verify file exists in the correct location
403            let home = tree.root.get_dir("home").unwrap();
404            let user = home.get_dir("user").unwrap();
405            assert!(user.files_contains("document.txt"));
406        }
407
408        /// Test adding file creates necessary intermediate directories.
409        #[test]
410        fn test_add_file_creates_intermediate_dirs() {
411            let mut tree = FileTree::new();
412
413            // Add file — this should create /projects/rust/ directories automatically
414            assert!(tree.add_file(Path::new("/projects/rust/main.rs")).is_ok());
415
416            // Verify full path was created
417            let projects = tree
418                .root
419                .get_dir("projects")
420                .unwrap();
421            let rust = projects.get_dir("rust").unwrap();
422            assert!(rust.files_contains("main.rs"));
423        }
424
425        /// Test error when adding non‑absolute path for directory.
426        #[test]
427        fn test_add_non_absolute_dir_path_error() {
428            let mut tree = FileTree::new();
429
430            // Relative path should return error
431            let result = tree.add_dir(Path::new("relative/path"));
432            assert!(result.is_err());
433            if let Err(DSError::NotAbsolutePath { path }) = result {
434                assert_eq!(path, PathBuf::from("relative/path"));
435            } else {
436                panic!("Expected NotAbsolutePath error");
437            }
438        }
439
440        /// Test error when adding non‑absolute path for file.
441        #[test]
442        fn test_add_non_absolute_file_path_error() {
443            let mut tree = FileTree::new();
444
445            // Relative path should return error
446            let result = tree.add_file(Path::new("document.txt"));
447            assert!(result.is_err());
448            if let Err(DSError::NotAbsolutePath { path }) = result {
449                assert_eq!(path, PathBuf::from("document.txt"));
450            } else {
451                panic!("Expected NotAbsolutePath error");
452            }
453        }
454
455        /// Test adding multiple files in the same directory.
456        #[test]
457        fn test_add_multiple_files_same_dir() {
458            let mut tree = FileTree::new();
459
460            // Add multiple files to /tmp directory
461            assert!(tree.add_file(Path::new("/tmp/file1.txt")).is_ok());
462            assert!(tree.add_file(Path::new("/tmp/file2.txt")).is_ok());
463            assert!(tree.add_file(Path::new("/tmp/script.sh")).is_ok());
464
465            // Verify all files exist
466            let tmp = tree.root.get_dir("tmp").unwrap();
467            assert!(tmp.files_contains("file1.txt"));
468            assert!(tmp.files_contains("file2.txt"));
469            assert!(tmp.files_contains("script.sh"));
470        }
471
472        /// Test idempotent behavior — adding the same path multiple times.
473        #[test]
474        fn test_idempotent_add_operations() {
475            let mut tree = FileTree::new();
476
477            // Add the same directory twice
478            assert!(tree.add_dir(Path::new("/var/log")).is_ok());
479            assert!(tree.add_dir(Path::new("/var/log")).is_ok()); // Should not fail
480
481            // Add the same file twice
482            assert!(tree.add_file(Path::new("/var/log/system.log")).is_ok());
483            // Second add should overwrite or be idempotent
484            assert!(tree.add_file(Path::new("/var/log/system.log")).is_ok());
485
486            // Verify structure is correct
487            let var = tree.root.get_dir("var").unwrap();
488            let log = var.get_dir("log").unwrap();
489            assert!(log.files_contains("system.log"));
490        }
491
492        /// Test handling of paths with special characters.
493        #[test]
494        fn test_add_path_with_special_chars() {
495            let mut tree = FileTree::new();
496
497            // Add directory with special characters
498            assert!(tree.add_dir(Path::new("/special-@#$%/test")).is_ok());
499
500            // Verify it was added correctly
501            let special = tree
502                .root
503                .get_dir("special-@#$%")
504                .unwrap();
505            assert!(special.dirs_contains("test"));
506        }
507
508        /// Test empty path handling.
509        #[test]
510        fn test_empty_path_handling() {
511            let mut tree = FileTree::new();
512
513            // Empty path should be handled gracefully
514            let empty_path = Path::new("");
515            let result = tree.add_file(empty_path);
516            assert!(result.is_err());
517            if let Err(DSError::EmptyPath) = result {
518                // Expected error type
519            } else {
520                panic!("Expected EmptyPath error for empty path");
521            }
522        }
523    }
524
525    mod contains {
526        use super::*;
527
528        /// Test that root path is always contained in the tree.
529        #[test]
530        fn test_contains_root_path() {
531            let tree = FileTree::new();
532
533            assert_eq!(tree.contains_dir("/"), Ok(true));
534            assert_eq!(
535                tree.contains_file("/"),
536                Err(DSError::NotFile {
537                    path: PathBuf::from("/")
538                })
539            ); // Root can be considered as existing
540        }
541
542        /// Test checking existence of a simple directory.
543        #[test]
544        fn test_contains_simple_dir() {
545            let mut tree = FileTree::new();
546            tree.add_dir(Path::new("/home")).unwrap();
547
548            assert_eq!(tree.contains_dir("/home"), Ok(true));
549            assert_eq!(tree.contains_file("/home"), Ok(false)); // Not a file
550        }
551
552        /// Test checking existence of a nested directory.
553        #[test]
554        fn test_contains_nested_dir() {
555            let mut tree = FileTree::new();
556            tree.add_dir(Path::new("/home/user/documents")).unwrap();
557
558            assert_eq!(tree.contains_dir("/home"), Ok(true));
559            assert_eq!(tree.contains_dir("/home/user"), Ok(true));
560            assert_eq!(tree.contains_dir("/home/user/documents"), Ok(true));
561        }
562
563        /// Test checking existence of a file.
564        #[test]
565        fn test_contains_file() {
566            let mut tree = FileTree::new();
567            tree.add_file(Path::new("/home/user/document.txt")).unwrap();
568
569            assert_eq!(tree.contains_file("/home/user/document.txt"), Ok(true));
570            assert_eq!(tree.contains_dir("/home/user/document.txt"), Ok(false)); // It's a file, not a directory
571        }
572
573        /// Test checking non‑existent path.
574        #[test]
575        fn test_contains_nonexistent_path() {
576            let tree = FileTree::new();
577
578            assert_eq!(tree.contains_dir("/nonexistent"), Ok(false));
579            assert_eq!(tree.contains_file("/home/user/file.txt"), Ok(false));
580        }
581
582        /// Test checking file existence in non‑existent directory.
583        #[test]
584        fn test_contains_file_in_nonexistent_dir() {
585            let mut tree = FileTree::new();
586            // Add only the parent directory
587            tree.add_dir(Path::new("/home")).unwrap();
588
589            // File doesn't exist
590            assert_eq!(tree.contains_file("/home/user/document.txt"), Ok(false));
591
592            // Directory doesn't exist
593            assert_eq!(tree.contains_dir("/home/user"), Ok(false));
594        }
595
596        /// Test error when checking empty path.
597        #[test]
598        fn test_contains_empty_path_error() {
599            let tree = FileTree::new();
600
601            let result = tree.contains_dir("");
602            assert!(result.is_err());
603            if let Err(DSError::EmptyPath) = result {
604                // Expected error type
605            } else {
606                panic!("Expected EmptyPath error for empty path");
607            }
608        }
609
610        /// Test error when checking non‑absolute path.
611        #[test]
612        fn test_contains_non_absolute_path_error() {
613            let tree = FileTree::new();
614
615            let result = tree.contains_dir("relative/path");
616            assert!(result.is_err());
617            if let Err(DSError::NotAbsolutePath { path }) = result {
618                assert_eq!(path, PathBuf::from("relative/path"));
619            } else {
620                panic!("Expected NotAbsolutePath error");
621            }
622        }
623
624        /// Test checking multiple paths in a complex tree structure.
625        #[test]
626        fn test_contains_multiple_paths_complex_tree() {
627            let mut tree = FileTree::new();
628
629            // Build a complex tree
630            tree.add_dir(Path::new("/etc")).unwrap();
631            tree.add_dir(Path::new("/var/log")).unwrap();
632            tree.add_file(Path::new("/etc/config")).unwrap();
633            tree.add_file(Path::new("/var/log/system.log")).unwrap();
634
635            // Test various paths
636            assert_eq!(tree.contains_dir("/etc"), Ok(true));
637            assert_eq!(tree.contains_dir("/var"), Ok(true));
638            assert_eq!(tree.contains_dir("/var/log"), Ok(true));
639            assert_eq!(tree.contains_file("/etc/config"), Ok(true));
640            assert_eq!(tree.contains_file("/var/log/system.log"), Ok(true));
641            assert_eq!(tree.contains_file("/etc/passwd"), Ok(false));
642            assert_eq!(tree.contains_dir("/tmp"), Ok(false));
643        }
644
645        /// Test checking path with special characters.
646        #[test]
647        fn test_contains_path_with_special_chars() {
648            let mut tree = FileTree::new();
649
650            tree.add_dir(Path::new("/special-@#$%")).unwrap();
651            tree.add_file(Path::new("/special-@#$%/test.file")).unwrap();
652
653            assert_eq!(tree.contains_dir("/special-@#$%"), Ok(true));
654            assert_eq!(tree.contains_file("/special-@#$%/test.file"), Ok(true));
655            assert_eq!(tree.contains_file("/special-@#$%/nonexistent"), Ok(false));
656        }
657
658        /// Test checking directory that exists as a file (should return false).
659        #[test]
660        fn test_contains_dir_but_is_file() {
661            let mut tree = FileTree::new();
662
663            // Add a file that would conflict with directory name
664            tree.add_file(Path::new("/conflicted")).unwrap();
665
666            // Should not be found as a directory
667            assert_eq!(tree.contains_dir("/conflicted"), Ok(false));
668            // But should be found as a file
669            assert_eq!(tree.contains_file("/conflicted"), Ok(true));
670        }
671
672        /// Test checking file that exists as a directory (should return false).
673        #[test]
674        fn test_contains_file_but_is_dir() {
675            let mut tree = FileTree::new();
676
677            // Add a directory that would conflict with file name
678            tree.add_dir(Path::new("/conflicted")).unwrap();
679
680            // Should not be found as a file
681            assert_eq!(tree.contains_file("/conflicted"), Ok(false));
682            // But should be found as a directory
683            assert_eq!(tree.contains_dir("/conflicted"), Ok(true));
684        }
685    }
686
687    mod remove_file {
688        use super::*;
689
690        /// Test removing a simple file from root directory.
691        #[test]
692        fn test_remove_simple_file() {
693            let mut tree = FileTree::new();
694            tree.add_file(Path::new("/document.txt")).unwrap();
695
696            // Verify file exists before removal
697            assert_eq!(tree.contains_file("/document.txt"), Ok(true));
698
699            // Remove the file
700            assert!(tree.remove_file("/document.txt").is_ok());
701
702            // Verify file no longer exists
703            assert_eq!(tree.contains_file("/document.txt"), Ok(false));
704        }
705
706        /// Test removing file from nested directory.
707        #[test]
708        fn test_remove_nested_file() {
709            let mut tree = FileTree::new();
710            tree.add_file(Path::new("/home/user/document.txt")).unwrap();
711
712            // Verify file exists
713            assert_eq!(tree.contains_file("/home/user/document.txt"), Ok(true));
714
715            // Remove the file
716            assert!(tree.remove_file("/home/user/document.txt").is_ok());
717
718            // Verify file is gone
719            assert_eq!(tree.contains_file("/home/user/document.txt"), Ok(false));
720
721            // But the directories should still exist
722            assert_eq!(tree.contains_dir("/home"), Ok(true));
723            assert_eq!(tree.contains_dir("/home/user"), Ok(true));
724        }
725
726        /// Test idempotent behavior — removing same file twice.
727        #[test]
728        fn test_idempotent_remove_file() {
729            let mut tree = FileTree::new();
730            tree.add_file(Path::new("/tmp/file.txt")).unwrap();
731
732            // First removal
733            assert!(tree.remove_file("/tmp/file.txt").is_ok());
734            // Second removal of same path
735            assert!(tree.remove_file("/tmp/file.txt").is_err()); // Should not fail
736
737            // File should not exist
738            assert_eq!(tree.contains_file("/tmp/file.txt"), Ok(false));
739        }
740
741        /// Test removing non-existent file.
742        #[test]
743        fn test_remove_nonexistent_file() {
744            let mut tree = FileTree::new();
745
746            // Try to remove file that doesn't exist
747            assert!(tree.remove_file("/nonexistent.txt").is_err());
748
749            // Tree should be empty
750            assert!(tree.root.is_empty());
751        }
752
753        /// Test error when removing empty path.
754        #[test]
755        fn test_remove_empty_path_error() {
756            let mut tree = FileTree::new();
757
758            let result = tree.remove_file("");
759            assert!(result.is_err());
760            if let Err(DSError::EmptyPath) = result {
761                // Expected error type
762            } else {
763                panic!("Expected EmptyPath error for empty path");
764            }
765        }
766
767        /// Test error when removing non-absolute path.
768        #[test]
769        fn test_remove_non_absolute_path_error() {
770            let mut tree = FileTree::new();
771
772            let result = tree.remove_file("relative/path/file.txt");
773            assert!(result.is_err());
774            if let Err(DSError::NotAbsolutePath { path }) = result {
775                assert_eq!(path, PathBuf::from("relative/path/file.txt"));
776            } else {
777                panic!("Expected NotAbsolutePath error");
778            }
779        }
780
781        /// Test error when trying to remove root as file.
782        #[test]
783        fn test_remove_root_as_file_error() {
784            let mut tree = FileTree::new();
785
786            let result = tree.remove_file("/");
787            assert!(result.is_err());
788            if let Err(DSError::NotFile { path }) = result {
789                assert_eq!(path, PathBuf::from("/"));
790            } else {
791                panic!("Expected NotFile error for root path");
792            }
793        }
794
795        /// Test removing multiple files from same directory.
796        #[test]
797        fn test_remove_multiple_files() {
798            let mut tree = FileTree::new();
799            tree.add_file(Path::new("/tmp/file1.txt")).unwrap();
800            tree.add_file(Path::new("/tmp/file2.txt")).unwrap();
801            tree.add_file(Path::new("/tmp/script.sh")).unwrap();
802
803            // Remove one file
804            assert!(tree.remove_file("/tmp/file1.txt").is_ok());
805            assert_eq!(tree.contains_file("/tmp/file1.txt"), Ok(false));
806
807            // Remove another
808            assert!(tree.remove_file("/tmp/script.sh").is_ok());
809            assert_eq!(tree.contains_file("/tmp/script.sh"), Ok(false));
810
811            // One file should still exist
812            assert_eq!(tree.contains_file("/tmp/file2.txt"), Ok(true));
813        }
814
815        /// Test removing file with special characters in name.
816        #[test]
817        fn test_remove_file_with_special_chars() {
818            let mut tree = FileTree::new();
819            tree.add_file(Path::new("/special-@#$%/test.file")).unwrap();
820
821            assert_eq!(tree.contains_file("/special-@#$%/test.file"), Ok(true));
822
823            // Remove file with special characters
824            assert!(tree.remove_file("/special-@#$%/test.file").is_ok());
825
826            assert_eq!(tree.contains_file("/special-@#$%/test.file"), Ok(false));
827        }
828    }
829
830    mod remove_dir {
831        use super::*;
832
833        /// Test removing simple directory.
834        #[test]
835        fn test_remove_simple_dir() {
836            let mut tree = FileTree::new();
837            tree.add_dir(Path::new("/home")).unwrap();
838
839            assert_eq!(tree.contains_dir("/home"), Ok(true));
840
841            assert!(tree.remove_dir("/home").is_ok());
842
843            assert_eq!(tree.contains_dir("/home"), Ok(false));
844        }
845
846        /// Test removing nested directory with files.
847        #[test]
848        fn test_remove_nested_dir_with_files() {
849            let mut tree = FileTree::new();
850            tree.add_file(Path::new("/projects/rust/main.rs")).unwrap();
851            tree.add_file(Path::new("/projects/python/script.py"))
852                .unwrap();
853
854            // Remove parent directory — this should remove all contents
855            assert!(tree.remove_dir("/projects").is_ok());
856
857            // All paths under /projects should be gone
858            assert_eq!(tree.contains_dir("/projects"), Ok(false));
859            assert_eq!(tree.contains_dir("/projects/rust"), Ok(false));
860            assert_eq!(tree.contains_file("/projects/rust/main.rs"), Ok(false));
861            assert_eq!(tree.contains_file("/projects/python/script.py"), Ok(false));
862        }
863
864        /// Test idempotent directory removal.
865        #[test]
866        fn test_idempotent_remove_dir() {
867            let mut tree = FileTree::new();
868            tree.add_dir(Path::new("/var/log")).unwrap();
869
870            assert!(tree.remove_dir("/var/log").is_ok());
871            assert!(tree.remove_dir("/var/log").is_err()); // Second removal
872
873            assert_eq!(tree.contains_dir("/var/log"), Ok(false));
874        }
875
876        /// Test removing non-existent directory.
877        #[test]
878        fn test_remove_nonexistent_dir() {
879            let mut tree = FileTree::new();
880
881            // Removing non-existent directory should succeed (idempotent)
882            assert!(tree.remove_dir("/nonexistent").is_err());
883        }
884
885        /// Test error when removing root directory.
886        #[test]
887        fn test_remove_root_dir_error() {
888            let mut tree = FileTree::new();
889
890            let result = tree.remove_dir("/");
891            assert!(result.is_err());
892            if let Err(DSError::NotFile { path }) = result {
893                assert_eq!(path, PathBuf::from("/"));
894            } else {
895                panic!("Expected NotFile error for root path");
896            }
897        }
898
899        /// Test removing directory with special characters in name.
900        #[test]
901        fn test_remove_dir_with_special_chars() {
902            let mut tree = FileTree::new();
903            tree.add_dir(Path::new("/special-@#$%/test")).unwrap();
904
905            assert_eq!(tree.contains_dir("/special-@#$%/test"), Ok(true));
906
907            // Remove directory with special characters
908            assert!(tree.remove_dir("/special-@#$%/test").is_ok());
909
910            assert_eq!(tree.contains_dir("/special-@#$%/test"), Ok(false));
911        }
912
913        /// Test error when removing file path as directory.
914        #[test]
915        fn test_remove_file_path_as_dir() {
916            let mut tree = FileTree::new();
917            tree.add_file(Path::new("/tmp/document.txt")).unwrap();
918
919            // Try to remove file path as directory
920            assert!(tree.remove_dir("/tmp/document.txt").is_err());
921            // Should error (idempotent) even though it's not a directory
922
923            // File should still exist
924            assert_eq!(tree.contains_file("/tmp/document.txt"), Ok(true));
925        }
926
927        /// Test removing intermediate directory — should remove all children.
928        #[test]
929        fn test_remove_intermediate_dir() {
930            let mut tree = FileTree::new();
931            tree.add_file(Path::new("/projects/rust/src/main.rs"))
932                .unwrap();
933            tree.add_file(Path::new("/projects/rust/tests/unit.rs"))
934                .unwrap();
935            tree.add_file(Path::new("/projects/python/app.py")).unwrap();
936
937            // Remove intermediate directory /projects/rust
938            assert!(tree.remove_dir("/projects/rust").is_ok());
939
940            // All paths under /projects/rust should be gone
941            assert_eq!(tree.contains_dir("/projects/rust"), Ok(false));
942            assert_eq!(tree.contains_file("/projects/rust/src/main.rs"), Ok(false));
943            assert_eq!(
944                tree.contains_file("/projects/rust/tests/unit.rs"),
945                Ok(false)
946            );
947
948            // But /projects/python should still exist
949            assert_eq!(tree.contains_dir("/projects/python"), Ok(true));
950            assert_eq!(tree.contains_file("/projects/python/app.py"), Ok(true));
951        }
952
953        /// Test removing directory that doesn't exist in middle of path.
954        #[test]
955        fn test_remove_nonexistent_middle_dir() {
956            let mut tree = FileTree::new();
957            tree.add_dir("/existing/path").unwrap();
958
959            // Try to remove directory with non‑existent parent
960            let result = tree.remove_dir("/nonexistent/parent/dir");
961            assert!(result.is_err()); // Should be error
962
963            // Existing path should still be there
964            assert_eq!(tree.contains_dir("/existing/path"), Ok(true));
965        }
966    }
967
968    mod build_path {
969        use super::*;
970
971        /// Test building path from components.
972        #[test]
973        fn test_build_path_simple() {
974            let tree = FileTree::new();
975            let components: Vec<_> = Path::new("/home/user").components().skip(1).collect();
976
977            let path = tree.build_path(&components);
978            assert_eq!(path, PathBuf::from("/home/user"));
979        }
980
981        /// Test building root path.
982        #[test]
983        fn test_build_root_path() {
984            let tree = FileTree::new();
985            let empty_components: Vec<Component<'_>> = vec![];
986
987            let path = tree.build_path(&empty_components);
988            assert_eq!(path, PathBuf::from("/"));
989        }
990
991        /// Test building complex path with special characters.
992        #[test]
993        fn test_build_path_special_chars() {
994            let tree = FileTree::new();
995            let components: Vec<_> = Path::new("/special-@#$%/test/path")
996                .components()
997                .skip(1)
998                .collect();
999
1000            let path = tree.build_path(&components);
1001            assert_eq!(path, PathBuf::from("/special-@#$%/test/path"));
1002        }
1003    }
1004
1005    mod clear {
1006        use super::*;
1007
1008        /// Test that clear() removes all child nodes from the root.
1009        #[test]
1010        fn test_clear_removes_all_children() {
1011            let mut tree = FileTree::new();
1012
1013            // Add some directories and files
1014            tree.add_dir(Path::new("/home/user")).unwrap();
1015            tree.add_file(Path::new("/etc/config")).unwrap();
1016
1017            // Verify that tree has children before clearing
1018            assert!(!tree.root.is_empty());
1019
1020            // Clear the tree
1021            tree.clear();
1022
1023            // Verify that all children are removed
1024            assert!(tree.root.is_empty());
1025        }
1026
1027        /// Test clear() on empty tree — should be a no‑op.
1028        #[test]
1029        fn test_clear_on_empty_tree() {
1030            let mut tree = FileTree::new();
1031
1032            // Tree is initially empty (no children)
1033            assert!(tree.root.is_empty());
1034
1035            // Clear should not change anything
1036            tree.clear();
1037
1038            assert!(tree.root.is_empty());
1039        }
1040
1041        /// Test clearing tree with complex nested structure.
1042        #[test]
1043        fn test_clear_complex_structure() {
1044            let mut tree = FileTree::new();
1045
1046            // Build a complex tree
1047            tree.add_dir(Path::new("/var/log")).unwrap();
1048            tree.add_dir(Path::new("/home/user/projects")).unwrap();
1049            tree.add_file(Path::new("/etc/passwd")).unwrap();
1050            tree.add_file(Path::new("/home/user/notes.txt")).unwrap();
1051
1052            // Verify tree has content before clearing
1053            assert!(tree.contains_dir("/var/log").unwrap());
1054            assert!(tree.contains_dir("/home/user/projects").unwrap());
1055            assert!(tree.contains_file("/etc/passwd").unwrap());
1056
1057            // Clear the tree
1058            tree.clear();
1059
1060            // Verify all content is gone
1061            assert!(!tree.contains_dir("/var/log").unwrap_or(false));
1062            assert!(!tree.contains_dir("/home/user/projects").unwrap_or(false));
1063            assert!(!tree.contains_file("/etc/passwd").unwrap_or(false));
1064            assert!(tree.root.is_empty());
1065        }
1066
1067        /// Test calling clear() multiple times — should remain empty.
1068        #[test]
1069        fn test_multiple_clear_calls() {
1070            let mut tree = FileTree::new();
1071
1072            tree.add_dir(Path::new("/test")).unwrap();
1073            tree.clear(); // First clear
1074            tree.clear(); // Second clear
1075
1076            assert!(tree.root.is_empty());
1077        }
1078    }
1079
1080    mod visit {
1081        use std::path::PathBuf;
1082        use super::*;
1083
1084        /// Test visiting an empty tree — no paths should be visited.
1085        #[test]
1086        fn test_visit_empty_tree() {
1087            let tree = FileTree::new();
1088            let mut visited = Vec::new();
1089            tree.visit(|path| visited.push(path.to_path_buf()));
1090            assert!(visited.is_empty(), "Empty tree should not visit any paths");
1091        }
1092
1093        /// Test visiting a tree with a single directory at the root level.
1094        /// Expected: visitor is called with "/home".
1095        #[test]
1096        fn test_visit_single_root_directory() {
1097            let mut tree = FileTree::new();
1098            tree.add_dir("/home").unwrap();
1099            let mut visited = Vec::new();
1100            tree.visit(|path| visited.push(path.to_path_buf()));
1101            assert_eq!(visited, vec![PathBuf::from("/home")], "Should visit /home");
1102        }
1103
1104        /// Test visiting a tree with a single file at the root level.
1105        /// Expected: visitor is called with "/file.txt".
1106        #[test]
1107        fn test_visit_single_root_file() {
1108            let mut tree = FileTree::new();
1109            tree.add_file("/file.txt").unwrap();
1110            let mut visited = Vec::new();
1111            tree.visit(|path| visited.push(path.to_path_buf()));
1112            assert_eq!(visited, vec![PathBuf::from("/file.txt")], "Should visit file.txt");
1113        }
1114
1115        /// Test visiting a nested directory structure.
1116        /// Tree: /home/user/documents
1117        /// Expected paths: "/home/user/documents".
1118        #[test]
1119        fn test_visit_nested_directories() {
1120            let mut tree = FileTree::new();
1121            tree.add_dir("/home/user/documents").unwrap();
1122            let mut visited = Vec::new();
1123            tree.visit(|path| visited.push(path.to_path_buf()));
1124            assert_eq!(
1125                visited,
1126                vec![PathBuf::from("/home/user/documents")],
1127                "Should visit the full nested directory path"
1128            );
1129        }
1130
1131        /// Test visiting a tree with files in nested directories.
1132        /// Tree:
1133        ///   /etc/passwd
1134        ///   /var/log/messages
1135        /// Expected paths:
1136        ///   "/etc/passwd"
1137        ///   "/var/log/messages"
1138        #[test]
1139        fn test_visit_files_in_nested_directories() {
1140            let mut tree = FileTree::new();
1141            tree.add_file("/etc/passwd").unwrap();
1142            tree.add_file("/var/log/messages").unwrap();
1143            let mut visited = Vec::new();
1144            tree.visit(|path| visited.push(path.to_path_buf()));
1145            // BTreeSet гарантирует лексикографический порядок
1146            assert_eq!(
1147                visited,
1148                vec![
1149                    PathBuf::from("/etc/passwd"),
1150                    PathBuf::from("/var/log/messages")
1151                ],
1152                "Should visit all files with correct full paths"
1153            );
1154        }
1155
1156        /// Test visiting a mixed structure with directories and files at different levels.
1157        /// Tree:
1158        ///   /home/user/document.txt
1159        ///   /tmp/script.sh
1160        ///   /var
1161        /// Expected paths:
1162        ///   "/home/user/document.txt"
1163        ///   "/tmp/script.sh"
1164        ///   "/var"
1165        #[test]
1166        fn test_visit_mixed_structure() {
1167            let mut tree = FileTree::new();
1168            tree.add_file("/home/user/document.txt").unwrap();
1169            tree.add_file("/tmp/script.sh").unwrap();
1170            tree.add_dir("/var").unwrap();
1171            let mut visited = Vec::new();
1172            tree.visit(|path| visited.push(path.to_path_buf()));
1173            // Ожидаемый порядок посещения: лексикографический по именам файлов и директорий
1174            assert_eq!(
1175                visited,
1176                vec![
1177                    PathBuf::from("/home/user/document.txt"),
1178                    PathBuf::from("/tmp/script.sh"),
1179                    PathBuf::from("/var")
1180                ],
1181                "Should visit all items with correct full paths in lexical order"
1182            );
1183        }
1184
1185        /// Test that directories with children are not visited directly —
1186        /// only their leaf descendants are visited.
1187        /// Tree: /a/b/c (where c is empty)
1188        /// Expected: only "/a/b/c" is visited, not "/a" or "/a/b".
1189        #[test]
1190        fn test_visit_only_leaf_directories() {
1191            let mut tree = FileTree::new();
1192            tree.add_dir("/a/b/c").unwrap();
1193            let mut visited = Vec::new();
1194            tree.visit(|path| visited.push(path.to_path_buf()));
1195            assert_eq!(
1196                visited,
1197                vec![PathBuf::from("/a/b/c")],
1198                "Only leaf directories should be visited"
1199            );
1200        }
1201
1202        /// Test visiting a complex tree with multiple branches and depths.
1203        /// Tree:
1204        ///   /projects/rust/main.rs
1205        ///   /projects/python/app.py
1206        ///   /docs/README.md
1207        ///   /temp
1208        /// Expected: all files and leaf directories with full paths.
1209        #[test]
1210        fn test_visit_complex_tree() {
1211            let mut tree = FileTree::new();
1212            tree.add_file("/projects/rust/main.rs").unwrap();
1213            tree.add_file("/projects/python/app.py").unwrap();
1214            tree.add_file("/docs/README.md").unwrap();
1215            tree.add_dir("/temp").unwrap();
1216            let mut visited = Vec::new();
1217            tree.visit(|path| visited.push(path.to_path_buf()));
1218            assert_eq!(
1219                visited,
1220                vec![
1221                    PathBuf::from("/docs/README.md"),
1222                    PathBuf::from("/projects/python/app.py"),
1223                    PathBuf::from("/projects/rust/main.rs"),
1224                    PathBuf::from("/temp")
1225                ],
1226                "Should visit all leaf items with full paths in correct order"
1227            );
1228        }
1229
1230        /// Test visiting a tree where a directory contains both files and subdirectories.
1231        /// Tree:
1232        ///   /mixed/file1.txt
1233        ///   /mixed/subdir/file2.txt
1234        /// Expected:
1235        ///   "/mixed/file1.txt"
1236        ///   "/mixed/subdir/file2.txt"
1237        #[test]
1238        fn test_visit_directory_with_files_and_subdirs() {
1239            let mut tree = FileTree::new();
1240            tree.add_file("/mixed/file1.txt").unwrap();
1241            tree.add_file("/mixed/subdir/file2.txt").unwrap();
1242            let mut visited = Vec::new();
1243            tree.visit(|path| visited.push(path.to_path_buf()));
1244            assert_eq!(
1245                visited,
1246                vec![
1247                    PathBuf::from("/mixed/file1.txt"),
1248                    PathBuf::from("/mixed/subdir/file2.txt")
1249                ],
1250                "Should visit both files and files in subdirectories with full paths"
1251            );
1252        }
1253    }
1254}