use super::*;
pub(crate) fn load_previous_coverage(out_dir: &Path) -> (Option<f64>, Option<usize>) {
let parent = match out_dir.parent() {
Some(p) => p,
None => return (None, None),
};
let prev_report = find_previous_report(parent, out_dir);
let json = match prev_report {
Some(content) => content,
None => return (None, None),
};
let coverage_ratio = json
.pointer("/regression/tests/coverage_ratio")
.and_then(|v| v.as_f64())
.or_else(|| {
json.pointer("/quality/coverage/heuristic_ratio")
.and_then(|v| v.as_f64())
});
let untested_count = json
.pointer("/regression/tests/untested_code_count")
.and_then(|v| v.as_u64())
.or_else(|| {
json.pointer("/regression/tests/untested_critical_count")
.and_then(|v| v.as_u64())
})
.map(|v| v as usize);
(coverage_ratio, untested_count)
}
pub(crate) fn find_previous_report(
branch_dir: &Path,
current_dir: &Path,
) -> Option<serde_json::Value> {
let latest = branch_dir.join("latest");
if latest.is_symlink()
&& let Ok(resolved) = fs::read_link(&latest)
{
let resolved_full = if resolved.is_relative() {
branch_dir.join(&resolved)
} else {
resolved
};
if resolved_full != *current_dir
&& let Ok(content) = fs::read_to_string(resolved_full.join("report.json"))
&& let Ok(json) = serde_json::from_str(&content)
{
return Some(json);
}
}
let current_name = current_dir.file_name()?.to_str()?;
let mut dirs: Vec<String> = fs::read_dir(branch_dir)
.ok()?
.filter_map(|e| e.ok())
.filter(|e| {
let name = e.file_name().to_string_lossy().to_string();
e.path().is_dir() && name != "latest" && name != current_name
})
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
dirs.sort();
let prev_name = dirs.iter().rev().find(|d| d.as_str() < current_name)?;
let prev_dir = branch_dir.join(prev_name);
let content = fs::read_to_string(prev_dir.join("report.json")).ok()?;
serde_json::from_str(&content).ok()
}
pub(crate) fn load_previous_run_delta(out_dir: &Path) -> Option<PreviousRunDelta> {
let parent = out_dir.parent()?;
let latest = parent.join("latest");
if !latest.is_symlink() {
return None;
}
let resolved = fs::read_link(&latest).ok()?;
let resolved_full = if resolved.is_relative() {
parent.join(&resolved)
} else {
resolved
};
if resolved_full == *out_dir {
return None;
}
let report_path = resolved_full.join("report.json");
let content = fs::read_to_string(&report_path).ok()?;
let json: serde_json::Value = serde_json::from_str(&content).ok()?;
let gate = json.get("gate")?;
let quality_pass_before = gate.get("quality_pass")?.as_bool()?;
let checks_arr = json.get("checks")?.as_array()?;
let checks_passed_before = checks_arr
.iter()
.filter(|c| c.get("status").and_then(|s| s.as_str()) == Some("PASS"))
.count();
let checks_failed_before = checks_arr
.iter()
.filter(|c| {
let s = c.get("status").and_then(|s| s.as_str()).unwrap_or("");
s == "FAIL" || s == "ERROR"
})
.count();
let findings_before = json
.get("quality")
.and_then(|q| q.get("sarif"))
.and_then(|s| s.get("findings_count"))
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let breaking_section = json.get("quality").and_then(|q| q.get("breaking_changes"));
let removed = breaking_section
.and_then(|b| b.get("removed_public_symbols_count"))
.and_then(|v| v.as_u64())
.unwrap_or(0);
let env = breaking_section
.and_then(|b| b.get("new_env_vars_count"))
.and_then(|v| v.as_u64())
.unwrap_or(0);
let breaking_before = (removed + env) as usize;
Some(PreviousRunDelta {
checks_passed_before,
checks_failed_before,
findings_before,
breaking_before,
quality_pass_before,
})
}
pub(crate) fn load_run_history(out_dir: &Path, max_runs: usize) -> Vec<HistoricalRun> {
let parent = match out_dir.parent() {
Some(p) => p,
None => return Vec::new(),
};
let entries = match fs::read_dir(parent) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
let mut dirs: Vec<String> = entries
.filter_map(|e| e.ok())
.filter(|e| {
let name = e.file_name();
let name_str = name.to_string_lossy();
name_str != "latest"
&& e.path().is_dir()
&& name_str.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false)
})
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
dirs.sort();
if dirs.len() > max_runs {
dirs = dirs.split_off(dirs.len() - max_runs);
}
let mut history: Vec<HistoricalRun> = dirs
.iter()
.filter_map(|dir_name| {
let report_path = parent.join(dir_name).join("report.json");
let content = fs::read_to_string(&report_path).ok()?;
let json: serde_json::Value = serde_json::from_str(&content).ok()?;
let gate = json.get("gate")?;
let quality_pass = gate.get("quality_pass")?.as_bool()?;
let checks_arr = json.get("checks")?.as_array()?;
let checks_passed = checks_arr
.iter()
.filter(|c| c.get("status").and_then(|s| s.as_str()) == Some("PASS"))
.count();
let checks_failed = checks_arr
.iter()
.filter(|c| {
let s = c.get("status").and_then(|s| s.as_str()).unwrap_or("");
s == "FAIL" || s == "ERROR"
})
.count();
let checks_warned = checks_arr
.iter()
.filter(|c| c.get("status").and_then(|s| s.as_str()) == Some("WARN"))
.count();
let findings_count = json
.get("quality")
.and_then(|q| q.get("sarif"))
.and_then(|s| s.get("findings_count"))
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
Some(HistoricalRun {
timestamp: dir_name.clone(),
checks_passed,
checks_failed,
checks_warned,
quality_pass,
findings_count,
})
})
.collect();
history.reverse();
history
}
pub(crate) fn compute_flaky_scores(out_dir: &Path, max_runs: usize) -> Vec<FlakyCheckScore> {
let parent = match out_dir.parent() {
Some(p) => p,
None => return Vec::new(),
};
let entries = match fs::read_dir(parent) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
let mut dirs: Vec<String> = entries
.filter_map(|e| e.ok())
.filter(|e| {
let name = e.file_name();
let name_str = name.to_string_lossy();
name_str != "latest"
&& e.path().is_dir()
&& name_str
.chars()
.next()
.map(|c| c.is_ascii_digit())
.unwrap_or(false)
})
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
dirs.sort();
if dirs.len() > max_runs {
dirs = dirs.split_off(dirs.len() - max_runs);
}
if dirs.len() < 2 {
return Vec::new();
}
let mut check_history: std::collections::BTreeMap<String, Vec<(String, String)>> =
std::collections::BTreeMap::new();
for dir_name in &dirs {
let report_path = parent.join(dir_name).join("report.json");
let content = match fs::read_to_string(&report_path) {
Ok(c) => c,
Err(_) => continue,
};
let json: serde_json::Value = match serde_json::from_str(&content) {
Ok(v) => v,
Err(_) => continue,
};
let checks_arr = match json.get("checks").and_then(|c| c.as_array()) {
Some(arr) => arr,
None => continue,
};
for check in checks_arr {
let id = match check.get("id").and_then(|v| v.as_str()) {
Some(s) => s.to_string(),
None => continue,
};
let status = match check.get("status").and_then(|v| v.as_str()) {
Some(s) => s.to_string(),
None => continue,
};
let name = check
.get("name")
.and_then(|v| v.as_str())
.unwrap_or(&id)
.to_string();
check_history.entry(id).or_default().push((name, status));
}
}
let mut scores: Vec<FlakyCheckScore> = check_history
.into_iter()
.filter_map(|(check_id, entries)| {
let total_runs = entries.len();
if total_runs < 2 {
return None;
}
let statuses: Vec<&str> = entries.iter().map(|(_, s)| s.as_str()).collect();
let transitions = statuses.windows(2).filter(|w| w[0] != w[1]).count();
if transitions == 0 {
return None;
}
let flaky_score = transitions as f64 / (total_runs - 1) as f64;
let last_statuses: Vec<String> = entries
.iter()
.rev()
.take(10)
.map(|(_, s)| s.clone())
.collect();
let check_name = entries.last().map(|(n, _)| n.clone()).unwrap_or_default();
let confidence = match total_runs {
0..=3 => "low",
4..=7 => "medium",
_ => "high",
};
Some(FlakyCheckScore {
check_id,
check_name,
flaky_score,
total_runs,
transitions,
last_statuses,
confidence,
})
})
.collect();
scores.sort_by(|a, b| {
b.flaky_score
.partial_cmp(&a.flaky_score)
.unwrap_or(std::cmp::Ordering::Equal)
});
scores
}