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