Skip to main content

immich_lib/testing/
report.rs

1//! Report formatting for test scenario coverage.
2
3use std::collections::HashMap;
4use serde::{Deserialize, Serialize};
5
6use super::scenarios::{ScenarioMatch, TestScenario};
7
8/// Test scenario coverage report.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ScenarioReport {
11    /// Total groups analyzed
12    pub total_groups: usize,
13
14    /// Matches grouped by scenario
15    pub coverage: HashMap<String, Vec<ScenarioMatch>>,
16
17    /// Scenarios with no matches
18    pub uncovered: Vec<String>,
19
20    /// Unexpected patterns discovered
21    pub unexpected: Vec<String>,
22}
23
24impl ScenarioReport {
25    /// Create a new report from scenario matches.
26    pub fn from_matches(matches: Vec<ScenarioMatch>, total_groups: usize) -> Self {
27        let mut coverage: HashMap<String, Vec<ScenarioMatch>> = HashMap::new();
28
29        // Group matches by scenario
30        for m in matches {
31            let key = m.scenario.to_string();
32            coverage.entry(key).or_default().push(m);
33        }
34
35        // Find uncovered scenarios
36        let all_scenarios = TestScenario::all();
37        let uncovered: Vec<String> = all_scenarios
38            .iter()
39            .filter(|s| !coverage.contains_key(&s.to_string()))
40            .map(|s| s.to_string())
41            .collect();
42
43        Self {
44            total_groups,
45            coverage,
46            uncovered,
47            unexpected: Vec::new(),
48        }
49    }
50
51    /// Add an unexpected pattern.
52    pub fn add_unexpected(&mut self, pattern: String) {
53        self.unexpected.push(pattern);
54    }
55}
56
57/// Format the report for text output.
58pub fn format_report(report: &ScenarioReport) -> String {
59    let mut output = String::new();
60
61    output.push_str("=== Test Scenario Coverage Report ===\n\n");
62
63    // Coverage statistics
64    let total_scenarios = TestScenario::all().len();
65    let covered_count = report.coverage.len();
66    let coverage_pct = (covered_count as f64 / total_scenarios as f64) * 100.0;
67
68    // Covered scenarios by category
69    output.push_str(&format!(
70        "COVERED ({}/{} scenarios, {:.0}%):\n",
71        covered_count, total_scenarios, coverage_pct
72    ));
73
74    // Group by category
75    let categories = ["Winner Selection", "Consolidation", "Conflicts", "Edge Cases"];
76    for category in categories {
77        let category_scenarios: Vec<(&String, &Vec<ScenarioMatch>)> = report
78            .coverage
79            .iter()
80            .filter(|(k, _)| {
81                let prefix = k.chars().next().unwrap_or('?');
82                match category {
83                    "Winner Selection" => prefix == 'W',
84                    "Consolidation" => prefix == 'C',
85                    "Conflicts" => prefix == 'F',
86                    "Edge Cases" => prefix == 'X',
87                    _ => false,
88                }
89            })
90            .collect();
91
92        if !category_scenarios.is_empty() {
93            output.push_str(&format!("\n  {}:\n", category));
94            for (scenario, matches) in category_scenarios {
95                output.push_str(&format!("    {}: {} groups\n", scenario, matches.len()));
96                // Show first example
97                if let Some(first) = matches.first() {
98                    output.push_str(&format!(
99                        "      Example: {} ({})\n",
100                        first.duplicate_id, first.details
101                    ));
102                }
103            }
104        }
105    }
106
107    // Uncovered scenarios
108    if !report.uncovered.is_empty() {
109        output.push_str(&format!(
110            "\nNOT COVERED ({} scenarios):\n",
111            report.uncovered.len()
112        ));
113        for scenario in &report.uncovered {
114            output.push_str(&format!("  {}: 0 groups\n", scenario));
115        }
116    }
117
118    // Unexpected patterns
119    if !report.unexpected.is_empty() {
120        output.push_str("\nUNEXPECTED PATTERNS:\n");
121        for pattern in &report.unexpected {
122            output.push_str(&format!("  - {}\n", pattern));
123        }
124    }
125
126    // Summary
127    output.push_str("\n=== Summary ===\n");
128    output.push_str(&format!("Total groups analyzed: {}\n", report.total_groups));
129    output.push_str(&format!(
130        "Scenarios covered: {}/{} ({:.0}%)\n",
131        covered_count, total_scenarios, coverage_pct
132    ));
133    output.push_str(&format!(
134        "Synthetic images needed for: {} scenarios\n",
135        report.uncovered.len()
136    ));
137
138    output
139}