From 25c7a49c4f41c5750d24950511909db57226dda6 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Wed, 15 Jan 2025 12:55:35 -0500 Subject: [PATCH] feat(jans-cedarling): Make SparKV use generics, and update MemoryLogger to use those. (#10593) * feat(jans-cedarling): SparKV Generics Signed-off-by: John Anderson * feat(jans-cedarling): In MemoryLogger, use serde_json::Value as SparKV storage type. Signed-off-by: John Anderson * feat(jans-cedarling): SparKV key-value iterator Signed-off-by: John Anderson * feat(jans-cedarling): use SparKV key-value iterator in MemoryLogger for more efficient pop_logs Signed-off-by: John Anderson * feat(jans-cedarling): SparKV implement drain similar to std::collections Signed-off-by: John Anderson * feat(jans-cedarling): use SparKV drain in MemoryLogger for more efficient pop_logs Signed-off-by: John Anderson * chore(jans-cedarling): SparKV simplify clear_expired Signed-off-by: John Anderson * chore(jans-cedarling): simplify some Loggable code Signed-off-by: John Anderson * feat(jans-cedarling): SparKV add size function for non-byte types Signed-off-by: John Anderson * chore(jans-cedarling): SparKV test for serde_json::Value storage Signed-off-by: John Anderson * chore(jans-cedarling): SparKV small fixes Signed-off-by: John Anderson * chore(jans-cedarling): SparKV move tests to own file Signed-off-by: John Anderson * docs(jans-cedarling): SparKV documentation for generics Signed-off-by: John Anderson * feat(jans-cedarling): some notes on custom value length function Signed-off-by: John Anderson * chore(jans-cedarling): fallout from merge with wasm branch Signed-off-by: John Anderson * chore(jans-cedarling): in MemoryLogger remove unnecessary serde_json::Value conversions Signed-off-by: John Anderson * chore(jans-cedarling): SparKV rectify expiry tests, and use nicer Duration constructors Signed-off-by: John Anderson * chore(jans-cedarling): SparKV clean up some test cases Signed-off-by: John Anderson * chore(jans-cedarling): SparKV add copyright to tests files Signed-off-by: John Anderson * chore(jans-cedarling): SparKV cargo fmt -p sparkv Signed-off-by: John Anderson * chore(jans-cedarling): SparKV rectify expiry test Signed-off-by: John Anderson * feat(jans-cedarling): cargo fmt on log/memory_logger.rs Signed-off-by: John Anderson * feat(jans-cedarling): use fallback logger on failure in MemoryLogger Signed-off-by: John Anderson --------- Signed-off-by: John Anderson --- jans-cedarling/cedarling/src/log/interface.rs | 10 +- jans-cedarling/cedarling/src/log/log_entry.rs | 2 +- .../cedarling/src/log/memory_logger.rs | 117 ++++- jans-cedarling/sparkv/Cargo.toml | 3 + jans-cedarling/sparkv/README.md | 2 +- jans-cedarling/sparkv/src/config.rs | 14 +- jans-cedarling/sparkv/src/expentry.rs | 24 +- jans-cedarling/sparkv/src/kventry.rs | 22 +- jans-cedarling/sparkv/src/lib.rs | 428 +++++------------- jans-cedarling/sparkv/src/test_json_value.rs | 177 ++++++++ jans-cedarling/sparkv/src/tests.rs | 266 +++++++++++ 11 files changed, 686 insertions(+), 379 deletions(-) create mode 100644 jans-cedarling/sparkv/src/test_json_value.rs create mode 100644 jans-cedarling/sparkv/src/tests.rs diff --git a/jans-cedarling/cedarling/src/log/interface.rs b/jans-cedarling/cedarling/src/log/interface.rs index eb2a07bcb0e..be322c50285 100644 --- a/jans-cedarling/cedarling/src/log/interface.rs +++ b/jans-cedarling/cedarling/src/log/interface.rs @@ -25,6 +25,7 @@ pub(crate) trait LogWriter { pub(crate) trait Loggable: serde::Serialize { /// get unique request ID fn get_request_id(&self) -> Uuid; + /// get log level for entity /// not all log entities have log level, only when `log_kind` == `System` fn get_log_level(&self) -> Option; @@ -34,13 +35,8 @@ pub(crate) trait Loggable: serde::Serialize { // is used to avoid boilerplate code fn can_log(&self, logger_level: LogLevel) -> bool { if let Some(entry_log_level) = self.get_log_level() { - if entry_log_level < logger_level { - // entry log level lower than logger level - false - } else { - // entry log higher or equal than logger level - true - } + // higher level is more important, ie closer to fatal + logger_level <= entry_log_level } else { // if `.get_log_level` return None // it means that `log_kind` != `System` and we should log it diff --git a/jans-cedarling/cedarling/src/log/log_entry.rs b/jans-cedarling/cedarling/src/log/log_entry.rs index b3d4d009a2e..46add8d97fc 100644 --- a/jans-cedarling/cedarling/src/log/log_entry.rs +++ b/jans-cedarling/cedarling/src/log/log_entry.rs @@ -356,7 +356,7 @@ impl Loggable for &DecisionLogEntry<'_> { // TODO: maybe using wasm we can use `js_sys::Date::now()` // Static variable initialize only once at start of program and available during all program live cycle. // Import inside function guarantee that it is used only inside function. -fn gen_uuid7() -> Uuid { +pub fn gen_uuid7() -> Uuid { use std::sync::{LazyLock, Mutex}; use uuid7::V7Generator; diff --git a/jans-cedarling/cedarling/src/log/memory_logger.rs b/jans-cedarling/cedarling/src/log/memory_logger.rs index e744a559ec6..154b1fe187d 100644 --- a/jans-cedarling/cedarling/src/log/memory_logger.rs +++ b/jans-cedarling/cedarling/src/log/memory_logger.rs @@ -13,12 +13,10 @@ use super::interface::{LogStorage, LogWriter, Loggable}; use crate::bootstrap_config::log_config::MemoryLogConfig; const STORAGE_MUTEX_EXPECT_MESSAGE: &str = "MemoryLogger storage mutex should unlock"; -const STORAGE_JSON_PARSE_EXPECT_MESSAGE: &str = - "In MemoryLogger storage value should be valid LogEntry json string"; /// A logger that store logs in-memory. pub(crate) struct MemoryLogger { - storage: Mutex, + storage: Mutex>, log_level: LogLevel, } @@ -40,6 +38,44 @@ impl MemoryLogger { } } +/// In case of failure in MemoryLogger, log to stderr where supported. +/// On WASM, stderr is not supported, so log to whatever the wasm logger uses. +mod fallback { + use crate::LogLevel; + + /// conform to Loggable requirement imposed by LogStrategy + #[derive(serde::Serialize)] + struct StrWrap<'a>(&'a str); + + impl crate::log::interface::Loggable for StrWrap<'_> { + fn get_request_id(&self) -> uuid7::Uuid { + crate::log::log_entry::gen_uuid7() + } + + fn get_log_level(&self) -> Option { + // These must always be logged. + Some(LogLevel::TRACE) + } + } + + /// Fetch the correct logger. That takes some work, and it's done on every + /// call. But this is a fallback logger, so it is not intended to be used + /// often, and in this case correctness and non-fallibility are far more + /// important than performance. + pub fn log(msg: &str) { + let log_config = crate::bootstrap_config::LogConfig{ + log_type: crate::bootstrap_config::log_config::LogTypeConfig::StdOut, + // level is so that all messages passed here are logged. + log_level: LogLevel::TRACE, + }; + // This should always be a LogStrategy::StdOut(StdOutLogger) + let log_strategy = crate::log::LogStrategy::new(&log_config); + use crate::log::interface::LogWriter; + // a string is always serializable + log_strategy.log_any(StrWrap(msg)) + } +} + // Implementation of LogWriter impl LogWriter for MemoryLogger { fn log_any(&self, entry: T) { @@ -48,17 +84,22 @@ impl LogWriter for MemoryLogger { return; } - let json_string = serde_json::json!(entry).to_string(); + let json = match serde_json::to_value(&entry) { + Ok(json) => json, + Err(err) => { + fallback::log(&format!("could not serialize LogEntry to serde_json::Value: {err:?}")); + return; + }, + }; - let result = self + let set_result = self .storage .lock() .expect(STORAGE_MUTEX_EXPECT_MESSAGE) - .set(entry.get_request_id().to_string().as_str(), &json_string); + .set(&entry.get_request_id().to_string(), json); - if let Err(err) = result { - // log error to stderr - eprintln!("could not store LogEntry to memory: {err:?}"); + if let Err(err) = set_result { + fallback::log(&format!("could not store LogEntry to memory: {err:?}")); }; } } @@ -66,25 +107,20 @@ impl LogWriter for MemoryLogger { // Implementation of LogStorage impl LogStorage for MemoryLogger { fn pop_logs(&self) -> Vec { - // TODO: implement more efficient implementation - - let mut storage_guard = self.storage.lock().expect(STORAGE_MUTEX_EXPECT_MESSAGE); - - let keys = storage_guard.get_keys(); - - keys.iter() - .filter_map(|key| storage_guard.pop(key)) - // we call unwrap, because we know that the value is valid json - .map(|str_json| serde_json::from_str::(str_json.as_str()) - .expect(STORAGE_JSON_PARSE_EXPECT_MESSAGE)) + self.storage + .lock() + .expect(STORAGE_MUTEX_EXPECT_MESSAGE) + .drain() + .map(|(_k, value)| value) .collect() } fn get_log_by_id(&self, id: &str) -> Option { - self.storage.lock().expect(STORAGE_MUTEX_EXPECT_MESSAGE) + self.storage + .lock() + .expect(STORAGE_MUTEX_EXPECT_MESSAGE) .get(id) - // we call unwrap, because we know that the value is valid json - .map(|str_json| serde_json::from_str::(str_json.as_str()).expect(STORAGE_JSON_PARSE_EXPECT_MESSAGE)) + .cloned() } fn get_log_ids(&self) -> Vec { @@ -211,4 +247,39 @@ mod tests { "Logs were not fully popped" ); } + + #[test] + fn fallback_logger() { + struct FailSerialize; + + impl serde::Serialize for FailSerialize { + fn serialize(&self, _serializer: S) -> Result + where S: serde::Serializer { + Err(serde::ser::Error::custom("this always fails")) + } + } + + impl crate::log::interface::Loggable for FailSerialize { + fn get_request_id(&self) -> uuid7::Uuid { + crate::log::log_entry::gen_uuid7() + } + + fn get_log_level(&self) -> Option { + // These must always be logged. + Some(LogLevel::TRACE) + } + } + + let logger = create_memory_logger(); + logger.log_any(FailSerialize); + + // There isn't a good way, in unit tests, to verify the output was + // actually written to stderr/json console. + // + // To eyeball-verify it: + // cargo test -- --nocapture fall + // and look in the output for + // "could not serialize LogEntry to serde_json::Value: Error(\"this always fails\", line: 0, column: 0)" + assert!(logger.pop_logs().is_empty(), "logger should be empty"); + } } diff --git a/jans-cedarling/sparkv/Cargo.toml b/jans-cedarling/sparkv/Cargo.toml index ec0f51db975..4347be0c19d 100644 --- a/jans-cedarling/sparkv/Cargo.toml +++ b/jans-cedarling/sparkv/Cargo.toml @@ -14,3 +14,6 @@ homepage = "https://crates.io/crates/sparkv" [dependencies] thiserror = { workspace = true } chrono = { workspace = true } + +[dev-dependencies] +serde_json = "*" diff --git a/jans-cedarling/sparkv/README.md b/jans-cedarling/sparkv/README.md index b7278655800..ba6c66a180a 100644 --- a/jans-cedarling/sparkv/README.md +++ b/jans-cedarling/sparkv/README.md @@ -26,7 +26,7 @@ sparkv.set("your-key", "your-value"); // write let value = sparkv.get("your-key").unwrap(); // read // Write with unique TTL -sparkv.set_with_ttl("diff-ttl", "your-value", chrono::Duration::new(60, 0)); +sparkv.set_with_ttl("diff-ttl", "your-value", chrono::Duration::seconds(60)); ``` See `config.rs` for more configuration options. diff --git a/jans-cedarling/sparkv/src/config.rs b/jans-cedarling/sparkv/src/config.rs index d1f0c966c5c..0e3899a615c 100644 --- a/jans-cedarling/sparkv/src/config.rs +++ b/jans-cedarling/sparkv/src/config.rs @@ -21,8 +21,8 @@ impl Config { Config { max_items: 10_000, max_item_size: 500_000, - max_ttl: Duration::new(60 * 60, 0).expect("a valid duration"), - default_ttl: Duration::new(5 * 60, 0).expect("a valid duration"), // 5 minutes + max_ttl: Duration::seconds(60 * 60), + default_ttl: Duration::seconds(5 * 60), // 5 minutes auto_clear_expired: true, } } @@ -43,14 +43,8 @@ mod tests { let config: Config = Config::new(); assert_eq!(config.max_items, 10_000); assert_eq!(config.max_item_size, 500_000); - assert_eq!( - config.max_ttl, - Duration::new(60 * 60, 0).expect("a valid duration") - ); - assert_eq!( - config.default_ttl, - Duration::new(5 * 60, 0).expect("a valid duration") - ); + assert_eq!(config.max_ttl, Duration::seconds(60 * 60)); + assert_eq!(config.default_ttl, Duration::seconds(5 * 60)); assert!(config.auto_clear_expired); } } diff --git a/jans-cedarling/sparkv/src/expentry.rs b/jans-cedarling/sparkv/src/expentry.rs index a702962a93e..a4a1cd2e008 100644 --- a/jans-cedarling/sparkv/src/expentry.rs +++ b/jans-cedarling/sparkv/src/expentry.rs @@ -16,15 +16,15 @@ pub struct ExpEntry { } impl ExpEntry { - pub fn new(key: &str, expiration: Duration) -> Self { + pub fn new>(key: S, expiration: Duration) -> Self { let expired_at: DateTime = Utc::now() + expiration; Self { - key: String::from(key), + key: key.as_ref().into(), expired_at, } } - pub fn from_kv_entry(kv_entry: &KvEntry) -> Self { + pub fn from_kv_entry(kv_entry: &KvEntry) -> Self { Self { key: kv_entry.key.clone(), expired_at: kv_entry.expired_at, @@ -59,19 +59,15 @@ mod tests { #[test] fn test_new() { - let item = ExpEntry::new("key", Duration::new(10, 0).expect("a valid duration")); + let item = ExpEntry::new("key", Duration::seconds(10)); assert_eq!(item.key, "key"); - assert!(item.expired_at > Utc::now() + Duration::new(9, 0).expect("a valid duration")); - assert!(item.expired_at <= Utc::now() + Duration::new(10, 0).expect("a valid duration")); + assert!(item.expired_at > Utc::now() + Duration::seconds(9)); + assert!(item.expired_at <= Utc::now() + Duration::seconds(10)); } #[test] fn test_from_kventry() { - let kv_entry = KvEntry::new( - "keyFromKV", - "value from KV", - Duration::new(10, 0).expect("a valid duration"), - ); + let kv_entry = KvEntry::new("keyFromKV", "value from KV", Duration::seconds(10)); let exp_item = ExpEntry::from_kv_entry(&kv_entry); assert_eq!(exp_item.key, "keyFromKV"); assert_eq!(exp_item.expired_at, kv_entry.expired_at); @@ -79,15 +75,15 @@ mod tests { #[test] fn test_cmp() { - let item_small = ExpEntry::new("k1", Duration::new(10, 0).expect("a valid duration")); - let item_big = ExpEntry::new("k2", Duration::new(8000, 0).expect("a valid duration")); + let item_small = ExpEntry::new("k1", Duration::seconds(10)); + let item_big = ExpEntry::new("k2", Duration::seconds(8000)); assert!(item_small > item_big); // reverse order assert!(item_big < item_small); // reverse order } #[test] fn test_is_expired() { - let item = ExpEntry::new("k1", Duration::new(0, 100).expect("a valid duration")); + let item = ExpEntry::new("k1", Duration::seconds(0)); std::thread::sleep(std::time::Duration::from_nanos(200)); assert!(item.is_expired()); } diff --git a/jans-cedarling/sparkv/src/kventry.rs b/jans-cedarling/sparkv/src/kventry.rs index 8fd8efbe6aa..c8e38c62ee5 100644 --- a/jans-cedarling/sparkv/src/kventry.rs +++ b/jans-cedarling/sparkv/src/kventry.rs @@ -8,18 +8,18 @@ use chrono::Duration; use chrono::prelude::*; #[derive(Debug, Clone, PartialEq, Eq)] -pub struct KvEntry { +pub struct KvEntry { pub key: String, - pub value: String, + pub value: T, pub expired_at: DateTime, } -impl KvEntry { - pub fn new(key: &str, value: &str, expiration: Duration) -> Self { +impl KvEntry { + pub fn new>(key: S, value: T, expiration: Duration) -> Self { let expired_at: DateTime = Utc::now() + expiration; Self { - key: String::from(key), - value: String::from(value), + key: key.as_ref().into(), + value, expired_at, } } @@ -31,14 +31,10 @@ mod tests { #[test] fn test_new() { - let item = KvEntry::new( - "key", - "value", - Duration::new(10, 0).expect("a valid duration"), - ); + let item = KvEntry::::new("key", "value".into(), Duration::seconds(10)); assert_eq!(item.key, "key"); assert_eq!(item.value, "value"); - assert!(item.expired_at > Utc::now() + Duration::new(9, 0).expect("a valid duration")); - assert!(item.expired_at <= Utc::now() + Duration::new(10, 0).expect("a valid duration")); + assert!(item.expired_at > Utc::now() + Duration::seconds(9)); + assert!(item.expired_at <= Utc::now() + Duration::seconds(10)); } } diff --git a/jans-cedarling/sparkv/src/lib.rs b/jans-cedarling/sparkv/src/lib.rs index b5bdd8ca9e9..c44c5750f2e 100644 --- a/jans-cedarling/sparkv/src/lib.rs +++ b/jans-cedarling/sparkv/src/lib.rs @@ -18,13 +18,59 @@ pub use kventry::KvEntry; use chrono::Duration; use chrono::prelude::*; -pub struct SparKV { +pub struct SparKV { pub config: Config, - data: std::collections::BTreeMap, + data: std::collections::BTreeMap>, expiries: std::collections::BinaryHeap, + /// An optional function that calculates the memory size of a value. + /// + /// Used by `ensure_item_size`. + /// + /// If this function is not provided, the container will enforce + /// `Config.max_item_size` on the basis of `std::mem::size_of_val` which + /// probably won't be what you expect. + size_calculator: Option usize>, } -impl SparKV { +/// See the SparKV::iter function +pub struct Iter<'a, T: 'a> { + btree_value_iter: std::collections::btree_map::Values<'a, String, KvEntry>, +} + +impl<'a, T> Iterator for Iter<'a, T> { + type Item = (&'a String, &'a T); + + fn next(&mut self) -> Option { + self.btree_value_iter + .next() + .map(|kventry| (&kventry.key, &kventry.value)) + } + + fn size_hint(&self) -> (usize, Option) { + self.btree_value_iter.size_hint() + } +} + +/// See the SparKV::drain function +pub struct DrainIter { + value_iter: std::collections::btree_map::IntoValues>, +} + +impl Iterator for DrainIter { + type Item = (String, T); + + fn next(&mut self) -> Option { + self.value_iter + .next() + .map(|kventry| (kventry.key, kventry.value)) + } + + fn size_hint(&self) -> (usize, Option) { + self.value_iter.size_hint() + } +} + +impl SparKV { pub fn new() -> Self { let config = Config::new(); SparKV::with_config(config) @@ -35,53 +81,76 @@ impl SparKV { config, data: std::collections::BTreeMap::new(), expiries: std::collections::BinaryHeap::new(), + // This will underestimate the size of most things. + size_calculator: Some(|v| std::mem::size_of_val(v)), } } - pub fn set(&mut self, key: &str, value: &str) -> Result<(), Error> { + /// Provide optional size function. See SparKV.size_calculator comments. + pub fn with_config_and_sizer(config: Config, sizer: Option usize>) -> Self { + SparKV { + config, + data: std::collections::BTreeMap::new(), + expiries: std::collections::BinaryHeap::new(), + size_calculator: sizer, + } + } + + pub fn set(&mut self, key: &str, value: T) -> Result<(), Error> { self.set_with_ttl(key, value, self.config.default_ttl) } - pub fn set_with_ttl(&mut self, key: &str, value: &str, ttl: Duration) -> Result<(), Error> { + pub fn set_with_ttl(&mut self, key: &str, value: T, ttl: Duration) -> Result<(), Error> { self.clear_expired_if_auto(); self.ensure_capacity_ignore_key(key)?; - self.ensure_item_size(value)?; + self.ensure_item_size(&value)?; self.ensure_max_ttl(ttl)?; - let item: KvEntry = KvEntry::new(key, value, ttl); + let item: KvEntry = KvEntry::new(key, value, ttl); let exp_item: ExpEntry = ExpEntry::from_kv_entry(&item); self.expiries.push(exp_item); - self.data.insert(item.key.clone(), item); + self.data.insert(key.into(), item); Ok(()) } - pub fn get(&self, key: &str) -> Option { - let item = self.get_item(key)?; - Some(item.value.clone()) + pub fn get(&self, key: &str) -> Option<&T> { + Some(&self.get_item(key)?.value) } // Only returns if it is not yet expired - pub fn get_item(&self, key: &str) -> Option<&KvEntry> { + pub fn get_item(&self, key: &str) -> Option<&KvEntry> { let item = self.data.get(key)?; - if item.expired_at > Utc::now() { - Some(item) - } else { - None - } + (item.expired_at > Utc::now()).then_some(item) } pub fn get_keys(&self) -> Vec { - self.data - .keys() - .map(|key| key.to_string())// it clone the string - .collect() + self.data.keys().cloned().collect() + } + + /// Return an iterator of (key,value) : (&String,&T). + pub fn iter(&self) -> Iter { + Iter { + btree_value_iter: self.data.values(), + } + } + + /// Return an iterator of (key,value) : (String,T) which empties the container. + /// All entries will be owned by the iterator, and yielded entries will not be checked against expiry. + /// All entries and expiries will be cleared. + pub fn drain(&mut self) -> DrainIter { + // assume that slightly-expired entries should be returned. + self.expiries.clear(); + let data_only = std::mem::take(&mut self.data); + DrainIter { + value_iter: data_only.into_values(), + } } - pub fn pop(&mut self, key: &str) -> Option { + pub fn pop(&mut self, key: &str) -> Option { self.clear_expired_if_auto(); let item = self.data.remove(key)?; - // Does not delete from BinaryHeap as it's expensive. + // Does not delete expiry entry from BinaryHeap as it's expensive. Some(item.value) } @@ -90,7 +159,7 @@ impl SparKV { } pub fn is_empty(&self) -> bool { - self.data.len() == 0 + self.data.is_empty() } pub fn contains_key(&self, key: &str) -> bool { @@ -99,29 +168,27 @@ impl SparKV { pub fn clear_expired(&mut self) -> usize { let mut cleared_count: usize = 0; - loop { - let peeked = self.expiries.peek().cloned(); - match peeked { - Some(exp_item) => { - if exp_item.is_expired() { - let kv_entry = self.data.get(&exp_item.key).unwrap(); - if kv_entry.key == exp_item.key - && kv_entry.expired_at == exp_item.expired_at - { - cleared_count += 1; - self.pop(&exp_item.key); - } - self.expiries.pop(); - } else { - break; - } - }, - None => break, + while let Some(exp_item) = self.expiries.peek().cloned() { + if exp_item.is_expired() { + let kv_entry = self.data.get(&exp_item.key).unwrap(); + if kv_entry.key == exp_item.key && kv_entry.expired_at == exp_item.expired_at { + cleared_count += 1; + self.pop(&exp_item.key); + } + self.expiries.pop(); + } else { + break; } } cleared_count } + /// Empty the container. That is, remove all key-values and expiries. + pub fn clear(&mut self) { + self.data.clear(); + self.expiries.clear(); + } + fn clear_expired_if_auto(&mut self) { if self.config.auto_clear_expired { self.clear_expired(); @@ -142,9 +209,11 @@ impl SparKV { self.ensure_capacity() } - fn ensure_item_size(&self, value: &str) -> Result<(), Error> { - if value.len() > self.config.max_item_size { - return Err(Error::ItemSizeExceeded); + fn ensure_item_size(&self, value: &T) -> Result<(), Error> { + if let Some(calc) = self.size_calculator { + if calc(value) > self.config.max_item_size { + return Err(Error::ItemSizeExceeded); + } } Ok(()) } @@ -157,275 +226,14 @@ impl SparKV { } } -impl Default for SparKV { +impl Default for SparKV { fn default() -> Self { Self::new() } } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_sparkv_config() { - let config: Config = Config::new(); - assert_eq!(config.max_items, 10_000); - assert_eq!(config.max_item_size, 500_000); - assert_eq!( - config.max_ttl, - Duration::new(60 * 60, 0).expect("a valid duration") - ); - } - - #[test] - fn test_sparkv_new_with_config() { - let config: Config = Config::new(); - let sparkv = SparKV::with_config(config); - assert_eq!(sparkv.config, config); - } - - #[test] - fn test_len_is_empty() { - let mut sparkv = SparKV::new(); - assert_eq!(sparkv.len(), 0); - assert!(sparkv.is_empty()); - - _ = sparkv.set("keyA", "value"); - assert_eq!(sparkv.len(), 1); - assert!(!sparkv.is_empty()); - } - - #[test] - fn test_set_get() { - let mut sparkv = SparKV::new(); - _ = sparkv.set("keyA", "value"); - assert_eq!(sparkv.get("keyA"), Some(String::from("value"))); - assert_eq!(sparkv.expiries.len(), 1); - - // Overwrite the value - _ = sparkv.set("keyA", "value2"); - assert_eq!(sparkv.get("keyA"), Some(String::from("value2"))); - assert_eq!(sparkv.expiries.len(), 2); - - assert!(sparkv.get("non-existent").is_none()); - } - - #[test] - fn test_get_item() { - let mut sparkv = SparKV::new(); - let item = KvEntry::new( - "keyARaw", - "value99", - Duration::new(1, 0).expect("a valid duration"), - ); - sparkv.data.insert(item.key.clone(), item); - let get_result = sparkv.get_item("keyARaw"); - let unwrapped = get_result.unwrap(); - - assert!(get_result.is_some()); - assert_eq!(unwrapped.key, "keyARaw"); - assert_eq!(unwrapped.value, "value99"); - - assert!(sparkv.get_item("non-existent").is_none()); - } - - #[test] - fn test_get_item_return_none_if_expired() { - let mut sparkv = SparKV::new(); - _ = sparkv.set_with_ttl( - "key", - "value", - Duration::new(0, 40000).expect("a valid duration"), - ); - assert_eq!(sparkv.get("key"), Some(String::from("value"))); - - std::thread::sleep(std::time::Duration::from_nanos(50000)); - assert_eq!(sparkv.get("key"), None); - } - - #[test] - fn test_set_should_fail_if_capacity_exceeded() { - let mut config: Config = Config::new(); - config.max_items = 2; - - let mut sparkv = SparKV::with_config(config); - let mut set_result = sparkv.set("keyA", "value"); - assert!(set_result.is_ok()); - assert_eq!(sparkv.get("keyA"), Some(String::from("value"))); - - set_result = sparkv.set("keyB", "value2"); - assert!(set_result.is_ok()); +mod tests; - set_result = sparkv.set("keyC", "value3"); - assert!(set_result.is_err()); - assert_eq!(set_result.unwrap_err(), Error::CapacityExceeded); - assert!(sparkv.get("keyC").is_none()); - - // Overwrite existing key should not err - set_result = sparkv.set("keyB", "newValue1234"); - assert!(set_result.is_ok()); - assert_eq!(sparkv.get("keyB"), Some(String::from("newValue1234"))); - } - - #[test] - fn test_set_with_ttl() { - let mut sparkv = SparKV::new(); - _ = sparkv.set("longest", "value"); - _ = sparkv.set_with_ttl( - "longer", - "value", - Duration::new(2, 0).expect("a valid duration"), - ); - _ = sparkv.set_with_ttl( - "shorter", - "value", - Duration::new(1, 0).expect("a valid duration"), - ); - - assert_eq!(sparkv.get("longer"), Some(String::from("value"))); - assert_eq!(sparkv.get("shorter"), Some(String::from("value"))); - assert!( - sparkv.get_item("longer").unwrap().expired_at - > sparkv.get_item("shorter").unwrap().expired_at - ); - assert!( - sparkv.get_item("longest").unwrap().expired_at - > sparkv.get_item("longer").unwrap().expired_at - ); - } - - #[test] - fn test_ensure_max_ttl() { - let mut config: Config = Config::new(); - config.max_ttl = Duration::new(3600, 0).expect("a valid duration"); - config.default_ttl = Duration::new(5000, 0).expect("a valid duration"); - let mut sparkv = SparKV::with_config(config); - - let set_result_long_def = sparkv.set("default is longer than max", "should fail"); - assert!(set_result_long_def.is_err()); - assert_eq!(set_result_long_def.unwrap_err(), Error::TTLTooLong); - - let set_result_ok = sparkv.set_with_ttl( - "shorter", - "ok", - Duration::new(3599, 0).expect("a valid duration"), - ); - assert!(set_result_ok.is_ok()); - - let set_result_ok_2 = sparkv.set_with_ttl( - "exact", - "ok", - Duration::new(3600, 0).expect("a valid duration"), - ); - assert!(set_result_ok_2.is_ok()); - - let set_result_not_ok = sparkv.set_with_ttl( - "not", - "not ok", - Duration::new(3601, 0).expect("a valid duration"), - ); - assert!(set_result_not_ok.is_err()); - assert_eq!(set_result_not_ok.unwrap_err(), Error::TTLTooLong); - } - - #[test] - fn test_delete() { - let mut sparkv = SparKV::new(); - _ = sparkv.set("keyA", "value"); - assert_eq!(sparkv.get("keyA"), Some(String::from("value"))); - assert_eq!(sparkv.expiries.len(), 1); - - let deleted_value = sparkv.pop("keyA"); - assert_eq!(deleted_value, Some(String::from("value"))); - assert!(sparkv.get("keyA").is_none()); - assert_eq!(sparkv.expiries.len(), 1); // it does not delete - } - - #[test] - fn test_clear_expired() { - let mut config: Config = Config::new(); - config.auto_clear_expired = false; - let mut sparkv = SparKV::with_config(config); - _ = sparkv.set_with_ttl( - "not-yet-expired", - "v", - Duration::new(0, 90).expect("a valid duration"), - ); - _ = sparkv.set_with_ttl( - "expiring", - "value", - Duration::new(1, 0).expect("a valid duration"), - ); - _ = sparkv.set_with_ttl( - "not-expired", - "value", - Duration::new(60, 0).expect("a valid duration"), - ); - std::thread::sleep(std::time::Duration::from_nanos(2)) - } - - #[test] - fn test_clear_expired_with_overwritten_key() { - let mut config: Config = Config::new(); - config.auto_clear_expired = false; - let mut sparkv = SparKV::with_config(config); - _ = sparkv.set_with_ttl( - "no-longer", - "value", - Duration::new(0, 1).expect("a valid duration"), - ); - _ = sparkv.set_with_ttl( - "no-longer", - "v", - Duration::new(90, 0).expect("a valid duration"), - ); - _ = sparkv.set_with_ttl( - "not-expired", - "value", - Duration::new(60, 0).expect("a valid duration"), - ); - std::thread::sleep(std::time::Duration::from_nanos(2)); - assert_eq!(sparkv.expiries.len(), 3); // overwriting key does not update expiries - assert_eq!(sparkv.len(), 2); - - let cleared_count = sparkv.clear_expired(); - assert_eq!(cleared_count, 0); // no longer expiring - assert_eq!(sparkv.expiries.len(), 2); // should have cleared the expiries - assert_eq!(sparkv.len(), 2); // but not actually deleting - } - - #[test] - fn test_clear_expired_with_auto_clear_expired_enabled() { - let mut config: Config = Config::new(); - config.auto_clear_expired = true; // explicitly setting it to true - let mut sparkv = SparKV::with_config(config); - _ = sparkv.set_with_ttl( - "no-longer", - "value", - Duration::new(1, 0).expect("a valid duration"), - ); - _ = sparkv.set_with_ttl( - "no-longer", - "v", - Duration::new(90, 0).expect("a valid duration"), - ); - std::thread::sleep(std::time::Duration::from_secs(2)); - _ = sparkv.set_with_ttl( - "not-expired", - "value", - Duration::new(60, 0).expect("a valid duration"), - ); - assert_eq!(sparkv.expiries.len(), 2); // diff from above, because of auto clear - assert_eq!(sparkv.len(), 2); - - // auto clear 2 - _ = sparkv.set_with_ttl( - "new-", - "value", - Duration::new(60, 0).expect("a valid duration"), - ); - assert_eq!(sparkv.expiries.len(), 3); // should have cleared the expiries - assert_eq!(sparkv.len(), 3); // but not actually deleting - } -} +#[cfg(test)] +mod test_json_value; diff --git a/jans-cedarling/sparkv/src/test_json_value.rs b/jans-cedarling/sparkv/src/test_json_value.rs new file mode 100644 index 00000000000..f9e0afc1712 --- /dev/null +++ b/jans-cedarling/sparkv/src/test_json_value.rs @@ -0,0 +1,177 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use crate::Config; +use crate::SparKV; +use serde_json; + +#[cfg(test)] +fn json_value_size(value: &serde_json::Value) -> usize { + std::mem::size_of::() + + match value { + serde_json::Value::Null => 0, + serde_json::Value::Bool(_) => 0, + serde_json::Value::Number(_) => 0, // Incorrect if arbitrary_precision is enabled. oh well + serde_json::Value::String(s) => s.capacity(), + serde_json::Value::Array(a) => { + a.iter().map(json_value_size).sum::() + + a.capacity() * std::mem::size_of::() + }, + serde_json::Value::Object(o) => o + .iter() + .map(|(k, v)| { + std::mem::size_of::() + + k.capacity() + + json_value_size(v) + + std::mem::size_of::() * 3 // As a crude approximation, I pretend each map entry has 3 words of overhead + }) + .sum(), + } +} + +fn first_json() -> serde_json::Value { + serde_json::json!({ + "name" : "first_json", + "compile_kind": 0, + "config": 3355035640151825893usize, + "declared_features": ["bstr", "bytes", "default", "inline", "serde", "text", "unicode", "unicode-segmentation"], + "deps": [], + "features": ["default", "text"], + "local": [ + { + "CheckDepInfo": { + "checksum": false, + "dep_info": "debug/.fingerprint/similar-056a66f4ad898c88/dep-lib-similar" + } + } + ], + "metadata": 943206097653546126i64, + "path": 7620609427446831929u64, + "profile": 10243973527296709326usize, + "rustc": 11594289678289209806usize, + "rustflags": [ + "-C", + "link-arg=-fuse-ld=/usr/bin/mold" + ], + "target": 15605724903113465739u64 + }) +} + +fn second_json() -> serde_json::Value { + serde_json::json!({ + "name" : "second_json", + "compile_kind": 0, + "config": 5533035641051825893usize, + "declared_features": ["bstr", "bytes", "default", "inline", "serde", "text", "unicode", "unicode-segmentation"], + "deps": [], + "features": ["default", "text"], + "local": [ + { + "CheckDepInfo": { + "checksum": false, + "dep_info": "debug/.fingerprint/utterly-different-0a6664d898c8f8a5/dep-lib-utterly-different" + } + } + ], + "metadata": 943206097653546126i64, + "path": 7620609427446831929u64, + "profile": 10243973527296709326usize, + "rustc": 11594289678289209806usize, + "rustflags": [ + "-C", + "link-arg=-fuse-ld=/usr/bin/mold" + ], + "target": 15605724903113465739u64 + }) +} + +#[test] +fn simple_serde_json() { + let config: Config = Config::new(); + let mut sparkv = + SparKV::::with_config_and_sizer(config, Some(json_value_size)); + let json = first_json(); + sparkv.set("first", json.clone()).unwrap(); + let stored_first = sparkv.get("first").unwrap(); + assert_eq!(&json, stored_first); +} + +#[test] +fn type_serde_json() { + let config: Config = Config::new(); + let mut sparkv = + SparKV::::with_config_and_sizer(config, Some(json_value_size)); + let json = first_json(); + sparkv.set("first", json.clone()).unwrap(); + + // now make sure it's actually stored as the value, not as a String + let kv = sparkv.get_item("first").unwrap(); + use std::any::{Any, TypeId}; + assert_eq!(kv.value.type_id(), TypeId::of::()); +} + +#[test] +fn fails_size_calculator() { + // create this first, so we know what item size is too large + let json = first_json(); + + let mut config: Config = Config::new(); + // set item size to something smaller than item + config.max_item_size = json_value_size(&json) / 2; + let mut sparkv = + SparKV::::with_config_and_sizer(config, Some(json_value_size)); + + let should_be_error = sparkv.set("first", json.clone()); + assert_eq!(should_be_error, Err(crate::Error::ItemSizeExceeded)); +} + +#[test] +fn two_json_items() { + let mut sparkv = SparKV::::new(); + sparkv.set("first", first_json()).unwrap(); + sparkv.set("second", second_json()).unwrap(); + + let fj = sparkv.get("first").unwrap(); + assert_eq!( + fj.pointer("/name").unwrap(), + &serde_json::Value::String("first_json".into()) + ); + + let sj = sparkv.get("second").unwrap(); + assert_eq!( + sj.pointer("/name").unwrap(), + &serde_json::Value::String("second_json".into()) + ); +} + +#[test] +fn drain_all_json_items() { + let mut sparkv = SparKV::::new(); + sparkv.set("first", first_json()).unwrap(); + sparkv.set("second", second_json()).unwrap(); + + let all_items = sparkv.drain(); + let all_values = all_items.map(|(_, v)| v).collect::>(); + assert_eq!(all_values, vec![first_json(), second_json()]); + + assert!(sparkv.is_empty(), "sparkv not empty"); +} + +#[test] +fn rc_json_items() { + use std::rc::Rc; + let mut sparkv = SparKV::>::new(); + sparkv.set("first", Rc::new(first_json())).unwrap(); + sparkv.set("second", Rc::new(second_json())).unwrap(); + + let all_items = sparkv.drain(); + let all_values = all_items.map(|(_, v)| v).collect::>(); + assert_eq!(all_values, vec![ + Rc::new(first_json()), + Rc::new(second_json()) + ]); + + assert!(sparkv.is_empty(), "sparkv not empty"); +} diff --git a/jans-cedarling/sparkv/src/tests.rs b/jans-cedarling/sparkv/src/tests.rs new file mode 100644 index 00000000000..848fdd9db98 --- /dev/null +++ b/jans-cedarling/sparkv/src/tests.rs @@ -0,0 +1,266 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use crate::*; + +#[test] +fn test_sparkv_config() { + let config: Config = Config::new(); + assert_eq!(config.max_items, 10_000); + assert_eq!(config.max_item_size, 500_000); + assert_eq!(config.max_ttl, Duration::seconds(60 * 60)); +} + +#[test] +fn test_sparkv_new_with_config() { + let config: Config = Config::new(); + let sparkv = SparKV::::with_config(config); + assert_eq!(sparkv.config, config); +} + +#[test] +fn test_len_is_empty() { + let mut sparkv = SparKV::::new(); + assert_eq!(sparkv.len(), 0); + assert!(sparkv.is_empty()); + + _ = sparkv.set("keyA", "value".to_string()); + assert_eq!(sparkv.len(), 1); + assert!(!sparkv.is_empty()); +} + +#[test] +fn test_set_get() { + let mut sparkv = SparKV::::new(); + _ = sparkv.set("keyA", "value".into()); + assert_eq!(sparkv.get("keyA"), Some(&String::from("value"))); + assert_eq!(sparkv.expiries.len(), 1); + + // Overwrite the value + _ = sparkv.set("keyA", "value2".into()); + assert_eq!(sparkv.get("keyA"), Some(&String::from("value2"))); + assert_eq!(sparkv.expiries.len(), 2); + + assert!(sparkv.get("non-existent").is_none()); +} + +#[test] +fn test_get_item() { + let mut sparkv = SparKV::new(); + let item = KvEntry::new("keyARaw", "value99", 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()); + assert_eq!(unwrapped.key, "keyARaw"); + assert_eq!(unwrapped.value, "value99"); + + assert!(sparkv.get_item("non-existent").is_none()); +} + +#[test] +fn test_get_item_return_none_if_expired() { + let mut sparkv = SparKV::new(); + _ = sparkv.set_with_ttl("key", "value", Duration::microseconds(40)); + assert_eq!(sparkv.get("key"), Some(&"value")); + + std::thread::sleep(std::time::Duration::from_micros(80)); + assert_eq!(sparkv.get("key"), None); +} + +#[test] +fn test_set_should_fail_if_capacity_exceeded() { + let mut config: Config = Config::new(); + config.max_items = 2; + + let mut sparkv = SparKV::::with_config(config); + let mut set_result = sparkv.set("keyA", "value".to_string()); + assert!(set_result.is_ok()); + assert_eq!(sparkv.get("keyA"), Some(&String::from("value"))); + + set_result = sparkv.set("keyB", "value2".to_string()); + assert!(set_result.is_ok()); + + set_result = sparkv.set("keyC", "value3".to_string()); + assert!(set_result.is_err()); + assert_eq!(set_result.unwrap_err(), Error::CapacityExceeded); + assert!(sparkv.get("keyC").is_none()); + + // Overwrite existing key should not err + set_result = sparkv.set("keyB", "newValue1234".to_string()); + assert!(set_result.is_ok()); + assert_eq!(sparkv.get("keyB"), Some(&String::from("newValue1234"))); +} + +#[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::::with_config(config); + + let error = sparkv.set("blue", value); + assert_eq!(error, Err(crate::Error::ItemSizeExceeded)); +} + +#[test] +fn custom_item_capacity_exceeded() { + let mut config: Config = Config::new(); + config.max_item_size = 20; + let mut sparkv = SparKV::<&str>::with_config_and_sizer(config, Some(|s| s.len())); + + assert_eq!(Ok(()), sparkv.set("short", "value")); + assert_eq!( + Err(crate::Error::ItemSizeExceeded), + sparkv.set("long", "This is a value that exceeds 20 characters") + ); +} + +#[test] +fn test_set_with_ttl() { + let mut sparkv = SparKV::::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"))); + assert_eq!(sparkv.get("shorter"), Some(&String::from("value"))); + assert!( + sparkv.get_item("longer").unwrap().expired_at + > sparkv.get_item("shorter").unwrap().expired_at + ); + assert!( + sparkv.get_item("longest").unwrap().expired_at + > sparkv.get_item("longer").unwrap().expired_at + ); +} + +#[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::::with_config(config); + + let set_result_long_def = sparkv.set("default is longer than max", "should fail".to_string()); + assert!(set_result_long_def.is_err()); + assert_eq!(set_result_long_def.unwrap_err(), Error::TTLTooLong); + + let set_result_ok = sparkv.set_with_ttl("shorter", "ok".into(), Duration::seconds(3599)); + assert!(set_result_ok.is_ok()); + + let set_result_ok_2 = sparkv.set_with_ttl("exact", "ok".into(), Duration::seconds(3600)); + assert!(set_result_ok_2.is_ok()); + + let set_result_not_ok = sparkv.set_with_ttl("not", "not ok".into(), Duration::seconds(33601)); + assert!(set_result_not_ok.is_err()); + assert_eq!(set_result_not_ok.unwrap_err(), Error::TTLTooLong); +} + +#[test] +fn test_delete() { + let mut sparkv = SparKV::::new(); + _ = sparkv.set("keyA", "value".to_string()); + assert_eq!(sparkv.get("keyA"), Some(&String::from("value"))); + assert_eq!(sparkv.expiries.len(), 1); + + let deleted_value = sparkv.pop("keyA"); + assert_eq!(deleted_value, Some(String::from("value"))); + assert!(sparkv.get("keyA").is_none()); + assert_eq!(sparkv.expiries.len(), 1); // it does not delete +} + +#[test] +fn test_clear_expired() { + let mut config: Config = Config::new(); + config.auto_clear_expired = false; + let mut sparkv = SparKV::with_config(config); + _ = sparkv.set_with_ttl("not-yet-expired", "v", Duration::seconds(90)); + _ = sparkv.set_with_ttl("expiring", "value", Duration::milliseconds(1)); + _ = sparkv.set_with_ttl("not-expired", "value", Duration::seconds(60)); + std::thread::sleep(std::time::Duration::from_millis(2)); + assert_eq!(sparkv.len(), 3); + + let cleared_count = sparkv.clear_expired(); + assert_eq!(cleared_count, 1); + assert_eq!(sparkv.len(), 2); + + assert_eq!(sparkv.clear_expired(), 0); +} + +#[test] +fn test_clear_expired_with_overwritten_key() { + let mut config: Config = Config::new(); + config.auto_clear_expired = false; + let mut sparkv = SparKV::with_config(config); + _ = sparkv.set_with_ttl("no-longer", "value", Duration::milliseconds(1)); + _ = sparkv.set_with_ttl("no-longer", "v", Duration::seconds(90)); + _ = sparkv.set_with_ttl("not-expired", "value", Duration::seconds(60)); + std::thread::sleep(std::time::Duration::from_millis(2)); + assert_eq!(sparkv.expiries.len(), 3); // overwriting key does not update expiries + assert_eq!(sparkv.len(), 2); + + let cleared_count = sparkv.clear_expired(); + assert_eq!(cleared_count, 0); // no longer expiring + assert_eq!(sparkv.expiries.len(), 2); // should have cleared the expiries + assert_eq!(sparkv.len(), 2); // but not actually deleting +} + +#[test] +fn test_clear_expired_with_auto_clear_expired_enabled() { + let mut config: Config = Config::new(); + config.auto_clear_expired = true; // explicitly setting it to true + let mut sparkv = SparKV::::with_config(config); + _ = sparkv.set_with_ttl("no-longer", "value".into(), Duration::milliseconds(1)); + _ = sparkv.set_with_ttl("no-longer", "v".into(), Duration::seconds(90)); + std::thread::sleep(std::time::Duration::from_millis(2)); + _ = sparkv.set_with_ttl("not-expired", "value".into(), Duration::seconds(60)); + assert_eq!(sparkv.expiries.len(), 2); // diff from above, because of auto clear + assert_eq!(sparkv.len(), 2); + + // auto clear 2 + _ = sparkv.set_with_ttl("new-", "value".into(), Duration::seconds(60)); + assert_eq!(sparkv.expiries.len(), 3); // should have cleared the expiries + assert_eq!(sparkv.len(), 3); // but not actually deleting +} + +#[test] +fn iterator() { + let mut sparkv = SparKV::::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"); + + let (keys, values): (Vec<_>, Vec<_>) = iter.unzip(); + assert_eq!(keys, vec!["ghost", "is", "like", "oh", "this", "woo"]); + assert_eq!(values, vec!["town", "coming", "a", "yeah", "town", "oooo"]); +} + +#[test] +fn drain() { + let mut sparkv = SparKV::::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 (keys, values): (Vec<_>, Vec<_>) = iter.unzip(); + assert_eq!(keys, vec!["ghost", "is", "like", "oh", "this", "woo"]); + assert_eq!(values, vec!["town", "coming", "a", "yeah", "town", "oooo"]); +}