immich_lib/testing/
report.rs1use std::collections::HashMap;
4use serde::{Deserialize, Serialize};
5
6use super::scenarios::{ScenarioMatch, TestScenario};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ScenarioReport {
11 pub total_groups: usize,
13
14 pub coverage: HashMap<String, Vec<ScenarioMatch>>,
16
17 pub uncovered: Vec<String>,
19
20 pub unexpected: Vec<String>,
22}
23
24impl ScenarioReport {
25 pub fn from_matches(matches: Vec<ScenarioMatch>, total_groups: usize) -> Self {
27 let mut coverage: HashMap<String, Vec<ScenarioMatch>> = HashMap::new();
28
29 for m in matches {
31 let key = m.scenario.to_string();
32 coverage.entry(key).or_default().push(m);
33 }
34
35 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 pub fn add_unexpected(&mut self, pattern: String) {
53 self.unexpected.push(pattern);
54 }
55}
56
57pub 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 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 output.push_str(&format!(
70 "COVERED ({}/{} scenarios, {:.0}%):\n",
71 covered_count, total_scenarios, coverage_pct
72 ));
73
74 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 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 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 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 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}