Files
pfs-tftp-codex/crates/pfs-tftp-sync/tests/end_to_end.rs

210 lines
6.2 KiB
Rust

#![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::<Vec<u8>>();
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<AtomicBool>, 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<AtomicBool>, 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")
}