Skip to main content

seshat_cli/report/
overview.rs

1//! Project Overview section of the scan report.
2//!
3//! Displays a language breakdown with bar charts, total file count,
4//! and dependency count with per-ecosystem breakdown.
5
6use crate::format;
7use crate::report::ReportData;
8
9/// Print the Project Overview section.
10///
11/// ```text
12/// ── Project Overview ─────────────────────────────────────────
13///   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░  66.7%  Rust (40 files)
14///   ▓▓▓▓▓▓░░░░░░░░░░░░░░  33.3%  Python (20 files)
15///
16///   60 files, 127 packages (98 cargo, 29 pip)
17/// ```
18pub fn print_overview(data: &ReportData, color: bool) {
19    eprintln!(
20        "{}",
21        format::format_section_header("Project Overview", color)
22    );
23
24    // Language breakdown bar charts.
25    if !data.language_breakdown.is_empty() {
26        let total = data.total_files.max(1) as f64;
27        for lc in &data.language_breakdown {
28            let fraction = lc.count as f64 / total;
29            eprintln!(
30                "{}",
31                format::format_bar_chart(
32                    &lc.language.to_string(),
33                    lc.count,
34                    fraction,
35                    "files",
36                    color,
37                ),
38            );
39        }
40        eprintln!();
41    }
42
43    // Summary: file count + dependency count with ecosystem breakdown.
44    let dep_detail = format_dependency_detail(data);
45    eprintln!(
46        "  {} files, {}",
47        format::format_number(data.total_files as u64),
48        dep_detail,
49    );
50    eprintln!();
51}
52
53/// Format the dependency count with per-ecosystem breakdown.
54///
55/// Returns e.g. `"127 packages (98 cargo, 29 pip)"` or `"0 packages"`.
56fn format_dependency_detail(data: &ReportData) -> String {
57    let total = data.total_dependencies;
58    if data.dependency_breakdown.is_empty() {
59        return format!("{} packages", format::format_number(total as u64));
60    }
61
62    let parts: Vec<String> = data
63        .dependency_breakdown
64        .iter()
65        .map(|ec| format!("{} {}", ec.count, ec.label))
66        .collect();
67
68    format!(
69        "{} packages ({})",
70        format::format_number(total as u64),
71        parts.join(", "),
72    )
73}
74
75// ══════════════════════════════════════════════════════════════════════
76// Tests
77// ══════════════════════════════════════════════════════════════════════
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::report::{EcosystemCount, LanguageCount};
83    use seshat_core::Language;
84
85    fn make_report_data(
86        language_breakdown: Vec<LanguageCount>,
87        total_files: usize,
88        total_dependencies: usize,
89        dependency_breakdown: Vec<EcosystemCount>,
90    ) -> ReportData {
91        ReportData {
92            language_breakdown,
93            total_files,
94            total_dependencies,
95            dependency_breakdown,
96            conventions: vec![],
97            files_discovered: total_files,
98            files_parsed: total_files,
99            nodes_persisted: 0,
100            edges_persisted: 0,
101            manifests_analyzed: 0,
102            docs_ingested: 0,
103            db_path: std::path::PathBuf::from("/tmp/test.db"),
104            db_size: 0,
105            elapsed: std::time::Duration::from_secs(1),
106            excluded_submodules: vec![],
107            submodules_excluded_by_flag: false,
108        }
109    }
110
111    #[test]
112    fn test_format_dependency_detail_empty() {
113        let data = make_report_data(vec![], 0, 0, vec![]);
114        assert_eq!(format_dependency_detail(&data), "0 packages");
115    }
116
117    #[test]
118    fn test_format_dependency_detail_single_ecosystem() {
119        let data = make_report_data(
120            vec![],
121            10,
122            42,
123            vec![EcosystemCount {
124                label: "cargo".to_owned(),
125                count: 42,
126            }],
127        );
128        assert_eq!(format_dependency_detail(&data), "42 packages (42 cargo)");
129    }
130
131    #[test]
132    fn test_format_dependency_detail_multiple_ecosystems() {
133        let data = make_report_data(
134            vec![],
135            10,
136            127,
137            vec![
138                EcosystemCount {
139                    label: "npm".to_owned(),
140                    count: 98,
141                },
142                EcosystemCount {
143                    label: "pip".to_owned(),
144                    count: 29,
145                },
146            ],
147        );
148        assert_eq!(
149            format_dependency_detail(&data),
150            "127 packages (98 npm, 29 pip)",
151        );
152    }
153
154    #[test]
155    fn test_format_dependency_detail_large_number() {
156        let data = make_report_data(
157            vec![],
158            10,
159            1500,
160            vec![EcosystemCount {
161                label: "npm".to_owned(),
162                count: 1500,
163            }],
164        );
165        assert_eq!(format_dependency_detail(&data), "1,500 packages (1500 npm)",);
166    }
167
168    #[test]
169    fn test_print_overview_does_not_panic() {
170        let data = make_report_data(
171            vec![
172                LanguageCount {
173                    language: Language::Rust,
174                    count: 40,
175                },
176                LanguageCount {
177                    language: Language::Python,
178                    count: 20,
179                },
180            ],
181            60,
182            127,
183            vec![
184                EcosystemCount {
185                    label: "cargo".to_owned(),
186                    count: 98,
187                },
188                EcosystemCount {
189                    label: "pip".to_owned(),
190                    count: 29,
191                },
192            ],
193        );
194        // Just ensure it doesn't panic — output goes to stderr.
195        print_overview(&data, false);
196        print_overview(&data, true);
197    }
198
199    #[test]
200    fn test_print_overview_empty_data() {
201        let data = make_report_data(vec![], 0, 0, vec![]);
202        // Should not panic even with empty data.
203        print_overview(&data, false);
204    }
205}