use cuenv_cas::{LocalActionCache, LocalCas};
use cuenv_core::OutputCapture;
use cuenv_core::tasks::cache::TaskCacheConfig;
use cuenv_core::tasks::executor::{ExecutorConfig, TaskExecutor};
use cuenv_core::tasks::{Input, Task, TaskCacheMode, TaskCachePolicy};
use cuenv_vcs::WalkHasher;
use std::collections::BTreeMap;
use std::fs;
use std::sync::Arc;
use tempfile::TempDir;
fn build_executor(workspace: &std::path::Path, cache_root: &std::path::Path) -> TaskExecutor {
let cas = Arc::new(LocalCas::open(cache_root).unwrap());
let action_cache = Arc::new(LocalActionCache::open(cache_root).unwrap());
let vcs_hasher = Arc::new(WalkHasher::new(workspace));
let cache = TaskCacheConfig {
cas: cas.clone(),
action_cache: action_cache.clone(),
vcs_hasher,
vcs_hasher_root: workspace.to_path_buf(),
cuenv_version: "test".to_string(),
runtime_identity_properties: BTreeMap::new(),
cache_disabled_reason: None,
};
let config = ExecutorConfig {
capture_output: OutputCapture::Capture,
project_root: workspace.to_path_buf(),
cache: Some(cache),
..Default::default()
};
TaskExecutor::new(config)
}
#[tokio::test]
async fn second_run_with_unchanged_inputs_is_a_cache_hit() {
let workspace = TempDir::new().unwrap();
let cache_root = TempDir::new().unwrap();
fs::write(workspace.path().join("input.txt"), "v1").unwrap();
let executor = build_executor(workspace.path(), cache_root.path());
let task = Task {
command: "sh".to_string(),
args: vec![
"-c".to_string(),
"touch marker.txt; cat input.txt > out.txt".to_string(),
],
inputs: vec![Input::Path("input.txt".to_string())],
outputs: vec!["out.txt".to_string()],
cache: Some(TaskCachePolicy {
mode: TaskCacheMode::ReadWrite,
max_age: None,
}),
..Task::default()
};
let result1 = executor.execute_task("touch", &task).await.unwrap();
assert!(result1.success);
assert!(workspace.path().join("marker.txt").exists());
assert_eq!(fs::read(workspace.path().join("out.txt")).unwrap(), b"v1");
fs::remove_file(workspace.path().join("marker.txt")).unwrap();
fs::remove_file(workspace.path().join("out.txt")).unwrap();
let result2 = executor.execute_task("touch", &task).await.unwrap();
assert!(result2.success);
assert!(
!workspace.path().join("marker.txt").exists(),
"marker.txt was recreated, meaning the task actually re-ran instead of hitting the cache"
);
assert!(
workspace.path().join("out.txt").exists(),
"out.txt was not materialized from the cache"
);
assert_eq!(fs::read(workspace.path().join("out.txt")).unwrap(), b"v1");
}
#[tokio::test]
async fn cache_invalidates_when_input_changes() {
let workspace = TempDir::new().unwrap();
let cache_root = TempDir::new().unwrap();
fs::write(workspace.path().join("input.txt"), "v1").unwrap();
let executor = build_executor(workspace.path(), cache_root.path());
let task = Task {
command: "sh".to_string(),
args: vec![
"-c".to_string(),
"touch marker.txt; cat input.txt > out.txt".to_string(),
],
inputs: vec![Input::Path("input.txt".to_string())],
outputs: vec!["out.txt".to_string()],
cache: Some(TaskCachePolicy {
mode: TaskCacheMode::ReadWrite,
max_age: None,
}),
..Task::default()
};
let result1 = executor.execute_task("t", &task).await.unwrap();
assert!(result1.success);
fs::remove_file(workspace.path().join("marker.txt")).unwrap();
fs::write(workspace.path().join("input.txt"), "v2").unwrap();
let result2 = executor.execute_task("t", &task).await.unwrap();
assert!(result2.success);
assert!(
workspace.path().join("marker.txt").exists(),
"input change should have triggered a real re-execution"
);
assert_eq!(fs::read(workspace.path().join("out.txt")).unwrap(), b"v2");
}
#[tokio::test]
async fn task_without_inputs_is_never_cached() {
let workspace = TempDir::new().unwrap();
let cache_root = TempDir::new().unwrap();
let executor = build_executor(workspace.path(), cache_root.path());
let task = Task {
command: "sh".to_string(),
args: vec!["-c".to_string(), "touch marker.txt".to_string()],
..Task::default()
};
let result1 = executor.execute_task("no-inputs", &task).await.unwrap();
assert!(result1.success);
fs::remove_file(workspace.path().join("marker.txt")).unwrap();
let result2 = executor.execute_task("no-inputs", &task).await.unwrap();
assert!(result2.success);
assert!(
workspace.path().join("marker.txt").exists(),
"task without declared inputs must always re-run"
);
}