#![allow(clippy::expect_used, clippy::unwrap_used)] use std::{ fs, net::{SocketAddr, UdpSocket}, path::{Path, PathBuf}, sync::{ Arc, atomic::{AtomicBool, AtomicU64, Ordering}, }, thread::JoinHandle, time::{Duration, SystemTime, UNIX_EPOCH}, }; use pfs_tftp_sync::{Client, ClientConfig, Mode, Server, ServerConfig}; #[test] fn octet_put_and_get_roundtrip() { let server_root = TempDir::new("pfs_tftp_server_root"); let local_root = TempDir::new("pfs_tftp_local_root"); let (addr, shutdown, handle) = start_server(server_root.path(), true); let client = Client::new(addr, test_client_config()); let upload_local = local_root.path().join("upload.bin"); let upload_remote = "upload.bin"; let upload_data = vec![0xA5; 1500]; fs::write(&upload_local, &upload_data).unwrap(); client .put(&upload_local, upload_remote, Mode::Octet) .unwrap(); assert_eq!( fs::read(server_root.path().join(upload_remote)).unwrap(), upload_data ); let download_remote = "download.bin"; let download_data = (0u8..=200).cycle().take(2048).collect::>(); fs::write(server_root.path().join(download_remote), &download_data).unwrap(); let download_local = local_root.path().join("download.bin"); client .get(download_remote, &download_local, Mode::Octet) .unwrap(); assert_eq!(fs::read(download_local).unwrap(), download_data); stop_server(&shutdown, handle); } #[test] fn octet_exact_512_bytes_requires_final_empty_block() { let server_root = TempDir::new("pfs_tftp_server_root_512"); let local_root = TempDir::new("pfs_tftp_local_root_512"); let (addr, shutdown, handle) = start_server(server_root.path(), true); let client = Client::new(addr, test_client_config()); let data = vec![0x11; 512]; fs::write(server_root.path().join("exact512.bin"), &data).unwrap(); let local = local_root.path().join("exact512.bin"); client.get("exact512.bin", &local, Mode::Octet).unwrap(); assert_eq!(fs::read(local).unwrap(), data); let local_up = local_root.path().join("exact512_upload.bin"); fs::write(&local_up, &data).unwrap(); client .put(&local_up, "exact512_upload.bin", Mode::Octet) .unwrap(); assert_eq!( fs::read(server_root.path().join("exact512_upload.bin")).unwrap(), data ); stop_server(&shutdown, handle); } #[test] fn netascii_roundtrip_preserves_newlines_and_cr() { let server_root = TempDir::new("pfs_tftp_server_root_netascii"); let local_root = TempDir::new("pfs_tftp_local_root_netascii"); let (addr, shutdown, handle) = start_server(server_root.path(), true); let client = Client::new(addr, test_client_config()); let data = b"line1\nline2\rline3\n".to_vec(); let local_up = local_root.path().join("text.txt"); fs::write(&local_up, &data).unwrap(); client.put(&local_up, "text.txt", Mode::NetAscii).unwrap(); assert_eq!(fs::read(server_root.path().join("text.txt")).unwrap(), data); let local_down = local_root.path().join("text_downloaded.txt"); client.get("text.txt", &local_down, Mode::NetAscii).unwrap(); assert_eq!(fs::read(local_down).unwrap(), data); stop_server(&shutdown, handle); } #[test] fn missing_remote_file_does_not_create_local_file() { let server_root = TempDir::new("pfs_tftp_server_root_missing"); let local_root = TempDir::new("pfs_tftp_local_root_missing"); let (addr, shutdown, handle) = start_server(server_root.path(), true); let client = Client::new(addr, test_client_config()); let out = local_root.path().join("should_not_exist.bin"); assert!(!out.exists()); let _err = client .get("does_not_exist.bin", &out, Mode::Octet) .unwrap_err(); assert!(!out.exists()); stop_server(&shutdown, handle); } fn test_client_config() -> ClientConfig { ClientConfig { timeout: Duration::from_millis(200), retries: 20, dally_timeout: Duration::from_millis(200), dally_retries: 2, } } fn start_server(root: &Path, allow_write: bool) -> (SocketAddr, Arc, JoinHandle<()>) { let addr = reserve_local_addr(); let cfg = ServerConfig { bind: addr, root: root.to_path_buf(), allow_write, overwrite: true, timeout: Duration::from_millis(200), retries: 20, dally_timeout: Duration::from_millis(200), dally_retries: 2, ..ServerConfig::default() }; let shutdown = Arc::new(AtomicBool::new(false)); let shutdown_for_thread = Arc::clone(&shutdown); let handle = std::thread::spawn(move || { Server::new(cfg) .serve_until(shutdown_for_thread.as_ref()) .unwrap(); }); std::thread::sleep(Duration::from_millis(50)); (addr, shutdown, handle) } fn stop_server(shutdown: &Arc, handle: JoinHandle<()>) { shutdown.store(true, Ordering::Relaxed); handle.join().unwrap(); } fn reserve_local_addr() -> SocketAddr { let sock = UdpSocket::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap(); sock.local_addr().unwrap() } struct TempDir { path: PathBuf, } impl TempDir { fn new(prefix: &str) -> Self { static NEXT_ID: AtomicU64 = AtomicU64::new(0); let mut path = test_tmp_root(); let now = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_nanos()) .unwrap_or(0); let id = NEXT_ID.fetch_add(1, Ordering::Relaxed); path.push(format!("{prefix}_{now}_{}_{}", std::process::id(), id)); fs::create_dir_all(&path).unwrap(); Self { path } } fn path(&self) -> &Path { &self.path } } impl Drop for TempDir { fn drop(&mut self) { let _ignored = fs::remove_dir_all(&self.path); } } fn test_tmp_root() -> PathBuf { // Keep temporary test artifacts inside the workspace, since the Codex CLI // sandbox does not allow writing to system temp directories. let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let workspace_root = manifest_dir .parent() .and_then(Path::parent) .unwrap_or(&manifest_dir); workspace_root .join("target") .join("tmp") .join("pfs-tftp-sync-tests") }