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 --- tests/test_commands.rs | 312 ++++++++++++++++++++++++++ tests/test_parse_array.rs | 130 +++++++++++ tests/test_parse_boolean.rs | 87 ++++++++ tests/test_parse_bulk_string.rs | 214 ++++++++++++++++++ tests/test_parse_double.rs | 1 + tests/test_parse_integer.rs | 165 ++++++++++++++ tests/test_parse_map.rs | 452 ++++++++++++++++++++++++++++++++++++++ tests/test_parse_simple_error.rs | 145 ++++++++++++ tests/test_parse_simple_string.rs | 185 ++++++++++++++++ 9 files changed, 1691 insertions(+) create mode 100644 tests/test_commands.rs create mode 100644 tests/test_parse_array.rs create mode 100644 tests/test_parse_boolean.rs create mode 100644 tests/test_parse_bulk_string.rs create mode 100644 tests/test_parse_double.rs create mode 100644 tests/test_parse_integer.rs create mode 100644 tests/test_parse_map.rs create mode 100644 tests/test_parse_simple_error.rs create mode 100644 tests/test_parse_simple_string.rs (limited to 'tests') 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 { + 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 { + 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)); + expected_map.insert("e".to_string(), RespType::Doubles(2.71828)); + + assert_eq!( + parse_maps(b"%2\r\n+pi\r\n,3.14159\r\n+e\r\n,2.71828\r\n") + .unwrap() + .0, + RespType::Maps(expected_map) + ); +} + +#[test] +fn test_valid_simple_error_values() { + // Map with simple error values + // %2\r\n+error_key\r\n-ERR Something went wrong\r\n+another_key\r\n+value\r\n + let mut expected_map = HashMap::new(); + expected_map.insert( + "error_key".to_string(), + RespType::SimpleError("ERR Something went wrong".to_string()), + ); + expected_map.insert( + "another_key".to_string(), + RespType::SimpleString("value".to_string()), + ); + + assert_eq!( + parse_maps(b"%2\r\n+error_key\r\n-ERR Something went wrong\r\n+another_key\r\n+value\r\n") + .unwrap() + .0, + RespType::Maps(expected_map) + ); +} + +#[test] +fn test_valid_complex_mixed_map() { + // Complex map with various types mixed together + // %5\r\n+string\r\n+hello\r\n+number\r\n:42\r\n+list\r\n*2\r\n+a\r\n+b\r\n+map\r\n%1\r\n+nested\r\n+value\r\n+double\r\n,3.14\r\n + let mut nested_map = HashMap::new(); + nested_map.insert( + "nested".to_string(), + RespType::SimpleString("value".to_string()), + ); + + let mut expected_map = HashMap::new(); + expected_map.insert( + "string".to_string(), + RespType::SimpleString("hello".to_string()), + ); + expected_map.insert("number".to_string(), RespType::Integer(42)); + expected_map.insert( + "list".to_string(), + RespType::Array(vec![ + RespType::SimpleString("a".to_string()), + RespType::SimpleString("b".to_string()), + ]), + ); + expected_map.insert("map".to_string(), RespType::Maps(nested_map)); + expected_map.insert("double".to_string(), RespType::Doubles(3.14)); + + assert_eq!( + parse_maps(b"%5\r\n+string\r\n+hello\r\n+number\r\n:42\r\n+list\r\n*2\r\n+a\r\n+b\r\n+map\r\n%1\r\n+nested\r\n+value\r\n+double\r\n,3.14\r\n").unwrap().0, + RespType::Maps(expected_map) + ); +} + +#[test] +fn test_invalid_maps() { + // Wrong data type marker + assert_eq!( + parse_maps(b"+2\r\n+key\r\n+value\r\n") + .err() + .unwrap() + .message(), + "ERR Invalid data type" + ); + + // Wrong prefix + assert_eq!( + parse_maps(b"*2\r\n+key\r\n+value\r\n") + .err() + .unwrap() + .message(), + "ERR Invalid data type" + ); + + // Missing \r\n terminator after count + assert_eq!( + parse_maps(b"%1\r\n+key\r\n+value").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Invalid length - negative + assert_eq!( + parse_maps(b"%-1\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Non-numeric length + assert_eq!( + parse_maps(b"%abc\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Odd number of elements (maps need key-value pairs) + assert_eq!( + parse_maps(b"%1\r\n+key\r\n").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Incomplete map elements + assert_eq!( + parse_maps(b"%1\r\n+key\r\n").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Empty input + assert_eq!(parse_maps(b"").err().unwrap().message(), "ERR Empty data"); + + // Just the marker + assert_eq!( + parse_maps(b"%").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Invalid element type + assert_eq!( + parse_maps(b"%1\r\n@invalid\r\n+value\r\n") + .err() + .unwrap() + .message(), + "ERR Invalid data type" + ); +} + +#[test] +fn test_map_remaining_bytes() { + // Test with remaining data + let mut expected_map = HashMap::new(); + expected_map.insert( + "key".to_string(), + RespType::SimpleString("value".to_string()), + ); + + let (value, remaining) = parse_maps(b"%1\r\n+key\r\n+value\r\n+OK\r\n").unwrap(); + assert_eq!(value, RespType::Maps(expected_map)); + assert_eq!(remaining, b"+OK\r\n"); + + // Test with no remaining data + let mut expected_map = HashMap::new(); + expected_map.insert("test".to_string(), RespType::Integer(42)); + + let (value, remaining) = parse_maps(b"%1\r\n+test\r\n:42\r\n").unwrap(); + assert_eq!(value, RespType::Maps(expected_map)); + assert_eq!(remaining, b""); + + // Test with multiple commands + let mut expected_map = HashMap::new(); + expected_map.insert("null_key".to_string(), RespType::Null()); + + let (value, remaining) = parse_maps(b"%1\r\n+null_key\r\n_\r\n*0\r\n").unwrap(); + assert_eq!(value, RespType::Maps(expected_map)); + assert_eq!(remaining, b"*0\r\n"); + + // Test with empty map and remaining data + let expected_map = HashMap::new(); + let (value, remaining) = parse_maps(b"%0\r\n-ERR test\r\n").unwrap(); + assert_eq!(value, RespType::Maps(expected_map)); + assert_eq!(remaining, b"-ERR test\r\n"); +} + +#[test] +fn test_duplicate_keys() { + // Duplicate keys should overwrite (Redis behavior) + let mut expected_map = HashMap::new(); + expected_map.insert( + "key1".to_string(), + RespType::SimpleString("value2".to_string()), + ); + + assert_eq!( + parse_maps(b"%2\r\n+key1\r\n+value1\r\n+key1\r\n+value2\r\n") + .unwrap() + .0, + RespType::Maps(expected_map) + ); +} + +#[test] +fn test_large_map() { + // Test with a larger map + let mut input = b"%100\r\n".to_vec(); + let mut expected_map = HashMap::new(); + + for i in 0..100 { + input.extend_from_slice(format!("+key{}\r\n", i).as_bytes()); + input.extend_from_slice(format!("+value{}\r\n", i).as_bytes()); + expected_map.insert( + format!("key{}", i), + RespType::SimpleString(format!("value{}", i)), + ); + } + + assert_eq!(parse_maps(&input).unwrap().0, RespType::Maps(expected_map)); +} + +#[test] +fn test_edge_cases() { + // Empty string keys and values + let mut expected_map = HashMap::new(); + expected_map.insert("".to_string(), RespType::SimpleString("".to_string())); + + assert_eq!( + parse_maps(b"%1\r\n+\r\n+\r\n").unwrap().0, + RespType::Maps(expected_map) + ); + + // String with spaces and special characters + let mut expected_map = HashMap::new(); + expected_map.insert( + "key with spaces".to_string(), + RespType::SimpleString("value with spaces".to_string()), + ); + + assert_eq!( + parse_maps(b"%1\r\n+key with spaces\r\n+value with spaces\r\n") + .unwrap() + .0, + RespType::Maps(expected_map) + ); +} + +#[test] +fn test_nested_complex_structures() { + // Map containing array containing map + // %1\r\n+complex\r\n*1\r\n%1\r\n+nested\r\n+deep\r\n + let mut inner_map = HashMap::new(); + inner_map.insert( + "nested".to_string(), + RespType::SimpleString("deep".to_string()), + ); + + let mut expected_map = HashMap::new(); + expected_map.insert( + "complex".to_string(), + RespType::Array(vec![RespType::Maps(inner_map)]), + ); + + assert_eq!( + parse_maps(b"%1\r\n+complex\r\n*1\r\n%1\r\n+nested\r\n+deep\r\n") + .unwrap() + .0, + RespType::Maps(expected_map) + ); +} + +#[test] +fn test_binary_data_in_bulk_strings() { + // Map with binary data in bulk string values + let mut input = b"%1\r\n+binary_key\r\n$3\r\n".to_vec(); + input.extend_from_slice(&[0xFF, 0x00, 0xFE]); // Binary data + input.extend_from_slice(b"\r\n"); + + let mut expected_map = HashMap::new(); + expected_map.insert( + "binary_key".to_string(), + RespType::BulkString(vec![0xFF, 0x00, 0xFE]), + ); + + assert_eq!(parse_maps(&input).unwrap().0, RespType::Maps(expected_map)); +} + +#[test] +fn test_unicode_keys() { + // Map with Unicode keys + let mut expected_map = HashMap::new(); + expected_map.insert( + "éáñ".to_string(), + RespType::SimpleString("value1".to_string()), + ); + expected_map.insert( + "中文".to_string(), + RespType::SimpleString("value2".to_string()), + ); + + assert_eq!( + parse_maps(b"%2\r\n+\xc3\xa9\xc3\xa1\xc3\xb1\r\n+value1\r\n+\xe4\xb8\xad\xe6\x96\x87\r\n+value2\r\n").unwrap().0, + RespType::Maps(expected_map) + ); +} + +#[test] +fn test_mixed_key_types() { + // Test with bulk string keys + let mut expected_map = HashMap::new(); + expected_map.insert( + "bulk_key".to_string(), + RespType::SimpleString("value".to_string()), + ); + expected_map.insert("simple_key".to_string(), RespType::Integer(42)); + + assert_eq!( + parse_maps(b"%2\r\n$8\r\nbulk_key\r\n+value\r\n+simple_key\r\n:42\r\n") + .unwrap() + .0, + RespType::Maps(expected_map) + ); +} + +#[test] +fn test_deeply_nested_structures() { + // Map -> Array -> Map -> Array + // %1\r\n+level1\r\n*1\r\n%1\r\n+level2\r\n*2\r\n+item1\r\n+item2\r\n + let mut level2_map = HashMap::new(); + level2_map.insert( + "level2".to_string(), + RespType::Array(vec![ + RespType::SimpleString("item1".to_string()), + RespType::SimpleString("item2".to_string()), + ]), + ); + + let mut expected_map = HashMap::new(); + expected_map.insert( + "level1".to_string(), + RespType::Array(vec![RespType::Maps(level2_map)]), + ); + + assert_eq!( + parse_maps(b"%1\r\n+level1\r\n*1\r\n%1\r\n+level2\r\n*2\r\n+item1\r\n+item2\r\n") + .unwrap() + .0, + RespType::Maps(expected_map) + ); +} diff --git a/tests/test_parse_simple_error.rs b/tests/test_parse_simple_error.rs new file mode 100644 index 0000000..e3e9a58 --- /dev/null +++ b/tests/test_parse_simple_error.rs @@ -0,0 +1,145 @@ +use codecrafters_redis::resp_parser::*; + +#[test] +fn test_valid_simple_errors() { + // Basic valid cases + assert_eq!( + parse_simple_errors(b"-ERR unknown command\r\n").unwrap().0, + "ERR unknown command" + ); + assert_eq!( + parse_simple_errors(b"-WRONGTYPE\r\n").unwrap().0, + "WRONGTYPE" + ); + assert_eq!( + parse_simple_errors(b"-ERR syntax error\r\n").unwrap().0, + "ERR syntax error" + ); + + // Empty error string + assert_eq!(parse_simple_errors(b"-\r\n").unwrap().0, ""); + + // Error with spaces and special characters (but no \r or \n) + assert_eq!( + parse_simple_errors(b"-ERR invalid key: 'test123'\r\n") + .unwrap() + .0, + "ERR invalid key: 'test123'" + ); + + // Error with various ASCII characters + assert_eq!( + parse_simple_errors(b"-ERR !@#$%^&*()_+-={}[]|\\:;\"'<>?,./ \r\n") + .unwrap() + .0, + "ERR !@#$%^&*()_+-={}[]|\\:;\"'<>?,./ " + ); + + // Unicode characters in error message + assert_eq!( + parse_simple_errors(b"-ERR \xc3\xa9\xc3\xa1\xc3\xb1\r\n") + .unwrap() + .0, + "ERR éáñ" + ); + + // Common Redis error patterns + assert_eq!( + parse_simple_errors(b"-NOAUTH Authentication required\r\n") + .unwrap() + .0, + "NOAUTH Authentication required" + ); + assert_eq!( + parse_simple_errors( + b"-WRONGTYPE Operation against a key holding the wrong kind of value\r\n" + ) + .unwrap() + .0, + "WRONGTYPE Operation against a key holding the wrong kind of value" + ); +} + +#[test] +fn test_invalid_simple_errors() { + // Wrong data type marker + assert_eq!( + parse_simple_errors(b"+OK\r\n").err().unwrap().message(), + "ERR Invalid data type" + ); + + // Contains \r in content + assert_eq!( + parse_simple_errors(b"-ERR invalid\r character\r\n") + .err() + .unwrap() + .message(), + "ERR invalid value" + ); + + // Contains \n in content + assert_eq!( + parse_simple_errors(b"-ERR invalid\n character\r\n") + .err() + .unwrap() + .message(), + "ERR invalid value" + ); + + // Missing \r\n terminator + assert_eq!( + parse_simple_errors(b"-ERR no terminator") + .err() + .unwrap() + .message(), + "ERR Unexpected end of input" + ); + + // Only \r without \n + assert_eq!( + parse_simple_errors(b"-ERR only carriage return\r") + .err() + .unwrap() + .message(), + "ERR Unexpected end of input" + ); + + // Only \n without \r + assert_eq!( + parse_simple_errors(b"-ERR only newline\n") + .err() + .unwrap() + .message(), + "ERR Unexpected end of input" + ); + + // Empty input + assert_eq!( + parse_simple_errors(b"").err().unwrap().message(), + "ERR Empty data" + ); + + // Just the marker without content + assert_eq!( + parse_simple_errors(b"-").err().unwrap().message(), + "ERR Unexpected end of input" + ); +} + +#[test] +fn test_simple_error_remaining_bytes() { + // Test that remaining bytes are correctly returned + let (error, remaining) = parse_simple_errors(b"-ERR test\r\nnext data").unwrap(); + assert_eq!(error, "ERR test"); + assert_eq!(remaining, b"next data"); + + // Test with multiple commands + let (error, remaining) = parse_simple_errors(b"-WRONGTYPE\r\n+OK\r\n").unwrap(); + assert_eq!(error, "WRONGTYPE"); + assert_eq!(remaining, b"+OK\r\n"); + + // Test with no remaining data + let (error, remaining) = parse_simple_errors(b"-ERR final\r\n").unwrap(); + assert_eq!(error, "ERR final"); + assert_eq!(remaining, b""); +} diff --git a/tests/test_parse_simple_string.rs b/tests/test_parse_simple_string.rs new file mode 100644 index 0000000..2e9ed11 --- /dev/null +++ b/tests/test_parse_simple_string.rs @@ -0,0 +1,185 @@ +use codecrafters_redis::resp_parser::*; + +#[test] +fn test_valid_simple_strings() { + // Basic valid cases + assert_eq!(parse_simple_strings(b"+OK\r\n").unwrap().0, "OK"); + assert_eq!(parse_simple_strings(b"+PONG\r\n").unwrap().0, "PONG"); + assert_eq!( + parse_simple_strings(b"+Hello World\r\n").unwrap().0, + "Hello World" + ); + + // Empty string + assert_eq!(parse_simple_strings(b"+\r\n").unwrap().0, ""); + + // String with spaces and special characters (but no \r or \n) + assert_eq!( + parse_simple_strings(b"+Hello, World! 123\r\n").unwrap().0, + "Hello, World! 123" + ); + + // String with various ASCII characters + assert_eq!( + parse_simple_strings(b"+!@#$%^&*()_+-={}[]|\\:;\"'<>?,./ \r\n") + .unwrap() + .0, + "!@#$%^&*()_+-={}[]|\\:;\"'<>?,./ " + ); + + // Unicode characters (should work with UTF-8) + assert_eq!( + parse_simple_strings(b"+\xc3\xa9\xc3\xa1\xc3\xb1\r\n") + .unwrap() + .0, + "éáñ" + ); +} + +#[test] +fn test_invalid_prefix() { + // Missing '+' prefix + assert_eq!( + parse_simple_strings(b"OK\r\n").err().unwrap().message(), + "WRONGTYPE Operation against a key holding the wrong kind of value" + ); + + // Wrong prefix + assert_eq!( + parse_simple_strings(b"-Error\r\n").err().unwrap().message(), + "WRONGTYPE Operation against a key holding the wrong kind of value" + ); + assert_eq!( + parse_simple_strings(b":123\r\n").err().unwrap().message(), + "WRONGTYPE Operation against a key holding the wrong kind of value" + ); + assert_eq!( + parse_simple_strings(b"$5\r\nhello\r\n") + .err() + .unwrap() + .message(), + "WRONGTYPE Operation against a key holding the wrong kind of value" + ); +} + +#[test] +fn test_missing_crlf_terminator() { + // No CRLF at all + assert_eq!( + parse_simple_strings(b"+OK").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Only \r + assert_eq!( + parse_simple_strings(b"+OK\r").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Only \n + assert_eq!( + parse_simple_strings(b"+OK\n").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Wrong order (\n\r instead of \r\n) + assert_eq!( + parse_simple_strings(b"+OK\n\r").err().unwrap().message(), + "ERR Unexpected end of input" + ); +} + +#[test] +fn test_invalid_characters_in_content() { + // Contains \r in content + assert_eq!( + parse_simple_strings(b"+Hello\rWorld\r\n") + .err() + .unwrap() + .message(), + "ERR invalid value" + ); + + // Contains \n in content + assert_eq!( + parse_simple_strings(b"+Hello\nWorld\r\n") + .err() + .unwrap() + .message(), + "ERR invalid value" + ); +} + +#[test] +fn test_empty_input() { + assert_eq!( + parse_simple_strings(b"").err().unwrap().message(), + "ERR Empty data" + ); +} + +#[test] +fn test_with_trailing_data() { + // RESP simple string with extra data after CRLF (should be ignored) + assert_eq!(parse_simple_strings(b"+OK\r\nextra_data").unwrap().0, "OK"); + assert_eq!( + parse_simple_strings(b"+PONG\r\n+another_string\r\n") + .unwrap() + .0, + "PONG" + ); +} + +#[test] +fn test_real_world_redis_responses() { + // Common Redis simple string responses + assert_eq!(parse_simple_strings(b"+OK\r\n").unwrap().0, "OK"); + assert_eq!(parse_simple_strings(b"+PONG\r\n").unwrap().0, "PONG"); + assert_eq!(parse_simple_strings(b"+QUEUED\r\n").unwrap().0, "QUEUED"); + + // Redis status responses + assert_eq!( + parse_simple_strings(b"+Background saving started\r\n") + .unwrap() + .0, + "Background saving started" + ); + assert_eq!( + parse_simple_strings(b"+Background saving successfully finished\r\n") + .unwrap() + .0, + "Background saving successfully finished" + ); +} + +#[test] +fn test_edge_cases() { + // Just the prefix and CRLF + assert_eq!(parse_simple_strings(b"+\r\n").unwrap().0, ""); + + // Long string + let long_string = "a".repeat(1000); + let mut input = b"+".to_vec(); + input.extend_from_slice(long_string.as_bytes()); + input.extend_from_slice(b"\r\n"); + assert_eq!(parse_simple_strings(&input).unwrap().0, long_string); + + // String with only spaces + assert_eq!(parse_simple_strings(b"+ \r\n").unwrap().0, " "); + + // String with tabs and other whitespace + assert_eq!(parse_simple_strings(b"+\t \t\r\n").unwrap().0, "\t \t"); +} + +#[test] +fn test_binary_safety_within_limits() { + // Non-UTF8 bytes (but no \r or \n) + let mut input = b"+".to_vec(); + input.extend_from_slice(&[0xFF, 0xFE, 0xFD]); // Invalid UTF-8 + input.extend_from_slice(b"\r\n"); + + // Should handle invalid UTF-8 gracefully with replacement characters + if let RespType::SimpleString(data) = parse_simple_strings(&input).unwrap().0 { + assert!(!data.is_empty()); // Should contain replacement characters + } +} -- cgit v1.2.3