use std::time::SystemTime;
use crate::core::*;
fn cache_dir() -> std::path::PathBuf {
let base = std::env::var("CARGO_MANIFEST_DIR")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| std::env::current_dir().unwrap_or_default());
base.join("target/.rvtest-cache")
}
fn last_run_cache_path() -> std::path::PathBuf {
cache_dir().join("failed.json")
}
fn last_run_snapshot_path() -> std::path::PathBuf {
cache_dir().join("last-run.json")
}
fn flaky_cache_path() -> std::path::PathBuf {
cache_dir().join("flaky.json")
}
#[derive(serde::Serialize, serde::Deserialize)]
pub(crate) struct CachedRun {
suites: Vec<CachedSuite>,
duration_secs: f64,
start_time_secs: u64,
end_time_secs: u64,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct CachedSuite {
name: String,
tests: Vec<CachedTest>,
duration_secs: f64,
kind: String,
source_path: String,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct CachedTest {
name: String,
suite: Option<String>,
tags: Vec<String>,
status: String,
reason: Option<String>,
duration_secs: f64,
location_file: Option<String>,
location_line: Option<u32>,
location_column: Option<u32>,
bench_threshold_secs: Option<f64>,
bench_iterations: Option<u32>,
bench_mean_secs: Option<f64>,
bench_min_secs: Option<f64>,
bench_max_secs: Option<f64>,
}
impl From<&TestRun> for CachedRun {
fn from(run: &TestRun) -> Self {
CachedRun {
suites: run.suites.iter().map(|s| CachedSuite::from(s)).collect(),
duration_secs: run.duration.as_secs_f64(),
start_time_secs: run.start_time.duration_since(SystemTime::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0),
end_time_secs: run.end_time.duration_since(SystemTime::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0),
}
}
}
impl From<&TestSuite> for CachedSuite {
fn from(suite: &TestSuite) -> Self {
CachedSuite {
name: suite.name.clone(),
tests: suite.tests.iter().map(|t| CachedTest::from(t)).collect(),
duration_secs: suite.duration.as_secs_f64(),
kind: match suite.kind {
TestKind::Unit => "unit".into(),
TestKind::Integration => "integration".into(),
TestKind::Doc => "doc".into(),
},
source_path: suite.source_path.clone(),
}
}
}
impl From<&TestCase> for CachedTest {
fn from(test: &TestCase) -> Self {
let (status, reason) = match &test.status {
TestStatus::Passed => ("passed", None),
TestStatus::Failed { reason: r, .. } => ("failed", Some(r.clone())),
TestStatus::Skipped { reason: r } => ("skipped", r.clone()),
TestStatus::TimedOut { .. } => ("timed_out", None),
};
CachedTest {
name: test.name.clone(),
suite: test.suite.clone(),
tags: test.tags.clone(),
status: status.into(),
reason,
duration_secs: test.duration.as_secs_f64(),
location_file: test.location.as_ref().map(|l| l.file.clone()),
location_line: test.location.as_ref().map(|l| l.line),
location_column: test.location.as_ref().and_then(|l| l.column),
bench_threshold_secs: test.bench_threshold.map(|d| d.as_secs_f64()),
bench_iterations: test.bench_stats.as_ref().map(|s| s.iterations),
bench_mean_secs: test.bench_stats.as_ref().map(|s| s.mean.as_secs_f64()),
bench_min_secs: test.bench_stats.as_ref().map(|s| s.min.as_secs_f64()),
bench_max_secs: test.bench_stats.as_ref().map(|s| s.max.as_secs_f64()),
}
}
}
impl CachedRun {
fn diff(&self, previous: &CachedRun) -> RunDiff {
let mut new_failures = Vec::new();
let mut recovered = Vec::new();
let mut slower = Vec::new();
let mut faster = Vec::new();
let prev_map: std::collections::HashMap<&str, (&CachedSuite, &CachedTest)> = previous
.suites.iter()
.flat_map(|s| s.tests.iter().map(move |t| (t.name.as_str(), (s, t))))
.collect();
for suite in &self.suites {
for test in &suite.tests {
if test.status == "failed" || test.status == "timed_out" {
match prev_map.get(test.name.as_str()) {
Some((_, prev)) if prev.status == "passed" => {
new_failures.push(test.name.clone());
}
Some((_, prev)) if prev.status == "failed" || prev.status == "timed_out" => {
let change = test.duration_secs - prev.duration_secs;
if change > 0.5 {
slower.push((test.name.clone(), prev.duration_secs, test.duration_secs));
}
}
_ => {
new_failures.push(test.name.clone());
}
}
} else if test.status == "passed" {
match prev_map.get(test.name.as_str()) {
Some((_, prev)) if prev.status == "failed" || prev.status == "timed_out" => {
recovered.push(test.name.clone());
}
Some((_, prev)) => {
let change = prev.duration_secs - test.duration_secs;
if change > 0.5 {
faster.push((test.name.clone(), prev.duration_secs, test.duration_secs));
}
}
_ => {}
}
}
}
}
RunDiff { new_failures, recovered, slower, faster }
}
}
#[derive(Debug, Default)]
pub struct RunDiff {
pub new_failures: Vec<String>,
pub recovered: Vec<String>,
pub slower: Vec<(String, f64, f64)>,
pub faster: Vec<(String, f64, f64)>,
}
impl RunDiff {
pub fn has_changes(&self) -> bool {
!self.new_failures.is_empty() || !self.recovered.is_empty() || !self.slower.is_empty()
}
}
pub fn save_full_run(run: &TestRun) {
let cached: CachedRun = CachedRun::from(run);
let path = last_run_snapshot_path();
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let json = serde_json::to_string_pretty(&cached).expect("serialize full run");
let _ = std::fs::write(&path, &json);
}
pub(crate) fn load_previous_run() -> Option<CachedRun> {
let path = last_run_snapshot_path();
let content = std::fs::read_to_string(&path).ok()?;
serde_json::from_str(&content).ok()
}
pub fn compute_diff(run: &TestRun) -> RunDiff {
match load_previous_run() {
Some(prev) => {
let current = CachedRun::from(run);
current.diff(&prev)
}
None => RunDiff::default(),
}
}
pub fn save_failed_tests(run: &TestRun) {
let path = last_run_cache_path();
let failed: Vec<String> = run
.suites
.iter()
.flat_map(|s| s.tests.iter())
.filter(|t| t.status.is_failed())
.map(|t| t.name.clone())
.collect();
if failed.is_empty() {
let _ = std::fs::remove_file(&path);
return;
}
let json = serde_json::to_string(&failed).expect("serialize failed tests");
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(&path, &json);
}
pub fn load_failed_tests() -> Vec<String> {
let path = last_run_cache_path();
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => return Vec::new(),
};
serde_json::from_str(&content).unwrap_or_default()
}
pub fn save_flaky_tests(tests: &[String]) {
let path = flaky_cache_path();
if tests.is_empty() {
let _ = std::fs::remove_file(&path);
return;
}
let json = serde_json::to_string(tests).expect("serialize flaky tests");
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(&path, &json);
}
pub fn load_flaky_tests() -> Vec<String> {
let path = flaky_cache_path();
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => return Vec::new(),
};
serde_json::from_str(&content).unwrap_or_default()
}
fn baseline_cache_path() -> std::path::PathBuf {
cache_dir().join("baseline.json")
}
pub fn save_bench_baseline(run: &TestRun) {
use std::collections::HashMap;
let mut baseline: HashMap<String, serde_json::Value> = HashMap::new();
for suite in &run.suites {
for test in &suite.tests {
if let Some(ref stats) = test.bench_stats {
let entry = serde_json::json!({
"mean_secs": stats.mean.as_secs_f64(),
"min_secs": stats.min.as_secs_f64(),
"max_secs": stats.max.as_secs_f64(),
"iterations": stats.iterations,
});
baseline.insert(test.name.clone(), entry);
}
}
}
let path = baseline_cache_path();
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let json = serde_json::to_string_pretty(&baseline).expect("serialize baseline");
let _ = std::fs::write(&path, &json);
}
pub fn load_bench_baseline() -> std::collections::HashMap<String, serde_json::Value> {
let path = baseline_cache_path();
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => return std::collections::HashMap::new(),
};
serde_json::from_str(&content).unwrap_or_default()
}