aboutsummaryrefslogtreecommitdiff
path: root/src/resp_commands.rs
diff options
context:
space:
mode:
authoromagdy <omar.professional8777@gmail.com>2025-07-17 08:06:26 +0300
committeromagdy <omar.professional8777@gmail.com>2025-07-17 08:06:26 +0300
commit38b649ea16d8ed053fd9222bfb9867e3432ee2a6 (patch)
treea7fbde68ad869e1b74071207bdf7b7c159c7f75f /src/resp_commands.rs
parentc880c7ad3eba9546ce95bc268218c66a128d319f (diff)
downloadredis-rust-38b649ea16d8ed053fd9222bfb9867e3432ee2a6.tar.xz
redis-rust-38b649ea16d8ed053fd9222bfb9867e3432ee2a6.zip
test: Moved tests to seprate files under tests folder for more structure
Diffstat (limited to 'src/resp_commands.rs')
-rw-r--r--src/resp_commands.rs315
1 files changed, 8 insertions, 307 deletions
diff --git a/src/resp_commands.rs b/src/resp_commands.rs
index 2663cc5..4d01507 100644
--- a/src/resp_commands.rs
+++ b/src/resp_commands.rs
@@ -1,5 +1,5 @@
-use crate::{macros::*, resp_parser::*, CacheEntry, SharedCache};
-use std::collections::{HashMap, HashSet};
+use crate::{resp_parser::*, shared_cache::*};
+use std::collections::HashMap;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone)]
@@ -42,11 +42,11 @@ pub enum ExpiryOption {
/// GET -- Return the old string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value stored at key is not a string.
#[derive(Debug, Clone)]
pub struct SetCommand {
- key: String,
- value: String,
- condition: Option<SetCondition>,
- expiry: Option<ExpiryOption>,
- get_old_value: bool,
+ pub key: String,
+ pub value: String,
+ pub condition: Option<SetCondition>,
+ pub expiry: Option<ExpiryOption>,
+ pub get_old_value: bool,
}
impl SetCommand {
@@ -353,303 +353,4 @@ impl From<RespType> for RedisCommands {
}
#[cfg(test)]
-mod tests {
- use super::*;
- use std::sync::{Arc, Mutex};
- use std::thread;
-
- // Test Helpers & Mocks
-
- /// Creates a new, empty shared cache for each test.
- fn new_cache() -> SharedCache {
- Arc::new(Mutex::new(HashMap::new()))
- }
-
- /// Builds a `RespType::Array` from string slices to simplify parser tests.
- fn build_command_from_str_slice(args: &[&str]) -> RespType {
- let resp_args = args
- .iter()
- .map(|s| RespType::BulkString(s.to_string().into_bytes()))
- .collect();
- RespType::Array(resp_args)
- }
-
- /// A helper to get a value directly from the cache for assertions.
- fn get_from_cache(cache: &SharedCache, key: &str) -> Option<CacheEntry> {
- cache.lock().unwrap().get(key).cloned()
- }
-
- /// Tests for the `RedisCommands::from(RespType)` parser logic.
- mod command_parser_tests {
- use super::*;
-
- #[test]
- fn test_parse_ping() {
- let cmd = build_command_from_str_slice(&["PING"]);
- assert!(matches!(RedisCommands::from(cmd), RedisCommands::PING));
- }
-
- #[test]
- fn test_parse_ping_case_insensitive() {
- let cmd = build_command_from_str_slice(&["pInG"]);
- assert!(matches!(RedisCommands::from(cmd), RedisCommands::PING));
- }
-
- #[test]
- fn test_parse_ping_with_extra_args_is_invalid() {
- let cmd = build_command_from_str_slice(&["PING", "extra"]);
- assert!(matches!(RedisCommands::from(cmd), RedisCommands::Invalid));
- }
-
- #[test]
- fn test_parse_echo() {
- let cmd = build_command_from_str_slice(&["ECHO", "hello world"]);
- match RedisCommands::from(cmd) {
- RedisCommands::ECHO(s) => assert_eq!(s, "hello world"),
- _ => panic!("Expected ECHO command"),
- }
- }
-
- #[test]
- fn test_parse_echo_no_args_is_invalid() {
- let cmd = build_command_from_str_slice(&["ECHO"]);
- assert!(matches!(RedisCommands::from(cmd), RedisCommands::Invalid));
- }
-
- #[test]
- fn test_parse_get() {
- let cmd = build_command_from_str_slice(&["GET", "mykey"]);
- match RedisCommands::from(cmd) {
- RedisCommands::GET(k) => assert_eq!(k, "mykey"),
- _ => panic!("Expected GET command"),
- }
- }
-
- #[test]
- fn test_parse_simple_set() {
- let cmd = build_command_from_str_slice(&["SET", "mykey", "myvalue"]);
- match RedisCommands::from(cmd) {
- RedisCommands::SET(c) => {
- assert_eq!(c.key, "mykey");
- assert_eq!(c.value, "myvalue");
- assert!(c.condition.is_none() && c.expiry.is_none() && !c.get_old_value);
- }
- _ => panic!("Expected SET command"),
- }
- }
-
- #[test]
- fn test_parse_set_with_all_options() {
- let cmd = build_command_from_str_slice(&["SET", "k", "v", "NX", "PX", "5000", "GET"]);
- match RedisCommands::from(cmd) {
- RedisCommands::SET(c) => {
- assert!(matches!(c.condition, Some(SetCondition::NotExists)));
- assert!(matches!(c.expiry, Some(ExpiryOption::Milliseconds(5000))));
- assert!(c.get_old_value);
- }
- _ => panic!("Expected SET command"),
- }
- }
-
- #[test]
- fn test_parse_set_options_case_insensitive() {
- let cmd = build_command_from_str_slice(&["set", "k", "v", "nx", "px", "100"]);
- match RedisCommands::from(cmd) {
- RedisCommands::SET(c) => {
- assert!(matches!(c.condition, Some(SetCondition::NotExists)));
- assert!(matches!(c.expiry, Some(ExpiryOption::Milliseconds(100))));
- }
- _ => panic!("Expected SET command"),
- }
- }
-
- #[test]
- fn test_parse_set_invalid_option_value() {
- let cmd = build_command_from_str_slice(&["SET", "k", "v", "EX", "not-a-number"]);
- assert!(matches!(RedisCommands::from(cmd), RedisCommands::Invalid));
- }
-
- #[test]
- fn test_parse_set_option_missing_value() {
- let cmd = build_command_from_str_slice(&["SET", "k", "v", "PX"]);
- assert!(matches!(RedisCommands::from(cmd), RedisCommands::Invalid));
- }
-
- #[test]
- fn test_parse_unknown_command() {
- let cmd = build_command_from_str_slice(&["UNKNOWN", "foo"]);
- assert!(matches!(RedisCommands::from(cmd), RedisCommands::Invalid));
- }
-
- #[test]
- fn test_parse_not_an_array_is_invalid() {
- let cmd = RespType::SimpleString("SET k v".into());
- assert!(matches!(RedisCommands::from(cmd), RedisCommands::Invalid));
- }
- }
-
- /// Tests for the command execution logic in `RedisCommands::execute`.
- mod command_execution_tests {
- use super::*;
-
- /// Helper to parse and execute a command against a cache.
- fn run_command(cache: &SharedCache, args: &[&str]) -> Vec<u8> {
- let command = RedisCommands::from(build_command_from_str_slice(args));
- command.execute(Arc::clone(cache))
- }
-
- #[test]
- fn test_execute_ping() {
- let cache = new_cache();
- let result = run_command(&cache, &["PING"]);
- assert_eq!(result, b"+PONG\r\n");
- }
-
- #[test]
- fn test_execute_echo() {
- let cache = new_cache();
- // Note: the provided code has a bug, it returns a Simple String, not a Bulk String.
- // A correct implementation would return `resp!(bulk "hello")`.
- let result = run_command(&cache, &["ECHO", "hello"]);
- assert_eq!(result, b"+hello\r\n");
- }
-
- #[test]
- fn test_execute_get_non_existent() {
- let cache = new_cache();
- let result = run_command(&cache, &["GET", "mykey"]);
- assert_eq!(result, b"$-1\r\n"); // Null Bulk String
- }
-
- #[test]
- fn test_execute_set_and_get() {
- let cache = new_cache();
- let set_result = run_command(&cache, &["SET", "mykey", "myvalue"]);
- assert_eq!(set_result, b"+OK\r\n");
-
- let get_result = run_command(&cache, &["GET", "mykey"]);
- assert_eq!(get_result, b"$7\r\nmyvalue\r\n");
- }
-
- #[test]
- fn test_execute_set_nx() {
- let cache = new_cache();
- // Should succeed when key doesn't exist
- assert_eq!(run_command(&cache, &["SET", "k", "v1", "NX"]), b"+OK\r\n");
- assert_eq!(get_from_cache(&cache, "k").unwrap().value, "v1");
-
- // Should fail when key exists
- assert_eq!(run_command(&cache, &["SET", "k", "v2", "NX"]), b"$-1\r\n");
- assert_eq!(get_from_cache(&cache, "k").unwrap().value, "v1"); // Value is unchanged
- }
-
- #[test]
- fn test_execute_set_xx() {
- let cache = new_cache();
- // Should fail when key doesn't exist
- assert_eq!(run_command(&cache, &["SET", "k", "v1", "XX"]), b"$-1\r\n");
- assert!(get_from_cache(&cache, "k").is_none());
-
- // Pre-populate and should succeed
- run_command(&cache, &["SET", "k", "v1"]);
- assert_eq!(run_command(&cache, &["SET", "k", "v2", "XX"]), b"+OK\r\n");
- assert_eq!(get_from_cache(&cache, "k").unwrap().value, "v2");
- }
-
- #[test]
- fn test_execute_set_with_get_option() {
- let cache = new_cache();
- run_command(&cache, &["SET", "mykey", "old"]);
-
- // Note: This test will fail with the provided code, which incorrectly returns
- // a Simple String `+old\r\n`. The test correctly expects a Bulk String.
- let result = run_command(&cache, &["SET", "mykey", "new", "GET"]);
- assert_eq!(result, b"$3\r\nold\r\n");
- assert_eq!(get_from_cache(&cache, "mykey").unwrap().value, "new");
- }
-
- #[test]
- fn test_execute_set_get_on_non_existent() {
- let cache = new_cache();
- // Note: This test will fail with the provided code, which incorrectly
- // returns `+OK\r\n`. The spec requires a nil reply.
- let result = run_command(&cache, &["SET", "mykey", "new", "GET"]);
- assert_eq!(result, b"$-1\r\n");
- assert!(get_from_cache(&cache, "mykey").is_some());
- }
-
- #[test]
- fn test_expiry_with_px_and_cleanup() {
- let cache = new_cache();
- run_command(&cache, &["SET", "mykey", "val", "PX", "50"]);
-
- assert!(get_from_cache(&cache, "mykey").is_some());
- thread::sleep(Duration::from_millis(60));
-
- // GET on an expired key should return nil and trigger cleanup
- assert_eq!(run_command(&cache, &["GET", "mykey"]), b"$-1\r\n");
- assert!(get_from_cache(&cache, "mykey").is_none());
- }
-
- #[test]
- fn test_keepttl() {
- let cache = new_cache();
- run_command(&cache, &["SET", "mykey", "v1", "EX", "2"]);
- let expiry1 = get_from_cache(&cache, "mykey").unwrap().expires_at;
-
- thread::sleep(Duration::from_millis(100));
- run_command(&cache, &["SET", "mykey", "v2", "KEEPTTL"]);
-
- let entry2 = get_from_cache(&cache, "mykey").unwrap();
- assert_eq!(entry2.value, "v2"); // Value is updated
- assert_eq!(entry2.expires_at, expiry1); // TTL is retained
- }
- }
-
- /// Unit tests for the `SetCommand` helper methods.
- mod set_command_tests {
- use super::*;
-
- #[test]
- fn test_calculate_expiry_seconds() {
- let cmd = SetCommand::new("k".into(), "v".into())
- .with_expiry(Some(ExpiryOption::Seconds(10)));
- let expiry = cmd.calculate_expiry_time().unwrap();
- let now = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_millis() as u64;
- let delta = expiry.saturating_sub(now);
- // Allow a small delta for execution time variance
- assert!((9990..=10010).contains(&delta), "Delta was {}", delta);
- }
-
- #[test]
- fn test_calculate_expiry_at_seconds() {
- let ts_secs = 1893456000; // 2030-01-01 00:00:00 UTC
- let cmd = SetCommand::new("k".into(), "v".into())
- .with_expiry(Some(ExpiryOption::ExpiresAtSeconds(ts_secs)));
- let expiry = cmd.calculate_expiry_time().unwrap();
- assert_eq!(expiry, ts_secs * 1000);
- }
-
- #[test]
- fn test_calculate_expiry_at_milliseconds() {
- let ts_ms = 1893456000123;
- let cmd = SetCommand::new("k".into(), "v".into())
- .with_expiry(Some(ExpiryOption::ExpiresAtMilliseconds(ts_ms)));
- let expiry = cmd.calculate_expiry_time().unwrap();
- assert_eq!(expiry, ts_ms);
- }
-
- #[test]
- fn test_calculate_expiry_for_none_and_keepttl() {
- let cmd_keepttl =
- SetCommand::new("k".into(), "v".into()).with_expiry(Some(ExpiryOption::KeepTtl));
- assert!(cmd_keepttl.calculate_expiry_time().is_none());
-
- let cmd_none = SetCommand::new("k".into(), "v".into()).with_expiry(None);
- assert!(cmd_none.calculate_expiry_time().is_none());
- }
- }
-}
+mod tests {}