206 lines
7.4 KiB
Rust
206 lines
7.4 KiB
Rust
use redis_rs::{server::Server, options::DBOption};
|
|
use std::time::Duration;
|
|
use tokio::time::sleep;
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
use tokio::net::TcpStream;
|
|
|
|
// Helper function to start a test server with clean data directory
|
|
async fn start_test_server(test_name: &str) -> (Server, u16) {
|
|
use std::sync::atomic::{AtomicU16, Ordering};
|
|
static PORT_COUNTER: AtomicU16 = AtomicU16::new(17000);
|
|
|
|
// Get a unique port for this test
|
|
let port = PORT_COUNTER.fetch_add(1, Ordering::SeqCst);
|
|
|
|
let test_dir = format!("/tmp/herodb_test_{}", test_name);
|
|
|
|
// Clean up any existing test data
|
|
let _ = std::fs::remove_dir_all(&test_dir);
|
|
std::fs::create_dir_all(&test_dir).unwrap();
|
|
|
|
let option = DBOption {
|
|
dir: test_dir,
|
|
port,
|
|
};
|
|
|
|
let server = Server::new(option).await;
|
|
(server, port)
|
|
}
|
|
|
|
// Helper function to send Redis command and get response
|
|
async fn send_redis_command(port: u16, command: &str) -> String {
|
|
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).await.unwrap();
|
|
stream.write_all(command.as_bytes()).await.unwrap();
|
|
|
|
let mut buffer = [0; 1024];
|
|
let n = stream.read(&mut buffer).await.unwrap();
|
|
String::from_utf8_lossy(&buffer[..n]).to_string()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_basic_redis_functionality() {
|
|
let (mut server, port) = start_test_server("basic").await;
|
|
|
|
// Start server in background with timeout
|
|
let server_handle = tokio::spawn(async move {
|
|
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Accept only a few connections for testing
|
|
for _ in 0..10 {
|
|
if let Ok((stream, _)) = listener.accept().await {
|
|
let _ = server.handle(stream).await;
|
|
}
|
|
}
|
|
});
|
|
|
|
sleep(Duration::from_millis(100)).await;
|
|
|
|
// Test PING
|
|
let response = send_redis_command(port, "*1\r\n$4\r\nPING\r\n").await;
|
|
assert!(response.contains("PONG"));
|
|
|
|
// Test SET
|
|
let response = send_redis_command(port, "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n").await;
|
|
assert!(response.contains("OK"));
|
|
|
|
// Test GET
|
|
let response = send_redis_command(port, "*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n").await;
|
|
assert!(response.contains("value"));
|
|
|
|
// Test HSET
|
|
let response = send_redis_command(port, "*4\r\n$4\r\nHSET\r\n$4\r\nhash\r\n$5\r\nfield\r\n$5\r\nvalue\r\n").await;
|
|
assert!(response.contains("1"));
|
|
|
|
// Test HGET
|
|
let response = send_redis_command(port, "*3\r\n$4\r\nHGET\r\n$4\r\nhash\r\n$5\r\nfield\r\n").await;
|
|
assert!(response.contains("value"));
|
|
|
|
// Test EXISTS
|
|
let response = send_redis_command(port, "*2\r\n$6\r\nEXISTS\r\n$3\r\nkey\r\n").await;
|
|
assert!(response.contains("1"));
|
|
|
|
// Test TTL
|
|
let response = send_redis_command(port, "*2\r\n$3\r\nTTL\r\n$3\r\nkey\r\n").await;
|
|
assert!(response.contains("-1")); // No expiration
|
|
|
|
// Test TYPE
|
|
let response = send_redis_command(port, "*2\r\n$4\r\nTYPE\r\n$3\r\nkey\r\n").await;
|
|
assert!(response.contains("string"));
|
|
|
|
// Test QUIT to close connection gracefully
|
|
let response = send_redis_command(port, "*1\r\n$4\r\nQUIT\r\n").await;
|
|
assert!(response.contains("OK"));
|
|
|
|
// Stop the server
|
|
server_handle.abort();
|
|
|
|
println!("✅ All basic Redis functionality tests passed!");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_hash_operations() {
|
|
let (mut server, port) = start_test_server("hash_ops").await;
|
|
|
|
// Start server in background with timeout
|
|
let server_handle = tokio::spawn(async move {
|
|
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Accept only a few connections for testing
|
|
for _ in 0..5 {
|
|
if let Ok((stream, _)) = listener.accept().await {
|
|
let _ = server.handle(stream).await;
|
|
}
|
|
}
|
|
});
|
|
|
|
sleep(Duration::from_millis(100)).await;
|
|
|
|
// Test HSET multiple fields
|
|
let response = send_redis_command(port, "*6\r\n$4\r\nHSET\r\n$4\r\nhash\r\n$6\r\nfield1\r\n$6\r\nvalue1\r\n$6\r\nfield2\r\n$6\r\nvalue2\r\n").await;
|
|
assert!(response.contains("2")); // 2 new fields
|
|
|
|
// Test HGETALL
|
|
let response = send_redis_command(port, "*2\r\n$7\r\nHGETALL\r\n$4\r\nhash\r\n").await;
|
|
assert!(response.contains("field1"));
|
|
assert!(response.contains("value1"));
|
|
assert!(response.contains("field2"));
|
|
assert!(response.contains("value2"));
|
|
|
|
// Test HEXISTS
|
|
let response = send_redis_command(port, "*3\r\n$7\r\nHEXISTS\r\n$4\r\nhash\r\n$6\r\nfield1\r\n").await;
|
|
assert!(response.contains("1"));
|
|
|
|
// Test HLEN
|
|
let response = send_redis_command(port, "*2\r\n$4\r\nHLEN\r\n$4\r\nhash\r\n").await;
|
|
assert!(response.contains("2"));
|
|
|
|
// Test HSCAN
|
|
let response = send_redis_command(port, "*6\r\n$5\r\nHSCAN\r\n$4\r\nhash\r\n$1\r\n0\r\n$5\r\nMATCH\r\n$1\r\n*\r\n$5\r\nCOUNT\r\n$2\r\n10\r\n").await;
|
|
assert!(response.contains("*2\r\n$1\r\n0\r\n*4\r\n$6\r\nfield1\r\n$6\r\nvalue1\r\n$6\r\nfield2\r\n$6\r\nvalue2\r\n"));
|
|
|
|
// Stop the server
|
|
server_handle.abort();
|
|
|
|
println!("✅ All hash operations tests passed!");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_transaction_operations() {
|
|
let (mut server, port) = start_test_server("transactions").await;
|
|
|
|
// Start server in background with timeout
|
|
let server_handle = tokio::spawn(async move {
|
|
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Accept only a few connections for testing
|
|
for _ in 0..5 {
|
|
if let Ok((stream, _)) = listener.accept().await {
|
|
let _ = server.handle(stream).await;
|
|
}
|
|
}
|
|
});
|
|
|
|
sleep(Duration::from_millis(100)).await;
|
|
|
|
// Use a single connection for the transaction
|
|
let mut stream = TcpStream::connect(format!("127.0.0.1:{}", port)).await.unwrap();
|
|
|
|
// Test MULTI
|
|
stream.write_all("*1\r\n$5\r\nMULTI\r\n".as_bytes()).await.unwrap();
|
|
let mut buffer = [0; 1024];
|
|
let n = stream.read(&mut buffer).await.unwrap();
|
|
let response = String::from_utf8_lossy(&buffer[..n]);
|
|
assert!(response.contains("OK"));
|
|
|
|
// Test queued commands
|
|
stream.write_all("*3\r\n$3\r\nSET\r\n$4\r\nkey1\r\n$6\r\nvalue1\r\n".as_bytes()).await.unwrap();
|
|
let n = stream.read(&mut buffer).await.unwrap();
|
|
let response = String::from_utf8_lossy(&buffer[..n]);
|
|
assert!(response.contains("QUEUED"));
|
|
|
|
stream.write_all("*3\r\n$3\r\nSET\r\n$4\r\nkey2\r\n$6\r\nvalue2\r\n".as_bytes()).await.unwrap();
|
|
let n = stream.read(&mut buffer).await.unwrap();
|
|
let response = String::from_utf8_lossy(&buffer[..n]);
|
|
assert!(response.contains("QUEUED"));
|
|
|
|
// Test EXEC
|
|
stream.write_all("*1\r\n$4\r\nEXEC\r\n".as_bytes()).await.unwrap();
|
|
let n = stream.read(&mut buffer).await.unwrap();
|
|
let response = String::from_utf8_lossy(&buffer[..n]);
|
|
assert!(response.contains("OK")); // Should contain array of OK responses
|
|
|
|
// Verify commands were executed
|
|
let response = send_redis_command(port, "*2\r\n$3\r\nGET\r\n$4\r\nkey1\r\n").await;
|
|
assert!(response.contains("value1"));
|
|
|
|
// Stop the server
|
|
server_handle.abort();
|
|
|
|
println!("✅ All transaction operations tests passed!");
|
|
} |