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 !report.failures.is_empty() {
217 y -= 15.0;
218 current_layer.use_text("Failures", 14.0, Mm(20.0), Mm(y), &font_bold);
219
220 for failure in &report.failures {
221 y -= 7.0;
222 if y < 20.0 {
223 break; }
225 current_layer.use_text(
226 format!("• {}: {}", failure.step_name, failure.error_message),
227 9.0,
228 Mm(25.0),
229 Mm(y),
230 &font,
231 );
232 }
233 }
234
235 if self.config.include_recommendations && !report.recommendations.is_empty() {
237 y -= 15.0;
238 current_layer.use_text("Recommendations", 14.0, Mm(20.0), Mm(y), &font_bold);
239
240 for recommendation in &report.recommendations {
241 y -= 7.0;
242 if y < 20.0 {
243 break;
244 }
245 current_layer.use_text(
246 format!("• {}", recommendation),
247 9.0,
248 Mm(25.0),
249 Mm(y),
250 &font,
251 );
252 }
253 }
254
255 current_layer.use_text(
257 format!("Generated by MockForge on {}", Utc::now().format("%Y-%m-%d %H:%M UTC")),
258 8.0,
259 Mm(20.0),
260 Mm(10.0),
261 &font,
262 );
263
264 doc.save(&mut BufWriter::new(File::create(output_path)?))
266 .map_err(|e| ReportingError::Pdf(e.to_string()))?;
267
268 Ok(())
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use tempfile::tempdir;
276
277 fn create_test_report() -> ExecutionReport {
278 ExecutionReport {
279 orchestration_name: "test-orch".to_string(),
280 start_time: Utc::now(),
281 end_time: Utc::now(),
282 duration_seconds: 120,
283 status: "Completed".to_string(),
284 total_steps: 5,
285 completed_steps: 5,
286 failed_steps: 0,
287 metrics: ReportMetrics {
288 total_requests: 1000,
289 successful_requests: 980,
290 failed_requests: 20,
291 avg_latency_ms: 125.5,
292 p95_latency_ms: 250.0,
293 p99_latency_ms: 350.0,
294 error_rate: 0.02,
295 },
296 failures: vec![],
297 recommendations: vec!["Increase timeout thresholds".to_string()],
298 }
299 }
300
301 #[test]
302 fn test_pdf_generation() {
303 let config = PdfConfig::default();
304 let generator = PdfReportGenerator::new(config);
305 let report = create_test_report();
306
307 let temp_dir = tempdir().unwrap();
308 let output_path = temp_dir.path().join("report.pdf");
309
310 let result = generator.generate(&report, output_path.to_str().unwrap());
311 assert!(result.is_ok());
312 assert!(output_path.exists());
313 }
314
315 #[test]
316 fn test_pdf_config_default() {
317 let config = PdfConfig::default();
318 assert_eq!(config.title, "Chaos Orchestration Report");
319 assert_eq!(config.author, "MockForge");
320 assert!(config.include_charts);
321 assert!(config.include_metrics);
322 assert!(config.include_recommendations);
323 }
324
325 #[test]
326 fn test_pdf_config_custom() {
327 let config = PdfConfig {
328 title: "Custom Report".to_string(),
329 author: "Test Author".to_string(),
330 include_charts: false,
331 include_metrics: true,
332 include_recommendations: false,
333 };
334
335 assert_eq!(config.title, "Custom Report");
336 assert_eq!(config.author, "Test Author");
337 assert!(!config.include_charts);
338 assert!(config.include_metrics);
339 assert!(!config.include_recommendations);
340 }
341
342 #[test]
343 fn test_pdf_config_clone() {
344 let config = PdfConfig::default();
345 let cloned = config.clone();
346 assert_eq!(config.title, cloned.title);
347 assert_eq!(config.author, cloned.author);
348 }
349
350 #[test]
351 fn test_pdf_config_serialize() {
352 let config = PdfConfig::default();
353 let json = serde_json::to_string(&config).unwrap();
354 assert!(json.contains("title"));
355 assert!(json.contains("author"));
356 assert!(json.contains("include_charts"));
357 }
358
359 #[test]
360 fn test_pdf_config_deserialize() {
361 let json = r#"{"title":"Test","author":"Author","include_charts":true,"include_metrics":false,"include_recommendations":true}"#;
362 let config: PdfConfig = serde_json::from_str(json).unwrap();
363 assert_eq!(config.title, "Test");
364 assert_eq!(config.author, "Author");
365 assert!(config.include_charts);
366 assert!(!config.include_metrics);
367 }
368
369 #[test]
370 fn test_execution_report_clone() {
371 let report = create_test_report();
372 let cloned = report.clone();
373 assert_eq!(report.orchestration_name, cloned.orchestration_name);
374 assert_eq!(report.duration_seconds, cloned.duration_seconds);
375 }
376
377 #[test]
378 fn test_execution_report_serialize() {
379 let report = create_test_report();
380 let json = serde_json::to_string(&report).unwrap();
381 assert!(json.contains("orchestration_name"));
382 assert!(json.contains("metrics"));
383 assert!(json.contains("status"));
384 }
385
386 #[test]
387 fn test_report_metrics_clone() {
388 let metrics = ReportMetrics {
389 total_requests: 1000,
390 successful_requests: 980,
391 failed_requests: 20,
392 avg_latency_ms: 100.0,
393 p95_latency_ms: 200.0,
394 p99_latency_ms: 300.0,
395 error_rate: 0.02,
396 };
397
398 let cloned = metrics.clone();
399 assert_eq!(metrics.total_requests, cloned.total_requests);
400 assert_eq!(metrics.error_rate, cloned.error_rate);
401 }
402
403 #[test]
404 fn test_report_metrics_serialize() {
405 let metrics = ReportMetrics {
406 total_requests: 1000,
407 successful_requests: 980,
408 failed_requests: 20,
409 avg_latency_ms: 100.0,
410 p95_latency_ms: 200.0,
411 p99_latency_ms: 300.0,
412 error_rate: 0.02,
413 };
414
415 let json = serde_json::to_string(&metrics).unwrap();
416 assert!(json.contains("total_requests"));
417 assert!(json.contains("error_rate"));
418 }
419
420 #[test]
421 fn test_failure_detail_clone() {
422 let failure = FailureDetail {
423 step_name: "auth-step".to_string(),
424 error_message: "Connection timeout".to_string(),
425 timestamp: Utc::now(),
426 };
427
428 let cloned = failure.clone();
429 assert_eq!(failure.step_name, cloned.step_name);
430 assert_eq!(failure.error_message, cloned.error_message);
431 }
432
433 #[test]
434 fn test_failure_detail_serialize() {
435 let failure = FailureDetail {
436 step_name: "auth-step".to_string(),
437 error_message: "Connection timeout".to_string(),
438 timestamp: Utc::now(),
439 };
440
441 let json = serde_json::to_string(&failure).unwrap();
442 assert!(json.contains("step_name"));
443 assert!(json.contains("error_message"));
444 assert!(json.contains("timestamp"));
445 }
446
447 #[test]
448 fn test_pdf_with_failures() {
449 let config = PdfConfig::default();
450 let generator = PdfReportGenerator::new(config);
451
452 let mut report = create_test_report();
453 report.failures = vec![
454 FailureDetail {
455 step_name: "auth-step".to_string(),
456 error_message: "Connection timeout".to_string(),
457 timestamp: Utc::now(),
458 },
459 FailureDetail {
460 step_name: "data-step".to_string(),
461 error_message: "Invalid response".to_string(),
462 timestamp: Utc::now(),
463 },
464 ];
465 report.failed_steps = 2;
466
467 let temp_dir = tempdir().unwrap();
468 let output_path = temp_dir.path().join("report_with_failures.pdf");
469
470 let result = generator.generate(&report, output_path.to_str().unwrap());
471 assert!(result.is_ok());
472 assert!(output_path.exists());
473 }
474
475 #[test]
476 fn test_pdf_without_metrics() {
477 let config = PdfConfig {
478 include_metrics: false,
479 ..PdfConfig::default()
480 };
481 let generator = PdfReportGenerator::new(config);
482 let report = create_test_report();
483
484 let temp_dir = tempdir().unwrap();
485 let output_path = temp_dir.path().join("report_no_metrics.pdf");
486
487 let result = generator.generate(&report, output_path.to_str().unwrap());
488 assert!(result.is_ok());
489 }
490
491 #[test]
492 fn test_pdf_without_recommendations() {
493 let config = PdfConfig {
494 include_recommendations: false,
495 ..PdfConfig::default()
496 };
497 let generator = PdfReportGenerator::new(config);
498
499 let mut report = create_test_report();
500 report.recommendations = vec![];
501
502 let temp_dir = tempdir().unwrap();
503 let output_path = temp_dir.path().join("report_no_recs.pdf");
504
505 let result = generator.generate(&report, output_path.to_str().unwrap());
506 assert!(result.is_ok());
507 }
508
509 #[test]
510 fn test_pdf_generator_invalid_path() {
511 let config = PdfConfig::default();
512 let generator = PdfReportGenerator::new(config);
513 let report = create_test_report();
514
515 let result = generator.generate(&report, "/nonexistent/path/report.pdf");
516 assert!(result.is_err());
517 }
518
519 #[test]
520 fn test_pdf_config_debug() {
521 let config = PdfConfig::default();
522 let debug = format!("{:?}", config);
523 assert!(debug.contains("PdfConfig"));
524 }
525
526 #[test]
527 fn test_execution_report_debug() {
528 let report = create_test_report();
529 let debug = format!("{:?}", report);
530 assert!(debug.contains("ExecutionReport"));
531 }
532
533 #[test]
534 fn test_report_metrics_debug() {
535 let metrics = ReportMetrics {
536 total_requests: 1000,
537 successful_requests: 980,
538 failed_requests: 20,
539 avg_latency_ms: 100.0,
540 p95_latency_ms: 200.0,
541 p99_latency_ms: 300.0,
542 error_rate: 0.02,
543 };
544 let debug = format!("{:?}", metrics);
545 assert!(debug.contains("ReportMetrics"));
546 }
547}