use super::*;
macro_rules! sparkv_tests {
($sparkv_type:ty) => {
#[test]
fn test_sparkv_config() {
let config: Config = Config::new();
assert_eq!(
config.max_items, 10_000,
"default max_items should be 10_000"
);
assert_eq!(
config.max_item_size, 500_000,
"default max_item_size should be 500_000"
);
assert_eq!(
config.max_ttl,
Duration::seconds(60 * 60),
"default max_ttl should be 1 hour"
);
}
#[test]
fn test_sparkv_new_with_config() {
let config: Config = Config::new();
let sparkv = <$sparkv_type>::with_config(config);
assert_eq!(
sparkv.config, config,
"sparkv should use the provided config"
);
}
#[test]
fn test_len_is_empty() {
let mut sparkv = <$sparkv_type>::new();
assert_eq!(sparkv.len(), 0, "new sparkv should have length 0");
assert!(sparkv.is_empty(), "new sparkv should be empty");
_ = sparkv.set("keyA", "value".to_string(), &[]);
assert_eq!(sparkv.len(), 1, "after insert, length should be 1");
assert!(
!sparkv.is_empty(),
"after insert, sparkv should not be empty"
);
}
#[test]
fn test_set_get() {
let mut sparkv = <$sparkv_type>::new();
_ = sparkv.set("keyA", "value".into(), &[]);
assert_eq!(
sparkv.get("keyA"),
Some(&String::from("value")),
"should get back the inserted value"
);
assert_eq!(sparkv.expiries.len(), 1, "should have one expiry entry");
_ = sparkv.set("keyA", "value2".into(), &[]);
assert_eq!(
sparkv.get("keyA"),
Some(&String::from("value2")),
"overwritten value should be returned"
);
assert_eq!(
sparkv.expiries.len(),
2,
"overwriting adds a new expiry entry"
);
assert!(
sparkv.get("non-existent").is_none(),
"non-existent key should return None"
);
}
#[test]
fn test_get_item() {
let mut sparkv = <$sparkv_type>::new();
let item = KvEntry::new(
"keyARaw",
"value99".to_string(),
Duration::seconds(1),
);
sparkv.data.insert(item.key.clone(), item);
let get_result = sparkv.get_item("keyARaw");
let unwrapped = get_result.unwrap();
assert!(
get_result.is_some(),
"get_item should return Some for existing key"
);
assert_eq!(unwrapped.key, "keyARaw", "item key should match");
assert_eq!(unwrapped.value, "value99".to_string(), "item value should match");
assert!(
sparkv.get_item("non-existent").is_none(),
"get_item should return None for missing key"
);
}
#[test]
fn test_get_item_return_none_if_expired() {
let mut sparkv = <$sparkv_type>::new();
let val = "value".to_string();
_ = sparkv.set_with_ttl("key", val, Duration::milliseconds(10), &[]);
assert_eq!(
sparkv.get("key"),
Some(&String::from("value")),
"key should exist before expiration"
);
std::thread::sleep(std::time::Duration::from_millis(20));
assert_eq!(sparkv.get("key"), None, "expired item should return None");
}
#[test]
fn test_set_should_fail_if_capacity_exceeded() {
let mut config: Config = Config::new();
config.max_items = 2;
let mut sparkv = <$sparkv_type>::with_config(config);
let mut set_result = sparkv.set("keyA", "value".to_string(), &[]);
set_result.expect("first insert should succeed within capacity");
assert_eq!(
sparkv.get("keyA"),
Some(&String::from("value")),
"first value should be stored"
);
set_result = sparkv.set("keyB", "value2".to_string(), &[]);
set_result.expect("second insert should succeed within capacity");
set_result = sparkv.set("keyC", "value3".to_string(), &[]);
assert_eq!(
set_result.unwrap_err(),
Error::CapacityExceeded,
"third insert should exceed capacity"
);
assert!(
sparkv.get("keyC").is_none(),
"keyC should not exist after capacity failure"
);
set_result = sparkv.set("keyB", "newValue1234".to_string(), &[]);
set_result.expect("overwriting existing key should succeed even at capacity");
assert_eq!(
sparkv.get("keyB"),
Some(&String::from("newValue1234")),
"overwritten value should be stored"
);
}
#[test]
fn memsize_item_capacity_exceeded() {
let value: String = "jay".into();
let mut config: Config = Config::new();
config.max_item_size = std::mem::size_of_val(&value) / 2;
let mut sparkv = <$sparkv_type>::with_config(config);
let error = sparkv.set("blue", value, &[]);
assert_eq!(
error,
Err(Error::ItemSizeExceeded),
"item exceeding max_item_size should error"
);
}
#[test]
fn custom_item_capacity_exceeded() {
let mut config: Config = Config::new();
config.max_item_size = 20;
let mut sparkv =
<$sparkv_type>::with_config_and_sizer(config, Some(|s: &String| s.len()));
assert_eq!(
Ok(()),
sparkv.set("short", "value".to_string(), &[]),
"short value should succeed"
);
assert_eq!(
Err(Error::ItemSizeExceeded),
sparkv.set(
"long",
"This is a value that exceeds 20 characters".to_string(),
&[]
),
"long value exceeding max_item_size should fail"
);
}
#[test]
fn test_set_with_ttl() {
let mut sparkv = <$sparkv_type>::new();
_ = sparkv.set("longest", "value".into(), &[]);
_ = sparkv.set_with_ttl("longer", "value".into(), Duration::seconds(2), &[]);
_ = sparkv.set_with_ttl("shorter", "value".into(), Duration::seconds(1), &[]);
assert_eq!(
sparkv.get("longer"),
Some(&String::from("value")),
"longer key should be storable"
);
assert_eq!(
sparkv.get("shorter"),
Some(&String::from("value")),
"shorter key should be storable"
);
assert!(
sparkv.get_item("longer").unwrap().expired_at
> sparkv.get_item("shorter").unwrap().expired_at,
"longer TTL item should have later expiry than shorter TTL item"
);
assert!(
sparkv.get_item("longest").unwrap().expired_at
> sparkv.get_item("longer").unwrap().expired_at,
"default-TTL item should have later expiry than shorter TTL item"
);
}
#[test]
fn test_ensure_max_ttl() {
let mut config: Config = Config::new();
config.max_ttl = Duration::seconds(3600);
config.default_ttl = Duration::seconds(5000);
let mut sparkv = <$sparkv_type>::with_config(config);
let set_result_long_def =
sparkv.set("default is longer than max", "should fail".to_string(), &[]);
assert_eq!(
set_result_long_def.unwrap_err(),
Error::TTLTooLong,
"default TTL longer than max_ttl should fail"
);
let set_result_ok =
sparkv.set_with_ttl("shorter", "ok".into(), Duration::seconds(3599), &[]);
set_result_ok.expect("TTL within max should succeed");
let set_result_ok_2 =
sparkv.set_with_ttl("exact", "ok".into(), Duration::seconds(3600), &[]);
set_result_ok_2.expect("TTL equal to max should succeed");
let set_result_not_ok =
sparkv.set_with_ttl("not", "not ok".into(), Duration::seconds(33601), &[]);
assert_eq!(
set_result_not_ok.unwrap_err(),
Error::TTLTooLong,
"TTL exceeding max should fail"
);
}
#[test]
fn test_delete() {
let mut sparkv = <$sparkv_type>::new();
_ = sparkv.set("keyA", "value".to_string(), &[]);
assert_eq!(
sparkv.get("keyA"),
Some(&String::from("value")),
"keyA should exist before delete"
);
assert_eq!(sparkv.expiries.len(), 1, "should have one expiry entry");
let deleted_value = sparkv.pop("keyA");
assert_eq!(
deleted_value,
Some(String::from("value")),
"pop should return the deleted value"
);
assert!(
sparkv.get("keyA").is_none(),
"keyA should be gone after pop"
);
assert_eq!(sparkv.expiries.len(), 1, "pop does not remove expiry entry");
}
#[test]
fn test_clear_expired() {
let mut config: Config = Config::new();
config.auto_clear_expired = false;
let mut sparkv = <$sparkv_type>::with_config(config);
_ = sparkv.set_with_ttl("not-yet-expired", "v".into(), Duration::seconds(90), &[]);
_ = sparkv.set_with_ttl("expiring", "value".into(), Duration::milliseconds(10), &[]);
_ = sparkv.set_with_ttl("not-expired", "value".into(), Duration::seconds(60), &[]);
std::thread::sleep(std::time::Duration::from_millis(20));
assert_eq!(
sparkv.len(),
3,
"should have 3 items before clearing expired"
);
let cleared_count = sparkv.clear_expired();
assert_eq!(
cleared_count, 1,
"should have cleared exactly 1 expired item"
);
assert_eq!(
sparkv.len(),
2,
"should have 2 items after clearing expired"
);
assert_eq!(
sparkv.clear_expired(),
0,
"no more expired items should remain"
);
}
#[test]
fn test_clear_expired_with_overwritten_key() {
let mut config: Config = Config::new();
config.auto_clear_expired = false;
let mut sparkv = <$sparkv_type>::with_config(config);
_ = sparkv.set_with_ttl(
"no-longer",
"value".into(),
Duration::milliseconds(10),
&[],
);
_ = sparkv.set_with_ttl("no-longer", "v".into(), Duration::seconds(90), &[]);
_ = sparkv.set_with_ttl("not-expired", "value".into(), Duration::seconds(60), &[]);
std::thread::sleep(std::time::Duration::from_millis(20));
assert_eq!(
sparkv.expiries.len(),
3,
"overwriting key does not remove old expiry"
);
assert_eq!(sparkv.len(), 2, "should have 2 unique keys");
let cleared_count = sparkv.clear_expired();
assert_eq!(
cleared_count, 0,
"overwritten entry should no longer be expiring"
);
assert_eq!(
sparkv.expiries.len(),
2,
"stale expiry entry should be removed"
);
assert_eq!(sparkv.len(), 2, "items should not be deleted");
}
#[test]
fn test_capacity_disabled() {
let mut config: Config = Config::new();
config.max_items = 0; let mut sparkv = <$sparkv_type>::with_config(config);
for i in 0..1000 {
sparkv
.set(&format!("key{i}"), format!("value{i}"), &[])
.unwrap();
}
assert_eq!(
sparkv.len(),
1000,
"should be able to add 1000 items when capacity is disabled"
);
}
#[test]
fn test_item_size_disabled() {
let mut config: Config = Config::new();
config.max_item_size = 0; let mut sparkv = <$sparkv_type>::with_config(config);
sparkv.set("huge", "a".repeat(1_000_000), &[]).unwrap();
assert_eq!(
sparkv.len(),
1,
"should be able to add oversized item when item size limit is disabled"
);
}
#[test]
fn test_clear_expired_with_auto_clear_expired_enabled() {
let mut config: Config = Config::new();
config.auto_clear_expired = true; let mut sparkv = <$sparkv_type>::with_config(config);
_ = sparkv.set_with_ttl(
"no-longer",
"value".into(),
Duration::milliseconds(10),
&[],
);
_ = sparkv.set_with_ttl("no-longer", "v".into(), Duration::seconds(90), &[]);
std::thread::sleep(std::time::Duration::from_millis(20));
_ = sparkv.set_with_ttl("not-expired", "value".into(), Duration::seconds(60), &[]);
assert_eq!(
sparkv.expiries.len(),
2,
"auto clear should remove expired expiry entry"
);
assert_eq!(sparkv.len(), 2, "should have 2 items after auto clear");
_ = sparkv.set_with_ttl("new-", "value".into(), Duration::seconds(60), &[]);
assert_eq!(
sparkv.expiries.len(),
3,
"new set should add a new expiry entry"
);
assert_eq!(sparkv.len(), 3, "new set should add a new item");
}
#[test]
fn iterator() {
let mut sparkv = <$sparkv_type>::new();
sparkv.set("this", "town".into(), &[]).unwrap();
sparkv.set("woo", "oooo".into(), &[]).unwrap();
sparkv.set("is", "coming".into(), &[]).unwrap();
sparkv.set("like", "a".into(), &[]).unwrap();
sparkv.set("ghost", "town".into(), &[]).unwrap();
sparkv.set("oh", "yeah".into(), &[]).unwrap();
let iter = sparkv.iter();
assert!(!sparkv.is_empty(), "sparkv should be not empty");
assert_eq!(
sparkv.get("ghost").unwrap(),
"town",
"ghost value should be correct"
);
let mut pairs: Vec<_> = iter.collect();
pairs.sort_by(|(k1, _), (k2, _)| k1.cmp(k2));
let (keys, values): (Vec<_>, Vec<_>) = pairs.into_iter().unzip();
assert_eq!(
keys,
vec!["ghost", "is", "like", "oh", "this", "woo"],
"iterator should return sorted keys"
);
assert_eq!(
values,
vec!["town", "coming", "a", "yeah", "town", "oooo"],
"iterator should return values in key order"
);
}
#[test]
fn drain() {
let mut sparkv = <$sparkv_type>::new();
sparkv.set("this", "town".into(), &[]).unwrap();
sparkv.set("woo", "oooo".into(), &[]).unwrap();
sparkv.set("is", "coming".into(), &[]).unwrap();
sparkv.set("like", "a".into(), &[]).unwrap();
sparkv.set("ghost", "town".into(), &[]).unwrap();
sparkv.set("oh", "yeah".into(), &[]).unwrap();
let iter = sparkv.drain();
assert!(sparkv.is_empty(), "sparkv should be empty");
let mut pairs: Vec<_> = iter.collect();
pairs.sort_by(|(k1, _), (k2, _)| k1.cmp(k2));
let (keys, values): (Vec<_>, Vec<_>) = pairs.into_iter().unzip();
assert_eq!(
keys,
vec!["ghost", "is", "like", "oh", "this", "woo"],
"drain should return sorted keys"
);
assert_eq!(
values,
vec!["town", "coming", "a", "yeah", "town", "oooo"],
"drain should return values in key order"
);
}
#[test]
fn test_get_oldest_key_by_expiration() {
let mut sparkv = <$sparkv_type>::new();
sparkv
.set_with_ttl("short", "short_value".into(), Duration::seconds(1), &[])
.unwrap();
sparkv
.set_with_ttl("medium", "medium_value".into(), Duration::seconds(5), &[])
.unwrap();
sparkv
.set_with_ttl("long", "long_value".into(), Duration::seconds(10), &[])
.unwrap();
let oldest = sparkv.get_oldest_key_by_expiration();
assert!(oldest.is_some(), "should find oldest key by expiration");
assert_eq!(
oldest.unwrap().key,
"short",
"shortest TTL key should be the oldest"
);
}
#[test]
fn test_get_keys() {
let mut sparkv = <$sparkv_type>::new();
assert_eq!(
sparkv.get_keys(),
Vec::<String>::new(),
"empty sparkv should return empty keys"
);
sparkv.set("key1", "value1".into(), &[]).unwrap();
sparkv.set("key2", "value2".into(), &[]).unwrap();
sparkv.set("key3", "value3".into(), &[]).unwrap();
let mut keys = sparkv.get_keys();
keys.sort();
assert_eq!(
keys,
vec!["key1", "key2", "key3"],
"should return all inserted keys"
);
}
#[test]
fn test_contains_key() {
let mut sparkv = <$sparkv_type>::new();
assert!(
!sparkv.contains_key("key1"),
"empty sparkv should not contain key1"
);
sparkv.set("key1", "value1".into(), &[]).unwrap();
assert!(
sparkv.contains_key("key1"),
"sparkv should contain key1 after insert"
);
assert!(
!sparkv.contains_key("key2"),
"sparkv should not contain uninserted key2"
);
sparkv.set("key2", "value2".into(), &[]).unwrap();
assert!(
sparkv.contains_key("key2"),
"sparkv should contain key2 after insert"
);
sparkv.pop("key1");
assert!(
!sparkv.contains_key("key1"),
"sparkv should not contain key1 after pop"
);
assert!(
sparkv.contains_key("key2"),
"sparkv should still contain key2"
);
}
#[test]
fn test_add_additional_index() {
let mut sparkv = <$sparkv_type>::new();
sparkv
.set("key1", "value1".into(), &["index1".to_string()])
.unwrap();
sparkv.add_additional_index("key1", "index2");
let index1_results: Vec<_> = sparkv.get_by_index_key("index1").collect();
let index2_results: Vec<_> = sparkv.get_by_index_key("index2").collect();
assert_eq!(
index1_results,
vec![&"value1".to_string()],
"index1 should return value1"
);
assert_eq!(
index2_results,
vec![&"value1".to_string()],
"index2 should also return value1"
);
sparkv.add_additional_index("non_existent", "index3");
let index3_results: Vec<_> = sparkv.get_by_index_key("index3").collect();
assert_eq!(
index3_results,
Vec::<&String>::new(),
"non-existent key should add no index entries"
);
}
#[test]
fn test_get_by_index_key() {
let mut sparkv = <$sparkv_type>::new();
sparkv
.set("key1", "value1".into(), &["common_index".to_string()])
.unwrap();
sparkv
.set("key2", "value2".into(), &["common_index".to_string()])
.unwrap();
sparkv
.set("key3", "value3".into(), &["common_index".to_string()])
.unwrap();
sparkv
.set("key4", "value4".into(), &["other_index".to_string()])
.unwrap();
let common_results: Vec<_> = sparkv.get_by_index_key("common_index").collect();
assert_eq!(
common_results.len(),
3,
"common index should return 3 items"
);
assert!(
common_results.contains(&&"value1".to_string()),
"common index should contain value1"
);
assert!(
common_results.contains(&&"value2".to_string()),
"common index should contain value2"
);
assert!(
common_results.contains(&&"value3".to_string()),
"common index should contain value3"
);
let other_results: Vec<_> = sparkv.get_by_index_key("other_index").collect();
assert_eq!(
other_results,
vec![&"value4".to_string()],
"other index should return value4"
);
let non_existent_results: Vec<_> = sparkv.get_by_index_key("non_existent").collect();
assert_eq!(
non_existent_results,
Vec::<&String>::new(),
"non-existent index should return empty"
);
}
#[test]
fn test_remove_by_index() {
let mut sparkv = <$sparkv_type>::new();
sparkv
.set("key1", "value1".into(), &["index_to_remove".to_string()])
.unwrap();
sparkv
.set("key2", "value2".into(), &["index_to_remove".to_string()])
.unwrap();
sparkv
.set("key3", "value3".into(), &["other_index".to_string()])
.unwrap();
sparkv
.set("key4", "value4".into(), &["index_to_remove".to_string()])
.unwrap();
assert_eq!(sparkv.len(), 4, "should have 4 items before removal");
let removed_count = sparkv.remove_by_index("index_to_remove");
assert_eq!(removed_count, 3, "should remove 3 items by index");
assert_eq!(sparkv.len(), 1, "should have 1 item after removal");
assert!(
sparkv.contains_key("key3"),
"key3 with different index should remain"
);
assert_eq!(
sparkv.get("key3"),
Some(&"value3".to_string()),
"key3 value should be unchanged"
);
let removed_count = sparkv.remove_by_index("non_existent");
assert_eq!(removed_count, 0, "non-existent index should remove nothing");
assert_eq!(sparkv.len(), 1, "length should remain unchanged");
}
#[test]
fn test_clear() {
let mut sparkv = <$sparkv_type>::new();
sparkv
.set("key1", "value1".into(), &["index1".to_string()])
.unwrap();
sparkv
.set("key2", "value2".into(), &["index2".to_string()])
.unwrap();
sparkv
.set("key3", "value3".into(), &["index3".to_string()])
.unwrap();
assert_eq!(sparkv.len(), 3, "should have 3 items before clear");
assert!(
!sparkv.is_empty(),
"sparkv should not be empty before clear"
);
sparkv.clear();
assert_eq!(sparkv.len(), 0, "sparkv should be empty after clear");
assert!(sparkv.is_empty(), "is_empty should return true after clear");
let index_results: Vec<_> = sparkv.get_by_index_key("index1").collect();
assert_eq!(
index_results,
Vec::<&String>::new(),
"indexes should also be cleared"
);
}
#[test]
fn test_default_implementation() {
let sparkv = <$sparkv_type>::default();
assert_eq!(sparkv.len(), 0, "default Sparkv should be empty");
assert!(sparkv.is_empty(), "default Sparkv should be empty");
assert_eq!(
sparkv.config.max_items, 10_000,
"default config should have 10_000 max_items"
);
assert_eq!(
sparkv.config.max_item_size, 500_000,
"default config should have 500_000 max_item_size"
);
}
#[test]
fn test_set_with_index_keys() {
let mut sparkv = <$sparkv_type>::new();
let index_keys = vec![
"index1".to_string(),
"index2".to_string(),
"index3".to_string(),
];
sparkv.set("key1", "value1".into(), &index_keys).unwrap();
let index1_results: Vec<_> = sparkv.get_by_index_key("index1").collect();
let index2_results: Vec<_> = sparkv.get_by_index_key("index2").collect();
let index3_results: Vec<_> = sparkv.get_by_index_key("index3").collect();
assert_eq!(
index1_results,
vec![&"value1".to_string()],
"index1 should return value1"
);
assert_eq!(
index2_results,
vec![&"value1".to_string()],
"index2 should return value1"
);
assert_eq!(
index3_results,
vec![&"value1".to_string()],
"index3 should return value1"
);
}
#[test]
fn test_earliest_expiration_eviction() {
let mut config: Config = Config::new();
config.max_items = 2;
config.earliest_expiration_eviction = true;
let mut sparkv = <$sparkv_type>::with_config(config);
sparkv
.set_with_ttl("short", "short_value".into(), Duration::seconds(1), &[])
.unwrap();
sparkv
.set_with_ttl("long", "long_value".into(), Duration::seconds(10), &[])
.unwrap();
assert_eq!(sparkv.len(), 2, "should have 2 items before eviction");
sparkv
.set_with_ttl("new", "new_value".into(), Duration::seconds(5), &[])
.unwrap();
assert_eq!(sparkv.len(), 2, "should still have 2 items after eviction");
assert!(
!sparkv.contains_key("short"),
"shortest TTL item should be evicted"
);
assert!(sparkv.contains_key("long"), "long TTL item should remain");
assert!(sparkv.contains_key("new"), "new item should exist");
}
#[test]
fn test_auto_clear_with_expired_entry() {
let mut config: Config = Config::new();
config.auto_clear_expired = true;
let mut sparkv = <$sparkv_type>::with_config(config);
sparkv
.set_with_ttl("expired", "v".into(), Duration::milliseconds(10), &[])
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(20));
sparkv.set("other", "x".into(), &[]).unwrap();
assert!(
!sparkv.contains_key("expired"),
"expired entry should be auto-cleared"
);
assert!(sparkv.contains_key("other"), "newly set entry should exist");
}
};
}
mod btree_tests {
use super::*;
sparkv_tests!(BTreeSparKV<String>);
}
mod hashmap_tests {
use super::*;
sparkv_tests!(HashMapSparKV<String>);
}