aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authoromagdy <omar.professional8777@gmail.com>2025-07-17 08:06:26 +0300
committeromagdy <omar.professional8777@gmail.com>2025-07-17 08:06:26 +0300
commit38b649ea16d8ed053fd9222bfb9867e3432ee2a6 (patch)
treea7fbde68ad869e1b74071207bdf7b7c159c7f75f /tests
parentc880c7ad3eba9546ce95bc268218c66a128d319f (diff)
downloadredis-rust-38b649ea16d8ed053fd9222bfb9867e3432ee2a6.tar.xz
redis-rust-38b649ea16d8ed053fd9222bfb9867e3432ee2a6.zip
test: Moved tests to seprate files under tests folder for more structure
Diffstat (limited to 'tests')
-rw-r--r--tests/test_commands.rs312
-rw-r--r--tests/test_parse_array.rs130
-rw-r--r--tests/test_parse_boolean.rs87
-rw-r--r--tests/test_parse_bulk_string.rs214
-rw-r--r--tests/test_parse_double.rs1
-rw-r--r--tests/test_parse_integer.rs165
-rw-r--r--tests/test_parse_map.rs452
-rw-r--r--tests/test_parse_simple_error.rs145
-rw-r--r--tests/test_parse_simple_string.rs185
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")
+