diff --git a/tests/e2e_cache_location.rs b/tests/e2e_cache_location.rs new file mode 100644 index 0000000..fb828c0 --- /dev/null +++ b/tests/e2e_cache_location.rs @@ -0,0 +1,153 @@ +use std::env; +use std::fs; +use std::fs::OpenOptions; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::UNIX_EPOCH; + +use tempfile::TempDir; + +/// End-to-end integration test that verifies the crate's discovery and +/// default cache creation when a conventional binary (not `cargo`) uses +/// the library. +/// +/// Steps performed: +/// 1. Create isolated temporary paths for source code and runtime. +/// 2. Write a tiny `main.rs` that calls `CacheRoot::from_discovery()`, +/// ensures a group directory, touches a probe file, and prints the +/// three resolved paths (root, group, probe) to stdout. +/// 3. Compile that binary using `rustc` and run it directly via +/// `std::process::Command` (the test intentionally avoids `cargo`). +/// 4. Run the binary from a directory with no `Cargo.toml`, then assert +/// discovery falls back to `/.cache`, and that subgroup + probe +/// file exist and remain writable. This is cross-platform and does +/// not use any platform-specific APIs. +#[test] +fn e2e_binary_compiled_with_rustc_creates_default_cache_under_temp_root() { + let tmp = TempDir::new().expect("tempdir"); + let source_root = tmp.path().join("source"); + let runtime_root = tmp.path().join("runtime"); + fs::create_dir_all(&source_root).expect("create source dir"); + fs::create_dir_all(&runtime_root).expect("create runtime dir"); + + // Create a Cargo.toml near source to prove runtime discovery is based on + // process cwd (runtime_root), not where source files happened to live. + fs::write( + source_root.join("Cargo.toml"), + "[package]\nname='irrelevant-source-root'\nversion='0.1.0'\nedition='2024'\n", + ) + .expect("write source Cargo.toml"); + + let app_source = source_root.join("main.rs"); + fs::write( + &app_source, + "use cache_manager::CacheRoot;\n\ + use std::fs;\n\ + use std::io;\n\ + fn main() -> io::Result<()> {\n\ + let root = CacheRoot::from_discovery()?;\n\ + let group = root.group(\"e2e\");\n\ + let ensured = group.ensure_dir()?;\n\ + let probe = group.touch(\"probe.txt\")?;\n\ + fs::metadata(&probe)?;\n\ + println!(\"{}\", root.path().display());\n\ + println!(\"{}\", ensured.display());\n\ + println!(\"{}\", probe.display());\n\ + Ok(())\n\ + }\n", + ) + .expect("write tiny app source"); + + let current_test_exe = env::current_exe().expect("current exe"); + let deps_dir = current_test_exe.parent().expect("deps dir"); + let cache_manager_rlib = latest_cache_manager_rlib(deps_dir); + + let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string()); + let binary_name = if std::env::consts::EXE_EXTENSION.is_empty() { + "tiny-app".to_string() + } else { + format!("tiny-app.{}", std::env::consts::EXE_EXTENSION) + }; + let output_binary = runtime_root.join(binary_name); + + let compile_output = Command::new(&rustc) + .arg("--edition=2024") + .arg(&app_source) + .arg("-o") + .arg(&output_binary) + .arg("--extern") + .arg(format!("cache_manager={}", cache_manager_rlib.display())) + .arg("-L") + .arg(format!("dependency={}", deps_dir.display())) + .output() + .expect("spawn rustc"); + + assert!( + compile_output.status.success(), + "rustc failed: {}", + String::from_utf8_lossy(&compile_output.stderr) + ); + + let run_output = Command::new(&output_binary) + .current_dir(&runtime_root) + .output() + .expect("run tiny binary directly"); + + assert!( + run_output.status.success(), + "tiny binary failed: {}", + String::from_utf8_lossy(&run_output.stderr) + ); + + let expected_root = runtime_root + .canonicalize() + .expect("canonicalize runtime root") + .join(".cache"); + let expected_group = expected_root.join("e2e"); + let expected_probe = expected_group.join("probe.txt"); + + assert!( + expected_root.is_dir(), + "expected default cache root to exist" + ); + assert!(expected_group.is_dir(), "expected cache group to exist"); + assert!(expected_probe.is_file(), "expected probe file to exist"); + + let stdout = String::from_utf8(run_output.stdout).expect("stdout utf8"); + let mut lines = stdout.lines(); + let root_line = lines.next().expect("root stdout line"); + let group_line = lines.next().expect("group stdout line"); + let probe_line = lines.next().expect("probe stdout line"); + + assert_eq!(Path::new(root_line), expected_root.as_path()); + assert_eq!(Path::new(group_line), expected_group.as_path()); + assert_eq!(Path::new(probe_line), expected_probe.as_path()); + + OpenOptions::new() + .append(true) + .open(&expected_probe) + .expect("probe path remains writable"); +} + +fn latest_cache_manager_rlib(deps_dir: &Path) -> PathBuf { + let mut rlibs = fs::read_dir(deps_dir) + .expect("read deps dir") + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| { + path.file_name() + .and_then(|name| name.to_str()) + .map(|name| name.starts_with("libcache_manager-") && name.ends_with(".rlib")) + .unwrap_or(false) + }) + .collect::>(); + + rlibs.sort_by_key(|path| { + path.metadata() + .and_then(|m| m.modified()) + .ok() + .unwrap_or(UNIX_EPOCH) + }); + + rlibs.pop().expect("find libcache_manager rlib") +}