1use 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#[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}
41pub struct TestReportGenerator {
43 pub templates: HashMap<String, ReportTemplate>,
45 pub generated_reports: Vec<GeneratedReport>,
47 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 pub fn register_template(&mut self, template: ReportTemplate) {
63 self.templates.insert(template.name.clone(), template);
64 }
65
66 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 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 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 §ion.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 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 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 §ion.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 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 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 #[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 #[must_use]
317 pub fn list_reports(&self) -> Vec<&GeneratedReport> {
318 self.generated_reports.iter().collect()
319 }
320
321 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 pub fn clear_reports(&mut self) {
336 self.generated_reports.clear();
337 }
338
339 #[must_use]
341 pub fn report_count(&self) -> usize {
342 self.generated_reports.len()
343 }
344}
345
346#[derive(Debug, Clone)]
348pub struct ReportData {
349 pub test_results: Vec<super::results::IntegrationTestResult>,
351 pub performance_metrics: HashMap<String, f64>,
353 pub additional_data: HashMap<String, String>,
355}
356
357#[derive(Debug, Clone)]
359pub struct ReportTemplate {
360 pub name: String,
362 pub format: ReportFormat,
364 pub sections: Vec<ReportSection>,
366 pub metadata: ReportMetadata,
368}
369
370#[derive(Debug, Clone)]
372pub struct ReportSection {
373 pub name: String,
375 pub section_type: SectionType,
377 pub content: SectionContent,
379 pub formatting: SectionFormatting,
381}
382
383#[derive(Debug, Clone, PartialEq, Eq)]
385pub enum SectionType {
386 Summary,
387 TestResults,
388 PerformanceMetrics,
389 ErrorAnalysis,
390 Recommendations,
391 Custom(String),
392}
393
394#[derive(Debug, Clone)]
396pub enum SectionContent {
397 Text(String),
399 Data(DataQuery),
401 Chart(ChartDefinition),
403 Table(TableDefinition),
405 Custom(String),
407}
408
409#[derive(Debug, Clone)]
411pub struct DataQuery {
412 pub query_type: QueryType,
414 pub parameters: HashMap<String, String>,
416 pub transformation: Option<DataTransformation>,
418}
419
420#[derive(Debug, Clone, PartialEq, Eq)]
422pub enum QueryType {
423 TestResults,
424 PerformanceMetrics,
425 ErrorCounts,
426 TrendData,
427 ComparisonData,
428 Custom(String),
429}
430
431#[derive(Debug, Clone)]
433pub struct DataTransformation {
434 pub transformation_type: TransformationType,
436 pub parameters: HashMap<String, String>,
438}
439
440#[derive(Debug, Clone, PartialEq, Eq)]
442pub enum TransformationType {
443 Aggregate,
444 Filter,
445 Sort,
446 Group,
447 Calculate,
448 Custom(String),
449}
450
451#[derive(Debug, Clone)]
453pub struct ChartDefinition {
454 pub chart_type: ChartType,
456 pub data_source: DataQuery,
458 pub configuration: ChartConfiguration,
460}
461
462#[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#[derive(Debug, Clone)]
476pub struct ChartConfiguration {
477 pub title: String,
479 pub x_axis_label: String,
481 pub y_axis_label: String,
483 pub dimensions: (u32, u32),
485 pub color_scheme: Vec<String>,
487}
488
489#[derive(Debug, Clone)]
491pub struct TableDefinition {
492 pub columns: Vec<TableColumn>,
494 pub data_source: DataQuery,
496 pub formatting: TableFormatting,
498}
499
500#[derive(Debug, Clone)]
502pub struct TableColumn {
503 pub name: String,
505 pub column_type: ColumnType,
507 pub formatting: ColumnFormatting,
509}
510
511#[derive(Debug, Clone, PartialEq, Eq)]
513pub enum ColumnType {
514 Text,
515 Number,
516 DateTime,
517 Boolean,
518 Duration,
519 Custom(String),
520}
521
522#[derive(Debug, Clone)]
524pub struct ColumnFormatting {
525 pub number_format: Option<NumberFormat>,
527 pub date_format: Option<String>,
529 pub alignment: TextAlignment,
531}
532
533#[derive(Debug, Clone)]
535pub struct NumberFormat {
536 pub decimal_places: usize,
538 pub thousands_separator: bool,
540 pub unit: Option<String>,
542}
543
544#[derive(Debug, Clone, PartialEq, Eq)]
546pub enum TextAlignment {
547 Left,
548 Center,
549 Right,
550}
551
552#[derive(Debug, Clone)]
554pub struct TableFormatting {
555 pub show_headers: bool,
557 pub alternate_rows: bool,
559 pub border_style: BorderStyle,
561}
562
563#[derive(Debug, Clone, PartialEq, Eq)]
565pub enum BorderStyle {
566 None,
567 Simple,
568 Double,
569 Rounded,
570 Custom(String),
571}
572
573#[derive(Debug, Clone)]
575pub struct SectionFormatting {
576 pub font_size: u8,
578 pub font_weight: FontWeight,
580 pub text_color: String,
582 pub background_color: Option<String>,
584 pub padding: (u8, u8, u8, u8),
586}
587
588#[derive(Debug, Clone, PartialEq, Eq)]
590pub enum FontWeight {
591 Normal,
592 Bold,
593 Light,
594 ExtraBold,
595}
596
597#[derive(Debug, Clone)]
599pub struct GeneratedReport {
600 pub id: String,
602 pub name: String,
604 pub format: ReportFormat,
606 pub generated_at: SystemTime,
608 pub content: String,
610 pub metadata: ReportMetadata,
612 pub size: usize,
614}
615
616#[derive(Debug, Clone)]
618pub struct ReportMetadata {
619 pub title: String,
621 pub description: String,
623 pub author: String,
625 pub version: String,
627 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}