use std::io::{Read, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
static START_GATE: Mutex<()> = Mutex::new(());
fn free_port() -> u16 {
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
l.local_addr().unwrap().port()
}
fn req(parts: &[&[u8]]) -> Vec<u8> {
let mut v = format!("*{}\r\n", parts.len()).into_bytes();
for p in parts {
v.extend_from_slice(format!("${}\r\n", p.len()).as_bytes());
v.extend_from_slice(p);
v.extend_from_slice(b"\r\n");
}
v
}
struct Server {
port: u16,
dir: std::path::PathBuf,
stop: Arc<AtomicBool>,
handle: Option<std::thread::JoinHandle<()>>,
}
impl Server {
fn start(nshards: usize) -> Server {
let _gate = START_GATE.lock().unwrap_or_else(|e| e.into_inner());
let port = free_port();
let dir = std::env::temp_dir().join(format!(
"kevy-hello3-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
let stop = Arc::new(AtomicBool::new(false));
let stop_thread = stop.clone();
let dir_thread = dir.clone();
let handle = std::thread::spawn(move || {
let rt = kevy_rt::Runtime::new([127, 0, 0, 1], port, nshards, kevy::KevyCommands)
.with_data_dir(dir_thread);
rt.run(stop_thread).unwrap();
});
let mut ready = false;
for _ in 0..200 {
if std::net::TcpStream::connect(("127.0.0.1", port)).is_ok() {
ready = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert!(ready, "runtime did not come up");
Server { port, dir, stop, handle: Some(handle) }
}
fn connect(&self) -> std::net::TcpStream {
let s = std::net::TcpStream::connect(("127.0.0.1", self.port)).unwrap();
s.set_read_timeout(Some(std::time::Duration::from_secs(5)))
.unwrap();
s
}
}
impl Drop for Server {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(h) = self.handle.take() {
let _ = h.join();
}
let _ = std::fs::remove_dir_all(&self.dir);
}
}
fn read_n(s: &mut std::net::TcpStream, n: usize) -> Vec<u8> {
let mut buf = vec![0u8; n];
s.read_exact(&mut buf).unwrap();
buf
}
#[test]
fn hello_no_arg_keeps_v2_default() {
let srv = Server::start(1);
let mut c = srv.connect();
c.write_all(&req(&[b"HELLO"])).unwrap();
let mut head = [0u8; 4];
c.read_exact(&mut head).unwrap();
assert_eq!(&head, b"*14\r", "expected `*14\\r\\n` (RESP2 array)");
let mut sink = vec![0u8; 256];
let _ = c.read(&mut sink).unwrap();
}
#[test]
fn hello_3_returns_map_shape_and_flips_conn() {
let srv = Server::start(1);
let mut c = srv.connect();
c.write_all(&req(&[b"HELLO", b"3"])).unwrap();
let mut head = [0u8; 4];
c.read_exact(&mut head).unwrap();
assert_eq!(&head, b"%7\r\n", "expected RESP3 Map header `%7\\r\\n`");
let mut body = Vec::new();
let mut chunk = [0u8; 512];
let n = c.read(&mut chunk).unwrap();
body.extend_from_slice(&chunk[..n]);
let needle = b"proto\r\n:3\r\n";
assert!(
body.windows(needle.len()).any(|w| w == needle),
"expected `proto` → `:3` somewhere in HELLO 3 body, got {:?}",
String::from_utf8_lossy(&body)
);
}
#[test]
fn hello_2_explicit_returns_array_shape() {
let srv = Server::start(1);
let mut c = srv.connect();
c.write_all(&req(&[b"HELLO", b"2"])).unwrap();
let head = read_n(&mut c, 4);
assert_eq!(&head, b"*14\r", "HELLO 2 must reply in RESP2 array shape");
let mut sink = vec![0u8; 256];
let _ = c.read(&mut sink).unwrap();
}
#[test]
fn hello_4_unsupported_replies_noproto_error() {
let srv = Server::start(1);
let mut c = srv.connect();
c.write_all(&req(&[b"HELLO", b"4"])).unwrap();
let head = read_n(&mut c, 8);
assert!(
head.starts_with(b"-NOPROTO"),
"expected `-NOPROTO` error, got {:?}",
String::from_utf8_lossy(&head)
);
let mut tail = Vec::new();
let mut byte = [0u8; 1];
loop {
c.read_exact(&mut byte).unwrap();
tail.push(byte[0]);
if tail.ends_with(b"\r\n") {
break;
}
}
}
#[test]
fn hello_3_then_normal_command_still_works() {
let srv = Server::start(1);
let mut c = srv.connect();
c.write_all(&req(&[b"HELLO", b"3"])).unwrap();
let mut sink = [0u8; 512];
let _ = c.read(&mut sink).unwrap();
c.write_all(&req(&[b"SET", b"hi", b"there"])).unwrap();
let head = read_n(&mut c, 5);
assert_eq!(&head, b"+OK\r\n", "SET ack stays +OK (RESP2 shape) in P2");
c.write_all(&req(&[b"GET", b"hi"])).unwrap();
let mut buf = vec![0u8; 11]; c.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"$5\r\nthere\r\n");
}
#[test]
fn hello_3_proto_is_per_conn() {
let srv = Server::start(1);
let mut v3 = srv.connect();
let mut v2 = srv.connect();
v3.write_all(&req(&[b"HELLO", b"3"])).unwrap();
v2.write_all(&req(&[b"HELLO", b"2"])).unwrap();
let v3_head = read_n(&mut v3, 4);
let v2_head = read_n(&mut v2, 4);
assert_eq!(&v3_head, b"%7\r\n", "v3 conn should see Map header");
assert_eq!(&v2_head, b"*14\r", "v2 conn should see Array header");
}