use crate::converge::{
cache::{CacheConfig, CacheEntry, CompilationStatus, SqliteCache, TranspilationCacheKey},
CacheError,
};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WarmResult {
Compiled,
Cached,
TranspileFailed(String),
CompileFailed(String),
ReadError(String),
}
#[derive(Debug, Clone, Default)]
pub struct WarmStats {
pub compiled: usize,
pub cached: usize,
pub transpile_failed: usize,
pub compile_failed: usize,
pub read_errors: usize,
}
impl WarmStats {
pub fn total(&self) -> usize {
self.compiled + self.cached + self.transpile_failed + self.compile_failed + self.read_errors
}
pub fn compile_rate(&self) -> f64 {
let total = self.total();
if total == 0 {
0.0
} else {
((self.compiled + self.cached) as f64 / total as f64) * 100.0
}
}
pub fn update(&mut self, result: &WarmResult) {
match result {
WarmResult::Compiled => self.compiled += 1,
WarmResult::Cached => self.cached += 1,
WarmResult::TranspileFailed(_) => self.transpile_failed += 1,
WarmResult::CompileFailed(_) => self.compile_failed += 1,
WarmResult::ReadError(_) => self.read_errors += 1,
}
}
}
#[derive(Debug, Default)]
pub struct AtomicWarmStats {
pub compiled: AtomicUsize,
pub cached: AtomicUsize,
pub transpile_failed: AtomicUsize,
pub compile_failed: AtomicUsize,
pub read_errors: AtomicUsize,
}
impl AtomicWarmStats {
pub fn new() -> Self {
Self::default()
}
pub fn update(&self, result: &WarmResult) {
match result {
WarmResult::Compiled => self.compiled.fetch_add(1, Ordering::Relaxed),
WarmResult::Cached => self.cached.fetch_add(1, Ordering::Relaxed),
WarmResult::TranspileFailed(_) => self.transpile_failed.fetch_add(1, Ordering::Relaxed),
WarmResult::CompileFailed(_) => self.compile_failed.fetch_add(1, Ordering::Relaxed),
WarmResult::ReadError(_) => self.read_errors.fetch_add(1, Ordering::Relaxed),
};
}
pub fn to_stats(&self) -> WarmStats {
WarmStats {
compiled: self.compiled.load(Ordering::Relaxed),
cached: self.cached.load(Ordering::Relaxed),
transpile_failed: self.transpile_failed.load(Ordering::Relaxed),
compile_failed: self.compile_failed.load(Ordering::Relaxed),
read_errors: self.read_errors.load(Ordering::Relaxed),
}
}
}
pub struct CacheWarmer {
config: CacheConfig,
}
impl CacheWarmer {
pub fn new(config: CacheConfig) -> Self {
Self { config }
}
pub fn find_python_files(&self, dir: &Path) -> Vec<PathBuf> {
walkdir::WalkDir::new(dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
e.path().extension().is_some_and(|ext| ext == "py")
&& !e.path().to_string_lossy().contains("__pycache__")
})
.map(|e| e.path().to_path_buf())
.collect()
}
pub fn warm_file(&self, path: &Path, cache: &SqliteCache) -> WarmResult {
let source = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) => return WarmResult::ReadError(e.to_string()),
};
let cache_key = TranspilationCacheKey::compute(&source, &self.config);
if cache.lookup(&cache_key).ok().flatten().is_some() {
return WarmResult::Cached;
}
let pipeline = depyler_core::DepylerPipeline::new();
let rust_code = match pipeline.transpile(&source) {
Ok(code) => code,
Err(e) => return WarmResult::TranspileFailed(e.to_string()),
};
let compile_result = self.try_compile(&rust_code);
let (status, error_msg) = match &compile_result {
Ok(()) => (CompilationStatus::Success, None),
Err(e) => (CompilationStatus::Failure, Some(e.clone())),
};
let entry = CacheEntry {
rust_code_blob: String::new(),
cargo_toml_blob: String::new(),
dependencies: vec![],
status: status.clone(),
error_messages: error_msg.clone().into_iter().collect(),
created_at: 0,
last_accessed_at: 0,
transpilation_time_ms: 0,
};
let cargo_toml = "[package]\nname = \"cached\"\nversion = \"0.1.0\"\nedition = \"2021\"";
let _ = cache.store(&cache_key, &rust_code, cargo_toml, entry);
match status {
CompilationStatus::Success => WarmResult::Compiled,
CompilationStatus::Failure => WarmResult::CompileFailed(error_msg.unwrap_or_default()),
}
}
fn try_compile(&self, rust_code: &str) -> Result<(), String> {
let temp_dir = tempfile::tempdir().map_err(|e| e.to_string())?;
let src_dir = temp_dir.path().join("src");
std::fs::create_dir_all(&src_dir).map_err(|e| e.to_string())?;
let rs_path = src_dir.join("lib.rs");
std::fs::write(&rs_path, rust_code).map_err(|e| e.to_string())?;
let cargo_toml = r#"[package]
name = "depyler_cache_test"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/lib.rs"
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
clap = { version = "4.5", features = ["derive"] }
regex = "1.10"
chrono = "0.4"
"#;
std::fs::write(temp_dir.path().join("Cargo.toml"), cargo_toml)
.map_err(|e| e.to_string())?;
let output = std::process::Command::new("cargo")
.args(["check", "--lib"])
.current_dir(temp_dir.path())
.env_remove("CARGO_LLVM_COV")
.env_remove("CARGO_LLVM_COV_SHOW_ENV")
.env_remove("CARGO_LLVM_COV_TARGET_DIR")
.env_remove("LLVM_PROFILE_FILE")
.env_remove("RUSTFLAGS")
.env_remove("CARGO_INCREMENTAL")
.env_remove("CARGO_BUILD_JOBS")
.env_remove("CARGO_TARGET_DIR")
.output()
.map_err(|e| e.to_string())?;
if output.status.success() {
Ok(())
} else {
Err(String::from_utf8_lossy(&output.stderr).to_string())
}
}
pub fn warm_directory(&self, dir: &Path) -> Result<WarmStats, CacheError> {
let cache = SqliteCache::open(self.config.clone())?;
let files = self.find_python_files(dir);
let mut stats = WarmStats::default();
for file in files {
let result = self.warm_file(&file, &cache);
stats.update(&result);
}
Ok(stats)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_config(temp_dir: &TempDir) -> CacheConfig {
CacheConfig {
cache_dir: temp_dir.path().join("cache"),
..Default::default()
}
}
fn create_python_file(dir: &Path, name: &str, content: &str) -> PathBuf {
let path = dir.join(name);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, content).unwrap();
path
}
#[test]
fn test_warm_stats_default() {
let stats = WarmStats::default();
assert_eq!(stats.compiled, 0);
assert_eq!(stats.cached, 0);
assert_eq!(stats.transpile_failed, 0);
assert_eq!(stats.compile_failed, 0);
assert_eq!(stats.read_errors, 0);
assert_eq!(stats.total(), 0);
assert_eq!(stats.compile_rate(), 0.0);
}
#[test]
fn test_warm_stats_total() {
let stats = WarmStats {
compiled: 5,
cached: 3,
transpile_failed: 1,
compile_failed: 1,
read_errors: 0,
};
assert_eq!(stats.total(), 10);
}
#[test]
fn test_warm_stats_compile_rate() {
let stats = WarmStats {
compiled: 6,
cached: 2,
transpile_failed: 1,
compile_failed: 1,
read_errors: 0,
};
assert!((stats.compile_rate() - 80.0).abs() < 0.01);
}
#[test]
fn test_warm_stats_compile_rate_all_success() {
let stats = WarmStats {
compiled: 10,
cached: 0,
transpile_failed: 0,
compile_failed: 0,
read_errors: 0,
};
assert!((stats.compile_rate() - 100.0).abs() < 0.01);
}
#[test]
fn test_warm_stats_compile_rate_all_cached() {
let stats = WarmStats {
compiled: 0,
cached: 10,
transpile_failed: 0,
compile_failed: 0,
read_errors: 0,
};
assert!((stats.compile_rate() - 100.0).abs() < 0.01);
}
#[test]
fn test_warm_stats_update() {
let mut stats = WarmStats::default();
stats.update(&WarmResult::Compiled);
assert_eq!(stats.compiled, 1);
stats.update(&WarmResult::Cached);
assert_eq!(stats.cached, 1);
stats.update(&WarmResult::TranspileFailed("err".to_string()));
assert_eq!(stats.transpile_failed, 1);
stats.update(&WarmResult::CompileFailed("err".to_string()));
assert_eq!(stats.compile_failed, 1);
stats.update(&WarmResult::ReadError("err".to_string()));
assert_eq!(stats.read_errors, 1);
}
#[test]
fn test_atomic_warm_stats_thread_safe() {
use std::sync::Arc;
use std::thread;
let stats = Arc::new(AtomicWarmStats::new());
let mut handles = vec![];
for _ in 0..10 {
let stats_clone = Arc::clone(&stats);
handles.push(thread::spawn(move || {
for _ in 0..100 {
stats_clone.update(&WarmResult::Compiled);
}
}));
}
for handle in handles {
handle.join().unwrap();
}
let final_stats = stats.to_stats();
assert_eq!(final_stats.compiled, 1000);
}
#[test]
fn test_cache_warmer_find_python_files() {
let temp = TempDir::new().unwrap();
let warmer = CacheWarmer::new(create_test_config(&temp));
create_python_file(temp.path(), "test1.py", "x = 1");
create_python_file(temp.path(), "test2.py", "y = 2");
create_python_file(temp.path(), "subdir/test3.py", "z = 3");
create_python_file(temp.path(), "__pycache__/cached.py", "cached");
std::fs::write(temp.path().join("not_python.txt"), "text").unwrap();
let files = warmer.find_python_files(temp.path());
assert_eq!(files.len(), 3);
assert!(files.iter().all(|f| f.extension().unwrap() == "py"));
assert!(!files
.iter()
.any(|f| f.to_string_lossy().contains("__pycache__")));
}
#[test]
fn test_cache_warmer_warm_file_success() {
let temp = TempDir::new().unwrap();
let config = create_test_config(&temp);
let warmer = CacheWarmer::new(config.clone());
let cache = SqliteCache::open(config).unwrap();
let py_file = create_python_file(
temp.path(),
"simple.py",
"def add(a: int, b: int) -> int:\n return a + b\n",
);
let result = warmer.warm_file(&py_file, &cache);
matches!(
result,
WarmResult::Compiled | WarmResult::CompileFailed(_) | WarmResult::TranspileFailed(_)
);
}
#[test]
fn test_cache_warmer_warm_file_cached() {
let temp = TempDir::new().unwrap();
let config = create_test_config(&temp);
let warmer = CacheWarmer::new(config.clone());
let cache = SqliteCache::open(config.clone()).unwrap();
let py_file = create_python_file(temp.path(), "cached.py", "x = 1\n");
let first_result = warmer.warm_file(&py_file, &cache);
let second_result = warmer.warm_file(&py_file, &cache);
if matches!(first_result, WarmResult::Compiled) {
assert_eq!(second_result, WarmResult::Cached);
}
}
#[test]
fn test_cache_warmer_warm_file_read_error() {
let temp = TempDir::new().unwrap();
let config = create_test_config(&temp);
let warmer = CacheWarmer::new(config.clone());
let cache = SqliteCache::open(config).unwrap();
let result = warmer.warm_file(Path::new("/nonexistent/file.py"), &cache);
assert!(matches!(result, WarmResult::ReadError(_)));
}
#[test]
fn test_cache_warmer_warm_directory() {
let temp = TempDir::new().unwrap();
let config = create_test_config(&temp);
let warmer = CacheWarmer::new(config);
create_python_file(temp.path(), "a.py", "x = 1\n");
create_python_file(temp.path(), "b.py", "y = 2\n");
create_python_file(temp.path(), "c.py", "z = 3\n");
let stats = warmer.warm_directory(temp.path()).unwrap();
assert_eq!(stats.total(), 3);
assert_eq!(
stats.compiled + stats.cached + stats.transpile_failed + stats.compile_failed,
3
);
}
#[test]
fn test_warm_result_equality() {
assert_eq!(WarmResult::Compiled, WarmResult::Compiled);
assert_eq!(WarmResult::Cached, WarmResult::Cached);
assert_ne!(WarmResult::Compiled, WarmResult::Cached);
assert_eq!(
WarmResult::TranspileFailed("err".to_string()),
WarmResult::TranspileFailed("err".to_string())
);
}
#[test]
fn test_cache_warmer_integration() {
let temp = TempDir::new().unwrap();
let config = CacheConfig {
cache_dir: temp.path().join("cache"),
..Default::default()
};
create_python_file(
temp.path(),
"valid.py",
"def greet(name: str) -> str:\n return f'Hello {name}'\n",
);
create_python_file(
temp.path(),
"also_valid.py",
"def add(a: int, b: int) -> int:\n return a + b\n",
);
let warmer = CacheWarmer::new(config);
let stats = warmer.warm_directory(temp.path()).unwrap();
assert_eq!(stats.total(), 2);
let rate = stats.compile_rate();
assert!((0.0..=100.0).contains(&rate));
}
#[test]
fn test_property_stats_total_is_sum_of_parts() {
for compiled in 0..5 {
for cached in 0..5 {
for failed in 0..5 {
let stats = WarmStats {
compiled,
cached,
transpile_failed: failed,
compile_failed: 0,
read_errors: 0,
};
assert_eq!(stats.total(), compiled + cached + failed);
}
}
}
}
#[test]
fn test_property_compile_rate_bounded() {
for compiled in 0..10 {
for cached in 0..10 {
for failed in 0..10 {
let stats = WarmStats {
compiled,
cached,
transpile_failed: failed,
compile_failed: 0,
read_errors: 0,
};
let rate = stats.compile_rate();
assert!((0.0..=100.0).contains(&rate));
}
}
}
}
}