Skip to main content

st/formatters/
summary.rs

1//! Summary formatter - "Intelligent defaults for humans!" - Omni
2//! Provides an intelligent summary based on directory content
3
4use super::Formatter;
5use crate::content_detector::{ContentDetector, DirectoryType, Language};
6use crate::scanner::{FileNode, TreeStats};
7use anyhow::Result;
8use colored::Colorize;
9use std::collections::HashMap;
10use std::io::Write;
11use std::path::Path;
12
13pub struct SummaryFormatter {
14    use_color: bool,
15    max_examples: usize,
16}
17
18impl SummaryFormatter {
19    pub fn new(use_color: bool) -> Self {
20        Self {
21            use_color,
22            max_examples: 5,
23        }
24    }
25
26    fn colorize(&self, text: &str, color: &str) -> String {
27        if self.use_color {
28            match color {
29                "blue" => text.blue().to_string(),
30                "green" => text.green().to_string(),
31                "yellow" => text.yellow().to_string(),
32                "red" => text.red().to_string(),
33                "cyan" => text.cyan().to_string(),
34                "magenta" => text.magenta().to_string(),
35                "bold" => text.bold().to_string(),
36                _ => text.to_string(),
37            }
38        } else {
39            text.to_string()
40        }
41    }
42
43    fn is_high_level_directory(&self, nodes: &[FileNode], _stats: &TreeStats) -> bool {
44        // Heuristics for detecting high-level directories:
45        // 1. More than 20 subdirectories in root
46        // 2. Has typical home directory folders (Documents, Downloads, etc.)
47        // 3. Has multiple project-like directories
48
49        // Count directories at root level (relative to scanned path)
50        let mut root_dir_count = 0;
51        let mut seen_paths = std::collections::HashSet::new();
52
53        for node in nodes {
54            if node.is_dir {
55                // Get the depth relative to the first node's parent
56                if let Some(first) = nodes.first() {
57                    if let Some(base) = first.path.parent() {
58                        if let Ok(relative) = node.path.strip_prefix(base) {
59                            if relative.components().count() == 1
60                                && seen_paths.insert(node.path.clone())
61                            {
62                                root_dir_count += 1;
63                            }
64                        }
65                    }
66                }
67            }
68        }
69
70        if root_dir_count > 20 {
71            return true;
72        }
73
74        // Check for home directory patterns
75        let home_folders = [
76            "Documents",
77            "Downloads",
78            "Desktop",
79            "Pictures",
80            "Music",
81            "Videos",
82        ];
83        let mut home_folder_count = 0;
84
85        for node in nodes {
86            if node.is_dir {
87                if let Some(name) = node.path.file_name().and_then(|f| f.to_str()) {
88                    if home_folders.contains(&name) {
89                        home_folder_count += 1;
90                    }
91                }
92            }
93        }
94
95        if home_folder_count >= 3 {
96            return true;
97        }
98
99        // Check for multiple project-like directories
100        let project_indicators = [
101            "Cargo.toml",
102            "package.json",
103            "pom.xml",
104            ".git",
105            "requirements.txt",
106        ];
107        let mut project_dirs = std::collections::HashSet::new();
108
109        for node in nodes {
110            if let Some(name) = node.path.file_name().and_then(|f| f.to_str()) {
111                if project_indicators.contains(&name) {
112                    if let Some(parent) = node.path.parent() {
113                        project_dirs.insert(parent);
114                    }
115                }
116            }
117        }
118
119        project_dirs.len() > 5
120    }
121
122    fn format_high_level_summary(
123        &self,
124        writer: &mut dyn Write,
125        nodes: &[FileNode],
126        stats: &TreeStats,
127        root_path: &Path,
128    ) -> Result<()> {
129        // Header
130        writeln!(writer, "{}", self.colorize("📊 Directory Overview", "bold"))?;
131        writeln!(writer, "{}", self.colorize("─".repeat(50).as_str(), "blue"))?;
132        writeln!(writer)?;
133
134        // Path and basic stats
135        writeln!(
136            writer,
137            "📁 {}: {}",
138            self.colorize("Path", "cyan"),
139            root_path.display()
140        )?;
141        writeln!(
142            writer,
143            "📈 {}: {} files, {} directories, {}",
144            self.colorize("Total", "cyan"),
145            self.colorize(&stats.total_files.to_string(), "green"),
146            self.colorize(&stats.total_dirs.to_string(), "green"),
147            self.colorize(&format_size(stats.total_size), "green")
148        )?;
149        writeln!(writer)?;
150
151        // Analyze subdirectories (skip root-level files)
152        let mut subdirs: HashMap<String, (usize, usize, u64)> = HashMap::new();
153        let mut actual_dirs: std::collections::HashSet<String> = std::collections::HashSet::new();
154
155        for node in nodes {
156            if let Ok(relative) = node.path.strip_prefix(root_path) {
157                let components: Vec<_> = relative.components().collect();
158                if let Some(first) = components.first() {
159                    if let Some(name) = first.as_os_str().to_str() {
160                        // Only track as directory if:
161                        // 1. The node is a directory at depth 1, OR
162                        // 2. There are more components (meaning this is a parent dir)
163                        if node.is_dir && components.len() == 1 {
164                            actual_dirs.insert(name.to_string());
165                        }
166                        if components.len() > 1 {
167                            actual_dirs.insert(name.to_string());
168                        }
169
170                        let entry = subdirs.entry(name.to_string()).or_insert((0, 0, 0));
171                        if node.is_dir {
172                            entry.1 += 1;
173                        } else {
174                            entry.0 += 1;
175                            entry.2 += node.size;
176                        }
177                    }
178                }
179            }
180        }
181
182        // Filter to only actual directories and sort by size
183        let mut sorted_dirs: Vec<_> = subdirs
184            .into_iter()
185            .filter(|(name, _)| actual_dirs.contains(name))
186            .collect();
187        sorted_dirs.sort_by(|a, b| b.1 .2.cmp(&a.1 .2));
188
189        // Show top directories
190        writeln!(
191            writer,
192            "{}",
193            self.colorize("Top Directories by Size:", "yellow")
194        )?;
195        writeln!(writer)?;
196
197        for (name, (files, dirs, size)) in sorted_dirs.iter().take(10) {
198            let size_str = format_size(*size);
199            let size_bar = self.make_size_bar(*size, stats.total_size);
200
201            writeln!(
202                writer,
203                "  {} {} {}",
204                self.colorize(&format!("{:20}", name), "cyan"),
205                self.colorize(&format!("{:>10}", size_str), "green"),
206                size_bar
207            )?;
208            writeln!(
209                writer,
210                "  {:20} {} files, {} dirs",
211                "",
212                self.colorize(&files.to_string(), "blue"),
213                self.colorize(&dirs.to_string(), "blue")
214            )?;
215            writeln!(writer)?;
216        }
217
218        // Detect projects
219        let projects = self.detect_projects(nodes, root_path);
220        if !projects.is_empty() {
221            writeln!(writer, "{}", self.colorize("Detected Projects:", "yellow"))?;
222            writeln!(writer)?;
223
224            for (path, project_type) in projects.iter().take(10) {
225                writeln!(
226                    writer,
227                    "  • {} {}",
228                    self.colorize(path, "cyan"),
229                    self.colorize(&format!("({})", project_type), "magenta")
230                )?;
231            }
232            writeln!(writer)?;
233        }
234
235        // Footer
236        writeln!(writer, "{}", self.colorize("─".repeat(50).as_str(), "blue"))?;
237        writeln!(
238            writer,
239            "💡 {}: Use {} to analyze a specific directory",
240            self.colorize("Tip", "yellow"),
241            self.colorize("st <directory>", "cyan")
242        )?;
243
244        Ok(())
245    }
246
247    fn make_size_bar(&self, size: u64, total: u64) -> String {
248        if total == 0 {
249            return String::new();
250        }
251
252        let percentage = (size as f64 / total as f64) * 100.0;
253        let bar_width = 20;
254        let filled = ((percentage / 100.0) * bar_width as f64) as usize;
255
256        let bar = "█".repeat(filled) + &"░".repeat(bar_width - filled);
257
258        format!("{} {:5.1}%", self.colorize(&bar, "blue"), percentage)
259    }
260
261    fn detect_projects(&self, nodes: &[FileNode], root_path: &Path) -> Vec<(String, String)> {
262        let mut projects = Vec::new();
263        let mut checked_dirs = std::collections::HashSet::new();
264
265        for node in nodes {
266            if let Some(parent) = node.path.parent() {
267                if checked_dirs.contains(parent) {
268                    continue;
269                }
270                checked_dirs.insert(parent);
271
272                let name = node.path.file_name().and_then(|n| n.to_str()).unwrap_or("");
273
274                let project_type = match name {
275                    "Cargo.toml" => Some("Rust"),
276                    "package.json" => Some("Node.js"),
277                    "requirements.txt" | "setup.py" | "pyproject.toml" => Some("Python"),
278                    "go.mod" => Some("Go"),
279                    "pom.xml" => Some("Java/Maven"),
280                    "build.gradle" | "build.gradle.kts" => Some("Java/Gradle"),
281                    "Gemfile" => Some("Ruby"),
282                    ".git" if node.is_dir => Some("Git Repository"),
283                    _ => None,
284                };
285
286                if let Some(ptype) = project_type {
287                    if let Ok(relative) = parent.strip_prefix(root_path) {
288                        projects.push((relative.display().to_string(), ptype.to_string()));
289                    }
290                }
291            }
292        }
293
294        projects.sort();
295        projects.dedup();
296        projects
297    }
298}
299
300impl Formatter for SummaryFormatter {
301    fn format(
302        &self,
303        writer: &mut dyn Write,
304        nodes: &[FileNode],
305        stats: &TreeStats,
306        root_path: &Path,
307    ) -> Result<()> {
308        // Check if this looks like a high-level directory (home, root, etc)
309        let is_high_level = self.is_high_level_directory(nodes, stats);
310
311        if is_high_level {
312            return self.format_high_level_summary(writer, nodes, stats, root_path);
313        }
314
315        // Detect directory type for project-level analysis
316        let dir_type = ContentDetector::detect(nodes, root_path);
317
318        // Header
319        writeln!(writer, "{}", self.colorize("📊 Directory Summary", "bold"))?;
320        writeln!(writer, "{}", self.colorize("─".repeat(50).as_str(), "blue"))?;
321        writeln!(writer)?;
322
323        // Path and basic stats
324        writeln!(
325            writer,
326            "📁 {}: {}",
327            self.colorize("Path", "cyan"),
328            root_path.display()
329        )?;
330        writeln!(
331            writer,
332            "📈 {}: {} files, {} directories, {}",
333            self.colorize("Stats", "cyan"),
334            self.colorize(&stats.total_files.to_string(), "green"),
335            self.colorize(&stats.total_dirs.to_string(), "green"),
336            self.colorize(&format_size(stats.total_size), "green")
337        )?;
338        writeln!(writer)?;
339
340        // Content-specific analysis
341        match &dir_type {
342            DirectoryType::CodeProject {
343                language,
344                framework,
345                has_tests,
346                has_docs,
347            } => {
348                writeln!(
349                    writer,
350                    "🔧 {}: {} Project",
351                    self.colorize("Type", "yellow"),
352                    self.colorize(&format!("{:?}", language), "magenta")
353                )?;
354
355                if let Some(fw) = framework {
356                    writeln!(
357                        writer,
358                        "🚀 {}: {:?}",
359                        self.colorize("Framework", "yellow"),
360                        fw
361                    )?;
362                }
363
364                writeln!(
365                    writer,
366                    "✅ Tests: {} | 📚 Docs: {}",
367                    if *has_tests {
368                        self.colorize("Yes", "green")
369                    } else {
370                        self.colorize("No", "red")
371                    },
372                    if *has_docs {
373                        self.colorize("Yes", "green")
374                    } else {
375                        self.colorize("No", "red")
376                    }
377                )?;
378
379                // Show main files
380                writeln!(writer)?;
381                writeln!(writer, "{}", self.colorize("Key Files:", "cyan"))?;
382
383                // Find and display important files
384                let important_files = find_important_code_files(nodes, language);
385                for file in important_files.iter().take(self.max_examples) {
386                    writeln!(writer, "  • {}", file)?;
387                }
388
389                // Language-specific tips
390                writeln!(writer)?;
391                writeln!(writer, "{}", self.colorize("Quick Commands:", "cyan"))?;
392                match language {
393                    Language::Rust => {
394                        writeln!(writer, "  • cargo build --release")?;
395                        writeln!(writer, "  • cargo test")?;
396                        writeln!(writer, "  • cargo run")?;
397                    }
398                    Language::Python => {
399                        writeln!(writer, "  • python -m venv venv")?;
400                        writeln!(writer, "  • pip install -r requirements.txt")?;
401                        writeln!(writer, "  • python main.py")?;
402                    }
403                    Language::JavaScript | Language::TypeScript => {
404                        writeln!(writer, "  • npm install")?;
405                        writeln!(writer, "  • npm test")?;
406                        writeln!(writer, "  • npm start")?;
407                    }
408                    _ => {
409                        writeln!(writer, "  • Check README for build instructions")?;
410                    }
411                }
412            }
413
414            DirectoryType::PhotoCollection {
415                image_count,
416                date_range,
417                cameras,
418            } => {
419                writeln!(
420                    writer,
421                    "📷 {}: Photo Collection",
422                    self.colorize("Type", "yellow")
423                )?;
424                writeln!(
425                    writer,
426                    "🖼️  {}: {} images",
427                    self.colorize("Count", "cyan"),
428                    self.colorize(&image_count.to_string(), "green")
429                )?;
430
431                if let Some((start, end)) = date_range {
432                    writeln!(
433                        writer,
434                        "📅 {}: {} to {}",
435                        self.colorize("Date Range", "cyan"),
436                        start,
437                        end
438                    )?;
439                }
440
441                if !cameras.is_empty() {
442                    writeln!(
443                        writer,
444                        "📸 {}: {}",
445                        self.colorize("Cameras", "cyan"),
446                        cameras.join(", ")
447                    )?;
448                }
449
450                // Show file type breakdown
451                let mut type_counts: HashMap<&str, usize> = HashMap::new();
452                for node in nodes {
453                    if !node.is_dir {
454                        if let Some(ext) = node.path.extension().and_then(|e| e.to_str()) {
455                            *type_counts.entry(ext).or_insert(0) += 1;
456                        }
457                    }
458                }
459
460                writeln!(writer)?;
461                writeln!(writer, "{}", self.colorize("File Types:", "cyan"))?;
462                for (ext, count) in type_counts.iter() {
463                    writeln!(writer, "  • .{}: {}", ext, count)?;
464                }
465            }
466
467            DirectoryType::DocumentArchive {
468                categories,
469                total_docs,
470            } => {
471                writeln!(
472                    writer,
473                    "📚 {}: Document Archive",
474                    self.colorize("Type", "yellow")
475                )?;
476                writeln!(
477                    writer,
478                    "📄 {}: {} documents",
479                    self.colorize("Count", "cyan"),
480                    self.colorize(&total_docs.to_string(), "green")
481                )?;
482
483                if !categories.is_empty() {
484                    writeln!(writer)?;
485                    writeln!(writer, "{}", self.colorize("Categories:", "cyan"))?;
486                    for (category, count) in categories.iter() {
487                        writeln!(writer, "  • {}: {}", category, count)?;
488                    }
489                }
490            }
491
492            DirectoryType::MediaLibrary {
493                video_count,
494                audio_count,
495                total_duration,
496                quality,
497            } => {
498                writeln!(
499                    writer,
500                    "🎬 {}: Media Library",
501                    self.colorize("Type", "yellow")
502                )?;
503                writeln!(
504                    writer,
505                    "🎥 Videos: {} | 🎵 Audio: {}",
506                    self.colorize(&video_count.to_string(), "green"),
507                    self.colorize(&audio_count.to_string(), "green")
508                )?;
509
510                if let Some(duration) = total_duration {
511                    writeln!(
512                        writer,
513                        "⏱️  {}: {}",
514                        self.colorize("Total Duration", "cyan"),
515                        duration
516                    )?;
517                }
518
519                if !quality.is_empty() {
520                    writeln!(
521                        writer,
522                        "📺 {}: {}",
523                        self.colorize("Quality", "cyan"),
524                        quality.join(", ")
525                    )?;
526                }
527            }
528
529            DirectoryType::DataScience {
530                notebooks,
531                datasets,
532                languages,
533            } => {
534                writeln!(
535                    writer,
536                    "🔬 {}: Data Science Workspace",
537                    self.colorize("Type", "yellow")
538                )?;
539                writeln!(
540                    writer,
541                    "📓 Notebooks: {} | 📊 Datasets: {}",
542                    self.colorize(&notebooks.to_string(), "green"),
543                    self.colorize(&datasets.to_string(), "green")
544                )?;
545
546                if !languages.is_empty() {
547                    writeln!(
548                        writer,
549                        "🐍 {}: {}",
550                        self.colorize("Languages", "cyan"),
551                        languages.join(", ")
552                    )?;
553                }
554
555                writeln!(writer)?;
556                writeln!(writer, "{}", self.colorize("Quick Commands:", "cyan"))?;
557                writeln!(writer, "  • jupyter notebook")?;
558                writeln!(writer, "  • jupyter lab")?;
559                writeln!(writer, "  • python -m notebook")?;
560            }
561
562            DirectoryType::MixedContent {
563                dominant_type,
564                file_types,
565                total_files,
566            } => {
567                writeln!(
568                    writer,
569                    "📦 {}: Mixed Content",
570                    self.colorize("Type", "yellow")
571                )?;
572
573                if let Some(dominant) = dominant_type {
574                    writeln!(
575                        writer,
576                        "🎯 {}: {}",
577                        self.colorize("Dominant Type", "cyan"),
578                        dominant
579                    )?;
580                }
581
582                writeln!(
583                    writer,
584                    "📊 {}: {}",
585                    self.colorize("Total Files", "cyan"),
586                    self.colorize(&total_files.to_string(), "green")
587                )?;
588
589                // Show top file types
590                let mut types: Vec<_> = file_types.iter().collect();
591                types.sort_by(|a, b| b.1.cmp(a.1));
592
593                writeln!(writer)?;
594                writeln!(writer, "{}", self.colorize("Top File Types:", "cyan"))?;
595                for (ext, count) in types.iter().take(self.max_examples) {
596                    writeln!(writer, "  • .{}: {}", ext, count)?;
597                }
598            }
599        }
600
601        // Footer with suggestions
602        writeln!(writer)?;
603        writeln!(writer, "{}", self.colorize("─".repeat(50).as_str(), "blue"))?;
604        writeln!(
605            writer,
606            "💡 {}: Use {} for detailed analysis",
607            self.colorize("Tip", "yellow"),
608            self.colorize("st --mode relations", "cyan")
609        )?;
610
611        Ok(())
612    }
613}
614
615fn format_size(bytes: u64) -> String {
616    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
617    let mut size = bytes as f64;
618    let mut unit_index = 0;
619
620    while size >= 1024.0 && unit_index < UNITS.len() - 1 {
621        size /= 1024.0;
622        unit_index += 1;
623    }
624
625    if unit_index == 0 {
626        format!("{} {}", size as u64, UNITS[unit_index])
627    } else {
628        format!("{:.2} {}", size, UNITS[unit_index])
629    }
630}
631
632fn find_important_code_files(nodes: &[FileNode], language: &Language) -> Vec<String> {
633    let mut important = Vec::new();
634
635    for node in nodes {
636        if node.is_dir {
637            continue;
638        }
639
640        let name = node.path.file_name().and_then(|n| n.to_str()).unwrap_or("");
641
642        let is_important = match language {
643            Language::Rust => {
644                matches!(name, "main.rs" | "lib.rs" | "Cargo.toml" | "build.rs")
645            }
646            Language::Python => {
647                matches!(
648                    name,
649                    "main.py" | "__init__.py" | "setup.py" | "requirements.txt" | "pyproject.toml"
650                )
651            }
652            Language::JavaScript | Language::TypeScript => {
653                matches!(
654                    name,
655                    "index.js"
656                        | "index.ts"
657                        | "package.json"
658                        | "tsconfig.json"
659                        | "webpack.config.js"
660                )
661            }
662            Language::Go => {
663                matches!(name, "main.go" | "go.mod" | "go.sum")
664            }
665            Language::Java => {
666                matches!(name, "Main.java" | "pom.xml" | "build.gradle")
667            }
668            _ => false,
669        };
670
671        if is_important {
672            important.push(node.path.display().to_string());
673        }
674    }
675
676    important
677}
678
679#[cfg(test)]
680mod tests {
681    use super::*;
682    use crate::scanner::FileNode;
683    use std::path::PathBuf;
684
685    fn create_test_nodes() -> Vec<FileNode> {
686        use crate::scanner::{FileCategory, FileType, FilesystemType};
687        vec![
688            FileNode {
689                path: PathBuf::from("/test/src/main.rs"),
690                is_dir: false,
691                size: 1000,
692                permissions: 0o644,
693                uid: 1000,
694                gid: 1000,
695                modified: std::time::SystemTime::now(),
696                is_symlink: false,
697                is_hidden: false,
698                permission_denied: false,
699                is_ignored: false,
700                depth: 2,
701                file_type: FileType::RegularFile,
702                category: FileCategory::Rust,
703                search_matches: None,
704                filesystem_type: FilesystemType::Ext4,
705                git_branch: None,
706                traversal_context: None,
707                interest: None,
708                security_findings: Vec::new(),
709                change_status: None,
710                content_hash: None,
711            },
712            FileNode {
713                path: PathBuf::from("/test/Cargo.toml"),
714                is_dir: false,
715                size: 500,
716                permissions: 0o644,
717                uid: 1000,
718                gid: 1000,
719                modified: std::time::SystemTime::now(),
720                is_symlink: false,
721                is_hidden: false,
722                permission_denied: false,
723                is_ignored: false,
724                depth: 1,
725                file_type: FileType::RegularFile,
726                category: FileCategory::Toml,
727                search_matches: None,
728                filesystem_type: FilesystemType::Ext4,
729                git_branch: None,
730                traversal_context: None,
731                interest: None,
732                security_findings: Vec::new(),
733                change_status: None,
734                content_hash: None,
735            },
736            FileNode {
737                path: PathBuf::from("/test/src"),
738                is_dir: true,
739                size: 0,
740                permissions: 0o755,
741                uid: 1000,
742                gid: 1000,
743                modified: std::time::SystemTime::now(),
744                is_symlink: false,
745                is_hidden: false,
746                permission_denied: false,
747                is_ignored: false,
748                depth: 1,
749                file_type: FileType::Directory,
750                category: FileCategory::Unknown,
751                search_matches: None,
752                filesystem_type: FilesystemType::Ext4,
753                git_branch: None,
754                traversal_context: None,
755                interest: None,
756                security_findings: Vec::new(),
757                change_status: None,
758                content_hash: None,
759            },
760        ]
761    }
762
763    #[test]
764    fn test_summary_formatter_rust_project() {
765        let formatter = SummaryFormatter::new(false);
766        let nodes = create_test_nodes();
767        let stats = TreeStats {
768            total_files: 2,
769            total_dirs: 1,
770            total_size: 1500,
771            file_types: HashMap::new(),
772            largest_files: vec![],
773            newest_files: vec![],
774            oldest_files: vec![],
775        };
776
777        let mut output = Vec::new();
778        let result = formatter.format(&mut output, &nodes, &stats, &PathBuf::from("/test"));
779
780        assert!(result.is_ok());
781        let output_str = String::from_utf8(output).unwrap();
782        assert!(output_str.contains("Rust Project"));
783        assert!(output_str.contains("cargo build"));
784    }
785
786    #[test]
787    fn test_high_level_directory_detection() {
788        let formatter = SummaryFormatter::new(false);
789
790        // Create many directories to trigger high-level detection
791        use crate::scanner::{FileCategory, FileType, FilesystemType};
792        let mut nodes = vec![];
793        for i in 0..25 {
794            nodes.push(FileNode {
795                path: PathBuf::from(format!("/home/user/dir{}", i)),
796                is_dir: true,
797                size: 0,
798                permissions: 0o755,
799                uid: 1000,
800                gid: 1000,
801                modified: std::time::SystemTime::now(),
802                is_symlink: false,
803                is_hidden: false,
804                permission_denied: false,
805                is_ignored: false,
806                depth: 1,
807                file_type: FileType::Directory,
808                category: FileCategory::Unknown,
809                search_matches: None,
810                filesystem_type: FilesystemType::Ext4,
811                git_branch: None,
812                traversal_context: None,
813                interest: None,
814                security_findings: Vec::new(),
815                change_status: None,
816                content_hash: None,
817            });
818        }
819
820        let stats = TreeStats {
821            total_files: 0,
822            total_dirs: 25,
823            total_size: 0,
824            file_types: HashMap::new(),
825            largest_files: vec![],
826            newest_files: vec![],
827            oldest_files: vec![],
828        };
829
830        let is_high_level = formatter.is_high_level_directory(&nodes, &stats);
831        assert!(is_high_level);
832    }
833}