mod api; mod capture; mod local; mod manager; mod schroot; mod ssh; mod unshare; pub use api::{Context, ContextCommand, ContextConfig, LineSink, Stream}; pub use manager::ContextManager; use std::sync::Arc; /// Obtain global context manager pub fn manager() -> &'static ContextManager { &manager::MANAGER } /// Obtain current context pub fn current() -> Arc { manager::MANAGER.current() } #[cfg(test)] mod tests { use super::*; use std::fs; use tempfile::NamedTempFile; #[test] fn test_ensure_available_local() { let temp_dir = tempfile::tempdir().unwrap(); let src_file = temp_dir.path().join("src.txt"); fs::write(&src_file, "local").unwrap(); let ctx = Context::new(ContextConfig::Local); let dest = ctx.ensure_available(&src_file, "/tmp").unwrap(); // Should return a path that exists and has the same content assert!(dest.exists()); let content = fs::read_to_string(&dest).unwrap(); assert_eq!(content, "local"); // The dest should be in the /tmp directory assert!(dest.starts_with("/tmp")); } #[test] fn test_context_manager_crud() { let temp_file = NamedTempFile::new().unwrap(); let path = temp_file.path().to_path_buf(); let mgr = ContextManager::with_path(path.clone()); // Add let ssh_cfg = ContextConfig::Ssh { host: "10.0.0.1".into(), user: Some("admin".into()), port: Some(2222), }; mgr.add_context("myserver", ssh_cfg.clone()).unwrap(); assert!(mgr.list_contexts().contains(&"myserver".to_string())); // List let list = mgr.list_contexts(); assert!(list.contains(&"myserver".to_string())); // Set Current mgr.set_current("myserver").unwrap(); assert_eq!(mgr.current_name(), "myserver".to_string()); // Remove mgr.remove_context("myserver").unwrap(); assert!(!mgr.list_contexts().contains(&"myserver".to_string())); } #[test] fn test_persistence() { let temp_dir = tempfile::tempdir().unwrap(); let config_path = temp_dir.path().join("contexts.json"); { let mgr = ContextManager::with_path(config_path.clone()); mgr.add_context("persistent", ContextConfig::Local).unwrap(); mgr.set_current("persistent").unwrap(); } let content = fs::read_to_string(&config_path).unwrap(); let loaded_config: super::manager::Config = serde_json::from_str(&content).unwrap(); assert_eq!(loaded_config.context, "persistent".to_string()); assert!(loaded_config.contexts.contains_key("persistent")); } #[test] fn test_context_fallback_on_removal() { let temp_file = NamedTempFile::new().unwrap(); let path = temp_file.path().to_path_buf(); let mgr = ContextManager::with_path(path); // 1. Add and set a context mgr.add_context("temp", ContextConfig::Local).unwrap(); mgr.set_current("temp").unwrap(); assert_eq!(mgr.current_name(), "temp"); // 2. Remove it mgr.remove_context("temp").unwrap(); // 3. Should have fallen back to local assert_eq!(mgr.current_name(), "local"); assert!(mgr.list_contexts().contains(&"local".to_string())); } #[test] fn test_context_file_ops() { let temp_dir = tempfile::tempdir().unwrap(); let ctx = Context::new(ContextConfig::Local); let file_path = temp_dir.path().join("test.txt"); let content = "hello world"; // 1. Write file ctx.write_file(&file_path, content).unwrap(); // 2. Read file let read_content = ctx.read_file(&file_path).unwrap(); assert_eq!(read_content, content); // 3. Copy path let dest_path = temp_dir.path().join("test_copy.txt"); ctx.copy_path(&file_path, &dest_path).unwrap(); let copied_content = ctx.read_file(&dest_path).unwrap(); assert_eq!(copied_content, content); // 4. Recursive copy let subdir = temp_dir.path().join("subdir"); std::fs::create_dir_all(&subdir).unwrap(); let subfile = subdir.join("subfile.txt"); ctx.write_file(&subfile, "subcontent").unwrap(); let subdir_copy = temp_dir.path().join("subdir_copy"); ctx.copy_path(&subdir, &subdir_copy).unwrap(); assert!(subdir_copy.exists()); assert!(subdir_copy.join("subfile.txt").exists()); assert_eq!( ctx.read_file(&subdir_copy.join("subfile.txt")).unwrap(), "subcontent" ); } /// Copying a tree containing a dangling symlink must reproduce the link /// itself rather than failing to follow it (which previously surfaced as /// a bare "No such file or directory (os error 2)"). #[test] fn test_context_copy_preserves_dangling_symlink() { use std::os::unix::fs::symlink; let temp_dir = tempfile::tempdir().unwrap(); let ctx = Context::new(ContextConfig::Local); let src_dir = temp_dir.path().join("src"); std::fs::create_dir_all(&src_dir).unwrap(); // A regular file alongside the symlink, to ensure normal copies still work. std::fs::write(src_dir.join("real.txt"), "data").unwrap(); // A dangling symlink pointing to a non-existent target. symlink("/nonexistent/target", src_dir.join("dangling")).unwrap(); let dest_dir = temp_dir.path().join("dest"); ctx.copy_path(&src_dir, &dest_dir).unwrap(); // The regular file was copied. assert_eq!(ctx.read_file(&dest_dir.join("real.txt")).unwrap(), "data"); // The symlink was reproduced as a symlink (not followed). let meta = std::fs::symlink_metadata(dest_dir.join("dangling")).unwrap(); assert!(meta.file_type().is_symlink()); assert_eq!( std::fs::read_link(dest_dir.join("dangling")).unwrap(), std::path::Path::new("/nonexistent/target") ); } }