diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_commands.rs | 312 | ||||
| -rw-r--r-- | tests/test_parse_array.rs | 130 | ||||
| -rw-r--r-- | tests/test_parse_boolean.rs | 87 | ||||
| -rw-r--r-- | tests/test_parse_bulk_string.rs | 214 | ||||
| -rw-r--r-- | tests/test_parse_double.rs | 1 | ||||
| -rw-r--r-- | tests/test_parse_integer.rs | 165 | ||||
| -rw-r--r-- | tests/test_parse_map.rs | 452 | ||||
| -rw-r--r-- | tests/test_parse_simple_error.rs | 145 | ||||
| -rw-r--r-- | tests/test_parse_simple_string.rs | 185 |
9 files changed, 1691 insertions, 0 deletions
diff --git a/tests/test_commands.rs b/tests/test_commands.rs new file mode 100644 index 0000000..10e6c9c --- /dev/null +++ b/tests/test_commands.rs @@ -0,0 +1,312 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::thread; + +use codecrafters_redis::resp_parser::RespType; +use codecrafters_redis::shared_cache::{CacheEntry, SharedCache}; + +// 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 codecrafters_redis::resp_commands::ExpiryOption; + use codecrafters_redis::resp_commands::RedisCommands; + 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!(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 codecrafters_redis::resp_commands::RedisCommands; + use std::time::Duration; + + 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 std::time::{SystemTime, UNIX_EPOCH}; + + use codecrafters_redis::resp_commands::{ExpiryOption, SetCommand}; + + 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()); + } +} diff --git a/tests/test_parse_array.rs b/tests/test_parse_array.rs new file mode 100644 index 0000000..48e8826 --- /dev/null +++ b/tests/test_parse_array.rs @@ -0,0 +1,130 @@ +use codecrafters_redis::resp_parser::*; + +#[test] +fn test_valid_arrays() { + // Simple array with strings + let arr = vec![ + RespType::BulkString("hello".into()), + RespType::BulkString("world".into()), + ]; + assert_eq!( + parse_array(b"*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n") + .unwrap() + .0, + RespType::Array(arr) + ); + + // Array with mixed types + let arr = vec![ + RespType::Integer(42), + RespType::SimpleString("OK".to_string()), + RespType::Null(), + ]; + assert_eq!( + parse_array(b"*3\r\n:42\r\n+OK\r\n_\r\n").unwrap().0, + RespType::Array(arr) + ); + + // Nested array + let arr = vec![ + RespType::Array(vec![ + RespType::BulkString("nested".into()), + RespType::Integer(123), + ]), + RespType::SimpleError("ERR test".to_string()), + ]; + assert_eq!( + parse_array(b"*2\r\n*2\r\n$6\r\nnested\r\n:123\r\n-ERR test\r\n") + .unwrap() + .0, + RespType::Array(arr) + ); + + // Empty array + let arr = vec![]; + assert_eq!(parse_array(b"*0\r\n").unwrap().0, RespType::Array(arr)); +} + +#[test] +fn test_invalid_arrays() { + // Wrong data type marker + assert_eq!( + parse_array(b"+2\r\n$5\r\nhello\r\n") + .err() + .unwrap() + .message(), + "ERR Invalid data type" + ); + + // Missing \r\n terminator + assert_eq!( + parse_array(b"*2\r\n$5\r\nhello\r\n$5\r\nworld") + .err() + .unwrap() + .message(), + "ERR Unexpected end of input" + ); + + // Invalid length + assert_eq!( + parse_array(b"*-1\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Non-numeric length + assert_eq!( + parse_array(b"*abc\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Incomplete array elements + assert_eq!( + parse_array(b"*2\r\n$5\r\nhello\r\n") + .err() + .unwrap() + .message(), + "ERR Unexpected end of input" + ); + + // Empty input + assert_eq!(parse_array(b"").err().unwrap().message(), "ERR Empty data"); + + // Just the marker + assert_eq!( + parse_array(b"*").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Invalid element type + assert_eq!( + parse_array(b"*1\r\n@invalid\r\n").err().unwrap().message(), + "ERR Invalid data type" + ); +} + +#[test] +fn test_array_remaining_bytes() { + // Test with remaining data + let arr = vec![RespType::BulkString("test".into()), RespType::Integer(99)]; + let (value, remaining) = parse_array(b"*2\r\n$4\r\ntest\r\n:99\r\n+OK\r\n").unwrap(); + assert_eq!(value, RespType::Array(arr)); + assert_eq!(remaining, b"+OK\r\n"); + + // Test with no remaining data + let arr = vec![RespType::SimpleString("PONG".to_string())]; + let (value, remaining) = parse_array(b"*1\r\n+PONG\r\n").unwrap(); + assert_eq!(value, RespType::Array(arr)); + assert_eq!(remaining, b""); + + // Test with multiple commands + let arr = vec![RespType::Null()]; + let (value, remaining) = parse_array(b"*1\r\n_\r\n*0\r\n").unwrap(); + assert_eq!(value, RespType::Array(arr)); + assert_eq!(remaining, b"*0\r\n"); + + // Test with empty array and remaining data + let arr = vec![]; + let (value, remaining) = parse_array(b"*0\r\n-ERR test\r\n").unwrap(); + assert_eq!(value, RespType::Array(arr)); + assert_eq!(remaining, b"-ERR test\r\n"); +} diff --git a/tests/test_parse_boolean.rs b/tests/test_parse_boolean.rs new file mode 100644 index 0000000..9cf865c --- /dev/null +++ b/tests/test_parse_boolean.rs @@ -0,0 +1,87 @@ +use codecrafters_redis::resp_parser::*; + +#[test] +fn test_valid_booleans() { + // Basic true value + assert_eq!(parse_boolean(b"#t\r\n").unwrap().0, RespType::Boolean(true)); + + // Basic false value + assert_eq!( + parse_boolean(b"#f\r\n").unwrap().0, + RespType::Boolean(false) + ); +} + +#[test] +fn test_invalid_booleans() { + // Wrong data type marker + assert_eq!( + parse_boolean(b":t\r\n").err().unwrap().message(), + "ERR Invalid data type" + ); + + // Invalid boolean value + assert_eq!( + parse_boolean(b"#x\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Missing \r\n terminator + assert_eq!( + parse_boolean(b"#t").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Only \r without \n + assert_eq!( + parse_boolean(b"#t\r").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Empty input + assert_eq!( + parse_boolean(b"").err().unwrap().message(), + "ERR Empty data" + ); + + // Just the marker + assert_eq!( + parse_boolean(b"#").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Case sensitivity + assert_eq!( + parse_boolean(b"#T\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Extra content + assert_eq!( + parse_boolean(b"#ttrue\r\n").err().unwrap().message(), + "ERR Unexpected end of input" + ); +} + +#[test] +fn test_boolean_remaining_bytes() { + // Test with remaining data + let (value, remaining) = parse_boolean(b"#t\r\n+OK\r\n").unwrap(); + assert_eq!(value, RespType::Boolean(true)); + assert_eq!(remaining, b"+OK\r\n"); + + // Test with no remaining data + let (value, remaining) = parse_boolean(b"#f\r\n").unwrap(); + assert_eq!(value, RespType::Boolean(false)); + assert_eq!(remaining, b""); + + // Test with multiple commands + let (value, remaining) = parse_boolean(b"#t\r\n:42\r\n").unwrap(); + assert_eq!(value, RespType::Boolean(true)); + assert_eq!(remaining, b":42\r\n"); + + // Test with false and remaining data + let (value, remaining) = parse_boolean(b"#f\r\n-ERR test\r\n").unwrap(); + assert_eq!(value, RespType::Boolean(false)); + assert_eq!(remaining, b"-ERR test\r\n"); +} diff --git a/tests/test_parse_bulk_string.rs b/tests/test_parse_bulk_string.rs new file mode 100644 index 0000000..1543262 --- /dev/null +++ b/tests/test_parse_bulk_string.rs @@ -0,0 +1,214 @@ +use codecrafters_redis::resp_parser::*; + +#[test] +fn test_valid_bulk_strings() { + // basic valid cases + assert_eq!(parse_bulk_strings(b"$2\r\nok\r\n").unwrap().0, "ok"); + assert_eq!(parse_bulk_strings(b"$4\r\npong\r\n").unwrap().0, "pong"); + assert_eq!( + parse_bulk_strings(b"$11\r\nhello world\r\n").unwrap().0, + "hello world" + ); + + // empty string + assert_eq!(parse_bulk_strings(b"$0\r\n\r\n").unwrap().0, ""); + + // string with special characters (including \r and \n - allowed in bulk strings) + assert_eq!( + parse_bulk_strings(b"$13\r\nhello\r\nworld!\r\n").unwrap().0, + "hello\r\nworld!" + ); + + // string with various ascii characters + assert_eq!( + parse_bulk_strings(b"$30\r\n!@#$%^&*()_+-={}[]|\\:;\"'<>?,./\r\n") + .unwrap() + .0, + "!@#$%^&*()_+-={}[]|\\:;\"'<>?,./" + ); + + // large string + let large_content = "x".repeat(1000); + let large_bulk = format!("$1000\r\n{}\r\n", large_content); + if let RespType::BulkString(bulk) = parse_bulk_strings(large_bulk.as_bytes()).unwrap().0 {} + + assert_eq!( + parse_bulk_strings(large_bulk.as_bytes()).unwrap().0, + large_content + ); + + // string with only whitespace + assert_eq!(parse_bulk_strings(b"$3\r\n \r\n").unwrap().0, " "); + + // string with tabs and newlines + assert_eq!( + parse_bulk_strings(b"$7\r\nhe\tllo\n\r\n").unwrap().0, + "he\tllo\n" + ); +} + +#[test] +fn test_null_bulk_string() { + // Null bulk string + let (result, remaining) = parse_bulk_strings(b"$-1\r\n").unwrap(); + assert_eq!(result, RespType::Null()); + assert_eq!(remaining, b""); + + // Null bulk string with remaining data + let (result, remaining) = parse_bulk_strings(b"$-1\r\n+OK\r\n").unwrap(); + assert_eq!(result, RespType::Null()); + assert_eq!(remaining, b"+OK\r\n"); +} + +#[test] +fn test_invalid_bulk_strings() { + // Wrong data type marker + assert_eq!( + parse_bulk_strings(b"+OK\r\n").err().unwrap().message(), + "ERR Invalid data type" + ); + + // Invalid length format + assert_eq!( + parse_bulk_strings(b"$abc\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Negative length (other than -1) + assert_eq!( + parse_bulk_strings(b"$-5\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Missing length + assert_eq!( + parse_bulk_strings(b"$\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Missing first \r\n after length + assert_eq!( + parse_bulk_strings(b"$5hello\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Content shorter than declared length + assert_eq!( + parse_bulk_strings(b"$5\r\nhi\r\n").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Content longer than declared length (missing final \r\n) + assert_eq!( + parse_bulk_strings(b"$2\r\nhello\r\n") + .err() + .unwrap() + .message(), + "ERR Unexpected end of input" + ); + + // Missing final \r\n + assert_eq!( + parse_bulk_strings(b"$5\r\nhello").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Only \r without \n at the end + assert_eq!( + parse_bulk_strings(b"$5\r\nhello\r") + .err() + .unwrap() + .message(), + "ERR Unexpected end of input" + ); + + // Only \n without \r at the end + assert_eq!( + parse_bulk_strings(b"$5\r\nhello\n") + .err() + .unwrap() + .message(), + "ERR Unexpected end of input" + ); + + // Empty input + assert_eq!( + parse_bulk_strings(b"").err().unwrap().message(), + "ERR Empty data" + ); + + // Just the marker + assert_eq!( + parse_bulk_strings(b"$").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Length too large for available data + assert_eq!( + parse_bulk_strings(b"$100\r\nshort\r\n") + .err() + .unwrap() + .message(), + "ERR Unexpected end of input" + ); + + // Zero length but with content + assert_eq!( + parse_bulk_strings(b"$0\r\nhello\r\n") + .err() + .unwrap() + .message(), + "ERR Unexpected end of input" + ); +} + +#[test] +fn test_bulk_string_remaining_bytes() { + // Test that remaining bytes are correctly returned + let (string, remaining) = parse_bulk_strings(b"$5\r\nhello\r\nnext data").unwrap(); + assert_eq!(string, "hello"); + assert_eq!(remaining, b"next data"); + + // Test with multiple commands + let (string, remaining) = parse_bulk_strings(b"$4\r\ntest\r\n:42\r\n").unwrap(); + assert_eq!(string, "test"); + assert_eq!(remaining, b":42\r\n"); + + // Test with no remaining data + let (string, remaining) = parse_bulk_strings(b"$3\r\nend\r\n").unwrap(); + assert_eq!(string, "end"); + assert_eq!(remaining, b""); + + // Test null string with remaining data + let (result, remaining) = parse_bulk_strings(b"$-1\r\n+PONG\r\n").unwrap(); + assert_eq!(result, RespType::Null()); + assert_eq!(remaining, b"+PONG\r\n"); +} + +#[test] +fn test_bulk_string_edge_cases() { + // String that contains the exact sequence that would end it + assert_eq!( + parse_bulk_strings(b"$8\r\ntest\r\n\r\n\r\n").unwrap().0, + "test\r\n" + ); + + // String with only \r\n + assert_eq!(parse_bulk_strings(b"$2\r\n\r\n\r\n").unwrap().0, "\r\n"); + + // String that starts with numbers + assert_eq!(parse_bulk_strings(b"$5\r\n12345\r\n").unwrap().0, "12345"); + + // String with control characters + assert_eq!( + parse_bulk_strings(b"$5\r\n\x01\x02\x03\x04\x05\r\n") + .unwrap() + .0, + "\x01\x02\x03\x04\x05" + ); + + // Maximum length value (within reason) + let content = "a".repeat(65535); + let bulk = format!("$65535\r\n{}\r\n", content); + assert_eq!(parse_bulk_strings(bulk.as_bytes()).unwrap().0, content); +} diff --git a/tests/test_parse_double.rs b/tests/test_parse_double.rs new file mode 100644 index 0000000..f8fa550 --- /dev/null +++ b/tests/test_parse_double.rs @@ -0,0 +1 @@ +use codecrafters_redis::resp_parser::*; diff --git a/tests/test_parse_integer.rs b/tests/test_parse_integer.rs new file mode 100644 index 0000000..b97d330 --- /dev/null +++ b/tests/test_parse_integer.rs @@ -0,0 +1,165 @@ +use codecrafters_redis::resp_parser::*; + +#[test] +fn test_valid_integers() { + // Basic valid cases + assert_eq!(parse_integers(b":0\r\n").unwrap().0, 0u64); + assert_eq!(parse_integers(b":1\r\n").unwrap().0, 1u64); + assert_eq!(parse_integers(b":42\r\n").unwrap().0, 42u64); + assert_eq!(parse_integers(b":1000\r\n").unwrap().0, 1000u64); + + assert_eq!(parse_integers(b":+42\r\n").unwrap().0, 42u64); + + // Large numbers + assert_eq!( + parse_integers(b":9223372036854775807\r\n").unwrap().0, + 9223372036854775807u64 + ); + assert_eq!( + parse_integers(b":18446744073709551615\r\n").unwrap().0, + 18446744073709551615u64 + ); // u64::MAX + + // Edge cases + assert_eq!(parse_integers(b":123456789\r\n").unwrap().0, 123456789u64); + + // Numbers with leading zeros (should still parse correctly) + assert_eq!(parse_integers(b":0000042\r\n").unwrap().0, 42u64); + assert_eq!(parse_integers(b":00000\r\n").unwrap().0, 0u64); +} + +#[test] +fn test_invalid_integers() { + // Wrong data type marker + assert_eq!( + parse_integers(b"+42\r\n").err().unwrap().message(), + "ERR Invalid data type" + ); + + // Negative numbers (not valid for u64) + assert_eq!( + parse_integers(b":-42\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Non-numeric content + assert_eq!( + parse_integers(b":abc\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Mixed numeric and non-numeric + assert_eq!( + parse_integers(b":42abc\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Empty integer + assert_eq!( + parse_integers(b":\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Contains \r in content + assert_eq!( + parse_integers(b":42\r23\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Contains \n in content + assert_eq!( + parse_integers(b":42\n23\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Missing \r\n terminator + assert_eq!( + parse_integers(b":42").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Only \r without \n + assert_eq!( + parse_integers(b":42\r").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Only \n without \r + assert_eq!( + parse_integers(b":42\n").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Empty input + assert_eq!( + parse_integers(b"").err().unwrap().message(), + "ERR Empty data" + ); + + // Just the marker without content + assert_eq!( + parse_integers(b":").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Number too large for u64 + assert_eq!( + parse_integers(b":18446744073709551616\r\n") // u64::MAX + 1 + .err() + .unwrap() + .message(), + "ERR invalid value" + ); + + // Floating point numbers + assert_eq!( + parse_integers(b":42.5\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Scientific notation + assert_eq!( + parse_integers(b":1e5\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Hexadecimal numbers + assert_eq!( + parse_integers(b":0x42\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Whitespace + assert_eq!( + parse_integers(b": 42\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + assert_eq!( + parse_integers(b":42 \r\n").err().unwrap().message(), + "ERR invalid value" + ); +} + +#[test] +fn test_integer_remaining_bytes() { + // Test that remaining bytes are correctly returned + let (integer, remaining) = parse_integers(b":42\r\nnext data").unwrap(); + assert_eq!(integer, 42u64); + assert_eq!(remaining, b"next data"); + + // Test with multiple commands + let (integer, remaining) = parse_integers(b":1337\r\n+OK\r\n").unwrap(); + assert_eq!(integer, 1337u64); + assert_eq!(remaining, b"+OK\r\n"); + + // Test with no remaining data + let (integer, remaining) = parse_integers(b":999\r\n").unwrap(); + assert_eq!(integer, 999u64); + assert_eq!(remaining, b""); + + // Test with zero and remaining data + let (integer, remaining) = parse_integers(b":0\r\n-ERR test\r\n").unwrap(); + assert_eq!(integer, 0u64); + assert_eq!(remaining, b"-ERR test\r\n"); +} diff --git a/tests/test_parse_map.rs b/tests/test_parse_map.rs new file mode 100644 index 0000000..e2b1432 --- /dev/null +++ b/tests/test_parse_map.rs @@ -0,0 +1,452 @@ +use codecrafters_redis::resp_parser::*; +use std::collections::HashMap; + +#[test] +fn test_valid_empty_map() { + // Empty map: %0\r\n + let expected_map = HashMap::new(); + assert_eq!( + parse_maps(b"%0\r\n").unwrap().0, + RespType::Maps(expected_map) + ); +} + +#[test] +fn test_valid_simple_string_map() { + // Simple map with simple string key-value pairs + // %2\r\n+key1\r\n+value1\r\n+key2\r\n+value2\r\n + let mut expected_map = HashMap::new(); + expected_map.insert( + "key1".to_string(), + RespType::SimpleString("value1".to_string()), + ); + expected_map.insert( + "key2".to_string(), + RespType::SimpleString("value2".to_string()), + ); + + assert_eq!( + parse_maps(b"%2\r\n+key1\r\n+value1\r\n+key2\r\n+value2\r\n") + .unwrap() + .0, + RespType::Maps(expected_map) + ); +} + +#[test] +fn test_valid_mixed_types_map() { + // Map with different value types + // %4\r\n+string_key\r\n+string_value\r\n+int_key\r\n:42\r\n+bool_key\r\n#t\r\n+null_key\r\n_\r\n + let mut expected_map = HashMap::new(); + expected_map.insert( + "string_key".to_string(), + RespType::SimpleString("string_value".to_string()), + ); + expected_map.insert("int_key".to_string(), RespType::Integer(42)); + expected_map.insert("bool_key".to_string(), RespType::Boolean(true)); + expected_map.insert("null_key".to_string(), RespType::Null()); + + assert_eq!( + parse_maps(b"%4\r\n+string_key\r\n+string_value\r\n+int_key\r\n:42\r\n+bool_key\r\n#t\r\n+null_key\r\n_\r\n").unwrap().0, + RespType::Maps(expected_map) + ); +} + +#[test] +fn test_valid_bulk_string_keys_and_values() { + // Map with bulk string keys and values + // %2\r\n$4\r\nkey1\r\n$6\r\nvalue1\r\n$4\r\nkey2\r\n$6\r\nvalue2\r\n + let mut expected_map = HashMap::new(); + expected_map.insert("key1".to_string(), RespType::BulkString("value1".into())); + expected_map.insert("key2".to_string(), RespType::BulkString("value2".into())); + + assert_eq!( + parse_maps(b"%2\r\n$4\r\nkey1\r\n$6\r\nvalue1\r\n$4\r\nkey2\r\n$6\r\nvalue2\r\n") + .unwrap() + .0, + RespType::Maps(expected_map) + ); +} + +#[test] +fn test_valid_array_values() { + // Map with array values + // %1\r\n+array_key\r\n*3\r\n+item1\r\n:123\r\n#f\r\n + let mut expected_map = HashMap::new(); + expected_map.insert( + "array_key".to_string(), + RespType::Array(vec![ + RespType::SimpleString("item1".to_string()), + RespType::Integer(123), + RespType::Boolean(false), + ]), + ); + + assert_eq!( + parse_maps(b"%1\r\n+array_key\r\n*3\r\n+item1\r\n:123\r\n#f\r\n") + .unwrap() + .0, + RespType::Maps(expected_map) + ); +} + +#[test] +fn test_valid_nested_map() { + // Map with nested map value + // %1\r\n+nested_key\r\n%1\r\n+inner_key\r\n+inner_value\r\n + let mut inner_map = HashMap::new(); + inner_map.insert( + "inner_key".to_string(), + RespType::SimpleString("inner_value".to_string()), + ); + + let mut expected_map = HashMap::new(); + expected_map.insert("nested_key".to_string(), RespType::Maps(inner_map)); + + assert_eq!( + parse_maps(b"%1\r\n+nested_key\r\n%1\r\n+inner_key\r\n+inner_value\r\n") + .unwrap() + .0, + RespType::Maps(expected_map) + ); +} + +#[test] +fn test_valid_double_values() { + // Map with double values + // %2\r\n+pi\r\n,3.14159\r\n+e\r\n,2.71828\r\n + let mut expected_map = HashMap::new(); + expected_map.insert("pi".to_string(), RespType::Doubles(3.14159)); + exp |
