Skip to main content

optirs_bench/ci_cd_automation/
reporting.rs

1// Report Generation and Formatting
2//
3// This module provides comprehensive report generation capabilities for CI/CD automation,
4// including HTML, JSON, Markdown, PDF, and JUnit XML report formats with templating,
5// styling, and distribution support.
6
7use 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/// Report generator for CI/CD automation results
25#[derive(Debug, Clone)]
26pub struct ReportGenerator {
27    /// Template engine for report generation
28    pub template_engine: TemplateEngine,
29    /// Reporting configuration
30    pub config: ReportingConfig,
31    /// Generated reports cache
32    pub generated_reports: Vec<GeneratedReport>,
33}
34
35/// Template engine for processing report templates
36#[derive(Debug, Clone)]
37pub struct TemplateEngine {
38    /// Loaded templates cache
39    pub templates: HashMap<String, String>,
40    /// Template variables
41    pub variables: HashMap<String, String>,
42    /// Template functions
43    pub functions: HashMap<String, TemplateFunction>,
44}
45
46/// Template function definition
47#[derive(Debug, Clone)]
48pub struct TemplateFunction {
49    /// Function name
50    pub name: String,
51    /// Function implementation (simplified)
52    pub implementation: String,
53}
54
55/// Generated report information
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct GeneratedReport {
58    /// Report type
59    pub report_type: ReportType,
60    /// File path of generated report
61    pub file_path: PathBuf,
62    /// Report generation timestamp
63    pub generated_at: SystemTime,
64    /// Report size in bytes
65    pub size_bytes: u64,
66    /// Report metadata
67    pub metadata: ReportMetadata,
68    /// Report summary
69    pub summary: ReportSummary,
70}
71
72/// Report types supported by the generator
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
74pub enum ReportType {
75    /// HTML report with interactive elements
76    HTML,
77    /// JSON report for programmatic consumption
78    JSON,
79    /// JUnit XML report for CI/CD integration
80    JUnit,
81    /// Markdown report for documentation
82    Markdown,
83    /// PDF report for formal documentation
84    PDF,
85    /// CSV report for data analysis
86    CSV,
87    /// Text report for simple consumption
88    Text,
89    /// Custom report format
90    Custom(String),
91}
92
93/// Report metadata
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct ReportMetadata {
96    /// Report title
97    pub title: String,
98    /// Report description
99    pub description: Option<String>,
100    /// Generation tool information
101    pub generator: GeneratorInfo,
102    /// Report version
103    pub version: String,
104    /// Tags for categorization
105    pub tags: Vec<String>,
106    /// Custom metadata fields
107    pub custom_fields: HashMap<String, String>,
108}
109
110/// Report generator tool information
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct GeneratorInfo {
113    /// Tool name
114    pub name: String,
115    /// Tool version
116    pub version: String,
117    /// Generation timestamp
118    pub timestamp: SystemTime,
119    /// Generator configuration
120    pub config_hash: String,
121}
122
123/// Report summary information
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ReportSummary {
126    /// Total number of tests
127    pub total_tests: usize,
128    /// Number of passed tests
129    pub passed_tests: usize,
130    /// Number of failed tests
131    pub failed_tests: usize,
132    /// Number of skipped tests
133    pub skipped_tests: usize,
134    /// Success rate percentage
135    pub success_rate: f64,
136    /// Total execution time
137    pub total_duration_sec: f64,
138    /// Performance regressions detected
139    pub regressions_detected: usize,
140    /// Key insights
141    pub key_insights: Vec<String>,
142}
143
144/// Chart data for visualization
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct ChartData {
147    /// Chart type
148    pub chart_type: ChartType,
149    /// Chart title
150    pub title: String,
151    /// Data series
152    pub series: Vec<DataSeries>,
153    /// Chart configuration
154    pub config: ChartConfig,
155}
156
157/// Chart types for visualization
158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
159pub enum ChartType {
160    /// Line chart
161    Line,
162    /// Bar chart
163    Bar,
164    /// Pie chart
165    Pie,
166    /// Scatter plot
167    Scatter,
168    /// Area chart
169    Area,
170    /// Histogram
171    Histogram,
172    /// Box plot
173    BoxPlot,
174    /// Heatmap
175    Heatmap,
176}
177
178/// Data series for charts
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct DataSeries {
181    /// Series name
182    pub name: String,
183    /// Data points
184    pub data: Vec<DataPoint>,
185    /// Series color
186    pub color: Option<String>,
187    /// Series style
188    pub style: SeriesStyle,
189}
190
191/// Individual data point
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct DataPoint {
194    /// X-axis value
195    pub x: DataValue,
196    /// Y-axis value
197    pub y: DataValue,
198    /// Optional label
199    pub label: Option<String>,
200    /// Optional metadata
201    pub metadata: HashMap<String, String>,
202}
203
204/// Data value types
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub enum DataValue {
207    /// Numeric value
208    Number(f64),
209    /// String value
210    String(String),
211    /// Timestamp value
212    Timestamp(SystemTime),
213    /// Boolean value
214    Boolean(bool),
215}
216
217/// Series styling options
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct SeriesStyle {
220    /// Line width for line charts
221    pub line_width: Option<u32>,
222    /// Point size for scatter plots
223    pub point_size: Option<u32>,
224    /// Fill opacity (0.0 to 1.0)
225    pub fill_opacity: Option<f64>,
226    /// Stroke style
227    pub stroke_style: StrokeStyle,
228}
229
230/// Stroke styles for charts
231#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
232pub enum StrokeStyle {
233    /// Solid line
234    Solid,
235    /// Dashed line
236    Dashed,
237    /// Dotted line
238    Dotted,
239    /// Custom pattern
240    Custom(Vec<u32>),
241}
242
243/// Chart configuration
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct ChartConfig {
246    /// Chart width in pixels
247    pub width: u32,
248    /// Chart height in pixels
249    pub height: u32,
250    /// X-axis configuration
251    pub x_axis: AxisConfig,
252    /// Y-axis configuration
253    pub y_axis: AxisConfig,
254    /// Legend configuration
255    pub legend: LegendConfig,
256    /// Grid configuration
257    pub grid: GridConfig,
258    /// Animation settings
259    pub animation: AnimationConfig,
260}
261
262/// Axis configuration
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct AxisConfig {
265    /// Axis title
266    pub title: Option<String>,
267    /// Show axis labels
268    pub show_labels: bool,
269    /// Show axis ticks
270    pub show_ticks: bool,
271    /// Tick interval
272    pub tick_interval: Option<f64>,
273    /// Axis range
274    pub range: Option<(f64, f64)>,
275    /// Axis scale type
276    pub scale_type: ScaleType,
277}
278
279/// Scale types for axes
280#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
281pub enum ScaleType {
282    /// Linear scale
283    Linear,
284    /// Logarithmic scale
285    Logarithmic,
286    /// Time scale
287    Time,
288    /// Category scale
289    Category,
290}
291
292/// Legend configuration
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct LegendConfig {
295    /// Show legend
296    pub show: bool,
297    /// Legend position
298    pub position: LegendPosition,
299    /// Legend orientation
300    pub orientation: LegendOrientation,
301}
302
303/// Legend positions
304#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
305pub enum LegendPosition {
306    /// Top of chart
307    Top,
308    /// Bottom of chart
309    Bottom,
310    /// Left of chart
311    Left,
312    /// Right of chart
313    Right,
314    /// Inside chart area
315    Inside,
316}
317
318/// Legend orientations
319#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
320pub enum LegendOrientation {
321    /// Horizontal legend
322    Horizontal,
323    /// Vertical legend
324    Vertical,
325}
326
327/// Grid configuration
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct GridConfig {
330    /// Show grid
331    pub show: bool,
332    /// X-axis grid lines
333    pub show_x: bool,
334    /// Y-axis grid lines
335    pub show_y: bool,
336    /// Grid color
337    pub color: String,
338    /// Grid opacity (0.0 to 1.0)
339    pub opacity: f64,
340}
341
342/// Animation configuration
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct AnimationConfig {
345    /// Enable animations
346    pub enabled: bool,
347    /// Animation duration in milliseconds
348    pub duration_ms: u32,
349    /// Animation easing function
350    pub easing: EasingFunction,
351}
352
353/// Animation easing functions
354#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
355pub enum EasingFunction {
356    /// Linear easing
357    Linear,
358    /// Ease in
359    EaseIn,
360    /// Ease out
361    EaseOut,
362    /// Ease in-out
363    EaseInOut,
364    /// Bounce
365    Bounce,
366    /// Elastic
367    Elastic,
368}
369
370/// Performance trend analysis for reports
371#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct PerformanceTrendAnalysis {
373    /// Metric name
374    pub metric_name: String,
375    /// Trend direction
376    pub trend_direction: TrendDirection,
377    /// Trend strength (0.0 to 1.0)
378    pub trend_strength: f64,
379    /// Statistical significance
380    pub statistical_significance: f64,
381    /// Data points analyzed
382    pub data_points: Vec<TrendDataPoint>,
383    /// Trend summary
384    pub summary: String,
385}
386
387/// Trend directions
388#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
389pub enum TrendDirection {
390    /// Improving trend
391    Improving,
392    /// Degrading trend
393    Degrading,
394    /// Stable trend
395    Stable,
396    /// Volatile trend
397    Volatile,
398    /// Insufficient data
399    Unknown,
400}
401
402/// Trend data point
403#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct TrendDataPoint {
405    /// Timestamp
406    pub timestamp: SystemTime,
407    /// Metric value
408    pub value: f64,
409    /// Confidence interval
410    pub confidence_interval: Option<(f64, f64)>,
411    /// Data quality score
412    pub quality_score: f64,
413}
414
415impl ReportGenerator {
416    /// Create a new report generator
417    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    /// Generate all configured report types
426    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    /// Generate HTML report
464    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        // Prepare template variables
473        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        // Generate chart data
487        let charts = self.generate_chart_data(test_results, statistics)?;
488
489        // Load and process template
490        let template = self.load_html_template()?;
491        let content = self
492            .template_engine
493            .process_template(&template, &variables)?;
494
495        // Include charts and styling
496        let styled_content = self.apply_html_styling(&content, &charts)?;
497
498        // Write to file
499        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    /// Generate JSON report
518    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    /// Generate JUnit XML report
560    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    /// Generate Markdown report
587    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    /// Generate PDF report (simplified implementation)
614    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        // For now, generate PDF by creating HTML and indicating it should be converted
623        let html_content = self.create_pdf_html_content(test_results, statistics)?;
624
625        // In a real implementation, this would use a PDF generation library
626        // For now, just write the HTML content with a .pdf extension as a placeholder
627        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    /// Generate chart data for visualizations
648    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        // Test results pie chart
656        let status_chart = self.create_test_status_chart(test_results)?;
657        charts.push(status_chart);
658
659        // Performance timeline chart
660        let timeline_chart = self.create_performance_timeline_chart(test_results)?;
661        charts.push(timeline_chart);
662
663        // Resource usage chart
664        let resource_chart = self.create_resource_usage_chart(test_results)?;
665        charts.push(resource_chart);
666
667        Ok(charts)
668    }
669
670    /// Create test status pie chart
671    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    /// Create performance timeline chart
709    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    /// Create resource usage chart
742    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    /// Analyze performance trends
792    fn analyze_performance_trends(
793        &self,
794        test_results: &[CiCdTestResult],
795    ) -> Result<Vec<PerformanceTrendAnalysis>> {
796        let mut trends = Vec::new();
797
798        // Execution time trend
799        let execution_time_trend = self.analyze_execution_time_trend(test_results)?;
800        trends.push(execution_time_trend);
801
802        // Memory usage trend
803        let memory_trend = self.analyze_memory_usage_trend(test_results)?;
804        trends.push(memory_trend);
805
806        Ok(trends)
807    }
808
809    /// Analyze execution time trend
810    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, // Simplified
823                });
824            }
825        }
826
827        // Simple trend analysis
828        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,            // Simplified
857            statistical_significance: 0.95, // Simplified
858            data_points,
859            summary: "Execution time trend analysis based on recent test runs".to_string(),
860        })
861    }
862
863    /// Analyze memory usage trend
864    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, // Simplified
876            });
877        }
878
879        Ok(PerformanceTrendAnalysis {
880            metric_name: "Memory Usage".to_string(),
881            trend_direction: TrendDirection::Stable, // Simplified
882            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    /// Load HTML template
890    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    /// Get default HTML template
899    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    /// Apply HTML styling and include charts
952    fn apply_html_styling(&self, content: &str, charts: &[ChartData]) -> Result<String> {
953        let mut styled_content = content.to_string();
954
955        // Add timestamp
956        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        // Add charts
962        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    /// Generate HTML for charts
969    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    /// Create JUnit XML content
989    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    /// Create Markdown content
1041    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    /// Create PDF HTML content
1087    fn create_pdf_html_content(
1088        &self,
1089        test_results: &[CiCdTestResult],
1090        statistics: &TestSuiteStatistics,
1091    ) -> Result<String> {
1092        // Create a PDF-optimized HTML version
1093        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    /// Create report metadata
1106    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(), // Simplified
1115            },
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    /// Create report summary
1123    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, // Simplified
1132            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    /// Create a new template engine
1142    pub fn new() -> Result<Self> {
1143        Ok(Self {
1144            templates: HashMap::new(),
1145            variables: HashMap::new(),
1146            functions: HashMap::new(),
1147        })
1148    }
1149
1150    /// Process a template with variables
1151    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        // Simple variable substitution
1159        for (key, value) in variables {
1160            let placeholder = format!("{{{{{}}}}}", key);
1161            result = result.replace(&placeholder, value);
1162        }
1163
1164        Ok(result)
1165    }
1166
1167    /// Load template from file
1168    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    /// Set template variable
1175    pub fn set_variable(&mut self, name: String, value: String) {
1176        self.variables.insert(name, value);
1177    }
1178}
1179
1180/// JSON report data structure
1181#[derive(Debug, Clone, Serialize, Deserialize)]
1182pub struct JsonReportData {
1183    /// Report metadata
1184    pub metadata: ReportMetadata,
1185    /// Report summary
1186    pub summary: ReportSummary,
1187    /// Test suite statistics
1188    pub statistics: TestSuiteStatistics,
1189    /// Individual test results
1190    pub test_results: Vec<CiCdTestResult>,
1191    /// Chart data
1192    pub charts: Vec<ChartData>,
1193    /// Performance trends
1194    pub trends: Vec<PerformanceTrendAnalysis>,
1195    /// Generation timestamp
1196    pub generated_at: SystemTime,
1197}
1198
1199// Default implementations
1200
1201impl 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}