1use std::collections::HashMap;
7use std::time::Duration;
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct TestMetrics {
15 pub total_requests: u64,
17 pub successful_requests: u64,
19 pub failed_requests: u64,
21 pub total_connections: u64,
23 pub peak_connections: u64,
25 pub avg_tps: u64,
27 pub peak_tps: u64,
29 pub p50_latency_ms: f64,
31 pub p95_latency_ms: f64,
33 pub p99_latency_ms: f64,
35 pub error_rate: f64,
37 pub memory_peak_mb: f64,
39}
40
41impl TestMetrics {
42 pub fn is_healthy(&self) -> bool {
44 self.error_rate < 0.05 && self.p99_latency_ms < 100.0
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct TestSummary {
51 pub name: String,
53 pub description: String,
55 pub passed: bool,
57 #[serde(with = "humantime_serde")]
59 pub duration: Duration,
60 pub start_time: DateTime<Utc>,
62 pub end_time: DateTime<Utc>,
64 pub targets_checked: usize,
66 pub targets_passed: usize,
68 pub failure_reasons: Vec<String>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct TestReport {
75 pub metadata: ReportMetadata,
77 pub summary: TestSummary,
79 pub metrics: TestMetrics,
81 pub target_results: Vec<TargetResultEntry>,
83 pub timeline: Vec<TimelineEvent>,
85 pub notes: Vec<String>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct ReportMetadata {
92 pub version: String,
94 pub generated_at: DateTime<Utc>,
96 pub environment: String,
98 pub host_info: HostInfo,
100 pub config: HashMap<String, String>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct HostInfo {
107 pub os: String,
109 pub cpu_count: usize,
111 pub total_memory_bytes: u64,
113 pub hostname: String,
115}
116
117impl HostInfo {
118 pub fn collect() -> Self {
120 Self {
121 os: std::env::consts::OS.to_string(),
122 cpu_count: num_cpus::get(),
123 total_memory_bytes: Self::get_total_memory(),
124 hostname: hostname::get()
125 .map(|h| h.to_string_lossy().to_string())
126 .unwrap_or_else(|_| "unknown".to_string()),
127 }
128 }
129
130 #[cfg(target_os = "linux")]
131 fn get_total_memory() -> u64 {
132 std::fs::read_to_string("/proc/meminfo")
133 .ok()
134 .and_then(|s| {
135 s.lines()
136 .find(|l| l.starts_with("MemTotal:"))
137 .and_then(|l| {
138 l.split_whitespace()
139 .nth(1)
140 .and_then(|v| v.parse::<u64>().ok())
141 .map(|kb| kb * 1024)
142 })
143 })
144 .unwrap_or(0)
145 }
146
147 #[cfg(not(target_os = "linux"))]
148 fn get_total_memory() -> u64 {
149 0
151 }
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct TargetResultEntry {
157 pub name: String,
159 pub expected: String,
161 pub actual: String,
163 pub passed: bool,
165 pub comparison: String,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct TimelineEvent {
172 #[serde(with = "humantime_serde")]
174 pub timestamp: Duration,
175 pub event_type: EventType,
177 pub message: String,
179 pub metrics: Option<HashMap<String, f64>>,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185#[serde(rename_all = "snake_case")]
186pub enum EventType {
187 TestStart,
188 TestEnd,
189 RampUpComplete,
190 TargetReached,
191 TargetMissed,
192 Error,
193 Warning,
194 Milestone,
195 Snapshot,
196}
197
198impl TestReport {
199 pub fn builder(name: impl Into<String>) -> TestReportBuilder {
201 TestReportBuilder::new(name)
202 }
203
204 pub fn to_json(&self) -> Result<String, serde_json::Error> {
206 serde_json::to_string_pretty(self)
207 }
208
209 pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
211 serde_yaml::to_string(self)
212 }
213
214 pub fn to_markdown(&self) -> String {
216 let mut md = String::new();
217
218 md.push_str(&format!("# Test Report: {}\n\n", self.summary.name));
219 md.push_str(&format!("**Description:** {}\n\n", self.summary.description));
220 md.push_str(&format!("**Status:** {}\n\n", if self.summary.passed { "PASSED" } else { "FAILED" }));
221
222 md.push_str("## Summary\n\n");
223 md.push_str(&format!("| Metric | Value |\n"));
224 md.push_str(&format!("|--------|-------|\n"));
225 md.push_str(&format!("| Duration | {:?} |\n", self.summary.duration));
226 md.push_str(&format!("| Start Time | {} |\n", self.summary.start_time));
227 md.push_str(&format!("| End Time | {} |\n", self.summary.end_time));
228 md.push_str(&format!("| Targets Passed | {}/{} |\n", self.summary.targets_passed, self.summary.targets_checked));
229
230 md.push_str("\n## Metrics\n\n");
231 md.push_str(&format!("| Metric | Value |\n"));
232 md.push_str(&format!("|--------|-------|\n"));
233 md.push_str(&format!("| Total Requests | {} |\n", self.metrics.total_requests));
234 md.push_str(&format!("| Successful | {} |\n", self.metrics.successful_requests));
235 md.push_str(&format!("| Failed | {} |\n", self.metrics.failed_requests));
236 md.push_str(&format!("| Avg TPS | {} |\n", self.metrics.avg_tps));
237 md.push_str(&format!("| Peak TPS | {} |\n", self.metrics.peak_tps));
238 md.push_str(&format!("| P50 Latency | {:.2}ms |\n", self.metrics.p50_latency_ms));
239 md.push_str(&format!("| P95 Latency | {:.2}ms |\n", self.metrics.p95_latency_ms));
240 md.push_str(&format!("| P99 Latency | {:.2}ms |\n", self.metrics.p99_latency_ms));
241 md.push_str(&format!("| Error Rate | {:.2}% |\n", self.metrics.error_rate * 100.0));
242 md.push_str(&format!("| Peak Memory | {:.2}MB |\n", self.metrics.memory_peak_mb));
243
244 md.push_str("\n## Target Results\n\n");
245 md.push_str(&format!("| Target | Expected | Actual | Status |\n"));
246 md.push_str(&format!("|--------|----------|--------|--------|\n"));
247 for target in &self.target_results {
248 let status = if target.passed { "PASSED" } else { "FAILED" };
249 md.push_str(&format!("| {} | {} {} | {} | {} |\n",
250 target.name, target.comparison, target.expected, target.actual, status));
251 }
252
253 if !self.summary.failure_reasons.is_empty() {
254 md.push_str("\n## Failure Reasons\n\n");
255 for reason in &self.summary.failure_reasons {
256 md.push_str(&format!("- {}\n", reason));
257 }
258 }
259
260 md.push_str("\n## Environment\n\n");
261 md.push_str(&format!("- **OS:** {}\n", self.metadata.host_info.os));
262 md.push_str(&format!("- **CPUs:** {}\n", self.metadata.host_info.cpu_count));
263 md.push_str(&format!("- **Memory:** {:.2}GB\n",
264 self.metadata.host_info.total_memory_bytes as f64 / (1024.0 * 1024.0 * 1024.0)));
265 md.push_str(&format!("- **Host:** {}\n", self.metadata.host_info.hostname));
266
267 md
268 }
269
270 pub fn save(&self, path: &str) -> std::io::Result<()> {
272 let content = if path.ends_with(".json") {
273 self.to_json().map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?
274 } else if path.ends_with(".yaml") || path.ends_with(".yml") {
275 self.to_yaml().map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?
276 } else if path.ends_with(".md") {
277 self.to_markdown()
278 } else {
279 return Err(std::io::Error::new(
280 std::io::ErrorKind::InvalidInput,
281 "Unsupported file format",
282 ));
283 };
284
285 std::fs::write(path, content)
286 }
287}
288
289pub struct TestReportBuilder {
291 name: String,
292 description: String,
293 environment: String,
294 config: HashMap<String, String>,
295 start_time: DateTime<Utc>,
296 target_results: Vec<TargetResultEntry>,
297 timeline: Vec<TimelineEvent>,
298 notes: Vec<String>,
299}
300
301impl TestReportBuilder {
302 pub fn new(name: impl Into<String>) -> Self {
304 Self {
305 name: name.into(),
306 description: String::new(),
307 environment: "development".to_string(),
308 config: HashMap::new(),
309 start_time: Utc::now(),
310 target_results: Vec::new(),
311 timeline: Vec::new(),
312 notes: Vec::new(),
313 }
314 }
315
316 pub fn description(mut self, desc: impl Into<String>) -> Self {
318 self.description = desc.into();
319 self
320 }
321
322 pub fn environment(mut self, env: impl Into<String>) -> Self {
324 self.environment = env.into();
325 self
326 }
327
328 pub fn config(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
330 self.config.insert(key.into(), value.into());
331 self
332 }
333
334 pub fn add_target_result(
336 mut self,
337 name: impl Into<String>,
338 expected: impl Into<String>,
339 actual: impl Into<String>,
340 comparison: impl Into<String>,
341 passed: bool,
342 ) -> Self {
343 self.target_results.push(TargetResultEntry {
344 name: name.into(),
345 expected: expected.into(),
346 actual: actual.into(),
347 comparison: comparison.into(),
348 passed,
349 });
350 self
351 }
352
353 pub fn add_event(
355 mut self,
356 timestamp: Duration,
357 event_type: EventType,
358 message: impl Into<String>,
359 ) -> Self {
360 self.timeline.push(TimelineEvent {
361 timestamp,
362 event_type,
363 message: message.into(),
364 metrics: None,
365 });
366 self
367 }
368
369 pub fn add_note(mut self, note: impl Into<String>) -> Self {
371 self.notes.push(note.into());
372 self
373 }
374
375 pub fn build(self, metrics: TestMetrics, passed: bool, duration: Duration) -> TestReport {
377 let end_time = Utc::now();
378 let targets_passed = self.target_results.iter().filter(|t| t.passed).count();
379
380 let failure_reasons: Vec<String> = self
381 .target_results
382 .iter()
383 .filter(|t| !t.passed)
384 .map(|t| format!("{}: expected {} {}, got {}", t.name, t.comparison, t.expected, t.actual))
385 .collect();
386
387 TestReport {
388 metadata: ReportMetadata {
389 version: "1.0.0".to_string(),
390 generated_at: end_time,
391 environment: self.environment,
392 host_info: HostInfo::collect(),
393 config: self.config,
394 },
395 summary: TestSummary {
396 name: self.name,
397 description: self.description,
398 passed,
399 duration,
400 start_time: self.start_time,
401 end_time,
402 targets_checked: self.target_results.len(),
403 targets_passed,
404 failure_reasons,
405 },
406 metrics,
407 target_results: self.target_results,
408 timeline: self.timeline,
409 notes: self.notes,
410 }
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417
418 #[test]
419 fn test_test_metrics_healthy() {
420 let healthy = TestMetrics {
421 total_requests: 10000,
422 successful_requests: 9900,
423 failed_requests: 100,
424 total_connections: 100,
425 peak_connections: 100,
426 avg_tps: 1000,
427 peak_tps: 1500,
428 p50_latency_ms: 5.0,
429 p95_latency_ms: 15.0,
430 p99_latency_ms: 30.0,
431 error_rate: 0.01,
432 memory_peak_mb: 256.0,
433 };
434 assert!(healthy.is_healthy());
435
436 let unhealthy = TestMetrics {
437 error_rate: 0.10,
438 ..healthy.clone()
439 };
440 assert!(!unhealthy.is_healthy());
441 }
442
443 #[test]
444 fn test_report_builder() {
445 let metrics = TestMetrics {
446 total_requests: 10000,
447 successful_requests: 9900,
448 failed_requests: 100,
449 total_connections: 100,
450 peak_connections: 100,
451 avg_tps: 1000,
452 peak_tps: 1500,
453 p50_latency_ms: 5.0,
454 p95_latency_ms: 15.0,
455 p99_latency_ms: 30.0,
456 error_rate: 0.01,
457 memory_peak_mb: 256.0,
458 };
459
460 let report = TestReport::builder("Test Run")
461 .description("Performance test")
462 .environment("test")
463 .config("connections", "100")
464 .add_target_result("Min TPS", "1000", "1000", ">=", true)
465 .add_note("Test completed successfully")
466 .build(metrics, true, Duration::from_secs(60));
467
468 assert!(report.summary.passed);
469 assert_eq!(report.summary.targets_passed, 1);
470 }
471
472 #[test]
473 fn test_report_to_markdown() {
474 let metrics = TestMetrics {
475 total_requests: 10000,
476 successful_requests: 9900,
477 failed_requests: 100,
478 total_connections: 100,
479 peak_connections: 100,
480 avg_tps: 1000,
481 peak_tps: 1500,
482 p50_latency_ms: 5.0,
483 p95_latency_ms: 15.0,
484 p99_latency_ms: 30.0,
485 error_rate: 0.01,
486 memory_peak_mb: 256.0,
487 };
488
489 let report = TestReport::builder("Test")
490 .build(metrics, true, Duration::from_secs(60));
491
492 let md = report.to_markdown();
493 assert!(md.contains("# Test Report: Test"));
494 assert!(md.contains("PASSED"));
495 }
496
497 #[test]
498 fn test_host_info_collect() {
499 let info = HostInfo::collect();
500 assert!(!info.os.is_empty());
501 assert!(info.cpu_count > 0);
502 }
503}