Skip to main content

quantrs2_anneal/comprehensive_integration_testing/
reporting.rs

1//! Test reporting and report generation
2
3use std::collections::HashMap;
4use std::fs;
5use std::time::SystemTime;
6
7use super::config::ReportFormat;
8use super::results::TestOutcome;
9
10use std::fmt::Write;
11
12/// Real pass/fail/skip/timeout/error counts computed from `ReportData::test_results`.
13#[derive(Debug, Clone, Copy, Default)]
14struct TestOutcomeCounts {
15    passed: usize,
16    failed: usize,
17    skipped: usize,
18    timeout: usize,
19    error: usize,
20}
21
22impl TestOutcomeCounts {
23    fn from_data(data: &ReportData) -> Self {
24        let mut counts = Self::default();
25        for result in &data.test_results {
26            match result.outcome {
27                TestOutcome::Passed => counts.passed += 1,
28                TestOutcome::Failed => counts.failed += 1,
29                TestOutcome::Skipped => counts.skipped += 1,
30                TestOutcome::Timeout => counts.timeout += 1,
31                TestOutcome::Error => counts.error += 1,
32            }
33        }
34        counts
35    }
36
37    const fn total(&self) -> usize {
38        self.passed + self.failed + self.skipped + self.timeout + self.error
39    }
40}
41/// Test report generator
42pub struct TestReportGenerator {
43    /// Report templates
44    pub templates: HashMap<String, ReportTemplate>,
45    /// Generated reports
46    pub generated_reports: Vec<GeneratedReport>,
47    /// Report configuration
48    pub config: super::config::ReportingConfig,
49}
50
51impl TestReportGenerator {
52    #[must_use]
53    pub fn new() -> Self {
54        Self {
55            templates: HashMap::new(),
56            generated_reports: vec![],
57            config: super::config::ReportingConfig::default(),
58        }
59    }
60
61    /// Register a report template
62    pub fn register_template(&mut self, template: ReportTemplate) {
63        self.templates.insert(template.name.clone(), template);
64    }
65
66    /// Generate a report from a template
67    pub fn generate_report(
68        &mut self,
69        template_name: &str,
70        data: &ReportData,
71    ) -> Result<GeneratedReport, String> {
72        let template = self
73            .templates
74            .get(template_name)
75            .ok_or_else(|| format!("Template '{template_name}' not found"))?;
76
77        // Generate report content based on format
78        let content = match template.format {
79            ReportFormat::HTML => self.generate_html_report(template, data)?,
80            ReportFormat::JSON => self.generate_json_report(template, data)?,
81            ReportFormat::XML => self.generate_xml_report(template, data)?,
82            ReportFormat::PDF => self.generate_pdf_report(template, data)?,
83            ReportFormat::CSV => self.generate_csv_report(template, data)?,
84        };
85
86        let report = GeneratedReport {
87            id: format!(
88                "report_{}",
89                SystemTime::now()
90                    .duration_since(SystemTime::UNIX_EPOCH)
91                    .expect("system time before UNIX_EPOCH")
92                    .as_secs()
93            ),
94            name: template.name.clone(),
95            format: template.format.clone(),
96            generated_at: SystemTime::now(),
97            content: content.clone(),
98            metadata: template.metadata.clone(),
99            size: content.len(),
100        };
101
102        self.generated_reports.push(report.clone());
103        Ok(report)
104    }
105
106    /// Generate HTML format report
107    fn generate_html_report(
108        &self,
109        template: &ReportTemplate,
110        data: &ReportData,
111    ) -> Result<String, String> {
112        let mut html = String::from("<html><head><title>");
113        html.push_str(&template.metadata.title);
114        html.push_str("</title></head><body>");
115
116        for section in &template.sections {
117            write!(html, "<h2>{}</h2>", section.name).expect("failed to write to string");
118            match &section.content {
119                SectionContent::Text(text) => {
120                    write!(html, "<p>{text}</p>").expect("failed to write to string");
121                }
122                SectionContent::Table(_) => {
123                    let counts = TestOutcomeCounts::from_data(data);
124                    html.push_str("<table><tr><th>Metric</th><th>Value</th></tr>");
125                    write!(
126                        html,
127                        "<tr><td>Tests Passed</td><td>{}</td></tr>",
128                        counts.passed
129                    )
130                    .expect("failed to write to string");
131                    write!(
132                        html,
133                        "<tr><td>Tests Failed</td><td>{}</td></tr>",
134                        counts.failed
135                    )
136                    .expect("failed to write to string");
137                    write!(
138                        html,
139                        "<tr><td>Tests Skipped</td><td>{}</td></tr>",
140                        counts.skipped
141                    )
142                    .expect("failed to write to string");
143                    write!(
144                        html,
145                        "<tr><td>Tests Timed Out</td><td>{}</td></tr>",
146                        counts.timeout
147                    )
148                    .expect("failed to write to string");
149                    write!(
150                        html,
151                        "<tr><td>Tests Errored</td><td>{}</td></tr>",
152                        counts.error
153                    )
154                    .expect("failed to write to string");
155                    write!(
156                        html,
157                        "<tr><td>Total Tests</td><td>{}</td></tr>",
158                        counts.total()
159                    )
160                    .expect("failed to write to string");
161                    for (metric_name, metric_value) in &data.performance_metrics {
162                        write!(
163                            html,
164                            "<tr><td>{metric_name}</td><td>{metric_value}</td></tr>"
165                        )
166                        .expect("failed to write to string");
167                    }
168                    html.push_str("</table>");
169                }
170                _ => {
171                    html.push_str("<p>Content not implemented</p>");
172                }
173            }
174        }
175
176        html.push_str("</body></html>");
177        Ok(html)
178    }
179
180    /// Generate JSON format report
181    fn generate_json_report(
182        &self,
183        template: &ReportTemplate,
184        data: &ReportData,
185    ) -> Result<String, String> {
186        let counts = TestOutcomeCounts::from_data(data);
187        let value = serde_json::json!({
188            "title": template.metadata.title,
189            "description": template.metadata.description,
190            "summary": {
191                "tests_passed": counts.passed,
192                "tests_failed": counts.failed,
193                "tests_skipped": counts.skipped,
194                "tests_timeout": counts.timeout,
195                "tests_error": counts.error,
196                "total_tests": counts.total(),
197            },
198            "performance_metrics": data.performance_metrics,
199            "additional_data": data.additional_data,
200        });
201        serde_json::to_string(&value).map_err(|e| format!("failed to serialize JSON report: {e}"))
202    }
203
204    /// Generate XML format report
205    fn generate_xml_report(
206        &self,
207        template: &ReportTemplate,
208        data: &ReportData,
209    ) -> Result<String, String> {
210        let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
211        write!(xml, "<report title=\"{}\">\n", template.metadata.title)
212            .expect("failed to write to string");
213        write!(
214            xml,
215            "  <description>{}</description>\n",
216            template.metadata.description
217        )
218        .expect("failed to write to string");
219
220        for section in &template.sections {
221            write!(xml, "  <section name=\"{}\">\n", section.name)
222                .expect("failed to write to string");
223            match &section.content {
224                SectionContent::Text(text) => {
225                    writeln!(xml, "    <content>{text}</content>")
226                        .expect("failed to write to string");
227                }
228                SectionContent::Table(_) => {
229                    let counts = TestOutcomeCounts::from_data(data);
230                    xml.push_str("    <table>\n");
231                    writeln!(
232                        xml,
233                        "      <row><cell>Tests Passed</cell><cell>{}</cell></row>",
234                        counts.passed
235                    )
236                    .expect("failed to write to string");
237                    writeln!(
238                        xml,
239                        "      <row><cell>Tests Failed</cell><cell>{}</cell></row>",
240                        counts.failed
241                    )
242                    .expect("failed to write to string");
243                    writeln!(
244                        xml,
245                        "      <row><cell>Tests Skipped</cell><cell>{}</cell></row>",
246                        counts.skipped
247                    )
248                    .expect("failed to write to string");
249                    writeln!(
250                        xml,
251                        "      <row><cell>Tests Timed Out</cell><cell>{}</cell></row>",
252                        counts.timeout
253                    )
254                    .expect("failed to write to string");
255                    writeln!(
256                        xml,
257                        "      <row><cell>Tests Errored</cell><cell>{}</cell></row>",
258                        counts.error
259                    )
260                    .expect("failed to write to string");
261                    writeln!(
262                        xml,
263                        "      <row><cell>Total Tests</cell><cell>{}</cell></row>",
264                        counts.total()
265                    )
266                    .expect("failed to write to string");
267                    xml.push_str("    </table>\n");
268                }
269                _ => {
270                    xml.push_str("    <content>Content not implemented</content>\n");
271                }
272            }
273            xml.push_str("  </section>\n");
274        }
275
276        xml.push_str("</report>");
277        Ok(xml)
278    }
279
280    /// Generate PDF format report (placeholder)
281    fn generate_pdf_report(
282        &self,
283        template: &ReportTemplate,
284        _data: &ReportData,
285    ) -> Result<String, String> {
286        Ok(format!("PDF Report: {}", template.metadata.title))
287    }
288
289    /// Generate CSV format report
290    fn generate_csv_report(
291        &self,
292        _template: &ReportTemplate,
293        data: &ReportData,
294    ) -> Result<String, String> {
295        let counts = TestOutcomeCounts::from_data(data);
296        let mut csv = String::from("Metric,Value\n");
297        writeln!(csv, "Tests Passed,{}", counts.passed).expect("failed to write to string");
298        writeln!(csv, "Tests Failed,{}", counts.failed).expect("failed to write to string");
299        writeln!(csv, "Tests Skipped,{}", counts.skipped).expect("failed to write to string");
300        writeln!(csv, "Tests Timed Out,{}", counts.timeout).expect("failed to write to string");
301        writeln!(csv, "Tests Errored,{}", counts.error).expect("failed to write to string");
302        writeln!(csv, "Total Tests,{}", counts.total()).expect("failed to write to string");
303        for (metric_name, metric_value) in &data.performance_metrics {
304            writeln!(csv, "{metric_name},{metric_value}").expect("failed to write to string");
305        }
306        Ok(csv)
307    }
308
309    /// Get a generated report by ID
310    #[must_use]
311    pub fn get_report(&self, report_id: &str) -> Option<&GeneratedReport> {
312        self.generated_reports.iter().find(|r| r.id == report_id)
313    }
314
315    /// List all generated reports
316    #[must_use]
317    pub fn list_reports(&self) -> Vec<&GeneratedReport> {
318        self.generated_reports.iter().collect()
319    }
320
321    /// Export a previously generated report to disk at `file_path`.
322    ///
323    /// The report's already-rendered `content` (HTML/JSON/XML/CSV, or the
324    /// textual PDF placeholder) is written verbatim to the given path.
325    /// Returns an error if the report id is unknown or if the write fails.
326    pub fn export_report(&self, report_id: &str, file_path: &str) -> Result<(), String> {
327        let report = self
328            .get_report(report_id)
329            .ok_or_else(|| format!("Report {report_id} not found"))?;
330        fs::write(file_path, &report.content)
331            .map_err(|e| format!("failed to write report to '{file_path}': {e}"))
332    }
333
334    /// Clear all generated reports
335    pub fn clear_reports(&mut self) {
336        self.generated_reports.clear();
337    }
338
339    /// Get report count
340    #[must_use]
341    pub fn report_count(&self) -> usize {
342        self.generated_reports.len()
343    }
344}
345
346/// Report data container
347#[derive(Debug, Clone)]
348pub struct ReportData {
349    /// Test results
350    pub test_results: Vec<super::results::IntegrationTestResult>,
351    /// Performance metrics
352    pub performance_metrics: HashMap<String, f64>,
353    /// Additional data
354    pub additional_data: HashMap<String, String>,
355}
356
357/// Report template
358#[derive(Debug, Clone)]
359pub struct ReportTemplate {
360    /// Template name
361    pub name: String,
362    /// Template format
363    pub format: ReportFormat,
364    /// Template sections
365    pub sections: Vec<ReportSection>,
366    /// Template metadata
367    pub metadata: ReportMetadata,
368}
369
370/// Report section
371#[derive(Debug, Clone)]
372pub struct ReportSection {
373    /// Section name
374    pub name: String,
375    /// Section type
376    pub section_type: SectionType,
377    /// Section content
378    pub content: SectionContent,
379    /// Section formatting
380    pub formatting: SectionFormatting,
381}
382
383/// Section types
384#[derive(Debug, Clone, PartialEq, Eq)]
385pub enum SectionType {
386    Summary,
387    TestResults,
388    PerformanceMetrics,
389    ErrorAnalysis,
390    Recommendations,
391    Custom(String),
392}
393
394/// Section content
395#[derive(Debug, Clone)]
396pub enum SectionContent {
397    /// Static text
398    Text(String),
399    /// Dynamic data
400    Data(DataQuery),
401    /// Chart/visualization
402    Chart(ChartDefinition),
403    /// Table
404    Table(TableDefinition),
405    /// Custom content
406    Custom(String),
407}
408
409/// Data query for dynamic content
410#[derive(Debug, Clone)]
411pub struct DataQuery {
412    /// Query type
413    pub query_type: QueryType,
414    /// Query parameters
415    pub parameters: HashMap<String, String>,
416    /// Data transformation
417    pub transformation: Option<DataTransformation>,
418}
419
420/// Query types
421#[derive(Debug, Clone, PartialEq, Eq)]
422pub enum QueryType {
423    TestResults,
424    PerformanceMetrics,
425    ErrorCounts,
426    TrendData,
427    ComparisonData,
428    Custom(String),
429}
430
431/// Data transformation
432#[derive(Debug, Clone)]
433pub struct DataTransformation {
434    /// Transformation type
435    pub transformation_type: TransformationType,
436    /// Transformation parameters
437    pub parameters: HashMap<String, String>,
438}
439
440/// Transformation types
441#[derive(Debug, Clone, PartialEq, Eq)]
442pub enum TransformationType {
443    Aggregate,
444    Filter,
445    Sort,
446    Group,
447    Calculate,
448    Custom(String),
449}
450
451/// Chart definition
452#[derive(Debug, Clone)]
453pub struct ChartDefinition {
454    /// Chart type
455    pub chart_type: ChartType,
456    /// Chart data source
457    pub data_source: DataQuery,
458    /// Chart configuration
459    pub configuration: ChartConfiguration,
460}
461
462/// Chart types
463#[derive(Debug, Clone, PartialEq, Eq)]
464pub enum ChartType {
465    Line,
466    Bar,
467    Pie,
468    Scatter,
469    Histogram,
470    Heatmap,
471    Custom(String),
472}
473
474/// Chart configuration
475#[derive(Debug, Clone)]
476pub struct ChartConfiguration {
477    /// Chart title
478    pub title: String,
479    /// X-axis label
480    pub x_axis_label: String,
481    /// Y-axis label
482    pub y_axis_label: String,
483    /// Chart dimensions
484    pub dimensions: (u32, u32),
485    /// Color scheme
486    pub color_scheme: Vec<String>,
487}
488
489/// Table definition
490#[derive(Debug, Clone)]
491pub struct TableDefinition {
492    /// Table columns
493    pub columns: Vec<TableColumn>,
494    /// Table data source
495    pub data_source: DataQuery,
496    /// Table formatting
497    pub formatting: TableFormatting,
498}
499
500/// Table column
501#[derive(Debug, Clone)]
502pub struct TableColumn {
503    /// Column name
504    pub name: String,
505    /// Column type
506    pub column_type: ColumnType,
507    /// Column formatting
508    pub formatting: ColumnFormatting,
509}
510
511/// Column types
512#[derive(Debug, Clone, PartialEq, Eq)]
513pub enum ColumnType {
514    Text,
515    Number,
516    DateTime,
517    Boolean,
518    Duration,
519    Custom(String),
520}
521
522/// Column formatting
523#[derive(Debug, Clone)]
524pub struct ColumnFormatting {
525    /// Number format
526    pub number_format: Option<NumberFormat>,
527    /// Date format
528    pub date_format: Option<String>,
529    /// Text alignment
530    pub alignment: TextAlignment,
531}
532
533/// Number formatting
534#[derive(Debug, Clone)]
535pub struct NumberFormat {
536    /// Decimal places
537    pub decimal_places: usize,
538    /// Use thousands separator
539    pub thousands_separator: bool,
540    /// Unit suffix
541    pub unit: Option<String>,
542}
543
544/// Text alignment
545#[derive(Debug, Clone, PartialEq, Eq)]
546pub enum TextAlignment {
547    Left,
548    Center,
549    Right,
550}
551
552/// Table formatting
553#[derive(Debug, Clone)]
554pub struct TableFormatting {
555    /// Show headers
556    pub show_headers: bool,
557    /// Alternate row colors
558    pub alternate_rows: bool,
559    /// Border style
560    pub border_style: BorderStyle,
561}
562
563/// Border styles
564#[derive(Debug, Clone, PartialEq, Eq)]
565pub enum BorderStyle {
566    None,
567    Simple,
568    Double,
569    Rounded,
570    Custom(String),
571}
572
573/// Section formatting
574#[derive(Debug, Clone)]
575pub struct SectionFormatting {
576    /// Font size
577    pub font_size: u8,
578    /// Font weight
579    pub font_weight: FontWeight,
580    /// Text color
581    pub text_color: String,
582    /// Background color
583    pub background_color: Option<String>,
584    /// Padding
585    pub padding: (u8, u8, u8, u8),
586}
587
588/// Font weights
589#[derive(Debug, Clone, PartialEq, Eq)]
590pub enum FontWeight {
591    Normal,
592    Bold,
593    Light,
594    ExtraBold,
595}
596
597/// Generated report
598#[derive(Debug, Clone)]
599pub struct GeneratedReport {
600    /// Report ID
601    pub id: String,
602    /// Report name
603    pub name: String,
604    /// Report format
605    pub format: ReportFormat,
606    /// Generation timestamp
607    pub generated_at: SystemTime,
608    /// Report content
609    pub content: String,
610    /// Report metadata
611    pub metadata: ReportMetadata,
612    /// Report size
613    pub size: usize,
614}
615
616/// Report metadata
617#[derive(Debug, Clone)]
618pub struct ReportMetadata {
619    /// Report title
620    pub title: String,
621    /// Report description
622    pub description: String,
623    /// Report author
624    pub author: String,
625    /// Report version
626    pub version: String,
627    /// Custom metadata
628    pub custom: HashMap<String, String>,
629}
630
631#[cfg(test)]
632mod tests {
633    use super::super::results::{
634        IntegrationTestResult, PerformanceMetrics, ValidationResults, ValidationStatus,
635        ValidationSummary,
636    };
637    use super::*;
638    use std::time::Duration;
639
640    fn make_result(outcome: TestOutcome) -> IntegrationTestResult {
641        IntegrationTestResult {
642            test_case_id: "case".to_string(),
643            timestamp: SystemTime::now(),
644            outcome,
645            performance_metrics: PerformanceMetrics {
646                execution_duration: Duration::from_secs(1),
647                setup_duration: Duration::from_millis(10),
648                cleanup_duration: Duration::from_millis(5),
649                peak_memory_usage: 1024,
650                avg_cpu_usage: 0.5,
651                custom_metrics: HashMap::new(),
652            },
653            validation_results: ValidationResults {
654                status: ValidationStatus::Passed,
655                validations: vec![],
656                summary: ValidationSummary {
657                    total: 1,
658                    passed: 1,
659                    failed: 0,
660                    skipped: 0,
661                },
662            },
663            error_info: None,
664            artifacts: vec![],
665        }
666    }
667
668    fn make_data() -> ReportData {
669        ReportData {
670            test_results: vec![
671                make_result(TestOutcome::Passed),
672                make_result(TestOutcome::Passed),
673                make_result(TestOutcome::Failed),
674                make_result(TestOutcome::Skipped),
675            ],
676            performance_metrics: HashMap::new(),
677            additional_data: HashMap::new(),
678        }
679    }
680
681    fn make_template(format: ReportFormat) -> ReportTemplate {
682        ReportTemplate {
683            name: "regression_template".to_string(),
684            format,
685            sections: vec![ReportSection {
686                name: "Summary".to_string(),
687                section_type: SectionType::Summary,
688                content: SectionContent::Table(TableDefinition {
689                    columns: vec![],
690                    data_source: DataQuery {
691                        query_type: QueryType::TestResults,
692                        parameters: HashMap::new(),
693                        transformation: None,
694                    },
695                    formatting: TableFormatting {
696                        show_headers: true,
697                        alternate_rows: false,
698                        border_style: BorderStyle::Simple,
699                    },
700                }),
701                formatting: SectionFormatting {
702                    font_size: 12,
703                    font_weight: FontWeight::Normal,
704                    text_color: "#000".to_string(),
705                    background_color: None,
706                    padding: (0, 0, 0, 0),
707                },
708            }],
709            metadata: ReportMetadata {
710                title: "Regression Report".to_string(),
711                description: "test".to_string(),
712                author: "quantrs2".to_string(),
713                version: "1".to_string(),
714                custom: HashMap::new(),
715            },
716        }
717    }
718
719    #[test]
720    fn html_report_reflects_real_outcome_counts() {
721        let mut generator = TestReportGenerator::new();
722        generator.register_template(make_template(ReportFormat::HTML));
723        let data = make_data();
724        let report = generator
725            .generate_report("regression_template", &data)
726            .expect("report generation should succeed");
727        assert!(report.content.contains("<td>Tests Passed</td><td>2</td>"));
728        assert!(report.content.contains("<td>Tests Failed</td><td>1</td>"));
729        assert!(report.content.contains("<td>Tests Skipped</td><td>1</td>"));
730        assert!(report.content.contains("<td>Total Tests</td><td>4</td>"));
731    }
732
733    #[test]
734    fn csv_report_reflects_real_outcome_counts() {
735        let mut generator = TestReportGenerator::new();
736        generator.register_template(make_template(ReportFormat::CSV));
737        let data = make_data();
738        let report = generator
739            .generate_report("regression_template", &data)
740            .expect("report generation should succeed");
741        assert!(report.content.contains("Tests Passed,2"));
742        assert!(report.content.contains("Tests Failed,1"));
743        assert!(report.content.contains("Total Tests,4"));
744    }
745
746    #[test]
747    fn export_report_actually_writes_the_file() {
748        let mut generator = TestReportGenerator::new();
749        generator.register_template(make_template(ReportFormat::CSV));
750        let data = make_data();
751        let report = generator
752            .generate_report("regression_template", &data)
753            .expect("report generation should succeed");
754
755        let mut path = std::env::temp_dir();
756        path.push(format!(
757            "quantrs2_anneal_export_report_test_{}.csv",
758            report.id
759        ));
760        let path_str = path.to_str().expect("path should be valid UTF-8");
761
762        generator
763            .export_report(&report.id, path_str)
764            .expect("export should succeed");
765
766        let written = std::fs::read_to_string(&path).expect("exported file should exist");
767        assert_eq!(written, report.content);
768
769        std::fs::remove_file(&path).expect("cleanup should succeed");
770    }
771
772    #[test]
773    fn export_report_errors_on_unknown_report_id() {
774        let generator = TestReportGenerator::new();
775        let mut path = std::env::temp_dir();
776        path.push("quantrs2_anneal_export_report_missing.csv");
777        let result = generator.export_report("does-not-exist", path.to_str().unwrap());
778        assert!(result.is_err());
779    }
780}