use std::time::{Duration, Instant}; use fnct::{ backend::AsyncRedisBackend, format::{JsonFormatter, PostcardFormatter}, key, }; use redis::{aio::MultiplexedConnection, Client}; use tokio::sync::OnceCell; type AsyncCache = fnct::AsyncCache, F>; macro_rules! tests { ($name:ident, $formatter:tt) => { mod $name { use super::*; #[tokio::test] async fn test_cached() { let cache = get_cache("test_cached").await; struct App { cache: AsyncCache<$formatter>, } impl App { async fn expensive_computation(&self, a: i32, b: i32) -> i32 { self.cache .cached(key!(a, b), &[], None, || async { tokio::time::sleep(Duration::from_secs(3)).await; a + b }) .await .unwrap() } } let app = App { cache }; let now = Instant::now(); assert_eq!(app.expensive_computation(1, 2).await, 3); assert!(now.elapsed() >= Duration::from_secs(3)); let now = Instant::now(); assert_eq!(app.expensive_computation(1, 2).await, 3); assert_eq!(app.expensive_computation(1, 2).await, 3); assert_eq!(app.expensive_computation(1, 2).await, 3); assert!(now.elapsed() < Duration::from_secs(3)); } #[tokio::test] async fn test_cached_option() { let cache = get_cache("test_cached_option").await; struct App { cache: AsyncCache<$formatter>, } impl App { async fn expensive_computation(&self, a: i32, b: i32) -> Option { self.cache .cached_option((a, b), &[], None, || async { tokio::time::sleep(Duration::from_secs(2)).await; if a > 0 { Some(a + b) } else { None } }) .await .unwrap() } } let app = App { cache }; let now = Instant::now(); assert_eq!(app.expensive_computation(-1, 2).await, None); assert!(now.elapsed() >= Duration::from_secs(2)); let now = Instant::now(); assert_eq!(app.expensive_computation(-1, 2).await, None); assert!(now.elapsed() >= Duration::from_secs(2)); let now = Instant::now(); assert_eq!(app.expensive_computation(1, 2).await, Some(3)); assert!(now.elapsed() >= Duration::from_secs(2)); let now = Instant::now(); assert_eq!(app.expensive_computation(1, 2).await, Some(3)); assert_eq!(app.expensive_computation(1, 2).await, Some(3)); assert_eq!(app.expensive_computation(1, 2).await, Some(3)); assert!(now.elapsed() < Duration::from_secs(3)); } #[tokio::test] async fn test_cached_result() { let cache = get_cache("test_cached_result").await; struct App { cache: AsyncCache<$formatter>, } impl App { async fn expensive_computation(&self, a: i32, b: i32) -> Result { self.cache .cached_result((a, b), &[], None, || async { tokio::time::sleep(Duration::from_secs(2)).await; if a > 0 { Ok(a + b) } else { Err(a == 0) } }) .await .unwrap() } } let app = App { cache }; let now = Instant::now(); assert_eq!(app.expensive_computation(-1, 2).await, Err(false)); assert!(now.elapsed() >= Duration::from_secs(2)); let now = Instant::now(); assert_eq!(app.expensive_computation(-1, 2).await, Err(false)); assert!(now.elapsed() >= Duration::from_secs(2)); let now = Instant::now(); assert_eq!(app.expensive_computation(0, 2).await, Err(true)); assert!(now.elapsed() >= Duration::from_secs(2)); let now = Instant::now(); assert_eq!(app.expensive_computation(0, 2).await, Err(true)); assert!(now.elapsed() >= Duration::from_secs(2)); let now = Instant::now(); assert_eq!(app.expensive_computation(1, 2).await, Ok(3)); assert!(now.elapsed() >= Duration::from_secs(2)); let now = Instant::now(); assert_eq!(app.expensive_computation(1, 2).await, Ok(3)); assert_eq!(app.expensive_computation(1, 2).await, Ok(3)); assert_eq!(app.expensive_computation(1, 2).await, Ok(3)); assert!(now.elapsed() < Duration::from_secs(3)); } #[tokio::test] async fn test_basic_insert_expire() { let cache = get_cache("test_basic_insert_expire").await; assert_eq!(cache.get::("foo").await.unwrap(), None); assert_eq!(cache.get::("asdf").await.unwrap(), None); cache .put("foo", "bar".to_owned(), &[], Some(Duration::from_secs(1))) .await .unwrap(); cache .put("asdf", "baz".to_owned(), &[], Some(Duration::from_secs(2))) .await .unwrap(); assert_eq!(cache.get("foo").await.unwrap(), Some("bar".to_owned())); assert_eq!(cache.get("asdf").await.unwrap(), Some("baz".to_owned())); tokio::time::sleep(Duration::from_secs(1)).await; assert_eq!(cache.get::("foo").await.unwrap(), None); assert_eq!(cache.get("asdf").await.unwrap(), Some("baz".to_owned())); tokio::time::sleep(Duration::from_secs(1)).await; assert_eq!(cache.get::("foo").await.unwrap(), None); assert_eq!(cache.get::("asdf").await.unwrap(), None); } #[tokio::test] async fn test_delete_by_key() { let cache = get_cache("test_delete_by_key").await; cache.put("foo", "bar".to_owned(), &[], None).await.unwrap(); cache .put("asdf", "baz".to_owned(), &[], None) .await .unwrap(); assert_eq!(cache.get("foo").await.unwrap(), Some("bar".to_owned())); assert_eq!(cache.get("asdf").await.unwrap(), Some("baz".to_owned())); cache.pop_key("asdf").await.unwrap(); assert_eq!(cache.get("foo").await.unwrap(), Some("bar".to_owned())); assert_eq!(cache.get::("asdf").await.unwrap(), None); cache.pop_key("foo").await.unwrap(); assert_eq!(cache.get::("foo").await.unwrap(), None); assert_eq!(cache.get::("asdf").await.unwrap(), None); } #[tokio::test] async fn test_delete_by_tag() { let cache = get_cache("test_delete_by_tag").await; cache .put("foo", 1, &["t1", "t2", "t3"], None) .await .unwrap(); cache.put("bar", 2, &["t1", "t2"], None).await.unwrap(); cache.put("baz", 3, &["t1", "t3"], None).await.unwrap(); assert_eq!(cache.get("foo").await.unwrap(), Some(1)); assert_eq!(cache.get("bar").await.unwrap(), Some(2)); assert_eq!(cache.get("baz").await.unwrap(), Some(3)); cache.pop_tag("t3").await.unwrap(); assert_eq!(cache.get::("foo").await.unwrap(), None); assert_eq!(cache.get("bar").await.unwrap(), Some(2)); assert_eq!(cache.get::("baz").await.unwrap(), None); cache.pop_tag("t2").await.unwrap(); assert_eq!(cache.get::("foo").await.unwrap(), None); assert_eq!(cache.get::("bar").await.unwrap(), None); assert_eq!(cache.get::("baz").await.unwrap(), None); cache .put("foo", 1, &["t1", "t2", "t3"], None) .await .unwrap(); cache.put("bar", 2, &["t1", "t2"], None).await.unwrap(); cache.put("baz", 3, &["t1", "t3"], None).await.unwrap(); assert_eq!(cache.get("foo").await.unwrap(), Some(1)); assert_eq!(cache.get("bar").await.unwrap(), Some(2)); assert_eq!(cache.get("baz").await.unwrap(), Some(3)); cache.pop_tag("t1").await.unwrap(); assert_eq!(cache.get::("foo").await.unwrap(), None); assert_eq!(cache.get::("bar").await.unwrap(), None); assert_eq!(cache.get::("baz").await.unwrap(), None); } #[tokio::test] async fn test_delete_by_tags_intersect() { let cache = get_cache("test_delete_by_tags_intersect").await; cache .put("foo", 1, &["t1", "t2", "t3"], None) .await .unwrap(); cache.put("bar", 2, &["t1", "t2"], None).await.unwrap(); cache.put("baz", 3, &["t1", "t3"], None).await.unwrap(); assert_eq!(cache.get("foo").await.unwrap(), Some(1)); assert_eq!(cache.get("bar").await.unwrap(), Some(2)); assert_eq!(cache.get("baz").await.unwrap(), Some(3)); cache.pop_tags(&["t2", "t3"]).await.unwrap(); assert_eq!(cache.get::("foo").await.unwrap(), None); assert_eq!(cache.get("bar").await.unwrap(), Some(2)); assert_eq!(cache.get("baz").await.unwrap(), Some(3)); cache.pop_tags(&["t1", "t2"]).await.unwrap(); assert_eq!(cache.get::("foo").await.unwrap(), None); assert_eq!(cache.get::("bar").await.unwrap(), None); assert_eq!(cache.get("baz").await.unwrap(), Some(3)); cache .put("foo", 1, &["t1", "t2", "t3"], None) .await .unwrap(); cache.put("bar", 2, &["t1", "t2"], None).await.unwrap(); cache.put("baz", 3, &["t1", "t3"], None).await.unwrap(); assert_eq!(cache.get("foo").await.unwrap(), Some(1)); assert_eq!(cache.get("bar").await.unwrap(), Some(2)); assert_eq!(cache.get("baz").await.unwrap(), Some(3)); cache.pop_tags(&["t1", "t3"]).await.unwrap(); assert_eq!(cache.get::("foo").await.unwrap(), None); assert_eq!(cache.get("bar").await.unwrap(), Some(2)); assert_eq!(cache.get::("baz").await.unwrap(), None); cache .put("foo", 1, &["t1", "t2", "t3"], None) .await .unwrap(); cache.put("bar", 2, &["t1", "t2"], None).await.unwrap(); cache.put("baz", 3, &["t1", "t3"], None).await.unwrap(); cache.put("xy", 4, &["t4"], None).await.unwrap(); assert_eq!(cache.get("foo").await.unwrap(), Some(1)); assert_eq!(cache.get("bar").await.unwrap(), Some(2)); assert_eq!(cache.get("baz").await.unwrap(), Some(3)); cache.pop_tags(&["t1"]).await.unwrap(); assert_eq!(cache.get::("foo").await.unwrap(), None); assert_eq!(cache.get::("bar").await.unwrap(), None); assert_eq!(cache.get::("baz").await.unwrap(), None); assert_eq!(cache.get("xy").await.unwrap(), Some(4)); } #[tokio::test] async fn test_delete_by_tags_all() { let cache = get_cache("test_delete_by_tags_all").await; cache .put("foo", 1, &["t1", "t2", "t3"], None) .await .unwrap(); cache.put("bar", 2, &["t1", "t2"], None).await.unwrap(); cache.put("baz", 3, &["t1", "t3"], None).await.unwrap(); assert_eq!(cache.get("foo").await.unwrap(), Some(1)); assert_eq!(cache.get("bar").await.unwrap(), Some(2)); assert_eq!(cache.get("baz").await.unwrap(), Some(3)); cache.pop_tags(&[]).await.unwrap(); assert_eq!(cache.get::("foo").await.unwrap(), None); assert_eq!(cache.get::("bar").await.unwrap(), None); assert_eq!(cache.get::("baz").await.unwrap(), None); } #[tokio::test] async fn test_to_json() { let cache: AsyncCache<$formatter> = get_cache("test_to_json").await; let _: AsyncCache = cache.with_formatter(JsonFormatter); } async fn get_cache(namespace: &str) -> AsyncCache<$formatter> { AsyncCache::new( AsyncRedisBackend::new(get_redis().await, namespace.into()), Default::default(), Duration::from_secs(20), ) } } }; } tests!(postcard, PostcardFormatter); tests!(json, JsonFormatter); async fn get_redis() -> MultiplexedConnection { let client = Client::open(std::env::var("REDIS_SERVER").unwrap_or("redis://127.0.0.1:6379/0".into())) .unwrap(); let mut conn = client.get_multiplexed_async_connection().await.unwrap(); static ONCE: OnceCell<()> = OnceCell::const_new(); ONCE.get_or_init(|| async { redis::cmd("FLUSHDB") .query_async::<()>(&mut conn) .await .unwrap(); }) .await; conn }