...
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]` |
|
| `SSCAN` | Iterate over set members | `[cursor, array]` |
|
||||||
| `ZSCAN` | Iterate over sorted set | `[cursor, array]` |
|
| `ZSCAN` | Iterate over sorted set | `[cursor, array]` |
|
||||||
|
|
||||||
|
##
|
@@ -12,6 +12,7 @@ echo ""
|
|||||||
echo "2️⃣ Running Comprehensive Redis Integration Tests (13 tests)..."
|
echo "2️⃣ Running Comprehensive Redis Integration Tests (13 tests)..."
|
||||||
echo "----------------------------------------------------------------"
|
echo "----------------------------------------------------------------"
|
||||||
cargo test --test redis_integration_tests -- --nocapture
|
cargo test --test redis_integration_tests -- --nocapture
|
||||||
|
cargo test --test redis_basic_client -- --nocapture
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "3️⃣ Running All Tests..."
|
echo "3️⃣ Running All Tests..."
|
||||||
|
54
src/cmd.rs
54
src/cmd.rs
@@ -33,7 +33,10 @@ pub enum Cmd {
|
|||||||
Ttl(String),
|
Ttl(String),
|
||||||
Exists(String),
|
Exists(String),
|
||||||
Quit,
|
Quit,
|
||||||
Unknow,
|
Client(Vec<String>),
|
||||||
|
ClientSetName(String),
|
||||||
|
ClientGetName,
|
||||||
|
Unknow(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cmd {
|
impl Cmd {
|
||||||
@@ -274,7 +277,30 @@ impl Cmd {
|
|||||||
}
|
}
|
||||||
Cmd::Quit
|
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,
|
protocol.0,
|
||||||
))
|
))
|
||||||
@@ -288,7 +314,7 @@ impl Cmd {
|
|||||||
|
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
&self,
|
&self,
|
||||||
server: &Server,
|
server: &mut Server,
|
||||||
protocol: Protocol,
|
protocol: Protocol,
|
||||||
queued_cmd: &mut Option<Vec<(Cmd, Protocol)>>,
|
queued_cmd: &mut Option<Vec<(Cmd, Protocol)>>,
|
||||||
) -> Result<Protocol, DBError> {
|
) -> Result<Protocol, DBError> {
|
||||||
@@ -347,14 +373,20 @@ impl Cmd {
|
|||||||
Cmd::Ttl(key) => ttl_cmd(server, key).await,
|
Cmd::Ttl(key) => ttl_cmd(server, key).await,
|
||||||
Cmd::Exists(key) => exists_cmd(server, key).await,
|
Cmd::Exists(key) => exists_cmd(server, key).await,
|
||||||
Cmd::Quit => Ok(Protocol::SimpleString("OK".to_string())),
|
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(
|
async fn exec_cmd(
|
||||||
queued_cmd: &mut Option<Vec<(Cmd, Protocol)>>,
|
queued_cmd: &mut Option<Vec<(Cmd, Protocol)>>,
|
||||||
server: &Server,
|
server: &mut Server,
|
||||||
) -> Result<Protocol, DBError> {
|
) -> Result<Protocol, DBError> {
|
||||||
if queued_cmd.is_some() {
|
if queued_cmd.is_some() {
|
||||||
let mut vec = Vec::new();
|
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)),
|
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 redb;
|
||||||
use bincode;
|
use bincode;
|
||||||
|
|
||||||
use crate::protocol::Protocol;
|
|
||||||
|
|
||||||
// todo: more error types
|
// todo: more error types
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@@ -18,6 +18,10 @@ struct Args {
|
|||||||
/// The port of the Redis server, default is 6379 if not specified
|
/// The port of the Redis server, default is 6379 if not specified
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
port: Option<u16>,
|
port: Option<u16>,
|
||||||
|
|
||||||
|
/// Enable debug mode
|
||||||
|
#[arg(long)]
|
||||||
|
debug: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -36,11 +40,15 @@ async fn main() {
|
|||||||
let option = redis_rs::options::DBOption {
|
let option = redis_rs::options::DBOption {
|
||||||
dir: args.dir,
|
dir: args.dir,
|
||||||
port,
|
port,
|
||||||
|
debug: args.debug,
|
||||||
};
|
};
|
||||||
|
|
||||||
// new server
|
// new server
|
||||||
let server = server::Server::new(option).await;
|
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
|
// accept new connections
|
||||||
loop {
|
loop {
|
||||||
let stream = listener.accept().await;
|
let stream = listener.accept().await;
|
||||||
|
@@ -2,4 +2,5 @@
|
|||||||
pub struct DBOption {
|
pub struct DBOption {
|
||||||
pub dir: String,
|
pub dir: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
|
pub debug: bool,
|
||||||
}
|
}
|
||||||
|
@@ -159,18 +159,21 @@ impl Protocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_usize(protocol: &str) -> Result<usize, DBError> {
|
fn parse_usize(protocol: &str) -> Result<usize, DBError> {
|
||||||
match protocol.len() {
|
if protocol.is_empty() {
|
||||||
0 => Err(DBError(format!("parse usize error: {:?}", protocol))),
|
Err(DBError("Cannot parse usize from empty string".to_string()))
|
||||||
_ => Ok(protocol
|
} else {
|
||||||
|
protocol
|
||||||
.parse::<usize>()
|
.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> {
|
fn parse_string(protocol: &str) -> Result<String, DBError> {
|
||||||
match protocol.len() {
|
if protocol.is_empty() {
|
||||||
0 => Err(DBError(format!("parse usize error: {:?}", protocol))),
|
// Allow empty strings, but handle appropriately
|
||||||
_ => Ok(protocol.to_string()),
|
Ok("".to_string())
|
||||||
|
} else {
|
||||||
|
Ok(protocol.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,7 @@ use crate::storage::Storage;
|
|||||||
pub struct Server {
|
pub struct Server {
|
||||||
pub storage: Arc<Storage>,
|
pub storage: Arc<Storage>,
|
||||||
pub option: options::DBOption,
|
pub option: options::DBOption,
|
||||||
|
pub client_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
@@ -28,6 +29,7 @@ impl Server {
|
|||||||
Server {
|
Server {
|
||||||
storage: Arc::new(storage),
|
storage: Arc::new(storage),
|
||||||
option,
|
option,
|
||||||
|
client_name: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,20 +48,37 @@ impl Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let s = str::from_utf8(&buf[..len])?;
|
let s = str::from_utf8(&buf[..len])?;
|
||||||
let (cmd, protocol) =
|
let (cmd, protocol) = match Cmd::from(s) {
|
||||||
Cmd::from(s).unwrap_or((Cmd::Unknow, Protocol::err("unknow cmd")));
|
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);
|
println!("got command: {:?}, protocol: {:?}", cmd, protocol);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is a QUIT command before processing
|
// Check if this is a QUIT command before processing
|
||||||
let is_quit = matches!(cmd, Cmd::Quit);
|
let is_quit = matches!(cmd, Cmd::Quit);
|
||||||
|
|
||||||
let res = cmd
|
let res = cmd
|
||||||
.run(self, protocol, &mut queued_cmd)
|
.run(&mut self.clone(), protocol.clone(), &mut queued_cmd)
|
||||||
.await
|
.await
|
||||||
.unwrap_or(Protocol::err("unknow 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);
|
print!("queued cmd {:?}", queued_cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.option.debug {
|
||||||
|
println!("\x1b[32;1mgoing to send response {}\x1b[0m", res.encode());
|
||||||
|
} else {
|
||||||
println!("going to send response {}", res.encode());
|
println!("going to send response {}", res.encode());
|
||||||
|
}
|
||||||
_ = stream.write(res.encode().as_bytes()).await?;
|
_ = stream.write(res.encode().as_bytes()).await?;
|
||||||
|
|
||||||
// If this was a QUIT command, close the connection
|
// If this was a QUIT command, close the connection
|
||||||
|
@@ -3,7 +3,7 @@ use std::{
|
|||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
use redb::{Database, Error, ReadableTable, Table, TableDefinition, WriteTransaction, ReadTransaction};
|
use redb::{Database, ReadableTable, TableDefinition};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::error::DBError;
|
use crate::error::DBError;
|
||||||
@@ -493,7 +493,6 @@ impl Storage {
|
|||||||
|
|
||||||
// Stop if we've returned enough keys
|
// Stop if we've returned enough keys
|
||||||
if returned_keys >= count {
|
if returned_keys >= count {
|
||||||
current_cursor += 1;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -502,7 +501,7 @@ impl Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we've reached the end of iteration, return cursor 0 to indicate completion
|
// 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))
|
Ok((next_cursor, keys))
|
||||||
}
|
}
|
||||||
@@ -563,7 +562,6 @@ impl Storage {
|
|||||||
returned_fields += 1;
|
returned_fields += 1;
|
||||||
|
|
||||||
if returned_fields >= count {
|
if returned_fields >= count {
|
||||||
current_cursor += 1;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -571,7 +569,7 @@ impl Storage {
|
|||||||
current_cursor += 1;
|
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))
|
Ok((next_cursor, fields))
|
||||||
}
|
}
|
||||||
Some(_) => Err(DBError("WRONGTYPE Operation against a key holding the wrong kind of value".to_string())),
|
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 {
|
let option = DBOption {
|
||||||
dir: test_dir.to_string(),
|
dir: test_dir.to_string(),
|
||||||
port,
|
port,
|
||||||
|
debug: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut server = Server::new(option).await;
|
let mut server = Server::new(option).await;
|
||||||
|
@@ -15,6 +15,7 @@ async fn debug_hset_return_value() {
|
|||||||
let option = DBOption {
|
let option = DBOption {
|
||||||
dir: test_dir.to_string(),
|
dir: test_dir.to_string(),
|
||||||
port: 16390,
|
port: 16390,
|
||||||
|
debug: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut server = Server::new(option).await;
|
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,106 +1,124 @@
|
|||||||
use redis_rs::{server::Server, options::DBOption};
|
|
||||||
use redis::{Client, Commands, Connection};
|
use redis::{Client, Commands, Connection};
|
||||||
|
use std::process::{Child, Command};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::time::{sleep, timeout};
|
use tokio::time::sleep;
|
||||||
use tokio::sync::oneshot;
|
|
||||||
|
|
||||||
// Helper function to start a test server with clean data directory
|
// Helper function to get Redis connection, retrying until successful
|
||||||
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
|
|
||||||
fn get_redis_connection(port: u16) -> Connection {
|
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;
|
let mut attempts = 0;
|
||||||
loop {
|
loop {
|
||||||
match client.get_connection() {
|
match client.get_connection() {
|
||||||
Ok(conn) => return conn,
|
Ok(mut conn) => {
|
||||||
Err(_) if attempts < 20 => {
|
if redis::cmd("PING").query::<String>(&mut conn).is_ok() {
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if attempts >= 20 {
|
||||||
|
panic!(
|
||||||
|
"Failed to connect to Redis server after 20 attempts: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
attempts += 1;
|
attempts += 1;
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
}
|
}
|
||||||
Err(e) => panic!("Failed to connect to Redis server: {}", e),
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
// Create a new guard that also owns the test directory path
|
||||||
|
let guard = ServerProcessGuard {
|
||||||
|
process: child,
|
||||||
|
test_dir,
|
||||||
|
};
|
||||||
|
|
||||||
|
(guard, port)
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_basic_ping() {
|
async fn all_tests() {
|
||||||
let (mut server, port) = start_test_server("ping").await;
|
let (_server_guard, port) = setup_server();
|
||||||
|
|
||||||
// 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 mut conn = get_redis_connection(port);
|
||||||
let result: String = redis::cmd("PING").query(&mut conn).unwrap();
|
|
||||||
|
// 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");
|
assert_eq!(result, "PONG");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
async fn test_string_operations(conn: &mut Connection) {
|
||||||
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;
|
|
||||||
|
|
||||||
let mut conn = get_redis_connection(port);
|
|
||||||
|
|
||||||
// Test SET
|
// Test SET
|
||||||
let _: () = conn.set("key", "value").unwrap();
|
let _: () = conn.set("key", "value").unwrap();
|
||||||
|
|
||||||
@@ -121,26 +139,7 @@ async fn test_string_operations() {
|
|||||||
assert_eq!(result, None);
|
assert_eq!(result, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
async fn test_incr_operations(conn: &mut Connection) {
|
||||||
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);
|
|
||||||
|
|
||||||
// Test INCR on non-existent key
|
// Test INCR on non-existent key
|
||||||
let result: i32 = conn.incr("counter", 1).unwrap();
|
let result: i32 = conn.incr("counter", 1).unwrap();
|
||||||
assert_eq!(result, 1);
|
assert_eq!(result, 1);
|
||||||
@@ -155,26 +154,7 @@ async fn test_incr_operations() {
|
|||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
async fn test_hash_operations(conn: &mut Connection) {
|
||||||
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);
|
|
||||||
|
|
||||||
// Test HSET
|
// Test HSET
|
||||||
let result: i32 = conn.hset("hash", "field1", "value1").unwrap();
|
let result: i32 = conn.hset("hash", "field1", "value1").unwrap();
|
||||||
assert_eq!(result, 1); // 1 new field
|
assert_eq!(result, 1); // 1 new field
|
||||||
@@ -219,26 +199,7 @@ async fn test_hash_operations() {
|
|||||||
assert_eq!(result, vec!["value2", "value3"]);
|
assert_eq!(result, vec!["value2", "value3"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
async fn test_expiration(conn: &mut Connection) {
|
||||||
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);
|
|
||||||
|
|
||||||
// Test SETEX (expire in 1 second)
|
// Test SETEX (expire in 1 second)
|
||||||
let _: () = conn.set_ex("expkey", "value", 1).unwrap();
|
let _: () = conn.set_ex("expkey", "value", 1).unwrap();
|
||||||
|
|
||||||
@@ -266,26 +227,7 @@ async fn test_expiration() {
|
|||||||
assert_eq!(result, false);
|
assert_eq!(result, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
async fn test_scan_operations(conn: &mut Connection) {
|
||||||
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);
|
|
||||||
|
|
||||||
// Set up test data
|
// Set up test data
|
||||||
for i in 0..5 {
|
for i in 0..5 {
|
||||||
let _: () = conn.set(format!("key{}", i), format!("value{}", i)).unwrap();
|
let _: () = conn.set(format!("key{}", i), format!("value{}", i)).unwrap();
|
||||||
@@ -298,7 +240,7 @@ async fn test_scan_operations() {
|
|||||||
.arg("*")
|
.arg("*")
|
||||||
.arg("COUNT")
|
.arg("COUNT")
|
||||||
.arg(10)
|
.arg(10)
|
||||||
.query(&mut conn)
|
.query(conn)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (cursor, keys) = result;
|
let (cursor, keys) = result;
|
||||||
@@ -306,31 +248,80 @@ async fn test_scan_operations() {
|
|||||||
assert_eq!(keys.len(), 5);
|
assert_eq!(keys.len(), 5);
|
||||||
|
|
||||||
// Test KEYS
|
// 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();
|
result.sort();
|
||||||
assert_eq!(result, vec!["key0", "key1", "key2", "key3", "key4"]);
|
assert_eq!(result, vec!["key0", "key1", "key2", "key3", "key4"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
async fn test_scan_with_count(conn: &mut Connection) {
|
||||||
async fn test_hscan_operations() {
|
// Clean up previous keys
|
||||||
let (mut server, port) = start_test_server("hscan").await;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
tokio::spawn(async move {
|
// Set up test data
|
||||||
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
|
for i in 0..15 {
|
||||||
.await
|
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();
|
.unwrap();
|
||||||
|
|
||||||
loop {
|
assert_ne!(next_cursor, 0);
|
||||||
if let Ok((stream, _)) = listener.accept().await {
|
assert_eq!(keys.len(), 5);
|
||||||
let _ = server.handle(stream).await;
|
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;
|
||||||
|
|
||||||
sleep(Duration::from_millis(200)).await;
|
// 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();
|
||||||
|
|
||||||
let mut conn = get_redis_connection(port);
|
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
|
// Set up hash data
|
||||||
for i in 0..3 {
|
for i in 0..3 {
|
||||||
let _: () = conn.hset("testhash", format!("field{}", i), format!("value{}", i)).unwrap();
|
let _: () = conn.hset("testhash", format!("field{}", i), format!("value{}", i)).unwrap();
|
||||||
@@ -344,7 +335,7 @@ async fn test_hscan_operations() {
|
|||||||
.arg("*")
|
.arg("*")
|
||||||
.arg("COUNT")
|
.arg("COUNT")
|
||||||
.arg(10)
|
.arg(10)
|
||||||
.query(&mut conn)
|
.query(conn)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (cursor, fields) = result;
|
let (cursor, fields) = result;
|
||||||
@@ -352,31 +343,12 @@ async fn test_hscan_operations() {
|
|||||||
assert_eq!(fields.len(), 6); // 3 field-value pairs = 6 elements
|
assert_eq!(fields.len(), 6); // 3 field-value pairs = 6 elements
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
async fn test_transaction_operations(conn: &mut Connection) {
|
||||||
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);
|
|
||||||
|
|
||||||
// Test MULTI/EXEC
|
// Test MULTI/EXEC
|
||||||
let _: () = redis::cmd("MULTI").query(&mut conn).unwrap();
|
let _: () = redis::cmd("MULTI").query(conn).unwrap();
|
||||||
let _: () = redis::cmd("SET").arg("key1").arg("value1").query(&mut conn).unwrap();
|
let _: () = redis::cmd("SET").arg("key1").arg("value1").query(conn).unwrap();
|
||||||
let _: () = redis::cmd("SET").arg("key2").arg("value2").query(&mut conn).unwrap();
|
let _: () = redis::cmd("SET").arg("key2").arg("value2").query(conn).unwrap();
|
||||||
let _: Vec<String> = redis::cmd("EXEC").query(&mut conn).unwrap();
|
let _: Vec<String> = redis::cmd("EXEC").query(conn).unwrap();
|
||||||
|
|
||||||
// Verify commands were executed
|
// Verify commands were executed
|
||||||
let result: String = conn.get("key1").unwrap();
|
let result: String = conn.get("key1").unwrap();
|
||||||
@@ -386,96 +358,39 @@ async fn test_transaction_operations() {
|
|||||||
assert_eq!(result, "value2");
|
assert_eq!(result, "value2");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
async fn test_discard_transaction(conn: &mut Connection) {
|
||||||
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);
|
|
||||||
|
|
||||||
// Test MULTI/DISCARD
|
// Test MULTI/DISCARD
|
||||||
let _: () = redis::cmd("MULTI").query(&mut conn).unwrap();
|
let _: () = redis::cmd("MULTI").query(conn).unwrap();
|
||||||
let _: () = redis::cmd("SET").arg("discard").arg("value").query(&mut conn).unwrap();
|
let _: () = redis::cmd("SET").arg("discard").arg("value").query(conn).unwrap();
|
||||||
let _: () = redis::cmd("DISCARD").query(&mut conn).unwrap();
|
let _: () = redis::cmd("DISCARD").query(conn).unwrap();
|
||||||
|
|
||||||
// Verify command was not executed
|
// Verify command was not executed
|
||||||
let result: Option<String> = conn.get("discard").unwrap();
|
let result: Option<String> = conn.get("discard").unwrap();
|
||||||
assert_eq!(result, None);
|
assert_eq!(result, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
async fn test_type_command(conn: &mut Connection) {
|
||||||
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);
|
|
||||||
|
|
||||||
// Test string type
|
// Test string type
|
||||||
let _: () = conn.set("string", "value").unwrap();
|
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");
|
assert_eq!(result, "string");
|
||||||
|
|
||||||
// Test hash type
|
// Test hash type
|
||||||
let _: () = conn.hset("hash", "field", "value").unwrap();
|
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");
|
assert_eq!(result, "hash");
|
||||||
|
|
||||||
// Test non-existent key
|
// 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");
|
assert_eq!(result, "none");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
async fn test_config_commands(conn: &mut Connection) {
|
||||||
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);
|
|
||||||
|
|
||||||
// Test CONFIG GET databases
|
// Test CONFIG GET databases
|
||||||
let result: Vec<String> = redis::cmd("CONFIG")
|
let result: Vec<String> = redis::cmd("CONFIG")
|
||||||
.arg("GET")
|
.arg("GET")
|
||||||
.arg("databases")
|
.arg("databases")
|
||||||
.query(&mut conn)
|
.query(conn)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(result, vec!["databases", "16"]);
|
assert_eq!(result, vec!["databases", "16"]);
|
||||||
|
|
||||||
@@ -483,75 +398,37 @@ async fn test_config_commands() {
|
|||||||
let result: Vec<String> = redis::cmd("CONFIG")
|
let result: Vec<String> = redis::cmd("CONFIG")
|
||||||
.arg("GET")
|
.arg("GET")
|
||||||
.arg("dir")
|
.arg("dir")
|
||||||
.query(&mut conn)
|
.query(conn)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(result[0], "dir");
|
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(conn: &mut Connection) {
|
||||||
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);
|
|
||||||
|
|
||||||
// Test INFO
|
// 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"));
|
assert!(result.contains("redis_version"));
|
||||||
|
|
||||||
// Test INFO replication
|
// 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"));
|
assert!(result.contains("role:master"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
async fn test_error_handling(conn: &mut Connection) {
|
||||||
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);
|
|
||||||
|
|
||||||
// Test WRONGTYPE error - try to use hash command on string
|
// Test WRONGTYPE error - try to use hash command on string
|
||||||
let _: () = conn.set("string", "value").unwrap();
|
let _: () = conn.set("string", "value").unwrap();
|
||||||
let result: Result<String, _> = conn.hget("string", "field");
|
let result: Result<String, _> = conn.hget("string", "field");
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
|
|
||||||
// Test unknown command
|
// 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());
|
assert!(result.is_err());
|
||||||
|
|
||||||
// Test EXEC without MULTI
|
// 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());
|
assert!(result.is_err());
|
||||||
|
|
||||||
// Test DISCARD without MULTI
|
// 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());
|
assert!(result.is_err());
|
||||||
}
|
}
|
@@ -19,6 +19,7 @@ async fn start_test_server(test_name: &str) -> (Server, u16) {
|
|||||||
let option = DBOption {
|
let option = DBOption {
|
||||||
dir: test_dir,
|
dir: test_dir,
|
||||||
port,
|
port,
|
||||||
|
debug: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let server = Server::new(option).await;
|
let server = Server::new(option).await;
|
||||||
|
Reference in New Issue
Block a user