test(tree): add comprehensive unit tests and fix input_char_count bug (#16228)

This commit is contained in:
Simo Lin
2025-12-31 07:31:39 -08:00
committed by GitHub
parent abdf65d4f3
commit 60d7279c46

View File

@@ -554,6 +554,7 @@ impl Tree {
} else {
// Partial match - still use this node for tenant selection
matched_chars += shared_count;
remaining = advance_by_chars(remaining, shared_count);
prev = matched_node;
break;
}
@@ -1721,4 +1722,588 @@ mod tests {
let tenant2_size = final_sizes.get("tenant2").unwrap();
assert_eq!(tenant2_size, &(5 + 5 + 6 + 2)); // "apple" + "etite" + "banana" + "ll"
}
// ==================== Edge Case Tests ====================
#[test]
fn test_empty_string_input() {
let tree = Tree::new();
// Insert empty string
tree.insert("", "tenant1");
// Match empty string
let (matched, tenant) = tree.prefix_match("");
assert_eq!(matched, "");
assert_eq!(tenant, "tenant1");
// Insert non-empty, then match empty
tree.insert("hello", "tenant2");
let (matched, tenant) = tree.prefix_match("");
assert_eq!(matched, "");
assert_eq!(tenant, "tenant1");
}
#[test]
fn test_single_character_operations() {
let tree = Tree::new();
// Insert single characters
tree.insert("a", "tenant1");
tree.insert("b", "tenant2");
tree.insert("c", "tenant1");
let (matched, tenant) = tree.prefix_match("a");
assert_eq!(matched, "a");
assert_eq!(tenant, "tenant1");
let (matched, tenant) = tree.prefix_match("b");
assert_eq!(matched, "b");
assert_eq!(tenant, "tenant2");
// Match with longer string starting with single char
let (matched, tenant) = tree.prefix_match("abc");
assert_eq!(matched, "a");
assert_eq!(tenant, "tenant1");
}
#[test]
fn test_prefix_is_subset_of_existing() {
let tree = Tree::new();
// Insert longer string first
tree.insert("application", "tenant1");
// Now insert prefix of existing
tree.insert("app", "tenant2");
// Match the prefix - both tenants own "app" node
let (matched, tenant) = tree.prefix_match("app");
assert_eq!(matched, "app");
assert!(tenant == "tenant1" || tenant == "tenant2");
// Match longer string
let (matched, tenant) = tree.prefix_match("application");
assert_eq!(matched, "application");
assert_eq!(tenant, "tenant1");
// Match "apple" - matches "app" + "l" from the child node = "appl"
// Then 'e' doesn't match 'i' in the remaining suffix, so stops at 4 chars
let (matched, _tenant) = tree.prefix_match("apple");
assert_eq!(matched, "appl");
}
#[test]
fn test_existing_is_prefix_of_new() {
let tree = Tree::new();
// Insert shorter string first
tree.insert("app", "tenant1");
// Now insert longer string with same prefix
tree.insert("application", "tenant2");
let (matched, tenant) = tree.prefix_match("app");
assert_eq!(matched, "app");
assert!(tenant == "tenant1" || tenant == "tenant2");
let (matched, tenant) = tree.prefix_match("application");
assert_eq!(matched, "application");
assert_eq!(tenant, "tenant2");
// "applesauce" matches "app" + "l" from the child node = "appl"
// Then 'e' in "esauce" doesn't match 'i' in the suffix, so matching stops
let (matched, _tenant) = tree.prefix_match("applesauce");
assert_eq!(matched, "appl");
}
// ==================== prefix_match_with_counts Tests ====================
#[test]
fn test_prefix_match_with_counts_accuracy() {
let tree = Tree::new();
tree.insert("hello world", "tenant1");
// Exact match
let result = tree.prefix_match_with_counts("hello world");
assert_eq!(result.matched_char_count, 11);
assert_eq!(result.input_char_count, 11);
assert_eq!(&*result.tenant, "tenant1");
// Partial match
let result = tree.prefix_match_with_counts("hello");
assert_eq!(result.matched_char_count, 5);
assert_eq!(result.input_char_count, 5);
// Extended match
let result = tree.prefix_match_with_counts("hello world and more");
assert_eq!(result.matched_char_count, 11);
assert_eq!(result.input_char_count, 20);
// No match
let result = tree.prefix_match_with_counts("goodbye");
assert_eq!(result.matched_char_count, 0);
assert_eq!(result.input_char_count, 7);
}
#[test]
fn test_prefix_match_with_counts_utf8() {
let tree = Tree::new();
// UTF-8 string: 5 characters, more bytes
tree.insert("你好世界呀", "tenant1");
let result = tree.prefix_match_with_counts("你好世界呀");
assert_eq!(result.matched_char_count, 5);
assert_eq!(result.input_char_count, 5);
let result = tree.prefix_match_with_counts("你好");
assert_eq!(result.matched_char_count, 2);
assert_eq!(result.input_char_count, 2);
// Mixed ASCII and UTF-8
tree.insert("hello你好", "tenant2");
let result = tree.prefix_match_with_counts("hello你好世界");
assert_eq!(result.matched_char_count, 7); // "hello你好" = 7 chars
assert_eq!(result.input_char_count, 9); // "hello你好世界" = 9 chars
}
// ==================== Node Splitting Edge Cases ====================
#[test]
fn test_split_at_first_character() {
let tree = Tree::new();
// Insert "abc"
tree.insert("abc", "tenant1");
// Insert "aXX" - should split at first char
tree.insert("aXX", "tenant2");
let (matched, tenant) = tree.prefix_match("abc");
assert_eq!(matched, "abc");
assert_eq!(tenant, "tenant1");
let (matched, tenant) = tree.prefix_match("aXX");
assert_eq!(matched, "aXX");
assert_eq!(tenant, "tenant2");
let (matched, _) = tree.prefix_match("a");
assert_eq!(matched, "a");
}
#[test]
fn test_split_at_last_character() {
let tree = Tree::new();
// Insert "abcd"
tree.insert("abcd", "tenant1");
// Insert "abcX" - should split at last char of shared prefix
tree.insert("abcX", "tenant2");
let (matched, tenant) = tree.prefix_match("abcd");
assert_eq!(matched, "abcd");
assert_eq!(tenant, "tenant1");
let (matched, tenant) = tree.prefix_match("abcX");
assert_eq!(matched, "abcX");
assert_eq!(tenant, "tenant2");
let (matched, _) = tree.prefix_match("abc");
assert_eq!(matched, "abc");
}
#[test]
fn test_multiple_splits_same_path() {
let tree = Tree::new();
// Create a chain of splits
tree.insert("abcdefgh", "tenant1");
tree.insert("abcdef", "tenant2");
tree.insert("abcd", "tenant3");
tree.insert("ab", "tenant4");
// Verify all paths work
assert_eq!(tree.prefix_match("abcdefgh").0, "abcdefgh");
assert_eq!(tree.prefix_match("abcdef").0, "abcdef");
assert_eq!(tree.prefix_match("abcd").0, "abcd");
assert_eq!(tree.prefix_match("ab").0, "ab");
assert_eq!(tree.prefix_match("a").0, "a");
}
// ==================== High Contention Stress Tests ====================
#[test]
fn test_high_contention_same_prefix() {
let tree = Arc::new(Tree::new());
let num_threads = 16;
let ops_per_thread = 100;
let mut handles = vec![];
// All threads operate on strings with same prefix
for thread_id in 0..num_threads {
let tree = Arc::clone(&tree);
let handle = thread::spawn(move || {
let tenant = format!("tenant{}", thread_id);
for i in 0..ops_per_thread {
let text = format!("shared_prefix_{}", i);
tree.insert(&text, &tenant);
// Immediately try to match
let (matched, _) = tree.prefix_match(&text);
assert!(
matched.starts_with("shared_prefix_"),
"Match should start with shared_prefix_"
);
}
});
handles.push(handle);
}
for handle in handles {
handle.join().expect("Thread panicked");
}
// Verify tree is still consistent
let sizes = tree.get_used_size_per_tenant();
assert!(!sizes.is_empty(), "Tree should have entries");
}
#[test]
fn test_rapid_insert_remove_cycles() {
let tree = Arc::new(Tree::new());
let num_cycles = 50;
for cycle in 0..num_cycles {
let tenant = format!("tenant{}", cycle % 5);
// Insert several entries
for i in 0..10 {
let text = format!("cycle{}entry{}", cycle, i);
tree.insert(&text, &tenant);
}
// Remove the tenant
tree.remove_tenant(&tenant);
// Verify tenant is gone
let sizes = tree.get_used_size_per_tenant();
assert!(
!sizes.contains_key(&tenant),
"Tenant {} should be removed after cycle {}",
tenant,
cycle
);
}
}
// ==================== ASCII/UTF-8 Consistency Tests ====================
#[test]
fn test_ascii_utf8_consistency() {
let tree = Tree::new();
// Insert ASCII
tree.insert("hello", "tenant1");
// Insert UTF-8 with same logical prefix (none)
tree.insert("你好", "tenant2");
// Insert mixed
tree.insert("hello你好", "tenant3");
// All should be retrievable
assert_eq!(tree.prefix_match("hello").0, "hello");
assert_eq!(tree.prefix_match("你好").0, "你好");
assert_eq!(tree.prefix_match("hello你好").0, "hello你好");
// Counts should be correct
let result = tree.prefix_match_with_counts("hello");
assert_eq!(result.matched_char_count, 5);
assert_eq!(result.input_char_count, 5);
let result = tree.prefix_match_with_counts("你好");
assert_eq!(result.matched_char_count, 2);
assert_eq!(result.input_char_count, 2);
let result = tree.prefix_match_with_counts("hello你好");
assert_eq!(result.matched_char_count, 7);
assert_eq!(result.input_char_count, 7);
}
#[test]
fn test_emoji_handling() {
let tree = Tree::new();
// Emoji are multi-byte UTF-8
tree.insert("hello 👋", "tenant1");
tree.insert("hello 👋🌍", "tenant2");
let (matched, tenant) = tree.prefix_match("hello 👋");
assert_eq!(matched, "hello 👋");
assert_eq!(tenant, "tenant1");
let (matched, tenant) = tree.prefix_match("hello 👋🌍");
assert_eq!(matched, "hello 👋🌍");
assert_eq!(tenant, "tenant2");
// Verify char count (not byte count)
let result = tree.prefix_match_with_counts("hello 👋");
assert_eq!(result.matched_char_count, 7);
assert_eq!(result.input_char_count, 7); // h-e-l-l-o-space-emoji
}
// ==================== Eviction Edge Cases ====================
#[test]
fn test_eviction_empty_tree() {
let tree = Tree::new();
// Should not panic on empty tree
tree.evict_tenant_by_size(100);
let sizes = tree.get_used_size_per_tenant();
assert!(sizes.is_empty());
}
#[test]
fn test_eviction_zero_max_size() {
let tree = Tree::new();
tree.insert("hello", "tenant1");
tree.insert("world", "tenant1");
// Evict with max_size = 0 should remove everything
tree.evict_tenant_by_size(0);
let sizes = tree.get_used_size_per_tenant();
assert!(
sizes.is_empty() || sizes.values().all(|&v| v == 0),
"All tenants should be evicted or have zero size"
);
}
#[test]
fn test_eviction_single_tenant_all_entries() {
let tree = Tree::new();
// Insert many entries for single tenant
for i in 0..100 {
let text = format!("entry{:03}", i);
tree.insert(&text, "tenant1");
}
let initial_size = *tree.get_used_size_per_tenant().get("tenant1").unwrap();
assert!(initial_size > 50, "Should have significant size");
// Evict to small size
tree.evict_tenant_by_size(50);
let final_size = *tree.get_used_size_per_tenant().get("tenant1").unwrap_or(&0);
assert!(
final_size <= 50,
"Size {} should be <= 50 after eviction",
final_size
);
}
// ==================== Last Tenant Cache Tests ====================
#[test]
fn test_last_tenant_cache_update() {
let tree = Tree::new();
// Insert for tenant1
tree.insert("hello", "tenant1");
// First match should return tenant1
let (_, tenant) = tree.prefix_match("hello");
assert_eq!(tenant, "tenant1");
// Insert for tenant2 on same path
tree.insert("hello", "tenant2");
// Match again - should still work (cache or iteration)
let (matched, _) = tree.prefix_match("hello");
assert_eq!(matched, "hello");
}
#[test]
fn test_stale_cache_after_tenant_removal() {
let tree = Tree::new();
tree.insert("hello", "tenant1");
tree.insert("hello", "tenant2");
// Access to populate cache
let _ = tree.prefix_match("hello");
// Remove tenant1
tree.remove_tenant("tenant1");
// Should still work with tenant2
let (matched, tenant) = tree.prefix_match("hello");
assert_eq!(matched, "hello");
assert_eq!(tenant, "tenant2");
}
// ==================== Consistency Verification Tests ====================
#[test]
fn test_char_count_consistency_after_operations() {
let tree = Tree::new();
// Helper to verify consistency
let verify_consistency = |tree: &Tree| {
let maintained = get_maintained_counts(tree);
let computed = tree.get_used_size_per_tenant();
assert_eq!(
maintained, computed,
"Maintained counts should match computed counts"
);
};
// Insert phase
for i in 0..50 {
tree.insert(&format!("prefix{}", i), "tenant1");
tree.insert(&format!("other{}", i), "tenant2");
}
verify_consistency(&tree);
// Overlapping inserts
for i in 0..25 {
tree.insert(&format!("prefix{}", i), "tenant2");
}
verify_consistency(&tree);
// Eviction
tree.evict_tenant_by_size(100);
verify_consistency(&tree);
// Tenant removal
tree.remove_tenant("tenant1");
verify_consistency(&tree);
}
#[test]
fn test_tree_structure_integrity_after_stress() {
let tree = Arc::new(Tree::new());
let num_threads = 8;
let mut handles = vec![];
for thread_id in 0..num_threads {
let tree = Arc::clone(&tree);
let handle = thread::spawn(move || {
let mut rng = rand::rng();
let tenant = format!("tenant{}", thread_id);
for _ in 0..200 {
let op: u8 = rng.random_range(0..10);
let key = format!("key{}", rng.random_range(0..50));
match op {
0..=6 => {
// Insert (70%)
tree.insert(&key, &tenant);
}
7..=8 => {
// Match (20%)
let _ = tree.prefix_match(&key);
}
_ => {
// Match with counts (10%)
let _ = tree.prefix_match_with_counts(&key);
}
}
}
});
handles.push(handle);
}
for handle in handles {
handle.join().expect("Thread panicked during stress test");
}
// Verify tree is still functional
let sizes = tree.get_used_size_per_tenant();
for (tenant, size) in sizes.iter() {
assert!(*size > 0, "Tenant {} should have positive size", tenant);
}
// Verify char count consistency
let maintained = get_maintained_counts(&tree);
let computed = tree.get_used_size_per_tenant();
assert_eq!(
maintained, computed,
"Counts should be consistent after stress test"
);
}
// ==================== Boundary Condition Tests ====================
#[test]
fn test_very_long_strings() {
let tree = Tree::new();
// Create a very long string (10KB)
let long_string: String = (0..10000)
.map(|i| ((i % 26) as u8 + b'a') as char)
.collect();
tree.insert(&long_string, "tenant1");
let (matched, tenant) = tree.prefix_match(&long_string);
assert_eq!(matched.len(), long_string.len());
assert_eq!(tenant, "tenant1");
// Partial match of long string
let partial = &long_string[..5000];
let (matched, _) = tree.prefix_match(partial);
assert_eq!(matched, partial);
}
#[test]
fn test_many_tenants_same_path() {
let tree = Tree::new();
// 100 tenants all insert same string
for i in 0..100 {
tree.insert("shared_path", &format!("tenant{}", i));
}
// Match should return one of them
let (matched, _) = tree.prefix_match("shared_path");
assert_eq!(matched, "shared_path");
// Verify all tenants are tracked
let sizes = tree.get_used_size_per_tenant();
assert_eq!(sizes.len(), 100, "Should have 100 tenants");
}
#[test]
fn test_special_characters() {
let tree = Tree::new();
// Various special characters
let test_cases = vec![
("hello\nworld", "tenant1"), // newline
("hello\tworld", "tenant2"), // tab
("hello\0world", "tenant3"), // null byte
("hello\u{A0}world", "tenant4"), // non-breaking space
("path/to/file", "tenant5"), // slashes
("query?param=value", "tenant6"), // URL-like
];
for (text, tenant) in &test_cases {
tree.insert(text, tenant);
}
for (text, tenant) in &test_cases {
let (matched, matched_tenant) = tree.prefix_match(text);
assert_eq!(matched, *text, "Failed for: {:?}", text);
assert_eq!(matched_tenant, *tenant);
}
}
}