From 7bcb67336144ecbdd00a0cbb176aad35b9fed1e7 Mon Sep 17 00:00:00 2001 From: despiegk Date: Sat, 16 Aug 2025 08:25:25 +0200 Subject: [PATCH] ... --- instructions/redis_basic_client.md | 150 +++++++ instructions/redis_basics.md | 1 + run_tests.sh | 1 + src/cmd.rs | 54 ++- src/error.rs | 1 - src/main.rs | 8 + src/options.rs | 1 + src/protocol.rs | 17 +- src/server.rs | 33 +- src/storage.rs | 8 +- tests/debug_hset.rs | 1 + tests/debug_hset_simple.rs | 1 + tests/redis_basic_client.rs | 30 ++ tests/redis_integration_tests.rs | 603 ++++++++++++----------------- tests/simple_redis_test.rs | 1 + 15 files changed, 522 insertions(+), 388 deletions(-) create mode 100644 instructions/redis_basic_client.md create mode 100644 tests/redis_basic_client.rs diff --git a/instructions/redis_basic_client.md b/instructions/redis_basic_client.md new file mode 100644 index 0000000..f6a90e4 --- /dev/null +++ b/instructions/redis_basic_client.md @@ -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 `#
` 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 doesn’t 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 connection’s 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 ] [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 you’ll 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 (`$\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" diff --git a/instructions/redis_basics.md b/instructions/redis_basics.md index 1e0442e..302dd1e 100644 --- a/instructions/redis_basics.md +++ b/instructions/redis_basics.md @@ -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]` | +## \ No newline at end of file diff --git a/run_tests.sh b/run_tests.sh index 160e162..675ee2a 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -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..." diff --git a/src/cmd.rs b/src/cmd.rs index 3dd26e9..1e29178 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -33,7 +33,10 @@ pub enum Cmd { Ttl(String), Exists(String), Quit, - Unknow, + Client(Vec), + 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>, ) -> Result { @@ -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>, - server: &Server, + server: &mut Server, ) -> Result { if queued_cmd.is_some() { let mut vec = Vec::new(); @@ -593,3 +625,15 @@ async fn exists_cmd(server: &Server, key: &str) -> Result { Err(e) => Ok(Protocol::err(&e.0)), } } + +async fn client_setname_cmd(server: &mut Server, name: &str) -> Result { + server.client_name = Some(name.to_string()); + Ok(Protocol::SimpleString("OK".to_string())) +} + +async fn client_getname_cmd(server: &Server) -> Result { + match &server.client_name { + Some(name) => Ok(Protocol::BulkString(name.clone())), + None => Ok(Protocol::Null), + } +} diff --git a/src/error.rs b/src/error.rs index feed31e..018e878 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,7 +4,6 @@ use tokio::sync::mpsc; use redb; use bincode; -use crate::protocol::Protocol; // todo: more error types #[derive(Debug)] diff --git a/src/main.rs b/src/main.rs index 12ebc7c..40abf4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,10 @@ struct Args { /// The port of the Redis server, default is 6379 if not specified #[arg(long)] port: Option, + + /// 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; diff --git a/src/options.rs b/src/options.rs index 77afc2b..6e50a1c 100644 --- a/src/options.rs +++ b/src/options.rs @@ -2,4 +2,5 @@ pub struct DBOption { pub dir: String, pub port: u16, + pub debug: bool, } diff --git a/src/protocol.rs b/src/protocol.rs index beb1a73..c76402c 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -159,18 +159,21 @@ impl Protocol { } fn parse_usize(protocol: &str) -> Result { - 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::() - .map_err(|_| DBError(format!("parse usize error: {}", protocol)))?), + .map_err(|_| DBError(format!("Failed to parse usize from: {}", protocol))) } } fn parse_string(protocol: &str) -> Result { - 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()) } } } diff --git a/src/server.rs b/src/server.rs index 3c16c72..61b753c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -14,6 +14,7 @@ use crate::storage::Storage; pub struct Server { pub storage: Arc, pub option: options::DBOption, + pub client_name: Option, } 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 diff --git a/src/storage.rs b/src/storage.rs index f4020c2..6550c9a 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -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())), diff --git a/tests/debug_hset.rs b/tests/debug_hset.rs index 5793a61..777b5f6 100644 --- a/tests/debug_hset.rs +++ b/tests/debug_hset.rs @@ -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; diff --git a/tests/debug_hset_simple.rs b/tests/debug_hset_simple.rs index 969df73..1a2cb5c 100644 --- a/tests/debug_hset_simple.rs +++ b/tests/debug_hset_simple.rs @@ -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; diff --git a/tests/redis_basic_client.rs b/tests/redis_basic_client.rs new file mode 100644 index 0000000..af78c64 --- /dev/null +++ b/tests/redis_basic_client.rs @@ -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); +} \ No newline at end of file diff --git a/tests/redis_integration_tests.rs b/tests/redis_integration_tests.rs index 39b0460..38aeaa4 100644 --- a/tests/redis_integration_tests.rs +++ b/tests/redis_integration_tests.rs @@ -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::(&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 = 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 = 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 = 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 = 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 = conn.hkeys("hash").unwrap(); result.sort(); assert_eq!(result, vec!["field2", "field3"]); - + // Test HVALS let mut result: Vec = 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 = 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) = 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 = redis::cmd("KEYS").arg("*").query(&mut conn).unwrap(); + let mut result: Vec = 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 = 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) = 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) = 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) = 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) = 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 = 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 = 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 = 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 = 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 = 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 = conn.hget("string", "field"); assert!(result.is_err()); - + // Test unknown command - let result: Result = redis::cmd("UNKNOWN").query(&mut conn); + let result: Result = redis::cmd("UNKNOWN").query(conn); assert!(result.is_err()); - + // Test EXEC without MULTI - let result: Result, _> = redis::cmd("EXEC").query(&mut conn); + let result: Result, _> = 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()); } \ No newline at end of file diff --git a/tests/simple_redis_test.rs b/tests/simple_redis_test.rs index 270781d..acb41e6 100644 --- a/tests/simple_redis_test.rs +++ b/tests/simple_redis_test.rs @@ -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;