From 38b649ea16d8ed053fd9222bfb9867e3432ee2a6 Mon Sep 17 00:00:00 2001 From: omagdy Date: Thu, 17 Jul 2025 08:06:26 +0300 Subject: test: Moved tests to seprate files under tests folder for more structure --- src/resp_commands.rs | 315 ++------------------------------------------------- 1 file changed, 8 insertions(+), 307 deletions(-) (limited to 'src/resp_commands.rs') 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, - expiry: Option, - get_old_value: bool, + pub key: String, + pub value: String, + pub condition: Option, + pub expiry: Option, + pub get_old_value: bool, } impl SetCommand { @@ -353,303 +353,4 @@ impl From 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 { - 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 { - 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 {} -- cgit v1.2.3