210 lines
6.2 KiB
Rust
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")
|
|
}
|