1#![allow(clippy::cast_precision_loss)]
4#![allow(clippy::cast_possible_truncation)]
5#![allow(clippy::cast_sign_loss)]
6
7use crate::config::CliConfig;
8use crate::error::{CliError, CliResult};
9use crate::{CoverageArgs, PaletteArg};
10use jugar_probar::pixel_coverage::{ColorPalette, CoverageCell, PixelCoverageReport, PngHeatmap};
11use std::path::Path;
12
13pub fn execute_coverage(_config: &CliConfig, args: &CoverageArgs) -> CliResult<()> {
15 println!("Generating coverage heatmap...");
16 let cells = load_cells(args)?;
17 let heatmap = build_heatmap(args);
18
19 if let Some(ref png_path) = args.png {
20 export_png(&heatmap, &cells, png_path)?;
21 }
22 if let Some(ref json_path) = args.json {
23 export_json(&cells, json_path)?;
24 }
25 if args.png.is_none() && args.json.is_none() {
26 print_summary(&cells);
27 }
28 Ok(())
29}
30
31fn load_cells(args: &CoverageArgs) -> CliResult<Vec<Vec<CoverageCell>>> {
32 let Some(ref input) = args.input else {
33 println!("No input file specified, using sample data");
34 return Ok(create_sample_coverage_data());
35 };
36 println!("Loading coverage data from {}...", input.display());
37 load_coverage_from_json(input)
38}
39
40fn build_heatmap(args: &CoverageArgs) -> PngHeatmap {
41 let palette = match args.palette {
42 PaletteArg::Viridis => ColorPalette::viridis(),
43 PaletteArg::Magma => ColorPalette::magma(),
44 PaletteArg::Heat => ColorPalette::heat(),
45 };
46 let mut heatmap = PngHeatmap::new(args.width, args.height).with_palette(palette);
47 if args.legend {
48 heatmap = heatmap.with_legend();
49 }
50 if args.gaps {
51 heatmap = heatmap.with_gap_highlighting();
52 }
53 if let Some(ref title) = args.title {
54 heatmap = heatmap.with_title(title);
55 }
56 heatmap
57}
58
59fn export_png(heatmap: &PngHeatmap, cells: &[Vec<CoverageCell>], png_path: &Path) -> CliResult<()> {
60 heatmap
61 .export_to_file(cells, png_path)
62 .map_err(|e| CliError::report_generation(e.to_string()))?;
63 println!("PNG heatmap exported to: {}", png_path.display());
64 Ok(())
65}
66
67fn export_json(cells: &[Vec<CoverageCell>], json_path: &Path) -> CliResult<()> {
68 let report = generate_coverage_report(cells);
69 let json = serde_json::to_string_pretty(&report)
70 .map_err(|e| CliError::report_generation(e.to_string()))?;
71 std::fs::write(json_path, json).map_err(|e| CliError::report_generation(e.to_string()))?;
72 println!("Coverage report exported to: {}", json_path.display());
73 Ok(())
74}
75
76fn print_summary(cells: &[Vec<CoverageCell>]) {
77 let report = generate_coverage_report(cells);
78 println!("\nCoverage Summary:");
79 println!(
80 " Overall Coverage: {:.1}%",
81 report.overall_coverage * 100.0
82 );
83 println!(
84 " Covered Cells: {}/{}",
85 report.covered_cells, report.total_cells
86 );
87 println!(
88 " Meets Threshold: {}",
89 if report.meets_threshold { "Y" } else { "N" }
90 );
91 println!("\nUse --png <path> to export a heatmap image.");
92}
93
94pub fn load_coverage_from_json(path: &Path) -> CliResult<Vec<Vec<CoverageCell>>> {
96 #[derive(serde::Deserialize)]
97 struct CoverageData {
98 cells: Option<Vec<Vec<CoverageCell>>>,
99 #[serde(flatten)]
100 _extra: std::collections::HashMap<String, serde_json::Value>,
101 }
102
103 let content = std::fs::read_to_string(path).map_err(|e| {
104 CliError::report_generation(format!("Failed to read {}: {}", path.display(), e))
105 })?;
106
107 if let Ok(data) = serde_json::from_str::<CoverageData>(&content) {
108 if let Some(cells) = data.cells {
109 return Ok(cells);
110 }
111 }
112
113 serde_json::from_str::<Vec<Vec<CoverageCell>>>(&content)
114 .map_err(|e| CliError::report_generation(format!("Invalid JSON format: {e}")))
115}
116
117#[must_use]
119pub fn is_gap_cell(row: usize, col: usize) -> bool {
120 let middle_gap = row == 5 && (5..=7).contains(&col);
121 let end_gap = row == 2 && col > 10;
122 middle_gap || end_gap
123}
124
125#[must_use]
127pub fn calculate_coverage(row: usize, col: usize, rows: usize, cols: usize) -> f32 {
128 if is_gap_cell(row, col) {
129 return 0.0;
130 }
131 let x_factor = col as f32 / (cols - 1) as f32;
132 let y_factor = row as f32 / (rows - 1) as f32;
133 (x_factor + y_factor) / 2.0
134}
135
136#[must_use]
138pub fn create_sample_coverage_data() -> Vec<Vec<CoverageCell>> {
139 const ROWS: usize = 10;
140 const COLS: usize = 15;
141
142 (0..ROWS)
143 .map(|row| {
144 (0..COLS)
145 .map(|col| {
146 let coverage = calculate_coverage(row, col, ROWS, COLS);
147 CoverageCell {
148 coverage,
149 hit_count: (coverage * 10.0) as u64,
150 }
151 })
152 .collect()
153 })
154 .collect()
155}
156
157#[must_use]
159pub fn generate_coverage_report(cells: &[Vec<CoverageCell>]) -> PixelCoverageReport {
160 let total_cells = cells.iter().map(std::vec::Vec::len).sum::<usize>() as u32;
161 let covered_cells = cells
162 .iter()
163 .flat_map(|r| r.iter())
164 .filter(|c| c.coverage > 0.0)
165 .count() as u32;
166
167 let overall_coverage = if total_cells > 0 {
168 covered_cells as f32 / total_cells as f32
169 } else {
170 0.0
171 };
172
173 PixelCoverageReport {
174 grid_width: cells.first().map_or(0, |r| r.len() as u32),
175 grid_height: cells.len() as u32,
176 overall_coverage,
177 covered_cells,
178 total_cells,
179 min_coverage: 0.0,
180 max_coverage: 1.0,
181 total_interactions: 0,
182 meets_threshold: overall_coverage >= 0.8,
183 uncovered_regions: Vec::new(),
184 }
185}
186
187#[cfg(test)]
188#[allow(clippy::unwrap_used, clippy::float_cmp)]
189mod tests {
190 use super::*;
191 use tempfile::TempDir;
192
193 #[test]
194 fn test_is_gap_cell_middle() {
195 assert!(is_gap_cell(5, 5));
196 assert!(is_gap_cell(5, 6));
197 assert!(is_gap_cell(5, 7));
198 }
199
200 #[test]
201 fn test_is_gap_cell_end() {
202 assert!(is_gap_cell(2, 11));
203 assert!(is_gap_cell(2, 12));
204 assert!(is_gap_cell(2, 100));
205 }
206
207 #[test]
208 fn test_is_gap_cell_not_gap() {
209 assert!(!is_gap_cell(0, 0));
210 assert!(!is_gap_cell(5, 4));
211 assert!(!is_gap_cell(5, 8));
212 assert!(!is_gap_cell(2, 10));
213 assert!(!is_gap_cell(3, 11));
214 }
215
216 #[test]
217 fn test_calculate_coverage_gap_cell() {
218 assert_eq!(calculate_coverage(5, 6, 10, 15), 0.0);
219 assert_eq!(calculate_coverage(2, 11, 10, 15), 0.0);
220 }
221
222 #[test]
223 fn test_calculate_coverage_corners() {
224 assert_eq!(calculate_coverage(0, 0, 10, 15), 0.0);
225 assert_eq!(calculate_coverage(9, 14, 10, 15), 1.0);
226 }
227
228 #[test]
229 fn test_calculate_coverage_middle() {
230 let coverage = calculate_coverage(4, 7, 10, 15);
231 assert!(coverage > 0.0);
232 assert!(coverage < 1.0);
233 }
234
235 #[test]
236 fn test_create_sample_coverage_data() {
237 let data = create_sample_coverage_data();
238 assert_eq!(data.len(), 10);
239 assert_eq!(data[0].len(), 15);
240
241 assert_eq!(data[0][0].coverage, 0.0);
243 assert_eq!(data[9][14].coverage, 1.0);
244
245 assert_eq!(data[5][5].coverage, 0.0);
247 assert_eq!(data[5][6].coverage, 0.0);
248 assert_eq!(data[5][7].coverage, 0.0);
249 }
250
251 #[test]
252 fn test_generate_coverage_report() {
253 let data = create_sample_coverage_data();
254 let report = generate_coverage_report(&data);
255
256 assert_eq!(report.grid_width, 15);
257 assert_eq!(report.grid_height, 10);
258 assert_eq!(report.total_cells, 150);
259 assert!(report.covered_cells < 150); assert!(report.overall_coverage > 0.0);
261 assert!(report.overall_coverage < 1.0);
262 }
263
264 #[test]
265 fn test_generate_coverage_report_empty() {
266 let data: Vec<Vec<CoverageCell>> = vec![];
267 let report = generate_coverage_report(&data);
268
269 assert_eq!(report.total_cells, 0);
270 assert_eq!(report.covered_cells, 0);
271 assert_eq!(report.overall_coverage, 0.0);
272 assert!(!report.meets_threshold);
273 }
274
275 #[test]
276 fn test_generate_coverage_report_full_coverage() {
277 let data = vec![vec![
278 CoverageCell {
279 coverage: 1.0,
280 hit_count: 10,
281 };
282 10
283 ]];
284 let report = generate_coverage_report(&data);
285
286 assert_eq!(report.total_cells, 10);
287 assert_eq!(report.covered_cells, 10);
288 assert_eq!(report.overall_coverage, 1.0);
289 assert!(report.meets_threshold);
290 }
291
292 #[test]
293 fn test_load_coverage_from_json_array_format() {
294 let temp = TempDir::new().unwrap();
295 let path = temp.path().join("coverage.json");
296
297 let json = r#"[[{"coverage": 0.5, "hit_count": 5}]]"#;
298 std::fs::write(&path, json).unwrap();
299
300 let cells = load_coverage_from_json(&path).unwrap();
301 assert_eq!(cells.len(), 1);
302 assert_eq!(cells[0].len(), 1);
303 assert_eq!(cells[0][0].coverage, 0.5);
304 }
305
306 #[test]
307 fn test_load_coverage_from_json_wrapped_format() {
308 let temp = TempDir::new().unwrap();
309 let path = temp.path().join("coverage.json");
310
311 let json = r#"{"cells": [[{"coverage": 0.8, "hit_count": 8}]]}"#;
312 std::fs::write(&path, json).unwrap();
313
314 let cells = load_coverage_from_json(&path).unwrap();
315 assert_eq!(cells.len(), 1);
316 assert_eq!(cells[0][0].coverage, 0.8);
317 }
318
319 #[test]
320 fn test_load_coverage_from_json_not_found() {
321 let result = load_coverage_from_json(Path::new("/nonexistent/path.json"));
322 assert!(result.is_err());
323 }
324
325 #[test]
326 fn test_load_coverage_from_json_invalid() {
327 let temp = TempDir::new().unwrap();
328 let path = temp.path().join("invalid.json");
329
330 std::fs::write(&path, "not valid json").unwrap();
331
332 let result = load_coverage_from_json(&path);
333 assert!(result.is_err());
334 }
335
336 #[test]
337 fn test_execute_coverage_sample_data() {
338 let config = CliConfig::default();
339 let args = CoverageArgs {
340 png: None,
341 json: None,
342 palette: PaletteArg::Viridis,
343 legend: false,
344 gaps: false,
345 title: None,
346 width: 800,
347 height: 600,
348 input: None,
349 };
350
351 let result = execute_coverage(&config, &args);
353 assert!(result.is_ok());
354 }
355
356 #[test]
357 fn test_execute_coverage_with_json_output() {
358 let temp = TempDir::new().unwrap();
359 let json_path = temp.path().join("output.json");
360
361 let config = CliConfig::default();
362 let args = CoverageArgs {
363 png: None,
364 json: Some(json_path.clone()),
365 palette: PaletteArg::Magma,
366 legend: true,
367 gaps: true,
368 title: Some("Test Coverage".to_string()),
369 width: 400,
370 height: 300,
371 input: None,
372 };
373
374 let result = execute_coverage(&config, &args);
375 assert!(result.is_ok());
376 assert!(json_path.exists());
377
378 let content = std::fs::read_to_string(&json_path).unwrap();
379 let _: PixelCoverageReport = serde_json::from_str(&content).unwrap();
380 }
381}