This commit is contained in:
2025-08-16 08:25:25 +02:00
parent 0f6e595000
commit 7bcb673361
15 changed files with 522 additions and 388 deletions

View File

@@ -0,0 +1,150 @@
]
# INFO
**What it does**
Returns server stats in a human-readable text block, optionally filtered by sections. Typical sections: `server`, `clients`, `memory`, `persistence`, `stats`, `replication`, `cpu`, `commandstats`, `latencystats`, `cluster`, `modules`, `keyspace`, `errorstats`. Special args: `all`, `default`, `everything`. The reply is a **Bulk String** with `# <Section>` headers and `key:value` lines. ([Redis][1])
**Syntax**
```
INFO [section [section ...]]
```
**Return (RESP2/RESP3)**: Bulk String. ([Redis][1])
**RESP request/response**
```
# Request: whole default set
*1\r\n$4\r\nINFO\r\n
# Request: a specific section, e.g., clients
*2\r\n$4\r\nINFO\r\n$7\r\nclients\r\n
# Response (prefix shown; body is long)
$1234\r\n# Server\r\nredis_version:7.4.0\r\n...\r\n# Clients\r\nconnected_clients:3\r\n...\r\n
```
(Reply type/format per RESP spec and the INFO page.) ([Redis][2])
---
# Connection “name” (there is **no** top-level `NAME` command)
Redis doesnt have a standalone `NAME` command. Connection names are handled via `CLIENT SETNAME` and retrieved via `CLIENT GETNAME`. ([Redis][3])
## CLIENT SETNAME
Assigns a human label to the current connection (shown in `CLIENT LIST`, logs, etc.). No spaces allowed in the name; empty string clears it. Length is limited by Redis string limits (practically huge). **Reply**: Simple String `OK`. ([Redis][4])
**Syntax**
```
CLIENT SETNAME connection-name
```
**RESP**
```
# Set the name "myapp"
*3\r\n$6\r\nCLIENT\r\n$7\r\nSETNAME\r\n$5\r\nmyapp\r\n
# Reply
+OK\r\n
```
## CLIENT GETNAME
Returns the current connections name or **Null Bulk String** if unset. ([Redis][5])
**Syntax**
```
CLIENT GETNAME
```
**RESP**
```
# Before SETNAME:
*2\r\n$6\r\nCLIENT\r\n$7\r\nGETNAME\r\n
$-1\r\n # nil (no name)
# After SETNAME myapp:
*2\r\n$6\r\nCLIENT\r\n$7\r\nGETNAME\r\n
$5\r\nmyapp\r\n
```
(Null/Bulk String encoding per RESP spec.) ([Redis][2])
---
# CLIENT (container command + key subcommands)
`CLIENT` is a **container**; use subcommands like `CLIENT LIST`, `CLIENT INFO`, `CLIENT ID`, `CLIENT KILL`, `CLIENT TRACKING`, etc. Call `CLIENT HELP` to enumerate them. ([Redis][3])
## CLIENT LIST
Shows all connections as a single **Bulk String**: one line per client with `field=value` pairs (includes `id`, `addr`, `name`, `db`, `user`, `resp`, and more). Filters: `TYPE` and `ID`. **Return**: Bulk String (RESP2/RESP3). ([Redis][6])
**Syntax**
```
CLIENT LIST [TYPE <NORMAL|MASTER|REPLICA|PUBSUB>] [ID client-id ...]
```
**RESP**
```
*2\r\n$6\r\nCLIENT\r\n$4\r\nLIST\r\n
# Reply (single Bulk String; example with one line shown)
$188\r\nid=7 addr=127.0.0.1:60840 laddr=127.0.0.1:6379 fd=8 name=myapp age=12 idle=3 flags=N db=0 ...\r\n
```
## CLIENT INFO
Returns info for **this** connection only (same format/fields as a single line of `CLIENT LIST`). **Return**: Bulk String. Available since 6.2.0. ([Redis][7])
**Syntax**
```
CLIENT INFO
```
**RESP**
```
*2\r\n$6\r\nCLIENT\r\n$4\r\nINFO\r\n
$160\r\nid=7 addr=127.0.0.1:60840 laddr=127.0.0.1:6379 fd=8 name=myapp db=0 user=default resp=2 ...\r\n
```
---
# RESP notes youll need for your parser
* **Requests** are Arrays: `*N\r\n` followed by `N` Bulk Strings for verb/args.
* **Common replies here**: Simple String (`+OK\r\n`), Bulk String (`$<len>\r\n...\r\n`), and **Null Bulk String** (`$-1\r\n`). (These cover `INFO`, `CLIENT LIST/INFO`, `CLIENT GETNAME`, `CLIENT SETNAME`.) ([Redis][2])
---
## Sources (checked)
* INFO command (syntax, sections, behavior). ([Redis][1])
* RESP spec (request/response framing, Bulk/Null Bulk Strings). ([Redis][2])
* CLIENT container + subcommands index. ([Redis][3])
* CLIENT LIST (fields, bulk-string return, filters). ([Redis][6])
* CLIENT INFO (exists since 6.2, reply format). ([Redis][7])
* CLIENT SETNAME (no spaces; clears with empty string; huge length OK). ([Redis][4])
* CLIENT GETNAME (nil if unset). ([Redis][5])
If you want, I can fold this into a tiny Rust “command + RESP” test harness that exercises `INFO`, `CLIENT SETNAME/GETNAME`, `CLIENT LIST`, and `CLIENT INFO` against your in-mem RESP parser.
[1]: https://redis.io/docs/latest/commands/info/ "INFO | Docs"
[2]: https://redis.io/docs/latest/develop/reference/protocol-spec/?utm_source=chatgpt.com "Redis serialization protocol specification | Docs"
[3]: https://redis.io/docs/latest/commands/client/ "CLIENT | Docs"
[4]: https://redis.io/docs/latest/commands/client-setname/?utm_source=chatgpt.com "CLIENT SETNAME | Docs"
[5]: https://redis.io/docs/latest/commands/client-getname/?utm_source=chatgpt.com "CLIENT GETNAME | Docs"
[6]: https://redis.io/docs/latest/commands/client-list/ "CLIENT LIST | Docs"
[7]: https://redis.io/docs/latest/commands/client-info/?utm_source=chatgpt.com "CLIENT INFO | Docs"

