1use crate::error::{OptimError, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::fs;
11use std::io::Write;
12use std::path::{Path, PathBuf};
13use std::time::SystemTime;
14
15use super::config::{
16 ChartStyleConfig, ColorTheme, ReportDistributionConfig, ReportStylingConfig,
17 ReportTemplateConfig, ReportingConfig,
18};
19use super::test_execution::{
20 CiCdTestResult, RegressionAnalysisResult, ResourceUsageReport, TestExecutionStatus,
21 TestSuiteStatistics,
22};
23
24#[derive(Debug, Clone)]
26pub struct ReportGenerator {
27 pub template_engine: TemplateEngine,
29 pub config: ReportingConfig,
31 pub generated_reports: Vec<GeneratedReport>,
33}
34
35#[derive(Debug, Clone)]
37pub struct TemplateEngine {
38 pub templates: HashMap<String, String>,
40 pub variables: HashMap<String, String>,
42 pub functions: HashMap<String, TemplateFunction>,
44}
45
46#[derive(Debug, Clone)]
48pub struct TemplateFunction {
49 pub name: String,
51 pub implementation: String,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct GeneratedReport {
58 pub report_type: ReportType,
60 pub file_path: PathBuf,
62 pub generated_at: SystemTime,
64 pub size_bytes: u64,
66 pub metadata: ReportMetadata,
68 pub summary: ReportSummary,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
74pub enum ReportType {
75 HTML,
77 JSON,
79 JUnit,
81 Markdown,
83 PDF,
85 CSV,
87 Text,
89 Custom(String),
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct ReportMetadata {
96 pub title: String,
98 pub description: Option<String>,
100 pub generator: GeneratorInfo,
102 pub version: String,
104 pub tags: Vec<String>,
106 pub custom_fields: HashMap<String, String>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct GeneratorInfo {
113 pub name: String,
115 pub version: String,
117 pub timestamp: SystemTime,
119 pub config_hash: String,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ReportSummary {
126 pub total_tests: usize,
128 pub passed_tests: usize,
130 pub failed_tests: usize,
132 pub skipped_tests: usize,
134 pub success_rate: f64,
136 pub total_duration_sec: f64,
138 pub regressions_detected: usize,
140 pub key_insights: Vec<String>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct ChartData {
147 pub chart_type: ChartType,
149 pub title: String,
151 pub series: Vec<DataSeries>,
153 pub config: ChartConfig,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
159pub enum ChartType {
160 Line,
162 Bar,
164 Pie,
166 Scatter,
168 Area,
170 Histogram,
172 BoxPlot,
174 Heatmap,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct DataSeries {
181 pub name: String,
183 pub data: Vec<DataPoint>,
185 pub color: Option<String>,
187 pub style: SeriesStyle,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct DataPoint {
194 pub x: DataValue,
196 pub y: DataValue,
198 pub label: Option<String>,
200 pub metadata: HashMap<String, String>,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub enum DataValue {
207 Number(f64),
209 String(String),
211 Timestamp(SystemTime),
213 Boolean(bool),
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct SeriesStyle {
220 pub line_width: Option<u32>,
222 pub point_size: Option<u32>,
224 pub fill_opacity: Option<f64>,
226 pub stroke_style: StrokeStyle,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
232pub enum StrokeStyle {
233 Solid,
235 Dashed,
237 Dotted,
239 Custom(Vec<u32>),
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct ChartConfig {
246 pub width: u32,
248 pub height: u32,
250 pub x_axis: AxisConfig,
252 pub y_axis: AxisConfig,
254 pub legend: LegendConfig,
256 pub grid: GridConfig,
258 pub animation: AnimationConfig,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct AxisConfig {
265 pub title: Option<String>,
267 pub show_labels: bool,
269 pub show_ticks: bool,
271 pub tick_interval: Option<f64>,
273 pub range: Option<(f64, f64)>,
275 pub scale_type: ScaleType,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
281pub enum ScaleType {
282 Linear,
284 Logarithmic,
286 Time,
288 Category,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct LegendConfig {
295 pub show: bool,
297 pub position: LegendPosition,
299 pub orientation: LegendOrientation,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
305pub enum LegendPosition {
306 Top,
308 Bottom,
310 Left,
312 Right,
314 Inside,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
320pub enum LegendOrientation {
321 Horizontal,
323 Vertical,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct GridConfig {
330 pub show: bool,
332 pub show_x: bool,
334 pub show_y: bool,
336 pub color: String,
338 pub opacity: f64,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct AnimationConfig {
345 pub enabled: bool,
347 pub duration_ms: u32,
349 pub easing: EasingFunction,
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
355pub enum EasingFunction {
356 Linear,
358 EaseIn,
360 EaseOut,
362 EaseInOut,
364 Bounce,
366 Elastic,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct PerformanceTrendAnalysis {
373 pub metric_name: String,
375 pub trend_direction: TrendDirection,
377 pub trend_strength: f64,
379 pub statistical_significance: f64,
381 pub data_points: Vec<TrendDataPoint>,
383 pub summary: String,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
389pub enum TrendDirection {
390 Improving,
392 Degrading,
394 Stable,
396 Volatile,
398 Unknown,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct TrendDataPoint {
405 pub timestamp: SystemTime,
407 pub value: f64,
409 pub confidence_interval: Option<(f64, f64)>,
411 pub quality_score: f64,
413}
414
415impl ReportGenerator {
416 pub fn new(config: ReportingConfig) -> Result<Self> {
418 Ok(Self {
419 template_engine: TemplateEngine::new()?,
420 config,
421 generated_reports: Vec::new(),
422 })
423 }
424
425 pub fn generate_reports(
427 &mut self,
428 test_results: &[CiCdTestResult],
429 statistics: &TestSuiteStatistics,
430 output_dir: &Path,
431 ) -> Result<Vec<GeneratedReport>> {
432 let mut reports = Vec::new();
433
434 if self.config.generate_html {
435 let report = self.generate_html_report(test_results, statistics, output_dir)?;
436 reports.push(report);
437 }
438
439 if self.config.generate_json {
440 let report = self.generate_json_report(test_results, statistics, output_dir)?;
441 reports.push(report);
442 }
443
444 if self.config.generate_junit {
445 let report = self.generate_junit_report(test_results, statistics, output_dir)?;
446 reports.push(report);
447 }
448
449 if self.config.generate_markdown {
450 let report = self.generate_markdown_report(test_results, statistics, output_dir)?;
451 reports.push(report);
452 }
453
454 if self.config.generate_pdf {
455 let report = self.generate_pdf_report(test_results, statistics, output_dir)?;
456 reports.push(report);
457 }
458
459 self.generated_reports.extend(reports.clone());
460 Ok(reports)
461 }
462
463 pub fn generate_html_report(
465 &mut self,
466 test_results: &[CiCdTestResult],
467 statistics: &TestSuiteStatistics,
468 output_dir: &Path,
469 ) -> Result<GeneratedReport> {
470 let report_path = output_dir.join("performance_report.html");
471
472 let mut variables = HashMap::new();
474 variables.insert("title".to_string(), "Performance Test Report".to_string());
475 variables.insert(
476 "total_tests".to_string(),
477 statistics.total_tests.to_string(),
478 );
479 variables.insert("passed_tests".to_string(), statistics.passed.to_string());
480 variables.insert("failed_tests".to_string(), statistics.failed.to_string());
481 variables.insert(
482 "success_rate".to_string(),
483 format!("{:.1}%", statistics.success_rate * 100.0),
484 );
485
486 let charts = self.generate_chart_data(test_results, statistics)?;
488
489 let template = self.load_html_template()?;
491 let content = self
492 .template_engine
493 .process_template(&template, &variables)?;
494
495 let styled_content = self.apply_html_styling(&content, &charts)?;
497
498 fs::create_dir_all(output_dir).map_err(|e| {
500 OptimError::InvalidConfig(format!("Failed to create output directory: {}", e))
501 })?;
502
503 fs::write(&report_path, styled_content).map_err(|e| {
504 OptimError::InvalidConfig(format!("Failed to write HTML report: {}", e))
505 })?;
506
507 Ok(GeneratedReport {
508 report_type: ReportType::HTML,
509 file_path: report_path.clone(),
510 generated_at: SystemTime::now(),
511 size_bytes: fs::metadata(&report_path)?.len(),
512 metadata: self.create_report_metadata("Performance Test Report"),
513 summary: self.create_report_summary(statistics),
514 })
515 }
516
517 pub fn generate_json_report(
519 &mut self,
520 test_results: &[CiCdTestResult],
521 statistics: &TestSuiteStatistics,
522 output_dir: &Path,
523 ) -> Result<GeneratedReport> {
524 let report_path = output_dir.join("performance_report.json");
525
526 let report_data = JsonReportData {
527 metadata: self.create_report_metadata("Performance Test Report"),
528 summary: self.create_report_summary(statistics),
529 statistics: statistics.clone(),
530 test_results: test_results.to_vec(),
531 charts: self.generate_chart_data(test_results, statistics)?,
532 trends: self.analyze_performance_trends(test_results)?,
533 generated_at: SystemTime::now(),
534 };
535
536 let json_content = serde_json::to_string_pretty(&report_data).map_err(|e| {
537 OptimError::from(std::io::Error::new(
538 std::io::ErrorKind::InvalidData,
539 format!("Failed to serialize JSON report: {}", e),
540 ))
541 })?;
542
543 fs::create_dir_all(output_dir).map_err(|e| {
544 OptimError::InvalidConfig(format!("Failed to create output directory: {}", e))
545 })?;
546
547 fs::write(&report_path, json_content).map_err(OptimError::IO)?;
548
549 Ok(GeneratedReport {
550 report_type: ReportType::JSON,
551 file_path: report_path.clone(),
552 generated_at: SystemTime::now(),
553 size_bytes: fs::metadata(&report_path)?.len(),
554 metadata: self.create_report_metadata("Performance Test Report"),
555 summary: self.create_report_summary(statistics),
556 })
557 }
558
559 pub fn generate_junit_report(
561 &mut self,
562 test_results: &[CiCdTestResult],
563 statistics: &TestSuiteStatistics,
564 output_dir: &Path,
565 ) -> Result<GeneratedReport> {
566 let report_path = output_dir.join("junit_report.xml");
567
568 let xml_content = self.create_junit_xml(test_results, statistics)?;
569
570 fs::create_dir_all(output_dir).map_err(|e| {
571 OptimError::InvalidConfig(format!("Failed to create output directory: {}", e))
572 })?;
573
574 fs::write(&report_path, xml_content).map_err(OptimError::IO)?;
575
576 Ok(GeneratedReport {
577 report_type: ReportType::JUnit,
578 file_path: report_path.clone(),
579 generated_at: SystemTime::now(),
580 size_bytes: fs::metadata(&report_path)?.len(),
581 metadata: self.create_report_metadata("JUnit Test Report"),
582 summary: self.create_report_summary(statistics),
583 })
584 }
585
586 pub fn generate_markdown_report(
588 &mut self,
589 test_results: &[CiCdTestResult],
590 statistics: &TestSuiteStatistics,
591 output_dir: &Path,
592 ) -> Result<GeneratedReport> {
593 let report_path = output_dir.join("performance_report.md");
594
595 let markdown_content = self.create_markdown_content(test_results, statistics)?;
596
597 fs::create_dir_all(output_dir).map_err(|e| {
598 OptimError::InvalidConfig(format!("Failed to create output directory: {}", e))
599 })?;
600
601 fs::write(&report_path, markdown_content).map_err(OptimError::IO)?;
602
603 Ok(GeneratedReport {
604 report_type: ReportType::Markdown,
605 file_path: report_path.clone(),
606 generated_at: SystemTime::now(),
607 size_bytes: fs::metadata(&report_path)?.len(),
608 metadata: self.create_report_metadata("Performance Test Report"),
609 summary: self.create_report_summary(statistics),
610 })
611 }
612
613 pub fn generate_pdf_report(
615 &mut self,
616 test_results: &[CiCdTestResult],
617 statistics: &TestSuiteStatistics,
618 output_dir: &Path,
619 ) -> Result<GeneratedReport> {
620 let report_path = output_dir.join("performance_report.pdf");
621
622 let html_content = self.create_pdf_html_content(test_results, statistics)?;
624
625 fs::create_dir_all(output_dir).map_err(|e| {
628 OptimError::InvalidConfig(format!("Failed to create output directory: {}", e))
629 })?;
630
631 fs::write(
632 &report_path,
633 format!("<!-- PDF Report Content -->\n{}", html_content),
634 )
635 .map_err(OptimError::IO)?;
636
637 Ok(GeneratedReport {
638 report_type: ReportType::PDF,
639 file_path: report_path.clone(),
640 generated_at: SystemTime::now(),
641 size_bytes: fs::metadata(&report_path)?.len(),
642 metadata: self.create_report_metadata("Performance Test Report"),
643 summary: self.create_report_summary(statistics),
644 })
645 }
646
647 fn generate_chart_data(
649 &self,
650 test_results: &[CiCdTestResult],
651 _statistics: &TestSuiteStatistics,
652 ) -> Result<Vec<ChartData>> {
653 let mut charts = Vec::new();
654
655 let status_chart = self.create_test_status_chart(test_results)?;
657 charts.push(status_chart);
658
659 let timeline_chart = self.create_performance_timeline_chart(test_results)?;
661 charts.push(timeline_chart);
662
663 let resource_chart = self.create_resource_usage_chart(test_results)?;
665 charts.push(resource_chart);
666
667 Ok(charts)
668 }
669
670 fn create_test_status_chart(&self, test_results: &[CiCdTestResult]) -> Result<ChartData> {
672 let mut status_counts = HashMap::new();
673 for result in test_results {
674 *status_counts.entry(result.status.clone()).or_insert(0) += 1;
675 }
676
677 let mut series = Vec::new();
678 for (status, count) in status_counts {
679 let color = match status {
680 TestExecutionStatus::Passed => "#28a745".to_string(),
681 TestExecutionStatus::Failed => "#dc3545".to_string(),
682 TestExecutionStatus::Skipped => "#ffc107".to_string(),
683 TestExecutionStatus::Error => "#e83e8c".to_string(),
684 _ => "#6c757d".to_string(),
685 };
686
687 series.push(DataSeries {
688 name: format!("{:?}", status),
689 data: vec![DataPoint {
690 x: DataValue::String(format!("{:?}", status)),
691 y: DataValue::Number(count as f64),
692 label: Some(format!("{:?}: {}", status, count)),
693 metadata: HashMap::new(),
694 }],
695 color: Some(color),
696 style: SeriesStyle::default(),
697 });
698 }
699
700 Ok(ChartData {
701 chart_type: ChartType::Pie,
702 title: "Test Results Distribution".to_string(),
703 series,
704 config: ChartConfig::default(),
705 })
706 }
707
708 fn create_performance_timeline_chart(
710 &self,
711 test_results: &[CiCdTestResult],
712 ) -> Result<ChartData> {
713 let mut data_points = Vec::new();
714
715 for result in test_results {
716 if let Some(duration) = result.duration {
717 data_points.push(DataPoint {
718 x: DataValue::Timestamp(result.start_time),
719 y: DataValue::Number(duration.as_secs_f64()),
720 label: Some(result.test_name.clone()),
721 metadata: HashMap::new(),
722 });
723 }
724 }
725
726 let series = vec![DataSeries {
727 name: "Test Execution Time".to_string(),
728 data: data_points,
729 color: Some("#007bff".to_string()),
730 style: SeriesStyle::default(),
731 }];
732
733 Ok(ChartData {
734 chart_type: ChartType::Line,
735 title: "Test Execution Timeline".to_string(),
736 series,
737 config: ChartConfig::default(),
738 })
739 }
740
741 fn create_resource_usage_chart(&self, test_results: &[CiCdTestResult]) -> Result<ChartData> {
743 let mut memory_points = Vec::new();
744 let mut cpu_points = Vec::new();
745
746 for result in test_results {
747 memory_points.push(DataPoint {
748 x: DataValue::String(result.test_name.clone()),
749 y: DataValue::Number(result.resource_usage.peak_memory_mb),
750 label: Some(format!(
751 "{}: {:.1} MB",
752 result.test_name, result.resource_usage.peak_memory_mb
753 )),
754 metadata: HashMap::new(),
755 });
756
757 cpu_points.push(DataPoint {
758 x: DataValue::String(result.test_name.clone()),
759 y: DataValue::Number(result.resource_usage.peak_cpu_percent),
760 label: Some(format!(
761 "{}: {:.1}%",
762 result.test_name, result.resource_usage.peak_cpu_percent
763 )),
764 metadata: HashMap::new(),
765 });
766 }
767
768 let series = vec![
769 DataSeries {
770 name: "Peak Memory (MB)".to_string(),
771 data: memory_points,
772 color: Some("#28a745".to_string()),
773 style: SeriesStyle::default(),
774 },
775 DataSeries {
776 name: "Peak CPU (%)".to_string(),
777 data: cpu_points,
778 color: Some("#dc3545".to_string()),
779 style: SeriesStyle::default(),
780 },
781 ];
782
783 Ok(ChartData {
784 chart_type: ChartType::Bar,
785 title: "Resource Usage by Test".to_string(),
786 series,
787 config: ChartConfig::default(),
788 })
789 }
790
791 fn analyze_performance_trends(
793 &self,
794 test_results: &[CiCdTestResult],
795 ) -> Result<Vec<PerformanceTrendAnalysis>> {
796 let mut trends = Vec::new();
797
798 let execution_time_trend = self.analyze_execution_time_trend(test_results)?;
800 trends.push(execution_time_trend);
801
802 let memory_trend = self.analyze_memory_usage_trend(test_results)?;
804 trends.push(memory_trend);
805
806 Ok(trends)
807 }
808
809 fn analyze_execution_time_trend(
811 &self,
812 test_results: &[CiCdTestResult],
813 ) -> Result<PerformanceTrendAnalysis> {
814 let mut data_points = Vec::new();
815
816 for result in test_results {
817 if let Some(duration) = result.duration {
818 data_points.push(TrendDataPoint {
819 timestamp: result.start_time,
820 value: duration.as_secs_f64(),
821 confidence_interval: None,
822 quality_score: 1.0, });
824 }
825 }
826
827 let trend_direction = if data_points.len() >= 2 {
829 let first_half_avg = data_points
830 .iter()
831 .take(data_points.len() / 2)
832 .map(|p| p.value)
833 .sum::<f64>()
834 / (data_points.len() / 2) as f64;
835 let second_half_avg = data_points
836 .iter()
837 .skip(data_points.len() / 2)
838 .map(|p| p.value)
839 .sum::<f64>()
840 / (data_points.len() - data_points.len() / 2) as f64;
841
842 if second_half_avg > first_half_avg * 1.1 {
843 TrendDirection::Degrading
844 } else if second_half_avg < first_half_avg * 0.9 {
845 TrendDirection::Improving
846 } else {
847 TrendDirection::Stable
848 }
849 } else {
850 TrendDirection::Unknown
851 };
852
853 Ok(PerformanceTrendAnalysis {
854 metric_name: "Execution Time".to_string(),
855 trend_direction,
856 trend_strength: 0.7, statistical_significance: 0.95, data_points,
859 summary: "Execution time trend analysis based on recent test runs".to_string(),
860 })
861 }
862
863 fn analyze_memory_usage_trend(
865 &self,
866 test_results: &[CiCdTestResult],
867 ) -> Result<PerformanceTrendAnalysis> {
868 let mut data_points = Vec::new();
869
870 for result in test_results {
871 data_points.push(TrendDataPoint {
872 timestamp: result.start_time,
873 value: result.resource_usage.peak_memory_mb,
874 confidence_interval: None,
875 quality_score: 1.0, });
877 }
878
879 Ok(PerformanceTrendAnalysis {
880 metric_name: "Memory Usage".to_string(),
881 trend_direction: TrendDirection::Stable, trend_strength: 0.5,
883 statistical_significance: 0.85,
884 data_points,
885 summary: "Memory usage trend analysis based on recent test runs".to_string(),
886 })
887 }
888
889 fn load_html_template(&self) -> Result<String> {
891 if let Some(template_path) = &self.config.templates.html_template_path {
892 fs::read_to_string(template_path).map_err(OptimError::IO)
893 } else {
894 Ok(self.get_default_html_template())
895 }
896 }
897
898 fn get_default_html_template(&self) -> String {
900 r#"<!DOCTYPE html>
901<html lang="en">
902<head>
903 <meta charset="UTF-8">
904 <meta name="viewport" content="width=device-width, initial-scale=1.0">
905 <title>{{title}}</title>
906 <style>
907 body { font-family: Arial, sans-serif; margin: 40px; }
908 .header { background: #f8f9fa; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
909 .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
910 .metric { background: white; border: 1px solid #dee2e6; padding: 15px; border-radius: 5px; text-align: center; }
911 .metric h3 { margin: 0 0 10px 0; color: #495057; }
912 .metric .value { font-size: 2em; font-weight: bold; }
913 .passed { color: #28a745; }
914 .failed { color: #dc3545; }
915 .charts { margin: 20px 0; }
916 .chart { margin: 20px 0; padding: 20px; border: 1px solid #dee2e6; border-radius: 5px; }
917 </style>
918</head>
919<body>
920 <div class="header">
921 <h1>{{title}}</h1>
922 <p>Generated on {{timestamp}}</p>
923 </div>
924
925 <div class="summary">
926 <div class="metric">
927 <h3>Total Tests</h3>
928 <div class="value">{{total_tests}}</div>
929 </div>
930 <div class="metric">
931 <h3>Passed</h3>
932 <div class="value passed">{{passed_tests}}</div>
933 </div>
934 <div class="metric">
935 <h3>Failed</h3>
936 <div class="value failed">{{failed_tests}}</div>
937 </div>
938 <div class="metric">
939 <h3>Success Rate</h3>
940 <div class="value">{{success_rate}}</div>
941 </div>
942 </div>
943
944 <div class="charts">
945 {{charts}}
946 </div>
947</body>
948</html>"#.to_string()
949 }
950
951 fn apply_html_styling(&self, content: &str, charts: &[ChartData]) -> Result<String> {
953 let mut styled_content = content.to_string();
954
955 if let Ok(timestamp) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
957 styled_content =
958 styled_content.replace("{{timestamp}}", &format!("{}", timestamp.as_secs()));
959 }
960
961 let charts_html = self.generate_charts_html(charts)?;
963 styled_content = styled_content.replace("{{charts}}", &charts_html);
964
965 Ok(styled_content)
966 }
967
968 fn generate_charts_html(&self, charts: &[ChartData]) -> Result<String> {
970 let mut html = String::new();
971
972 for chart in charts {
973 html.push_str(&format!(
974 r#"<div class="chart">
975 <h3>{}</h3>
976 <p>Chart Type: {:?}</p>
977 <p>Series Count: {}</p>
978 </div>"#,
979 chart.title,
980 chart.chart_type,
981 chart.series.len()
982 ));
983 }
984
985 Ok(html)
986 }
987
988 fn create_junit_xml(
990 &self,
991 test_results: &[CiCdTestResult],
992 statistics: &TestSuiteStatistics,
993 ) -> Result<String> {
994 let mut xml = String::new();
995 xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
996 xml.push_str(&format!(
997 r#"<testsuite name="PerformanceTests" tests="{}" failures="{}" errors="{}" time="{}">"#,
998 statistics.total_tests,
999 statistics.failed,
1000 statistics.errors,
1001 statistics.total_duration.as_secs_f64()
1002 ));
1003 xml.push('\n');
1004
1005 for result in test_results {
1006 xml.push_str(&format!(
1007 r#" <testcase name="{}" time="{}">"#,
1008 result.test_name,
1009 result.duration.map_or(0.0, |d| d.as_secs_f64())
1010 ));
1011
1012 match result.status {
1013 TestExecutionStatus::Failed => {
1014 xml.push_str(&format!(
1015 r#"<failure message="{}">{}</failure>"#,
1016 result.error_message.as_deref().unwrap_or("Test failed"),
1017 result.output
1018 ));
1019 }
1020 TestExecutionStatus::Error => {
1021 xml.push_str(&format!(
1022 r#"<error message="{}">{}</error>"#,
1023 result.error_message.as_deref().unwrap_or("Test error"),
1024 result.output
1025 ));
1026 }
1027 TestExecutionStatus::Skipped => {
1028 xml.push_str("<skipped/>");
1029 }
1030 _ => {}
1031 }
1032
1033 xml.push_str("</testcase>\n");
1034 }
1035
1036 xml.push_str("</testsuite>\n");
1037 Ok(xml)
1038 }
1039
1040 fn create_markdown_content(
1042 &self,
1043 test_results: &[CiCdTestResult],
1044 statistics: &TestSuiteStatistics,
1045 ) -> Result<String> {
1046 let mut markdown = String::new();
1047
1048 markdown.push_str("# Performance Test Report\n\n");
1049 markdown.push_str(&format!(
1050 "Generated on: {}\n\n",
1051 chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
1052 ));
1053
1054 markdown.push_str("## Summary\n\n");
1055 markdown.push_str(&format!("- **Total Tests**: {}\n", statistics.total_tests));
1056 markdown.push_str(&format!("- **Passed**: {}\n", statistics.passed));
1057 markdown.push_str(&format!("- **Failed**: {}\n", statistics.failed));
1058 markdown.push_str(&format!("- **Skipped**: {}\n", statistics.skipped));
1059 markdown.push_str(&format!(
1060 "- **Success Rate**: {:.1}%\n",
1061 statistics.success_rate * 100.0
1062 ));
1063 markdown.push_str(&format!(
1064 "- **Total Duration**: {:.2}s\n\n",
1065 statistics.total_duration.as_secs_f64()
1066 ));
1067
1068 markdown.push_str("## Test Results\n\n");
1069 markdown.push_str("| Test Name | Status | Duration | Memory (MB) | CPU (%) |\n");
1070 markdown.push_str("|-----------|--------|----------|-------------|----------|\n");
1071
1072 for result in test_results {
1073 markdown.push_str(&format!(
1074 "| {} | {:?} | {:.2}s | {:.1} | {:.1} |\n",
1075 result.test_name,
1076 result.status,
1077 result.duration.map_or(0.0, |d| d.as_secs_f64()),
1078 result.resource_usage.peak_memory_mb,
1079 result.resource_usage.peak_cpu_percent
1080 ));
1081 }
1082
1083 Ok(markdown)
1084 }
1085
1086 fn create_pdf_html_content(
1088 &self,
1089 test_results: &[CiCdTestResult],
1090 statistics: &TestSuiteStatistics,
1091 ) -> Result<String> {
1092 let mut html = String::new();
1094 html.push_str("<!DOCTYPE html><html><head><title>Performance Report</title></head><body>");
1095 html.push_str("<h1>Performance Test Report</h1>");
1096 html.push_str(&format!("<p>Total Tests: {}</p>", statistics.total_tests));
1097 html.push_str(&format!(
1098 "<p>Success Rate: {:.1}%</p>",
1099 statistics.success_rate * 100.0
1100 ));
1101 html.push_str("</body></html>");
1102 Ok(html)
1103 }
1104
1105 fn create_report_metadata(&self, title: &str) -> ReportMetadata {
1107 ReportMetadata {
1108 title: title.to_string(),
1109 description: Some("Automated performance test report".to_string()),
1110 generator: GeneratorInfo {
1111 name: "CI/CD Automation".to_string(),
1112 version: "1.0.0".to_string(),
1113 timestamp: SystemTime::now(),
1114 config_hash: "abc123".to_string(), },
1116 version: "1.0".to_string(),
1117 tags: vec!["performance".to_string(), "ci-cd".to_string()],
1118 custom_fields: HashMap::new(),
1119 }
1120 }
1121
1122 fn create_report_summary(&self, statistics: &TestSuiteStatistics) -> ReportSummary {
1124 ReportSummary {
1125 total_tests: statistics.total_tests,
1126 passed_tests: statistics.passed,
1127 failed_tests: statistics.failed,
1128 skipped_tests: statistics.skipped,
1129 success_rate: statistics.success_rate * 100.0,
1130 total_duration_sec: statistics.total_duration.as_secs_f64(),
1131 regressions_detected: 0, key_insights: vec![
1133 "No significant performance regressions detected".to_string(),
1134 "Memory usage within expected ranges".to_string(),
1135 ],
1136 }
1137 }
1138}
1139
1140impl TemplateEngine {
1141 pub fn new() -> Result<Self> {
1143 Ok(Self {
1144 templates: HashMap::new(),
1145 variables: HashMap::new(),
1146 functions: HashMap::new(),
1147 })
1148 }
1149
1150 pub fn process_template(
1152 &self,
1153 template: &str,
1154 variables: &HashMap<String, String>,
1155 ) -> Result<String> {
1156 let mut result = template.to_string();
1157
1158 for (key, value) in variables {
1160 let placeholder = format!("{{{{{}}}}}", key);
1161 result = result.replace(&placeholder, value);
1162 }
1163
1164 Ok(result)
1165 }
1166
1167 pub fn load_template(&mut self, name: &str, path: &Path) -> Result<()> {
1169 let content = fs::read_to_string(path).map_err(OptimError::IO)?;
1170 self.templates.insert(name.to_string(), content);
1171 Ok(())
1172 }
1173
1174 pub fn set_variable(&mut self, name: String, value: String) {
1176 self.variables.insert(name, value);
1177 }
1178}
1179
1180#[derive(Debug, Clone, Serialize, Deserialize)]
1182pub struct JsonReportData {
1183 pub metadata: ReportMetadata,
1185 pub summary: ReportSummary,
1187 pub statistics: TestSuiteStatistics,
1189 pub test_results: Vec<CiCdTestResult>,
1191 pub charts: Vec<ChartData>,
1193 pub trends: Vec<PerformanceTrendAnalysis>,
1195 pub generated_at: SystemTime,
1197}
1198
1199impl Default for SeriesStyle {
1202 fn default() -> Self {
1203 Self {
1204 line_width: Some(2),
1205 point_size: Some(4),
1206 fill_opacity: Some(0.7),
1207 stroke_style: StrokeStyle::Solid,
1208 }
1209 }
1210}
1211
1212impl Default for ChartConfig {
1213 fn default() -> Self {
1214 Self {
1215 width: 800,
1216 height: 600,
1217 x_axis: AxisConfig::default(),
1218 y_axis: AxisConfig::default(),
1219 legend: LegendConfig::default(),
1220 grid: GridConfig::default(),
1221 animation: AnimationConfig::default(),
1222 }
1223 }
1224}
1225
1226impl Default for AxisConfig {
1227 fn default() -> Self {
1228 Self {
1229 title: None,
1230 show_labels: true,
1231 show_ticks: true,
1232 tick_interval: None,
1233 range: None,
1234 scale_type: ScaleType::Linear,
1235 }
1236 }
1237}
1238
1239impl Default for LegendConfig {
1240 fn default() -> Self {
1241 Self {
1242 show: true,
1243 position: LegendPosition::Right,
1244 orientation: LegendOrientation::Vertical,
1245 }
1246 }
1247}
1248
1249impl Default for GridConfig {
1250 fn default() -> Self {
1251 Self {
1252 show: true,
1253 show_x: true,
1254 show_y: true,
1255 color: "#e0e0e0".to_string(),
1256 opacity: 0.5,
1257 }
1258 }
1259}
1260
1261impl Default for AnimationConfig {
1262 fn default() -> Self {
1263 Self {
1264 enabled: true,
1265 duration_ms: 1000,
1266 easing: EasingFunction::EaseInOut,
1267 }
1268 }
1269}
1270
1271#[cfg(test)]
1272mod tests {
1273 use super::*;
1274 use crate::ci_cd_automation::test_execution::TestSuiteStatistics;
1275 use std::time::Duration;
1276
1277 #[test]
1278 fn test_report_generator_creation() {
1279 let config = ReportingConfig::default();
1280 let generator = ReportGenerator::new(config);
1281 assert!(generator.is_ok());
1282 }
1283
1284 #[test]
1285 fn test_template_engine() {
1286 let engine = TemplateEngine::new().expect("unwrap failed");
1287 let template = "Hello {{name}}!";
1288 let mut variables = HashMap::new();
1289 variables.insert("name".to_string(), "World".to_string());
1290
1291 let result = engine
1292 .process_template(template, &variables)
1293 .expect("unwrap failed");
1294 assert_eq!(result, "Hello World!");
1295 }
1296
1297 #[test]
1298 fn test_chart_data_creation() {
1299 let chart = ChartData {
1300 chart_type: ChartType::Line,
1301 title: "Test Chart".to_string(),
1302 series: Vec::new(),
1303 config: ChartConfig::default(),
1304 };
1305
1306 assert_eq!(chart.chart_type, ChartType::Line);
1307 assert_eq!(chart.title, "Test Chart");
1308 }
1309
1310 #[test]
1311 fn test_report_metadata() {
1312 let metadata = ReportMetadata {
1313 title: "Test Report".to_string(),
1314 description: None,
1315 generator: GeneratorInfo {
1316 name: "Test Generator".to_string(),
1317 version: "1.0.0".to_string(),
1318 timestamp: SystemTime::now(),
1319 config_hash: "test".to_string(),
1320 },
1321 version: "1.0".to_string(),
1322 tags: vec!["test".to_string()],
1323 custom_fields: HashMap::new(),
1324 };
1325
1326 assert_eq!(metadata.title, "Test Report");
1327 assert_eq!(metadata.version, "1.0");
1328 }
1329
1330 #[test]
1331 fn test_trend_analysis() {
1332 let trend = PerformanceTrendAnalysis {
1333 metric_name: "Test Metric".to_string(),
1334 trend_direction: TrendDirection::Improving,
1335 trend_strength: 0.8,
1336 statistical_significance: 0.95,
1337 data_points: Vec::new(),
1338 summary: "Test trend".to_string(),
1339 };
1340
1341 assert_eq!(trend.trend_direction, TrendDirection::Improving);
1342 assert_eq!(trend.trend_strength, 0.8);
1343 }
1344}