use crate::types::{OptResult, OptimizationError, TestId, TestType};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct MetadataCollector {
metadata_dir: PathBuf,
}
impl MetadataCollector {
pub fn new<P: AsRef<Path>>(metadata_dir: P) -> Self {
Self {
metadata_dir: metadata_dir.as_ref().to_path_buf(),
}
}
pub fn with_defaults() -> Self {
Self::new(".ggen/test-metadata")
}
fn ensure_metadata_dir(&self) -> OptResult<()> {
fs::create_dir_all(&self.metadata_dir)?;
Ok(())
}
pub fn collect_execution_times<P: AsRef<Path>>(
&self, nextest_json_path: P,
) -> OptResult<HashMap<TestId, (TestType, u64)>> {
let json_path = nextest_json_path.as_ref();
if !json_path.exists() {
return Err(OptimizationError::IoError(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Nextest JSON not found: {}", json_path.display()),
)));
}
let content = fs::read_to_string(json_path)?;
let report: NextestReport = serde_json::from_str(&content)?;
let mut execution_times = HashMap::new();
for (test_name, test_result) in report.test_results.iter() {
let test_id = TestId::new(test_name)?;
let test_type = self.infer_test_type(test_name);
let exec_time_ms = test_result.exec_time_ms;
execution_times.insert(test_id, (test_type, exec_time_ms));
}
Ok(execution_times)
}
pub fn collect_coverage_data<P: AsRef<Path>>(
&self, tarpaulin_json_path: P,
) -> OptResult<HashMap<TestId, (usize, usize)>> {
let json_path = tarpaulin_json_path.as_ref();
if !json_path.exists() {
return Err(OptimizationError::IoError(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Tarpaulin JSON not found: {}", json_path.display()),
)));
}
let content = fs::read_to_string(json_path)?;
let report: TarpaulinReport = serde_json::from_str(&content)?;
let mut coverage_data = HashMap::new();
for (test_name, coverage) in report.test_coverage.iter() {
let test_id = TestId::new(test_name)?;
coverage_data.insert(test_id, (coverage.lines_covered, coverage.total_lines));
}
Ok(coverage_data)
}
pub fn collect_failure_history(&self) -> OptResult<HashMap<TestId, (u32, u32)>> {
let history_path = self.metadata_dir.join("failure_history.json");
if !history_path.exists() {
return Ok(HashMap::new());
}
let content = fs::read_to_string(&history_path)?;
let history: FailureHistory = serde_json::from_str(&content)?;
let mut failure_data = HashMap::new();
for (test_name, stats) in history.test_stats.iter() {
let test_id = TestId::new(test_name)?;
failure_data.insert(test_id, (stats.failure_count, stats.total_runs));
}
Ok(failure_data)
}
pub fn update_failure_history(&self, test_results: &HashMap<TestId, bool>) -> OptResult<()> {
self.ensure_metadata_dir()?;
let history_path = self.metadata_dir.join("failure_history.json");
let mut history = if history_path.exists() {
let content = fs::read_to_string(&history_path)?;
serde_json::from_str::<FailureHistory>(&content)?
} else {
FailureHistory {
test_stats: HashMap::new(),
}
};
for (test_id, passed) in test_results {
let stats = history
.test_stats
.entry(test_id.as_str().to_string())
.or_insert(TestStats {
failure_count: 0,
total_runs: 0,
});
stats.total_runs += 1;
if !passed {
stats.failure_count += 1;
}
}
let json = serde_json::to_string_pretty(&history)?;
fs::write(&history_path, json)?;
Ok(())
}
fn infer_test_type(&self, test_name: &str) -> TestType {
if test_name.contains("integration") || test_name.starts_with("tests::") {
TestType::Integration
} else {
TestType::Unit
}
}
pub fn collect_all_metadata<P1: AsRef<Path>, P2: AsRef<Path>>(
&self, nextest_json: P1, tarpaulin_json: P2,
) -> OptResult<TestMetadata> {
let execution_times = self.collect_execution_times(nextest_json)?;
let coverage_data = self.collect_coverage_data(tarpaulin_json)?;
let failure_history = self.collect_failure_history()?;
Ok(TestMetadata {
execution_times,
coverage_data,
failure_history,
})
}
}
#[derive(Debug)]
pub struct TestMetadata {
pub execution_times: HashMap<TestId, (TestType, u64)>,
pub coverage_data: HashMap<TestId, (usize, usize)>,
pub failure_history: HashMap<TestId, (u32, u32)>,
}
#[derive(Debug, Deserialize)]
struct NextestReport {
test_results: HashMap<String, TestResult>,
}
#[derive(Debug, Deserialize)]
struct TestResult {
#[serde(rename = "exec_time")]
exec_time_ms: u64,
}
#[derive(Debug, Deserialize)]
struct TarpaulinReport {
test_coverage: HashMap<String, CoverageData>,
}
#[derive(Debug, Deserialize)]
struct CoverageData {
lines_covered: usize,
total_lines: usize,
}
#[derive(Debug, Serialize, Deserialize)]
struct FailureHistory {
test_stats: HashMap<String, TestStats>,
}
#[derive(Debug, Serialize, Deserialize)]
struct TestStats {
failure_count: u32,
total_runs: u32,
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_temp_collector() -> (MetadataCollector, TempDir) {
let temp_dir = TempDir::new().unwrap();
let collector = MetadataCollector::new(temp_dir.path());
(collector, temp_dir)
}
#[test]
fn test_collector_creation() {
let (collector, temp) = create_temp_collector();
assert_eq!(collector.metadata_dir, temp.path());
}
#[test]
fn test_ensure_metadata_dir() {
let (collector, temp) = create_temp_collector();
assert!(!temp.path().exists() || temp.path().read_dir().unwrap().count() == 0);
collector.ensure_metadata_dir().unwrap();
assert!(temp.path().exists());
}
#[test]
fn test_infer_test_type_unit() {
let (collector, _temp) = create_temp_collector();
let test_type = collector.infer_test_type("ggen_core::parser::tests::test_parse");
assert!(matches!(test_type, TestType::Unit));
}
#[test]
fn test_infer_test_type_integration() {
let (collector, _temp) = create_temp_collector();
let test_type = collector.infer_test_type("tests::integration::test_full_workflow");
assert!(matches!(test_type, TestType::Integration));
}
#[test]
fn test_collect_failure_history_empty() {
let (collector, _temp) = create_temp_collector();
let history = collector.collect_failure_history().unwrap();
assert_eq!(history.len(), 0);
}
#[test]
fn test_update_failure_history() {
let (collector, _temp) = create_temp_collector();
let mut results = HashMap::new();
results.insert(TestId::new("test1").unwrap(), true); results.insert(TestId::new("test2").unwrap(), false);
collector.update_failure_history(&results).unwrap();
let history = collector.collect_failure_history().unwrap();
assert_eq!(history.len(), 2);
let (failures, total) = history.get(&TestId::new("test1").unwrap()).unwrap();
assert_eq!(*failures, 0);
assert_eq!(*total, 1);
let (failures, total) = history.get(&TestId::new("test2").unwrap()).unwrap();
assert_eq!(*failures, 1);
assert_eq!(*total, 1);
}
#[test]
fn test_collect_execution_times_file_not_found() {
let (collector, _temp) = create_temp_collector();
let result = collector.collect_execution_times("/nonexistent/path.json");
assert!(result.is_err());
}
#[test]
fn test_collect_coverage_data_file_not_found() {
let (collector, _temp) = create_temp_collector();
let result = collector.collect_coverage_data("/nonexistent/path.json");
assert!(result.is_err());
}
}