...
This commit is contained in:
		
							
								
								
									
										150
									
								
								instructions/redis_basic_client.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								instructions/redis_basic_client.md
									
									
									
									
									
										Normal 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 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 <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 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 (`$<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" | ||||
| @@ -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]`     | | ||||
|  | ||||
| ## | ||||
| @@ -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..." | ||||
|   | ||||
							
								
								
									
										54
									
								
								src/cmd.rs
									
									
									
									
									
								
							
							
						
						
									
										54
									
								
								src/cmd.rs
									
									
									
									
									
								
							| @@ -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), | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,6 @@ use tokio::sync::mpsc; | ||||
| use redb; | ||||
| use bincode; | ||||
|  | ||||
| use crate::protocol::Protocol; | ||||
|  | ||||
| // todo: more error types | ||||
| #[derive(Debug)] | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -2,4 +2,5 @@ | ||||
| pub struct DBOption { | ||||
|     pub dir: String, | ||||
|     pub port: u16, | ||||
|     pub debug: bool, | ||||
| } | ||||
|   | ||||
| @@ -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()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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())), | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
							
								
								
									
										30
									
								
								tests/redis_basic_client.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								tests/redis_basic_client.rs
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
| @@ -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()); | ||||
| } | ||||
| @@ -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; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user