use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::thread; use codecrafters_redis::resp_parser::RespType; use codecrafters_redis::server::SharedMut; use codecrafters_redis::shared_cache::{Cache, CacheEntry}; // Test Helpers & Mocks /// Creates a new, empty shared cache for each test. fn new_cache() -> SharedMut { 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: &SharedMut, key: &str) -> Option { cache.lock().unwrap().get(key).cloned() } /// Tests for the `RedisCommands::from(RespType)` parser logic. mod command_parser_tests { use codecrafters_redis::resp_commands::ExpiryOption; use codecrafters_redis::resp_commands::RedisCommand; use codecrafters_redis::resp_commands::SetCondition; use codecrafters_redis::resp_parser::RespType; use super::*; #[test] fn test_parse_ping() { let cmd = build_command_from_str_slice(&["PING"]); assert!(matches!(RedisCommand::from(cmd), RedisCommand::Ping)); } #[test] fn test_parse_ping_case_insensitive() { let cmd = build_command_from_str_slice(&["pInG"]); assert!(matches!(RedisCommand::from(cmd), RedisCommand::Ping)); } #[test] fn test_parse_ping_with_extra_args_is_invalid() { let cmd = build_command_from_str_slice(&["PING", "extra"]); assert!(matches!(RedisCommand::from(cmd), RedisCommand::Invalid)); } #[test] fn test_parse_echo() { let cmd = build_command_from_str_slice(&["ECHO", "hello world"]); match RedisCommand::from(cmd) { RedisCommand::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!(RedisCommand::from(cmd), RedisCommand::Invalid)); } #[test] fn test_parse_get() { let cmd = build_command_from_str_slice(&["GET", "mykey"]); match RedisCommand::from(cmd) { RedisCommand::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 RedisCommand::from(cmd) { RedisCommand::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 RedisCommand::from(cmd) { RedisCommand::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 RedisCommand::from(cmd) { RedisCommand::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!(RedisCommand::from(cmd), RedisCommand::Invalid)); } #[test] fn test_parse_set_option_missing_value() { let cmd = build_command_from_str_slice(&["SET", "k", "v", "PX"]); assert!(matches!(RedisCommand::from(cmd), RedisCommand::Invalid)); } #[test] fn test_parse_unknown_command() { let cmd = build_command_from_str_slice(&["UNKNOWN", "foo"]); assert!(matches!(RedisCommand::from(cmd), RedisCommand::Invalid)); } #[test] fn test_parse_not_an_array_is_invalid() { let cmd = RespType::SimpleString("SET k v".into()); assert!(matches!(RedisCommand::from(cmd), RedisCommand::Invalid)); } } /// Tests for the command execution logic in `RedisCommands::execute`. mod command_execution_tests { use codecrafters_redis::resp_commands::RedisCommand; use codecrafters_redis::server::{CanBroadcast, RedisServer}; use std::time::Duration; use super::*; /// Helper to parse and execute a command against a cache. fn run_command(cache: &SharedMut, args: &[&str]) -> Vec { let command = RedisCommand::from(build_command_from_str_slice(args)); let mut server = RedisServer::master(); server.set_cache(cache); let config = server.config(); let state = server.get_server_state(); let broadcaster = None::>>; command.execute(cache.clone(), config, state, broadcaster) } #[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 std::time::{SystemTime, UNIX_EPOCH}; use codecrafters_redis::resp_commands::{ExpiryOption, SetCommand}; #[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()); } }