View File

@@ -248,3 +248,4 @@ Protocol and reply structure same as SCAN.
| `SSCAN` | Iterate over set members | `[cursor, array]` |
| `ZSCAN` | Iterate over sorted set | `[cursor, array]` |
##

View File

@@ -12,6 +12,7 @@ echo ""
echo "2⃣ Running Comprehensive Redis Integration Tests (13 tests)..."
echo "----------------------------------------------------------------"
cargo test --test redis_integration_tests -- --nocapture
cargo test --test redis_basic_client -- --nocapture
echo ""
echo "3⃣ Running All Tests..."

View File

@@ -33,7 +33,10 @@ pub enum Cmd {
Ttl(String),
Exists(String),
Quit,
Unknow,
Client(Vec<String>),
ClientSetName(String),
ClientGetName,
Unknow(String),
}
impl Cmd {
@@ -274,7 +277,30 @@ impl Cmd {
}
Cmd::Quit
}
_ => Cmd::Unknow,
"client" => {
if cmd.len() > 1 {
match cmd[1].to_lowercase().as_str() {
"setname" => {
if cmd.len() == 3 {
Cmd::ClientSetName(cmd[2].clone())
} else {
return Err(DBError("wrong number of arguments for 'client setname' command".to_string()));
}
}
"getname" => {
if cmd.len() == 2 {
Cmd::ClientGetName
} else {
return Err(DBError("wrong number of arguments for 'client getname' command".to_string()));
}
}
_ => Cmd::Client(cmd[1..].to_vec()),
}
} else {
Cmd::Client(vec![])
}
}
_ => Cmd::Unknow(cmd[0].clone()),
},
protocol.0,
))
@@ -288,7 +314,7 @@ impl Cmd {
pub async fn run(
&self,
server: &Server,
server: &mut Server,
protocol: Protocol,
queued_cmd: &mut Option<Vec<(Cmd, Protocol)>>,
) -> Result<Protocol, DBError> {
@@ -347,14 +373,20 @@ impl Cmd {
Cmd::Ttl(key) => ttl_cmd(server, key).await,
Cmd::Exists(key) => exists_cmd(server, key).await,
Cmd::Quit => Ok(Protocol::SimpleString("OK".to_string())),
Cmd::Unknow => Ok(Protocol::err("unknown cmd")),
Cmd::Client(_) => Ok(Protocol::SimpleString("OK".to_string())),
Cmd::ClientSetName(name) => client_setname_cmd(server, name).await,
Cmd::ClientGetName => client_getname_cmd(server).await,
Cmd::Unknow(s) => {
println!("\x1b[31;1munknown command: {}\x1b[0m", s);
Ok(Protocol::err(&format!("ERR unknown command '{}'", s)))
}
}
}
}
async fn exec_cmd(
queued_cmd: &mut Option<Vec<(Cmd, Protocol)>>,
server: &Server,
server: &mut Server,
) -> Result<Protocol, DBError> {
if queued_cmd.is_some() {
let mut vec = Vec::new();
@@ -593,3 +625,15 @@ async fn exists_cmd(server: &Server, key: &str) -> Result<Protocol, DBError> {
Err(e) => Ok(Protocol::err(&e.0)),
}
}
async fn client_setname_cmd(server: &mut Server, name: &str) -> Result<Protocol, DBError> {
server.client_name = Some(name.to_string());
Ok(Protocol::SimpleString("OK".to_string()))
}
async fn client_getname_cmd(server: &Server) -> Result<Protocol, DBError> {
match &server.client_name {
Some(name) => Ok(Protocol::BulkString(name.clone())),
None => Ok(Protocol::Null),
}
}

View File

@@ -4,7 +4,6 @@ use tokio::sync::mpsc;
use redb;
use bincode;
use crate::protocol::Protocol;
// todo: more error types
#[derive(Debug)]

View File

@@ -18,6 +18,10 @@ struct Args {
/// The port of the Redis server, default is 6379 if not specified
#[arg(long)]
port: Option<u16>,
/// Enable debug mode
#[arg(long)]
debug: bool,
}
#[tokio::main]
@@ -36,11 +40,15 @@ async fn main() {
let option = redis_rs::options::DBOption {
dir: args.dir,
port,
debug: args.debug,
};
// new server
let server = server::Server::new(option).await;
// Add a small delay to ensure the port is ready
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
// accept new connections
loop {
let stream = listener.accept().await;

View File

@@ -2,4 +2,5 @@
pub struct DBOption {
pub dir: String,
pub port: u16,
pub debug: bool,
}

View File

@@ -159,18 +159,21 @@ impl Protocol {
}
fn parse_usize(protocol: &str) -> Result<usize, DBError> {
match protocol.len() {
0 => Err(DBError(format!("parse usize error: {:?}", protocol))),
_ => Ok(protocol
if protocol.is_empty() {
Err(DBError("Cannot parse usize from empty string".to_string()))
} else {
protocol
.parse::<usize>()
.map_err(|_| DBError(format!("parse usize error: {}", protocol)))?),
.map_err(|_| DBError(format!("Failed to parse usize from: {}", protocol)))
}
}
fn parse_string(protocol: &str) -> Result<String, DBError> {
match protocol.len() {
0 => Err(DBError(format!("parse usize error: {:?}", protocol))),
_ => Ok(protocol.to_string()),
if protocol.is_empty() {
// Allow empty strings, but handle appropriately
Ok("".to_string())
} else {
Ok(protocol.to_string())
}
}
}

View File

@@ -14,6 +14,7 @@ use crate::storage::Storage;
pub struct Server {
pub storage: Arc<Storage>,
pub option: options::DBOption,
pub client_name: Option<String>,
}
impl Server {
@@ -28,6 +29,7 @@ impl Server {
Server {
storage: Arc::new(storage),
option,
client_name: None,
}
}
@@ -46,20 +48,37 @@ impl Server {
}
let s = str::from_utf8(&buf[..len])?;
let (cmd, protocol) =
Cmd::from(s).unwrap_or((Cmd::Unknow, Protocol::err("unknow cmd")));
println!("got command: {:?}, protocol: {:?}", cmd, protocol);
let (cmd, protocol) = match Cmd::from(s) {
Ok((cmd, protocol)) => (cmd, protocol),
Err(e) => {
println!("\x1b[31;1mprotocol error: {:?}\x1b[0m", e);
(Cmd::Unknow("protocol_error".to_string()), Protocol::err(&format!("protocol error: {}", e.0)))
}
};
if self.option.debug {
println!("\x1b[34;1mgot command: {:?}, protocol: {:?}\x1b[0m", cmd, protocol);
} else {
println!("got command: {:?}, protocol: {:?}", cmd, protocol);
}
// Check if this is a QUIT command before processing
let is_quit = matches!(cmd, Cmd::Quit);
let res = cmd
.run(self, protocol, &mut queued_cmd)
.run(&mut self.clone(), protocol.clone(), &mut queued_cmd)
.await
.unwrap_or(Protocol::err("unknow cmd"));
print!("queued cmd {:?}", queued_cmd);
.unwrap_or(Protocol::err("unknown cmd from server"));
if self.option.debug {
println!("\x1b[34;1mqueued cmd {:?}\x1b[0m", queued_cmd);
} else {
print!("queued cmd {:?}", queued_cmd);
}
println!("going to send response {}", res.encode());
if self.option.debug {
println!("\x1b[32;1mgoing to send response {}\x1b[0m", res.encode());
} else {
println!("going to send response {}", res.encode());
}
_ = stream.write(res.encode().as_bytes()).await?;
// If this was a QUIT command, close the connection

View File

@@ -3,7 +3,7 @@ use std::{
time::{SystemTime, UNIX_EPOCH},
};
use redb::{Database, Error, ReadableTable, Table, TableDefinition, WriteTransaction, ReadTransaction};
use redb::{Database, ReadableTable, TableDefinition};
use serde::{Deserialize, Serialize};
use crate::error::DBError;
@@ -493,7 +493,6 @@ impl Storage {
// Stop if we've returned enough keys
if returned_keys >= count {
current_cursor += 1;
break;
}
}
@@ -502,7 +501,7 @@ impl Storage {
}
// If we've reached the end of iteration, return cursor 0 to indicate completion
let next_cursor = if returned_keys < count { 0 } else { current_cursor };
let next_cursor = if iter.next().is_none() { 0 } else { current_cursor };
Ok((next_cursor, keys))
}
@@ -563,7 +562,6 @@ impl Storage {
returned_fields += 1;
if returned_fields >= count {
current_cursor += 1;
break;
}
}
@@ -571,7 +569,7 @@ impl Storage {
current_cursor += 1;
}
let next_cursor = if returned_fields < count { 0 } else { current_cursor };
let next_cursor = if iter.next().is_none() { 0 } else { current_cursor };
Ok((next_cursor, fields))
}
Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),

View File

@@ -24,6 +24,7 @@ async fn debug_hset_simple() {
let option = DBOption {
dir: test_dir.to_string(),
port,
debug: false,
};
let mut server = Server::new(option).await;

View File

@@ -15,6 +15,7 @@ async fn debug_hset_return_value() {
let option = DBOption {
dir: test_dir.to_string(),
port: 16390,
debug: false,
};
let mut server = Server::new(option).await;

View File

@@ -0,0 +1,30 @@
mod test_utils;
use test_utils::run_inst_redis;
#[test]
fn test_cmd_client_getname_setname() {
let instructions = r#"
[
{
"command": "start-server",
"port": 6380,
"args": ["--debug"]
},
{
"command": "send-redis-raw",
"port": 6380,
"payload": "*3\r\n$6\r\nCLIENT\r\n$7\r\nSETNAME\r\n$5\r\nmyapp\r\n",
"assert": "simple-string",
"value": "OK"
},
{
"command": "send-redis-raw",
"port": 6380,
"payload": "*2\r\n$6\r\nCLIENT\r\n$7\r\nGETNAME\r\n",
"assert": "bulk-string",
"value": "myapp"
}
]
"#;
run_inst_redis(instructions);
}

View File

@@ -1,296 +1,238 @@
use redis_rs::{server::Server, options::DBOption};
use redis::{Client, Commands, Connection};
use std::process::{Child, Command};
use std::time::Duration;
use tokio::time::{sleep, timeout};
use tokio::sync::oneshot;
use tokio::time::sleep;
// 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(16400);
// Get a unique port for this test
let port = PORT_COUNTER.fetch_add(1, Ordering::SeqCst);
// Ensure port is available by trying to bind to it first
let mut attempts = 0;
let final_port = loop {
let test_port = port + attempts;
match tokio::net::TcpListener::bind(format!("127.0.0.1:{}", test_port)).await {
Ok(_) => break test_port,
Err(_) => {
attempts += 1;
if attempts > 100 {
panic!("Could not find available port after 100 attempts");
}
}
}
};
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: final_port,
};
let server = Server::new(option).await;
(server, final_port)
}
// Helper function to get Redis connection
// Helper function to get Redis connection, retrying until successful
fn get_redis_connection(port: u16) -> Connection {
let client = Client::open(format!("redis://127.0.0.1:{}/", port)).unwrap();
let connection_info = format!("redis://127.0.0.1:{}", port);
let client = Client::open(connection_info).unwrap();
let mut attempts = 0;
loop {
match client.get_connection() {
Ok(conn) => return conn,
Err(_) if attempts < 20 => {
attempts += 1;
std::thread::sleep(Duration::from_millis(100));
Ok(mut conn) => {
if redis::cmd("PING").query::<String>(&mut conn).is_ok() {
return conn;
}
}
Err(e) => panic!("Failed to connect to Redis server: {}", e),
Err(e) => {
if attempts >= 20 {
panic!(
"Failed to connect to Redis server after 20 attempts: {}",
e
);
}
}
}
attempts += 1;
std::thread::sleep(Duration::from_millis(100));
}
}
// A guard to ensure the server process is killed when it goes out of scope
struct ServerProcessGuard {
process: Child,
test_dir: String,
}
impl Drop for ServerProcessGuard {
fn drop(&mut self) {
println!("Killing server process (pid: {})...", self.process.id());
if let Err(e) = self.process.kill() {
eprintln!("Failed to kill server process: {}", e);
}
match self.process.wait() {
Ok(status) => println!("Server process exited with: {}", status),
Err(e) => eprintln!("Failed to wait on server process: {}", e),
}
// Clean up the specific test directory
println!("Cleaning up test directory: {}", self.test_dir);
if let Err(e) = std::fs::remove_dir_all(&self.test_dir) {
eprintln!("Failed to clean up test directory: {}", e);
}
}
}
#[tokio::test]
async fn test_basic_ping() {
let (mut server, port) = start_test_server("ping").await;
// Helper to set up the server and return a connection
fn setup_server() -> (ServerProcessGuard, u16) {
use std::sync::atomic::{AtomicU16, Ordering};
static PORT_COUNTER: AtomicU16 = AtomicU16::new(16400);
let port = PORT_COUNTER.fetch_add(1, Ordering::SeqCst);
let test_dir = format!("/tmp/herodb_test_{}", port);
// Clean up previous test data
if std::path::Path::new(&test_dir).exists() {
let _ = std::fs::remove_dir_all(&test_dir);
}
std::fs::create_dir_all(&test_dir).unwrap();
// Start the server in a subprocess
let child = Command::new("cargo")
.args(&[
"run",
"--",
"--dir",
&test_dir,
"--port",
&port.to_string(),
])
.spawn()
.expect("Failed to start server process");
// Start server in background
tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.unwrap();
loop {
if let Ok((stream, _)) = listener.accept().await {
let _ = server.handle(stream).await;
}
}
});
sleep(Duration::from_millis(200)).await;
let mut conn = get_redis_connection(port);
let result: String = redis::cmd("PING").query(&mut conn).unwrap();
assert_eq!(result, "PONG");
// Create a new guard that also owns the test directory path
let guard = ServerProcessGuard {
process: child,
test_dir,
};
(guard, port)
}
#[tokio::test]
async fn test_string_operations() {
let (mut server, port) = start_test_server("string").await;
tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.unwrap();
loop {
if let Ok((stream, _)) = listener.accept().await {
let _ = server.handle(stream).await;
}
}
});
sleep(Duration::from_millis(200)).await;
async fn all_tests() {
let (_server_guard, port) = setup_server();
let mut conn = get_redis_connection(port);
// Run all tests using the same connection
test_basic_ping(&mut conn).await;
test_string_operations(&mut conn).await;
test_incr_operations(&mut conn).await;
test_hash_operations(&mut conn).await;
test_expiration(&mut conn).await;
test_scan_operations(&mut conn).await;
test_scan_with_count(&mut conn).await;
test_hscan_operations(&mut conn).await;
test_transaction_operations(&mut conn).await;
test_discard_transaction(&mut conn).await;
test_type_command(&mut conn).await;
test_config_commands(&mut conn).await;
test_info_command(&mut conn).await;
test_error_handling(&mut conn).await;
}
async fn test_basic_ping(conn: &mut Connection) {
let result: String = redis::cmd("PING").query(conn).unwrap();
assert_eq!(result, "PONG");
}
async fn test_string_operations(conn: &mut Connection) {
// Test SET
let _: () = conn.set("key", "value").unwrap();
// Test GET
let result: String = conn.get("key").unwrap();
assert_eq!(result, "value");
// Test GET non-existent key
let result: Option<String> = conn.get("noexist").unwrap();
assert_eq!(result, None);
// Test DEL
let deleted: i32 = conn.del("key").unwrap();
assert_eq!(deleted, 1);
// Test GET after DEL
let result: Option<String> = conn.get("key").unwrap();
assert_eq!(result, None);
}
#[tokio::test]
async fn test_incr_operations() {
let (mut server, port) = start_test_server("incr").await;
tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.unwrap();
loop {
if let Ok((stream, _)) = listener.accept().await {
let _ = server.handle(stream).await;
}
}
});
sleep(Duration::from_millis(200)).await;
let mut conn = get_redis_connection(port);
async fn test_incr_operations(conn: &mut Connection) {
// Test INCR on non-existent key
let result: i32 = conn.incr("counter", 1).unwrap();
assert_eq!(result, 1);
// Test INCR on existing key
let result: i32 = conn.incr("counter", 1).unwrap();
assert_eq!(result, 2);
// Test INCR on string value (should fail)
let _: () = conn.set("string", "hello").unwrap();
let result: Result<i32, _> = conn.incr("string", 1);
assert!(result.is_err());
}
#[tokio::test]
async fn test_hash_operations() {
let (mut server, port) = start_test_server("hash").await;
tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.unwrap();
loop {
if let Ok((stream, _)) = listener.accept().await {
let _ = server.handle(stream).await;
}
}
});
sleep(Duration::from_millis(200)).await;
let mut conn = get_redis_connection(port);
async fn test_hash_operations(conn: &mut Connection) {
// Test HSET
let result: i32 = conn.hset("hash", "field1", "value1").unwrap();
assert_eq!(result, 1); // 1 new field
// Test HGET
let result: String = conn.hget("hash", "field1").unwrap();
assert_eq!(result, "value1");
// Test HSET multiple fields
let _: () = conn.hset_multiple("hash", &[("field2", "value2"), ("field3", "value3")]).unwrap();
// Test HGETALL
let result: std::collections::HashMap<String, String> = conn.hgetall("hash").unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result.get("field1").unwrap(), "value1");
assert_eq!(result.get("field2").unwrap(), "value2");
assert_eq!(result.get("field3").unwrap(), "value3");
// Test HLEN
let result: i32 = conn.hlen("hash").unwrap();
assert_eq!(result, 3);
// Test HEXISTS
let result: bool = conn.hexists("hash", "field1").unwrap();
assert_eq!(result, true);
let result: bool = conn.hexists("hash", "noexist").unwrap();
assert_eq!(result, false);
// Test HDEL
let result: i32 = conn.hdel("hash", "field1").unwrap();
assert_eq!(result, 1);
// Test HKEYS
let mut result: Vec<String> = conn.hkeys("hash").unwrap();
result.sort();
assert_eq!(result, vec!["field2", "field3"]);
// Test HVALS
let mut result: Vec<String> = conn.hvals("hash").unwrap();
result.sort();
assert_eq!(result, vec!["value2", "value3"]);
}
#[tokio::test]
async fn test_expiration() {
let (mut server, port) = start_test_server("expiration").await;
tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.unwrap();
loop {
if let Ok((stream, _)) = listener.accept().await {
let _ = server.handle(stream).await;
}
}
});
sleep(Duration::from_millis(200)).await;
let mut conn = get_redis_connection(port);
async fn test_expiration(conn: &mut Connection) {
// Test SETEX (expire in 1 second)
let _: () = conn.set_ex("expkey", "value", 1).unwrap();
// Test TTL
let result: i32 = conn.ttl("expkey").unwrap();
assert!(result == 1 || result == 0); // Should be 1 or 0 seconds
// Test EXISTS
let result: bool = conn.exists("expkey").unwrap();
assert_eq!(result, true);
// Wait for expiration
sleep(Duration::from_millis(1100)).await;
// Test GET after expiration
let result: Option<String> = conn.get("expkey").unwrap();
assert_eq!(result, None);
// Test TTL after expiration
let result: i32 = conn.ttl("expkey").unwrap();
assert_eq!(result, -2); // Key doesn't exist
// Test EXISTS after expiration
let result: bool = conn.exists("expkey").unwrap();
assert_eq!(result, false);
}
#[tokio::test]
async fn test_scan_operations() {
let (mut server, port) = start_test_server("scan").await;
tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.unwrap();
loop {
if let Ok((stream, _)) = listener.accept().await {
let _ = server.handle(stream).await;
}
}
});
sleep(Duration::from_millis(200)).await;
let mut conn = get_redis_connection(port);
async fn test_scan_operations(conn: &mut Connection) {
// Set up test data
for i in 0..5 {
let _: () = conn.set(format!("key{}", i), format!("value{}", i)).unwrap();
}
// Test SCAN
let result: (u64, Vec<String>) = redis::cmd("SCAN")
.arg(0)
@@ -298,44 +240,93 @@ async fn test_scan_operations() {
.arg("*")
.arg("COUNT")
.arg(10)
.query(&mut conn)
.query(conn)
.unwrap();
let (cursor, keys) = result;
assert_eq!(cursor, 0); // Should complete in one scan
assert_eq!(keys.len(), 5);
// Test KEYS
let mut result: Vec<String> = redis::cmd("KEYS").arg("*").query(&mut conn).unwrap();
let mut result: Vec<String> = redis::cmd("KEYS").arg("*").query(conn).unwrap();
result.sort();
assert_eq!(result, vec!["key0", "key1", "key2", "key3", "key4"]);
}
#[tokio::test]
async fn test_hscan_operations() {
let (mut server, port) = start_test_server("hscan").await;
tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.unwrap();
loop {
if let Ok((stream, _)) = listener.accept().await {
let _ = server.handle(stream).await;
}
}
});
sleep(Duration::from_millis(200)).await;
let mut conn = get_redis_connection(port);
async fn test_scan_with_count(conn: &mut Connection) {
// Clean up previous keys
let keys: Vec<String> = redis::cmd("KEYS").arg("scan_key*").query(conn).unwrap();
if !keys.is_empty() {
let _: () = redis::cmd("DEL").arg(keys).query(conn).unwrap();
}
// Set up test data
for i in 0..15 {
let _: () = conn.set(format!("scan_key{}", i), i).unwrap();
}
let mut cursor = 0;
let mut all_keys = std::collections::HashSet::new();
// First SCAN
let (next_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN")
.arg(cursor)
.arg("MATCH")
.arg("scan_key*")
.arg("COUNT")
.arg(5)
.query(conn)
.unwrap();
assert_ne!(next_cursor, 0);
assert_eq!(keys.len(), 5);
for key in keys {
all_keys.insert(key);
}
cursor = next_cursor;
// Second SCAN
let (next_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN")
.arg(cursor)
.arg("MATCH")
.arg("scan_key*")
.arg("COUNT")
.arg(5)
.query(conn)
.unwrap();
assert_ne!(next_cursor, 0);
assert_eq!(keys.len(), 5);
for key in keys {
all_keys.insert(key);
}
cursor = next_cursor;
// Final SCAN
let (next_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN")
.arg(cursor)
.arg("MATCH")
.arg("scan_key*")
.arg("COUNT")
.arg(5)
.query(conn)
.unwrap();
assert_eq!(next_cursor, 0);
assert_eq!(keys.len(), 5);
for key in keys {
all_keys.insert(key);
}
assert_eq!(all_keys.len(), 15);
}
async fn test_hscan_operations(conn: &mut Connection) {
// Set up hash data
for i in 0..3 {
let _: () = conn.hset("testhash", format!("field{}", i), format!("value{}", i)).unwrap();
}
// Test HSCAN
let result: (u64, Vec<String>) = redis::cmd("HSCAN")
.arg("testhash")
@@ -344,214 +335,100 @@ async fn test_hscan_operations() {
.arg("*")
.arg("COUNT")
.arg(10)
.query(&mut conn)
.query(conn)
.unwrap();
let (cursor, fields) = result;
assert_eq!(cursor, 0); // Should complete in one scan
assert_eq!(fields.len(), 6); // 3 field-value pairs = 6 elements
}
#[tokio::test]
async fn test_transaction_operations() {
let (mut server, port) = start_test_server("transaction").await;
tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.unwrap();
loop {
if let Ok((stream, _)) = listener.accept().await {
let _ = server.handle(stream).await;
}
}
});
sleep(Duration::from_millis(200)).await;
let mut conn = get_redis_connection(port);
async fn test_transaction_operations(conn: &mut Connection) {
// Test MULTI/EXEC
let _: () = redis::cmd("MULTI").query(&mut conn).unwrap();
let _: () = redis::cmd("SET").arg("key1").arg("value1").query(&mut conn).unwrap();
let _: () = redis::cmd("SET").arg("key2").arg("value2").query(&mut conn).unwrap();
let _: Vec<String> = redis::cmd("EXEC").query(&mut conn).unwrap();
let _: () = redis::cmd("MULTI").query(conn).unwrap();
let _: () = redis::cmd("SET").arg("key1").arg("value1").query(conn).unwrap();
let _: () = redis::cmd("SET").arg("key2").arg("value2").query(conn).unwrap();
let _: Vec<String> = redis::cmd("EXEC").query(conn).unwrap();
// Verify commands were executed
let result: String = conn.get("key1").unwrap();
assert_eq!(result, "value1");
let result: String = conn.get("key2").unwrap();
assert_eq!(result, "value2");
}
#[tokio::test]
async fn test_discard_transaction() {
let (mut server, port) = start_test_server("discard").await;
tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.unwrap();
loop {
if let Ok((stream, _)) = listener.accept().await {
let _ = server.handle(stream).await;
}
}
});
sleep(Duration::from_millis(200)).await;
let mut conn = get_redis_connection(port);
async fn test_discard_transaction(conn: &mut Connection) {
// Test MULTI/DISCARD
let _: () = redis::cmd("MULTI").query(&mut conn).unwrap();
let _: () = redis::cmd("SET").arg("discard").arg("value").query(&mut conn).unwrap();
let _: () = redis::cmd("DISCARD").query(&mut conn).unwrap();
let _: () = redis::cmd("MULTI").query(conn).unwrap();
let _: () = redis::cmd("SET").arg("discard").arg("value").query(conn).unwrap();
let _: () = redis::cmd("DISCARD").query(conn).unwrap();
// Verify command was not executed
let result: Option<String> = conn.get("discard").unwrap();
assert_eq!(result, None);
}
#[tokio::test]
async fn test_type_command() {
let (mut server, port) = start_test_server("type").await;
tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.unwrap();
loop {
if let Ok((stream, _)) = listener.accept().await {
let _ = server.handle(stream).await;
}
}
});
sleep(Duration::from_millis(200)).await;
let mut conn = get_redis_connection(port);
async fn test_type_command(conn: &mut Connection) {
// Test string type
let _: () = conn.set("string", "value").unwrap();
let result: String = redis::cmd("TYPE").arg("string").query(&mut conn).unwrap();
let result: String = redis::cmd("TYPE").arg("string").query(conn).unwrap();
assert_eq!(result, "string");
// Test hash type
let _: () = conn.hset("hash", "field", "value").unwrap();
let result: String = redis::cmd("TYPE").arg("hash").query(&mut conn).unwrap();
let result: String = redis::cmd("TYPE").arg("hash").query(conn).unwrap();
assert_eq!(result, "hash");
// Test non-existent key
let result: String = redis::cmd("TYPE").arg("noexist").query(&mut conn).unwrap();
let result: String = redis::cmd("TYPE").arg("noexist").query(conn).unwrap();
assert_eq!(result, "none");
}
#[tokio::test]
async fn test_config_commands() {
let (mut server, port) = start_test_server("config").await;
tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.unwrap();
loop {
if let Ok((stream, _)) = listener.accept().await {
let _ = server.handle(stream).await;
}
}
});
sleep(Duration::from_millis(200)).await;
let mut conn = get_redis_connection(port);
async fn test_config_commands(conn: &mut Connection) {
// Test CONFIG GET databases
let result: Vec<String> = redis::cmd("CONFIG")
.arg("GET")
.arg("databases")
.query(&mut conn)
.query(conn)
.unwrap();
assert_eq!(result, vec!["databases", "16"]);
// Test CONFIG GET dir
let result: Vec<String> = redis::cmd("CONFIG")
.arg("GET")
.arg("dir")
.query(&mut conn)
.query(conn)
.unwrap();
assert_eq!(result[0], "dir");
assert!(result[1].contains("/tmp/herodb_test_config"));
assert!(result[1].contains("/tmp/herodb_test_"));
}
#[tokio::test]
async fn test_info_command() {
let (mut server, port) = start_test_server("info").await;
tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.unwrap();
loop {
if let Ok((stream, _)) = listener.accept().await {
let _ = server.handle(stream).await;
}
}
});
sleep(Duration::from_millis(200)).await;
let mut conn = get_redis_connection(port);
async fn test_info_command(conn: &mut Connection) {
// Test INFO
let result: String = redis::cmd("INFO").query(&mut conn).unwrap();
let result: String = redis::cmd("INFO").query(conn).unwrap();
assert!(result.contains("redis_version"));
// Test INFO replication
let result: String = redis::cmd("INFO").arg("replication").query(&mut conn).unwrap();
let result: String = redis::cmd("INFO").arg("replication").query(conn).unwrap();
assert!(result.contains("role:master"));
}
#[tokio::test]
async fn test_error_handling() {
let (mut server, port) = start_test_server("error").await;
tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.unwrap();
loop {
if let Ok((stream, _)) = listener.accept().await {
let _ = server.handle(stream).await;
}
}
});
sleep(Duration::from_millis(200)).await;
let mut conn = get_redis_connection(port);
async fn test_error_handling(conn: &mut Connection) {
// Test WRONGTYPE error - try to use hash command on string
let _: () = conn.set("string", "value").unwrap();
let result: Result<String, _> = conn.hget("string", "field");
assert!(result.is_err());
// Test unknown command
let result: Result<String, _> = redis::cmd("UNKNOWN").query(&mut conn);
let result: Result<String, _> = redis::cmd("UNKNOWN").query(conn);
assert!(result.is_err());
// Test EXEC without MULTI
let result: Result<Vec<String>, _> = redis::cmd("EXEC").query(&mut conn);
let result: Result<Vec<String>, _> = redis::cmd("EXEC").query(conn);
assert!(result.is_err());
// Test DISCARD without MULTI
let result: Result<(), _> = redis::cmd("DISCARD").query(&mut conn);
let result: Result<(), _> = redis::cmd("DISCARD").query(conn);
assert!(result.is_err());
}

View File

@@ -19,6 +19,7 @@ async fn start_test_server(test_name: &str) -> (Server, u16) {
let option = DBOption {
dir: test_dir,
port,
debug: false,
};
let server = Server::new(option).await;