memscope_rs/cli/commands/
generate_report.rs

1//! Report generation command implementation
2//!
3//! This module provides the report generation subcommand functionality.
4
5use clap::ArgMatches;
6use std::error::Error;
7
8/// Run the generate report command
9pub fn run_generate_report(matches: &ArgMatches) -> Result<(), Box<dyn Error>> {
10    let input_file = matches
11        .get_one::<String>("input")
12        .ok_or("Input file is required")?;
13    let output_file = matches
14        .get_one::<String>("output")
15        .ok_or("Output file is required")?;
16    let format = matches
17        .get_one::<String>("format")
18        .map(|s| s.as_str())
19        .unwrap_or("html");
20
21    tracing::info!("📊 Generating report...");
22    tracing::info!("Input file: {}", input_file);
23    tracing::info!("Output file: {}", output_file);
24    tracing::info!("Format: {}", format);
25
26    match format {
27        "html" => {
28            let default_template = "report_template.html".to_string();
29            embed_json_to_html(input_file, &default_template, output_file)?;
30        }
31        _ => {
32            return Err(format!("Unsupported format: {format}").into());
33        }
34    }
35
36    Ok(())
37}
38
39fn _original_main() {
40    let args: Vec<String> = std::env::args().collect();
41
42    if args.len() < 2 {
43        _print_usage();
44        return;
45    }
46
47    match args[1].as_str() {
48        "template" => {
49            let default_output = "interactive_template.html".to_string();
50            let output = args.get(2).unwrap_or(&default_output);
51
52            // Check if source file exists
53            if !std::path::Path::new("interactive_template.html").exists() {
54                tracing::error!("❌ Source template 'interactive_template.html' not found!");
55                tracing::error!("Please make sure the interactive_template.html file exists in the current directory.");
56                std::process::exit(1);
57            }
58
59            if let Err(e) = std::fs::copy("interactive_template.html", output) {
60                tracing::error!("❌ Error creating template: {e}");
61                std::process::exit(1);
62            }
63            tracing::info!("✅ Created interactive template: {output}");
64        }
65        "generate" => {
66            if args.len() < 4 {
67                tracing::error!(
68                    "❌ Usage: generate_report generate <json_file> <output_file> [template_file]"
69                );
70                std::process::exit(1);
71            }
72
73            let json_file = &args[2];
74            let output_file = &args[3];
75            let default_template = "report_template.html".to_string();
76            let template_file = args.get(4).unwrap_or(&default_template);
77
78            if let Err(e) = embed_json_to_html(json_file, template_file, output_file) {
79                tracing::error!("❌ Error generating report: {e}");
80                std::process::exit(1);
81            }
82        }
83        _ => {
84            _print_usage();
85        }
86    }
87}
88
89fn embed_json_to_html(
90    json_file: &str,
91    template_file: &str,
92    output_file: &str,
93) -> Result<(), Box<dyn std::error::Error>> {
94    let json_content = std::fs::read_to_string(json_file)?;
95
96    let template_content = std::fs::read_to_string(template_file)?;
97    let inline_script = format!(
98        r#"<script type="text/javascript">
99// Embedded JSON data for offline analysis
100window.EMBEDDED_MEMORY_DATA = {json_content};
101console.log('📊 Loaded embedded memory analysis data');
102</script>"#
103    );
104
105    let final_html = template_content.replace("<!-- DATA_INJECTION_POINT -->", &inline_script);
106
107    std::fs::write(output_file, final_html)?;
108
109    tracing::info!("✅ Generated self-contained HTML report: {output_file}");
110    Ok(())
111}
112
113fn _print_usage() {
114    tracing::info!("🔍 Memory Analysis Report Generator");
115    tracing::info!("");
116    tracing::info!("Usage:");
117    tracing::info!("  generate_report template [output_file]");
118    tracing::info!("    Create a standalone HTML template");
119    tracing::info!("");
120    tracing::info!("  generate_report generate <json_file> <output_file> [template_file]");
121    tracing::info!("    Generate a self-contained HTML report from JSON data");
122    tracing::info!("");
123    tracing::info!("Examples:");
124    tracing::info!("  generate_report template report_template.html");
125    tracing::info!("  generate_report generate data.json report.html report_template.html");
126    tracing::info!("");
127    tracing::info!("The generated report.html is completely self-contained and can be:");
128    tracing::info!("  ✅ Opened directly in any browser (no server needed)");
129    tracing::info!("  ✅ Shared via email or file transfer");
130    tracing::info!("  ✅ Archived for historical analysis");
131    tracing::info!("  ✅ Viewed offline without any dependencies");
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use clap::{Arg, ArgMatches, Command as ClapCommand};
138    use std::fs;
139    use tempfile::TempDir;
140
141    fn create_test_matches(input: &str, output: &str, format: Option<&str>) -> ArgMatches {
142        let cmd = ClapCommand::new("test")
143            .arg(Arg::new("input"))
144            .arg(Arg::new("output"))
145            .arg(Arg::new("format").long("format"));
146
147        let mut args = vec!["test", input, output];
148        if let Some(fmt) = format {
149            args.extend_from_slice(&["--format", fmt]);
150        }
151
152        cmd.try_get_matches_from(args)
153            .expect("Failed to create test matches")
154    }
155
156    #[test]
157    fn test_argument_extraction() {
158        // Test argument extraction from ArgMatches
159        let matches = create_test_matches("input.json", "output.html", Some("html"));
160
161        let input_file = matches.get_one::<String>("input").unwrap();
162        let output_file = matches.get_one::<String>("output").unwrap();
163        let format = matches
164            .get_one::<String>("format")
165            .map(|s| s.as_str())
166            .unwrap_or("html");
167
168        assert_eq!(input_file, "input.json");
169        assert_eq!(output_file, "output.html");
170        assert_eq!(format, "html");
171    }
172
173    #[test]
174    fn test_default_format() {
175        // Test default format handling
176        let matches = create_test_matches("input.json", "output.html", None);
177
178        let format = matches
179            .get_one::<String>("format")
180            .map(|s| s.as_str())
181            .unwrap_or("html");
182
183        assert_eq!(format, "html");
184    }
185
186    #[test]
187    fn test_embed_json_to_html() {
188        // Test JSON embedding into HTML template
189        let temp_dir = TempDir::new().expect("Failed to create temp directory");
190
191        // Create test JSON file
192        let json_file = temp_dir.path().join("test_data.json");
193        let test_json = r#"{"memory_analysis": {"total_allocations": 100}}"#;
194        fs::write(&json_file, test_json).expect("Failed to write JSON file");
195
196        // Create test HTML template
197        let template_file = temp_dir.path().join("template.html");
198        let template_content = r#"<!DOCTYPE html>
199<html>
200<head><title>Test Report</title></head>
201<body>
202    <h1>Memory Analysis Report</h1>
203    <!-- DATA_INJECTION_POINT -->
204    <div id="content"></div>
205</body>
206</html>"#;
207        fs::write(&template_file, template_content).expect("Failed to write template file");
208
209        // Test embedding
210        let output_file = temp_dir.path().join("output.html");
211        let result = embed_json_to_html(
212            json_file.to_str().unwrap(),
213            template_file.to_str().unwrap(),
214            output_file.to_str().unwrap(),
215        );
216
217        assert!(result.is_ok());
218        assert!(output_file.exists());
219
220        // Verify embedded content
221        let output_content = fs::read_to_string(&output_file).expect("Failed to read output file");
222        assert!(output_content.contains("window.EMBEDDED_MEMORY_DATA"));
223        assert!(output_content.contains("memory_analysis"));
224        assert!(output_content.contains("total_allocations"));
225        assert!(!output_content.contains("<!-- DATA_INJECTION_POINT -->"));
226    }
227
228    #[test]
229    fn test_run_generate_report_html_format() {
230        // Test report generation with HTML format
231        let temp_dir = TempDir::new().expect("Failed to create temp directory");
232
233        // Create test files
234        let json_file = temp_dir.path().join("input.json");
235        let template_file = temp_dir.path().join("report_template.html");
236        let output_file = temp_dir.path().join("output.html");
237
238        let test_json = r#"{"test": "data"}"#;
239        fs::write(&json_file, test_json).expect("Failed to write JSON file");
240
241        let template_content = r#"<html><body><!-- DATA_INJECTION_POINT --></body></html>"#;
242        fs::write(&template_file, template_content).expect("Failed to write template file");
243
244        let matches = create_test_matches(
245            json_file.to_str().unwrap(),
246            output_file.to_str().unwrap(),
247            Some("html"),
248        );
249
250        // Change to temp directory to find the template
251        let original_dir = std::env::current_dir().expect("Failed to get current directory");
252        std::env::set_current_dir(&temp_dir).expect("Failed to change directory");
253
254        let result = run_generate_report(&matches);
255
256        // Restore original directory
257        std::env::set_current_dir(original_dir).expect("Failed to restore directory");
258
259        assert!(result.is_ok());
260        assert!(output_file.exists());
261    }
262
263    #[test]
264    fn test_run_generate_report_unsupported_format() {
265        // Test error handling for unsupported format
266        let temp_dir = TempDir::new().expect("Failed to create temp directory");
267        let json_file = temp_dir.path().join("input.json");
268        let output_file = temp_dir.path().join("output.xml");
269
270        fs::write(&json_file, "{}").expect("Failed to write JSON file");
271
272        let matches = create_test_matches(
273            json_file.to_str().unwrap(),
274            output_file.to_str().unwrap(),
275            Some("xml"),
276        );
277
278        let result = run_generate_report(&matches);
279        assert!(result.is_err());
280        assert!(result
281            .unwrap_err()
282            .to_string()
283            .contains("Unsupported format: xml"));
284    }
285
286    #[test]
287    fn test_missing_arguments() {
288        // Test error handling for missing arguments
289        let cmd = ClapCommand::new("test")
290            .arg(Arg::new("input"))
291            .arg(Arg::new("output"));
292
293        let matches = cmd.try_get_matches_from(vec!["test"]).unwrap();
294
295        let input_result = matches.get_one::<String>("input");
296        let output_result = matches.get_one::<String>("output");
297
298        assert!(input_result.is_none());
299        assert!(output_result.is_none());
300    }
301
302    #[test]
303    fn test_json_injection_script_generation() {
304        // Test JSON injection script generation
305        let test_json = r#"{"allocations": [{"id": 1}, {"id": 2}]}"#;
306
307        let inline_script = format!(
308            r#"<script type="text/javascript">
309// Embedded JSON data for offline analysis
310window.EMBEDDED_MEMORY_DATA = {test_json};
311console.log('📊 Loaded embedded memory analysis data');
312</script>"#
313        );
314
315        assert!(inline_script.contains("window.EMBEDDED_MEMORY_DATA"));
316        assert!(inline_script.contains("allocations"));
317        assert!(inline_script.contains("console.log"));
318        assert!(inline_script.contains("<script"));
319        assert!(inline_script.contains("</script>"));
320    }
321
322    #[test]
323    fn test_template_replacement() {
324        // Test template content replacement
325        let template_content = r#"<html>
326<head><title>Report</title></head>
327<body>
328    <h1>Analysis</h1>
329    <!-- DATA_INJECTION_POINT -->
330    <footer>End</footer>
331</body>
332</html>"#;
333
334        let replacement_script = r#"<script>window.DATA = {};</script>"#;
335        let final_html =
336            template_content.replace("<!-- DATA_INJECTION_POINT -->", replacement_script);
337
338        assert!(!final_html.contains("<!-- DATA_INJECTION_POINT -->"));
339        assert!(final_html.contains("<script>window.DATA = {};</script>"));
340        assert!(final_html.contains("<title>Report</title>"));
341        assert!(final_html.contains("<footer>End</footer>"));
342    }
343
344    #[test]
345    fn test_file_operations() {
346        // Test file read/write operations
347        let temp_dir = TempDir::new().expect("Failed to create temp directory");
348
349        // Test file writing
350        let test_file = temp_dir.path().join("test.txt");
351        let test_content = "Hello, World!";
352        let write_result = fs::write(&test_file, test_content);
353        assert!(write_result.is_ok());
354        assert!(test_file.exists());
355
356        // Test file reading
357        let read_result = fs::read_to_string(&test_file);
358        assert!(read_result.is_ok());
359        assert_eq!(read_result.unwrap(), test_content);
360
361        // Test reading non-existent file
362        let non_existent = temp_dir.path().join("non_existent.txt");
363        let read_error = fs::read_to_string(&non_existent);
364        assert!(read_error.is_err());
365    }
366
367    #[test]
368    fn test_format_validation() {
369        // Test format validation logic
370        let supported_formats = ["html"];
371        let unsupported_formats = ["xml", "pdf", "docx", "txt"];
372
373        for format in supported_formats {
374            assert_eq!(format, "html");
375        }
376
377        for format in unsupported_formats {
378            assert_ne!(format, "html");
379        }
380    }
381
382    #[test]
383    fn test_complex_json_embedding() {
384        // Test embedding complex JSON data
385        let temp_dir = TempDir::new().expect("Failed to create temp directory");
386
387        let complex_json = r#"{
388            "memory_analysis": {
389                "statistics": {
390                    "total_allocations": 1000,
391                    "active_allocations": 500,
392                    "peak_memory": 1048576
393                },
394                "allocations": [
395                    {"id": 1, "size": 1024, "type": "Vec<i32>"},
396                    {"id": 2, "size": 2048, "type": "String"}
397                ]
398            }
399        }"#;
400
401        let json_file = temp_dir.path().join("complex.json");
402        let template_file = temp_dir.path().join("template.html");
403        let output_file = temp_dir.path().join("output.html");
404
405        fs::write(&json_file, complex_json).expect("Failed to write JSON");
406        fs::write(&template_file, "<html><!-- DATA_INJECTION_POINT --></html>")
407            .expect("Failed to write template");
408
409        let result = embed_json_to_html(
410            json_file.to_str().unwrap(),
411            template_file.to_str().unwrap(),
412            output_file.to_str().unwrap(),
413        );
414
415        assert!(result.is_ok());
416
417        let output_content = fs::read_to_string(&output_file).expect("Failed to read output");
418        assert!(output_content.contains("total_allocations"));
419        assert!(output_content.contains("1000"));
420        assert!(output_content.contains("Vec<i32>"));
421    }
422}