use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum TestError {
#[error("Setup failed: {0}")]
SetupFailed(String),
#[error("Teardown failed: {0}")]
TeardownFailed(String),
#[error("Test execution failed: {0}")]
ExecutionFailed(String),
#[error("HTTP request failed: {0}")]
HttpError(String),
#[error("Database error: {0}")]
DatabaseError(String),
#[error("Assertion failed: {0}")]
AssertionFailed(String),
#[error("Timeout: {0}")]
Timeout(String),
#[error("Configuration error: {0}")]
ConfigError(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestResult {
pub test_name: String,
pub success: bool,
pub details: String,
pub duration_seconds: f64,
pub input: serde_json::Value,
pub output: serde_json::Value,
pub timestamp: DateTime<Utc>,
}
impl TestResult {
pub fn success(test_name: impl Into<String>, details: impl Into<String>) -> Self {
Self {
test_name: test_name.into(),
success: true,
details: details.into(),
duration_seconds: 0.0,
input: serde_json::Value::Null,
output: serde_json::Value::Null,
timestamp: Utc::now(),
}
}
pub fn failure(test_name: impl Into<String>, details: impl Into<String>) -> Self {
Self {
test_name: test_name.into(),
success: false,
details: details.into(),
duration_seconds: 0.0,
input: serde_json::Value::Null,
output: serde_json::Value::Null,
timestamp: Utc::now(),
}
}
pub fn with_duration(mut self, seconds: f64) -> Self {
self.duration_seconds = seconds;
self
}
pub fn with_input(mut self, input: serde_json::Value) -> Self {
self.input = input;
self
}
pub fn with_output(mut self, output: serde_json::Value) -> Self {
self.output = output;
self
}
}
impl fmt::Display for TestResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let status = if self.success { "✓" } else { "✗" };
write!(
f,
"{} {} ({:.3}s): {}",
status, self.test_name, self.duration_seconds, self.details
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestSuiteResult {
pub suite_name: String,
pub start_time: DateTime<Utc>,
pub end_time: Option<DateTime<Utc>>,
pub results: Vec<TestResult>,
}
impl TestSuiteResult {
pub fn new(suite_name: impl Into<String>) -> Self {
Self {
suite_name: suite_name.into(),
start_time: Utc::now(),
end_time: None,
results: Vec::new(),
}
}
pub fn add_result(&mut self, result: TestResult) {
self.results.push(result);
}
pub fn complete(&mut self) {
self.end_time = Some(Utc::now());
}
pub fn passed_count(&self) -> usize {
self.results.iter().filter(|r| r.success).count()
}
pub fn failed_count(&self) -> usize {
self.results.iter().filter(|r| !r.success).count()
}
pub fn total_count(&self) -> usize {
self.results.len()
}
pub fn success_rate(&self) -> f64 {
if self.results.is_empty() {
return 0.0;
}
(self.passed_count() as f64 / self.results.len() as f64) * 100.0
}
pub fn total_duration(&self) -> f64 {
self.results.iter().map(|r| r.duration_seconds).sum()
}
pub fn all_passed(&self) -> bool {
self.failed_count() == 0 && !self.results.is_empty()
}
pub fn failed_tests(&self) -> Vec<&TestResult> {
self.results.iter().filter(|r| !r.success).collect()
}
pub fn save_to_file(&self, path: &std::path::Path) -> Result<(), std::io::Error> {
let json = serde_json::to_string_pretty(self)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
std::fs::write(path, json)
}
}
impl fmt::Display for TestSuiteResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\n══════════════════════════════════════════════════════════")?;
writeln!(f, "Test Suite: {}", self.suite_name)?;
writeln!(f, "══════════════════════════════════════════════════════════")?;
for result in &self.results {
writeln!(f, " {}", result)?;
}
writeln!(f, "──────────────────────────────────────────────────────────")?;
writeln!(
f,
"Results: {} passed, {} failed, {} total ({:.1}%)",
self.passed_count(),
self.failed_count(),
self.total_count(),
self.success_rate()
)?;
writeln!(f, "Duration: {:.3}s", self.total_duration())?;
writeln!(f, "══════════════════════════════════════════════════════════")?;
Ok(())
}
}
#[async_trait]
pub trait Test: Send + Sync {
fn name(&self) -> &str;
async fn setup(&mut self) -> Result<(), TestError>;
async fn teardown(&mut self) -> Result<(), TestError>;
async fn run_tests(&mut self) -> Vec<TestResult>;
async fn execute(&mut self) -> TestSuiteResult {
let mut suite = TestSuiteResult::new(self.name());
if let Err(e) = self.setup().await {
suite.add_result(TestResult::failure(
format!("{} - Setup", self.name()),
format!("Setup failed: {}", e),
));
suite.complete();
return suite;
}
let results = self.run_tests().await;
for result in results {
suite.add_result(result);
}
if let Err(e) = self.teardown().await {
suite.add_result(TestResult::failure(
format!("{} - Teardown", self.name()),
format!("Teardown failed: {}", e),
));
}
suite.complete();
suite
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_result_success() {
let result = TestResult::success("Test 1", "Passed successfully");
assert!(result.success);
assert_eq!(result.test_name, "Test 1");
}
#[test]
fn test_result_failure() {
let result = TestResult::failure("Test 2", "Failed with error");
assert!(!result.success);
assert_eq!(result.details, "Failed with error");
}
#[test]
fn test_suite_statistics() {
let mut suite = TestSuiteResult::new("Test Suite");
suite.add_result(TestResult::success("Test 1", "OK"));
suite.add_result(TestResult::success("Test 2", "OK"));
suite.add_result(TestResult::failure("Test 3", "Failed"));
assert_eq!(suite.passed_count(), 2);
assert_eq!(suite.failed_count(), 1);
assert_eq!(suite.total_count(), 3);
}
}