use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssertionResult {
pub passed: bool,
pub message: Option<String>,
pub expected: Option<String>,
pub actual: Option<String>,
pub error: Option<String>,
}
impl AssertionResult {
pub fn passed() -> Self {
Self {
passed: true,
message: None,
expected: None,
actual: None,
error: None,
}
}
pub fn failed(message: &str, expected: String, actual: String) -> Self {
Self {
passed: false,
message: Some(message.to_string()),
expected: Some(expected),
actual: Some(actual),
error: None,
}
}
pub fn error(error: &str) -> Self {
Self {
passed: false,
message: None,
expected: None,
actual: None,
error: Some(error.to_string()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestResult {
pub name: String,
pub description: Option<String>,
pub status: TestStatus,
pub duration_ms: u64,
pub request_count: usize,
pub assertions: AssertionSummary,
pub failures: Vec<TestFailure>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TestStatus {
Passed,
Failed,
Skipped,
Error,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AssertionSummary {
pub total: usize,
pub passed: usize,
pub failed: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestFailure {
pub request_index: usize,
pub assertion: String,
pub expected: Option<String>,
pub actual: Option<String>,
pub message: Option<String>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestSuiteResult {
pub suite: Option<String>,
pub timestamp: String,
pub duration_ms: u64,
pub summary: SuiteSummary,
pub tests: Vec<TestResult>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SuiteSummary {
pub total: usize,
pub passed: usize,
pub failed: usize,
pub skipped: usize,
pub errors: usize,
}
impl TestSuiteResult {
pub fn new(suite_name: Option<String>) -> Self {
Self {
suite: suite_name,
timestamp: chrono_lite_timestamp(),
duration_ms: 0,
summary: SuiteSummary::default(),
tests: Vec::new(),
}
}
pub fn add_result(&mut self, result: TestResult) {
match result.status {
TestStatus::Passed => self.summary.passed += 1,
TestStatus::Failed => self.summary.failed += 1,
TestStatus::Skipped => self.summary.skipped += 1,
TestStatus::Error => self.summary.errors += 1,
}
self.summary.total += 1;
self.tests.push(result);
}
pub fn set_duration(&mut self, duration_ms: u64) {
self.duration_ms = duration_ms;
}
pub fn all_passed(&self) -> bool {
self.summary.failed == 0 && self.summary.errors == 0
}
}
fn chrono_lite_timestamp() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let secs = duration.as_secs();
let days_since_epoch = secs / 86400;
let time_of_day = secs % 86400;
let hours = time_of_day / 3600;
let minutes = (time_of_day % 3600) / 60;
let seconds = time_of_day % 60;
let mut year = 1970;
let mut remaining_days = days_since_epoch as i64;
loop {
let days_in_year = if is_leap_year(year) { 366 } else { 365 };
if remaining_days < days_in_year {
break;
}
remaining_days -= days_in_year;
year += 1;
}
let days_in_months: [i64; 12] = if is_leap_year(year) {
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
} else {
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
};
let mut month = 1;
for &days in &days_in_months {
if remaining_days < days {
break;
}
remaining_days -= days;
month += 1;
}
let day = remaining_days + 1;
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
year, month, day, hours, minutes, seconds
)
}
fn is_leap_year(year: i64) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}