diff options
Diffstat (limited to 'tests/test_rdb.rs')
| -rw-r--r-- | tests/test_rdb.rs | 704 |
1 files changed, 704 insertions, 0 deletions
diff --git a/tests/test_rdb.rs b/tests/test_rdb.rs new file mode 100644 index 0000000..109872c --- /dev/null +++ b/tests/test_rdb.rs @@ -0,0 +1,704 @@ +use codecrafters_redis::rdb::*; + +#[test] +fn test_rdb_header_valid_parsing() { + // Valid RDB header: "REDIS" + "0009" (version 9) + let input = b"REDIS0009"; + + let result = RDBHeader::from_bytes(input); + assert!(result.is_ok()); + + let (header, bytes_consumed) = result.unwrap(); + assert_eq!(header.magic_number, *b"REDIS"); + assert_eq!(header.version, *b"0009"); + assert_eq!(bytes_consumed, 9); +} + +#[test] +fn test_rdb_header_invalid_magic_number() { + let input = b"BADIS0009"; + + let result = RDBHeader::from_bytes(input); + assert_eq!(result, Err(ParseError::InvalidMagicNumber)); +} + +#[test] +fn test_rdb_header_insufficient_bytes() { + let input = b"RED"; // Too short + + let result = RDBHeader::from_bytes(input); + assert_eq!(result, Err(ParseError::UnexpectedEof)); +} + +#[test] +fn test_rdb_header_version_variations() { + // Test different valid versions + let test_cases = vec![ + (b"REDIS0006", *b"0006"), + (b"REDIS0007", *b"0007"), + (b"REDIS0008", *b"0008"), + (b"REDIS0009", *b"0009"), + (b"REDIS0010", *b"0010"), + ]; + + for (input, expected_version) in test_cases { + let (header, _) = RDBHeader::from_bytes(input).unwrap(); + assert_eq!(header.version, expected_version); + } +} + +#[test] +fn test_rdb_metadata_empty() { + // Empty metadata (no AUX fields before next opcode) + let input = &[RDBFile::SELECT_DB]; // Next opcode, no metadata + + let result = RDBMetaData::from_bytes(input); + assert!(result.is_ok()); + + let (metadata, bytes_consumed) = result.unwrap(); + assert!(metadata.metadata.is_empty()); + assert_eq!(bytes_consumed, 0); // No bytes consumed, just detected end +} + +#[test] +fn test_rdb_metadata_single_aux_field() { + // AUX opcode (0xFA) + key + value + // Format: 0xFA + length-encoded key + length-encoded value + let mut input = Vec::new(); + input.push(RDBFile::AUX); // 0xFA + + // Length-encoded string "redis-ver" (9 bytes, so length = 9) + input.push(9); // length of key + input.extend_from_slice(b"redis-ver"); + + // Length-encoded string "6.0.0" (5 bytes, so length = 5) + input.push(5); // length of value + input.extend_from_slice(b"6.0.0"); + + // End marker (next opcode) + input.push(RDBFile::SELECT_DB); + + let result = RDBMetaData::from_bytes(&input); + assert!(result.is_ok()); + + let (metadata, bytes_consumed) = result.unwrap(); + assert_eq!(metadata.metadata.len(), 1); + assert_eq!( + metadata.metadata.get(&"redis-ver".as_bytes().to_vec()), + Some(&"6.0.0".as_bytes().to_vec()) + ); + assert_eq!(bytes_consumed, 17); // 1 + 1 + 9 + 1 + 5 = 17 bytes +} + +#[test] +fn test_rdb_metadata_multiple_aux_fields() { + let mut input = Vec::new(); + + // First AUX field: redis-ver = 6.0.0 + input.push(RDBFile::AUX); + input.push(9); + input.extend_from_slice(b"redis-ver"); + input.push(5); + input.extend_from_slice(b"6.0.0"); + + // Second AUX field: redis-bits = 64 + input.push(RDBFile::AUX); + input.push(10); + input.extend_from_slice(b"redis-bits"); + input.push(2); + input.extend_from_slice(b"64"); + + // End marker + input.push(RDBFile::SELECT_DB); + + let result = RDBMetaData::from_bytes(&input); + assert!(result.is_ok()); + + let (metadata, bytes_consumed) = result.unwrap(); + assert_eq!(metadata.metadata.len(), 2); + assert_eq!( + metadata.metadata.get(&"redis-ver".as_bytes().to_vec()), + Some(&"6.0.0".as_bytes().to_vec()) + ); + assert_eq!( + metadata.metadata.get(&"redis-bits".as_bytes().to_vec()), + Some(&"64".as_bytes().to_vec()) + ); + assert_eq!(bytes_consumed, 32); // Calculate: 1+1+9+1+5 + 1+1+10+1+2 = 32 +} + +#[test] +fn test_rdb_metadata_invalid_aux_field() { + let mut input = Vec::new(); + input.push(RDBFile::AUX); + input.push(5); // Claims 5 bytes but only provides 3 + input.extend_from_slice(b"key"); // Only 3 bytes + + let result = RDBMetaData::from_bytes(&input); + assert_eq!(result.err().unwrap(), (ParseError::UnexpectedEof)); +} + +#[test] +fn test_rdb_metadata_string_encoding_edge_cases() { + // Test with binary data in metadata values + let mut input = Vec::new(); + input.push(RDBFile::AUX); + input.push(4); + input.extend_from_slice(b"test"); + input.push(3); + input.extend_from_slice(&[0xFF, 0x00, 0x7F]); // Binary data + input.push(RDBFile::SELECT_DB); + + let result = RDBMetaData::from_bytes(&input); + assert!(result.is_ok()); + + let (metadata, _) = result.unwrap(); + // Since we're storing as String, this tests how we handle non-UTF8 + assert_eq!(metadata.metadata.len(), 1); + assert!(metadata.metadata.contains_key(&"test".as_bytes().to_vec())); +} + +#[test] +fn test_integration_header_plus_metadata() { + // Test parsing header followed by metadata + let mut input = Vec::new(); + + // Header + input.extend_from_slice(b"REDIS0009"); + + // Metadata + input.push(RDBFile::AUX); + input.push(9); + input.extend_from_slice(b"redis-ver"); + input.push(5); + input.extend_from_slice(b"6.0.0"); + + // End of metadata + input.push(RDBFile::SELECT_DB); + + // Parse header first + let (_, header_consumed) = RDBHeader::from_bytes(&input).unwrap(); + assert_eq!(header_consumed, 9); + + // Parse metadata from remaining bytes + let (metadata, metadata_consumed) = RDBMetaData::from_bytes(&input[header_consumed..]).unwrap(); + assert_eq!(metadata.metadata.len(), 1); + + let total_consumed = header_consumed + metadata_consumed; + assert_eq!(total_consumed, input.len() - 1); // -1 for the SELECT_DB marker +} + +// Helper function to create test data for string entries +fn create_test_string_entry(key: &[u8], value: &[u8]) -> Vec<u8> { + let mut data = Vec::new(); + // Value type (0 = String) + data.push(0); + // Key (length-prefixed) + data.push(key.len() as u8); + data.extend_from_slice(key); + // Value (length-prefixed) + data.push(value.len() as u8); + data.extend_from_slice(value); + data +} + +// Helper function to create string entry with integer value encoding +fn create_test_integer_string_entry(key: &[u8], value: i64) -> Vec<u8> { + let mut data = Vec::new(); + // Value type (0 = String) + data.push(0); + // Key + data.push(key.len() as u8); + data.extend_from_slice(key); + // Integer value encoded as special format + if value >= i8::MIN as i64 && value <= i8::MAX as i64 { + data.extend_from_slice(&[0xC0, value as u8]); + } else if value >= i16::MIN as i64 && value <= i16::MAX as i64 { + data.push(0xC1); + data.extend_from_slice(&(value as i16).to_be_bytes()); + } else if value >= i32::MIN as i64 && value <= i32::MAX as i64 { + data.push(0xC2); + data.extend_from_slice(&(value as i32).to_be_bytes()); + } + data +} + +#[test] +fn test_empty_database_parsing() { + // Test parsing a database with no entries + let mut input = Vec::new(); + + // Hash table size info (both sizes are 0) + input.push(RDBFile::RESIZE_DB); + input.push(0); // hash_table_size = 0 + input.push(0); // expired_hash_table_size = 0 + input.push(RDBFile::EOF); // EOF + + // No entries follow + + let result = RDBDatabase::from_bytes(&input); + assert!(result.is_ok()); + + let (database, bytes_consumed) = result.unwrap(); + assert_eq!(database.size_hints.hash_table_size, 0); + assert_eq!(database.size_hints.expired_hash_table_size, 0); + assert!(database.hash_table.is_empty()); + assert_eq!(bytes_consumed, 3); // RESIZE_DB + 2 size bytes +} + +#[test] +fn test_single_string_entry_database() { + let mut input = Vec::new(); + + // Hash table size info + input.push(RDBFile::RESIZE_DB); + input.push(1); // hash_table_size = 1 + input.push(0); // expired_hash_table_size = 0 + + // Single string entry + input.extend_from_slice(&create_test_string_entry(b"mykey", b"myvalue")); + input.push(RDBFile::EOF); // EOF + + let result = RDBDatabase::from_bytes(&input); + assert!(result.is_ok()); + + let (database, bytes_consumed) = result.unwrap(); + assert_eq!(database.size_hints.hash_table_size, 1); + assert_eq!(database.size_hints.expired_hash_table_size, 0); + assert_eq!(database.hash_table.len(), 1); + + let entry = database.hash_table.get(b"mykey".as_slice()).unwrap(); + assert!(entry.expiry.is_none()); + assert_eq!(entry.value_type as u8, 0); // String type + if let RedisValue::String(value) = &entry.value { + assert_eq!(value, b"myvalue"); + } else { + panic!("Expected string value"); + } + + // Verify bytes consumed: RESIZE_DB(1) + sizes(2) + entry(1+1+5+1+7) = 17 + assert_eq!(bytes_consumed, 18); +} + +#[test] +fn test_multiple_string_entries() { + let mut input = Vec::new(); + + // Hash table size info + input.push(RDBFile::RESIZE_DB); + input.push(3); // hash_table_size = 3 + input.push(0); // expired_hash_table_size = 0 + + // Multiple string entries + input.extend_from_slice(&create_test_string_entry(b"key1", b"value1")); + input.extend_from_slice(&create_test_string_entry(b"key2", b"value2")); + input.extend_from_slice(&create_test_string_entry(b"key3", b"value3")); + input.push(RDBFile::EOF); + + let result = RDBDatabase::from_bytes(&input); + assert!(result.is_ok()); + + let (database, _) = result.unwrap(); + assert_eq!(database.size_hints.hash_table_size, 3); + assert_eq!(database.hash_table.len(), 3); + + // Verify all entries + let entry1 = database.hash_table.get(b"key1".as_slice()).unwrap(); + if let RedisValue::String(value) = &entry1.value { + assert_eq!(value, b"value1"); + } else { + panic!("Expected string value"); + } + + let entry2 = database.hash_table.get(b"key2".as_slice()).unwrap(); + if let RedisValue::String(value) = &entry2.value { + assert_eq!(value, b"value2"); + } else { + panic!("Expected string value"); + } + + let entry3 = database.hash_table.get(b"key3".as_slice()).unwrap(); + if let RedisValue::String(value) = &entry3.value { + assert_eq!(value, b"value3"); + } else { + panic!("Expected string value"); + } +} + +#[test] +fn test_string_entry_with_expiry_seconds() { + let mut input = Vec::new(); + + // Hash table size info + input.push(RDBFile::RESIZE_DB); + input.push(1); // hash_table_size = 1 + input.push(1); // expired_hash_table_size = 1 (this entry will have expiry) + + // Entry with expiry in seconds + input.push(RDBFile::EXPIRE_TIME); // 0xFD + input.extend_from_slice(&1672531200u32.to_le_bytes()); // Unix timestamp (4 bytes) + input.extend_from_slice(&create_test_string_entry( + b"expiring_key", + b"expiring_value", + )); + input.push(RDBFile::EOF); + + let result = RDBDatabase::from_bytes(&input); + assert!(result.is_ok()); + + let (database, _) = result.unwrap(); + assert_eq!(database.size_hints.expired_hash_table_size, 1); + assert_eq!(database.hash_table.len(), 1); + + let entry = database.hash_table.get(b"expiring_key".as_slice()).unwrap(); + assert!(entry.expiry.is_some()); + + let expiry = entry.expiry.as_ref().unwrap(); + assert_eq!(expiry.timestamp, 1672531200); + assert!(matches!(expiry.unit, ExpiryUnit::Seconds)); + + if let RedisValue::String(value) = &entry.value { + assert_eq!(value, b"expiring_value"); + } else { + panic!("Expected string value"); + } +} + +#[test] +fn test_string_entry_with_expiry_milliseconds() { + let mut input = Vec::new(); + + // Hash table size info + input.push(RDBFile::RESIZE_DB); + input.push(1); + input.push(1); // 1 entry will have expiry + + // Entry with expiry in milliseconds + input.push(RDBFile::EXPIRE_TIME_MS); // 0xFC + input.extend_from_slice(&1672531200000u64.to_le_bytes()); // Unix timestamp in ms (8 bytes) + input.extend_from_slice(&create_test_string_entry( + b"expiring_ms_key", + b"expiring_ms_value", + )); + input.push(RDBFile::EOF); + + let result = RDBDatabase::from_bytes(&input); + assert!(result.is_ok()); + + let (database, _) = result.unwrap(); + + let entry = database + .hash_table + .get(b"expiring_ms_key".as_slice()) + .unwrap(); + assert!(entry.expiry.is_some()); + + let expiry = entry.expiry.as_ref().unwrap(); + assert_eq!(expiry.timestamp, 1672531200000); + assert!(matches!(expiry.unit, ExpiryUnit::Milliseconds)); + + if let RedisValue::String(value) = &entry.value { + assert_eq!(value, b"expiring_ms_value"); + } else { + panic!("Expected string value"); + } +} + +#[test] +fn test_mixed_expiry_and_non_expiry_entries() { + let mut input = Vec::new(); + + input.push(RDBFile::RESIZE_DB); + input.push(3); // 3 total entries + input.push(2); // 2 entries will have expiry + + // Non-expiring entry + input.extend_from_slice(&create_test_string_entry( + b"permanent_key", + b"permanent_value", + )); + + // Entry with seconds expiry + input.push(RDBFile::EXPIRE_TIME); + input.extend_from_slice(&1672531200u32.to_le_bytes()); + input.extend_from_slice(&create_test_string_entry( + b"expires_sec", + b"expires_sec_value", + )); + + // Entry with milliseconds expiry + input.push(RDBFile::EXPIRE_TIME_MS); + input.extend_from_slice(&1672531300000u64.to_le_bytes()); + input.extend_from_slice(&create_test_string_entry( + b"expires_ms", + b"expires_ms_value", + )); + input.push(RDBFile::EOF); + + let result = RDBDatabase::from_bytes(&input); + assert!(result.is_ok()); + + let (database, _) = result.unwrap(); + assert_eq!(database.hash_table.len(), 3); + + // Check permanent entry + let permanent = database + .hash_table + .get(b"permanent_key".as_slice()) + .unwrap(); + assert!(permanent.expiry.is_none()); + + // Check seconds expiry entry + let sec_expiry = database.hash_table.get(b"expires_sec".as_slice()).unwrap(); + assert!(sec_expiry.expiry.is_some()); + assert!(matches!( + sec_expiry.expiry.as_ref().unwrap().unit, + ExpiryUnit::Seconds + )); + + // Check milliseconds expiry entry + let ms_expiry = database.hash_table.get(b"expires_ms".as_slice()).unwrap(); + assert!(ms_expiry.expiry.is_some()); + assert!(matches!( + ms_expiry.expiry.as_ref().unwrap().unit, + ExpiryUnit::Milliseconds + )); +} + +#[test] +fn test_string_with_integer_value_encoding() { + let mut input = Vec::new(); + + input.push(RDBFile::RESIZE_DB); + input.push(3); // 3 entries + input.push(0); + + // Test different integer encodings + input.extend_from_slice(&create_test_integer_string_entry(b"int8", 42)); // 8-bit + input.extend_from_slice(&create_test_integer_string_entry(b"int16", 1000)); // 16-bit + input.extend_from_slice(&create_test_integer_string_entry(b"int32", 100000)); // 32-bit + input.push(RDBFile::EOF); + + let result = RDBDatabase::from_bytes(&input); + assert!(result.is_ok()); + + let (database, _) = result.unwrap(); + assert_eq!(database.hash_table.len(), 3); + + // All should be parsed as Integer values due to special encoding + let int8_entry = database.hash_table.get(b"int8".as_slice()).unwrap(); + if let RedisValue::Integer(value) = &int8_entry.value { + assert_eq!(*value, 42); + } else { + panic!("Expected integer value, got: {:?}", int8_entry.value); + } + + let int16_entry = database.hash_table.get(b"int16".as_slice()).unwrap(); + if let RedisValue::Integer(value) = &int16_entry.value { + assert_eq!(*value, 1000); + } else { + panic!("Expected integer value"); + } + + let int32_entry = database.hash_table.get(b"int32".as_slice()).unwrap(); + if let RedisValue::Integer(value) = &int32_entry.value { + assert_eq!(*value, 100000); + } else { + panic!("Expected integer value"); + } +} + +#[test] +fn test_large_hash_table_sizes_14bit_encoding() { + let mut input = Vec::new(); + + input.push(RDBFile::RESIZE_DB); + + // Test 14-bit length encoding (values 64-16383) + let large_size = 16000; + // 14-bit encoding: 0b01xxxxxx xxxxxxxx + let first_byte = 0x40 | ((large_size >> 8) as u8); + let second_byte = (large_size & 0xFF) as u8; + input.extend_from_slice(&[first_byte, second_byte]); + + input.push(0); // No expired entries + input.push(RDBFile::EOF); + + let result = RDBDatabase::from_bytes(&input); + assert!(result.is_ok()); + + let (database, bytes_consumed) = result.unwrap(); + assert_eq!(database.size_hints.hash_table_size, large_size); + assert_eq!(database.size_hints.expired_hash_table_size, 0); + assert_eq!(bytes_consumed, 4); // RESIZE_DB + 2 bytes for 14-bit size + 1 byte for expired size +} + +#[test] +fn test_binary_string_data() { + let mut input = Vec::new(); + + input.push(RDBFile::RESIZE_DB); + input.push(2); + input.push(0); + + // Test with binary data containing null bytes and non-UTF8 sequences + let binary_key = &[0xFF, 0x00, 0x7F, b'k', b'e', b'y']; + let binary_value = &[0x00, 0xFF, 0x80, 0x81, 0x82]; + + input.extend_from_slice(&create_test_string_entry(binary_key, binary_value)); + + // Test with empty string + input.extend_from_slice(&create_test_string_entry(b"empty_value", b"")); + input.push(RDBFile::EOF); + + let result = RDBDatabase::from_bytes(&input); + assert!(result.is_ok()); + + let (database, _) = result.unwrap(); + assert_eq!(database.hash_table.len(), 2); + + // Verify binary data preserved + let binary_entry = database.hash_table.get(&binary_key.to_vec()).unwrap(); + if let RedisValue::String(value) = &binary_entry.value { + assert_eq!(value, binary_value); + } else { + panic!("Expected string value"); + } + + // Verify empty string + let empty_entry = database.hash_table.get(b"empty_value".as_slice()).unwrap(); + if let RedisValue::String(value) = &empty_entry.value { + assert!(value.is_empty()); + } else { + panic!("Expected string value"); + } +} + +#[test] +fn test_very_long_strings() { + let mut input = Vec::new(); + + input.push(RDBFile::RESIZE_DB); + input.push(1); + input.push(0); + + // Create a long string (using 14-bit length encoding) + let long_value = vec![b'A'; 1000]; + + // Manually create the entry with proper length encoding + let mut entry = Vec::new(); + entry.push(0); // String type + entry.push(8); // Key length + entry.extend_from_slice(b"long_key"); + + // 14-bit length encoding for 1000 bytes + let first_byte = 0x40 | ((1000 >> 8) as u8); + let second_byte = (1000 & 0xFF) as u8; + entry.extend_from_slice(&[first_byte, second_byte]); + entry.extend_from_slice(&long_value); + + input.extend_from_slice(&entry); + input.push(RDBFile::EOF); + + let result = RDBDatabase::from_bytes(&input); + assert!(result.is_ok()); + + let (database, _) = result.unwrap(); + assert_eq!(database.hash_table.len(), 1); + + let long_entry = database.hash_table.get(b"long_key".as_slice()).unwrap(); + if let RedisValue::String(value) = &long_entry.value { + assert_eq!(value.len(), 1000); + assert!(value.iter().all(|&b| b == b'A')); + } else { + panic!("Expected string value"); + } +} + +#[test] +fn test_database_parsing_errors() { + // Test truncated data + let truncated = vec![RDBFile::RESIZE_DB, 1]; // Missing expired size + let result = RDBDatabase::from_bytes(&truncated); + assert_eq!(result.err().unwrap(), ParseError::UnexpectedEof); + + // Test truncated string entry + let mut truncated_entry = Vec::new(); + truncated_entry.push(RDBFile::RESIZE_DB); + truncated_entry.push(1); + truncated_entry.push(0); + truncated_entry.push(0); // String type + truncated_entry.push(5); // Key length + truncated_entry.extend_from_slice(b"key"); // Only 3 bytes instead of 5 + truncated_entry.push(RDBFile::EOF); + + let result = RDBDatabase::from_bytes(&truncated_entry); + assert_eq!(result.err().unwrap(), ParseError::UnexpectedEof); +} + +#[test] +fn test_utf8_and_special_characters() { + let mut input = Vec::new(); + + input.push(RDBFile::RESIZE_DB); + input.push(3); + input.push(0); + + // UTF-8 strings + input.extend_from_slice(&create_test_string_entry( + "héllo".as_bytes(), + "wörld".as_bytes(), + )); + + // String with emoji + input.extend_from_slice(&create_test_string_entry( + "emoji_key".as_bytes(), + "🚀🔥".as_bytes(), + )); + + // String with various special characters + input.extend_from_slice(&create_test_string_entry( + "special".as_bytes(), + "!@#$%^&*()_+-=[]{}|;':\",./<>?`~".as_bytes(), + )); + input.push(RDBFile::EOF); + + let result = RDBDatabase::from_bytes(&input); + assert!(result.is_ok()); + + let (database, _) = result.unwrap(); + assert_eq!(database.hash_table.len(), 3); + + // Verify UTF-8 preservation + let utf8_entry = database.hash_table.get("héllo".as_bytes()).unwrap(); + if let RedisValue::String(value) = &utf8_entry.value { + assert_eq!(value, "wörld".as_bytes()); + } else { + panic!("Expected string value"); + } + + // Verify emoji preservation + let emoji_entry = database.hash_table.get("emoji_key".as_bytes()).unwrap(); + if let RedisValue::String(value) = &emoji_entry.value { + assert_eq!(value, "🚀🔥".as_bytes()); + } else { + panic!("Expected string value"); + } +} + +#[test] +fn test_rdb_file() { + let input: Vec<u8> = vec![ + 0x52, 0x45, 0x44, 0x49, 0x53, 0x30, 0x30, 0x31, 0x31, 0xfa, 0x09, 0x72, 0x65, 0x64, 0x69, + 0x73, 0x2d, 0x76, 0x65, 0x72, 0x05, 0x37, 0x2e, 0x32, 0x2e, 0x30, 0xfa, 0x0a, 0x72, 0x65, + 0x64, 0x69, 0x73, 0x2d, 0x62, 0x69, 0x74, 0x73, 0xc0, 0x40, 0xfe, 0x00, 0xfb, 0x01, 0x00, + 0x00, 0x04, 0x70, 0x65, 0x61, 0x72, 0x06, 0x6f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0xff, 0xe9, + 0xa3, 0x16, 0x7c, 0x84, 0x37, 0x9e, 0xb4, + ]; + + let result = RDBFile::from_bytes(&input); + assert!(result.is_ok()); + + let (rdb, _) = result.unwrap(); + assert_eq!(rdb.databases.get(&0).unwrap().hash_table.len(), 1); +} |
