1use crate::{ReportingError, Result};
4use chrono::{DateTime, Utc};
5use printpdf::*;
6use serde::{Deserialize, Serialize};
7use std::fs::File;
8use std::io::BufWriter;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct PdfConfig {
13 pub title: String,
14 pub author: String,
15 pub include_charts: bool,
16 pub include_metrics: bool,
17 pub include_recommendations: bool,
18}
19
20impl Default for PdfConfig {
21 fn default() -> Self {
22 Self {
23 title: "Chaos Orchestration Report".to_string(),
24 author: "MockForge".to_string(),
25 include_charts: true,
26 include_metrics: true,
27 include_recommendations: true,
28 }
29 }
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ExecutionReport {
35 pub orchestration_name: String,
36 pub start_time: DateTime<Utc>,
37 pub end_time: DateTime<Utc>,
38 pub duration_seconds: u64,
39 pub status: String,
40 pub total_steps: usize,
41 pub completed_steps: usize,
42 pub failed_steps: usize,
43 pub metrics: ReportMetrics,
44 pub failures: Vec<FailureDetail>,
45 pub recommendations: Vec<String>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ReportMetrics {
51 pub total_requests: u64,
52 pub successful_requests: u64,
53 pub failed_requests: u64,
54 pub avg_latency_ms: f64,
55 pub p95_latency_ms: f64,
56 pub p99_latency_ms: f64,
57 pub error_rate: f64,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct FailureDetail {
63 pub step_name: String,
64 pub error_message: String,
65 pub timestamp: DateTime<Utc>,
66}
67
68pub struct PdfReportGenerator {
70 config: PdfConfig,
71}
72
73impl PdfReportGenerator {
74 pub fn new(config: PdfConfig) -> Self {
76 Self { config }
77 }
78
79 pub fn generate(&self, report: &ExecutionReport, output_path: &str) -> Result<()> {
81 let (doc, page1, layer1) =
82 PdfDocument::new(&self.config.title, Mm(210.0), Mm(297.0), "Layer 1");
83
84 let font = doc
85 .add_builtin_font(BuiltinFont::Helvetica)
86 .map_err(|e| ReportingError::Pdf(e.to_string()))?;
87 let font_bold = doc
88 .add_builtin_font(BuiltinFont::HelveticaBold)
89 .map_err(|e| ReportingError::Pdf(e.to_string()))?;
90
91 let current_layer = doc.get_page(page1).get_layer(layer1);
92
93 current_layer.use_text(&self.config.title, 24.0, Mm(20.0), Mm(270.0), &font_bold);
95
96 let mut y = 255.0;
98 current_layer.use_text(
99 format!("Orchestration: {}", report.orchestration_name),
100 12.0,
101 Mm(20.0),
102 Mm(y),
103 &font,
104 );
105
106 y -= 7.0;
107 current_layer.use_text(
108 format!("Start: {}", report.start_time.format("%Y-%m-%d %H:%M:%S UTC")),
109 10.0,
110 Mm(20.0),
111 Mm(y),
112 &font,
113 );
114
115 y -= 5.0;
116 current_layer.use_text(
117 format!("End: {}", report.end_time.format("%Y-%m-%d %H:%M:%S UTC")),
118 10.0,
119 Mm(20.0),
120 Mm(y),
121 &font,
122 );
123
124 y -= 5.0;
125 current_layer.use_text(
126 format!("Duration: {}s", report.duration_seconds),
127 10.0,
128 Mm(20.0),
129 Mm(y),
130 &font,
131 );
132
133 y -= 5.0;
134 current_layer.use_text(
135 format!("Status: {}", report.status),
136 10.0,
137 Mm(20.0),
138 Mm(y),
139 &font_bold,
140 );
141
142 y -= 15.0;
144 current_layer.use_text("Summary", 14.0, Mm(20.0), Mm(y), &font_bold);
145
146 y -= 7.0;
147 current_layer.use_text(
148 format!("Total Steps: {}", report.total_steps),
149 10.0,
150 Mm(20.0),
151 Mm(y),
152 &font,
153 );
154
155 y -= 5.0;
156 current_layer.use_text(
157 format!("Completed: {}", report.completed_steps),
158 10.0,
159 Mm(20.0),
160 Mm(y),
161 &font,
162 );
163
164 y -= 5.0;
165 current_layer.use_text(
166 format!("Failed: {}", report.failed_steps),
167 10.0,
168 Mm(20.0),
169 Mm(y),
170 &font,
171 );
172
173 if self.config.include_metrics {
175 y -= 15.0;
176 current_layer.use_text("Metrics", 14.0, Mm(20.0), Mm(y), &font_bold);
177
178 y -= 7.0;
179 current_layer.use_text(
180 format!("Total Requests: {}", report.metrics.total_requests),
181 10.0,
182 Mm(20.0),
183 Mm(y),
184 &font,
185 );
186
187 y -= 5.0;
188 current_layer.use_text(
189 format!("Error Rate: {:.2}%", report.metrics.error_rate * 100.0),
190 10.0,
191 Mm(20.0),
192 Mm(y),
193 &font,
194 );
195
196 y -= 5.0;
197 current_layer.use_text(
198 format!("Avg Latency: {:.2}ms", report.metrics.avg_latency_ms),
199 10.0,
200 Mm(20.0),
201 Mm(y),
202 &font,
203 );
204
205 y -= 5.0;
206 current_layer.use_text(
207 format!("P95 Latency: {:.2}ms", report.metrics.p95_latency_ms),
208 10.0,
209 Mm(20.0),
210 Mm(y),
211 &font,
212 );
213 }
214
215 if self.config.include_charts {
217 y -= 15.0;
218 current_layer.use_text("Performance Overview", 14.0, Mm(20.0), Mm(y), &font_bold);
219
220 y -= 8.0;
222 let success_rate = if report.metrics.total_requests > 0 {
223 report.metrics.successful_requests as f64 / report.metrics.total_requests as f64
224 } else {
225 0.0
226 };
227 let bar_len = (success_rate * 30.0) as usize;
228 let bar =
229 format!("Success Rate: [{:>30}] {:.1}%", "#".repeat(bar_len), success_rate * 100.0);
230 current_layer.use_text(bar, 9.0, Mm(20.0), Mm(y), &font);
231
232 y -= 6.0;
234 let step_rate = if report.total_steps > 0 {
235 report.completed_steps as f64 / report.total_steps as f64
236 } else {
237 0.0
238 };
239 let bar_len = (step_rate * 30.0) as usize;
240 let bar =
241 format!("Steps Done: [{:>30}] {:.1}%", "#".repeat(bar_len), step_rate * 100.0);
242 current_layer.use_text(bar, 9.0, Mm(20.0), Mm(y), &font);
243
244 y -= 6.0;
246 current_layer.use_text(
247 format!(
248 "Latency (ms): avg={:.1} p95={:.1} p99={:.1}",
249 report.metrics.avg_latency_ms,
250 report.metrics.p95_latency_ms,
251 report.metrics.p99_latency_ms,
252 ),
253 9.0,
254 Mm(20.0),
255 Mm(y),
256 &font,
257 );
258 }
259
260 if !report.failures.is_empty() {
262 y -= 15.0;
263 current_layer.use_text("Failures", 14.0, Mm(20.0), Mm(y), &font_bold);
264
265 for failure in &report.failures {
266 y -= 7.0;
267 if y < 20.0 {
268 break; }
270 current_layer.use_text(
271 format!("• {}: {}", failure.step_name, failure.error_message),
272 9.0,
273 Mm(25.0),
274 Mm(y),
275 &font,
276 );
277 }
278 }
279
280 if self.config.include_recommendations && !report.recommendations.is_empty() {
282 y -= 15.0;
283 current_layer.use_text("Recommendations", 14.0, Mm(20.0), Mm(y), &font_bold);
284
285 for recommendation in &report.recommendations {
286 y -= 7.0;
287 if y < 20.0 {
288 break;
289 }
290 current_layer.use_text(
291 format!("• {}", recommendation),
292 9.0,
293 Mm(25.0),
294 Mm(y),
295 &font,
296 );
297 }
298 }
299
300 current_layer.use_text(
302 format!("Generated by MockForge on {}", Utc::now().format("%Y-%m-%d %H:%M UTC")),
303 8.0,
304 Mm(20.0),
305 Mm(10.0),
306 &font,
307 );
308
309 doc.save(&mut BufWriter::new(File::create(output_path)?))
311 .map_err(|e| ReportingError::Pdf(e.to_string()))?;
312
313 Ok(())
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use tempfile::tempdir;
321
322 fn create_test_report() -> ExecutionReport {
323 ExecutionReport {
324 orchestration_name: "test-orch".to_string(),
325 start_time: Utc::now(),
326 end_time: Utc::now(),
327 duration_seconds: 120,
328 status: "Completed".to_string(),
329 total_steps: 5,
330 completed_steps: 5,
331 failed_steps: 0,
332 metrics: ReportMetrics {
333 total_requests: 1000,
334 successful_requests: 980,
335 failed_requests: 20,
336 avg_latency_ms: 125.5,
337 p95_latency_ms: 250.0,
338 p99_latency_ms: 350.0,
339 error_rate: 0.02,
340 },
341 failures: vec![],
342 recommendations: vec!["Increase timeout thresholds".to_string()],
343 }
344 }
345
346 #[test]
347 fn test_pdf_generation() {
348 let config = PdfConfig::default();
349 let generator = PdfReportGenerator::new(config);
350 let report = create_test_report();
351
352 let temp_dir = tempdir().unwrap();
353 let output_path = temp_dir.path().join("report.pdf");
354
355 let result = generator.generate(&report, output_path.to_str().unwrap());
356 assert!(result.is_ok());
357 assert!(output_path.exists());
358 }
359
360 #[test]
361 fn test_pdf_config_default() {
362 let config = PdfConfig::default();
363 assert_eq!(config.title, "Chaos Orchestration Report");
364 assert_eq!(config.author, "MockForge");
365 assert!(config.include_charts);
366 assert!(config.include_metrics);
367 assert!(config.include_recommendations);
368 }
369
370 #[test]
371 fn test_pdf_config_custom() {
372 let config = PdfConfig {
373 title: "Custom Report".to_string(),
374 author: "Test Author".to_string(),
375 include_charts: false,
376 include_metrics: true,
377 include_recommendations: false,
378 };
379
380 assert_eq!(config.title, "Custom Report");
381 assert_eq!(config.author, "Test Author");
382 assert!(!config.include_charts);
383 assert!(config.include_metrics);
384 assert!(!config.include_recommendations);
385 }
386
387 #[test]
388 fn test_pdf_config_clone() {
389 let config = PdfConfig::default();
390 let cloned = config.clone();
391 assert_eq!(config.title, cloned.title);
392 assert_eq!(config.author, cloned.author);
393 }
394
395 #[test]
396 fn test_pdf_config_serialize() {
397 let config = PdfConfig::default();
398 let json = serde_json::to_string(&config).unwrap();
399 assert!(json.contains("title"));
400 assert!(json.contains("author"));
401 assert!(json.contains("include_charts"));
402 }
403
404 #[test]
405 fn test_pdf_config_deserialize() {
406 let json = r#"{"title":"Test","author":"Author","include_charts":true,"include_metrics":false,"include_recommendations":true}"#;
407 let config: PdfConfig = serde_json::from_str(json).unwrap();
408 assert_eq!(config.title, "Test");
409 assert_eq!(config.author, "Author");
410 assert!(config.include_charts);
411 assert!(!config.include_metrics);
412 }
413
414 #[test]
415 fn test_execution_report_clone() {
416 let report = create_test_report();
417 let cloned = report.clone();
418 assert_eq!(report.orchestration_name, cloned.orchestration_name);
419 assert_eq!(report.duration_seconds, cloned.duration_seconds);
420 }
421
422 #[test]
423 fn test_execution_report_serialize() {
424 let report = create_test_report();
425 let json = serde_json::to_string(&report).unwrap();
426 assert!(json.contains("orchestration_name"));
427 assert!(json.contains("metrics"));
428 assert!(json.contains("status"));
429 }
430
431 #[test]
432 fn test_report_metrics_clone() {
433 let metrics = ReportMetrics {
434 total_requests: 1000,
435 successful_requests: 980,
436 failed_requests: 20,
437 avg_latency_ms: 100.0,
438 p95_latency_ms: 200.0,
439 p99_latency_ms: 300.0,
440 error_rate: 0.02,
441 };
442
443 let cloned = metrics.clone();
444 assert_eq!(metrics.total_requests, cloned.total_requests);
445 assert_eq!(metrics.error_rate, cloned.error_rate);
446 }
447
448 #[test]
449 fn test_report_metrics_serialize() {
450 let metrics = ReportMetrics {
451 total_requests: 1000,
452 successful_requests: 980,
453 failed_requests: 20,
454 avg_latency_ms: 100.0,
455 p95_latency_ms: 200.0,
456 p99_latency_ms: 300.0,
457 error_rate: 0.02,
458 };
459
460 let json = serde_json::to_string(&metrics).unwrap();
461 assert!(json.contains("total_requests"));
462 assert!(json.contains("error_rate"));
463 }
464
465 #[test]
466 fn test_failure_detail_clone() {
467 let failure = FailureDetail {
468 step_name: "auth-step".to_string(),
469 error_message: "Connection timeout".to_string(),
470 timestamp: Utc::now(),
471 };
472
473 let cloned = failure.clone();
474 assert_eq!(failure.step_name, cloned.step_name);
475 assert_eq!(failure.error_message, cloned.error_message);
476 }
477
478 #[test]
479 fn test_failure_detail_serialize() {
480 let failure = FailureDetail {
481 step_name: "auth-step".to_string(),
482 error_message: "Connection timeout".to_string(),
483 timestamp: Utc::now(),
484 };
485
486 let json = serde_json::to_string(&failure).unwrap();
487 assert!(json.contains("step_name"));
488 assert!(json.contains("error_message"));
489 assert!(json.contains("timestamp"));
490 }
491
492 #[test]
493 fn test_pdf_with_failures() {
494 let config = PdfConfig::default();
495 let generator = PdfReportGenerator::new(config);
496
497 let mut report = create_test_report();
498 report.failures = vec![
499 FailureDetail {
500 step_name: "auth-step".to_string(),
501 error_message: "Connection timeout".to_string(),
502 timestamp: Utc::now(),
503 },
504 FailureDetail {
505 step_name: "data-step".to_string(),
506 error_message: "Invalid response".to_string(),
507 timestamp: Utc::now(),
508 },
509 ];
510 report.failed_steps = 2;
511
512 let temp_dir = tempdir().unwrap();
513 let output_path = temp_dir.path().join("report_with_failures.pdf");
514
515 let result = generator.generate(&report, output_path.to_str().unwrap());
516 assert!(result.is_ok());
517 assert!(output_path.exists());
518 }
519
520 #[test]
521 fn test_pdf_without_metrics() {
522 let config = PdfConfig {
523 include_metrics: false,
524 ..PdfConfig::default()
525 };
526 let generator = PdfReportGenerator::new(config);
527 let report = create_test_report();
528
529 let temp_dir = tempdir().unwrap();
530 let output_path = temp_dir.path().join("report_no_metrics.pdf");
531
532 let result = generator.generate(&report, output_path.to_str().unwrap());
533 assert!(result.is_ok());
534 }
535
536 #[test]
537 fn test_pdf_without_recommendations() {
538 let config = PdfConfig {
539 include_recommendations: false,
540 ..PdfConfig::default()
541 };
542 let generator = PdfReportGenerator::new(config);
543
544 let mut report = create_test_report();
545 report.recommendations = vec![];
546
547 let temp_dir = tempdir().unwrap();
548 let output_path = temp_dir.path().join("report_no_recs.pdf");
549
550 let result = generator.generate(&report, output_path.to_str().unwrap());
551 assert!(result.is_ok());
552 }
553
554 #[test]
555 fn test_pdf_generator_invalid_path() {
556 let config = PdfConfig::default();
557 let generator = PdfReportGenerator::new(config);
558 let report = create_test_report();
559
560 let result = generator.generate(&report, "/nonexistent/path/report.pdf");
561 assert!(result.is_err());
562 }
563
564 #[test]
565 fn test_pdf_config_debug() {
566 let config = PdfConfig::default();
567 let debug = format!("{:?}", config);
568 assert!(debug.contains("PdfConfig"));
569 }
570
571 #[test]
572 fn test_execution_report_debug() {
573 let report = create_test_report();
574 let debug = format!("{:?}", report);
575 assert!(debug.contains("ExecutionReport"));
576 }
577
578 #[test]
579 fn test_report_metrics_debug() {
580 let metrics = ReportMetrics {
581 total_requests: 1000,
582 successful_requests: 980,
583 failed_requests: 20,
584 avg_latency_ms: 100.0,
585 p95_latency_ms: 200.0,
586 p99_latency_ms: 300.0,
587 error_rate: 0.02,
588 };
589 let debug = format!("{:?}", metrics);
590 assert!(debug.contains("ReportMetrics"));
591 }
592}