use std::fmt;
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceLocation {
pub file: String,
pub line: u32,
pub column: Option<u32>,
}
impl fmt::Display for SourceLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.column {
Some(col) => write!(f, "{}:{}:{}", self.file, self.line, col),
None => write!(f, "{}:{}", self.file, self.line),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TestStatus {
Passed,
Failed {
reason: String,
location: Option<SourceLocation>,
},
Skipped {
reason: Option<String>,
},
TimedOut {
duration: Duration,
location: Option<SourceLocation>,
},
}
impl TestStatus {
pub fn is_passed(&self) -> bool {
matches!(self, TestStatus::Passed)
}
pub fn is_failed(&self) -> bool {
matches!(self, TestStatus::Failed { .. } | TestStatus::TimedOut { .. })
}
pub fn is_skipped(&self) -> bool {
matches!(self, TestStatus::Skipped { .. })
}
}
impl fmt::Display for TestStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TestStatus::Passed => write!(f, "PASSED"),
TestStatus::Failed { reason, .. } => write!(f, "FAILED: {reason}"),
TestStatus::Skipped { reason: Some(r) } => write!(f, "SKIPPED: {r}"),
TestStatus::Skipped { reason: None } => write!(f, "SKIPPED"),
TestStatus::TimedOut { duration, .. } => {
write!(f, "TIMED OUT after {duration:?}")
}
}
}
}
#[derive(Debug, Clone)]
pub struct TestCase {
pub name: String,
pub suite: Option<String>,
pub tags: Vec<String>,
pub status: TestStatus,
pub duration: Duration,
pub assertions: u64,
pub location: Option<SourceLocation>,
pub parameters: Vec<(String, String)>,
}
impl TestCase {
pub fn new(name: impl Into<String>) -> Self {
TestCase {
name: name.into(),
suite: None,
tags: Vec::new(),
status: TestStatus::Passed,
duration: Duration::ZERO,
assertions: 0,
location: None,
parameters: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct TestSuite {
pub name: String,
pub description: Option<String>,
pub tests: Vec<TestCase>,
pub duration: Duration,
}
impl TestSuite {
pub fn new(name: impl Into<String>) -> Self {
TestSuite {
name: name.into(),
description: None,
tests: Vec::new(),
duration: Duration::ZERO,
}
}
pub fn len(&self) -> usize {
self.tests.len()
}
pub fn is_empty(&self) -> bool {
self.tests.is_empty()
}
pub fn passed(&self) -> impl Iterator<Item = &TestCase> {
self.tests.iter().filter(|t| t.status.is_passed())
}
pub fn failed(&self) -> impl Iterator<Item = &TestCase> {
self.tests.iter().filter(|t| t.status.is_failed())
}
pub fn skipped(&self) -> impl Iterator<Item = &TestCase> {
self.tests.iter().filter(|t| t.status.is_skipped())
}
pub fn success(&self) -> bool {
self.failed().count() == 0
}
pub fn assert_all_pass(&self) {
let failed: Vec<&TestCase> = self.failed().collect();
if !failed.is_empty() {
let mut msg = format!(
"{} test(s) failed in suite '{}':\n",
failed.len(),
self.name,
);
for t in &failed {
let dur_ms = t.duration.as_secs_f64() * 1000.0;
let reason = match &t.status {
TestStatus::Failed { reason, .. } => reason.as_str(),
TestStatus::TimedOut { .. } => "timed out",
_ => "unknown",
};
msg.push_str(&format!(" ✗ {} [{dur_ms:.1}ms] — {reason}\n", t.name));
}
panic!("{msg}");
}
}
}
#[derive(Debug, Clone)]
pub struct TestRun {
pub suites: Vec<TestSuite>,
pub start_time: SystemTime,
pub end_time: SystemTime,
pub duration: Duration,
}
impl TestRun {
pub fn new() -> Self {
TestRun {
suites: Vec::new(),
start_time: SystemTime::now(),
end_time: SystemTime::now(),
duration: Duration::ZERO,
}
}
pub fn total(&self) -> usize {
self.suites.iter().map(|s| s.tests.len()).sum()
}
pub fn total_passed(&self) -> usize {
self.suites.iter().flat_map(|s| s.tests.iter()).filter(|t| t.status.is_passed()).count()
}
pub fn total_failed(&self) -> usize {
self.suites.iter().flat_map(|s| s.tests.iter()).filter(|t| t.status.is_failed()).count()
}
pub fn total_skipped(&self) -> usize {
self.suites.iter().flat_map(|s| s.tests.iter()).filter(|t| t.status.is_skipped()).count()
}
pub fn success(&self) -> bool {
self.total_failed() == 0
}
pub fn all_failed(&self) -> impl Iterator<Item = &TestCase> {
self.suites.iter().flat_map(|s| s.failed())
}
}
impl Default for TestRun {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ReportFormat {
#[default]
Pretty,
Tap,
Junit,
Json,
Compact,
}
impl std::str::FromStr for ReportFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"pretty" | "human" => Ok(Self::Pretty),
"tap" => Ok(Self::Tap),
"junit" | "xml" => Ok(Self::Junit),
"json" => Ok(Self::Json),
"compact" => Ok(Self::Compact),
_ => Err(format!("unknown report format: {s}")),
}
}
}
#[derive(Debug, Clone)]
pub struct RunnerConfig {
pub filter: Option<String>,
pub include_tags: Vec<String>,
pub exclude_tags: Vec<String>,
pub default_retries: u32,
pub default_timeout: Option<Duration>,
pub parallel: bool,
pub max_threads: usize,
pub format: ReportFormat,
pub fail_fast: bool,
pub seed: Option<u64>,
pub verbose: bool,
}
impl Default for RunnerConfig {
fn default() -> Self {
RunnerConfig {
filter: None,
include_tags: Vec::new(),
exclude_tags: Vec::new(),
default_retries: 0,
default_timeout: None,
parallel: true,
max_threads: num_cpus(),
format: ReportFormat::Pretty,
fail_fast: false,
seed: None,
verbose: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CoverageFormat {
#[default]
Summary,
Html,
Lcov,
Json,
Cobertura,
}
impl std::str::FromStr for CoverageFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"summary" | "text" => Ok(Self::Summary),
"html" => Ok(Self::Html),
"lcov" | "tracefile" => Ok(Self::Lcov),
"json" => Ok(Self::Json),
"cobertura" | "xml" => Ok(Self::Cobertura),
_ => Err(format!("unknown coverage format: {s}")),
}
}
}
#[derive(Debug, Clone)]
pub struct CoverageReport {
pub line_coverage: f64,
pub function_coverage: f64,
pub region_coverage: f64,
pub format: CoverageFormat,
pub report_path: Option<std::path::PathBuf>,
}
fn num_cpus() -> usize {
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4)
}