From 3c31d0cc42327b8dc02a7246f3922ffd960c69be Mon Sep 17 00:00:00 2001 From: Shachar Langbeheim Date: Tue, 19 Mar 2024 15:25:37 +0200 Subject: [PATCH] Wrap tests with modules. This will allow us to run the tests in a group directly from the IDE or from commandline - `cargo test cluster_async` / c`argo test async`, etc. --- redis/tests/test_async.rs | 1853 ++++++++-------- redis/tests/test_basic.rs | 2623 +++++++++++----------- redis/tests/test_cluster.rs | 1706 +++++++-------- redis/tests/test_cluster_async.rs | 3354 +++++++++++++++-------------- redis/tests/test_types.rs | 953 ++++---- 5 files changed, 5275 insertions(+), 5214 deletions(-) diff --git a/redis/tests/test_async.rs b/redis/tests/test_async.rs index 13236bd8f..0020898e8 100644 --- a/redis/tests/test_async.rs +++ b/redis/tests/test_async.rs @@ -1,1059 +1,1068 @@ -use std::collections::HashMap; +mod support; -use futures::{prelude::*, StreamExt}; -use redis::{ - aio::{ConnectionLike, MultiplexedConnection}, - cmd, pipe, AsyncCommands, ErrorKind, PushInfo, PushKind, RedisResult, Value, -}; -use tokio::sync::mpsc::error::TryRecvError; +#[cfg(test)] +mod basic_async { + use std::collections::HashMap; -use crate::support::*; -mod support; + use futures::{prelude::*, StreamExt}; + use redis::{ + aio::{ConnectionLike, MultiplexedConnection}, + cmd, pipe, AsyncCommands, ErrorKind, PushInfo, PushKind, RedisResult, Value, + }; + use tokio::sync::mpsc::error::TryRecvError; -#[test] -fn test_args() { - let ctx = TestContext::new(); - let connect = ctx.async_connection(); - - block_on_all(connect.and_then(|mut con| async move { - redis::cmd("SET") - .arg("key1") - .arg(b"foo") - .query_async(&mut con) - .await?; - redis::cmd("SET") - .arg(&["key2", "bar"]) - .query_async(&mut con) - .await?; - let result = redis::cmd("MGET") - .arg(&["key1", "key2"]) - .query_async(&mut con) - .await; - assert_eq!(result, Ok(("foo".to_string(), b"bar".to_vec()))); - result - })) - .unwrap(); -} + use crate::support::*; -#[test] -fn test_nice_hash_api() { - let ctx = TestContext::new(); + #[test] + fn test_args() { + let ctx = TestContext::new(); + let connect = ctx.async_connection(); - block_on_all(async move { - let mut connection = ctx.async_connection().await.unwrap(); + block_on_all(connect.and_then(|mut con| async move { + redis::cmd("SET") + .arg("key1") + .arg(b"foo") + .query_async(&mut con) + .await?; + redis::cmd("SET") + .arg(&["key2", "bar"]) + .query_async(&mut con) + .await?; + let result = redis::cmd("MGET") + .arg(&["key1", "key2"]) + .query_async(&mut con) + .await; + assert_eq!(result, Ok(("foo".to_string(), b"bar".to_vec()))); + result + })) + .unwrap(); + } - assert_eq!( - connection - .hset_multiple("my_hash", &[("f1", 1), ("f2", 2), ("f3", 4), ("f4", 8)]) - .await, + #[test] + fn test_nice_hash_api() { + let ctx = TestContext::new(); + + block_on_all(async move { + let mut connection = ctx.async_connection().await.unwrap(); + + assert_eq!( + connection + .hset_multiple("my_hash", &[("f1", 1), ("f2", 2), ("f3", 4), ("f4", 8)]) + .await, + Ok(()) + ); + + let hm: HashMap = connection.hgetall("my_hash").await.unwrap(); + assert_eq!(hm.len(), 4); + assert_eq!(hm.get("f1"), Some(&1)); + assert_eq!(hm.get("f2"), Some(&2)); + assert_eq!(hm.get("f3"), Some(&4)); + assert_eq!(hm.get("f4"), Some(&8)); Ok(()) - ); + }) + .unwrap(); + } - let hm: HashMap = connection.hgetall("my_hash").await.unwrap(); - assert_eq!(hm.len(), 4); - assert_eq!(hm.get("f1"), Some(&1)); - assert_eq!(hm.get("f2"), Some(&2)); - assert_eq!(hm.get("f3"), Some(&4)); - assert_eq!(hm.get("f4"), Some(&8)); - Ok(()) - }) - .unwrap(); -} + #[test] + fn test_nice_hash_api_in_pipe() { + let ctx = TestContext::new(); -#[test] -fn test_nice_hash_api_in_pipe() { - let ctx = TestContext::new(); + block_on_all(async move { + let mut connection = ctx.async_connection().await.unwrap(); - block_on_all(async move { - let mut connection = ctx.async_connection().await.unwrap(); + assert_eq!( + connection + .hset_multiple("my_hash", &[("f1", 1), ("f2", 2), ("f3", 4), ("f4", 8)]) + .await, + Ok(()) + ); + + let mut pipe = redis::pipe(); + pipe.cmd("HGETALL").arg("my_hash"); + let mut vec: Vec> = + pipe.query_async(&mut connection).await.unwrap(); + assert_eq!(vec.len(), 1); + let hash = vec.pop().unwrap(); + assert_eq!(hash.len(), 4); + assert_eq!(hash.get("f1"), Some(&1)); + assert_eq!(hash.get("f2"), Some(&2)); + assert_eq!(hash.get("f3"), Some(&4)); + assert_eq!(hash.get("f4"), Some(&8)); - assert_eq!( - connection - .hset_multiple("my_hash", &[("f1", 1), ("f2", 2), ("f3", 4), ("f4", 8)]) - .await, Ok(()) - ); + }) + .unwrap(); + } - let mut pipe = redis::pipe(); - pipe.cmd("HGETALL").arg("my_hash"); - let mut vec: Vec> = pipe.query_async(&mut connection).await.unwrap(); - assert_eq!(vec.len(), 1); - let hash = vec.pop().unwrap(); - assert_eq!(hash.len(), 4); - assert_eq!(hash.get("f1"), Some(&1)); - assert_eq!(hash.get("f2"), Some(&2)); - assert_eq!(hash.get("f3"), Some(&4)); - assert_eq!(hash.get("f4"), Some(&8)); - - Ok(()) - }) - .unwrap(); -} + #[test] + fn dont_panic_on_closed_multiplexed_connection() { + let ctx = TestContext::new(); + let client = ctx.client.clone(); + let connect = client.get_multiplexed_async_connection(); + drop(ctx); -#[test] -fn dont_panic_on_closed_multiplexed_connection() { - let ctx = TestContext::new(); - let client = ctx.client.clone(); - let connect = client.get_multiplexed_async_connection(); - drop(ctx); - - block_on_all(async move { - connect - .and_then(|con| async move { - let cmd = move || { - let mut con = con.clone(); - async move { - redis::cmd("SET") - .arg("key1") - .arg(b"foo") - .query_async(&mut con) - .await - } - }; - let result: RedisResult<()> = cmd().await; - assert_eq!( - result.as_ref().unwrap_err().kind(), - redis::ErrorKind::IoError, - "{}", - result.as_ref().unwrap_err() - ); - cmd().await - }) - .map(|result| { - assert_eq!( - result.as_ref().unwrap_err().kind(), - redis::ErrorKind::IoError, - "{}", - result.as_ref().unwrap_err() - ); - }) - .await; - Ok(()) - }) - .unwrap(); -} + block_on_all(async move { + connect + .and_then(|con| async move { + let cmd = move || { + let mut con = con.clone(); + async move { + redis::cmd("SET") + .arg("key1") + .arg(b"foo") + .query_async(&mut con) + .await + } + }; + let result: RedisResult<()> = cmd().await; + assert_eq!( + result.as_ref().unwrap_err().kind(), + redis::ErrorKind::IoError, + "{}", + result.as_ref().unwrap_err() + ); + cmd().await + }) + .map(|result| { + assert_eq!( + result.as_ref().unwrap_err().kind(), + redis::ErrorKind::IoError, + "{}", + result.as_ref().unwrap_err() + ); + }) + .await; + Ok(()) + }) + .unwrap(); + } -#[test] -fn test_pipeline_transaction() { - let ctx = TestContext::new(); - block_on_all(async move { - let mut con = ctx.async_connection().await?; - let mut pipe = redis::pipe(); - pipe.atomic() - .cmd("SET") - .arg("key_1") - .arg(42) - .ignore() - .cmd("SET") - .arg("key_2") - .arg(43) - .ignore() - .cmd("MGET") - .arg(&["key_1", "key_2"]); - pipe.query_async(&mut con) - .map_ok(|((k1, k2),): ((i32, i32),)| { - assert_eq!(k1, 42); - assert_eq!(k2, 43); - }) - .await - }) - .unwrap(); -} + #[test] + fn test_pipeline_transaction() { + let ctx = TestContext::new(); + block_on_all(async move { + let mut con = ctx.async_connection().await?; + let mut pipe = redis::pipe(); + pipe.atomic() + .cmd("SET") + .arg("key_1") + .arg(42) + .ignore() + .cmd("SET") + .arg("key_2") + .arg(43) + .ignore() + .cmd("MGET") + .arg(&["key_1", "key_2"]); + pipe.query_async(&mut con) + .map_ok(|((k1, k2),): ((i32, i32),)| { + assert_eq!(k1, 42); + assert_eq!(k2, 43); + }) + .await + }) + .unwrap(); + } -#[test] -fn test_client_tracking_doesnt_block_execution() { - //It checks if the library distinguish a push-type message from the others and continues its normal operation. - let ctx = TestContext::new(); - block_on_all(async move { - let mut con = ctx.async_connection().await.unwrap(); - let mut pipe = redis::pipe(); - pipe.cmd("CLIENT") - .arg("TRACKING") - .arg("ON") - .ignore() - .cmd("GET") - .arg("key_1") - .ignore() - .cmd("SET") - .arg("key_1") - .arg(42) - .ignore(); - let _: RedisResult<()> = pipe.query_async(&mut con).await; - let num: i32 = con.get("key_1").await.unwrap(); - assert_eq!(num, 42); - Ok(()) - }) - .unwrap(); -} + #[test] + fn test_client_tracking_doesnt_block_execution() { + //It checks if the library distinguish a push-type message from the others and continues its normal operation. + let ctx = TestContext::new(); + block_on_all(async move { + let mut con = ctx.async_connection().await.unwrap(); + let mut pipe = redis::pipe(); + pipe.cmd("CLIENT") + .arg("TRACKING") + .arg("ON") + .ignore() + .cmd("GET") + .arg("key_1") + .ignore() + .cmd("SET") + .arg("key_1") + .arg(42) + .ignore(); + let _: RedisResult<()> = pipe.query_async(&mut con).await; + let num: i32 = con.get("key_1").await.unwrap(); + assert_eq!(num, 42); + Ok(()) + }) + .unwrap(); + } -#[test] -fn test_pipeline_transaction_with_errors() { - use redis::RedisError; - let ctx = TestContext::new(); + #[test] + fn test_pipeline_transaction_with_errors() { + use redis::RedisError; + let ctx = TestContext::new(); + + block_on_all(async move { + let mut con = ctx.async_connection().await?; + con.set::<_, _, ()>("x", 42).await.unwrap(); + + // Make Redis a replica of a nonexistent master, thereby making it read-only. + redis::cmd("slaveof") + .arg("1.1.1.1") + .arg("1") + .query_async::<_, ()>(&mut con) + .await + .unwrap(); - block_on_all(async move { - let mut con = ctx.async_connection().await?; - con.set::<_, _, ()>("x", 42).await.unwrap(); + // Ensure that a write command fails with a READONLY error + let err: RedisResult<()> = redis::pipe() + .atomic() + .set("x", 142) + .ignore() + .get("x") + .query_async(&mut con) + .await; - // Make Redis a replica of a nonexistent master, thereby making it read-only. - redis::cmd("slaveof") - .arg("1.1.1.1") - .arg("1") - .query_async::<_, ()>(&mut con) - .await - .unwrap(); + assert_eq!(err.unwrap_err().kind(), ErrorKind::ReadOnly); - // Ensure that a write command fails with a READONLY error - let err: RedisResult<()> = redis::pipe() - .atomic() - .set("x", 142) - .ignore() - .get("x") - .query_async(&mut con) - .await; + let x: i32 = con.get("x").await.unwrap(); + assert_eq!(x, 42); - assert_eq!(err.unwrap_err().kind(), ErrorKind::ReadOnly); + Ok::<_, RedisError>(()) + }) + .unwrap(); + } - let x: i32 = con.get("x").await.unwrap(); - assert_eq!(x, 42); + fn test_cmd( + con: &MultiplexedConnection, + i: i32, + ) -> impl Future> + Send { + let mut con = con.clone(); + async move { + let key = format!("key{i}"); + let key_2 = key.clone(); + let key2 = format!("key{i}_2"); + let key2_2 = key2.clone(); - Ok::<_, RedisError>(()) - }) - .unwrap(); -} + let foo_val = format!("foo{i}"); -fn test_cmd(con: &MultiplexedConnection, i: i32) -> impl Future> + Send { - let mut con = con.clone(); - async move { - let key = format!("key{i}"); - let key_2 = key.clone(); - let key2 = format!("key{i}_2"); - let key2_2 = key2.clone(); - - let foo_val = format!("foo{i}"); - - redis::cmd("SET") - .arg(&key[..]) - .arg(foo_val.as_bytes()) - .query_async(&mut con) - .await?; - redis::cmd("SET") - .arg(&[&key2, "bar"]) - .query_async(&mut con) - .await?; - redis::cmd("MGET") - .arg(&[&key_2, &key2_2]) - .query_async(&mut con) - .map(|result| { - assert_eq!(Ok((foo_val, b"bar".to_vec())), result); - Ok(()) - }) - .await + redis::cmd("SET") + .arg(&key[..]) + .arg(foo_val.as_bytes()) + .query_async(&mut con) + .await?; + redis::cmd("SET") + .arg(&[&key2, "bar"]) + .query_async(&mut con) + .await?; + redis::cmd("MGET") + .arg(&[&key_2, &key2_2]) + .query_async(&mut con) + .map(|result| { + assert_eq!(Ok((foo_val, b"bar".to_vec())), result); + Ok(()) + }) + .await + } } -} -fn test_error(con: &MultiplexedConnection) -> impl Future> { - let mut con = con.clone(); - async move { - redis::cmd("SET") - .query_async(&mut con) - .map(|result| match result { - Ok(()) => panic!("Expected redis to return an error"), - Err(_) => Ok(()), - }) - .await + fn test_error(con: &MultiplexedConnection) -> impl Future> { + let mut con = con.clone(); + async move { + redis::cmd("SET") + .query_async(&mut con) + .map(|result| match result { + Ok(()) => panic!("Expected redis to return an error"), + Err(_) => Ok(()), + }) + .await + } } -} -#[test] -fn test_pipe_over_multiplexed_connection() { - let ctx = TestContext::new(); - block_on_all(async move { - let mut con = ctx.multiplexed_async_connection().await?; - let mut pipe = pipe(); - pipe.zrange("zset", 0, 0); - pipe.zrange("zset", 0, 0); - let frames = con.send_packed_commands(&pipe, 0, 2).await?; - assert_eq!(frames.len(), 2); - assert!(matches!(frames[0], redis::Value::Array(_))); - assert!(matches!(frames[1], redis::Value::Array(_))); - RedisResult::Ok(()) - }) - .unwrap(); -} + #[test] + fn test_pipe_over_multiplexed_connection() { + let ctx = TestContext::new(); + block_on_all(async move { + let mut con = ctx.multiplexed_async_connection().await?; + let mut pipe = pipe(); + pipe.zrange("zset", 0, 0); + pipe.zrange("zset", 0, 0); + let frames = con.send_packed_commands(&pipe, 0, 2).await?; + assert_eq!(frames.len(), 2); + assert!(matches!(frames[0], redis::Value::Array(_))); + assert!(matches!(frames[1], redis::Value::Array(_))); + RedisResult::Ok(()) + }) + .unwrap(); + } -#[test] -fn test_args_multiplexed_connection() { - let ctx = TestContext::new(); - block_on_all(async move { - ctx.multiplexed_async_connection() - .and_then(|con| { - let cmds = (0..100).map(move |i| test_cmd(&con, i)); - future::try_join_all(cmds).map_ok(|results| { - assert_eq!(results.len(), 100); + #[test] + fn test_args_multiplexed_connection() { + let ctx = TestContext::new(); + block_on_all(async move { + ctx.multiplexed_async_connection() + .and_then(|con| { + let cmds = (0..100).map(move |i| test_cmd(&con, i)); + future::try_join_all(cmds).map_ok(|results| { + assert_eq!(results.len(), 100); + }) }) - }) - .map_err(|err| panic!("{}", err)) - .await - }) - .unwrap(); -} + .map_err(|err| panic!("{}", err)) + .await + }) + .unwrap(); + } -#[test] -fn test_args_with_errors_multiplexed_connection() { - let ctx = TestContext::new(); - block_on_all(async move { - ctx.multiplexed_async_connection() - .and_then(|con| { - let cmds = (0..100).map(move |i| { - let con = con.clone(); - async move { - if i % 2 == 0 { - test_cmd(&con, i).await - } else { - test_error(&con).await + #[test] + fn test_args_with_errors_multiplexed_connection() { + let ctx = TestContext::new(); + block_on_all(async move { + ctx.multiplexed_async_connection() + .and_then(|con| { + let cmds = (0..100).map(move |i| { + let con = con.clone(); + async move { + if i % 2 == 0 { + test_cmd(&con, i).await + } else { + test_error(&con).await + } } - } - }); - future::try_join_all(cmds).map_ok(|results| { + }); + future::try_join_all(cmds).map_ok(|results| { + assert_eq!(results.len(), 100); + }) + }) + .map_err(|err| panic!("{}", err)) + .await + }) + .unwrap(); + } + + #[test] + fn test_transaction_multiplexed_connection() { + let ctx = TestContext::new(); + block_on_all(async move { + ctx.multiplexed_async_connection() + .and_then(|con| { + let cmds = (0..100).map(move |i| { + let mut con = con.clone(); + async move { + let foo_val = i; + let bar_val = format!("bar{i}"); + + let mut pipe = redis::pipe(); + pipe.atomic() + .cmd("SET") + .arg("key") + .arg(foo_val) + .ignore() + .cmd("SET") + .arg(&["key2", &bar_val[..]]) + .ignore() + .cmd("MGET") + .arg(&["key", "key2"]); + + pipe.query_async(&mut con) + .map(move |result| { + assert_eq!(Ok(((foo_val, bar_val.into_bytes()),)), result); + result + }) + .await + } + }); + future::try_join_all(cmds) + }) + .map_ok(|results| { assert_eq!(results.len(), 100); }) - }) - .map_err(|err| panic!("{}", err)) - .await - }) - .unwrap(); -} + .map_err(|err| panic!("{}", err)) + .await + }) + .unwrap(); + } -#[test] -fn test_transaction_multiplexed_connection() { - let ctx = TestContext::new(); - block_on_all(async move { - ctx.multiplexed_async_connection() - .and_then(|con| { - let cmds = (0..100).map(move |i| { - let mut con = con.clone(); + fn test_async_scanning(batch_size: usize) { + let ctx = TestContext::new(); + block_on_all(async move { + ctx.multiplexed_async_connection() + .and_then(|mut con| { async move { - let foo_val = i; - let bar_val = format!("bar{i}"); - - let mut pipe = redis::pipe(); - pipe.atomic() - .cmd("SET") - .arg("key") - .arg(foo_val) - .ignore() - .cmd("SET") - .arg(&["key2", &bar_val[..]]) - .ignore() - .cmd("MGET") - .arg(&["key", "key2"]); - - pipe.query_async(&mut con) - .map(move |result| { - assert_eq!(Ok(((foo_val, bar_val.into_bytes()),)), result); - result - }) - .await - } - }); - future::try_join_all(cmds) - }) - .map_ok(|results| { - assert_eq!(results.len(), 100); - }) - .map_err(|err| panic!("{}", err)) - .await - }) - .unwrap(); -} - -fn test_async_scanning(batch_size: usize) { - let ctx = TestContext::new(); - block_on_all(async move { - ctx.multiplexed_async_connection() - .and_then(|mut con| { - async move { - let mut unseen = std::collections::HashSet::new(); + let mut unseen = std::collections::HashSet::new(); + + for x in 0..batch_size { + redis::cmd("SADD") + .arg("foo") + .arg(x) + .query_async(&mut con) + .await?; + unseen.insert(x); + } - for x in 0..batch_size { - redis::cmd("SADD") + let mut iter = redis::cmd("SSCAN") .arg("foo") - .arg(x) - .query_async(&mut con) - .await?; - unseen.insert(x); - } + .cursor_arg(0) + .clone() + .iter_async(&mut con) + .await + .unwrap(); - let mut iter = redis::cmd("SSCAN") - .arg("foo") - .cursor_arg(0) - .clone() - .iter_async(&mut con) - .await - .unwrap(); - - while let Some(x) = iter.next_item().await { - // type inference limitations - let x: usize = x; - // if this assertion fails, too many items were returned by the iterator. - assert!(unseen.remove(&x)); + while let Some(x) = iter.next_item().await { + // type inference limitations + let x: usize = x; + // if this assertion fails, too many items were returned by the iterator. + assert!(unseen.remove(&x)); + } + + assert_eq!(unseen.len(), 0); + Ok(()) } + }) + .map_err(|err| panic!("{}", err)) + .await + }) + .unwrap(); + } - assert_eq!(unseen.len(), 0); - Ok(()) - } - }) - .map_err(|err| panic!("{}", err)) - .await - }) - .unwrap(); -} + #[test] + fn test_async_scanning_big_batch() { + test_async_scanning(1000) + } -#[test] -fn test_async_scanning_big_batch() { - test_async_scanning(1000) -} + #[test] + fn test_async_scanning_small_batch() { + test_async_scanning(2) + } -#[test] -fn test_async_scanning_small_batch() { - test_async_scanning(2) -} + #[test] + fn test_response_timeout_multiplexed_connection() { + let ctx = TestContext::new(); + block_on_all(async move { + let mut connection = ctx.multiplexed_async_connection().await.unwrap(); + connection.set_response_timeout(std::time::Duration::from_millis(1)); + let mut cmd = redis::Cmd::new(); + cmd.arg("BLPOP").arg("foo").arg(0); // 0 timeout blocks indefinitely + let result = connection.req_packed_command(&cmd).await; + assert!(result.is_err()); + assert!(result.unwrap_err().is_timeout()); + Ok(()) + }) + .unwrap(); + } -#[test] -fn test_response_timeout_multiplexed_connection() { - let ctx = TestContext::new(); - block_on_all(async move { - let mut connection = ctx.multiplexed_async_connection().await.unwrap(); - connection.set_response_timeout(std::time::Duration::from_millis(1)); - let mut cmd = redis::Cmd::new(); - cmd.arg("BLPOP").arg("foo").arg(0); // 0 timeout blocks indefinitely - let result = connection.req_packed_command(&cmd).await; - assert!(result.is_err()); - assert!(result.unwrap_err().is_timeout()); - Ok(()) - }) - .unwrap(); -} + #[test] + #[cfg(feature = "script")] + fn test_script() { + use redis::RedisError; -#[test] -#[cfg(feature = "script")] -fn test_script() { - use redis::RedisError; - - // Note this test runs both scripts twice to test when they have already been loaded - // into Redis and when they need to be loaded in - let script1 = redis::Script::new("return redis.call('SET', KEYS[1], ARGV[1])"); - let script2 = redis::Script::new("return redis.call('GET', KEYS[1])"); - let script3 = redis::Script::new("return redis.call('KEYS', '*')"); - - let ctx = TestContext::new(); - - block_on_all(async move { - let mut con = ctx.multiplexed_async_connection().await?; - script1 - .key("key1") - .arg("foo") - .invoke_async(&mut con) - .await?; - let val: String = script2.key("key1").invoke_async(&mut con).await?; - assert_eq!(val, "foo"); - let keys: Vec = script3.invoke_async(&mut con).await?; - assert_eq!(keys, ["key1"]); - script1 - .key("key1") - .arg("bar") - .invoke_async(&mut con) - .await?; - let val: String = script2.key("key1").invoke_async(&mut con).await?; - assert_eq!(val, "bar"); - let keys: Vec = script3.invoke_async(&mut con).await?; - assert_eq!(keys, ["key1"]); - Ok::<_, RedisError>(()) - }) - .unwrap(); -} + // Note this test runs both scripts twice to test when they have already been loaded + // into Redis and when they need to be loaded in + let script1 = redis::Script::new("return redis.call('SET', KEYS[1], ARGV[1])"); + let script2 = redis::Script::new("return redis.call('GET', KEYS[1])"); + let script3 = redis::Script::new("return redis.call('KEYS', '*')"); -#[test] -#[cfg(feature = "script")] -fn test_script_load() { - let ctx = TestContext::new(); - let script = redis::Script::new("return 'Hello World'"); + let ctx = TestContext::new(); - block_on_all(async move { - let mut con = ctx.multiplexed_async_connection().await.unwrap(); + block_on_all(async move { + let mut con = ctx.multiplexed_async_connection().await?; + script1 + .key("key1") + .arg("foo") + .invoke_async(&mut con) + .await?; + let val: String = script2.key("key1").invoke_async(&mut con).await?; + assert_eq!(val, "foo"); + let keys: Vec = script3.invoke_async(&mut con).await?; + assert_eq!(keys, ["key1"]); + script1 + .key("key1") + .arg("bar") + .invoke_async(&mut con) + .await?; + let val: String = script2.key("key1").invoke_async(&mut con).await?; + assert_eq!(val, "bar"); + let keys: Vec = script3.invoke_async(&mut con).await?; + assert_eq!(keys, ["key1"]); + Ok::<_, RedisError>(()) + }) + .unwrap(); + } - let hash = script.prepare_invoke().load_async(&mut con).await.unwrap(); - assert_eq!(hash, script.get_hash().to_string()); - Ok(()) - }) - .unwrap(); -} + #[test] + #[cfg(feature = "script")] + fn test_script_load() { + let ctx = TestContext::new(); + let script = redis::Script::new("return 'Hello World'"); -#[test] -#[cfg(feature = "script")] -fn test_script_returning_complex_type() { - let ctx = TestContext::new(); - block_on_all(async { - let mut con = ctx.multiplexed_async_connection().await?; - redis::Script::new("return {1, ARGV[1], true}") - .arg("hello") - .invoke_async(&mut con) - .map_ok(|(i, s, b): (i32, String, bool)| { - assert_eq!(i, 1); - assert_eq!(s, "hello"); - assert!(b); - }) - .await - }) - .unwrap(); -} + block_on_all(async move { + let mut con = ctx.multiplexed_async_connection().await.unwrap(); -// Allowing `nth(0)` for similarity with the following `nth(1)`. -// Allowing `let ()` as `query_async` requries the type it converts the result to. -#[allow(clippy::let_unit_value, clippy::iter_nth_zero)] -#[tokio::test] -async fn io_error_on_kill_issue_320() { - let ctx = TestContext::new(); - - let mut conn_to_kill = ctx.async_connection().await.unwrap(); - cmd("CLIENT") - .arg("SETNAME") - .arg("to-kill") - .query_async::<_, ()>(&mut conn_to_kill) - .await + let hash = script.prepare_invoke().load_async(&mut con).await.unwrap(); + assert_eq!(hash, script.get_hash().to_string()); + Ok(()) + }) .unwrap(); + } - let client_list: String = cmd("CLIENT") - .arg("LIST") - .query_async(&mut conn_to_kill) - .await + #[test] + #[cfg(feature = "script")] + fn test_script_returning_complex_type() { + let ctx = TestContext::new(); + block_on_all(async { + let mut con = ctx.multiplexed_async_connection().await?; + redis::Script::new("return {1, ARGV[1], true}") + .arg("hello") + .invoke_async(&mut con) + .map_ok(|(i, s, b): (i32, String, bool)| { + assert_eq!(i, 1); + assert_eq!(s, "hello"); + assert!(b); + }) + .await + }) .unwrap(); + } - eprintln!("{client_list}"); - let client_to_kill = client_list - .split('\n') - .find(|line| line.contains("to-kill")) - .expect("line") - .split(' ') - .nth(0) - .expect("id") - .split('=') - .nth(1) - .expect("id value"); - - let mut killer_conn = ctx.async_connection().await.unwrap(); - let () = cmd("CLIENT") - .arg("KILL") - .arg("ID") - .arg(client_to_kill) - .query_async(&mut killer_conn) - .await - .unwrap(); - let mut killed_client = conn_to_kill; + // Allowing `nth(0)` for similarity with the following `nth(1)`. + // Allowing `let ()` as `query_async` requries the type it converts the result to. + #[allow(clippy::let_unit_value, clippy::iter_nth_zero)] + #[tokio::test] + async fn io_error_on_kill_issue_320() { + let ctx = TestContext::new(); - let err = loop { - match killed_client.get::<_, Option>("a").await { - // We are racing against the server being shutdown so try until we a get an io error - Ok(_) => tokio::time::sleep(std::time::Duration::from_millis(50)).await, - Err(err) => break err, - } - }; - assert_eq!(err.kind(), ErrorKind::IoError); // Shouldn't this be IoError? -} + let mut conn_to_kill = ctx.async_connection().await.unwrap(); + cmd("CLIENT") + .arg("SETNAME") + .arg("to-kill") + .query_async::<_, ()>(&mut conn_to_kill) + .await + .unwrap(); -#[tokio::test] -async fn invalid_password_issue_343() { - let ctx = TestContext::new(); - let coninfo = redis::ConnectionInfo { - addr: ctx.server.client_addr().clone(), - redis: redis::RedisConnectionInfo { - password: Some("asdcasc".to_string()), - ..Default::default() - }, - }; - let client = redis::Client::open(coninfo).unwrap(); + let client_list: String = cmd("CLIENT") + .arg("LIST") + .query_async(&mut conn_to_kill) + .await + .unwrap(); - let err = client - .get_multiplexed_tokio_connection() - .await - .err() - .unwrap(); - assert_eq!( - err.kind(), - ErrorKind::AuthenticationFailed, - "Unexpected error: {err}", - ); -} + eprintln!("{client_list}"); + let client_to_kill = client_list + .split('\n') + .find(|line| line.contains("to-kill")) + .expect("line") + .split(' ') + .nth(0) + .expect("id") + .split('=') + .nth(1) + .expect("id value"); + + let mut killer_conn = ctx.async_connection().await.unwrap(); + let () = cmd("CLIENT") + .arg("KILL") + .arg("ID") + .arg(client_to_kill) + .query_async(&mut killer_conn) + .await + .unwrap(); + let mut killed_client = conn_to_kill; -// Test issue of Stream trait blocking if we try to iterate more than 10 items -// https://github.com/mitsuhiko/redis-rs/issues/537 and https://github.com/mitsuhiko/redis-rs/issues/583 -#[tokio::test] -async fn test_issue_stream_blocks() { - let ctx = TestContext::new(); - let mut con = ctx.multiplexed_async_connection().await.unwrap(); - for i in 0..20usize { - let _: () = con.append(format!("test/{i}"), i).await.unwrap(); + let err = loop { + match killed_client.get::<_, Option>("a").await { + // We are racing against the server being shutdown so try until we a get an io error + Ok(_) => tokio::time::sleep(std::time::Duration::from_millis(50)).await, + Err(err) => break err, + } + }; + assert_eq!(err.kind(), ErrorKind::IoError); // Shouldn't this be IoError? } - let values = con.scan_match::<&str, String>("test/*").await.unwrap(); - tokio::time::timeout(std::time::Duration::from_millis(100), async move { - let values: Vec<_> = values.collect().await; - assert_eq!(values.len(), 20); - }) - .await - .unwrap(); -} -// Test issue of AsyncCommands::scan returning the wrong number of keys -// https://github.com/redis-rs/redis-rs/issues/759 -#[tokio::test] -async fn test_issue_async_commands_scan_broken() { - let ctx = TestContext::new(); - let mut con = ctx.async_connection().await.unwrap(); - let mut keys: Vec = (0..100).map(|k| format!("async-key{k}")).collect(); - keys.sort(); - for key in &keys { - let _: () = con.set(key, b"foo").await.unwrap(); + #[tokio::test] + async fn invalid_password_issue_343() { + let ctx = TestContext::new(); + let coninfo = redis::ConnectionInfo { + addr: ctx.server.client_addr().clone(), + redis: redis::RedisConnectionInfo { + password: Some("asdcasc".to_string()), + ..Default::default() + }, + }; + let client = redis::Client::open(coninfo).unwrap(); + + let err = client + .get_multiplexed_tokio_connection() + .await + .err() + .unwrap(); + assert_eq!( + err.kind(), + ErrorKind::AuthenticationFailed, + "Unexpected error: {err}", + ); } - let iter: redis::AsyncIter = con.scan().await.unwrap(); - let mut keys_from_redis: Vec<_> = iter.collect().await; - keys_from_redis.sort(); - assert_eq!(keys, keys_from_redis); - assert_eq!(keys.len(), 100); -} + // Test issue of Stream trait blocking if we try to iterate more than 10 items + // https://github.com/mitsuhiko/redis-rs/issues/537 and https://github.com/mitsuhiko/redis-rs/issues/583 + #[tokio::test] + async fn test_issue_stream_blocks() { + let ctx = TestContext::new(); + let mut con = ctx.multiplexed_async_connection().await.unwrap(); + for i in 0..20usize { + let _: () = con.append(format!("test/{i}"), i).await.unwrap(); + } + let values = con.scan_match::<&str, String>("test/*").await.unwrap(); + tokio::time::timeout(std::time::Duration::from_millis(100), async move { + let values: Vec<_> = values.collect().await; + assert_eq!(values.len(), 20); + }) + .await + .unwrap(); + } -mod pub_sub { - use std::time::Duration; + // Test issue of AsyncCommands::scan returning the wrong number of keys + // https://github.com/redis-rs/redis-rs/issues/759 + #[tokio::test] + async fn test_issue_async_commands_scan_broken() { + let ctx = TestContext::new(); + let mut con = ctx.async_connection().await.unwrap(); + let mut keys: Vec = (0..100).map(|k| format!("async-key{k}")).collect(); + keys.sort(); + for key in &keys { + let _: () = con.set(key, b"foo").await.unwrap(); + } - use redis::ProtocolVersion; + let iter: redis::AsyncIter = con.scan().await.unwrap(); + let mut keys_from_redis: Vec<_> = iter.collect().await; + keys_from_redis.sort(); + assert_eq!(keys, keys_from_redis); + assert_eq!(keys.len(), 100); + } - use super::*; + mod pub_sub { + use std::time::Duration; - #[test] - fn pub_sub_subscription() { - use redis::RedisError; + use redis::ProtocolVersion; - let ctx = TestContext::new(); - block_on_all(async move { - let mut pubsub_conn = ctx.async_pubsub().await?; - pubsub_conn.subscribe("phonewave").await?; - let mut pubsub_stream = pubsub_conn.on_message(); - let mut publish_conn = ctx.async_connection().await?; - publish_conn.publish("phonewave", "banana").await?; + use super::*; - let msg_payload: String = pubsub_stream.next().await.unwrap().get_payload()?; - assert_eq!("banana".to_string(), msg_payload); + #[test] + fn pub_sub_subscription() { + use redis::RedisError; - Ok::<_, RedisError>(()) - }) - .unwrap(); - } + let ctx = TestContext::new(); + block_on_all(async move { + let mut pubsub_conn = ctx.async_pubsub().await?; + pubsub_conn.subscribe("phonewave").await?; + let mut pubsub_stream = pubsub_conn.on_message(); + let mut publish_conn = ctx.async_connection().await?; + publish_conn.publish("phonewave", "banana").await?; - #[test] - fn pub_sub_unsubscription() { - use redis::RedisError; + let msg_payload: String = pubsub_stream.next().await.unwrap().get_payload()?; + assert_eq!("banana".to_string(), msg_payload); - const SUBSCRIPTION_KEY: &str = "phonewave-pub-sub-unsubscription"; + Ok::<_, RedisError>(()) + }) + .unwrap(); + } - let ctx = TestContext::new(); - block_on_all(async move { - let mut pubsub_conn = ctx.async_pubsub().await?; - pubsub_conn.subscribe(SUBSCRIPTION_KEY).await?; - pubsub_conn.unsubscribe(SUBSCRIPTION_KEY).await?; - - let mut conn = ctx.async_connection().await?; - let subscriptions_counts: HashMap = redis::cmd("PUBSUB") - .arg("NUMSUB") - .arg(SUBSCRIPTION_KEY) - .query_async(&mut conn) - .await?; - let subscription_count = *subscriptions_counts.get(SUBSCRIPTION_KEY).unwrap(); - assert_eq!(subscription_count, 0); + #[test] + fn pub_sub_unsubscription() { + use redis::RedisError; - Ok::<_, RedisError>(()) - }) - .unwrap(); - } + const SUBSCRIPTION_KEY: &str = "phonewave-pub-sub-unsubscription"; - #[test] - fn automatic_unsubscription() { - use redis::RedisError; + let ctx = TestContext::new(); + block_on_all(async move { + let mut pubsub_conn = ctx.async_pubsub().await?; + pubsub_conn.subscribe(SUBSCRIPTION_KEY).await?; + pubsub_conn.unsubscribe(SUBSCRIPTION_KEY).await?; - const SUBSCRIPTION_KEY: &str = "phonewave-automatic-unsubscription"; - - let ctx = TestContext::new(); - block_on_all(async move { - let mut pubsub_conn = ctx.async_pubsub().await?; - pubsub_conn.subscribe(SUBSCRIPTION_KEY).await?; - drop(pubsub_conn); - - let mut conn = ctx.async_connection().await?; - let mut subscription_count = 1; - // Allow for the unsubscription to occur within 5 seconds - for _ in 0..100 { + let mut conn = ctx.async_connection().await?; let subscriptions_counts: HashMap = redis::cmd("PUBSUB") .arg("NUMSUB") .arg(SUBSCRIPTION_KEY) .query_async(&mut conn) .await?; - subscription_count = *subscriptions_counts.get(SUBSCRIPTION_KEY).unwrap(); - if subscription_count == 0 { - break; - } + let subscription_count = *subscriptions_counts.get(SUBSCRIPTION_KEY).unwrap(); + assert_eq!(subscription_count, 0); - std::thread::sleep(Duration::from_millis(50)); - } - assert_eq!(subscription_count, 0); + Ok::<_, RedisError>(()) + }) + .unwrap(); + } - Ok::<_, RedisError>(()) - }) - .unwrap(); - } + #[test] + fn automatic_unsubscription() { + use redis::RedisError; + + const SUBSCRIPTION_KEY: &str = "phonewave-automatic-unsubscription"; + + let ctx = TestContext::new(); + block_on_all(async move { + let mut pubsub_conn = ctx.async_pubsub().await?; + pubsub_conn.subscribe(SUBSCRIPTION_KEY).await?; + drop(pubsub_conn); + + let mut conn = ctx.async_connection().await?; + let mut subscription_count = 1; + // Allow for the unsubscription to occur within 5 seconds + for _ in 0..100 { + let subscriptions_counts: HashMap = redis::cmd("PUBSUB") + .arg("NUMSUB") + .arg(SUBSCRIPTION_KEY) + .query_async(&mut conn) + .await?; + subscription_count = *subscriptions_counts.get(SUBSCRIPTION_KEY).unwrap(); + if subscription_count == 0 { + break; + } - #[test] - fn pub_sub_conn_reuse() { - use redis::RedisError; + std::thread::sleep(Duration::from_millis(50)); + } + assert_eq!(subscription_count, 0); - let ctx = TestContext::new(); - block_on_all(async move { - let mut pubsub_conn = ctx.async_pubsub().await?; - pubsub_conn.subscribe("phonewave").await?; - pubsub_conn.psubscribe("*").await?; + Ok::<_, RedisError>(()) + }) + .unwrap(); + } - #[allow(deprecated)] - let mut conn = pubsub_conn.into_connection().await; - redis::cmd("SET") - .arg("foo") - .arg("bar") - .query_async(&mut conn) - .await?; + #[test] + fn pub_sub_conn_reuse() { + use redis::RedisError; + + let ctx = TestContext::new(); + block_on_all(async move { + let mut pubsub_conn = ctx.async_pubsub().await?; + pubsub_conn.subscribe("phonewave").await?; + pubsub_conn.psubscribe("*").await?; + + #[allow(deprecated)] + let mut conn = pubsub_conn.into_connection().await; + redis::cmd("SET") + .arg("foo") + .arg("bar") + .query_async(&mut conn) + .await?; - let res: String = redis::cmd("GET").arg("foo").query_async(&mut conn).await?; - assert_eq!(&res, "bar"); + let res: String = redis::cmd("GET").arg("foo").query_async(&mut conn).await?; + assert_eq!(&res, "bar"); - Ok::<_, RedisError>(()) - }) - .unwrap(); - } + Ok::<_, RedisError>(()) + }) + .unwrap(); + } - #[test] - fn pipe_errors_do_not_affect_subsequent_commands() { - use redis::RedisError; + #[test] + fn pipe_errors_do_not_affect_subsequent_commands() { + use redis::RedisError; - let ctx = TestContext::new(); - block_on_all(async move { - let mut conn = ctx.multiplexed_async_connection().await?; + let ctx = TestContext::new(); + block_on_all(async move { + let mut conn = ctx.multiplexed_async_connection().await?; - conn.lpush::<&str, &str, ()>("key", "value").await?; + conn.lpush::<&str, &str, ()>("key", "value").await?; - let res: Result<(String, usize), redis::RedisError> = redis::pipe() + let res: Result<(String, usize), redis::RedisError> = redis::pipe() .get("key") // WRONGTYPE .llen("key") .query_async(&mut conn) .await; - assert!(res.is_err()); - - let list: Vec = conn.lrange("key", 0, -1).await?; - - assert_eq!(list, vec!["value".to_owned()]); + assert!(res.is_err()); - Ok::<_, RedisError>(()) - }) - .unwrap(); - } + let list: Vec = conn.lrange("key", 0, -1).await?; - #[test] - fn pub_sub_multiple() { - use redis::RedisError; + assert_eq!(list, vec!["value".to_owned()]); - let ctx = TestContext::new(); - if ctx.protocol == ProtocolVersion::RESP2 { - return; + Ok::<_, RedisError>(()) + }) + .unwrap(); } - block_on_all(async move { - let mut conn = ctx.multiplexed_async_connection().await?; - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - let pub_count = 10; - let channel_name = "phonewave".to_string(); - conn.get_push_manager().replace_sender(tx.clone()); - conn.subscribe(channel_name.clone()).await?; - rx.recv().await.unwrap(); //PASS SUBSCRIBE - - let mut publish_conn = ctx.async_connection().await?; - for i in 0..pub_count { - publish_conn - .publish(channel_name.clone(), format!("banana {i}")) - .await?; - } - for _ in 0..pub_count { - rx.recv().await.unwrap(); - } - assert!(rx.try_recv().is_err()); - { - //Lets test if unsubscribing from individual channel subscription works - publish_conn - .publish(channel_name.clone(), "banana!") - .await?; - rx.recv().await.unwrap(); + #[test] + fn pub_sub_multiple() { + use redis::RedisError; + + let ctx = TestContext::new(); + if ctx.protocol == ProtocolVersion::RESP2 { + return; } - { - //Giving none for channel id should unsubscribe all subscriptions from that channel and send unsubcribe command to server. - conn.unsubscribe(channel_name.clone()).await?; - rx.recv().await.unwrap(); //PASS UNSUBSCRIBE - publish_conn - .publish(channel_name.clone(), "banana!") - .await?; - //Let's wait for 100ms to make sure there is nothing in channel. - tokio::time::sleep(Duration::from_millis(100)).await; + block_on_all(async move { + let mut conn = ctx.multiplexed_async_connection().await?; + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + let pub_count = 10; + let channel_name = "phonewave".to_string(); + conn.get_push_manager().replace_sender(tx.clone()); + conn.subscribe(channel_name.clone()).await?; + rx.recv().await.unwrap(); //PASS SUBSCRIBE + + let mut publish_conn = ctx.async_connection().await?; + for i in 0..pub_count { + publish_conn + .publish(channel_name.clone(), format!("banana {i}")) + .await?; + } + for _ in 0..pub_count { + rx.recv().await.unwrap(); + } assert!(rx.try_recv().is_err()); - } - Ok::<_, RedisError>(()) - }) - .unwrap(); + { + //Lets test if unsubscribing from individual channel subscription works + publish_conn + .publish(channel_name.clone(), "banana!") + .await?; + rx.recv().await.unwrap(); + } + { + //Giving none for channel id should unsubscribe all subscriptions from that channel and send unsubcribe command to server. + conn.unsubscribe(channel_name.clone()).await?; + rx.recv().await.unwrap(); //PASS UNSUBSCRIBE + publish_conn + .publish(channel_name.clone(), "banana!") + .await?; + //Let's wait for 100ms to make sure there is nothing in channel. + tokio::time::sleep(Duration::from_millis(100)).await; + assert!(rx.try_recv().is_err()); + } + + Ok::<_, RedisError>(()) + }) + .unwrap(); + } + #[test] + fn push_manager_disconnection() { + use redis::RedisError; + + let ctx = TestContext::new(); + if ctx.protocol == ProtocolVersion::RESP2 { + return; + } + block_on_all(async move { + let mut conn = ctx.multiplexed_async_connection().await?; + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + conn.get_push_manager().replace_sender(tx.clone()); + + conn.set("A", "1").await?; + assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty); + drop(ctx); + let x: RedisResult<()> = conn.set("A", "1").await; + assert!(x.is_err()); + assert_eq!(rx.recv().await.unwrap().kind, PushKind::Disconnection); + + Ok::<_, RedisError>(()) + }) + .unwrap(); + } } - #[test] - fn push_manager_disconnection() { - use redis::RedisError; + #[test] + fn test_async_basic_pipe_with_parsing_error() { + // Tests a specific case involving repeated errors in transactions. let ctx = TestContext::new(); - if ctx.protocol == ProtocolVersion::RESP2 { - return; - } + block_on_all(async move { let mut conn = ctx.multiplexed_async_connection().await?; - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - conn.get_push_manager().replace_sender(tx.clone()); - - conn.set("A", "1").await?; - assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty); - drop(ctx); - let x: RedisResult<()> = conn.set("A", "1").await; - assert!(x.is_err()); - assert_eq!(rx.recv().await.unwrap().kind, PushKind::Disconnection); - Ok::<_, RedisError>(()) + // create a transaction where 2 errors are returned. + // we call EVALSHA twice with no loaded script, thus triggering 2 errors. + redis::pipe() + .atomic() + .cmd("EVALSHA") + .arg("foobar") + .arg(0) + .cmd("EVALSHA") + .arg("foobar") + .arg(0) + .query_async::<_, ((), ())>(&mut conn) + .await + .expect_err("should return an error"); + + assert!( + // Arbitrary Redis command that should not return an error. + redis::cmd("SMEMBERS") + .arg("nonexistent_key") + .query_async::<_, Vec>(&mut conn) + .await + .is_ok(), + "Failed transaction should not interfere with future calls." + ); + + Ok::<_, redis::RedisError>(()) }) - .unwrap(); + .unwrap() } -} - -#[test] -fn test_async_basic_pipe_with_parsing_error() { - // Tests a specific case involving repeated errors in transactions. - let ctx = TestContext::new(); - - block_on_all(async move { - let mut conn = ctx.multiplexed_async_connection().await?; - - // create a transaction where 2 errors are returned. - // we call EVALSHA twice with no loaded script, thus triggering 2 errors. - redis::pipe() - .atomic() - .cmd("EVALSHA") - .arg("foobar") - .arg(0) - .cmd("EVALSHA") - .arg("foobar") - .arg(0) - .query_async::<_, ((), ())>(&mut conn) - .await - .expect_err("should return an error"); - - assert!( - // Arbitrary Redis command that should not return an error. - redis::cmd("SMEMBERS") - .arg("nonexistent_key") - .query_async::<_, Vec>(&mut conn) - .await - .is_ok(), - "Failed transaction should not interfere with future calls." - ); - - Ok::<_, redis::RedisError>(()) - }) - .unwrap() -} -#[cfg(feature = "connection-manager")] -async fn wait_for_server_to_become_ready(client: redis::Client) { - let millisecond = std::time::Duration::from_millis(1); - let mut retries = 0; - loop { - match client.get_multiplexed_async_connection().await { - Err(err) => { - if err.is_connection_refusal() { - tokio::time::sleep(millisecond).await; - retries += 1; - if retries > 100000 { - panic!("Tried to connect too many times, last error: {err}"); + #[cfg(feature = "connection-manager")] + async fn wait_for_server_to_become_ready(client: redis::Client) { + let millisecond = std::time::Duration::from_millis(1); + let mut retries = 0; + loop { + match client.get_multiplexed_async_connection().await { + Err(err) => { + if err.is_connection_refusal() { + tokio::time::sleep(millisecond).await; + retries += 1; + if retries > 100000 { + panic!("Tried to connect too many times, last error: {err}"); + } + } else { + panic!("Could not connect: {err}"); } - } else { - panic!("Could not connect: {err}"); } - } - Ok(mut con) => { - let _: RedisResult<()> = redis::cmd("FLUSHDB").query_async(&mut con).await; - break; + Ok(mut con) => { + let _: RedisResult<()> = redis::cmd("FLUSHDB").query_async(&mut con).await; + break; + } } } } -} - -#[test] -#[cfg(feature = "connection-manager")] -fn test_connection_manager_reconnect_after_delay() { - use redis::ProtocolVersion; - - let tempdir = tempfile::Builder::new() - .prefix("redis") - .tempdir() - .expect("failed to create tempdir"); - let tls_files = build_keys_and_certs_for_tls(&tempdir); - let ctx = TestContext::with_tls(tls_files.clone(), false); - block_on_all(async move { - let mut manager = redis::aio::ConnectionManager::new(ctx.client.clone()) - .await - .unwrap(); - let server = ctx.server; - let addr = server.client_addr().clone(); - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - manager.get_push_manager().replace_sender(tx.clone()); - drop(server); - - let _result: RedisResult = manager.set("foo", "bar").await; // one call is ignored because it's required to trigger the connection manager's reconnect. - if ctx.protocol != ProtocolVersion::RESP2 { - assert_eq!(rx.recv().await.unwrap().kind, PushKind::Disconnection); - } - tokio::time::sleep(std::time::Duration::from_millis(100)).await; + #[test] + #[cfg(feature = "connection-manager")] + fn test_connection_manager_reconnect_after_delay() { + use redis::ProtocolVersion; - let _new_server = RedisServer::new_with_addr_and_modules(addr.clone(), &[], false); - wait_for_server_to_become_ready(ctx.client.clone()).await; + let tempdir = tempfile::Builder::new() + .prefix("redis") + .tempdir() + .expect("failed to create tempdir"); + let tls_files = build_keys_and_certs_for_tls(&tempdir); - let result: redis::Value = manager.set("foo", "bar").await.unwrap(); - assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty); - assert_eq!(result, redis::Value::Okay); - Ok(()) - }) - .unwrap(); -} + let ctx = TestContext::with_tls(tls_files.clone(), false); + block_on_all(async move { + let mut manager = redis::aio::ConnectionManager::new(ctx.client.clone()) + .await + .unwrap(); + let server = ctx.server; + let addr = server.client_addr().clone(); + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + manager.get_push_manager().replace_sender(tx.clone()); + drop(server); -#[cfg(feature = "tls-rustls")] -mod mtls_test { - use super::*; + let _result: RedisResult = manager.set("foo", "bar").await; // one call is ignored because it's required to trigger the connection manager's reconnect. + if ctx.protocol != ProtocolVersion::RESP2 { + assert_eq!(rx.recv().await.unwrap().kind, PushKind::Disconnection); + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; - #[test] - fn test_should_connect_mtls() { - let ctx = TestContext::new_with_mtls(); + let _new_server = RedisServer::new_with_addr_and_modules(addr.clone(), &[], false); + wait_for_server_to_become_ready(ctx.client.clone()).await; - let client = - build_single_client(ctx.server.connection_info(), &ctx.server.tls_paths, true).unwrap(); - let connect = client.get_multiplexed_async_connection(); - block_on_all(connect.and_then(|mut con| async move { - redis::cmd("SET") - .arg("key1") - .arg(b"foo") - .query_async(&mut con) - .await?; - let result = redis::cmd("GET").arg(&["key1"]).query_async(&mut con).await; - assert_eq!(result, Ok("foo".to_string())); - result - })) + let result: redis::Value = manager.set("foo", "bar").await.unwrap(); + assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty); + assert_eq!(result, redis::Value::Okay); + Ok(()) + }) .unwrap(); } - #[test] - fn test_should_not_connect_if_tls_active() { - let ctx = TestContext::new_with_mtls(); + #[cfg(feature = "tls-rustls")] + mod mtls_test { + use super::*; + + #[test] + fn test_should_connect_mtls() { + let ctx = TestContext::new_with_mtls(); + + let client = + build_single_client(ctx.server.connection_info(), &ctx.server.tls_paths, true) + .unwrap(); + let connect = client.get_multiplexed_async_connection(); + block_on_all(connect.and_then(|mut con| async move { + redis::cmd("SET") + .arg("key1") + .arg(b"foo") + .query_async(&mut con) + .await?; + let result = redis::cmd("GET").arg(&["key1"]).query_async(&mut con).await; + assert_eq!(result, Ok("foo".to_string())); + result + })) + .unwrap(); + } - let client = - build_single_client(ctx.server.connection_info(), &ctx.server.tls_paths, false) - .unwrap(); - let connect = client.get_multiplexed_async_connection(); - let result = block_on_all(connect.and_then(|mut con| async move { - redis::cmd("SET") - .arg("key1") - .arg(b"foo") - .query_async(&mut con) - .await?; - let result = redis::cmd("GET").arg(&["key1"]).query_async(&mut con).await; - assert_eq!(result, Ok("foo".to_string())); - result - })); - - // depends on server type set (REDISRS_SERVER_TYPE) - match ctx.server.connection_info() { - redis::ConnectionInfo { - addr: redis::ConnectionAddr::TcpTls { .. }, - .. - } => { - if result.is_ok() { - panic!("Must NOT be able to connect without client credentials if server accepts TLS"); + #[test] + fn test_should_not_connect_if_tls_active() { + let ctx = TestContext::new_with_mtls(); + + let client = + build_single_client(ctx.server.connection_info(), &ctx.server.tls_paths, false) + .unwrap(); + let connect = client.get_multiplexed_async_connection(); + let result = block_on_all(connect.and_then(|mut con| async move { + redis::cmd("SET") + .arg("key1") + .arg(b"foo") + .query_async(&mut con) + .await?; + let result = redis::cmd("GET").arg(&["key1"]).query_async(&mut con).await; + assert_eq!(result, Ok("foo".to_string())); + result + })); + + // depends on server type set (REDISRS_SERVER_TYPE) + match ctx.server.connection_info() { + redis::ConnectionInfo { + addr: redis::ConnectionAddr::TcpTls { .. }, + .. + } => { + if result.is_ok() { + panic!("Must NOT be able to connect without client credentials if server accepts TLS"); + } } - } - _ => { - if result.is_err() { - panic!("Must be able to connect without client credentials if server does NOT accept TLS"); + _ => { + if result.is_err() { + panic!("Must be able to connect without client credentials if server does NOT accept TLS"); + } } } } } -} -#[test] -#[cfg(feature = "connection-manager")] -fn test_push_manager_cm() { - use redis::ProtocolVersion; + #[test] + #[cfg(feature = "connection-manager")] + fn test_push_manager_cm() { + use redis::ProtocolVersion; - let ctx = TestContext::new(); - if ctx.protocol == ProtocolVersion::RESP2 { - return; - } + let ctx = TestContext::new(); + if ctx.protocol == ProtocolVersion::RESP2 { + return; + } - block_on_all(async move { - let mut manager = redis::aio::ConnectionManager::new(ctx.client.clone()) - .await - .unwrap(); - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - manager.get_push_manager().replace_sender(tx.clone()); - manager - .send_packed_command(cmd("CLIENT").arg("TRACKING").arg("ON")) - .await - .unwrap(); - let pipe = build_simple_pipeline_for_invalidation(); - let _: RedisResult<()> = pipe.query_async(&mut manager).await; - let _: i32 = manager.get("key_1").await.unwrap(); - let PushInfo { kind, data } = rx.try_recv().unwrap(); - assert_eq!( - ( - PushKind::Invalidate, - vec![Value::Array(vec![Value::BulkString( - "key_1".as_bytes().to_vec() - )])] - ), - (kind, data) - ); - let (new_tx, mut new_rx) = tokio::sync::mpsc::unbounded_channel(); - manager.get_push_manager().replace_sender(new_tx); - drop(rx); - let _: RedisResult<()> = pipe.query_async(&mut manager).await; - let _: i32 = manager.get("key_1").await.unwrap(); - let PushInfo { kind, data } = new_rx.try_recv().unwrap(); - assert_eq!( - ( - PushKind::Invalidate, - vec![Value::Array(vec![Value::BulkString( - "key_1".as_bytes().to_vec() - )])] - ), - (kind, data) - ); - assert_eq!(TryRecvError::Empty, new_rx.try_recv().err().unwrap()); - Ok(()) - }) - .unwrap(); + block_on_all(async move { + let mut manager = redis::aio::ConnectionManager::new(ctx.client.clone()) + .await + .unwrap(); + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + manager.get_push_manager().replace_sender(tx.clone()); + manager + .send_packed_command(cmd("CLIENT").arg("TRACKING").arg("ON")) + .await + .unwrap(); + let pipe = build_simple_pipeline_for_invalidation(); + let _: RedisResult<()> = pipe.query_async(&mut manager).await; + let _: i32 = manager.get("key_1").await.unwrap(); + let PushInfo { kind, data } = rx.try_recv().unwrap(); + assert_eq!( + ( + PushKind::Invalidate, + vec![Value::Array(vec![Value::BulkString( + "key_1".as_bytes().to_vec() + )])] + ), + (kind, data) + ); + let (new_tx, mut new_rx) = tokio::sync::mpsc::unbounded_channel(); + manager.get_push_manager().replace_sender(new_tx); + drop(rx); + let _: RedisResult<()> = pipe.query_async(&mut manager).await; + let _: i32 = manager.get("key_1").await.unwrap(); + let PushInfo { kind, data } = new_rx.try_recv().unwrap(); + assert_eq!( + ( + PushKind::Invalidate, + vec![Value::Array(vec![Value::BulkString( + "key_1".as_bytes().to_vec() + )])] + ), + (kind, data) + ); + assert_eq!(TryRecvError::Empty, new_rx.try_recv().err().unwrap()); + Ok(()) + }) + .unwrap(); + } } diff --git a/redis/tests/test_basic.rs b/redis/tests/test_basic.rs index 213abe532..d9be937ff 100644 --- a/redis/tests/test_basic.rs +++ b/redis/tests/test_basic.rs @@ -1,1505 +1,1523 @@ #![allow(clippy::let_unit_value)] -use redis::{cmd, ProtocolVersion, PushInfo}; -use redis::{ - Commands, ConnectionInfo, ConnectionLike, ControlFlow, ErrorKind, ExistenceCheck, Expiry, - PubSubCommands, PushKind, RedisResult, SetExpiry, SetOptions, ToRedisArgs, Value, -}; -use std::collections::{BTreeMap, BTreeSet}; -use std::collections::{HashMap, HashSet}; -use std::thread::{sleep, spawn}; -use std::time::Duration; -use std::vec; -use tokio::sync::mpsc::error::TryRecvError; - -use crate::support::*; - mod support; -#[test] -fn test_parse_redis_url() { - let redis_url = "redis://127.0.0.1:1234/0".to_string(); - redis::parse_redis_url(&redis_url).unwrap(); - redis::parse_redis_url("unix:/var/run/redis/redis.sock").unwrap(); - assert!(redis::parse_redis_url("127.0.0.1").is_none()); -} +#[cfg(test)] +mod basic { + use redis::{cmd, ProtocolVersion, PushInfo}; + use redis::{ + Commands, ConnectionInfo, ConnectionLike, ControlFlow, ErrorKind, ExistenceCheck, Expiry, + PubSubCommands, PushKind, RedisResult, SetExpiry, SetOptions, ToRedisArgs, Value, + }; + use std::collections::{BTreeMap, BTreeSet}; + use std::collections::{HashMap, HashSet}; + use std::thread::{sleep, spawn}; + use std::time::Duration; + use std::vec; + use tokio::sync::mpsc::error::TryRecvError; + + use crate::{assert_args, support::*}; + + #[test] + fn test_parse_redis_url() { + let redis_url = "redis://127.0.0.1:1234/0".to_string(); + redis::parse_redis_url(&redis_url).unwrap(); + redis::parse_redis_url("unix:/var/run/redis/redis.sock").unwrap(); + assert!(redis::parse_redis_url("127.0.0.1").is_none()); + } -#[test] -fn test_redis_url_fromstr() { - let _info: ConnectionInfo = "redis://127.0.0.1:1234/0".parse().unwrap(); -} + #[test] + fn test_redis_url_fromstr() { + let _info: ConnectionInfo = "redis://127.0.0.1:1234/0".parse().unwrap(); + } -#[test] -fn test_args() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + #[test] + fn test_args() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - redis::cmd("SET").arg("key1").arg(b"foo").execute(&mut con); - redis::cmd("SET").arg(&["key2", "bar"]).execute(&mut con); + redis::cmd("SET").arg("key1").arg(b"foo").execute(&mut con); + redis::cmd("SET").arg(&["key2", "bar"]).execute(&mut con); - assert_eq!( - redis::cmd("MGET").arg(&["key1", "key2"]).query(&mut con), - Ok(("foo".to_string(), b"bar".to_vec())) - ); -} + assert_eq!( + redis::cmd("MGET").arg(&["key1", "key2"]).query(&mut con), + Ok(("foo".to_string(), b"bar".to_vec())) + ); + } -#[test] -fn test_getset() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + #[test] + fn test_getset() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - redis::cmd("SET").arg("foo").arg(42).execute(&mut con); - assert_eq!(redis::cmd("GET").arg("foo").query(&mut con), Ok(42)); + redis::cmd("SET").arg("foo").arg(42).execute(&mut con); + assert_eq!(redis::cmd("GET").arg("foo").query(&mut con), Ok(42)); - redis::cmd("SET").arg("bar").arg("foo").execute(&mut con); - assert_eq!( - redis::cmd("GET").arg("bar").query(&mut con), - Ok(b"foo".to_vec()) - ); -} + redis::cmd("SET").arg("bar").arg("foo").execute(&mut con); + assert_eq!( + redis::cmd("GET").arg("bar").query(&mut con), + Ok(b"foo".to_vec()) + ); + } -//unit test for key_type function -#[test] -fn test_key_type() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - - //The key is a simple value - redis::cmd("SET").arg("foo").arg(42).execute(&mut con); - let string_key_type: String = con.key_type("foo").unwrap(); - assert_eq!(string_key_type, "string"); - - //The key is a list - redis::cmd("LPUSH") - .arg("list_bar") - .arg("foo") - .execute(&mut con); - let list_key_type: String = con.key_type("list_bar").unwrap(); - assert_eq!(list_key_type, "list"); - - //The key is a set - redis::cmd("SADD") - .arg("set_bar") - .arg("foo") - .execute(&mut con); - let set_key_type: String = con.key_type("set_bar").unwrap(); - assert_eq!(set_key_type, "set"); - - //The key is a sorted set - redis::cmd("ZADD") - .arg("sorted_set_bar") - .arg("1") - .arg("foo") - .execute(&mut con); - let zset_key_type: String = con.key_type("sorted_set_bar").unwrap(); - assert_eq!(zset_key_type, "zset"); - - //The key is a hash - redis::cmd("HSET") - .arg("hset_bar") - .arg("hset_key_1") - .arg("foo") - .execute(&mut con); - let hash_key_type: String = con.key_type("hset_bar").unwrap(); - assert_eq!(hash_key_type, "hash"); -} + //unit test for key_type function + #[test] + fn test_key_type() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); -#[test] -fn test_client_tracking_doesnt_block_execution() { - //It checks if the library distinguish a push-type message from the others and continues its normal operation. - let ctx = TestContext::new(); - let mut con = ctx.connection(); - let (k1, k2): (i32, i32) = redis::pipe() - .cmd("CLIENT") - .arg("TRACKING") - .arg("ON") - .ignore() - .cmd("GET") - .arg("key_1") - .ignore() - .cmd("SET") - .arg("key_1") - .arg(42) - .ignore() - .cmd("SET") - .arg("key_2") - .arg(43) - .ignore() - .cmd("GET") - .arg("key_1") - .cmd("GET") - .arg("key_2") - .cmd("SET") - .arg("key_1") - .arg(45) - .ignore() - .query(&mut con) - .unwrap(); - assert_eq!(k1, 42); - assert_eq!(k2, 43); - let num: i32 = con.get("key_1").unwrap(); - assert_eq!(num, 45); -} + //The key is a simple value + redis::cmd("SET").arg("foo").arg(42).execute(&mut con); + let string_key_type: String = con.key_type("foo").unwrap(); + assert_eq!(string_key_type, "string"); -#[test] -fn test_incr() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + //The key is a list + redis::cmd("LPUSH") + .arg("list_bar") + .arg("foo") + .execute(&mut con); + let list_key_type: String = con.key_type("list_bar").unwrap(); + assert_eq!(list_key_type, "list"); - redis::cmd("SET").arg("foo").arg(42).execute(&mut con); - assert_eq!(redis::cmd("INCR").arg("foo").query(&mut con), Ok(43usize)); -} + //The key is a set + redis::cmd("SADD") + .arg("set_bar") + .arg("foo") + .execute(&mut con); + let set_key_type: String = con.key_type("set_bar").unwrap(); + assert_eq!(set_key_type, "set"); + + //The key is a sorted set + redis::cmd("ZADD") + .arg("sorted_set_bar") + .arg("1") + .arg("foo") + .execute(&mut con); + let zset_key_type: String = con.key_type("sorted_set_bar").unwrap(); + assert_eq!(zset_key_type, "zset"); + + //The key is a hash + redis::cmd("HSET") + .arg("hset_bar") + .arg("hset_key_1") + .arg("foo") + .execute(&mut con); + let hash_key_type: String = con.key_type("hset_bar").unwrap(); + assert_eq!(hash_key_type, "hash"); + } -#[test] -fn test_getdel() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + #[test] + fn test_client_tracking_doesnt_block_execution() { + //It checks if the library distinguish a push-type message from the others and continues its normal operation. + let ctx = TestContext::new(); + let mut con = ctx.connection(); + let (k1, k2): (i32, i32) = redis::pipe() + .cmd("CLIENT") + .arg("TRACKING") + .arg("ON") + .ignore() + .cmd("GET") + .arg("key_1") + .ignore() + .cmd("SET") + .arg("key_1") + .arg(42) + .ignore() + .cmd("SET") + .arg("key_2") + .arg(43) + .ignore() + .cmd("GET") + .arg("key_1") + .cmd("GET") + .arg("key_2") + .cmd("SET") + .arg("key_1") + .arg(45) + .ignore() + .query(&mut con) + .unwrap(); + assert_eq!(k1, 42); + assert_eq!(k2, 43); + let num: i32 = con.get("key_1").unwrap(); + assert_eq!(num, 45); + } - redis::cmd("SET").arg("foo").arg(42).execute(&mut con); + #[test] + fn test_incr() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - assert_eq!(con.get_del("foo"), Ok(42usize)); + redis::cmd("SET").arg("foo").arg(42).execute(&mut con); + assert_eq!(redis::cmd("INCR").arg("foo").query(&mut con), Ok(43usize)); + } - assert_eq!( - redis::cmd("GET").arg("foo").query(&mut con), - Ok(None::) - ); -} + #[test] + fn test_getdel() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); -#[test] -fn test_getex() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + redis::cmd("SET").arg("foo").arg(42).execute(&mut con); - redis::cmd("SET").arg("foo").arg(42usize).execute(&mut con); + assert_eq!(con.get_del("foo"), Ok(42usize)); - // Return of get_ex must match set value - let ret_value = con.get_ex::<_, usize>("foo", Expiry::EX(1)).unwrap(); - assert_eq!(ret_value, 42usize); + assert_eq!( + redis::cmd("GET").arg("foo").query(&mut con), + Ok(None::) + ); + } - // Get before expiry time must also return value - sleep(Duration::from_millis(100)); - let delayed_get = con.get::<_, usize>("foo").unwrap(); - assert_eq!(delayed_get, 42usize); + #[test] + fn test_getex() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - // Get after expiry time mustn't return value - sleep(Duration::from_secs(1)); - let after_expire_get = con.get::<_, Option>("foo").unwrap(); - assert_eq!(after_expire_get, None); + redis::cmd("SET").arg("foo").arg(42usize).execute(&mut con); - // Persist option test prep - redis::cmd("SET").arg("foo").arg(420usize).execute(&mut con); + // Return of get_ex must match set value + let ret_value = con.get_ex::<_, usize>("foo", Expiry::EX(1)).unwrap(); + assert_eq!(ret_value, 42usize); - // Return of get_ex with persist option must match set value - let ret_value = con.get_ex::<_, usize>("foo", Expiry::PERSIST).unwrap(); - assert_eq!(ret_value, 420usize); + // Get before expiry time must also return value + sleep(Duration::from_millis(100)); + let delayed_get = con.get::<_, usize>("foo").unwrap(); + assert_eq!(delayed_get, 42usize); - // Get after persist get_ex must return value - sleep(Duration::from_millis(200)); - let delayed_get = con.get::<_, usize>("foo").unwrap(); - assert_eq!(delayed_get, 420usize); -} + // Get after expiry time mustn't return value + sleep(Duration::from_secs(1)); + let after_expire_get = con.get::<_, Option>("foo").unwrap(); + assert_eq!(after_expire_get, None); -#[test] -fn test_info() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - - let info: redis::InfoDict = redis::cmd("INFO").query(&mut con).unwrap(); - assert_eq!( - info.find(&"role"), - Some(&redis::Value::SimpleString("master".to_string())) - ); - assert_eq!(info.get("role"), Some("master".to_string())); - assert_eq!(info.get("loading"), Some(false)); - assert!(!info.is_empty()); - assert!(info.contains_key(&"role")); -} + // Persist option test prep + redis::cmd("SET").arg("foo").arg(420usize).execute(&mut con); -#[test] -fn test_hash_ops() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - - redis::cmd("HSET") - .arg("foo") - .arg("key_1") - .arg(1) - .execute(&mut con); - redis::cmd("HSET") - .arg("foo") - .arg("key_2") - .arg(2) - .execute(&mut con); - - let h: HashMap = redis::cmd("HGETALL").arg("foo").query(&mut con).unwrap(); - assert_eq!(h.len(), 2); - assert_eq!(h.get("key_1"), Some(&1i32)); - assert_eq!(h.get("key_2"), Some(&2i32)); - - let h: BTreeMap = redis::cmd("HGETALL").arg("foo").query(&mut con).unwrap(); - assert_eq!(h.len(), 2); - assert_eq!(h.get("key_1"), Some(&1i32)); - assert_eq!(h.get("key_2"), Some(&2i32)); -} + // Return of get_ex with persist option must match set value + let ret_value = con.get_ex::<_, usize>("foo", Expiry::PERSIST).unwrap(); + assert_eq!(ret_value, 420usize); -// Requires redis-server >= 4.0.0. -// Not supported with the current appveyor/windows binary deployed. -#[cfg(not(target_os = "windows"))] -#[test] -fn test_unlink() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - - redis::cmd("SET").arg("foo").arg(42).execute(&mut con); - assert_eq!(redis::cmd("GET").arg("foo").query(&mut con), Ok(42)); - assert_eq!(con.unlink("foo"), Ok(1)); - - redis::cmd("SET").arg("foo").arg(42).execute(&mut con); - redis::cmd("SET").arg("bar").arg(42).execute(&mut con); - assert_eq!(con.unlink(&["foo", "bar"]), Ok(2)); -} + // Get after persist get_ex must return value + sleep(Duration::from_millis(200)); + let delayed_get = con.get::<_, usize>("foo").unwrap(); + assert_eq!(delayed_get, 420usize); + } -#[test] -fn test_set_ops() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - - assert_eq!(con.sadd("foo", &[1, 2, 3]), Ok(3)); - - let mut s: Vec = con.smembers("foo").unwrap(); - s.sort_unstable(); - assert_eq!(s.len(), 3); - assert_eq!(&s, &[1, 2, 3]); - - let set: HashSet = con.smembers("foo").unwrap(); - assert_eq!(set.len(), 3); - assert!(set.contains(&1i32)); - assert!(set.contains(&2i32)); - assert!(set.contains(&3i32)); - - let set: BTreeSet = con.smembers("foo").unwrap(); - assert_eq!(set.len(), 3); - assert!(set.contains(&1i32)); - assert!(set.contains(&2i32)); - assert!(set.contains(&3i32)); -} + #[test] + fn test_info() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); -#[test] -fn test_scan() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + let info: redis::InfoDict = redis::cmd("INFO").query(&mut con).unwrap(); + assert_eq!( + info.find(&"role"), + Some(&redis::Value::SimpleString("master".to_string())) + ); + assert_eq!(info.get("role"), Some("master".to_string())); + assert_eq!(info.get("loading"), Some(false)); + assert!(!info.is_empty()); + assert!(info.contains_key(&"role")); + } - assert_eq!(con.sadd("foo", &[1, 2, 3]), Ok(3)); + #[test] + fn test_hash_ops() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - let (cur, mut s): (i32, Vec) = redis::cmd("SSCAN") - .arg("foo") - .arg(0) - .query(&mut con) - .unwrap(); - s.sort_unstable(); - assert_eq!(cur, 0i32); - assert_eq!(s.len(), 3); - assert_eq!(&s, &[1, 2, 3]); -} + redis::cmd("HSET") + .arg("foo") + .arg("key_1") + .arg(1) + .execute(&mut con); + redis::cmd("HSET") + .arg("foo") + .arg("key_2") + .arg(2) + .execute(&mut con); + + let h: HashMap = redis::cmd("HGETALL").arg("foo").query(&mut con).unwrap(); + assert_eq!(h.len(), 2); + assert_eq!(h.get("key_1"), Some(&1i32)); + assert_eq!(h.get("key_2"), Some(&2i32)); + + let h: BTreeMap = redis::cmd("HGETALL").arg("foo").query(&mut con).unwrap(); + assert_eq!(h.len(), 2); + assert_eq!(h.get("key_1"), Some(&1i32)); + assert_eq!(h.get("key_2"), Some(&2i32)); + } -#[test] -fn test_optionals() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + // Requires redis-server >= 4.0.0. + // Not supported with the current appveyor/windows binary deployed. + #[cfg(not(target_os = "windows"))] + #[test] + fn test_unlink() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); + + redis::cmd("SET").arg("foo").arg(42).execute(&mut con); + assert_eq!(redis::cmd("GET").arg("foo").query(&mut con), Ok(42)); + assert_eq!(con.unlink("foo"), Ok(1)); + + redis::cmd("SET").arg("foo").arg(42).execute(&mut con); + redis::cmd("SET").arg("bar").arg(42).execute(&mut con); + assert_eq!(con.unlink(&["foo", "bar"]), Ok(2)); + } - redis::cmd("SET").arg("foo").arg(1).execute(&mut con); + #[test] + fn test_set_ops() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); + + assert_eq!(con.sadd("foo", &[1, 2, 3]), Ok(3)); + + let mut s: Vec = con.smembers("foo").unwrap(); + s.sort_unstable(); + assert_eq!(s.len(), 3); + assert_eq!(&s, &[1, 2, 3]); + + let set: HashSet = con.smembers("foo").unwrap(); + assert_eq!(set.len(), 3); + assert!(set.contains(&1i32)); + assert!(set.contains(&2i32)); + assert!(set.contains(&3i32)); + + let set: BTreeSet = con.smembers("foo").unwrap(); + assert_eq!(set.len(), 3); + assert!(set.contains(&1i32)); + assert!(set.contains(&2i32)); + assert!(set.contains(&3i32)); + } - let (a, b): (Option, Option) = redis::cmd("MGET") - .arg("foo") - .arg("missing") - .query(&mut con) - .unwrap(); - assert_eq!(a, Some(1i32)); - assert_eq!(b, None); - - let a = redis::cmd("GET") - .arg("missing") - .query(&mut con) - .unwrap_or(0i32); - assert_eq!(a, 0i32); -} + #[test] + fn test_scan() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); -#[test] -fn test_scanning() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - let mut unseen = HashSet::new(); + assert_eq!(con.sadd("foo", &[1, 2, 3]), Ok(3)); - for x in 0..1000 { - redis::cmd("SADD").arg("foo").arg(x).execute(&mut con); - unseen.insert(x); + let (cur, mut s): (i32, Vec) = redis::cmd("SSCAN") + .arg("foo") + .arg(0) + .query(&mut con) + .unwrap(); + s.sort_unstable(); + assert_eq!(cur, 0i32); + assert_eq!(s.len(), 3); + assert_eq!(&s, &[1, 2, 3]); } - let iter = redis::cmd("SSCAN") - .arg("foo") - .cursor_arg(0) - .clone() - .iter(&mut con) - .unwrap(); + #[test] + fn test_optionals() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); + + redis::cmd("SET").arg("foo").arg(1).execute(&mut con); + + let (a, b): (Option, Option) = redis::cmd("MGET") + .arg("foo") + .arg("missing") + .query(&mut con) + .unwrap(); + assert_eq!(a, Some(1i32)); + assert_eq!(b, None); - for x in iter { - // type inference limitations - let x: usize = x; - unseen.remove(&x); + let a = redis::cmd("GET") + .arg("missing") + .query(&mut con) + .unwrap_or(0i32); + assert_eq!(a, 0i32); } - assert_eq!(unseen.len(), 0); -} + #[test] + fn test_scanning() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); + let mut unseen = HashSet::new(); -#[test] -fn test_filtered_scanning() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - let mut unseen = HashSet::new(); + for x in 0..1000 { + redis::cmd("SADD").arg("foo").arg(x).execute(&mut con); + unseen.insert(x); + } - for x in 0..3000 { - let _: () = con - .hset("foo", format!("key_{}_{}", x % 100, x), x) + let iter = redis::cmd("SSCAN") + .arg("foo") + .cursor_arg(0) + .clone() + .iter(&mut con) .unwrap(); - if x % 100 == 0 { - unseen.insert(x); + + for x in iter { + // type inference limitations + let x: usize = x; + unseen.remove(&x); } + + assert_eq!(unseen.len(), 0); } - let iter = con - .hscan_match::<&str, &str, (String, usize)>("foo", "key_0_*") - .unwrap(); + #[test] + fn test_filtered_scanning() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); + let mut unseen = HashSet::new(); + + for x in 0..3000 { + let _: () = con + .hset("foo", format!("key_{}_{}", x % 100, x), x) + .unwrap(); + if x % 100 == 0 { + unseen.insert(x); + } + } + + let iter = con + .hscan_match::<&str, &str, (String, usize)>("foo", "key_0_*") + .unwrap(); + + for (_field, value) in iter { + unseen.remove(&value); + } - for (_field, value) in iter { - unseen.remove(&value); + assert_eq!(unseen.len(), 0); } - assert_eq!(unseen.len(), 0); -} + #[test] + fn test_pipeline() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); -#[test] -fn test_pipeline() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - - let ((k1, k2),): ((i32, i32),) = redis::pipe() - .cmd("SET") - .arg("key_1") - .arg(42) - .ignore() - .cmd("SET") - .arg("key_2") - .arg(43) - .ignore() - .cmd("MGET") - .arg(&["key_1", "key_2"]) - .query(&mut con) - .unwrap(); + let ((k1, k2),): ((i32, i32),) = redis::pipe() + .cmd("SET") + .arg("key_1") + .arg(42) + .ignore() + .cmd("SET") + .arg("key_2") + .arg(43) + .ignore() + .cmd("MGET") + .arg(&["key_1", "key_2"]) + .query(&mut con) + .unwrap(); - assert_eq!(k1, 42); - assert_eq!(k2, 43); -} + assert_eq!(k1, 42); + assert_eq!(k2, 43); + } -#[test] -fn test_pipeline_with_err() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + #[test] + fn test_pipeline_with_err() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - let _: () = redis::cmd("SET") - .arg("x") - .arg("x-value") - .query(&mut con) - .unwrap(); - let _: () = redis::cmd("SET") - .arg("y") - .arg("y-value") - .query(&mut con) - .unwrap(); + let _: () = redis::cmd("SET") + .arg("x") + .arg("x-value") + .query(&mut con) + .unwrap(); + let _: () = redis::cmd("SET") + .arg("y") + .arg("y-value") + .query(&mut con) + .unwrap(); - let _: () = redis::cmd("SLAVEOF") - .arg("1.1.1.1") - .arg("99") - .query(&mut con) - .unwrap(); + let _: () = redis::cmd("SLAVEOF") + .arg("1.1.1.1") + .arg("99") + .query(&mut con) + .unwrap(); - let res = redis::pipe() - .set("x", "another-x-value") - .ignore() - .get("y") - .query::<()>(&mut con); - assert!(res.is_err() && res.unwrap_err().kind() == ErrorKind::ReadOnly); - - // Make sure we don't get leftover responses from the pipeline ("y-value"). See #436. - let res = redis::cmd("GET") - .arg("x") - .query::(&mut con) - .unwrap(); - assert_eq!(res, "x-value"); -} + let res = redis::pipe() + .set("x", "another-x-value") + .ignore() + .get("y") + .query::<()>(&mut con); + assert!(res.is_err() && res.unwrap_err().kind() == ErrorKind::ReadOnly); + + // Make sure we don't get leftover responses from the pipeline ("y-value"). See #436. + let res = redis::cmd("GET") + .arg("x") + .query::(&mut con) + .unwrap(); + assert_eq!(res, "x-value"); + } -#[test] -fn test_empty_pipeline() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + #[test] + fn test_empty_pipeline() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - let _: () = redis::pipe().cmd("PING").ignore().query(&mut con).unwrap(); + let _: () = redis::pipe().cmd("PING").ignore().query(&mut con).unwrap(); - let _: () = redis::pipe().query(&mut con).unwrap(); -} + let _: () = redis::pipe().query(&mut con).unwrap(); + } -#[test] -fn test_pipeline_transaction() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - - let ((k1, k2),): ((i32, i32),) = redis::pipe() - .atomic() - .cmd("SET") - .arg("key_1") - .arg(42) - .ignore() - .cmd("SET") - .arg("key_2") - .arg(43) - .ignore() - .cmd("MGET") - .arg(&["key_1", "key_2"]) - .query(&mut con) - .unwrap(); + #[test] + fn test_pipeline_transaction() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - assert_eq!(k1, 42); - assert_eq!(k2, 43); -} + let ((k1, k2),): ((i32, i32),) = redis::pipe() + .atomic() + .cmd("SET") + .arg("key_1") + .arg(42) + .ignore() + .cmd("SET") + .arg("key_2") + .arg(43) + .ignore() + .cmd("MGET") + .arg(&["key_1", "key_2"]) + .query(&mut con) + .unwrap(); -#[test] -fn test_pipeline_transaction_with_errors() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + assert_eq!(k1, 42); + assert_eq!(k2, 43); + } - let _: () = con.set("x", 42).unwrap(); + #[test] + fn test_pipeline_transaction_with_errors() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - // Make Redis a replica of a nonexistent master, thereby making it read-only. - let _: () = redis::cmd("slaveof") - .arg("1.1.1.1") - .arg("1") - .query(&mut con) - .unwrap(); + let _: () = con.set("x", 42).unwrap(); - // Ensure that a write command fails with a READONLY error - let err: RedisResult<()> = redis::pipe() - .atomic() - .set("x", 142) - .ignore() - .get("x") - .query(&mut con); + // Make Redis a replica of a nonexistent master, thereby making it read-only. + let _: () = redis::cmd("slaveof") + .arg("1.1.1.1") + .arg("1") + .query(&mut con) + .unwrap(); - assert_eq!(err.unwrap_err().kind(), ErrorKind::ReadOnly); + // Ensure that a write command fails with a READONLY error + let err: RedisResult<()> = redis::pipe() + .atomic() + .set("x", 142) + .ignore() + .get("x") + .query(&mut con); - let x: i32 = con.get("x").unwrap(); - assert_eq!(x, 42); -} + assert_eq!(err.unwrap_err().kind(), ErrorKind::ReadOnly); -#[test] -fn test_pipeline_reuse_query() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - - let mut pl = redis::pipe(); - - let ((k1,),): ((i32,),) = pl - .cmd("SET") - .arg("pkey_1") - .arg(42) - .ignore() - .cmd("MGET") - .arg(&["pkey_1"]) - .query(&mut con) - .unwrap(); + let x: i32 = con.get("x").unwrap(); + assert_eq!(x, 42); + } - assert_eq!(k1, 42); + #[test] + fn test_pipeline_reuse_query() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - redis::cmd("DEL").arg("pkey_1").execute(&mut con); + let mut pl = redis::pipe(); - // The internal commands vector of the pipeline still contains the previous commands. - let ((k1,), (k2, k3)): ((i32,), (i32, i32)) = pl - .cmd("SET") - .arg("pkey_2") - .arg(43) - .ignore() - .cmd("MGET") - .arg(&["pkey_1"]) - .arg(&["pkey_2"]) - .query(&mut con) - .unwrap(); + let ((k1,),): ((i32,),) = pl + .cmd("SET") + .arg("pkey_1") + .arg(42) + .ignore() + .cmd("MGET") + .arg(&["pkey_1"]) + .query(&mut con) + .unwrap(); - assert_eq!(k1, 42); - assert_eq!(k2, 42); - assert_eq!(k3, 43); -} + assert_eq!(k1, 42); -#[test] -fn test_pipeline_reuse_query_clear() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - - let mut pl = redis::pipe(); - - let ((k1,),): ((i32,),) = pl - .cmd("SET") - .arg("pkey_1") - .arg(44) - .ignore() - .cmd("MGET") - .arg(&["pkey_1"]) - .query(&mut con) - .unwrap(); - pl.clear(); + redis::cmd("DEL").arg("pkey_1").execute(&mut con); - assert_eq!(k1, 44); + // The internal commands vector of the pipeline still contains the previous commands. + let ((k1,), (k2, k3)): ((i32,), (i32, i32)) = pl + .cmd("SET") + .arg("pkey_2") + .arg(43) + .ignore() + .cmd("MGET") + .arg(&["pkey_1"]) + .arg(&["pkey_2"]) + .query(&mut con) + .unwrap(); - redis::cmd("DEL").arg("pkey_1").execute(&mut con); + assert_eq!(k1, 42); + assert_eq!(k2, 42); + assert_eq!(k3, 43); + } - let ((k1, k2),): ((bool, i32),) = pl - .cmd("SET") - .arg("pkey_2") - .arg(45) - .ignore() - .cmd("MGET") - .arg(&["pkey_1"]) - .arg(&["pkey_2"]) - .query(&mut con) - .unwrap(); - pl.clear(); + #[test] + fn test_pipeline_reuse_query_clear() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - assert!(!k1); - assert_eq!(k2, 45); -} + let mut pl = redis::pipe(); + + let ((k1,),): ((i32,),) = pl + .cmd("SET") + .arg("pkey_1") + .arg(44) + .ignore() + .cmd("MGET") + .arg(&["pkey_1"]) + .query(&mut con) + .unwrap(); + pl.clear(); -#[test] -fn test_real_transaction() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + assert_eq!(k1, 44); - let key = "the_key"; - let _: () = redis::cmd("SET").arg(key).arg(42).query(&mut con).unwrap(); + redis::cmd("DEL").arg("pkey_1").execute(&mut con); - loop { - let _: () = redis::cmd("WATCH").arg(key).query(&mut con).unwrap(); - let val: isize = redis::cmd("GET").arg(key).query(&mut con).unwrap(); - let response: Option<(isize,)> = redis::pipe() - .atomic() + let ((k1, k2),): ((bool, i32),) = pl .cmd("SET") - .arg(key) - .arg(val + 1) + .arg("pkey_2") + .arg(45) .ignore() - .cmd("GET") - .arg(key) + .cmd("MGET") + .arg(&["pkey_1"]) + .arg(&["pkey_2"]) .query(&mut con) .unwrap(); + pl.clear(); - match response { - None => { - continue; - } - Some(response) => { - assert_eq!(response, (43,)); - break; - } - } + assert!(!k1); + assert_eq!(k2, 45); } -} -#[test] -fn test_real_transaction_highlevel() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + #[test] + fn test_real_transaction() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - let key = "the_key"; - let _: () = redis::cmd("SET").arg(key).arg(42).query(&mut con).unwrap(); + let key = "the_key"; + let _: () = redis::cmd("SET").arg(key).arg(42).query(&mut con).unwrap(); - let response: (isize,) = redis::transaction(&mut con, &[key], |con, pipe| { - let val: isize = redis::cmd("GET").arg(key).query(con)?; - pipe.cmd("SET") - .arg(key) - .arg(val + 1) - .ignore() - .cmd("GET") - .arg(key) - .query(con) - }) - .unwrap(); + loop { + let _: () = redis::cmd("WATCH").arg(key).query(&mut con).unwrap(); + let val: isize = redis::cmd("GET").arg(key).query(&mut con).unwrap(); + let response: Option<(isize,)> = redis::pipe() + .atomic() + .cmd("SET") + .arg(key) + .arg(val + 1) + .ignore() + .cmd("GET") + .arg(key) + .query(&mut con) + .unwrap(); + + match response { + None => { + continue; + } + Some(response) => { + assert_eq!(response, (43,)); + break; + } + } + } + } - assert_eq!(response, (43,)); -} + #[test] + fn test_real_transaction_highlevel() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); + + let key = "the_key"; + let _: () = redis::cmd("SET").arg(key).arg(42).query(&mut con).unwrap(); + + let response: (isize,) = redis::transaction(&mut con, &[key], |con, pipe| { + let val: isize = redis::cmd("GET").arg(key).query(con)?; + pipe.cmd("SET") + .arg(key) + .arg(val + 1) + .ignore() + .cmd("GET") + .arg(key) + .query(con) + }) + .unwrap(); -#[test] -fn test_pubsub() { - use std::sync::{Arc, Barrier}; - let ctx = TestContext::new(); - let mut con = ctx.connection(); + assert_eq!(response, (43,)); + } - // Connection for subscriber api - let mut pubsub_con = ctx.connection(); + #[test] + fn test_pubsub() { + use std::sync::{Arc, Barrier}; + let ctx = TestContext::new(); + let mut con = ctx.connection(); - // Barrier is used to make test thread wait to publish - // until after the pubsub thread has subscribed. - let barrier = Arc::new(Barrier::new(2)); - let pubsub_barrier = barrier.clone(); + // Connection for subscriber api + let mut pubsub_con = ctx.connection(); - let thread = spawn(move || { - let mut pubsub = pubsub_con.as_pubsub(); - pubsub.subscribe("foo").unwrap(); + // Barrier is used to make test thread wait to publish + // until after the pubsub thread has subscribed. + let barrier = Arc::new(Barrier::new(2)); + let pubsub_barrier = barrier.clone(); - let _ = pubsub_barrier.wait(); + let thread = spawn(move || { + let mut pubsub = pubsub_con.as_pubsub(); + pubsub.subscribe("foo").unwrap(); - let msg = pubsub.get_message().unwrap(); - assert_eq!(msg.get_channel(), Ok("foo".to_string())); - assert_eq!(msg.get_payload(), Ok(42)); + let _ = pubsub_barrier.wait(); - let msg = pubsub.get_message().unwrap(); - assert_eq!(msg.get_channel(), Ok("foo".to_string())); - assert_eq!(msg.get_payload(), Ok(23)); - }); + let msg = pubsub.get_message().unwrap(); + assert_eq!(msg.get_channel(), Ok("foo".to_string())); + assert_eq!(msg.get_payload(), Ok(42)); - let _ = barrier.wait(); - redis::cmd("PUBLISH").arg("foo").arg(42).execute(&mut con); - // We can also call the command directly - assert_eq!(con.publish("foo", 23), Ok(1)); + let msg = pubsub.get_message().unwrap(); + assert_eq!(msg.get_channel(), Ok("foo".to_string())); + assert_eq!(msg.get_payload(), Ok(23)); + }); - thread.join().expect("Something went wrong"); -} + let _ = barrier.wait(); + redis::cmd("PUBLISH").arg("foo").arg(42).execute(&mut con); + // We can also call the command directly + assert_eq!(con.publish("foo", 23), Ok(1)); -#[test] -fn test_pubsub_unsubscribe() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - - { - let mut pubsub = con.as_pubsub(); - pubsub.subscribe("foo").unwrap(); - pubsub.subscribe("bar").unwrap(); - pubsub.subscribe("baz").unwrap(); - pubsub.psubscribe("foo*").unwrap(); - pubsub.psubscribe("bar*").unwrap(); - pubsub.psubscribe("baz*").unwrap(); + thread.join().expect("Something went wrong"); } - // Connection should be usable again for non-pubsub commands - let _: redis::Value = con.set("foo", "bar").unwrap(); - let value: String = con.get("foo").unwrap(); - assert_eq!(&value[..], "bar"); -} + #[test] + fn test_pubsub_unsubscribe() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); + + { + let mut pubsub = con.as_pubsub(); + pubsub.subscribe("foo").unwrap(); + pubsub.subscribe("bar").unwrap(); + pubsub.subscribe("baz").unwrap(); + pubsub.psubscribe("foo*").unwrap(); + pubsub.psubscribe("bar*").unwrap(); + pubsub.psubscribe("baz*").unwrap(); + } -#[test] -fn test_pubsub_subscribe_while_messages_are_sent() { - let ctx = TestContext::new(); - let mut conn_external = ctx.connection(); - let mut conn_internal = ctx.connection(); - let received = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); - let received_clone = received.clone(); - let (sender, receiver) = std::sync::mpsc::channel(); - // receive message from foo channel - let thread = std::thread::spawn(move || { - let mut pubsub = conn_internal.as_pubsub(); - pubsub.subscribe("foo").unwrap(); - sender.send(()).unwrap(); - loop { - let msg = pubsub.get_message().unwrap(); - let channel = msg.get_channel_name(); - let content: i32 = msg.get_payload().unwrap(); - received - .lock() - .unwrap() - .push(format!("{channel}:{content}")); - if content == -1 { - return; - } - if content == 5 { - // subscribe bar channel using the same pubsub - pubsub.subscribe("bar").unwrap(); - sender.send(()).unwrap(); + // Connection should be usable again for non-pubsub commands + let _: redis::Value = con.set("foo", "bar").unwrap(); + let value: String = con.get("foo").unwrap(); + assert_eq!(&value[..], "bar"); + } + + #[test] + fn test_pubsub_subscribe_while_messages_are_sent() { + let ctx = TestContext::new(); + let mut conn_external = ctx.connection(); + let mut conn_internal = ctx.connection(); + let received = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); + let received_clone = received.clone(); + let (sender, receiver) = std::sync::mpsc::channel(); + // receive message from foo channel + let thread = std::thread::spawn(move || { + let mut pubsub = conn_internal.as_pubsub(); + pubsub.subscribe("foo").unwrap(); + sender.send(()).unwrap(); + loop { + let msg = pubsub.get_message().unwrap(); + let channel = msg.get_channel_name(); + let content: i32 = msg.get_payload().unwrap(); + received + .lock() + .unwrap() + .push(format!("{channel}:{content}")); + if content == -1 { + return; + } + if content == 5 { + // subscribe bar channel using the same pubsub + pubsub.subscribe("bar").unwrap(); + sender.send(()).unwrap(); + } } + }); + receiver.recv().unwrap(); + + // send message to foo channel after channel is ready. + for index in 0..10 { + println!("publishing on foo {index}"); + redis::cmd("PUBLISH") + .arg("foo") + .arg(index) + .query::(&mut conn_external) + .unwrap(); } - }); - receiver.recv().unwrap(); - - // send message to foo channel after channel is ready. - for index in 0..10 { - println!("publishing on foo {index}"); + receiver.recv().unwrap(); redis::cmd("PUBLISH") - .arg("foo") - .arg(index) + .arg("bar") + .arg(-1) .query::(&mut conn_external) .unwrap(); + thread.join().unwrap(); + assert_eq!( + *received_clone.lock().unwrap(), + (0..10) + .map(|index| format!("foo:{}", index)) + .chain(std::iter::once("bar:-1".to_string())) + .collect::>() + ); } - receiver.recv().unwrap(); - redis::cmd("PUBLISH") - .arg("bar") - .arg(-1) - .query::(&mut conn_external) - .unwrap(); - thread.join().unwrap(); - assert_eq!( - *received_clone.lock().unwrap(), - (0..10) - .map(|index| format!("foo:{}", index)) - .chain(std::iter::once("bar:-1".to_string())) - .collect::>() - ); -} -#[test] -fn test_pubsub_unsubscribe_no_subs() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + #[test] + fn test_pubsub_unsubscribe_no_subs() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - { - let _pubsub = con.as_pubsub(); + { + let _pubsub = con.as_pubsub(); + } + + // Connection should be usable again for non-pubsub commands + let _: redis::Value = con.set("foo", "bar").unwrap(); + let value: String = con.get("foo").unwrap(); + assert_eq!(&value[..], "bar"); } - // Connection should be usable again for non-pubsub commands - let _: redis::Value = con.set("foo", "bar").unwrap(); - let value: String = con.get("foo").unwrap(); - assert_eq!(&value[..], "bar"); -} + #[test] + fn test_pubsub_unsubscribe_one_sub() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); -#[test] -fn test_pubsub_unsubscribe_one_sub() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + { + let mut pubsub = con.as_pubsub(); + pubsub.subscribe("foo").unwrap(); + } - { - let mut pubsub = con.as_pubsub(); - pubsub.subscribe("foo").unwrap(); + // Connection should be usable again for non-pubsub commands + let _: redis::Value = con.set("foo", "bar").unwrap(); + let value: String = con.get("foo").unwrap(); + assert_eq!(&value[..], "bar"); } - // Connection should be usable again for non-pubsub commands - let _: redis::Value = con.set("foo", "bar").unwrap(); - let value: String = con.get("foo").unwrap(); - assert_eq!(&value[..], "bar"); -} + #[test] + fn test_pubsub_unsubscribe_one_sub_one_psub() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); -#[test] -fn test_pubsub_unsubscribe_one_sub_one_psub() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + { + let mut pubsub = con.as_pubsub(); + pubsub.subscribe("foo").unwrap(); + pubsub.psubscribe("foo*").unwrap(); + } - { - let mut pubsub = con.as_pubsub(); - pubsub.subscribe("foo").unwrap(); - pubsub.psubscribe("foo*").unwrap(); + // Connection should be usable again for non-pubsub commands + let _: redis::Value = con.set("foo", "bar").unwrap(); + let value: String = con.get("foo").unwrap(); + assert_eq!(&value[..], "bar"); } - // Connection should be usable again for non-pubsub commands - let _: redis::Value = con.set("foo", "bar").unwrap(); - let value: String = con.get("foo").unwrap(); - assert_eq!(&value[..], "bar"); -} - -#[test] -fn scoped_pubsub() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - - // Connection for subscriber api - let mut pubsub_con = ctx.connection(); - - let thread = spawn(move || { - let mut count = 0; - pubsub_con - .subscribe(&["foo", "bar"], |msg| { - count += 1; - match count { - 1 => { - assert_eq!(msg.get_channel(), Ok("foo".to_string())); - assert_eq!(msg.get_payload(), Ok(42)); - ControlFlow::Continue - } - 2 => { - assert_eq!(msg.get_channel(), Ok("bar".to_string())); - assert_eq!(msg.get_payload(), Ok(23)); - ControlFlow::Break(()) + #[test] + fn scoped_pubsub() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); + + // Connection for subscriber api + let mut pubsub_con = ctx.connection(); + + let thread = spawn(move || { + let mut count = 0; + pubsub_con + .subscribe(&["foo", "bar"], |msg| { + count += 1; + match count { + 1 => { + assert_eq!(msg.get_channel(), Ok("foo".to_string())); + assert_eq!(msg.get_payload(), Ok(42)); + ControlFlow::Continue + } + 2 => { + assert_eq!(msg.get_channel(), Ok("bar".to_string())); + assert_eq!(msg.get_payload(), Ok(23)); + ControlFlow::Break(()) + } + _ => ControlFlow::Break(()), } - _ => ControlFlow::Break(()), - } - }) - .unwrap(); + }) + .unwrap(); - pubsub_con - }); + pubsub_con + }); - // Can't use a barrier in this case since there's no opportunity to run code - // between channel subscription and blocking for messages. - sleep(Duration::from_millis(100)); + // Can't use a barrier in this case since there's no opportunity to run code + // between channel subscription and blocking for messages. + sleep(Duration::from_millis(100)); - redis::cmd("PUBLISH").arg("foo").arg(42).execute(&mut con); - assert_eq!(con.publish("bar", 23), Ok(1)); + redis::cmd("PUBLISH").arg("foo").arg(42).execute(&mut con); + assert_eq!(con.publish("bar", 23), Ok(1)); - // Wait for thread - let mut pubsub_con = thread.join().expect("pubsub thread terminates ok"); + // Wait for thread + let mut pubsub_con = thread.join().expect("pubsub thread terminates ok"); - // Connection should be usable again for non-pubsub commands - let _: redis::Value = pubsub_con.set("foo", "bar").unwrap(); - let value: String = pubsub_con.get("foo").unwrap(); - assert_eq!(&value[..], "bar"); -} + // Connection should be usable again for non-pubsub commands + let _: redis::Value = pubsub_con.set("foo", "bar").unwrap(); + let value: String = pubsub_con.get("foo").unwrap(); + assert_eq!(&value[..], "bar"); + } -#[test] -#[cfg(feature = "script")] -fn test_script() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + #[test] + #[cfg(feature = "script")] + fn test_script() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - let script = redis::Script::new( - r" + let script = redis::Script::new( + r" return {redis.call('GET', KEYS[1]), ARGV[1]} ", - ); - - let _: () = redis::cmd("SET") - .arg("my_key") - .arg("foo") - .query(&mut con) - .unwrap(); - let response = script.key("my_key").arg(42).invoke(&mut con); + ); - assert_eq!(response, Ok(("foo".to_string(), 42))); -} + let _: () = redis::cmd("SET") + .arg("my_key") + .arg("foo") + .query(&mut con) + .unwrap(); + let response = script.key("my_key").arg(42).invoke(&mut con); -#[test] -#[cfg(feature = "script")] -fn test_script_load() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + assert_eq!(response, Ok(("foo".to_string(), 42))); + } - let script = redis::Script::new("return 'Hello World'"); + #[test] + #[cfg(feature = "script")] + fn test_script_load() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - let hash = script.prepare_invoke().load(&mut con); + let script = redis::Script::new("return 'Hello World'"); - assert_eq!(hash, Ok(script.get_hash().to_string())); -} + let hash = script.prepare_invoke().load(&mut con); -#[test] -fn test_tuple_args() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + assert_eq!(hash, Ok(script.get_hash().to_string())); + } - redis::cmd("HMSET") - .arg("my_key") - .arg(&[("field_1", 42), ("field_2", 23)]) - .execute(&mut con); + #[test] + fn test_tuple_args() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - assert_eq!( - redis::cmd("HGET") - .arg("my_key") - .arg("field_1") - .query(&mut con), - Ok(42) - ); - assert_eq!( - redis::cmd("HGET") + redis::cmd("HMSET") .arg("my_key") - .arg("field_2") - .query(&mut con), - Ok(23) - ); -} + .arg(&[("field_1", 42), ("field_2", 23)]) + .execute(&mut con); -#[test] -fn test_nice_api() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - - assert_eq!(con.set("my_key", 42), Ok(())); - assert_eq!(con.get("my_key"), Ok(42)); - - let (k1, k2): (i32, i32) = redis::pipe() - .atomic() - .set("key_1", 42) - .ignore() - .set("key_2", 43) - .ignore() - .get("key_1") - .get("key_2") - .query(&mut con) - .unwrap(); + assert_eq!( + redis::cmd("HGET") + .arg("my_key") + .arg("field_1") + .query(&mut con), + Ok(42) + ); + assert_eq!( + redis::cmd("HGET") + .arg("my_key") + .arg("field_2") + .query(&mut con), + Ok(23) + ); + } - assert_eq!(k1, 42); - assert_eq!(k2, 43); -} + #[test] + fn test_nice_api() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); -#[test] -fn test_auto_m_versions() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + assert_eq!(con.set("my_key", 42), Ok(())); + assert_eq!(con.get("my_key"), Ok(42)); - assert_eq!(con.mset(&[("key1", 1), ("key2", 2)]), Ok(())); - assert_eq!(con.get(&["key1", "key2"]), Ok((1, 2))); - assert_eq!(con.get(vec!["key1", "key2"]), Ok((1, 2))); - assert_eq!(con.get(&vec!["key1", "key2"]), Ok((1, 2))); -} + let (k1, k2): (i32, i32) = redis::pipe() + .atomic() + .set("key_1", 42) + .ignore() + .set("key_2", 43) + .ignore() + .get("key_1") + .get("key_2") + .query(&mut con) + .unwrap(); -#[test] -fn test_nice_hash_api() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - - assert_eq!( - con.hset_multiple("my_hash", &[("f1", 1), ("f2", 2), ("f3", 4), ("f4", 8)]), - Ok(()) - ); - - let hm: HashMap = con.hgetall("my_hash").unwrap(); - assert_eq!(hm.get("f1"), Some(&1)); - assert_eq!(hm.get("f2"), Some(&2)); - assert_eq!(hm.get("f3"), Some(&4)); - assert_eq!(hm.get("f4"), Some(&8)); - assert_eq!(hm.len(), 4); - - let hm: BTreeMap = con.hgetall("my_hash").unwrap(); - assert_eq!(hm.get("f1"), Some(&1)); - assert_eq!(hm.get("f2"), Some(&2)); - assert_eq!(hm.get("f3"), Some(&4)); - assert_eq!(hm.get("f4"), Some(&8)); - assert_eq!(hm.len(), 4); - - let v: Vec<(String, isize)> = con.hgetall("my_hash").unwrap(); - assert_eq!( - v, - vec![ - ("f1".to_string(), 1), - ("f2".to_string(), 2), - ("f3".to_string(), 4), - ("f4".to_string(), 8), - ] - ); - - assert_eq!(con.hget("my_hash", &["f2", "f4"]), Ok((2, 8))); - assert_eq!(con.hincr("my_hash", "f1", 1), Ok(2)); - assert_eq!(con.hincr("my_hash", "f2", 1.5f32), Ok(3.5f32)); - assert_eq!(con.hexists("my_hash", "f2"), Ok(true)); - assert_eq!(con.hdel("my_hash", &["f1", "f2"]), Ok(())); - assert_eq!(con.hexists("my_hash", "f2"), Ok(false)); - - let iter: redis::Iter<'_, (String, isize)> = con.hscan("my_hash").unwrap(); - let mut found = HashSet::new(); - for item in iter { - found.insert(item); + assert_eq!(k1, 42); + assert_eq!(k2, 43); } - assert_eq!(found.len(), 2); - assert!(found.contains(&("f3".to_string(), 4))); - assert!(found.contains(&("f4".to_string(), 8))); -} + #[test] + fn test_auto_m_versions() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); + + assert_eq!(con.mset(&[("key1", 1), ("key2", 2)]), Ok(())); + assert_eq!(con.get(&["key1", "key2"]), Ok((1, 2))); + assert_eq!(con.get(vec!["key1", "key2"]), Ok((1, 2))); + assert_eq!(con.get(&vec!["key1", "key2"]), Ok((1, 2))); + } + + #[test] + fn test_nice_hash_api() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); + + assert_eq!( + con.hset_multiple("my_hash", &[("f1", 1), ("f2", 2), ("f3", 4), ("f4", 8)]), + Ok(()) + ); + + let hm: HashMap = con.hgetall("my_hash").unwrap(); + assert_eq!(hm.get("f1"), Some(&1)); + assert_eq!(hm.get("f2"), Some(&2)); + assert_eq!(hm.get("f3"), Some(&4)); + assert_eq!(hm.get("f4"), Some(&8)); + assert_eq!(hm.len(), 4); + + let hm: BTreeMap = con.hgetall("my_hash").unwrap(); + assert_eq!(hm.get("f1"), Some(&1)); + assert_eq!(hm.get("f2"), Some(&2)); + assert_eq!(hm.get("f3"), Some(&4)); + assert_eq!(hm.get("f4"), Some(&8)); + assert_eq!(hm.len(), 4); + + let v: Vec<(String, isize)> = con.hgetall("my_hash").unwrap(); + assert_eq!( + v, + vec![ + ("f1".to_string(), 1), + ("f2".to_string(), 2), + ("f3".to_string(), 4), + ("f4".to_string(), 8), + ] + ); + + assert_eq!(con.hget("my_hash", &["f2", "f4"]), Ok((2, 8))); + assert_eq!(con.hincr("my_hash", "f1", 1), Ok(2)); + assert_eq!(con.hincr("my_hash", "f2", 1.5f32), Ok(3.5f32)); + assert_eq!(con.hexists("my_hash", "f2"), Ok(true)); + assert_eq!(con.hdel("my_hash", &["f1", "f2"]), Ok(())); + assert_eq!(con.hexists("my_hash", "f2"), Ok(false)); + + let iter: redis::Iter<'_, (String, isize)> = con.hscan("my_hash").unwrap(); + let mut found = HashSet::new(); + for item in iter { + found.insert(item); + } + + assert_eq!(found.len(), 2); + assert!(found.contains(&("f3".to_string(), 4))); + assert!(found.contains(&("f4".to_string(), 8))); + } + + #[test] + fn test_nice_list_api() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); + + assert_eq!(con.rpush("my_list", &[1, 2, 3, 4]), Ok(4)); + assert_eq!(con.rpush("my_list", &[5, 6, 7, 8]), Ok(8)); + assert_eq!(con.llen("my_list"), Ok(8)); + + assert_eq!(con.lpop("my_list", Default::default()), Ok(1)); + assert_eq!(con.llen("my_list"), Ok(7)); + + assert_eq!(con.lrange("my_list", 0, 2), Ok((2, 3, 4))); + + assert_eq!(con.lset("my_list", 0, 4), Ok(true)); + assert_eq!(con.lrange("my_list", 0, 2), Ok((4, 3, 4))); + + #[cfg(not(windows))] + //Windows version of redis is limited to v3.x + { + let my_list: Vec = con.lrange("my_list", 0, 10).expect("To get range"); + assert_eq!( + con.lpop("my_list", core::num::NonZeroUsize::new(10)), + Ok(my_list) + ); + } + } -#[test] -fn test_nice_list_api() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + #[test] + fn test_tuple_decoding_regression() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); + + assert_eq!(con.del("my_zset"), Ok(())); + assert_eq!(con.zadd("my_zset", "one", 1), Ok(1)); + assert_eq!(con.zadd("my_zset", "two", 2), Ok(1)); + + let vec: Vec<(String, u32)> = con.zrangebyscore_withscores("my_zset", 0, 10).unwrap(); + assert_eq!(vec.len(), 2); + + assert_eq!(con.del("my_zset"), Ok(1)); + + let vec: Vec<(String, u32)> = con.zrangebyscore_withscores("my_zset", 0, 10).unwrap(); + assert_eq!(vec.len(), 0); + } + + #[test] + fn test_bit_operations() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); + + assert_eq!(con.setbit("bitvec", 10, true), Ok(false)); + assert_eq!(con.getbit("bitvec", 10), Ok(true)); + } + + #[test] + fn test_redis_server_down() { + let mut ctx = TestContext::new(); + let mut con = ctx.connection(); + + let ping = redis::cmd("PING").query::(&mut con); + assert_eq!(ping, Ok("PONG".into())); + + ctx.stop_server(); + + let ping = redis::cmd("PING").query::(&mut con); + + assert!(ping.is_err()); + eprintln!("{}", ping.unwrap_err()); + assert!(!con.is_open()); + } + + #[test] + fn test_zinterstore_weights() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); + + let _: () = con + .zadd_multiple("zset1", &[(1, "one"), (2, "two"), (4, "four")]) + .unwrap(); + let _: () = con + .zadd_multiple("zset2", &[(1, "one"), (2, "two"), (3, "three")]) + .unwrap(); + + // zinterstore_weights + assert_eq!( + con.zinterstore_weights("out", &[("zset1", 2), ("zset2", 3)]), + Ok(2) + ); - assert_eq!(con.rpush("my_list", &[1, 2, 3, 4]), Ok(4)); - assert_eq!(con.rpush("my_list", &[5, 6, 7, 8]), Ok(8)); - assert_eq!(con.llen("my_list"), Ok(8)); + assert_eq!( + con.zrange_withscores("out", 0, -1), + Ok(vec![ + ("one".to_string(), "5".to_string()), + ("two".to_string(), "10".to_string()) + ]) + ); - assert_eq!(con.lpop("my_list", Default::default()), Ok(1)); - assert_eq!(con.llen("my_list"), Ok(7)); + // zinterstore_min_weights + assert_eq!( + con.zinterstore_min_weights("out", &[("zset1", 2), ("zset2", 3)]), + Ok(2) + ); - assert_eq!(con.lrange("my_list", 0, 2), Ok((2, 3, 4))); + assert_eq!( + con.zrange_withscores("out", 0, -1), + Ok(vec![ + ("one".to_string(), "2".to_string()), + ("two".to_string(), "4".to_string()), + ]) + ); - assert_eq!(con.lset("my_list", 0, 4), Ok(true)); - assert_eq!(con.lrange("my_list", 0, 2), Ok((4, 3, 4))); + // zinterstore_max_weights + assert_eq!( + con.zinterstore_max_weights("out", &[("zset1", 2), ("zset2", 3)]), + Ok(2) + ); - #[cfg(not(windows))] - //Windows version of redis is limited to v3.x - { - let my_list: Vec = con.lrange("my_list", 0, 10).expect("To get range"); assert_eq!( - con.lpop("my_list", core::num::NonZeroUsize::new(10)), - Ok(my_list) + con.zrange_withscores("out", 0, -1), + Ok(vec![ + ("one".to_string(), "3".to_string()), + ("two".to_string(), "6".to_string()), + ]) ); } -} -#[test] -fn test_tuple_decoding_regression() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + #[test] + fn test_zunionstore_weights() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - assert_eq!(con.del("my_zset"), Ok(())); - assert_eq!(con.zadd("my_zset", "one", 1), Ok(1)); - assert_eq!(con.zadd("my_zset", "two", 2), Ok(1)); + let _: () = con + .zadd_multiple("zset1", &[(1, "one"), (2, "two")]) + .unwrap(); + let _: () = con + .zadd_multiple("zset2", &[(1, "one"), (2, "two"), (3, "three")]) + .unwrap(); - let vec: Vec<(String, u32)> = con.zrangebyscore_withscores("my_zset", 0, 10).unwrap(); - assert_eq!(vec.len(), 2); + // zunionstore_weights + assert_eq!( + con.zunionstore_weights("out", &[("zset1", 2), ("zset2", 3)]), + Ok(3) + ); - assert_eq!(con.del("my_zset"), Ok(1)); + assert_eq!( + con.zrange_withscores("out", 0, -1), + Ok(vec![ + ("one".to_string(), "5".to_string()), + ("three".to_string(), "9".to_string()), + ("two".to_string(), "10".to_string()) + ]) + ); + // test converting to double + assert_eq!( + con.zrange_withscores("out", 0, -1), + Ok(vec![ + ("one".to_string(), 5.0), + ("three".to_string(), 9.0), + ("two".to_string(), 10.0) + ]) + ); - let vec: Vec<(String, u32)> = con.zrangebyscore_withscores("my_zset", 0, 10).unwrap(); - assert_eq!(vec.len(), 0); -} + // zunionstore_min_weights + assert_eq!( + con.zunionstore_min_weights("out", &[("zset1", 2), ("zset2", 3)]), + Ok(3) + ); -#[test] -fn test_bit_operations() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + assert_eq!( + con.zrange_withscores("out", 0, -1), + Ok(vec![ + ("one".to_string(), "2".to_string()), + ("two".to_string(), "4".to_string()), + ("three".to_string(), "9".to_string()) + ]) + ); - assert_eq!(con.setbit("bitvec", 10, true), Ok(false)); - assert_eq!(con.getbit("bitvec", 10), Ok(true)); -} + // zunionstore_max_weights + assert_eq!( + con.zunionstore_max_weights("out", &[("zset1", 2), ("zset2", 3)]), + Ok(3) + ); -#[test] -fn test_redis_server_down() { - let mut ctx = TestContext::new(); - let mut con = ctx.connection(); + assert_eq!( + con.zrange_withscores("out", 0, -1), + Ok(vec![ + ("one".to_string(), "3".to_string()), + ("two".to_string(), "6".to_string()), + ("three".to_string(), "9".to_string()) + ]) + ); + } - let ping = redis::cmd("PING").query::(&mut con); - assert_eq!(ping, Ok("PONG".into())); + #[test] + fn test_zrembylex() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - ctx.stop_server(); + let setname = "myzset"; + assert_eq!( + con.zadd_multiple( + setname, + &[ + (0, "apple"), + (0, "banana"), + (0, "carrot"), + (0, "durian"), + (0, "eggplant"), + (0, "grapes"), + ], + ), + Ok(6) + ); - let ping = redis::cmd("PING").query::(&mut con); + // Will remove "banana", "carrot", "durian" and "eggplant" + let num_removed: u32 = con.zrembylex(setname, "[banana", "[eggplant").unwrap(); + assert_eq!(4, num_removed); - assert!(ping.is_err()); - eprintln!("{}", ping.unwrap_err()); - assert!(!con.is_open()); -} + let remaining: Vec = con.zrange(setname, 0, -1).unwrap(); + assert_eq!(remaining, vec!["apple".to_string(), "grapes".to_string()]); + } -#[test] -fn test_zinterstore_weights() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + // Requires redis-server >= 6.2.0. + // Not supported with the current appveyor/windows binary deployed. + #[cfg(not(target_os = "windows"))] + #[test] + fn test_zrandmember() { + use redis::ProtocolVersion; - let _: () = con - .zadd_multiple("zset1", &[(1, "one"), (2, "two"), (4, "four")]) - .unwrap(); - let _: () = con - .zadd_multiple("zset2", &[(1, "one"), (2, "two"), (3, "three")]) - .unwrap(); + let ctx = TestContext::new(); + let mut con = ctx.connection(); - // zinterstore_weights - assert_eq!( - con.zinterstore_weights("out", &[("zset1", 2), ("zset2", 3)]), - Ok(2) - ); - - assert_eq!( - con.zrange_withscores("out", 0, -1), - Ok(vec![ - ("one".to_string(), "5".to_string()), - ("two".to_string(), "10".to_string()) - ]) - ); - - // zinterstore_min_weights - assert_eq!( - con.zinterstore_min_weights("out", &[("zset1", 2), ("zset2", 3)]), - Ok(2) - ); - - assert_eq!( - con.zrange_withscores("out", 0, -1), - Ok(vec![ - ("one".to_string(), "2".to_string()), - ("two".to_string(), "4".to_string()), - ]) - ); - - // zinterstore_max_weights - assert_eq!( - con.zinterstore_max_weights("out", &[("zset1", 2), ("zset2", 3)]), - Ok(2) - ); - - assert_eq!( - con.zrange_withscores("out", 0, -1), - Ok(vec![ - ("one".to_string(), "3".to_string()), - ("two".to_string(), "6".to_string()), - ]) - ); -} + let setname = "myzrandset"; + let () = con.zadd(setname, "one", 1).unwrap(); -#[test] -fn test_zunionstore_weights() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + let result: String = con.zrandmember(setname, None).unwrap(); + assert_eq!(result, "one".to_string()); - let _: () = con - .zadd_multiple("zset1", &[(1, "one"), (2, "two")]) - .unwrap(); - let _: () = con - .zadd_multiple("zset2", &[(1, "one"), (2, "two"), (3, "three")]) - .unwrap(); + let result: Vec = con.zrandmember(setname, Some(1)).unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0], "one".to_string()); - // zunionstore_weights - assert_eq!( - con.zunionstore_weights("out", &[("zset1", 2), ("zset2", 3)]), - Ok(3) - ); - - assert_eq!( - con.zrange_withscores("out", 0, -1), - Ok(vec![ - ("one".to_string(), "5".to_string()), - ("three".to_string(), "9".to_string()), - ("two".to_string(), "10".to_string()) - ]) - ); - // test converting to double - assert_eq!( - con.zrange_withscores("out", 0, -1), - Ok(vec![ - ("one".to_string(), 5.0), - ("three".to_string(), 9.0), - ("two".to_string(), 10.0) - ]) - ); - - // zunionstore_min_weights - assert_eq!( - con.zunionstore_min_weights("out", &[("zset1", 2), ("zset2", 3)]), - Ok(3) - ); - - assert_eq!( - con.zrange_withscores("out", 0, -1), - Ok(vec![ - ("one".to_string(), "2".to_string()), - ("two".to_string(), "4".to_string()), - ("three".to_string(), "9".to_string()) - ]) - ); - - // zunionstore_max_weights - assert_eq!( - con.zunionstore_max_weights("out", &[("zset1", 2), ("zset2", 3)]), - Ok(3) - ); - - assert_eq!( - con.zrange_withscores("out", 0, -1), - Ok(vec![ - ("one".to_string(), "3".to_string()), - ("two".to_string(), "6".to_string()), - ("three".to_string(), "9".to_string()) - ]) - ); -} + let result: Vec = con.zrandmember(setname, Some(2)).unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0], "one".to_string()); -#[test] -fn test_zrembylex() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - - let setname = "myzset"; - assert_eq!( - con.zadd_multiple( - setname, - &[ - (0, "apple"), - (0, "banana"), - (0, "carrot"), - (0, "durian"), - (0, "eggplant"), - (0, "grapes"), - ], - ), - Ok(6) - ); - - // Will remove "banana", "carrot", "durian" and "eggplant" - let num_removed: u32 = con.zrembylex(setname, "[banana", "[eggplant").unwrap(); - assert_eq!(4, num_removed); - - let remaining: Vec = con.zrange(setname, 0, -1).unwrap(); - assert_eq!(remaining, vec!["apple".to_string(), "grapes".to_string()]); -} + assert_eq!( + con.zadd_multiple( + setname, + &[(2, "two"), (3, "three"), (4, "four"), (5, "five")] + ), + Ok(4) + ); -// Requires redis-server >= 6.2.0. -// Not supported with the current appveyor/windows binary deployed. -#[cfg(not(target_os = "windows"))] -#[test] -fn test_zrandmember() { - use redis::ProtocolVersion; + let results: Vec = con.zrandmember(setname, Some(5)).unwrap(); + assert_eq!(results.len(), 5); - let ctx = TestContext::new(); - let mut con = ctx.connection(); + let results: Vec = con.zrandmember(setname, Some(-5)).unwrap(); + assert_eq!(results.len(), 5); - let setname = "myzrandset"; - let () = con.zadd(setname, "one", 1).unwrap(); + if ctx.protocol == ProtocolVersion::RESP2 { + let results: Vec = con.zrandmember_withscores(setname, 5).unwrap(); + assert_eq!(results.len(), 10); - let result: String = con.zrandmember(setname, None).unwrap(); - assert_eq!(result, "one".to_string()); + let results: Vec = con.zrandmember_withscores(setname, -5).unwrap(); + assert_eq!(results.len(), 10); + } - let result: Vec = con.zrandmember(setname, Some(1)).unwrap(); - assert_eq!(result.len(), 1); - assert_eq!(result[0], "one".to_string()); + let results: Vec<(String, f64)> = con.zrandmember_withscores(setname, 5).unwrap(); + assert_eq!(results.len(), 5); - let result: Vec = con.zrandmember(setname, Some(2)).unwrap(); - assert_eq!(result.len(), 1); - assert_eq!(result[0], "one".to_string()); + let results: Vec<(String, f64)> = con.zrandmember_withscores(setname, -5).unwrap(); + assert_eq!(results.len(), 5); + } - assert_eq!( - con.zadd_multiple( - setname, - &[(2, "two"), (3, "three"), (4, "four"), (5, "five")] - ), - Ok(4) - ); + #[test] + fn test_sismember() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - let results: Vec = con.zrandmember(setname, Some(5)).unwrap(); - assert_eq!(results.len(), 5); + let setname = "myset"; + assert_eq!(con.sadd(setname, &["a"]), Ok(1)); - let results: Vec = con.zrandmember(setname, Some(-5)).unwrap(); - assert_eq!(results.len(), 5); + let result: bool = con.sismember(setname, &["a"]).unwrap(); + assert!(result); - if ctx.protocol == ProtocolVersion::RESP2 { - let results: Vec = con.zrandmember_withscores(setname, 5).unwrap(); - assert_eq!(results.len(), 10); + let result: bool = con.sismember(setname, &["b"]).unwrap(); + assert!(!result); + } - let results: Vec = con.zrandmember_withscores(setname, -5).unwrap(); - assert_eq!(results.len(), 10); + // Requires redis-server >= 6.2.0. + // Not supported with the current appveyor/windows binary deployed. + #[cfg(not(target_os = "windows"))] + #[test] + fn test_smismember() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); + + let setname = "myset"; + assert_eq!(con.sadd(setname, &["a", "b", "c"]), Ok(3)); + let results: Vec = con.smismember(setname, &["0", "a", "b", "c", "x"]).unwrap(); + assert_eq!(results, vec![false, true, true, true, false]); } - let results: Vec<(String, f64)> = con.zrandmember_withscores(setname, 5).unwrap(); - assert_eq!(results.len(), 5); + #[test] + fn test_object_commands() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - let results: Vec<(String, f64)> = con.zrandmember_withscores(setname, -5).unwrap(); - assert_eq!(results.len(), 5); -} + let _: () = con.set("object_key_str", "object_value_str").unwrap(); + let _: () = con.set("object_key_int", 42).unwrap(); -#[test] -fn test_sismember() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + assert_eq!( + con.object_encoding::<_, String>("object_key_str").unwrap(), + "embstr" + ); - let setname = "myset"; - assert_eq!(con.sadd(setname, &["a"]), Ok(1)); + assert_eq!( + con.object_encoding::<_, String>("object_key_int").unwrap(), + "int" + ); - let result: bool = con.sismember(setname, &["a"]).unwrap(); - assert!(result); + assert!(con.object_idletime::<_, i32>("object_key_str").unwrap() <= 1); + assert_eq!(con.object_refcount::<_, i32>("object_key_str").unwrap(), 1); + + // Needed for OBJECT FREQ and can't be set before object_idletime + // since that will break getting the idletime before idletime adjuts + redis::cmd("CONFIG") + .arg("SET") + .arg(b"maxmemory-policy") + .arg("allkeys-lfu") + .execute(&mut con); + + let _: () = con.get("object_key_str").unwrap(); + // since maxmemory-policy changed, freq should reset to 1 since we only called + // get after that + assert_eq!(con.object_freq::<_, i32>("object_key_str").unwrap(), 1); + } - let result: bool = con.sismember(setname, &["b"]).unwrap(); - assert!(!result); -} + #[test] + fn test_mget() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); -// Requires redis-server >= 6.2.0. -// Not supported with the current appveyor/windows binary deployed. -#[cfg(not(target_os = "windows"))] -#[test] -fn test_smismember() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - - let setname = "myset"; - assert_eq!(con.sadd(setname, &["a", "b", "c"]), Ok(3)); - let results: Vec = con.smismember(setname, &["0", "a", "b", "c", "x"]).unwrap(); - assert_eq!(results, vec![false, true, true, true, false]); -} + let _: () = con.set(1, "1").unwrap(); + let data: Vec = con.mget(&[1]).unwrap(); + assert_eq!(data, vec!["1"]); -#[test] -fn test_object_commands() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - - let _: () = con.set("object_key_str", "object_value_str").unwrap(); - let _: () = con.set("object_key_int", 42).unwrap(); - - assert_eq!( - con.object_encoding::<_, String>("object_key_str").unwrap(), - "embstr" - ); - - assert_eq!( - con.object_encoding::<_, String>("object_key_int").unwrap(), - "int" - ); - - assert!(con.object_idletime::<_, i32>("object_key_str").unwrap() <= 1); - assert_eq!(con.object_refcount::<_, i32>("object_key_str").unwrap(), 1); - - // Needed for OBJECT FREQ and can't be set before object_idletime - // since that will break getting the idletime before idletime adjuts - redis::cmd("CONFIG") - .arg("SET") - .arg(b"maxmemory-policy") - .arg("allkeys-lfu") - .execute(&mut con); - - let _: () = con.get("object_key_str").unwrap(); - // since maxmemory-policy changed, freq should reset to 1 since we only called - // get after that - assert_eq!(con.object_freq::<_, i32>("object_key_str").unwrap(), 1); -} + let _: () = con.set(2, "2").unwrap(); + let data: Vec = con.mget(&[1, 2]).unwrap(); + assert_eq!(data, vec!["1", "2"]); -#[test] -fn test_mget() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + let data: Vec> = con.mget(&[4]).unwrap(); + assert_eq!(data, vec![None]); - let _: () = con.set(1, "1").unwrap(); - let data: Vec = con.mget(&[1]).unwrap(); - assert_eq!(data, vec!["1"]); + let data: Vec> = con.mget(&[2, 4]).unwrap(); + assert_eq!(data, vec![Some("2".to_string()), None]); + } - let _: () = con.set(2, "2").unwrap(); - let data: Vec = con.mget(&[1, 2]).unwrap(); - assert_eq!(data, vec!["1", "2"]); + #[test] + fn test_variable_length_get() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - let data: Vec> = con.mget(&[4]).unwrap(); - assert_eq!(data, vec![None]); + let _: () = con.set(1, "1").unwrap(); + let keys = vec![1]; + assert_eq!(keys.len(), 1); + let data: Vec = con.get(&keys).unwrap(); + assert_eq!(data, vec!["1"]); + } - let data: Vec> = con.mget(&[2, 4]).unwrap(); - assert_eq!(data, vec![Some("2".to_string()), None]); -} + #[test] + fn test_multi_generics() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); -#[test] -fn test_variable_length_get() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + assert_eq!(con.sadd(b"set1", vec![5, 42]), Ok(2)); + assert_eq!(con.sadd(999_i64, vec![42, 123]), Ok(2)); + let _: () = con.rename(999_i64, b"set2").unwrap(); + assert_eq!(con.sunionstore("res", &[b"set1", b"set2"]), Ok(3)); + } - let _: () = con.set(1, "1").unwrap(); - let keys = vec![1]; - assert_eq!(keys.len(), 1); - let data: Vec = con.get(&keys).unwrap(); - assert_eq!(data, vec!["1"]); -} + #[test] + fn test_set_options_with_get() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); -#[test] -fn test_multi_generics() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + let opts = SetOptions::default().get(true); + let data: Option = con.set_options(1, "1", opts).unwrap(); + assert_eq!(data, None); - assert_eq!(con.sadd(b"set1", vec![5, 42]), Ok(2)); - assert_eq!(con.sadd(999_i64, vec![42, 123]), Ok(2)); - let _: () = con.rename(999_i64, b"set2").unwrap(); - assert_eq!(con.sunionstore("res", &[b"set1", b"set2"]), Ok(3)); -} + let opts = SetOptions::default().get(true); + let data: Option = con.set_options(1, "1", opts).unwrap(); + assert_eq!(data, Some("1".to_string())); + } -#[test] -fn test_set_options_with_get() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); + #[test] + fn test_set_options_options() { + let empty = SetOptions::default(); + assert_eq!(ToRedisArgs::to_redis_args(&empty).len(), 0); - let opts = SetOptions::default().get(true); - let data: Option = con.set_options(1, "1", opts).unwrap(); - assert_eq!(data, None); + let opts = SetOptions::default() + .conditional_set(ExistenceCheck::NX) + .get(true) + .with_expiration(SetExpiry::PX(1000)); - let opts = SetOptions::default().get(true); - let data: Option = con.set_options(1, "1", opts).unwrap(); - assert_eq!(data, Some("1".to_string())); -} + assert_args!(&opts, "NX", "GET", "PX", "1000"); -#[test] -fn test_set_options_options() { - let empty = SetOptions::default(); - assert_eq!(ToRedisArgs::to_redis_args(&empty).len(), 0); + let opts = SetOptions::default() + .conditional_set(ExistenceCheck::XX) + .get(true) + .with_expiration(SetExpiry::PX(1000)); - let opts = SetOptions::default() - .conditional_set(ExistenceCheck::NX) - .get(true) - .with_expiration(SetExpiry::PX(1000)); + assert_args!(&opts, "XX", "GET", "PX", "1000"); - assert_args!(&opts, "NX", "GET", "PX", "1000"); + let opts = SetOptions::default() + .conditional_set(ExistenceCheck::XX) + .with_expiration(SetExpiry::KEEPTTL); - let opts = SetOptions::default() - .conditional_set(ExistenceCheck::XX) - .get(true) - .with_expiration(SetExpiry::PX(1000)); + assert_args!(&opts, "XX", "KEEPTTL"); - assert_args!(&opts, "XX", "GET", "PX", "1000"); + let opts = SetOptions::default() + .conditional_set(ExistenceCheck::XX) + .with_expiration(SetExpiry::EXAT(100)); - let opts = SetOptions::default() - .conditional_set(ExistenceCheck::XX) - .with_expiration(SetExpiry::KEEPTTL); + assert_args!(&opts, "XX", "EXAT", "100"); - assert_args!(&opts, "XX", "KEEPTTL"); + let opts = SetOptions::default().with_expiration(SetExpiry::EX(1000)); - let opts = SetOptions::default() - .conditional_set(ExistenceCheck::XX) - .with_expiration(SetExpiry::EXAT(100)); + assert_args!(&opts, "EX", "1000"); + } - assert_args!(&opts, "XX", "EXAT", "100"); + #[test] + fn test_blocking_sorted_set_api() { + let ctx = TestContext::new(); + let mut con = ctx.connection(); - let opts = SetOptions::default().with_expiration(SetExpiry::EX(1000)); + // setup version & input data followed by assertions that take into account Redis version + // BZPOPMIN & BZPOPMAX are available from Redis version 5.0.0 + // BZMPOP is available from Redis version 7.0.0 - assert_args!(&opts, "EX", "1000"); -} + let redis_version = ctx.get_version(); + assert!(redis_version.0 >= 5); -#[test] -fn test_blocking_sorted_set_api() { - let ctx = TestContext::new(); - let mut con = ctx.connection(); - - // setup version & input data followed by assertions that take into account Redis version - // BZPOPMIN & BZPOPMAX are available from Redis version 5.0.0 - // BZMPOP is available from Redis version 7.0.0 - - let redis_version = ctx.get_version(); - assert!(redis_version.0 >= 5); - - assert_eq!(con.zadd("a", "1a", 1), Ok(())); - assert_eq!(con.zadd("b", "2b", 2), Ok(())); - assert_eq!(con.zadd("c", "3c", 3), Ok(())); - assert_eq!(con.zadd("d", "4d", 4), Ok(())); - assert_eq!(con.zadd("a", "5a", 5), Ok(())); - assert_eq!(con.zadd("b", "6b", 6), Ok(())); - assert_eq!(con.zadd("c", "7c", 7), Ok(())); - assert_eq!(con.zadd("d", "8d", 8), Ok(())); - - let min = con.bzpopmin::<&str, (String, String, String)>("b", 0.0); - let max = con.bzpopmax::<&str, (String, String, String)>("b", 0.0); - - assert_eq!( - min.unwrap(), - (String::from("b"), String::from("2b"), String::from("2")) - ); - assert_eq!( - max.unwrap(), - (String::from("b"), String::from("6b"), String::from("6")) - ); - - if redis_version.0 >= 7 { - let min = con.bzmpop_min::<&str, (String, Vec>)>( - 0.0, - vec!["a", "b", "c", "d"].as_slice(), - 1, - ); - let max = con.bzmpop_max::<&str, (String, Vec>)>( - 0.0, - vec!["a", "b", "c", "d"].as_slice(), - 1, - ); + assert_eq!(con.zadd("a", "1a", 1), Ok(())); + assert_eq!(con.zadd("b", "2b", 2), Ok(())); + assert_eq!(con.zadd("c", "3c", 3), Ok(())); + assert_eq!(con.zadd("d", "4d", 4), Ok(())); + assert_eq!(con.zadd("a", "5a", 5), Ok(())); + assert_eq!(con.zadd("b", "6b", 6), Ok(())); + assert_eq!(con.zadd("c", "7c", 7), Ok(())); + assert_eq!(con.zadd("d", "8d", 8), Ok(())); + + let min = con.bzpopmin::<&str, (String, String, String)>("b", 0.0); + let max = con.bzpopmax::<&str, (String, String, String)>("b", 0.0); assert_eq!( - min.unwrap().1[0][0], - (String::from("1a"), String::from("1")) + min.unwrap(), + (String::from("b"), String::from("2b"), String::from("2")) ); assert_eq!( - max.unwrap().1[0][0], - (String::from("5a"), String::from("5")) + max.unwrap(), + (String::from("b"), String::from("6b"), String::from("6")) ); - } -} -#[test] -fn test_push_manager() { - let ctx = TestContext::new(); - if ctx.protocol == ProtocolVersion::RESP2 { - return; + if redis_version.0 >= 7 { + let min = con.bzmpop_min::<&str, (String, Vec>)>( + 0.0, + vec!["a", "b", "c", "d"].as_slice(), + 1, + ); + let max = con.bzmpop_max::<&str, (String, Vec>)>( + 0.0, + vec!["a", "b", "c", "d"].as_slice(), + 1, + ); + + assert_eq!( + min.unwrap().1[0][0], + (String::from("1a"), String::from("1")) + ); + assert_eq!( + max.unwrap().1[0][0], + (String::from("5a"), String::from("5")) + ); + } } - let mut con = ctx.connection(); - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - con.get_push_manager().replace_sender(tx.clone()); - let _ = cmd("CLIENT") - .arg("TRACKING") - .arg("ON") - .query::<()>(&mut con) - .unwrap(); - let pipe = build_simple_pipeline_for_invalidation(); - for _ in 0..10 { + + #[test] + fn test_push_manager() { + let ctx = TestContext::new(); + if ctx.protocol == ProtocolVersion::RESP2 { + return; + } + let mut con = ctx.connection(); + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + con.get_push_manager().replace_sender(tx.clone()); + let _ = cmd("CLIENT") + .arg("TRACKING") + .arg("ON") + .query::<()>(&mut con) + .unwrap(); + let pipe = build_simple_pipeline_for_invalidation(); + for _ in 0..10 { + let _: RedisResult<()> = pipe.query(&mut con); + let _: i32 = con.get("key_1").unwrap(); + let PushInfo { kind, data } = rx.try_recv().unwrap(); + assert_eq!( + ( + PushKind::Invalidate, + vec![Value::Array(vec![Value::BulkString( + "key_1".as_bytes().to_vec() + )])] + ), + (kind, data) + ); + } + let (new_tx, mut new_rx) = tokio::sync::mpsc::unbounded_channel(); + con.get_push_manager().replace_sender(new_tx.clone()); + drop(rx); let _: RedisResult<()> = pipe.query(&mut con); let _: i32 = con.get("key_1").unwrap(); - let PushInfo { kind, data } = rx.try_recv().unwrap(); + let PushInfo { kind, data } = new_rx.try_recv().unwrap(); assert_eq!( ( PushKind::Invalidate, @@ -1509,47 +1527,32 @@ fn test_push_manager() { ), (kind, data) ); - } - let (new_tx, mut new_rx) = tokio::sync::mpsc::unbounded_channel(); - con.get_push_manager().replace_sender(new_tx.clone()); - drop(rx); - let _: RedisResult<()> = pipe.query(&mut con); - let _: i32 = con.get("key_1").unwrap(); - let PushInfo { kind, data } = new_rx.try_recv().unwrap(); - assert_eq!( - ( - PushKind::Invalidate, - vec![Value::Array(vec![Value::BulkString( - "key_1".as_bytes().to_vec() - )])] - ), - (kind, data) - ); - - { - drop(new_rx); - for _ in 0..10 { - let _: RedisResult<()> = pipe.query(&mut con); - let v: i32 = con.get("key_1").unwrap(); - assert_eq!(v, 42); + + { + drop(new_rx); + for _ in 0..10 { + let _: RedisResult<()> = pipe.query(&mut con); + let v: i32 = con.get("key_1").unwrap(); + assert_eq!(v, 42); + } } } -} -#[test] -fn test_push_manager_disconnection() { - let ctx = TestContext::new(); - if ctx.protocol == ProtocolVersion::RESP2 { - return; + #[test] + fn test_push_manager_disconnection() { + let ctx = TestContext::new(); + if ctx.protocol == ProtocolVersion::RESP2 { + return; + } + let mut con = ctx.connection(); + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + con.get_push_manager().replace_sender(tx.clone()); + + let _: () = con.set("A", "1").unwrap(); + assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty); + drop(ctx); + let x: RedisResult<()> = con.set("A", "1"); + assert!(x.is_err()); + assert_eq!(rx.try_recv().unwrap().kind, PushKind::Disconnection); } - let mut con = ctx.connection(); - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - con.get_push_manager().replace_sender(tx.clone()); - - let _: () = con.set("A", "1").unwrap(); - assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty); - drop(ctx); - let x: RedisResult<()> = con.set("A", "1"); - assert!(x.is_err()); - assert_eq!(rx.try_recv().unwrap().kind, PushKind::Disconnection); } diff --git a/redis/tests/test_cluster.rs b/redis/tests/test_cluster.rs index 01312058e..fdb652ea8 100644 --- a/redis/tests/test_cluster.rs +++ b/redis/tests/test_cluster.rs @@ -1,962 +1,974 @@ #![cfg(feature = "cluster")] mod support; -use std::sync::{ - atomic::{self, AtomicI32, Ordering}, - Arc, -}; - -use crate::support::*; -use redis::{ - cluster::{cluster_pipe, ClusterClient}, - cmd, parse_redis_value, Commands, ConnectionLike, ErrorKind, ProtocolVersion, RedisError, - Value, -}; - -#[test] -fn test_cluster_basics() { - let cluster = TestClusterContext::new(3, 0); - let mut con = cluster.connection(); - - redis::cmd("SET") - .arg("{x}key1") - .arg(b"foo") - .execute(&mut con); - redis::cmd("SET").arg(&["{x}key2", "bar"]).execute(&mut con); - - assert_eq!( - redis::cmd("MGET") - .arg(&["{x}key1", "{x}key2"]) - .query(&mut con), - Ok(("foo".to_string(), b"bar".to_vec())) - ); -} -#[test] -fn test_cluster_with_username_and_password() { - let cluster = TestClusterContext::new_with_cluster_client_builder( - 3, - 0, - |builder| { - builder - .username(RedisCluster::username().to_string()) - .password(RedisCluster::password().to_string()) - }, - false, - ); - cluster.disable_default_user(); +#[cfg(test)] +mod cluster { + use std::sync::{ + atomic::{self, AtomicI32, Ordering}, + Arc, + }; - let mut con = cluster.connection(); + use crate::support::*; + use redis::{ + cluster::{cluster_pipe, ClusterClient}, + cmd, parse_redis_value, Commands, ConnectionLike, ErrorKind, ProtocolVersion, RedisError, + Value, + }; - redis::cmd("SET") - .arg("{x}key1") - .arg(b"foo") - .execute(&mut con); - redis::cmd("SET").arg(&["{x}key2", "bar"]).execute(&mut con); + #[test] + fn test_cluster_basics() { + let cluster = TestClusterContext::new(3, 0); + let mut con = cluster.connection(); - assert_eq!( - redis::cmd("MGET") - .arg(&["{x}key1", "{x}key2"]) - .query(&mut con), - Ok(("foo".to_string(), b"bar".to_vec())) - ); -} + redis::cmd("SET") + .arg("{x}key1") + .arg(b"foo") + .execute(&mut con); + redis::cmd("SET").arg(&["{x}key2", "bar"]).execute(&mut con); -#[test] -fn test_cluster_with_bad_password() { - let cluster = TestClusterContext::new_with_cluster_client_builder( - 3, - 0, - |builder| { - builder - .username(RedisCluster::username().to_string()) - .password("not the right password".to_string()) - }, - false, - ); - assert!(cluster.client.get_connection().is_err()); -} + assert_eq!( + redis::cmd("MGET") + .arg(&["{x}key1", "{x}key2"]) + .query(&mut con), + Ok(("foo".to_string(), b"bar".to_vec())) + ); + } -#[test] -fn test_cluster_read_from_replicas() { - let cluster = TestClusterContext::new_with_cluster_client_builder( - 6, - 1, - |builder| builder.read_from_replicas(), - false, - ); - let mut con = cluster.connection(); - - // Write commands would go to the primary nodes - redis::cmd("SET") - .arg("{x}key1") - .arg(b"foo") - .execute(&mut con); - redis::cmd("SET").arg(&["{x}key2", "bar"]).execute(&mut con); - - // Read commands would go to the replica nodes - assert_eq!( - redis::cmd("MGET") - .arg(&["{x}key1", "{x}key2"]) - .query(&mut con), - Ok(("foo".to_string(), b"bar".to_vec())) - ); -} + #[test] + fn test_cluster_with_username_and_password() { + let cluster = TestClusterContext::new_with_cluster_client_builder( + 3, + 0, + |builder| { + builder + .username(RedisCluster::username().to_string()) + .password(RedisCluster::password().to_string()) + }, + false, + ); + cluster.disable_default_user(); -#[test] -fn test_cluster_eval() { - let cluster = TestClusterContext::new(3, 0); - let mut con = cluster.connection(); + let mut con = cluster.connection(); - let rv = redis::cmd("EVAL") - .arg( - r#" + redis::cmd("SET") + .arg("{x}key1") + .arg(b"foo") + .execute(&mut con); + redis::cmd("SET").arg(&["{x}key2", "bar"]).execute(&mut con); + + assert_eq!( + redis::cmd("MGET") + .arg(&["{x}key1", "{x}key2"]) + .query(&mut con), + Ok(("foo".to_string(), b"bar".to_vec())) + ); + } + + #[test] + fn test_cluster_with_bad_password() { + let cluster = TestClusterContext::new_with_cluster_client_builder( + 3, + 0, + |builder| { + builder + .username(RedisCluster::username().to_string()) + .password("not the right password".to_string()) + }, + false, + ); + assert!(cluster.client.get_connection().is_err()); + } + + #[test] + fn test_cluster_read_from_replicas() { + let cluster = TestClusterContext::new_with_cluster_client_builder( + 6, + 1, + |builder| builder.read_from_replicas(), + false, + ); + let mut con = cluster.connection(); + + // Write commands would go to the primary nodes + redis::cmd("SET") + .arg("{x}key1") + .arg(b"foo") + .execute(&mut con); + redis::cmd("SET").arg(&["{x}key2", "bar"]).execute(&mut con); + + // Read commands would go to the replica nodes + assert_eq!( + redis::cmd("MGET") + .arg(&["{x}key1", "{x}key2"]) + .query(&mut con), + Ok(("foo".to_string(), b"bar".to_vec())) + ); + } + + #[test] + fn test_cluster_eval() { + let cluster = TestClusterContext::new(3, 0); + let mut con = cluster.connection(); + + let rv = redis::cmd("EVAL") + .arg( + r#" redis.call("SET", KEYS[1], "1"); redis.call("SET", KEYS[2], "2"); return redis.call("MGET", KEYS[1], KEYS[2]); "#, - ) - .arg("2") - .arg("{x}a") - .arg("{x}b") - .query(&mut con); + ) + .arg("2") + .arg("{x}a") + .arg("{x}b") + .query(&mut con); - assert_eq!(rv, Ok(("1".to_string(), "2".to_string()))); -} + assert_eq!(rv, Ok(("1".to_string(), "2".to_string()))); + } + + #[test] + fn test_cluster_resp3() { + if use_protocol() == ProtocolVersion::RESP2 { + return; + } + let cluster = TestClusterContext::new(3, 0); + + let mut connection = cluster.connection(); + + let _: () = connection.hset("hash", "foo", "baz").unwrap(); + let _: () = connection.hset("hash", "bar", "foobar").unwrap(); + let result: Value = connection.hgetall("hash").unwrap(); -#[test] -fn test_cluster_resp3() { - if use_protocol() == ProtocolVersion::RESP2 { - return; + assert_eq!( + result, + Value::Map(vec![ + ( + Value::BulkString("foo".as_bytes().to_vec()), + Value::BulkString("baz".as_bytes().to_vec()) + ), + ( + Value::BulkString("bar".as_bytes().to_vec()), + Value::BulkString("foobar".as_bytes().to_vec()) + ) + ]) + ); } - let cluster = TestClusterContext::new(3, 0); - - let mut connection = cluster.connection(); - - let _: () = connection.hset("hash", "foo", "baz").unwrap(); - let _: () = connection.hset("hash", "bar", "foobar").unwrap(); - let result: Value = connection.hgetall("hash").unwrap(); - - assert_eq!( - result, - Value::Map(vec![ - ( - Value::BulkString("foo".as_bytes().to_vec()), - Value::BulkString("baz".as_bytes().to_vec()) - ), - ( - Value::BulkString("bar".as_bytes().to_vec()), - Value::BulkString("foobar".as_bytes().to_vec()) - ) - ]) - ); -} -#[test] -fn test_cluster_multi_shard_commands() { - let cluster = TestClusterContext::new(3, 0); + #[test] + fn test_cluster_multi_shard_commands() { + let cluster = TestClusterContext::new(3, 0); - let mut connection = cluster.connection(); + let mut connection = cluster.connection(); - let res: String = connection - .mset(&[("foo", "bar"), ("bar", "foo"), ("baz", "bazz")]) - .unwrap(); - assert_eq!(res, "OK"); - let res: Vec = connection.mget(&["baz", "foo", "bar"]).unwrap(); - assert_eq!(res, vec!["bazz", "bar", "foo"]); -} + let res: String = connection + .mset(&[("foo", "bar"), ("bar", "foo"), ("baz", "bazz")]) + .unwrap(); + assert_eq!(res, "OK"); + let res: Vec = connection.mget(&["baz", "foo", "bar"]).unwrap(); + assert_eq!(res, vec!["bazz", "bar", "foo"]); + } -#[test] -#[cfg(feature = "script")] -fn test_cluster_script() { - let cluster = TestClusterContext::new(3, 0); - let mut con = cluster.connection(); + #[test] + #[cfg(feature = "script")] + fn test_cluster_script() { + let cluster = TestClusterContext::new(3, 0); + let mut con = cluster.connection(); - let script = redis::Script::new( - r#" + let script = redis::Script::new( + r#" redis.call("SET", KEYS[1], "1"); redis.call("SET", KEYS[2], "2"); return redis.call("MGET", KEYS[1], KEYS[2]); "#, - ); - - let rv = script.key("{x}a").key("{x}b").invoke(&mut con); - assert_eq!(rv, Ok(("1".to_string(), "2".to_string()))); -} + ); -#[test] -fn test_cluster_pipeline() { - let cluster = TestClusterContext::new(3, 0); - cluster.wait_for_cluster_up(); - let mut con = cluster.connection(); + let rv = script.key("{x}a").key("{x}b").invoke(&mut con); + assert_eq!(rv, Ok(("1".to_string(), "2".to_string()))); + } - let resp = cluster_pipe() - .cmd("SET") - .arg("key_1") - .arg(42) - .query::>(&mut con) - .unwrap(); + #[test] + fn test_cluster_pipeline() { + let cluster = TestClusterContext::new(3, 0); + cluster.wait_for_cluster_up(); + let mut con = cluster.connection(); + + let resp = cluster_pipe() + .cmd("SET") + .arg("key_1") + .arg(42) + .query::>(&mut con) + .unwrap(); + + assert_eq!(resp, vec!["OK".to_string()]); + } - assert_eq!(resp, vec!["OK".to_string()]); -} + #[test] + fn test_cluster_pipeline_multiple_keys() { + use redis::FromRedisValue; + let cluster = TestClusterContext::new(3, 0); + cluster.wait_for_cluster_up(); + let mut con = cluster.connection(); + + let resp = cluster_pipe() + .cmd("HSET") + .arg("hash_1") + .arg("key_1") + .arg("value_1") + .cmd("ZADD") + .arg("zset") + .arg(1) + .arg("zvalue_2") + .query::>(&mut con) + .unwrap(); + + assert_eq!(resp, vec![1i64, 1i64]); + + let resp = cluster_pipe() + .cmd("HGET") + .arg("hash_1") + .arg("key_1") + .cmd("ZCARD") + .arg("zset") + .query::>(&mut con) + .unwrap(); + + let resp_1: String = FromRedisValue::from_redis_value(&resp[0]).unwrap(); + assert_eq!(resp_1, "value_1".to_string()); + + let resp_2: usize = FromRedisValue::from_redis_value(&resp[1]).unwrap(); + assert_eq!(resp_2, 1); + } -#[test] -fn test_cluster_pipeline_multiple_keys() { - use redis::FromRedisValue; - let cluster = TestClusterContext::new(3, 0); - cluster.wait_for_cluster_up(); - let mut con = cluster.connection(); - - let resp = cluster_pipe() - .cmd("HSET") - .arg("hash_1") - .arg("key_1") - .arg("value_1") - .cmd("ZADD") - .arg("zset") - .arg(1) - .arg("zvalue_2") - .query::>(&mut con) - .unwrap(); - - assert_eq!(resp, vec![1i64, 1i64]); - - let resp = cluster_pipe() - .cmd("HGET") - .arg("hash_1") - .arg("key_1") - .cmd("ZCARD") - .arg("zset") - .query::>(&mut con) - .unwrap(); - - let resp_1: String = FromRedisValue::from_redis_value(&resp[0]).unwrap(); - assert_eq!(resp_1, "value_1".to_string()); - - let resp_2: usize = FromRedisValue::from_redis_value(&resp[1]).unwrap(); - assert_eq!(resp_2, 1); -} + #[test] + fn test_cluster_pipeline_invalid_command() { + let cluster = TestClusterContext::new(3, 0); + cluster.wait_for_cluster_up(); + let mut con = cluster.connection(); + + let err = cluster_pipe() + .cmd("SET") + .arg("foo") + .arg(42) + .ignore() + .cmd(" SCRIPT kill ") + .query::<()>(&mut con) + .unwrap_err(); -#[test] -fn test_cluster_pipeline_invalid_command() { - let cluster = TestClusterContext::new(3, 0); - cluster.wait_for_cluster_up(); - let mut con = cluster.connection(); - - let err = cluster_pipe() - .cmd("SET") - .arg("foo") - .arg(42) - .ignore() - .cmd(" SCRIPT kill ") - .query::<()>(&mut con) - .unwrap_err(); - - assert_eq!( + assert_eq!( err.to_string(), "This command cannot be safely routed in cluster mode - ClientError: Command 'SCRIPT KILL' can't be executed in a cluster pipeline." ); - let err = cluster_pipe().keys("*").query::<()>(&mut con).unwrap_err(); + let err = cluster_pipe().keys("*").query::<()>(&mut con).unwrap_err(); - assert_eq!( + assert_eq!( err.to_string(), "This command cannot be safely routed in cluster mode - ClientError: Command 'KEYS' can't be executed in a cluster pipeline." ); -} - -#[test] -fn test_cluster_pipeline_command_ordering() { - let cluster = TestClusterContext::new(3, 0); - cluster.wait_for_cluster_up(); - let mut con = cluster.connection(); - let mut pipe = cluster_pipe(); - - let mut queries = Vec::new(); - let mut expected = Vec::new(); - for i in 0..100 { - queries.push(format!("foo{i}")); - expected.push(format!("bar{i}")); - pipe.set(&queries[i], &expected[i]).ignore(); - } - pipe.execute(&mut con); - - pipe.clear(); - for q in &queries { - pipe.get(q); } - let got = pipe.query::>(&mut con).unwrap(); - assert_eq!(got, expected); -} - -#[test] -#[ignore] // Flaky -fn test_cluster_pipeline_ordering_with_improper_command() { - let cluster = TestClusterContext::new(3, 0); - cluster.wait_for_cluster_up(); - let mut con = cluster.connection(); - let mut pipe = cluster_pipe(); - - let mut queries = Vec::new(); - let mut expected = Vec::new(); - for i in 0..10 { - if i == 5 { - pipe.cmd("hset").arg("foo").ignore(); - } else { - let query = format!("foo{i}"); - let r = format!("bar{i}"); - pipe.set(&query, &r).ignore(); - queries.push(query); - expected.push(r); + #[test] + fn test_cluster_pipeline_command_ordering() { + let cluster = TestClusterContext::new(3, 0); + cluster.wait_for_cluster_up(); + let mut con = cluster.connection(); + let mut pipe = cluster_pipe(); + + let mut queries = Vec::new(); + let mut expected = Vec::new(); + for i in 0..100 { + queries.push(format!("foo{i}")); + expected.push(format!("bar{i}")); + pipe.set(&queries[i], &expected[i]).ignore(); } - } - pipe.query::<()>(&mut con).unwrap_err(); + pipe.execute(&mut con); - std::thread::sleep(std::time::Duration::from_secs(5)); + pipe.clear(); + for q in &queries { + pipe.get(q); + } - pipe.clear(); - for q in &queries { - pipe.get(q); + let got = pipe.query::>(&mut con).unwrap(); + assert_eq!(got, expected); } - let got = pipe.query::>(&mut con).unwrap(); - assert_eq!(got, expected); -} - -#[test] -fn test_cluster_retries() { - let name = "tryagain"; - - let requests = atomic::AtomicUsize::new(0); - let MockEnv { - mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]).retries(5), - name, - move |cmd: &[u8], _| { - respond_startup(name, cmd)?; - - match requests.fetch_add(1, atomic::Ordering::SeqCst) { - 0..=4 => Err(parse_redis_value(b"-TRYAGAIN mock\r\n")), - _ => Err(Ok(Value::BulkString(b"123".to_vec()))), + #[test] + #[ignore] // Flaky + fn test_cluster_pipeline_ordering_with_improper_command() { + let cluster = TestClusterContext::new(3, 0); + cluster.wait_for_cluster_up(); + let mut con = cluster.connection(); + let mut pipe = cluster_pipe(); + + let mut queries = Vec::new(); + let mut expected = Vec::new(); + for i in 0..10 { + if i == 5 { + pipe.cmd("hset").arg("foo").ignore(); + } else { + let query = format!("foo{i}"); + let r = format!("bar{i}"); + pipe.set(&query, &r).ignore(); + queries.push(query); + expected.push(r); } - }, - ); + } + pipe.query::<()>(&mut con).unwrap_err(); - let value = cmd("GET").arg("test").query::>(&mut connection); + std::thread::sleep(std::time::Duration::from_secs(5)); - assert_eq!(value, Ok(Some(123))); -} + pipe.clear(); + for q in &queries { + pipe.get(q); + } -#[test] -fn test_cluster_exhaust_retries() { - let name = "tryagain_exhaust_retries"; - - let requests = Arc::new(atomic::AtomicUsize::new(0)); - - let MockEnv { - mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]).retries(2), - name, - { - let requests = requests.clone(); + let got = pipe.query::>(&mut con).unwrap(); + assert_eq!(got, expected); + } + + #[test] + fn test_cluster_retries() { + let name = "tryagain"; + + let requests = atomic::AtomicUsize::new(0); + let MockEnv { + mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]).retries(5), + name, move |cmd: &[u8], _| { respond_startup(name, cmd)?; - requests.fetch_add(1, atomic::Ordering::SeqCst); - Err(parse_redis_value(b"-TRYAGAIN mock\r\n")) - } - }, - ); - let result = cmd("GET").arg("test").query::>(&mut connection); + match requests.fetch_add(1, atomic::Ordering::SeqCst) { + 0..=4 => Err(parse_redis_value(b"-TRYAGAIN mock\r\n")), + _ => Err(Ok(Value::BulkString(b"123".to_vec()))), + } + }, + ); + + let value = cmd("GET").arg("test").query::>(&mut connection); - match result { - Ok(_) => panic!("result should be an error"), - Err(e) => match e.kind() { - ErrorKind::TryAgain => {} - _ => panic!("Expected TryAgain but got {:?}", e.kind()), - }, + assert_eq!(value, Ok(Some(123))); } - assert_eq!(requests.load(atomic::Ordering::SeqCst), 3); -} -#[test] -fn test_cluster_move_error_when_new_node_is_added() { - let name = "rebuild_with_extra_nodes"; - - let requests = atomic::AtomicUsize::new(0); - let started = atomic::AtomicBool::new(false); - let MockEnv { - mut connection, - handler: _handler, - .. - } = MockEnv::new(name, move |cmd: &[u8], port| { - if !started.load(atomic::Ordering::SeqCst) { - respond_startup(name, cmd)?; - } - started.store(true, atomic::Ordering::SeqCst); + #[test] + fn test_cluster_exhaust_retries() { + let name = "tryagain_exhaust_retries"; + + let requests = Arc::new(atomic::AtomicUsize::new(0)); + + let MockEnv { + mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]).retries(2), + name, + { + let requests = requests.clone(); + move |cmd: &[u8], _| { + respond_startup(name, cmd)?; + requests.fetch_add(1, atomic::Ordering::SeqCst); + Err(parse_redis_value(b"-TRYAGAIN mock\r\n")) + } + }, + ); + + let result = cmd("GET").arg("test").query::>(&mut connection); - if contains_slice(cmd, b"PING") { - return Err(Ok(Value::SimpleString("OK".into()))); + match result { + Ok(_) => panic!("result should be an error"), + Err(e) => match e.kind() { + ErrorKind::TryAgain => {} + _ => panic!("Expected TryAgain but got {:?}", e.kind()), + }, } + assert_eq!(requests.load(atomic::Ordering::SeqCst), 3); + } - let i = requests.fetch_add(1, atomic::Ordering::SeqCst); + #[test] + fn test_cluster_move_error_when_new_node_is_added() { + let name = "rebuild_with_extra_nodes"; + + let requests = atomic::AtomicUsize::new(0); + let started = atomic::AtomicBool::new(false); + let MockEnv { + mut connection, + handler: _handler, + .. + } = MockEnv::new(name, move |cmd: &[u8], port| { + if !started.load(atomic::Ordering::SeqCst) { + respond_startup(name, cmd)?; + } + started.store(true, atomic::Ordering::SeqCst); - match i { - // Respond that the key exists on a node that does not yet have a connection: - 0 => Err(parse_redis_value(b"-MOVED 123\r\n")), - // Respond with the new masters - 1 => Err(Ok(Value::Array(vec![ - Value::Array(vec![ - Value::Int(0), - Value::Int(1), + if contains_slice(cmd, b"PING") { + return Err(Ok(Value::SimpleString("OK".into()))); + } + + let i = requests.fetch_add(1, atomic::Ordering::SeqCst); + + match i { + // Respond that the key exists on a node that does not yet have a connection: + 0 => Err(parse_redis_value(b"-MOVED 123\r\n")), + // Respond with the new masters + 1 => Err(Ok(Value::Array(vec![ Value::Array(vec![ - Value::BulkString(name.as_bytes().to_vec()), - Value::Int(6379), + Value::Int(0), + Value::Int(1), + Value::Array(vec![ + Value::BulkString(name.as_bytes().to_vec()), + Value::Int(6379), + ]), ]), - ]), - Value::Array(vec![ - Value::Int(2), - Value::Int(16383), Value::Array(vec![ - Value::BulkString(name.as_bytes().to_vec()), - Value::Int(6380), + Value::Int(2), + Value::Int(16383), + Value::Array(vec![ + Value::BulkString(name.as_bytes().to_vec()), + Value::Int(6380), + ]), ]), - ]), - ]))), - _ => { - // Check that the correct node receives the request after rebuilding - assert_eq!(port, 6380); - Err(Ok(Value::BulkString(b"123".to_vec()))) + ]))), + _ => { + // Check that the correct node receives the request after rebuilding + assert_eq!(port, 6380); + Err(Ok(Value::BulkString(b"123".to_vec()))) + } } - } - }); + }); - let value = cmd("GET").arg("test").query::>(&mut connection); + let value = cmd("GET").arg("test").query::>(&mut connection); - assert_eq!(value, Ok(Some(123))); -} + assert_eq!(value, Ok(Some(123))); + } -#[test] -fn test_cluster_ask_redirect() { - let name = "node"; - let completed = Arc::new(AtomicI32::new(0)); - let MockEnv { - mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]), - name, - { - move |cmd: &[u8], port| { - respond_startup_two_nodes(name, cmd)?; - // Error twice with io-error, ensure connection is reestablished w/out calling - // other node (i.e., not doing a full slot rebuild) - let count = completed.fetch_add(1, Ordering::SeqCst); - match port { - 6379 => match count { - 0 => Err(parse_redis_value(b"-ASK 14000 node:6380\r\n")), - _ => panic!("Node should not be called now"), - }, - 6380 => match count { - 1 => { - assert!(contains_slice(cmd, b"ASKING")); - Err(Ok(Value::Okay)) - } - 2 => { - assert!(contains_slice(cmd, b"GET")); - Err(Ok(Value::BulkString(b"123".to_vec()))) - } - _ => panic!("Node should not be called now"), - }, - _ => panic!("Wrong node"), + #[test] + fn test_cluster_ask_redirect() { + let name = "node"; + let completed = Arc::new(AtomicI32::new(0)); + let MockEnv { + mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]), + name, + { + move |cmd: &[u8], port| { + respond_startup_two_nodes(name, cmd)?; + // Error twice with io-error, ensure connection is reestablished w/out calling + // other node (i.e., not doing a full slot rebuild) + let count = completed.fetch_add(1, Ordering::SeqCst); + match port { + 6379 => match count { + 0 => Err(parse_redis_value(b"-ASK 14000 node:6380\r\n")), + _ => panic!("Node should not be called now"), + }, + 6380 => match count { + 1 => { + assert!(contains_slice(cmd, b"ASKING")); + Err(Ok(Value::Okay)) + } + 2 => { + assert!(contains_slice(cmd, b"GET")); + Err(Ok(Value::BulkString(b"123".to_vec()))) + } + _ => panic!("Node should not be called now"), + }, + _ => panic!("Wrong node"), + } } - } - }, - ); + }, + ); - let value = cmd("GET").arg("test").query::>(&mut connection); + let value = cmd("GET").arg("test").query::>(&mut connection); - assert_eq!(value, Ok(Some(123))); -} + assert_eq!(value, Ok(Some(123))); + } -#[test] -fn test_cluster_ask_error_when_new_node_is_added() { - let name = "ask_with_extra_nodes"; + #[test] + fn test_cluster_ask_error_when_new_node_is_added() { + let name = "ask_with_extra_nodes"; + + let requests = atomic::AtomicUsize::new(0); + let started = atomic::AtomicBool::new(false); + + let MockEnv { + mut connection, + handler: _handler, + .. + } = MockEnv::new(name, move |cmd: &[u8], port| { + if !started.load(atomic::Ordering::SeqCst) { + respond_startup(name, cmd)?; + } + started.store(true, atomic::Ordering::SeqCst); - let requests = atomic::AtomicUsize::new(0); - let started = atomic::AtomicBool::new(false); + if contains_slice(cmd, b"PING") { + return Err(Ok(Value::SimpleString("OK".into()))); + } - let MockEnv { - mut connection, - handler: _handler, - .. - } = MockEnv::new(name, move |cmd: &[u8], port| { - if !started.load(atomic::Ordering::SeqCst) { - respond_startup(name, cmd)?; - } - started.store(true, atomic::Ordering::SeqCst); + let i = requests.fetch_add(1, atomic::Ordering::SeqCst); + + match i { + // Respond that the key exists on a node that does not yet have a connection: + 0 => Err(parse_redis_value( + format!("-ASK 123 {name}:6380\r\n").as_bytes(), + )), + 1 => { + assert_eq!(port, 6380); + assert!(contains_slice(cmd, b"ASKING")); + Err(Ok(Value::Okay)) + } + 2 => { + assert_eq!(port, 6380); + assert!(contains_slice(cmd, b"GET")); + Err(Ok(Value::BulkString(b"123".to_vec()))) + } + _ => { + panic!("Unexpected request: {:?}", cmd); + } + } + }); - if contains_slice(cmd, b"PING") { - return Err(Ok(Value::SimpleString("OK".into()))); - } + let value = cmd("GET").arg("test").query::>(&mut connection); - let i = requests.fetch_add(1, atomic::Ordering::SeqCst); - - match i { - // Respond that the key exists on a node that does not yet have a connection: - 0 => Err(parse_redis_value( - format!("-ASK 123 {name}:6380\r\n").as_bytes(), - )), - 1 => { - assert_eq!(port, 6380); - assert!(contains_slice(cmd, b"ASKING")); - Err(Ok(Value::Okay)) - } - 2 => { - assert_eq!(port, 6380); - assert!(contains_slice(cmd, b"GET")); - Err(Ok(Value::BulkString(b"123".to_vec()))) - } - _ => { - panic!("Unexpected request: {:?}", cmd); - } - } - }); + assert_eq!(value, Ok(Some(123))); + } - let value = cmd("GET").arg("test").query::>(&mut connection); + #[test] + fn test_cluster_replica_read() { + let name = "node"; + + // requests should route to replica + let MockEnv { + mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |cmd: &[u8], port| { + respond_startup_with_replica(name, cmd)?; - assert_eq!(value, Ok(Some(123))); -} + match port { + 6380 => Err(Ok(Value::BulkString(b"123".to_vec()))), + _ => panic!("Wrong node"), + } + }, + ); -#[test] -fn test_cluster_replica_read() { - let name = "node"; - - // requests should route to replica - let MockEnv { - mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |cmd: &[u8], port| { - respond_startup_with_replica(name, cmd)?; - - match port { - 6380 => Err(Ok(Value::BulkString(b"123".to_vec()))), - _ => panic!("Wrong node"), - } - }, - ); + let value = cmd("GET").arg("test").query::>(&mut connection); + assert_eq!(value, Ok(Some(123))); + + // requests should route to primary + let MockEnv { + mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |cmd: &[u8], port| { + respond_startup_with_replica(name, cmd)?; + match port { + 6379 => Err(Ok(Value::SimpleString("OK".into()))), + _ => panic!("Wrong node"), + } + }, + ); - let value = cmd("GET").arg("test").query::>(&mut connection); - assert_eq!(value, Ok(Some(123))); - - // requests should route to primary - let MockEnv { - mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |cmd: &[u8], port| { - respond_startup_with_replica(name, cmd)?; - match port { - 6379 => Err(Ok(Value::SimpleString("OK".into()))), - _ => panic!("Wrong node"), - } - }, - ); + let value = cmd("SET") + .arg("test") + .arg("123") + .query::>(&mut connection); + assert_eq!(value, Ok(Some(Value::SimpleString("OK".to_owned())))); + } - let value = cmd("SET") - .arg("test") - .arg("123") - .query::>(&mut connection); - assert_eq!(value, Ok(Some(Value::SimpleString("OK".to_owned())))); -} + #[test] + fn test_cluster_io_error() { + let name = "node"; + let completed = Arc::new(AtomicI32::new(0)); + let MockEnv { + mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]).retries(2), + name, + move |cmd: &[u8], port| { + respond_startup_two_nodes(name, cmd)?; + // Error twice with io-error, ensure connection is reestablished w/out calling + // other node (i.e., not doing a full slot rebuild) + match port { + 6380 => panic!("Node should not be called"), + _ => match completed.fetch_add(1, Ordering::SeqCst) { + 0..=1 => Err(Err(RedisError::from(std::io::Error::new( + std::io::ErrorKind::ConnectionReset, + "mock-io-error", + )))), + _ => Err(Ok(Value::BulkString(b"123".to_vec()))), + }, + } + }, + ); -#[test] -fn test_cluster_io_error() { - let name = "node"; - let completed = Arc::new(AtomicI32::new(0)); - let MockEnv { - mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]).retries(2), - name, - move |cmd: &[u8], port| { - respond_startup_two_nodes(name, cmd)?; - // Error twice with io-error, ensure connection is reestablished w/out calling - // other node (i.e., not doing a full slot rebuild) - match port { - 6380 => panic!("Node should not be called"), - _ => match completed.fetch_add(1, Ordering::SeqCst) { - 0..=1 => Err(Err(RedisError::from(std::io::Error::new( - std::io::ErrorKind::ConnectionReset, - "mock-io-error", - )))), - _ => Err(Ok(Value::BulkString(b"123".to_vec()))), - }, + let value = cmd("GET").arg("test").query::>(&mut connection); + + assert_eq!(value, Ok(Some(123))); + } + + #[test] + fn test_cluster_non_retryable_error_should_not_retry() { + let name = "node"; + let completed = Arc::new(AtomicI32::new(0)); + let MockEnv { mut connection, .. } = MockEnv::new(name, { + let completed = completed.clone(); + move |cmd: &[u8], _| { + respond_startup_two_nodes(name, cmd)?; + // Error twice with io-error, ensure connection is reestablished w/out calling + // other node (i.e., not doing a full slot rebuild) + completed.fetch_add(1, Ordering::SeqCst); + Err(Err((ErrorKind::ReadOnly, "").into())) } - }, - ); + }); - let value = cmd("GET").arg("test").query::>(&mut connection); + let value = cmd("GET").arg("test").query::>(&mut connection); - assert_eq!(value, Ok(Some(123))); -} + match value { + Ok(_) => panic!("result should be an error"), + Err(e) => match e.kind() { + ErrorKind::ReadOnly => {} + _ => panic!("Expected ReadOnly but got {:?}", e.kind()), + }, + } + assert_eq!(completed.load(Ordering::SeqCst), 1); + } -#[test] -fn test_cluster_non_retryable_error_should_not_retry() { - let name = "node"; - let completed = Arc::new(AtomicI32::new(0)); - let MockEnv { mut connection, .. } = MockEnv::new(name, { - let completed = completed.clone(); - move |cmd: &[u8], _| { - respond_startup_two_nodes(name, cmd)?; - // Error twice with io-error, ensure connection is reestablished w/out calling - // other node (i.e., not doing a full slot rebuild) - completed.fetch_add(1, Ordering::SeqCst); - Err(Err((ErrorKind::ReadOnly, "").into())) + fn test_cluster_fan_out( + command: &'static str, + expected_ports: Vec, + slots_config: Option>, + ) { + let name = "node"; + let found_ports = Arc::new(std::sync::Mutex::new(Vec::new())); + let ports_clone = found_ports.clone(); + let mut cmd = redis::Cmd::new(); + for arg in command.split_whitespace() { + cmd.arg(arg); } - }); + let packed_cmd = cmd.get_packed_command(); + // requests should route to replica + let MockEnv { + mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |received_cmd: &[u8], port| { + respond_startup_with_replica_using_config( + name, + received_cmd, + slots_config.clone(), + )?; + if received_cmd == packed_cmd { + ports_clone.lock().unwrap().push(port); + return Err(Ok(Value::SimpleString("OK".into()))); + } + Ok(()) + }, + ); - let value = cmd("GET").arg("test").query::>(&mut connection); + let _ = cmd.query::>(&mut connection); + found_ports.lock().unwrap().sort(); + // MockEnv creates 2 mock connections. + assert_eq!(*found_ports.lock().unwrap(), expected_ports); + } - match value { - Ok(_) => panic!("result should be an error"), - Err(e) => match e.kind() { - ErrorKind::ReadOnly => {} - _ => panic!("Expected ReadOnly but got {:?}", e.kind()), - }, + #[test] + fn test_cluster_fan_out_to_all_primaries() { + test_cluster_fan_out("FLUSHALL", vec![6379, 6381], None); } - assert_eq!(completed.load(Ordering::SeqCst), 1); -} -fn test_cluster_fan_out( - command: &'static str, - expected_ports: Vec, - slots_config: Option>, -) { - let name = "node"; - let found_ports = Arc::new(std::sync::Mutex::new(Vec::new())); - let ports_clone = found_ports.clone(); - let mut cmd = redis::Cmd::new(); - for arg in command.split_whitespace() { - cmd.arg(arg); + #[test] + fn test_cluster_fan_out_to_all_nodes() { + test_cluster_fan_out("CONFIG SET", vec![6379, 6380, 6381, 6382], None); } - let packed_cmd = cmd.get_packed_command(); - // requests should route to replica - let MockEnv { - mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |received_cmd: &[u8], port| { - respond_startup_with_replica_using_config(name, received_cmd, slots_config.clone())?; - if received_cmd == packed_cmd { - ports_clone.lock().unwrap().push(port); - return Err(Ok(Value::SimpleString("OK".into()))); - } - Ok(()) - }, - ); - let _ = cmd.query::>(&mut connection); - found_ports.lock().unwrap().sort(); - // MockEnv creates 2 mock connections. - assert_eq!(*found_ports.lock().unwrap(), expected_ports); -} + #[test] + fn test_cluster_fan_out_out_once_to_each_primary_when_no_replicas_are_available() { + test_cluster_fan_out( + "CONFIG SET", + vec![6379, 6381], + Some(vec![ + MockSlotRange { + primary_port: 6379, + replica_ports: Vec::new(), + slot_range: (0..8191), + }, + MockSlotRange { + primary_port: 6381, + replica_ports: Vec::new(), + slot_range: (8192..16383), + }, + ]), + ); + } -#[test] -fn test_cluster_fan_out_to_all_primaries() { - test_cluster_fan_out("FLUSHALL", vec![6379, 6381], None); -} + #[test] + fn test_cluster_fan_out_out_once_even_if_primary_has_multiple_slot_ranges() { + test_cluster_fan_out( + "CONFIG SET", + vec![6379, 6380, 6381, 6382], + Some(vec![ + MockSlotRange { + primary_port: 6379, + replica_ports: vec![6380], + slot_range: (0..4000), + }, + MockSlotRange { + primary_port: 6381, + replica_ports: vec![6382], + slot_range: (4001..8191), + }, + MockSlotRange { + primary_port: 6379, + replica_ports: vec![6380], + slot_range: (8192..8200), + }, + MockSlotRange { + primary_port: 6381, + replica_ports: vec![6382], + slot_range: (8201..16383), + }, + ]), + ); + } -#[test] -fn test_cluster_fan_out_to_all_nodes() { - test_cluster_fan_out("CONFIG SET", vec![6379, 6380, 6381, 6382], None); -} + #[test] + fn test_cluster_split_multi_shard_command_and_combine_arrays_of_values() { + let name = "test_cluster_split_multi_shard_command_and_combine_arrays_of_values"; + let mut cmd = cmd("MGET"); + cmd.arg("foo").arg("bar").arg("baz"); + let MockEnv { + mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |received_cmd: &[u8], port| { + respond_startup_with_replica_using_config(name, received_cmd, None)?; + let cmd_str = std::str::from_utf8(received_cmd).unwrap(); + let results = ["foo", "bar", "baz"] + .iter() + .filter_map(|expected_key| { + if cmd_str.contains(expected_key) { + Some(Value::BulkString( + format!("{expected_key}-{port}").into_bytes(), + )) + } else { + None + } + }) + .collect(); + Err(Ok(Value::Array(results))) + }, + ); -#[test] -fn test_cluster_fan_out_out_once_to_each_primary_when_no_replicas_are_available() { - test_cluster_fan_out( - "CONFIG SET", - vec![6379, 6381], - Some(vec![ - MockSlotRange { - primary_port: 6379, - replica_ports: Vec::new(), - slot_range: (0..8191), + let result = cmd.query::>(&mut connection).unwrap(); + assert_eq!(result, vec!["foo-6382", "bar-6380", "baz-6380"]); + } + + #[test] + fn test_cluster_route_correctly_on_packed_transaction_with_single_node_requests() { + let name = "test_cluster_route_correctly_on_packed_transaction_with_single_node_requests"; + let mut pipeline = redis::pipe(); + pipeline.atomic().set("foo", "bar").get("foo"); + let packed_pipeline = pipeline.get_packed_pipeline(); + + let MockEnv { + mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |received_cmd: &[u8], port| { + respond_startup_with_replica_using_config(name, received_cmd, None)?; + if port == 6381 { + let results = vec![ + Value::BulkString("OK".as_bytes().to_vec()), + Value::BulkString("QUEUED".as_bytes().to_vec()), + Value::BulkString("QUEUED".as_bytes().to_vec()), + Value::Array(vec![ + Value::BulkString("OK".as_bytes().to_vec()), + Value::BulkString("bar".as_bytes().to_vec()), + ]), + ]; + return Err(Ok(Value::Array(results))); + } + Err(Err(RedisError::from(std::io::Error::new( + std::io::ErrorKind::ConnectionReset, + format!("wrong port: {port}"), + )))) }, - MockSlotRange { - primary_port: 6381, - replica_ports: Vec::new(), - slot_range: (8192..16383), + ); + + let result = connection + .req_packed_commands(&packed_pipeline, 3, 1) + .unwrap(); + assert_eq!( + result, + vec![ + Value::BulkString("OK".as_bytes().to_vec()), + Value::BulkString("bar".as_bytes().to_vec()), + ] + ); + } + + #[test] + fn test_cluster_route_correctly_on_packed_transaction_with_single_node_requests2() { + let name = "test_cluster_route_correctly_on_packed_transaction_with_single_node_requests2"; + let mut pipeline = redis::pipe(); + pipeline.atomic().set("foo", "bar").get("foo"); + let packed_pipeline = pipeline.get_packed_pipeline(); + let results = vec![ + Value::BulkString("OK".as_bytes().to_vec()), + Value::BulkString("QUEUED".as_bytes().to_vec()), + Value::BulkString("QUEUED".as_bytes().to_vec()), + Value::Array(vec![ + Value::BulkString("OK".as_bytes().to_vec()), + Value::BulkString("bar".as_bytes().to_vec()), + ]), + ]; + let expected_result = Value::Array(results); + let cloned_result = expected_result.clone(); + + let MockEnv { + mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |received_cmd: &[u8], port| { + respond_startup_with_replica_using_config(name, received_cmd, None)?; + if port == 6381 { + return Err(Ok(cloned_result.clone())); + } + Err(Err(RedisError::from(std::io::Error::new( + std::io::ErrorKind::ConnectionReset, + format!("wrong port: {port}"), + )))) }, - ]), - ); -} + ); -#[test] -fn test_cluster_fan_out_out_once_even_if_primary_has_multiple_slot_ranges() { - test_cluster_fan_out( - "CONFIG SET", - vec![6379, 6380, 6381, 6382], - Some(vec![ + let result = connection.req_packed_command(&packed_pipeline).unwrap(); + assert_eq!(result, expected_result); + } + + #[test] + fn test_cluster_can_be_created_with_partial_slot_coverage() { + let name = "test_cluster_can_be_created_with_partial_slot_coverage"; + let slots_config = Some(vec![ MockSlotRange { primary_port: 6379, - replica_ports: vec![6380], - slot_range: (0..4000), + replica_ports: vec![], + slot_range: (0..8000), }, MockSlotRange { primary_port: 6381, - replica_ports: vec![6382], - slot_range: (4001..8191), - }, - MockSlotRange { - primary_port: 6379, - replica_ports: vec![6380], - slot_range: (8192..8200), + replica_ports: vec![], + slot_range: (8201..16380), }, - MockSlotRange { - primary_port: 6381, - replica_ports: vec![6382], - slot_range: (8201..16383), + ]); + + let MockEnv { + mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |received_cmd: &[u8], _| { + respond_startup_with_replica_using_config( + name, + received_cmd, + slots_config.clone(), + )?; + Err(Ok(Value::SimpleString("PONG".into()))) }, - ]), - ); -} - -#[test] -fn test_cluster_split_multi_shard_command_and_combine_arrays_of_values() { - let name = "test_cluster_split_multi_shard_command_and_combine_arrays_of_values"; - let mut cmd = cmd("MGET"); - cmd.arg("foo").arg("bar").arg("baz"); - let MockEnv { - mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |received_cmd: &[u8], port| { - respond_startup_with_replica_using_config(name, received_cmd, None)?; - let cmd_str = std::str::from_utf8(received_cmd).unwrap(); - let results = ["foo", "bar", "baz"] - .iter() - .filter_map(|expected_key| { - if cmd_str.contains(expected_key) { - Some(Value::BulkString( - format!("{expected_key}-{port}").into_bytes(), - )) - } else { - None - } - }) - .collect(); - Err(Ok(Value::Array(results))) - }, - ); - - let result = cmd.query::>(&mut connection).unwrap(); - assert_eq!(result, vec!["foo-6382", "bar-6380", "baz-6380"]); -} - -#[test] -fn test_cluster_route_correctly_on_packed_transaction_with_single_node_requests() { - let name = "test_cluster_route_correctly_on_packed_transaction_with_single_node_requests"; - let mut pipeline = redis::pipe(); - pipeline.atomic().set("foo", "bar").get("foo"); - let packed_pipeline = pipeline.get_packed_pipeline(); - - let MockEnv { - mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |received_cmd: &[u8], port| { - respond_startup_with_replica_using_config(name, received_cmd, None)?; - if port == 6381 { - let results = vec![ - Value::BulkString("OK".as_bytes().to_vec()), - Value::BulkString("QUEUED".as_bytes().to_vec()), - Value::BulkString("QUEUED".as_bytes().to_vec()), - Value::Array(vec![ - Value::BulkString("OK".as_bytes().to_vec()), - Value::BulkString("bar".as_bytes().to_vec()), - ]), - ]; - return Err(Ok(Value::Array(results))); - } - Err(Err(RedisError::from(std::io::Error::new( - std::io::ErrorKind::ConnectionReset, - format!("wrong port: {port}"), - )))) - }, - ); - - let result = connection - .req_packed_commands(&packed_pipeline, 3, 1) - .unwrap(); - assert_eq!( - result, - vec![ - Value::BulkString("OK".as_bytes().to_vec()), - Value::BulkString("bar".as_bytes().to_vec()), - ] - ); -} - -#[test] -fn test_cluster_route_correctly_on_packed_transaction_with_single_node_requests2() { - let name = "test_cluster_route_correctly_on_packed_transaction_with_single_node_requests2"; - let mut pipeline = redis::pipe(); - pipeline.atomic().set("foo", "bar").get("foo"); - let packed_pipeline = pipeline.get_packed_pipeline(); - let results = vec![ - Value::BulkString("OK".as_bytes().to_vec()), - Value::BulkString("QUEUED".as_bytes().to_vec()), - Value::BulkString("QUEUED".as_bytes().to_vec()), - Value::Array(vec![ - Value::BulkString("OK".as_bytes().to_vec()), - Value::BulkString("bar".as_bytes().to_vec()), - ]), - ]; - let expected_result = Value::Array(results); - let cloned_result = expected_result.clone(); - - let MockEnv { - mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |received_cmd: &[u8], port| { - respond_startup_with_replica_using_config(name, received_cmd, None)?; - if port == 6381 { - return Err(Ok(cloned_result.clone())); - } - Err(Err(RedisError::from(std::io::Error::new( - std::io::ErrorKind::ConnectionReset, - format!("wrong port: {port}"), - )))) - }, - ); - - let result = connection.req_packed_command(&packed_pipeline).unwrap(); - assert_eq!(result, expected_result); -} - -#[test] -fn test_cluster_can_be_created_with_partial_slot_coverage() { - let name = "test_cluster_can_be_created_with_partial_slot_coverage"; - let slots_config = Some(vec![ - MockSlotRange { - primary_port: 6379, - replica_ports: vec![], - slot_range: (0..8000), - }, - MockSlotRange { - primary_port: 6381, - replica_ports: vec![], - slot_range: (8201..16380), - }, - ]); - - let MockEnv { - mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |received_cmd: &[u8], _| { - respond_startup_with_replica_using_config(name, received_cmd, slots_config.clone())?; - Err(Ok(Value::SimpleString("PONG".into()))) - }, - ); - - let res = connection.req_command(&redis::cmd("PING")); - assert!(res.is_ok()); -} - -#[cfg(feature = "tls-rustls")] -mod mtls_test { - use super::*; - use crate::support::mtls_test::create_cluster_client_from_cluster; - use redis::ConnectionInfo; + ); - #[test] - fn test_cluster_basics_with_mtls() { - let cluster = TestClusterContext::new_with_mtls(3, 0); + let res = connection.req_command(&redis::cmd("PING")); + assert!(res.is_ok()); + } - let client = create_cluster_client_from_cluster(&cluster, true).unwrap(); - let mut con = client.get_connection().unwrap(); + #[cfg(feature = "tls-rustls")] + mod mtls_test { + use super::*; + use crate::support::mtls_test::create_cluster_client_from_cluster; + use redis::ConnectionInfo; + + #[test] + fn test_cluster_basics_with_mtls() { + let cluster = TestClusterContext::new_with_mtls(3, 0); + + let client = create_cluster_client_from_cluster(&cluster, true).unwrap(); + let mut con = client.get_connection().unwrap(); + + redis::cmd("SET") + .arg("{x}key1") + .arg(b"foo") + .execute(&mut con); + redis::cmd("SET").arg(&["{x}key2", "bar"]).execute(&mut con); + + assert_eq!( + redis::cmd("MGET") + .arg(&["{x}key1", "{x}key2"]) + .query(&mut con), + Ok(("foo".to_string(), b"bar".to_vec())) + ); + } - redis::cmd("SET") - .arg("{x}key1") - .arg(b"foo") - .execute(&mut con); - redis::cmd("SET").arg(&["{x}key2", "bar"]).execute(&mut con); + #[test] + fn test_cluster_should_not_connect_without_mtls() { + let cluster = TestClusterContext::new_with_mtls(3, 0); - assert_eq!( - redis::cmd("MGET") - .arg(&["{x}key1", "{x}key2"]) - .query(&mut con), - Ok(("foo".to_string(), b"bar".to_vec())) - ); - } + let client = create_cluster_client_from_cluster(&cluster, false).unwrap(); + let connection = client.get_connection(); - #[test] - fn test_cluster_should_not_connect_without_mtls() { - let cluster = TestClusterContext::new_with_mtls(3, 0); - - let client = create_cluster_client_from_cluster(&cluster, false).unwrap(); - let connection = client.get_connection(); - - match cluster.cluster.servers.first().unwrap().connection_info() { - ConnectionInfo { - addr: redis::ConnectionAddr::TcpTls { .. }, - .. - } => { - if connection.is_ok() { - panic!("Must NOT be able to connect without client credentials if server accepts TLS"); + match cluster.cluster.servers.first().unwrap().connection_info() { + ConnectionInfo { + addr: redis::ConnectionAddr::TcpTls { .. }, + .. + } => { + if connection.is_ok() { + panic!("Must NOT be able to connect without client credentials if server accepts TLS"); + } } - } - _ => { - if let Err(e) = connection { - panic!("Must be able to connect without client credentials if server does NOT accept TLS: {e:?}"); + _ => { + if let Err(e) = connection { + panic!("Must be able to connect without client credentials if server does NOT accept TLS: {e:?}"); + } } } } diff --git a/redis/tests/test_cluster_async.rs b/redis/tests/test_cluster_async.rs index 42c5a9a5e..6304d8d69 100644 --- a/redis/tests/test_cluster_async.rs +++ b/redis/tests/test_cluster_async.rs @@ -1,1850 +1,1875 @@ #![cfg(feature = "cluster-async")] mod support; -use std::{ - collections::HashMap, - sync::{ - atomic::{self, AtomicBool, AtomicI32, AtomicU16, Ordering}, - Arc, - }, -}; - -use futures::prelude::*; -use once_cell::sync::Lazy; - -use redis::{ - aio::{ConnectionLike, MultiplexedConnection}, - cluster::ClusterClient, - cluster_async::Connect, - cluster_routing::{MultipleNodeRoutingInfo, RoutingInfo, SingleNodeRoutingInfo}, - cmd, from_owned_redis_value, parse_redis_value, AsyncCommands, Cmd, ErrorKind, InfoDict, - IntoConnectionInfo, ProtocolVersion, RedisError, RedisFuture, RedisResult, Script, Value, -}; - -use crate::support::*; - -#[test] -fn test_async_cluster_basic_cmd() { - let cluster = TestClusterContext::new(3, 0); - - block_on_all(async move { - let mut connection = cluster.async_connection().await; - cmd("SET") - .arg("test") - .arg("test_data") - .query_async(&mut connection) - .await?; - let res: String = cmd("GET") - .arg("test") - .clone() - .query_async(&mut connection) - .await?; - assert_eq!(res, "test_data"); - Ok::<_, RedisError>(()) - }) - .unwrap(); -} -#[test] -fn test_async_cluster_basic_eval() { - let cluster = TestClusterContext::new(3, 0); +#[cfg(test)] +mod cluster_async { + use std::{ + collections::HashMap, + sync::{ + atomic::{self, AtomicBool, AtomicI32, AtomicU16, Ordering}, + Arc, + }, + }; - block_on_all(async move { - let mut connection = cluster.async_connection().await; - let res: String = cmd("EVAL") - .arg(r#"redis.call("SET", KEYS[1], ARGV[1]); return redis.call("GET", KEYS[1])"#) - .arg(1) - .arg("key") - .arg("test") - .query_async(&mut connection) - .await?; - assert_eq!(res, "test"); - Ok::<_, RedisError>(()) - }) - .unwrap(); -} + use futures::prelude::*; + use once_cell::sync::Lazy; -#[test] -fn test_async_cluster_basic_script() { - let cluster = TestClusterContext::new(3, 0); - - block_on_all(async move { - let mut connection = cluster.async_connection().await; - let res: String = Script::new( - r#"redis.call("SET", KEYS[1], ARGV[1]); return redis.call("GET", KEYS[1])"#, - ) - .key("key") - .arg("test") - .invoke_async(&mut connection) - .await?; - assert_eq!(res, "test"); - Ok::<_, RedisError>(()) - }) - .unwrap(); -} + use redis::{ + aio::{ConnectionLike, MultiplexedConnection}, + cluster::ClusterClient, + cluster_async::Connect, + cluster_routing::{MultipleNodeRoutingInfo, RoutingInfo, SingleNodeRoutingInfo}, + cmd, from_owned_redis_value, parse_redis_value, AsyncCommands, Cmd, ErrorKind, InfoDict, + IntoConnectionInfo, ProtocolVersion, RedisError, RedisFuture, RedisResult, Script, Value, + }; -#[test] -fn test_async_cluster_route_flush_to_specific_node() { - let cluster = TestClusterContext::new(3, 0); + use crate::support::*; - block_on_all(async move { - let mut connection = cluster.async_connection().await; - let _: () = connection.set("foo", "bar").await.unwrap(); - let _: () = connection.set("bar", "foo").await.unwrap(); + #[test] + fn test_async_cluster_basic_cmd() { + let cluster = TestClusterContext::new(3, 0); - let res: String = connection.get("foo").await.unwrap(); - assert_eq!(res, "bar".to_string()); - let res2: Option = connection.get("bar").await.unwrap(); - assert_eq!(res2, Some("foo".to_string())); + block_on_all(async move { + let mut connection = cluster.async_connection().await; + cmd("SET") + .arg("test") + .arg("test_data") + .query_async(&mut connection) + .await?; + let res: String = cmd("GET") + .arg("test") + .clone() + .query_async(&mut connection) + .await?; + assert_eq!(res, "test_data"); + Ok::<_, RedisError>(()) + }) + .unwrap(); + } - let route = redis::cluster_routing::Route::new(1, redis::cluster_routing::SlotAddr::Master); - let single_node_route = redis::cluster_routing::SingleNodeRoutingInfo::SpecificNode(route); - let routing = RoutingInfo::SingleNode(single_node_route); - assert_eq!( - connection - .route_command(&redis::cmd("FLUSHALL"), routing) - .await - .unwrap(), - Value::Okay - ); - let res: String = connection.get("foo").await.unwrap(); - assert_eq!(res, "bar".to_string()); - let res2: Option = connection.get("bar").await.unwrap(); - assert_eq!(res2, None); - Ok::<_, RedisError>(()) - }) - .unwrap(); -} + #[test] + fn test_async_cluster_basic_eval() { + let cluster = TestClusterContext::new(3, 0); -#[test] -fn test_async_cluster_route_flush_to_node_by_address() { - let cluster = TestClusterContext::new(3, 0); - - block_on_all(async move { - let mut connection = cluster.async_connection().await; - let mut cmd = redis::cmd("INFO"); - // The other sections change with time. - // TODO - after we remove support of redis 6, we can add more than a single section - .arg("Persistence").arg("Memory").arg("Replication") - cmd.arg("Clients"); - let value = connection - .route_command( - &cmd, - RoutingInfo::MultiNode((MultipleNodeRoutingInfo::AllNodes, None)), - ) - .await - .unwrap(); + block_on_all(async move { + let mut connection = cluster.async_connection().await; + let res: String = cmd("EVAL") + .arg(r#"redis.call("SET", KEYS[1], ARGV[1]); return redis.call("GET", KEYS[1])"#) + .arg(1) + .arg("key") + .arg("test") + .query_async(&mut connection) + .await?; + assert_eq!(res, "test"); + Ok::<_, RedisError>(()) + }) + .unwrap(); + } + + #[test] + fn test_async_cluster_basic_script() { + let cluster = TestClusterContext::new(3, 0); - let info_by_address = from_owned_redis_value::>(value).unwrap(); - // find the info of the first returned node - let (address, info) = info_by_address.into_iter().next().unwrap(); - let mut split_address = address.split(':'); - let host = split_address.next().unwrap().to_string(); - let port = split_address.next().unwrap().parse().unwrap(); - - let value = connection - .route_command( - &cmd, - RoutingInfo::SingleNode(SingleNodeRoutingInfo::ByAddress { host, port }), + block_on_all(async move { + let mut connection = cluster.async_connection().await; + let res: String = Script::new( + r#"redis.call("SET", KEYS[1], ARGV[1]); return redis.call("GET", KEYS[1])"#, ) - .await - .unwrap(); - let new_info = from_owned_redis_value::(value).unwrap(); + .key("key") + .arg("test") + .invoke_async(&mut connection) + .await?; + assert_eq!(res, "test"); + Ok::<_, RedisError>(()) + }) + .unwrap(); + } - assert_eq!(new_info, info); - Ok::<_, RedisError>(()) - }) - .unwrap(); -} + #[test] + fn test_async_cluster_route_flush_to_specific_node() { + let cluster = TestClusterContext::new(3, 0); -#[test] -fn test_async_cluster_route_info_to_nodes() { - let cluster = TestClusterContext::new(12, 1); + block_on_all(async move { + let mut connection = cluster.async_connection().await; + let _: () = connection.set("foo", "bar").await.unwrap(); + let _: () = connection.set("bar", "foo").await.unwrap(); + + let res: String = connection.get("foo").await.unwrap(); + assert_eq!(res, "bar".to_string()); + let res2: Option = connection.get("bar").await.unwrap(); + assert_eq!(res2, Some("foo".to_string())); + + let route = + redis::cluster_routing::Route::new(1, redis::cluster_routing::SlotAddr::Master); + let single_node_route = + redis::cluster_routing::SingleNodeRoutingInfo::SpecificNode(route); + let routing = RoutingInfo::SingleNode(single_node_route); + assert_eq!( + connection + .route_command(&redis::cmd("FLUSHALL"), routing) + .await + .unwrap(), + Value::Okay + ); + let res: String = connection.get("foo").await.unwrap(); + assert_eq!(res, "bar".to_string()); + let res2: Option = connection.get("bar").await.unwrap(); + assert_eq!(res2, None); + Ok::<_, RedisError>(()) + }) + .unwrap(); + } - let split_to_addresses_and_info = |res| -> (Vec, Vec) { - if let Value::Map(values) = res { - let mut pairs: Vec<_> = values - .into_iter() - .map(|(key, value)| { - ( - redis::from_redis_value::(&key).unwrap(), - redis::from_redis_value::(&value).unwrap(), - ) - }) - .collect(); - pairs.sort_by(|(address1, _), (address2, _)| address1.cmp(address2)); - pairs.into_iter().unzip() - } else { - unreachable!("{:?}", res); - } - }; + #[test] + fn test_async_cluster_route_flush_to_node_by_address() { + let cluster = TestClusterContext::new(3, 0); - block_on_all(async move { - let cluster_addresses: Vec<_> = cluster - .cluster - .servers - .iter() - .map(|server| server.connection_info()) - .collect(); - let client = ClusterClient::builder(cluster_addresses.clone()) - .read_from_replicas() - .build()?; - let mut connection = client.get_async_connection().await?; - - let route_to_all_nodes = redis::cluster_routing::MultipleNodeRoutingInfo::AllNodes; - let routing = RoutingInfo::MultiNode((route_to_all_nodes, None)); - let res = connection - .route_command(&redis::cmd("INFO"), routing) - .await - .unwrap(); - let (addresses, infos) = split_to_addresses_and_info(res); - - let mut cluster_addresses: Vec<_> = cluster_addresses - .into_iter() - .map(|info| info.addr.to_string()) - .collect(); - cluster_addresses.sort(); - - assert_eq!(addresses.len(), 12); - assert_eq!(addresses, cluster_addresses); - assert_eq!(infos.len(), 12); - for i in 0..12 { - let split: Vec<_> = addresses[i].split(':').collect(); - assert!(infos[i].contains(&format!("tcp_port:{}", split[1]))); - } + block_on_all(async move { + let mut connection = cluster.async_connection().await; + let mut cmd = redis::cmd("INFO"); + // The other sections change with time. + // TODO - after we remove support of redis 6, we can add more than a single section - .arg("Persistence").arg("Memory").arg("Replication") + cmd.arg("Clients"); + let value = connection + .route_command( + &cmd, + RoutingInfo::MultiNode((MultipleNodeRoutingInfo::AllNodes, None)), + ) + .await + .unwrap(); + + let info_by_address = from_owned_redis_value::>(value).unwrap(); + // find the info of the first returned node + let (address, info) = info_by_address.into_iter().next().unwrap(); + let mut split_address = address.split(':'); + let host = split_address.next().unwrap().to_string(); + let port = split_address.next().unwrap().parse().unwrap(); + + let value = connection + .route_command( + &cmd, + RoutingInfo::SingleNode(SingleNodeRoutingInfo::ByAddress { host, port }), + ) + .await + .unwrap(); + let new_info = from_owned_redis_value::(value).unwrap(); - let route_to_all_primaries = redis::cluster_routing::MultipleNodeRoutingInfo::AllMasters; - let routing = RoutingInfo::MultiNode((route_to_all_primaries, None)); - let res = connection - .route_command(&redis::cmd("INFO"), routing) - .await - .unwrap(); - let (addresses, infos) = split_to_addresses_and_info(res); - assert_eq!(addresses.len(), 6); - assert_eq!(infos.len(), 6); - // verify that all primaries have the correct port & host, and are marked as primaries. - for i in 0..6 { - assert!(cluster_addresses.contains(&addresses[i])); - let split: Vec<_> = addresses[i].split(':').collect(); - assert!(infos[i].contains(&format!("tcp_port:{}", split[1]))); - assert!(infos[i].contains("role:primary") || infos[i].contains("role:master")); - } + assert_eq!(new_info, info); + Ok::<_, RedisError>(()) + }) + .unwrap(); + } - Ok::<_, RedisError>(()) - }) - .unwrap(); -} + #[test] + fn test_async_cluster_route_info_to_nodes() { + let cluster = TestClusterContext::new(12, 1); + + let split_to_addresses_and_info = |res| -> (Vec, Vec) { + if let Value::Map(values) = res { + let mut pairs: Vec<_> = values + .into_iter() + .map(|(key, value)| { + ( + redis::from_redis_value::(&key).unwrap(), + redis::from_redis_value::(&value).unwrap(), + ) + }) + .collect(); + pairs.sort_by(|(address1, _), (address2, _)| address1.cmp(address2)); + pairs.into_iter().unzip() + } else { + unreachable!("{:?}", res); + } + }; -#[test] -fn test_cluster_resp3() { - if use_protocol() == ProtocolVersion::RESP2 { - return; - } - block_on_all(async move { - let cluster = TestClusterContext::new(3, 0); + block_on_all(async move { + let cluster_addresses: Vec<_> = cluster + .cluster + .servers + .iter() + .map(|server| server.connection_info()) + .collect(); + let client = ClusterClient::builder(cluster_addresses.clone()) + .read_from_replicas() + .build()?; + let mut connection = client.get_async_connection().await?; + + let route_to_all_nodes = redis::cluster_routing::MultipleNodeRoutingInfo::AllNodes; + let routing = RoutingInfo::MultiNode((route_to_all_nodes, None)); + let res = connection + .route_command(&redis::cmd("INFO"), routing) + .await + .unwrap(); + let (addresses, infos) = split_to_addresses_and_info(res); - let mut connection = cluster.async_connection().await; + let mut cluster_addresses: Vec<_> = cluster_addresses + .into_iter() + .map(|info| info.addr.to_string()) + .collect(); + cluster_addresses.sort(); + + assert_eq!(addresses.len(), 12); + assert_eq!(addresses, cluster_addresses); + assert_eq!(infos.len(), 12); + for i in 0..12 { + let split: Vec<_> = addresses[i].split(':').collect(); + assert!(infos[i].contains(&format!("tcp_port:{}", split[1]))); + } - let _: () = connection.hset("hash", "foo", "baz").await.unwrap(); - let _: () = connection.hset("hash", "bar", "foobar").await.unwrap(); - let result: Value = connection.hgetall("hash").await.unwrap(); + let route_to_all_primaries = + redis::cluster_routing::MultipleNodeRoutingInfo::AllMasters; + let routing = RoutingInfo::MultiNode((route_to_all_primaries, None)); + let res = connection + .route_command(&redis::cmd("INFO"), routing) + .await + .unwrap(); + let (addresses, infos) = split_to_addresses_and_info(res); + assert_eq!(addresses.len(), 6); + assert_eq!(infos.len(), 6); + // verify that all primaries have the correct port & host, and are marked as primaries. + for i in 0..6 { + assert!(cluster_addresses.contains(&addresses[i])); + let split: Vec<_> = addresses[i].split(':').collect(); + assert!(infos[i].contains(&format!("tcp_port:{}", split[1]))); + assert!(infos[i].contains("role:primary") || infos[i].contains("role:master")); + } - assert_eq!( - result, - Value::Map(vec![ - ( - Value::BulkString("foo".as_bytes().to_vec()), - Value::BulkString("baz".as_bytes().to_vec()) - ), - ( - Value::BulkString("bar".as_bytes().to_vec()), - Value::BulkString("foobar".as_bytes().to_vec()) - ) - ]) - ); + Ok::<_, RedisError>(()) + }) + .unwrap(); + } - Ok(()) - }) - .unwrap(); -} + #[test] + fn test_cluster_resp3() { + if use_protocol() == ProtocolVersion::RESP2 { + return; + } + block_on_all(async move { + let cluster = TestClusterContext::new(3, 0); -#[test] -fn test_async_cluster_basic_pipe() { - let cluster = TestClusterContext::new(3, 0); - - block_on_all(async move { - let mut connection = cluster.async_connection().await; - let mut pipe = redis::pipe(); - pipe.add_command(cmd("SET").arg("test").arg("test_data").clone()); - pipe.add_command(cmd("SET").arg("{test}3").arg("test_data3").clone()); - pipe.query_async(&mut connection).await?; - let res: String = connection.get("test").await?; - assert_eq!(res, "test_data"); - let res: String = connection.get("{test}3").await?; - assert_eq!(res, "test_data3"); - Ok::<_, RedisError>(()) - }) - .unwrap() -} + let mut connection = cluster.async_connection().await; -#[test] -fn test_async_cluster_multi_shard_commands() { - let cluster = TestClusterContext::new(3, 0); + let _: () = connection.hset("hash", "foo", "baz").await.unwrap(); + let _: () = connection.hset("hash", "bar", "foobar").await.unwrap(); + let result: Value = connection.hgetall("hash").await.unwrap(); - block_on_all(async move { - let mut connection = cluster.async_connection().await; + assert_eq!( + result, + Value::Map(vec![ + ( + Value::BulkString("foo".as_bytes().to_vec()), + Value::BulkString("baz".as_bytes().to_vec()) + ), + ( + Value::BulkString("bar".as_bytes().to_vec()), + Value::BulkString("foobar".as_bytes().to_vec()) + ) + ]) + ); - let res: String = connection - .mset(&[("foo", "bar"), ("bar", "foo"), ("baz", "bazz")]) - .await?; - assert_eq!(res, "OK"); - let res: Vec = connection.mget(&["baz", "foo", "bar"]).await?; - assert_eq!(res, vec!["bazz", "bar", "foo"]); - Ok::<_, RedisError>(()) - }) - .unwrap() -} + Ok(()) + }) + .unwrap(); + } -#[test] -fn test_async_cluster_basic_failover() { - block_on_all(async move { - test_failover(&TestClusterContext::new(6, 1), 10, 123, false).await; - Ok::<_, RedisError>(()) - }) - .unwrap() -} + #[test] + fn test_async_cluster_basic_pipe() { + let cluster = TestClusterContext::new(3, 0); -async fn do_failover(redis: &mut redis::aio::MultiplexedConnection) -> Result<(), anyhow::Error> { - cmd("CLUSTER").arg("FAILOVER").query_async(redis).await?; - Ok(()) -} + block_on_all(async move { + let mut connection = cluster.async_connection().await; + let mut pipe = redis::pipe(); + pipe.add_command(cmd("SET").arg("test").arg("test_data").clone()); + pipe.add_command(cmd("SET").arg("{test}3").arg("test_data3").clone()); + pipe.query_async(&mut connection).await?; + let res: String = connection.get("test").await?; + assert_eq!(res, "test_data"); + let res: String = connection.get("{test}3").await?; + assert_eq!(res, "test_data3"); + Ok::<_, RedisError>(()) + }) + .unwrap() + } + + #[test] + fn test_async_cluster_multi_shard_commands() { + let cluster = TestClusterContext::new(3, 0); -// parameter `_mtls_enabled` can only be used if `feature = tls-rustls` is active -#[allow(dead_code)] -async fn test_failover(env: &TestClusterContext, requests: i32, value: i32, _mtls_enabled: bool) { - let completed = Arc::new(AtomicI32::new(0)); + block_on_all(async move { + let mut connection = cluster.async_connection().await; - let connection = env.async_connection().await; - let mut node_conns: Vec = Vec::new(); + let res: String = connection + .mset(&[("foo", "bar"), ("bar", "foo"), ("baz", "bazz")]) + .await?; + assert_eq!(res, "OK"); + let res: Vec = connection.mget(&["baz", "foo", "bar"]).await?; + assert_eq!(res, vec!["bazz", "bar", "foo"]); + Ok::<_, RedisError>(()) + }) + .unwrap() + } - 'outer: loop { - node_conns.clear(); - let cleared_nodes = async { - for server in env.cluster.iter_servers() { - let addr = server.client_addr(); + #[test] + fn test_async_cluster_basic_failover() { + block_on_all(async move { + test_failover(&TestClusterContext::new(6, 1), 10, 123, false).await; + Ok::<_, RedisError>(()) + }) + .unwrap() + } - #[cfg(feature = "tls-rustls")] - let client = - build_single_client(server.connection_info(), &server.tls_paths, _mtls_enabled) - .unwrap_or_else(|e| panic!("Failed to connect to '{addr}': {e}")); + async fn do_failover( + redis: &mut redis::aio::MultiplexedConnection, + ) -> Result<(), anyhow::Error> { + cmd("CLUSTER").arg("FAILOVER").query_async(redis).await?; + Ok(()) + } - #[cfg(not(feature = "tls-rustls"))] - let client = redis::Client::open(server.connection_info()) + // parameter `_mtls_enabled` can only be used if `feature = tls-rustls` is active + #[allow(dead_code)] + async fn test_failover( + env: &TestClusterContext, + requests: i32, + value: i32, + _mtls_enabled: bool, + ) { + let completed = Arc::new(AtomicI32::new(0)); + + let connection = env.async_connection().await; + let mut node_conns: Vec = Vec::new(); + + 'outer: loop { + node_conns.clear(); + let cleared_nodes = async { + for server in env.cluster.iter_servers() { + let addr = server.client_addr(); + + #[cfg(feature = "tls-rustls")] + let client = build_single_client( + server.connection_info(), + &server.tls_paths, + _mtls_enabled, + ) .unwrap_or_else(|e| panic!("Failed to connect to '{addr}': {e}")); - let mut conn = client - .get_multiplexed_async_connection() - .await - .unwrap_or_else(|e| panic!("Failed to get connection: {e}")); + #[cfg(not(feature = "tls-rustls"))] + let client = redis::Client::open(server.connection_info()) + .unwrap_or_else(|e| panic!("Failed to connect to '{addr}': {e}")); - let info: InfoDict = redis::Cmd::new() - .arg("INFO") - .query_async(&mut conn) - .await - .expect("INFO"); - let role: String = info.get("role").expect("cluster role"); - - if role == "master" { - tokio::time::timeout(std::time::Duration::from_secs(3), async { - Ok(redis::Cmd::new() - .arg("FLUSHALL") - .query_async(&mut conn) - .await?) - }) - .await - .unwrap_or_else(|err| Err(anyhow::Error::from(err)))?; - } + let mut conn = client + .get_multiplexed_async_connection() + .await + .unwrap_or_else(|e| panic!("Failed to get connection: {e}")); + + let info: InfoDict = redis::Cmd::new() + .arg("INFO") + .query_async(&mut conn) + .await + .expect("INFO"); + let role: String = info.get("role").expect("cluster role"); + + if role == "master" { + tokio::time::timeout(std::time::Duration::from_secs(3), async { + Ok(redis::Cmd::new() + .arg("FLUSHALL") + .query_async(&mut conn) + .await?) + }) + .await + .unwrap_or_else(|err| Err(anyhow::Error::from(err)))?; + } - node_conns.push(conn); + node_conns.push(conn); + } + Ok::<_, anyhow::Error>(()) } - Ok::<_, anyhow::Error>(()) - } - .await; - match cleared_nodes { - Ok(()) => break 'outer, - Err(err) => { - // Failed to clear the databases, retry - log::warn!("{}", err); + .await; + match cleared_nodes { + Ok(()) => break 'outer, + Err(err) => { + // Failed to clear the databases, retry + log::warn!("{}", err); + } } } - } - (0..requests + 1) - .map(|i| { - let mut connection = connection.clone(); - let mut node_conns = node_conns.clone(); - let completed = completed.clone(); - async move { - if i == requests / 2 { - // Failover all the nodes, error only if all the failover requests error - let mut results = future::join_all( - node_conns - .iter_mut() - .map(|conn| Box::pin(do_failover(conn))), - ) - .await; - if results.iter().all(|res| res.is_err()) { - results.pop().unwrap() + (0..requests + 1) + .map(|i| { + let mut connection = connection.clone(); + let mut node_conns = node_conns.clone(); + let completed = completed.clone(); + async move { + if i == requests / 2 { + // Failover all the nodes, error only if all the failover requests error + let mut results = future::join_all( + node_conns + .iter_mut() + .map(|conn| Box::pin(do_failover(conn))), + ) + .await; + if results.iter().all(|res| res.is_err()) { + results.pop().unwrap() + } else { + Ok::<_, anyhow::Error>(()) + } } else { + let key = format!("test-{value}-{i}"); + cmd("SET") + .arg(&key) + .arg(i) + .clone() + .query_async(&mut connection) + .await?; + let res: i32 = cmd("GET") + .arg(key) + .clone() + .query_async(&mut connection) + .await?; + assert_eq!(res, i); + completed.fetch_add(1, Ordering::SeqCst); Ok::<_, anyhow::Error>(()) } - } else { - let key = format!("test-{value}-{i}"); - cmd("SET") - .arg(&key) - .arg(i) - .clone() - .query_async(&mut connection) - .await?; - let res: i32 = cmd("GET") - .arg(key) - .clone() - .query_async(&mut connection) - .await?; - assert_eq!(res, i); - completed.fetch_add(1, Ordering::SeqCst); - Ok::<_, anyhow::Error>(()) } - } - }) - .collect::>() - .try_collect() - .await - .unwrap_or_else(|e| panic!("{e}")); - - assert_eq!( - completed.load(Ordering::SeqCst), - requests, - "Some requests never completed!" - ); -} + }) + .collect::>() + .try_collect() + .await + .unwrap_or_else(|e| panic!("{e}")); -static ERROR: Lazy = Lazy::new(Default::default); + assert_eq!( + completed.load(Ordering::SeqCst), + requests, + "Some requests never completed!" + ); + } -#[derive(Clone)] -struct ErrorConnection { - inner: MultiplexedConnection, -} + static ERROR: Lazy = Lazy::new(Default::default); -impl Connect for ErrorConnection { - fn connect<'a, T>( - info: T, - response_timeout: std::time::Duration, - connection_timeout: std::time::Duration, - ) -> RedisFuture<'a, Self> - where - T: IntoConnectionInfo + Send + 'a, - { - Box::pin(async move { - let inner = - MultiplexedConnection::connect(info, response_timeout, connection_timeout).await?; - Ok(ErrorConnection { inner }) - }) + #[derive(Clone)] + struct ErrorConnection { + inner: MultiplexedConnection, } -} -impl ConnectionLike for ErrorConnection { - fn req_packed_command<'a>(&'a mut self, cmd: &'a Cmd) -> RedisFuture<'a, Value> { - if ERROR.load(Ordering::SeqCst) { - Box::pin(async move { Err(RedisError::from((redis::ErrorKind::Moved, "ERROR"))) }) - } else { - self.inner.req_packed_command(cmd) + impl Connect for ErrorConnection { + fn connect<'a, T>( + info: T, + response_timeout: std::time::Duration, + connection_timeout: std::time::Duration, + ) -> RedisFuture<'a, Self> + where + T: IntoConnectionInfo + Send + 'a, + { + Box::pin(async move { + let inner = + MultiplexedConnection::connect(info, response_timeout, connection_timeout) + .await?; + Ok(ErrorConnection { inner }) + }) } } - fn req_packed_commands<'a>( - &'a mut self, - pipeline: &'a redis::Pipeline, - offset: usize, - count: usize, - ) -> RedisFuture<'a, Vec> { - self.inner.req_packed_commands(pipeline, offset, count) - } + impl ConnectionLike for ErrorConnection { + fn req_packed_command<'a>(&'a mut self, cmd: &'a Cmd) -> RedisFuture<'a, Value> { + if ERROR.load(Ordering::SeqCst) { + Box::pin(async move { Err(RedisError::from((redis::ErrorKind::Moved, "ERROR"))) }) + } else { + self.inner.req_packed_command(cmd) + } + } + + fn req_packed_commands<'a>( + &'a mut self, + pipeline: &'a redis::Pipeline, + offset: usize, + count: usize, + ) -> RedisFuture<'a, Vec> { + self.inner.req_packed_commands(pipeline, offset, count) + } - fn get_db(&self) -> i64 { - self.inner.get_db() + fn get_db(&self) -> i64 { + self.inner.get_db() + } } -} -#[test] -fn test_async_cluster_error_in_inner_connection() { - let cluster = TestClusterContext::new(3, 0); + #[test] + fn test_async_cluster_error_in_inner_connection() { + let cluster = TestClusterContext::new(3, 0); - block_on_all(async move { - let mut con = cluster.async_generic_connection::().await; + block_on_all(async move { + let mut con = cluster.async_generic_connection::().await; - ERROR.store(false, Ordering::SeqCst); - let r: Option = con.get("test").await?; - assert_eq!(r, None::); + ERROR.store(false, Ordering::SeqCst); + let r: Option = con.get("test").await?; + assert_eq!(r, None::); - ERROR.store(true, Ordering::SeqCst); + ERROR.store(true, Ordering::SeqCst); - let result: RedisResult<()> = con.get("test").await; - assert_eq!( - result, - Err(RedisError::from((redis::ErrorKind::Moved, "ERROR"))) - ); + let result: RedisResult<()> = con.get("test").await; + assert_eq!( + result, + Err(RedisError::from((redis::ErrorKind::Moved, "ERROR"))) + ); - Ok::<_, RedisError>(()) - }) - .unwrap(); -} + Ok::<_, RedisError>(()) + }) + .unwrap(); + } -#[test] -#[cfg(all(not(feature = "tokio-comp"), feature = "async-std-comp"))] -fn test_async_cluster_async_std_basic_cmd() { - let cluster = TestClusterContext::new(3, 0); + #[test] + #[cfg(all(not(feature = "tokio-comp"), feature = "async-std-comp"))] + fn test_async_cluster_async_std_basic_cmd() { + let cluster = TestClusterContext::new(3, 0); - block_on_all_using_async_std(async { - let mut connection = cluster.async_connection().await; - redis::cmd("SET") - .arg("test") - .arg("test_data") - .query_async(&mut connection) - .await?; - redis::cmd("GET") - .arg("test") - .clone() - .query_async(&mut connection) - .map_ok(|res: String| { - assert_eq!(res, "test_data"); - }) - .await - }) - .unwrap(); -} + block_on_all_using_async_std(async { + let mut connection = cluster.async_connection().await; + redis::cmd("SET") + .arg("test") + .arg("test_data") + .query_async(&mut connection) + .await?; + redis::cmd("GET") + .arg("test") + .clone() + .query_async(&mut connection) + .map_ok(|res: String| { + assert_eq!(res, "test_data"); + }) + .await + }) + .unwrap(); + } -#[test] -fn test_async_cluster_retries() { - let name = "tryagain"; - - let requests = atomic::AtomicUsize::new(0); - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]).retries(5), - name, - move |cmd: &[u8], _| { - respond_startup(name, cmd)?; - - match requests.fetch_add(1, atomic::Ordering::SeqCst) { - 0..=4 => Err(parse_redis_value(b"-TRYAGAIN mock\r\n")), - _ => Err(Ok(Value::BulkString(b"123".to_vec()))), - } - }, - ); + #[test] + fn test_async_cluster_retries() { + let name = "tryagain"; + + let requests = atomic::AtomicUsize::new(0); + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]).retries(5), + name, + move |cmd: &[u8], _| { + respond_startup(name, cmd)?; - let value = runtime.block_on( - cmd("GET") - .arg("test") - .query_async::<_, Option>(&mut connection), - ); + match requests.fetch_add(1, atomic::Ordering::SeqCst) { + 0..=4 => Err(parse_redis_value(b"-TRYAGAIN mock\r\n")), + _ => Err(Ok(Value::BulkString(b"123".to_vec()))), + } + }, + ); - assert_eq!(value, Ok(Some(123))); -} + let value = runtime.block_on( + cmd("GET") + .arg("test") + .query_async::<_, Option>(&mut connection), + ); -#[test] -fn test_async_cluster_tryagain_exhaust_retries() { - let name = "tryagain_exhaust_retries"; + assert_eq!(value, Ok(Some(123))); + } - let requests = Arc::new(atomic::AtomicUsize::new(0)); + #[test] + fn test_async_cluster_tryagain_exhaust_retries() { + let name = "tryagain_exhaust_retries"; + + let requests = Arc::new(atomic::AtomicUsize::new(0)); + + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]).retries(2), + name, + { + let requests = requests.clone(); + move |cmd: &[u8], _| { + respond_startup(name, cmd)?; + requests.fetch_add(1, atomic::Ordering::SeqCst); + Err(parse_redis_value(b"-TRYAGAIN mock\r\n")) + } + }, + ); - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]).retries(2), - name, - { - let requests = requests.clone(); - move |cmd: &[u8], _| { - respond_startup(name, cmd)?; - requests.fetch_add(1, atomic::Ordering::SeqCst); - Err(parse_redis_value(b"-TRYAGAIN mock\r\n")) - } - }, - ); - - let result = runtime.block_on( - cmd("GET") - .arg("test") - .query_async::<_, Option>(&mut connection), - ); - - match result { - Ok(_) => panic!("result should be an error"), - Err(e) => match e.kind() { - ErrorKind::TryAgain => {} - _ => panic!("Expected TryAgain but got {:?}", e.kind()), - }, - } - assert_eq!(requests.load(atomic::Ordering::SeqCst), 3); -} + let result = runtime.block_on( + cmd("GET") + .arg("test") + .query_async::<_, Option>(&mut connection), + ); -#[test] -fn test_async_cluster_move_error_when_new_node_is_added() { - let name = "rebuild_with_extra_nodes"; - - let requests = atomic::AtomicUsize::new(0); - let started = atomic::AtomicBool::new(false); - let refreshed = atomic::AtomicBool::new(false); - - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::new(name, move |cmd: &[u8], port| { - if !started.load(atomic::Ordering::SeqCst) { - respond_startup(name, cmd)?; + match result { + Ok(_) => panic!("result should be an error"), + Err(e) => match e.kind() { + ErrorKind::TryAgain => {} + _ => panic!("Expected TryAgain but got {:?}", e.kind()), + }, } - started.store(true, atomic::Ordering::SeqCst); + assert_eq!(requests.load(atomic::Ordering::SeqCst), 3); + } - if contains_slice(cmd, b"PING") { - return Err(Ok(Value::SimpleString("OK".into()))); - } + #[test] + fn test_async_cluster_move_error_when_new_node_is_added() { + let name = "rebuild_with_extra_nodes"; + + let requests = atomic::AtomicUsize::new(0); + let started = atomic::AtomicBool::new(false); + let refreshed = atomic::AtomicBool::new(false); + + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::new(name, move |cmd: &[u8], port| { + if !started.load(atomic::Ordering::SeqCst) { + respond_startup(name, cmd)?; + } + started.store(true, atomic::Ordering::SeqCst); - let i = requests.fetch_add(1, atomic::Ordering::SeqCst); + if contains_slice(cmd, b"PING") { + return Err(Ok(Value::SimpleString("OK".into()))); + } - let is_get_cmd = contains_slice(cmd, b"GET"); - let get_response = Err(Ok(Value::BulkString(b"123".to_vec()))); - match i { - // Respond that the key exists on a node that does not yet have a connection: - 0 => Err(parse_redis_value( - format!("-MOVED 123 {name}:6380\r\n").as_bytes(), - )), - _ => { - if contains_slice(cmd, b"CLUSTER") && contains_slice(cmd, b"SLOTS") { - // Should not attempt to refresh slots more than once: - assert!(!refreshed.swap(true, Ordering::SeqCst)); - Err(Ok(Value::Array(vec![ - Value::Array(vec![ - Value::Int(0), - Value::Int(1), + let i = requests.fetch_add(1, atomic::Ordering::SeqCst); + + let is_get_cmd = contains_slice(cmd, b"GET"); + let get_response = Err(Ok(Value::BulkString(b"123".to_vec()))); + match i { + // Respond that the key exists on a node that does not yet have a connection: + 0 => Err(parse_redis_value( + format!("-MOVED 123 {name}:6380\r\n").as_bytes(), + )), + _ => { + if contains_slice(cmd, b"CLUSTER") && contains_slice(cmd, b"SLOTS") { + // Should not attempt to refresh slots more than once: + assert!(!refreshed.swap(true, Ordering::SeqCst)); + Err(Ok(Value::Array(vec![ Value::Array(vec![ - Value::BulkString(name.as_bytes().to_vec()), - Value::Int(6379), + Value::Int(0), + Value::Int(1), + Value::Array(vec![ + Value::BulkString(name.as_bytes().to_vec()), + Value::Int(6379), + ]), ]), - ]), - Value::Array(vec![ - Value::Int(2), - Value::Int(16383), Value::Array(vec![ - Value::BulkString(name.as_bytes().to_vec()), - Value::Int(6380), + Value::Int(2), + Value::Int(16383), + Value::Array(vec![ + Value::BulkString(name.as_bytes().to_vec()), + Value::Int(6380), + ]), ]), - ]), - ]))) - } else { - assert_eq!(port, 6380); - assert!(is_get_cmd, "{:?}", std::str::from_utf8(cmd)); - get_response + ]))) + } else { + assert_eq!(port, 6380); + assert!(is_get_cmd, "{:?}", std::str::from_utf8(cmd)); + get_response + } } } - } - }); + }); - let value = runtime.block_on( - cmd("GET") - .arg("test") - .query_async::<_, Option>(&mut connection), - ); + let value = runtime.block_on( + cmd("GET") + .arg("test") + .query_async::<_, Option>(&mut connection), + ); - assert_eq!(value, Ok(Some(123))); -} + assert_eq!(value, Ok(Some(123))); + } -#[test] -fn test_async_cluster_ask_redirect() { - let name = "node"; - let completed = Arc::new(AtomicI32::new(0)); - let MockEnv { - async_connection: mut connection, - handler: _handler, - runtime, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]), - name, - { - move |cmd: &[u8], port| { - respond_startup_two_nodes(name, cmd)?; - // Error twice with io-error, ensure connection is reestablished w/out calling - // other node (i.e., not doing a full slot rebuild) - let count = completed.fetch_add(1, Ordering::SeqCst); - match port { - 6379 => match count { - 0 => Err(parse_redis_value(b"-ASK 14000 node:6380\r\n")), - _ => panic!("Node should not be called now"), - }, - 6380 => match count { - 1 => { - assert!(contains_slice(cmd, b"ASKING")); - Err(Ok(Value::Okay)) - } - 2 => { - assert!(contains_slice(cmd, b"GET")); - Err(Ok(Value::BulkString(b"123".to_vec()))) - } - _ => panic!("Node should not be called now"), - }, - _ => panic!("Wrong node"), + #[test] + fn test_async_cluster_ask_redirect() { + let name = "node"; + let completed = Arc::new(AtomicI32::new(0)); + let MockEnv { + async_connection: mut connection, + handler: _handler, + runtime, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]), + name, + { + move |cmd: &[u8], port| { + respond_startup_two_nodes(name, cmd)?; + // Error twice with io-error, ensure connection is reestablished w/out calling + // other node (i.e., not doing a full slot rebuild) + let count = completed.fetch_add(1, Ordering::SeqCst); + match port { + 6379 => match count { + 0 => Err(parse_redis_value(b"-ASK 14000 node:6380\r\n")), + _ => panic!("Node should not be called now"), + }, + 6380 => match count { + 1 => { + assert!(contains_slice(cmd, b"ASKING")); + Err(Ok(Value::Okay)) + } + 2 => { + assert!(contains_slice(cmd, b"GET")); + Err(Ok(Value::BulkString(b"123".to_vec()))) + } + _ => panic!("Node should not be called now"), + }, + _ => panic!("Wrong node"), + } } - } - }, - ); + }, + ); - let value = runtime.block_on( - cmd("GET") - .arg("test") - .query_async::<_, Option>(&mut connection), - ); + let value = runtime.block_on( + cmd("GET") + .arg("test") + .query_async::<_, Option>(&mut connection), + ); - assert_eq!(value, Ok(Some(123))); -} + assert_eq!(value, Ok(Some(123))); + } -#[test] -fn test_async_cluster_ask_save_new_connection() { - let name = "node"; - let ping_attempts = Arc::new(AtomicI32::new(0)); - let ping_attempts_clone = ping_attempts.clone(); - let MockEnv { - async_connection: mut connection, - handler: _handler, - runtime, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]), - name, - { - move |cmd: &[u8], port| { - if port != 6391 { + #[test] + fn test_async_cluster_ask_save_new_connection() { + let name = "node"; + let ping_attempts = Arc::new(AtomicI32::new(0)); + let ping_attempts_clone = ping_attempts.clone(); + let MockEnv { + async_connection: mut connection, + handler: _handler, + runtime, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]), + name, + { + move |cmd: &[u8], port| { + if port != 6391 { + respond_startup_two_nodes(name, cmd)?; + return Err(parse_redis_value(b"-ASK 14000 node:6391\r\n")); + } + + if contains_slice(cmd, b"PING") { + ping_attempts_clone.fetch_add(1, Ordering::Relaxed); + } respond_startup_two_nodes(name, cmd)?; - return Err(parse_redis_value(b"-ASK 14000 node:6391\r\n")); + Err(Ok(Value::Okay)) } + }, + ); - if contains_slice(cmd, b"PING") { - ping_attempts_clone.fetch_add(1, Ordering::Relaxed); - } - respond_startup_two_nodes(name, cmd)?; - Err(Ok(Value::Okay)) - } - }, - ); + for _ in 0..4 { + runtime + .block_on( + cmd("GET") + .arg("test") + .query_async::<_, Value>(&mut connection), + ) + .unwrap(); + } - for _ in 0..4 { - runtime - .block_on( - cmd("GET") - .arg("test") - .query_async::<_, Value>(&mut connection), - ) - .unwrap(); + assert_eq!(ping_attempts.load(Ordering::Relaxed), 1); } - assert_eq!(ping_attempts.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_async_cluster_reset_routing_if_redirect_fails() { - let name = "test_async_cluster_reset_routing_if_redirect_fails"; - let completed = Arc::new(AtomicI32::new(0)); - let MockEnv { - async_connection: mut connection, - handler: _handler, - runtime, - .. - } = MockEnv::new(name, move |cmd: &[u8], port| { - if port != 6379 && port != 6380 { - return Err(Err(RedisError::from(std::io::Error::new( - std::io::ErrorKind::BrokenPipe, - "mock-io-error", - )))); - } - respond_startup_two_nodes(name, cmd)?; - let count = completed.fetch_add(1, Ordering::SeqCst); - match (port, count) { - // redirect once to non-existing node - (6379, 0) => Err(parse_redis_value( - format!("-ASK 14000 {name}:9999\r\n").as_bytes(), - )), - // accept the next request - (6379, 1) => { - assert!(contains_slice(cmd, b"GET")); - Err(Ok(Value::BulkString(b"123".to_vec()))) + #[test] + fn test_async_cluster_reset_routing_if_redirect_fails() { + let name = "test_async_cluster_reset_routing_if_redirect_fails"; + let completed = Arc::new(AtomicI32::new(0)); + let MockEnv { + async_connection: mut connection, + handler: _handler, + runtime, + .. + } = MockEnv::new(name, move |cmd: &[u8], port| { + if port != 6379 && port != 6380 { + return Err(Err(RedisError::from(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "mock-io-error", + )))); } - _ => panic!("Wrong node. port: {port}, received count: {count}"), - } - }); - - let value = runtime.block_on( - cmd("GET") - .arg("test") - .query_async::<_, Option>(&mut connection), - ); - - assert_eq!(value, Ok(Some(123))); -} - -#[test] -fn test_async_cluster_ask_redirect_even_if_original_call_had_no_route() { - let name = "node"; - let completed = Arc::new(AtomicI32::new(0)); - let MockEnv { - async_connection: mut connection, - handler: _handler, - runtime, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]), - name, - { - move |cmd: &[u8], port| { - respond_startup_two_nodes(name, cmd)?; - // Error twice with io-error, ensure connection is reestablished w/out calling - // other node (i.e., not doing a full slot rebuild) - let count = completed.fetch_add(1, Ordering::SeqCst); - if count == 0 { - return Err(parse_redis_value(b"-ASK 14000 node:6380\r\n")); - } - match port { - 6380 => match count { - 1 => { - assert!( - contains_slice(cmd, b"ASKING"), - "{:?}", - std::str::from_utf8(cmd) - ); - Err(Ok(Value::Okay)) - } - 2 => { - assert!(contains_slice(cmd, b"EVAL")); - Err(Ok(Value::Okay)) - } - _ => panic!("Node should not be called now"), - }, - _ => panic!("Wrong node"), + respond_startup_two_nodes(name, cmd)?; + let count = completed.fetch_add(1, Ordering::SeqCst); + match (port, count) { + // redirect once to non-existing node + (6379, 0) => Err(parse_redis_value( + format!("-ASK 14000 {name}:9999\r\n").as_bytes(), + )), + // accept the next request + (6379, 1) => { + assert!(contains_slice(cmd, b"GET")); + Err(Ok(Value::BulkString(b"123".to_vec()))) } + _ => panic!("Wrong node. port: {port}, received count: {count}"), } - }, - ); + }); - let value = runtime.block_on( - cmd("EVAL") // Eval command has no directed, and so is redirected randomly - .query_async::<_, Value>(&mut connection), - ); + let value = runtime.block_on( + cmd("GET") + .arg("test") + .query_async::<_, Option>(&mut connection), + ); - assert_eq!(value, Ok(Value::Okay)); -} + assert_eq!(value, Ok(Some(123))); + } -#[test] -fn test_async_cluster_ask_error_when_new_node_is_added() { - let name = "ask_with_extra_nodes"; - - let requests = atomic::AtomicUsize::new(0); - let started = atomic::AtomicBool::new(false); - - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::new(name, move |cmd: &[u8], port| { - if !started.load(atomic::Ordering::SeqCst) { - respond_startup(name, cmd)?; - } - started.store(true, atomic::Ordering::SeqCst); + #[test] + fn test_async_cluster_ask_redirect_even_if_original_call_had_no_route() { + let name = "node"; + let completed = Arc::new(AtomicI32::new(0)); + let MockEnv { + async_connection: mut connection, + handler: _handler, + runtime, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]), + name, + { + move |cmd: &[u8], port| { + respond_startup_two_nodes(name, cmd)?; + // Error twice with io-error, ensure connection is reestablished w/out calling + // other node (i.e., not doing a full slot rebuild) + let count = completed.fetch_add(1, Ordering::SeqCst); + if count == 0 { + return Err(parse_redis_value(b"-ASK 14000 node:6380\r\n")); + } + match port { + 6380 => match count { + 1 => { + assert!( + contains_slice(cmd, b"ASKING"), + "{:?}", + std::str::from_utf8(cmd) + ); + Err(Ok(Value::Okay)) + } + 2 => { + assert!(contains_slice(cmd, b"EVAL")); + Err(Ok(Value::Okay)) + } + _ => panic!("Node should not be called now"), + }, + _ => panic!("Wrong node"), + } + } + }, + ); - if contains_slice(cmd, b"PING") { - return Err(Ok(Value::SimpleString("OK".into()))); - } + let value = runtime.block_on( + cmd("EVAL") // Eval command has no directed, and so is redirected randomly + .query_async::<_, Value>(&mut connection), + ); - let i = requests.fetch_add(1, atomic::Ordering::SeqCst); + assert_eq!(value, Ok(Value::Okay)); + } - match i { - // Respond that the key exists on a node that does not yet have a connection: - 0 => Err(parse_redis_value( - format!("-ASK 123 {name}:6380\r\n").as_bytes(), - )), - 1 => { - assert_eq!(port, 6380); - assert!(contains_slice(cmd, b"ASKING")); - Err(Ok(Value::Okay)) - } - 2 => { - assert_eq!(port, 6380); - assert!(contains_slice(cmd, b"GET")); - Err(Ok(Value::BulkString(b"123".to_vec()))) - } - _ => { - panic!("Unexpected request: {:?}", cmd); + #[test] + fn test_async_cluster_ask_error_when_new_node_is_added() { + let name = "ask_with_extra_nodes"; + + let requests = atomic::AtomicUsize::new(0); + let started = atomic::AtomicBool::new(false); + + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::new(name, move |cmd: &[u8], port| { + if !started.load(atomic::Ordering::SeqCst) { + respond_startup(name, cmd)?; } - } - }); + started.store(true, atomic::Ordering::SeqCst); - let value = runtime.block_on( - cmd("GET") - .arg("test") - .query_async::<_, Option>(&mut connection), - ); - - assert_eq!(value, Ok(Some(123))); -} - -#[test] -fn test_async_cluster_replica_read() { - let name = "node"; - - // requests should route to replica - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |cmd: &[u8], port| { - respond_startup_with_replica(name, cmd)?; - match port { - 6380 => Err(Ok(Value::BulkString(b"123".to_vec()))), - _ => panic!("Wrong node"), + if contains_slice(cmd, b"PING") { + return Err(Ok(Value::SimpleString("OK".into()))); } - }, - ); - let value = runtime.block_on( - cmd("GET") - .arg("test") - .query_async::<_, Option>(&mut connection), - ); - assert_eq!(value, Ok(Some(123))); - - // requests should route to primary - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |cmd: &[u8], port| { - respond_startup_with_replica(name, cmd)?; - match port { - 6379 => Err(Ok(Value::SimpleString("OK".into()))), - _ => panic!("Wrong node"), + let i = requests.fetch_add(1, atomic::Ordering::SeqCst); + + match i { + // Respond that the key exists on a node that does not yet have a connection: + 0 => Err(parse_redis_value( + format!("-ASK 123 {name}:6380\r\n").as_bytes(), + )), + 1 => { + assert_eq!(port, 6380); + assert!(contains_slice(cmd, b"ASKING")); + Err(Ok(Value::Okay)) + } + 2 => { + assert_eq!(port, 6380); + assert!(contains_slice(cmd, b"GET")); + Err(Ok(Value::BulkString(b"123".to_vec()))) + } + _ => { + panic!("Unexpected request: {:?}", cmd); + } } - }, - ); + }); - let value = runtime.block_on( - cmd("SET") - .arg("test") - .arg("123") - .query_async::<_, Option>(&mut connection), - ); - assert_eq!(value, Ok(Some(Value::SimpleString("OK".to_owned())))); -} + let value = runtime.block_on( + cmd("GET") + .arg("test") + .query_async::<_, Option>(&mut connection), + ); -fn test_async_cluster_fan_out( - command: &'static str, - expected_ports: Vec, - slots_config: Option>, -) { - let name = "node"; - let found_ports = Arc::new(std::sync::Mutex::new(Vec::new())); - let ports_clone = found_ports.clone(); - let mut cmd = Cmd::new(); - for arg in command.split_whitespace() { - cmd.arg(arg); + assert_eq!(value, Ok(Some(123))); } - let packed_cmd = cmd.get_packed_command(); - // requests should route to replica - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |received_cmd: &[u8], port| { - respond_startup_with_replica_using_config(name, received_cmd, slots_config.clone())?; - if received_cmd == packed_cmd { - ports_clone.lock().unwrap().push(port); - return Err(Ok(Value::SimpleString("OK".into()))); - } - Ok(()) - }, - ); - let _ = runtime.block_on(cmd.query_async::<_, Option<()>>(&mut connection)); - found_ports.lock().unwrap().sort(); - // MockEnv creates 2 mock connections. - assert_eq!(*found_ports.lock().unwrap(), expected_ports); -} - -#[test] -fn test_async_cluster_fan_out_to_all_primaries() { - test_async_cluster_fan_out("FLUSHALL", vec![6379, 6381], None); -} + #[test] + fn test_async_cluster_replica_read() { + let name = "node"; + + // requests should route to replica + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |cmd: &[u8], port| { + respond_startup_with_replica(name, cmd)?; + match port { + 6380 => Err(Ok(Value::BulkString(b"123".to_vec()))), + _ => panic!("Wrong node"), + } + }, + ); -#[test] -fn test_async_cluster_fan_out_to_all_nodes() { - test_async_cluster_fan_out("CONFIG SET", vec![6379, 6380, 6381, 6382], None); -} + let value = runtime.block_on( + cmd("GET") + .arg("test") + .query_async::<_, Option>(&mut connection), + ); + assert_eq!(value, Ok(Some(123))); -#[test] -fn test_async_cluster_fan_out_once_to_each_primary_when_no_replicas_are_available() { - test_async_cluster_fan_out( - "CONFIG SET", - vec![6379, 6381], - Some(vec![ - MockSlotRange { - primary_port: 6379, - replica_ports: Vec::new(), - slot_range: (0..8191), - }, - MockSlotRange { - primary_port: 6381, - replica_ports: Vec::new(), - slot_range: (8192..16383), + // requests should route to primary + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |cmd: &[u8], port| { + respond_startup_with_replica(name, cmd)?; + match port { + 6379 => Err(Ok(Value::SimpleString("OK".into()))), + _ => panic!("Wrong node"), + } }, - ]), - ); -} + ); -#[test] -fn test_async_cluster_fan_out_once_even_if_primary_has_multiple_slot_ranges() { - test_async_cluster_fan_out( - "CONFIG SET", - vec![6379, 6380, 6381, 6382], - Some(vec![ - MockSlotRange { - primary_port: 6379, - replica_ports: vec![6380], - slot_range: (0..4000), - }, - MockSlotRange { - primary_port: 6381, - replica_ports: vec![6382], - slot_range: (4001..8191), - }, - MockSlotRange { - primary_port: 6379, - replica_ports: vec![6380], - slot_range: (8192..8200), - }, - MockSlotRange { - primary_port: 6381, - replica_ports: vec![6382], - slot_range: (8201..16383), + let value = runtime.block_on( + cmd("SET") + .arg("test") + .arg("123") + .query_async::<_, Option>(&mut connection), + ); + assert_eq!(value, Ok(Some(Value::SimpleString("OK".to_owned())))); + } + + fn test_async_cluster_fan_out( + command: &'static str, + expected_ports: Vec, + slots_config: Option>, + ) { + let name = "node"; + let found_ports = Arc::new(std::sync::Mutex::new(Vec::new())); + let ports_clone = found_ports.clone(); + let mut cmd = Cmd::new(); + for arg in command.split_whitespace() { + cmd.arg(arg); + } + let packed_cmd = cmd.get_packed_command(); + // requests should route to replica + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |received_cmd: &[u8], port| { + respond_startup_with_replica_using_config( + name, + received_cmd, + slots_config.clone(), + )?; + if received_cmd == packed_cmd { + ports_clone.lock().unwrap().push(port); + return Err(Ok(Value::SimpleString("OK".into()))); + } + Ok(()) }, - ]), - ); -} + ); -#[test] -fn test_async_cluster_route_according_to_passed_argument() { - let name = "test_async_cluster_route_according_to_passed_argument"; - - let touched_ports = Arc::new(std::sync::Mutex::new(Vec::new())); - let cloned_ports = touched_ports.clone(); - - // requests should route to replica - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |cmd: &[u8], port| { - respond_startup_with_replica(name, cmd)?; - cloned_ports.lock().unwrap().push(port); - Err(Ok(Value::Nil)) - }, - ); - - let mut cmd = cmd("GET"); - cmd.arg("test"); - let _ = runtime.block_on(connection.route_command( - &cmd, - RoutingInfo::MultiNode((MultipleNodeRoutingInfo::AllMasters, None)), - )); - { - let mut touched_ports = touched_ports.lock().unwrap(); - touched_ports.sort(); - assert_eq!(*touched_ports, vec![6379, 6381]); - touched_ports.clear(); + let _ = runtime.block_on(cmd.query_async::<_, Option<()>>(&mut connection)); + found_ports.lock().unwrap().sort(); + // MockEnv creates 2 mock connections. + assert_eq!(*found_ports.lock().unwrap(), expected_ports); } - let _ = runtime.block_on(connection.route_command( - &cmd, - RoutingInfo::MultiNode((MultipleNodeRoutingInfo::AllNodes, None)), - )); - { - let mut touched_ports = touched_ports.lock().unwrap(); - touched_ports.sort(); - assert_eq!(*touched_ports, vec![6379, 6380, 6381, 6382]); - touched_ports.clear(); + #[test] + fn test_async_cluster_fan_out_to_all_primaries() { + test_async_cluster_fan_out("FLUSHALL", vec![6379, 6381], None); } - let _ = runtime.block_on(connection.route_command( - &cmd, - RoutingInfo::SingleNode(SingleNodeRoutingInfo::ByAddress { - host: name.to_string(), - port: 6382, - }), - )); - { - let mut touched_ports = touched_ports.lock().unwrap(); - touched_ports.sort(); - assert_eq!(*touched_ports, vec![6382]); - touched_ports.clear(); + #[test] + fn test_async_cluster_fan_out_to_all_nodes() { + test_async_cluster_fan_out("CONFIG SET", vec![6379, 6380, 6381, 6382], None); } -} -#[test] -fn test_async_cluster_fan_out_and_aggregate_numeric_response_with_min() { - let name = "test_async_cluster_fan_out_and_aggregate_numeric_response"; - let mut cmd = Cmd::new(); - cmd.arg("SLOWLOG").arg("LEN"); - - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |received_cmd: &[u8], port| { - respond_startup_with_replica_using_config(name, received_cmd, None)?; - - let res = 6383 - port as i64; - Err(Ok(Value::Int(res))) // this results in 1,2,3,4 - }, - ); + #[test] + fn test_async_cluster_fan_out_once_to_each_primary_when_no_replicas_are_available() { + test_async_cluster_fan_out( + "CONFIG SET", + vec![6379, 6381], + Some(vec![ + MockSlotRange { + primary_port: 6379, + replica_ports: Vec::new(), + slot_range: (0..8191), + }, + MockSlotRange { + primary_port: 6381, + replica_ports: Vec::new(), + slot_range: (8192..16383), + }, + ]), + ); + } - let result = runtime - .block_on(cmd.query_async::<_, i64>(&mut connection)) - .unwrap(); - assert_eq!(result, 10, "{result}"); -} + #[test] + fn test_async_cluster_fan_out_once_even_if_primary_has_multiple_slot_ranges() { + test_async_cluster_fan_out( + "CONFIG SET", + vec![6379, 6380, 6381, 6382], + Some(vec![ + MockSlotRange { + primary_port: 6379, + replica_ports: vec![6380], + slot_range: (0..4000), + }, + MockSlotRange { + primary_port: 6381, + replica_ports: vec![6382], + slot_range: (4001..8191), + }, + MockSlotRange { + primary_port: 6379, + replica_ports: vec![6380], + slot_range: (8192..8200), + }, + MockSlotRange { + primary_port: 6381, + replica_ports: vec![6382], + slot_range: (8201..16383), + }, + ]), + ); + } -#[test] -fn test_async_cluster_fan_out_and_aggregate_logical_array_response() { - let name = "test_async_cluster_fan_out_and_aggregate_logical_array_response"; - let mut cmd = Cmd::new(); - cmd.arg("SCRIPT") - .arg("EXISTS") - .arg("foo") - .arg("bar") - .arg("baz") - .arg("barvaz"); - - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |received_cmd: &[u8], port| { - respond_startup_with_replica_using_config(name, received_cmd, None)?; - - if port == 6381 { - return Err(Ok(Value::Array(vec![ - Value::Int(0), - Value::Int(0), - Value::Int(1), - Value::Int(1), - ]))); - } else if port == 6379 { - return Err(Ok(Value::Array(vec![ - Value::Int(0), - Value::Int(1), - Value::Int(0), - Value::Int(1), - ]))); - } + #[test] + fn test_async_cluster_route_according_to_passed_argument() { + let name = "test_async_cluster_route_according_to_passed_argument"; + + let touched_ports = Arc::new(std::sync::Mutex::new(Vec::new())); + let cloned_ports = touched_ports.clone(); + + // requests should route to replica + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |cmd: &[u8], port| { + respond_startup_with_replica(name, cmd)?; + cloned_ports.lock().unwrap().push(port); + Err(Ok(Value::Nil)) + }, + ); - panic!("unexpected port {port}"); - }, - ); + let mut cmd = cmd("GET"); + cmd.arg("test"); + let _ = runtime.block_on(connection.route_command( + &cmd, + RoutingInfo::MultiNode((MultipleNodeRoutingInfo::AllMasters, None)), + )); + { + let mut touched_ports = touched_ports.lock().unwrap(); + touched_ports.sort(); + assert_eq!(*touched_ports, vec![6379, 6381]); + touched_ports.clear(); + } - let result = runtime - .block_on(cmd.query_async::<_, Vec>(&mut connection)) - .unwrap(); - assert_eq!(result, vec![0, 0, 0, 1], "{result:?}"); -} + let _ = runtime.block_on(connection.route_command( + &cmd, + RoutingInfo::MultiNode((MultipleNodeRoutingInfo::AllNodes, None)), + )); + { + let mut touched_ports = touched_ports.lock().unwrap(); + touched_ports.sort(); + assert_eq!(*touched_ports, vec![6379, 6380, 6381, 6382]); + touched_ports.clear(); + } -#[test] -fn test_async_cluster_fan_out_and_return_one_succeeded_response() { - let name = "test_async_cluster_fan_out_and_return_one_succeeded_response"; - let mut cmd = Cmd::new(); - cmd.arg("SCRIPT").arg("KILL"); - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |received_cmd: &[u8], port| { - respond_startup_with_replica_using_config(name, received_cmd, None)?; - if port == 6381 { - return Err(Ok(Value::Okay)); - } else if port == 6379 { - return Err(Err(( - ErrorKind::NotBusy, - "No scripts in execution right now", - ) - .into())); - } + let _ = runtime.block_on(connection.route_command( + &cmd, + RoutingInfo::SingleNode(SingleNodeRoutingInfo::ByAddress { + host: name.to_string(), + port: 6382, + }), + )); + { + let mut touched_ports = touched_ports.lock().unwrap(); + touched_ports.sort(); + assert_eq!(*touched_ports, vec![6382]); + touched_ports.clear(); + } + } - panic!("unexpected port {port}"); - }, - ); + #[test] + fn test_async_cluster_fan_out_and_aggregate_numeric_response_with_min() { + let name = "test_async_cluster_fan_out_and_aggregate_numeric_response"; + let mut cmd = Cmd::new(); + cmd.arg("SLOWLOG").arg("LEN"); + + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |received_cmd: &[u8], port| { + respond_startup_with_replica_using_config(name, received_cmd, None)?; + + let res = 6383 - port as i64; + Err(Ok(Value::Int(res))) // this results in 1,2,3,4 + }, + ); - let result = runtime - .block_on(cmd.query_async::<_, Value>(&mut connection)) - .unwrap(); - assert_eq!(result, Value::Okay, "{result:?}"); -} + let result = runtime + .block_on(cmd.query_async::<_, i64>(&mut connection)) + .unwrap(); + assert_eq!(result, 10, "{result}"); + } -#[test] -fn test_async_cluster_fan_out_and_fail_one_succeeded_if_there_are_no_successes() { - let name = "test_async_cluster_fan_out_and_fail_one_succeeded_if_there_are_no_successes"; - let mut cmd = Cmd::new(); - cmd.arg("SCRIPT").arg("KILL"); - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |received_cmd: &[u8], _port| { - respond_startup_with_replica_using_config(name, received_cmd, None)?; - - Err(Err(( - ErrorKind::NotBusy, - "No scripts in execution right now", - ) - .into())) - }, - ); + #[test] + fn test_async_cluster_fan_out_and_aggregate_logical_array_response() { + let name = "test_async_cluster_fan_out_and_aggregate_logical_array_response"; + let mut cmd = Cmd::new(); + cmd.arg("SCRIPT") + .arg("EXISTS") + .arg("foo") + .arg("bar") + .arg("baz") + .arg("barvaz"); + + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |received_cmd: &[u8], port| { + respond_startup_with_replica_using_config(name, received_cmd, None)?; + + if port == 6381 { + return Err(Ok(Value::Array(vec![ + Value::Int(0), + Value::Int(0), + Value::Int(1), + Value::Int(1), + ]))); + } else if port == 6379 { + return Err(Ok(Value::Array(vec![ + Value::Int(0), + Value::Int(1), + Value::Int(0), + Value::Int(1), + ]))); + } - let result = runtime - .block_on(cmd.query_async::<_, Value>(&mut connection)) - .unwrap_err(); - assert_eq!(result.kind(), ErrorKind::NotBusy, "{:?}", result.kind()); -} + panic!("unexpected port {port}"); + }, + ); -#[test] -fn test_async_cluster_fan_out_and_return_all_succeeded_response() { - let name = "test_async_cluster_fan_out_and_return_all_succeeded_response"; - let cmd = cmd("FLUSHALL"); - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |received_cmd: &[u8], _port| { - respond_startup_with_replica_using_config(name, received_cmd, None)?; - Err(Ok(Value::Okay)) - }, - ); + let result = runtime + .block_on(cmd.query_async::<_, Vec>(&mut connection)) + .unwrap(); + assert_eq!(result, vec![0, 0, 0, 1], "{result:?}"); + } + + #[test] + fn test_async_cluster_fan_out_and_return_one_succeeded_response() { + let name = "test_async_cluster_fan_out_and_return_one_succeeded_response"; + let mut cmd = Cmd::new(); + cmd.arg("SCRIPT").arg("KILL"); + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |received_cmd: &[u8], port| { + respond_startup_with_replica_using_config(name, received_cmd, None)?; + if port == 6381 { + return Err(Ok(Value::Okay)); + } else if port == 6379 { + return Err(Err(( + ErrorKind::NotBusy, + "No scripts in execution right now", + ) + .into())); + } + + panic!("unexpected port {port}"); + }, + ); - let result = runtime - .block_on(cmd.query_async::<_, Value>(&mut connection)) - .unwrap(); - assert_eq!(result, Value::Okay, "{result:?}"); -} + let result = runtime + .block_on(cmd.query_async::<_, Value>(&mut connection)) + .unwrap(); + assert_eq!(result, Value::Okay, "{result:?}"); + } -#[test] -fn test_async_cluster_fan_out_and_fail_all_succeeded_if_there_is_a_single_failure() { - let name = "test_async_cluster_fan_out_and_fail_all_succeeded_if_there_is_a_single_failure"; - let cmd = cmd("FLUSHALL"); - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |received_cmd: &[u8], port| { - respond_startup_with_replica_using_config(name, received_cmd, None)?; - if port == 6381 { - return Err(Err(( + #[test] + fn test_async_cluster_fan_out_and_fail_one_succeeded_if_there_are_no_successes() { + let name = "test_async_cluster_fan_out_and_fail_one_succeeded_if_there_are_no_successes"; + let mut cmd = Cmd::new(); + cmd.arg("SCRIPT").arg("KILL"); + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |received_cmd: &[u8], _port| { + respond_startup_with_replica_using_config(name, received_cmd, None)?; + + Err(Err(( ErrorKind::NotBusy, "No scripts in execution right now", ) - .into())); - } - Err(Ok(Value::Okay)) - }, - ); - - let result = runtime - .block_on(cmd.query_async::<_, Value>(&mut connection)) - .unwrap_err(); - assert_eq!(result.kind(), ErrorKind::NotBusy, "{:?}", result.kind()); -} - -#[test] -fn test_async_cluster_fan_out_and_return_one_succeeded_ignoring_empty_values() { - let name = "test_async_cluster_fan_out_and_return_one_succeeded_ignoring_empty_values"; - let cmd = cmd("RANDOMKEY"); - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |received_cmd: &[u8], port| { - respond_startup_with_replica_using_config(name, received_cmd, None)?; - if port == 6381 { - return Err(Ok(Value::BulkString("foo".as_bytes().to_vec()))); - } - Err(Ok(Value::Nil)) - }, - ); - - let result = runtime - .block_on(cmd.query_async::<_, String>(&mut connection)) - .unwrap(); - assert_eq!(result, "foo", "{result:?}"); -} + .into())) + }, + ); -#[test] -fn test_async_cluster_fan_out_and_return_map_of_results_for_special_response_policy() { - let name = "foo"; - let mut cmd = Cmd::new(); - cmd.arg("LATENCY").arg("LATEST"); - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |received_cmd: &[u8], port| { - respond_startup_with_replica_using_config(name, received_cmd, None)?; - Err(Ok(Value::BulkString( - format!("latency: {port}").into_bytes(), - ))) - }, - ); + let result = runtime + .block_on(cmd.query_async::<_, Value>(&mut connection)) + .unwrap_err(); + assert_eq!(result.kind(), ErrorKind::NotBusy, "{:?}", result.kind()); + } - // TODO once RESP3 is in, return this as a map - let mut result = runtime - .block_on(cmd.query_async::<_, Vec<(String, String)>>(&mut connection)) - .unwrap(); - result.sort(); - assert_eq!( - result, - vec![ - (format!("{name}:6379"), "latency: 6379".to_string()), - (format!("{name}:6380"), "latency: 6380".to_string()), - (format!("{name}:6381"), "latency: 6381".to_string()), - (format!("{name}:6382"), "latency: 6382".to_string()) - ], - "{result:?}" - ); -} + #[test] + fn test_async_cluster_fan_out_and_return_all_succeeded_response() { + let name = "test_async_cluster_fan_out_and_return_all_succeeded_response"; + let cmd = cmd("FLUSHALL"); + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |received_cmd: &[u8], _port| { + respond_startup_with_replica_using_config(name, received_cmd, None)?; + Err(Ok(Value::Okay)) + }, + ); -#[test] -fn test_async_cluster_fan_out_and_combine_arrays_of_values() { - let name = "foo"; - let cmd = cmd("KEYS"); - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |received_cmd: &[u8], port| { - respond_startup_with_replica_using_config(name, received_cmd, None)?; - Err(Ok(Value::Array(vec![Value::BulkString( - format!("key:{port}").into_bytes(), - )]))) - }, - ); + let result = runtime + .block_on(cmd.query_async::<_, Value>(&mut connection)) + .unwrap(); + assert_eq!(result, Value::Okay, "{result:?}"); + } - let mut result = runtime - .block_on(cmd.query_async::<_, Vec>(&mut connection)) - .unwrap(); - result.sort(); - assert_eq!( - result, - vec!["key:6379".to_string(), "key:6381".to_string(),], - "{result:?}" - ); -} + #[test] + fn test_async_cluster_fan_out_and_fail_all_succeeded_if_there_is_a_single_failure() { + let name = "test_async_cluster_fan_out_and_fail_all_succeeded_if_there_is_a_single_failure"; + let cmd = cmd("FLUSHALL"); + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |received_cmd: &[u8], port| { + respond_startup_with_replica_using_config(name, received_cmd, None)?; + if port == 6381 { + return Err(Err(( + ErrorKind::NotBusy, + "No scripts in execution right now", + ) + .into())); + } + Err(Ok(Value::Okay)) + }, + ); -#[test] -fn test_async_cluster_split_multi_shard_command_and_combine_arrays_of_values() { - let name = "test_async_cluster_split_multi_shard_command_and_combine_arrays_of_values"; - let mut cmd = cmd("MGET"); - cmd.arg("foo").arg("bar").arg("baz"); - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |received_cmd: &[u8], port| { - respond_startup_with_replica_using_config(name, received_cmd, None)?; - let cmd_str = std::str::from_utf8(received_cmd).unwrap(); - let results = ["foo", "bar", "baz"] - .iter() - .filter_map(|expected_key| { - if cmd_str.contains(expected_key) { - Some(Value::BulkString( - format!("{expected_key}-{port}").into_bytes(), - )) - } else { - None - } - }) - .collect(); - Err(Ok(Value::Array(results))) - }, - ); + let result = runtime + .block_on(cmd.query_async::<_, Value>(&mut connection)) + .unwrap_err(); + assert_eq!(result.kind(), ErrorKind::NotBusy, "{:?}", result.kind()); + } - let result = runtime - .block_on(cmd.query_async::<_, Vec>(&mut connection)) - .unwrap(); - assert_eq!(result, vec!["foo-6382", "bar-6380", "baz-6380"]); -} + #[test] + fn test_async_cluster_fan_out_and_return_one_succeeded_ignoring_empty_values() { + let name = "test_async_cluster_fan_out_and_return_one_succeeded_ignoring_empty_values"; + let cmd = cmd("RANDOMKEY"); + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |received_cmd: &[u8], port| { + respond_startup_with_replica_using_config(name, received_cmd, None)?; + if port == 6381 { + return Err(Ok(Value::BulkString("foo".as_bytes().to_vec()))); + } + Err(Ok(Value::Nil)) + }, + ); -#[test] -fn test_async_cluster_handle_asking_error_in_split_multi_shard_command() { - let name = "test_async_cluster_handle_asking_error_in_split_multi_shard_command"; - let mut cmd = cmd("MGET"); - cmd.arg("foo").arg("bar").arg("baz"); - let asking_called = Arc::new(AtomicU16::new(0)); - let asking_called_cloned = asking_called.clone(); - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]).read_from_replicas(), - name, - move |received_cmd: &[u8], port| { - respond_startup_with_replica_using_config(name, received_cmd, None)?; - let cmd_str = std::str::from_utf8(received_cmd).unwrap(); - if cmd_str.contains("ASKING") && port == 6382 { - asking_called_cloned.fetch_add(1, Ordering::Relaxed); - } - if port == 6380 && cmd_str.contains("baz") { - return Err(parse_redis_value( - format!("-ASK 14000 {name}:6382\r\n").as_bytes(), - )); - } - let results = ["foo", "bar", "baz"] - .iter() - .filter_map(|expected_key| { - if cmd_str.contains(expected_key) { - Some(Value::BulkString( - format!("{expected_key}-{port}").into_bytes(), - )) - } else { - None - } - }) - .collect(); - Err(Ok(Value::Array(results))) - }, - ); + let result = runtime + .block_on(cmd.query_async::<_, String>(&mut connection)) + .unwrap(); + assert_eq!(result, "foo", "{result:?}"); + } - let result = runtime - .block_on(cmd.query_async::<_, Vec>(&mut connection)) - .unwrap(); - assert_eq!(result, vec!["foo-6382", "bar-6380", "baz-6382"]); - assert_eq!(asking_called.load(Ordering::Relaxed), 1); -} + #[test] + fn test_async_cluster_fan_out_and_return_map_of_results_for_special_response_policy() { + let name = "foo"; + let mut cmd = Cmd::new(); + cmd.arg("LATENCY").arg("LATEST"); + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |received_cmd: &[u8], port| { + respond_startup_with_replica_using_config(name, received_cmd, None)?; + Err(Ok(Value::BulkString( + format!("latency: {port}").into_bytes(), + ))) + }, + ); -#[test] -fn test_async_cluster_with_username_and_password() { - let cluster = TestClusterContext::new_with_cluster_client_builder( - 3, - 0, - |builder| { - builder - .username(RedisCluster::username().to_string()) - .password(RedisCluster::password().to_string()) - }, - false, - ); - cluster.disable_default_user(); + // TODO once RESP3 is in, return this as a map + let mut result = runtime + .block_on(cmd.query_async::<_, Vec<(String, String)>>(&mut connection)) + .unwrap(); + result.sort(); + assert_eq!( + result, + vec![ + (format!("{name}:6379"), "latency: 6379".to_string()), + (format!("{name}:6380"), "latency: 6380".to_string()), + (format!("{name}:6381"), "latency: 6381".to_string()), + (format!("{name}:6382"), "latency: 6382".to_string()) + ], + "{result:?}" + ); + } - block_on_all(async move { - let mut connection = cluster.async_connection().await; - cmd("SET") - .arg("test") - .arg("test_data") - .query_async(&mut connection) - .await?; - let res: String = cmd("GET") - .arg("test") - .clone() - .query_async(&mut connection) - .await?; - assert_eq!(res, "test_data"); - Ok::<_, RedisError>(()) - }) - .unwrap(); -} + #[test] + fn test_async_cluster_fan_out_and_combine_arrays_of_values() { + let name = "foo"; + let cmd = cmd("KEYS"); + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |received_cmd: &[u8], port| { + respond_startup_with_replica_using_config(name, received_cmd, None)?; + Err(Ok(Value::Array(vec![Value::BulkString( + format!("key:{port}").into_bytes(), + )]))) + }, + ); -#[test] -fn test_async_cluster_io_error() { - let name = "node"; - let completed = Arc::new(AtomicI32::new(0)); - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]).retries(2), - name, - move |cmd: &[u8], port| { - respond_startup_two_nodes(name, cmd)?; - // Error twice with io-error, ensure connection is reestablished w/out calling - // other node (i.e., not doing a full slot rebuild) - match port { - 6380 => panic!("Node should not be called"), - _ => match completed.fetch_add(1, Ordering::SeqCst) { - 0..=1 => Err(Err(RedisError::from(std::io::Error::new( - std::io::ErrorKind::ConnectionReset, - "mock-io-error", - )))), - _ => Err(Ok(Value::BulkString(b"123".to_vec()))), - }, - } - }, - ); + let mut result = runtime + .block_on(cmd.query_async::<_, Vec>(&mut connection)) + .unwrap(); + result.sort(); + assert_eq!( + result, + vec!["key:6379".to_string(), "key:6381".to_string(),], + "{result:?}" + ); + } - let value = runtime.block_on( - cmd("GET") - .arg("test") - .query_async::<_, Option>(&mut connection), - ); + #[test] + fn test_async_cluster_split_multi_shard_command_and_combine_arrays_of_values() { + let name = "test_async_cluster_split_multi_shard_command_and_combine_arrays_of_values"; + let mut cmd = cmd("MGET"); + cmd.arg("foo").arg("bar").arg("baz"); + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |received_cmd: &[u8], port| { + respond_startup_with_replica_using_config(name, received_cmd, None)?; + let cmd_str = std::str::from_utf8(received_cmd).unwrap(); + let results = ["foo", "bar", "baz"] + .iter() + .filter_map(|expected_key| { + if cmd_str.contains(expected_key) { + Some(Value::BulkString( + format!("{expected_key}-{port}").into_bytes(), + )) + } else { + None + } + }) + .collect(); + Err(Ok(Value::Array(results))) + }, + ); - assert_eq!(value, Ok(Some(123))); -} + let result = runtime + .block_on(cmd.query_async::<_, Vec>(&mut connection)) + .unwrap(); + assert_eq!(result, vec!["foo-6382", "bar-6380", "baz-6380"]); + } -#[test] -fn test_async_cluster_non_retryable_error_should_not_retry() { - let name = "node"; - let completed = Arc::new(AtomicI32::new(0)); - let MockEnv { - async_connection: mut connection, - handler: _handler, - runtime, - .. - } = MockEnv::new(name, { - let completed = completed.clone(); - move |cmd: &[u8], _| { - respond_startup_two_nodes(name, cmd)?; - // Error twice with io-error, ensure connection is reestablished w/out calling - // other node (i.e., not doing a full slot rebuild) - completed.fetch_add(1, Ordering::SeqCst); - Err(Err((ErrorKind::ReadOnly, "").into())) - } - }); + #[test] + fn test_async_cluster_handle_asking_error_in_split_multi_shard_command() { + let name = "test_async_cluster_handle_asking_error_in_split_multi_shard_command"; + let mut cmd = cmd("MGET"); + cmd.arg("foo").arg("bar").arg("baz"); + let asking_called = Arc::new(AtomicU16::new(0)); + let asking_called_cloned = asking_called.clone(); + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]).read_from_replicas(), + name, + move |received_cmd: &[u8], port| { + respond_startup_with_replica_using_config(name, received_cmd, None)?; + let cmd_str = std::str::from_utf8(received_cmd).unwrap(); + if cmd_str.contains("ASKING") && port == 6382 { + asking_called_cloned.fetch_add(1, Ordering::Relaxed); + } + if port == 6380 && cmd_str.contains("baz") { + return Err(parse_redis_value( + format!("-ASK 14000 {name}:6382\r\n").as_bytes(), + )); + } + let results = ["foo", "bar", "baz"] + .iter() + .filter_map(|expected_key| { + if cmd_str.contains(expected_key) { + Some(Value::BulkString( + format!("{expected_key}-{port}").into_bytes(), + )) + } else { + None + } + }) + .collect(); + Err(Ok(Value::Array(results))) + }, + ); - let value = runtime.block_on( - cmd("GET") - .arg("test") - .query_async::<_, Option>(&mut connection), - ); - - match value { - Ok(_) => panic!("result should be an error"), - Err(e) => match e.kind() { - ErrorKind::ReadOnly => {} - _ => panic!("Expected ReadOnly but got {:?}", e.kind()), - }, + let result = runtime + .block_on(cmd.query_async::<_, Vec>(&mut connection)) + .unwrap(); + assert_eq!(result, vec!["foo-6382", "bar-6380", "baz-6382"]); + assert_eq!(asking_called.load(Ordering::Relaxed), 1); } - assert_eq!(completed.load(Ordering::SeqCst), 1); -} -#[test] -fn test_async_cluster_can_be_created_with_partial_slot_coverage() { - let name = "test_async_cluster_can_be_created_with_partial_slot_coverage"; - let slots_config = Some(vec![ - MockSlotRange { - primary_port: 6379, - replica_ports: vec![], - slot_range: (0..8000), - }, - MockSlotRange { - primary_port: 6381, - replica_ports: vec![], - slot_range: (8201..16380), - }, - ]); - - let MockEnv { - async_connection: mut connection, - handler: _handler, - runtime, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]) - .retries(0) - .read_from_replicas(), - name, - move |received_cmd: &[u8], _| { - respond_startup_with_replica_using_config(name, received_cmd, slots_config.clone())?; - Err(Ok(Value::SimpleString("PONG".into()))) - }, - ); - - let res = runtime.block_on(connection.req_packed_command(&redis::cmd("PING"))); - assert!(res.is_ok()); -} + #[test] + fn test_async_cluster_with_username_and_password() { + let cluster = TestClusterContext::new_with_cluster_client_builder( + 3, + 0, + |builder| { + builder + .username(RedisCluster::username().to_string()) + .password(RedisCluster::password().to_string()) + }, + false, + ); + cluster.disable_default_user(); -#[test] -fn test_async_cluster_handle_complete_server_disconnect_without_panicking() { - let cluster = TestClusterContext::new_with_cluster_client_builder( - 3, - 0, - |builder| builder.retries(2), - false, - ); - block_on_all(async move { - let mut connection = cluster.async_connection().await; - drop(cluster); - for _ in 0..5 { - let cmd = cmd("PING"); - let result = connection - .route_command(&cmd, RoutingInfo::SingleNode(SingleNodeRoutingInfo::Random)) - .await; - // TODO - this should be a NoConnectionError, but ATM we get the errors from the failing - assert!(result.is_err()); - // This will route to all nodes - different path through the code. - let result = connection.req_packed_command(&cmd).await; - // TODO - this should be a NoConnectionError, but ATM we get the errors from the failing - assert!(result.is_err()); - } - Ok::<_, RedisError>(()) - }) - .unwrap(); -} + block_on_all(async move { + let mut connection = cluster.async_connection().await; + cmd("SET") + .arg("test") + .arg("test_data") + .query_async(&mut connection) + .await?; + let res: String = cmd("GET") + .arg("test") + .clone() + .query_async(&mut connection) + .await?; + assert_eq!(res, "test_data"); + Ok::<_, RedisError>(()) + }) + .unwrap(); + } -#[test] -fn test_async_cluster_reconnect_after_complete_server_disconnect() { - let cluster = TestClusterContext::new_with_cluster_client_builder( - 3, - 0, - |builder| builder.retries(2), - false, - ); - - block_on_all(async move { - let mut connection = cluster.async_connection().await; - drop(cluster); - for _ in 0..5 { - let cmd = cmd("PING"); - - let result = connection - .route_command(&cmd, RoutingInfo::SingleNode(SingleNodeRoutingInfo::Random)) - .await; - // TODO - this should be a NoConnectionError, but ATM we get the errors from the failing - assert!(result.is_err()); - - // This will route to all nodes - different path through the code. - let result = connection.req_packed_command(&cmd).await; - // TODO - this should be a NoConnectionError, but ATM we get the errors from the failing - assert!(result.is_err()); - - let _cluster = TestClusterContext::new_with_cluster_client_builder( - 3, - 0, - |builder| builder.retries(2), - false, - ); + #[test] + fn test_async_cluster_io_error() { + let name = "node"; + let completed = Arc::new(AtomicI32::new(0)); + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]).retries(2), + name, + move |cmd: &[u8], port| { + respond_startup_two_nodes(name, cmd)?; + // Error twice with io-error, ensure connection is reestablished w/out calling + // other node (i.e., not doing a full slot rebuild) + match port { + 6380 => panic!("Node should not be called"), + _ => match completed.fetch_add(1, Ordering::SeqCst) { + 0..=1 => Err(Err(RedisError::from(std::io::Error::new( + std::io::ErrorKind::ConnectionReset, + "mock-io-error", + )))), + _ => Err(Ok(Value::BulkString(b"123".to_vec()))), + }, + } + }, + ); - let result = connection.req_packed_command(&cmd).await.unwrap(); - assert_eq!(result, Value::SimpleString("PONG".to_string())); - } - Ok::<_, RedisError>(()) - }) - .unwrap(); -} + let value = runtime.block_on( + cmd("GET") + .arg("test") + .query_async::<_, Option>(&mut connection), + ); -#[test] -fn test_async_cluster_saves_reconnected_connection() { - let name = "test_async_cluster_saves_reconnected_connection"; - let ping_attempts = Arc::new(AtomicI32::new(0)); - let ping_attempts_clone = ping_attempts.clone(); - let get_attempts = AtomicI32::new(0); - - let MockEnv { - runtime, - async_connection: mut connection, - handler: _handler, - .. - } = MockEnv::with_client_builder( - ClusterClient::builder(vec![&*format!("redis://{name}")]).retries(1), - name, - move |cmd: &[u8], port| { - if port == 6380 { - respond_startup_two_nodes(name, cmd)?; - return Err(parse_redis_value( - format!("-MOVED 123 {name}:6379\r\n").as_bytes(), - )); - } + assert_eq!(value, Ok(Some(123))); + } - if contains_slice(cmd, b"PING") { - let connect_attempt = ping_attempts_clone.fetch_add(1, Ordering::Relaxed); - let past_get_attempts = get_attempts.load(Ordering::Relaxed); - // We want connection checks to fail after the first GET attempt, until it retries. Hence, we wait for 5 PINGs - - // 1. initial connection, - // 2. refresh slots on client creation, - // 3. refresh_connections `check_connection` after first GET failed, - // 4. refresh_connections `connect_and_check` after first GET failed, - // 5. reconnect on 2nd GET attempt. - // more than 5 attempts mean that the server reconnects more than once, which is the behavior we're testing against. - if past_get_attempts != 1 || connect_attempt > 3 { - respond_startup_two_nodes(name, cmd)?; - } - if connect_attempt > 5 { - panic!("Too many pings!"); - } - Err(Err(RedisError::from(std::io::Error::new( - std::io::ErrorKind::BrokenPipe, - "mock-io-error", - )))) - } else { + #[test] + fn test_async_cluster_non_retryable_error_should_not_retry() { + let name = "node"; + let completed = Arc::new(AtomicI32::new(0)); + let MockEnv { + async_connection: mut connection, + handler: _handler, + runtime, + .. + } = MockEnv::new(name, { + let completed = completed.clone(); + move |cmd: &[u8], _| { respond_startup_two_nodes(name, cmd)?; - let past_get_attempts = get_attempts.fetch_add(1, Ordering::Relaxed); - // we fail the initial GET request, and after that we'll fail the first reconnect attempt, in the `refresh_connections` attempt. - if past_get_attempts == 0 { - // Error once with io-error, ensure connection is reestablished w/out calling - // other node (i.e., not doing a full slot rebuild) - Err(Err(RedisError::from(std::io::Error::new( - std::io::ErrorKind::BrokenPipe, - "mock-io-error", - )))) - } else { - Err(Ok(Value::BulkString(b"123".to_vec()))) - } + // Error twice with io-error, ensure connection is reestablished w/out calling + // other node (i.e., not doing a full slot rebuild) + completed.fetch_add(1, Ordering::SeqCst); + Err(Err((ErrorKind::ReadOnly, "").into())) } - }, - ); + }); - for _ in 0..4 { let value = runtime.block_on( cmd("GET") .arg("test") .query_async::<_, Option>(&mut connection), ); - assert_eq!(value, Ok(Some(123))); + match value { + Ok(_) => panic!("result should be an error"), + Err(e) => match e.kind() { + ErrorKind::ReadOnly => {} + _ => panic!("Expected ReadOnly but got {:?}", e.kind()), + }, + } + assert_eq!(completed.load(Ordering::SeqCst), 1); } - // If you need to change the number here due to a change in the cluster, you probably also need to adjust the test. - // See the PING counts above to explain why 5 is the target number. - assert_eq!(ping_attempts.load(Ordering::Acquire), 5); -} -#[cfg(feature = "tls-rustls")] -mod mtls_test { - use crate::support::mtls_test::create_cluster_client_from_cluster; - use redis::ConnectionInfo; + #[test] + fn test_async_cluster_can_be_created_with_partial_slot_coverage() { + let name = "test_async_cluster_can_be_created_with_partial_slot_coverage"; + let slots_config = Some(vec![ + MockSlotRange { + primary_port: 6379, + replica_ports: vec![], + slot_range: (0..8000), + }, + MockSlotRange { + primary_port: 6381, + replica_ports: vec![], + slot_range: (8201..16380), + }, + ]); + + let MockEnv { + async_connection: mut connection, + handler: _handler, + runtime, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]) + .retries(0) + .read_from_replicas(), + name, + move |received_cmd: &[u8], _| { + respond_startup_with_replica_using_config( + name, + received_cmd, + slots_config.clone(), + )?; + Err(Ok(Value::SimpleString("PONG".into()))) + }, + ); - use super::*; + let res = runtime.block_on(connection.req_packed_command(&redis::cmd("PING"))); + assert!(res.is_ok()); + } #[test] - fn test_async_cluster_basic_cmd_with_mtls() { - let cluster = TestClusterContext::new_with_mtls(3, 0); + fn test_async_cluster_handle_complete_server_disconnect_without_panicking() { + let cluster = TestClusterContext::new_with_cluster_client_builder( + 3, + 0, + |builder| builder.retries(2), + false, + ); block_on_all(async move { - let client = create_cluster_client_from_cluster(&cluster, true).unwrap(); - let mut connection = client.get_async_connection().await.unwrap(); - cmd("SET") - .arg("test") - .arg("test_data") - .query_async(&mut connection) - .await?; - let res: String = cmd("GET") - .arg("test") - .clone() - .query_async(&mut connection) - .await?; - assert_eq!(res, "test_data"); + let mut connection = cluster.async_connection().await; + drop(cluster); + for _ in 0..5 { + let cmd = cmd("PING"); + let result = connection + .route_command(&cmd, RoutingInfo::SingleNode(SingleNodeRoutingInfo::Random)) + .await; + // TODO - this should be a NoConnectionError, but ATM we get the errors from the failing + assert!(result.is_err()); + // This will route to all nodes - different path through the code. + let result = connection.req_packed_command(&cmd).await; + // TODO - this should be a NoConnectionError, but ATM we get the errors from the failing + assert!(result.is_err()); + } Ok::<_, RedisError>(()) }) .unwrap(); } #[test] - fn test_async_cluster_should_not_connect_without_mtls_enabled() { - let cluster = TestClusterContext::new_with_mtls(3, 0); + fn test_async_cluster_reconnect_after_complete_server_disconnect() { + let cluster = TestClusterContext::new_with_cluster_client_builder( + 3, + 0, + |builder| builder.retries(2), + false, + ); + block_on_all(async move { + let mut connection = cluster.async_connection().await; + drop(cluster); + for _ in 0..5 { + let cmd = cmd("PING"); + + let result = connection + .route_command(&cmd, RoutingInfo::SingleNode(SingleNodeRoutingInfo::Random)) + .await; + // TODO - this should be a NoConnectionError, but ATM we get the errors from the failing + assert!(result.is_err()); + + // This will route to all nodes - different path through the code. + let result = connection.req_packed_command(&cmd).await; + // TODO - this should be a NoConnectionError, but ATM we get the errors from the failing + assert!(result.is_err()); + + let _cluster = TestClusterContext::new_with_cluster_client_builder( + 3, + 0, + |builder| builder.retries(2), + false, + ); + + let result = connection.req_packed_command(&cmd).await.unwrap(); + assert_eq!(result, Value::SimpleString("PONG".to_string())); + } + Ok::<_, RedisError>(()) + }) + .unwrap(); + } + + #[test] + fn test_async_cluster_saves_reconnected_connection() { + let name = "test_async_cluster_saves_reconnected_connection"; + let ping_attempts = Arc::new(AtomicI32::new(0)); + let ping_attempts_clone = ping_attempts.clone(); + let get_attempts = AtomicI32::new(0); + + let MockEnv { + runtime, + async_connection: mut connection, + handler: _handler, + .. + } = MockEnv::with_client_builder( + ClusterClient::builder(vec![&*format!("redis://{name}")]).retries(1), + name, + move |cmd: &[u8], port| { + if port == 6380 { + respond_startup_two_nodes(name, cmd)?; + return Err(parse_redis_value( + format!("-MOVED 123 {name}:6379\r\n").as_bytes(), + )); + } + + if contains_slice(cmd, b"PING") { + let connect_attempt = ping_attempts_clone.fetch_add(1, Ordering::Relaxed); + let past_get_attempts = get_attempts.load(Ordering::Relaxed); + // We want connection checks to fail after the first GET attempt, until it retries. Hence, we wait for 5 PINGs - + // 1. initial connection, + // 2. refresh slots on client creation, + // 3. refresh_connections `check_connection` after first GET failed, + // 4. refresh_connections `connect_and_check` after first GET failed, + // 5. reconnect on 2nd GET attempt. + // more than 5 attempts mean that the server reconnects more than once, which is the behavior we're testing against. + if past_get_attempts != 1 || connect_attempt > 3 { + respond_startup_two_nodes(name, cmd)?; + } + if connect_attempt > 5 { + panic!("Too many pings!"); + } + Err(Err(RedisError::from(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "mock-io-error", + )))) + } else { + respond_startup_two_nodes(name, cmd)?; + let past_get_attempts = get_attempts.fetch_add(1, Ordering::Relaxed); + // we fail the initial GET request, and after that we'll fail the first reconnect attempt, in the `refresh_connections` attempt. + if past_get_attempts == 0 { + // Error once with io-error, ensure connection is reestablished w/out calling + // other node (i.e., not doing a full slot rebuild) + Err(Err(RedisError::from(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "mock-io-error", + )))) + } else { + Err(Ok(Value::BulkString(b"123".to_vec()))) + } + } + }, + ); + + for _ in 0..4 { + let value = runtime.block_on( + cmd("GET") + .arg("test") + .query_async::<_, Option>(&mut connection), + ); + + assert_eq!(value, Ok(Some(123))); + } + // If you need to change the number here due to a change in the cluster, you probably also need to adjust the test. + // See the PING counts above to explain why 5 is the target number. + assert_eq!(ping_attempts.load(Ordering::Acquire), 5); + } + + #[cfg(feature = "tls-rustls")] + mod mtls_test { + use crate::support::mtls_test::create_cluster_client_from_cluster; + use redis::ConnectionInfo; + + use super::*; + + #[test] + fn test_async_cluster_basic_cmd_with_mtls() { + let cluster = TestClusterContext::new_with_mtls(3, 0); + block_on_all(async move { + let client = create_cluster_client_from_cluster(&cluster, true).unwrap(); + let mut connection = client.get_async_connection().await.unwrap(); + cmd("SET") + .arg("test") + .arg("test_data") + .query_async(&mut connection) + .await?; + let res: String = cmd("GET") + .arg("test") + .clone() + .query_async(&mut connection) + .await?; + assert_eq!(res, "test_data"); + Ok::<_, RedisError>(()) + }) + .unwrap(); + } + + #[test] + fn test_async_cluster_should_not_connect_without_mtls_enabled() { + let cluster = TestClusterContext::new_with_mtls(3, 0); + block_on_all(async move { let client = create_cluster_client_from_cluster(&cluster, false).unwrap(); let connection = client.get_async_connection().await; match cluster.cluster.servers.first().unwrap().connection_info() { @@ -1864,5 +1889,6 @@ mod mtls_test { } Ok::<_, RedisError>(()) }).unwrap(); + } } } diff --git a/redis/tests/test_types.rs b/redis/tests/test_types.rs index aeecc4938..f55560b66 100644 --- a/redis/tests/test_types.rs +++ b/redis/tests/test_types.rs @@ -1,595 +1,606 @@ -use redis::{FromRedisValue, ToRedisArgs, Value}; mod support; -#[test] -fn test_is_single_arg() { - let sslice: &[_] = &["foo"][..]; - let nestslice: &[_] = &[sslice][..]; - let nestvec = vec![nestslice]; - let bytes = b"Hello World!"; - let twobytesslice: &[_] = &[bytes, bytes][..]; - let twobytesvec = vec![bytes, bytes]; - - assert!("foo".is_single_arg()); - assert!(sslice.is_single_arg()); - assert!(nestslice.is_single_arg()); - assert!(nestvec.is_single_arg()); - assert!(bytes.is_single_arg()); - - assert!(!twobytesslice.is_single_arg()); - assert!(!twobytesvec.is_single_arg()); -} +#[cfg(test)] +mod types { + use redis::{FromRedisValue, ToRedisArgs, Value}; + #[test] + fn test_is_single_arg() { + let sslice: &[_] = &["foo"][..]; + let nestslice: &[_] = &[sslice][..]; + let nestvec = vec![nestslice]; + let bytes = b"Hello World!"; + let twobytesslice: &[_] = &[bytes, bytes][..]; + let twobytesvec = vec![bytes, bytes]; + + assert!("foo".is_single_arg()); + assert!(sslice.is_single_arg()); + assert!(nestslice.is_single_arg()); + assert!(nestvec.is_single_arg()); + assert!(bytes.is_single_arg()); + + assert!(!twobytesslice.is_single_arg()); + assert!(!twobytesvec.is_single_arg()); + } -/// The `FromRedisValue` trait provides two methods for parsing: -/// - `fn from_redis_value(&Value) -> Result` -/// - `fn from_owned_redis_value(Value) -> Result` -/// The `RedisParseMode` below allows choosing between the two -/// so that test logic does not need to be duplicated for each. -enum RedisParseMode { - Owned, - Ref, -} + /// The `FromRedisValue` trait provides two methods for parsing: + /// - `fn from_redis_value(&Value) -> Result` + /// - `fn from_owned_redis_value(Value) -> Result` + /// The `RedisParseMode` below allows choosing between the two + /// so that test logic does not need to be duplicated for each. + enum RedisParseMode { + Owned, + Ref, + } -impl RedisParseMode { - /// Calls either `FromRedisValue::from_owned_redis_value` or - /// `FromRedisValue::from_redis_value`. - fn parse_redis_value( - &self, - value: redis::Value, - ) -> Result { - match self { - Self::Owned => redis::FromRedisValue::from_owned_redis_value(value), - Self::Ref => redis::FromRedisValue::from_redis_value(&value), + impl RedisParseMode { + /// Calls either `FromRedisValue::from_owned_redis_value` or + /// `FromRedisValue::from_redis_value`. + fn parse_redis_value( + &self, + value: redis::Value, + ) -> Result { + match self { + Self::Owned => redis::FromRedisValue::from_owned_redis_value(value), + Self::Ref => redis::FromRedisValue::from_redis_value(&value), + } } } -} -#[test] -fn test_info_dict() { - use redis::{InfoDict, Value}; + #[test] + fn test_info_dict() { + use redis::{InfoDict, Value}; - for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { - let d: InfoDict = parse_mode - .parse_redis_value(Value::SimpleString( - "# this is a comment\nkey1:foo\nkey2:42\n".into(), - )) - .unwrap(); + for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { + let d: InfoDict = parse_mode + .parse_redis_value(Value::SimpleString( + "# this is a comment\nkey1:foo\nkey2:42\n".into(), + )) + .unwrap(); - assert_eq!(d.get("key1"), Some("foo".to_string())); - assert_eq!(d.get("key2"), Some(42i64)); - assert_eq!(d.get::("key3"), None); + assert_eq!(d.get("key1"), Some("foo".to_string())); + assert_eq!(d.get("key2"), Some(42i64)); + assert_eq!(d.get::("key3"), None); + } } -} -#[test] -fn test_i32() { - use redis::{ErrorKind, Value}; + #[test] + fn test_i32() { + use redis::{ErrorKind, Value}; - for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { - let i = parse_mode.parse_redis_value(Value::SimpleString("42".into())); - assert_eq!(i, Ok(42i32)); + for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { + let i = parse_mode.parse_redis_value(Value::SimpleString("42".into())); + assert_eq!(i, Ok(42i32)); - let i = parse_mode.parse_redis_value(Value::Int(42)); - assert_eq!(i, Ok(42i32)); + let i = parse_mode.parse_redis_value(Value::Int(42)); + assert_eq!(i, Ok(42i32)); - let i = parse_mode.parse_redis_value(Value::BulkString("42".into())); - assert_eq!(i, Ok(42i32)); + let i = parse_mode.parse_redis_value(Value::BulkString("42".into())); + assert_eq!(i, Ok(42i32)); - let bad_i: Result = parse_mode.parse_redis_value(Value::SimpleString("42x".into())); - assert_eq!(bad_i.unwrap_err().kind(), ErrorKind::TypeError); + let bad_i: Result = + parse_mode.parse_redis_value(Value::SimpleString("42x".into())); + assert_eq!(bad_i.unwrap_err().kind(), ErrorKind::TypeError); + } } -} -#[test] -fn test_u32() { - use redis::{ErrorKind, Value}; + #[test] + fn test_u32() { + use redis::{ErrorKind, Value}; - for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { - let i = parse_mode.parse_redis_value(Value::SimpleString("42".into())); - assert_eq!(i, Ok(42u32)); + for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { + let i = parse_mode.parse_redis_value(Value::SimpleString("42".into())); + assert_eq!(i, Ok(42u32)); - let bad_i: Result = parse_mode.parse_redis_value(Value::SimpleString("-1".into())); - assert_eq!(bad_i.unwrap_err().kind(), ErrorKind::TypeError); + let bad_i: Result = + parse_mode.parse_redis_value(Value::SimpleString("-1".into())); + assert_eq!(bad_i.unwrap_err().kind(), ErrorKind::TypeError); + } } -} -#[test] -fn test_vec() { - use redis::Value; - - for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { - let v = parse_mode.parse_redis_value(Value::Array(vec![ - Value::BulkString("1".into()), - Value::BulkString("2".into()), - Value::BulkString("3".into()), - ])); - assert_eq!(v, Ok(vec![1i32, 2, 3])); - - let content: &[u8] = b"\x01\x02\x03\x04"; - let content_vec: Vec = Vec::from(content); - let v = parse_mode.parse_redis_value(Value::BulkString(content_vec.clone())); - assert_eq!(v, Ok(content_vec)); - - let content: &[u8] = b"1"; - let content_vec: Vec = Vec::from(content); - let v = parse_mode.parse_redis_value(Value::BulkString(content_vec.clone())); - assert_eq!(v, Ok(vec![b'1'])); - let v = parse_mode.parse_redis_value(Value::BulkString(content_vec)); - assert_eq!(v, Ok(vec![1_u16])); + #[test] + fn test_vec() { + use redis::Value; + + for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { + let v = parse_mode.parse_redis_value(Value::Array(vec![ + Value::BulkString("1".into()), + Value::BulkString("2".into()), + Value::BulkString("3".into()), + ])); + assert_eq!(v, Ok(vec![1i32, 2, 3])); + + let content: &[u8] = b"\x01\x02\x03\x04"; + let content_vec: Vec = Vec::from(content); + let v = parse_mode.parse_redis_value(Value::BulkString(content_vec.clone())); + assert_eq!(v, Ok(content_vec)); + + let content: &[u8] = b"1"; + let content_vec: Vec = Vec::from(content); + let v = parse_mode.parse_redis_value(Value::BulkString(content_vec.clone())); + assert_eq!(v, Ok(vec![b'1'])); + let v = parse_mode.parse_redis_value(Value::BulkString(content_vec)); + assert_eq!(v, Ok(vec![1_u16])); + } } -} -#[test] -fn test_box_slice() { - use redis::{FromRedisValue, Value}; - for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { - let v = parse_mode.parse_redis_value(Value::Array(vec![ - Value::BulkString("1".into()), - Value::BulkString("2".into()), - Value::BulkString("3".into()), - ])); - assert_eq!(v, Ok(vec![1i32, 2, 3].into_boxed_slice())); - - let content: &[u8] = b"\x01\x02\x03\x04"; - let content_vec: Vec = Vec::from(content); - let v = parse_mode.parse_redis_value(Value::BulkString(content_vec.clone())); - assert_eq!(v, Ok(content_vec.into_boxed_slice())); - - let content: &[u8] = b"1"; - let content_vec: Vec = Vec::from(content); - let v = parse_mode.parse_redis_value(Value::BulkString(content_vec.clone())); - assert_eq!(v, Ok(vec![b'1'].into_boxed_slice())); - let v = parse_mode.parse_redis_value(Value::BulkString(content_vec)); - assert_eq!(v, Ok(vec![1_u16].into_boxed_slice())); - - assert_eq!( + #[test] + fn test_box_slice() { + use redis::{FromRedisValue, Value}; + for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { + let v = parse_mode.parse_redis_value(Value::Array(vec![ + Value::BulkString("1".into()), + Value::BulkString("2".into()), + Value::BulkString("3".into()), + ])); + assert_eq!(v, Ok(vec![1i32, 2, 3].into_boxed_slice())); + + let content: &[u8] = b"\x01\x02\x03\x04"; + let content_vec: Vec = Vec::from(content); + let v = parse_mode.parse_redis_value(Value::BulkString(content_vec.clone())); + assert_eq!(v, Ok(content_vec.into_boxed_slice())); + + let content: &[u8] = b"1"; + let content_vec: Vec = Vec::from(content); + let v = parse_mode.parse_redis_value(Value::BulkString(content_vec.clone())); + assert_eq!(v, Ok(vec![b'1'].into_boxed_slice())); + let v = parse_mode.parse_redis_value(Value::BulkString(content_vec)); + assert_eq!(v, Ok(vec![1_u16].into_boxed_slice())); + + assert_eq!( Box::<[i32]>::from_redis_value( &Value::BulkString("just a string".into()) ).unwrap_err().to_string(), "Response was of incompatible type - TypeError: \"Conversion to alloc::boxed::Box<[i32]> failed.\" (response was bulk-string('\"just a string\"'))", ); + } } -} -#[test] -fn test_arc_slice() { - use redis::{FromRedisValue, Value}; - use std::sync::Arc; - - for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { - let v = parse_mode.parse_redis_value(Value::Array(vec![ - Value::BulkString("1".into()), - Value::BulkString("2".into()), - Value::BulkString("3".into()), - ])); - assert_eq!(v, Ok(Arc::from(vec![1i32, 2, 3]))); - - let content: &[u8] = b"\x01\x02\x03\x04"; - let content_vec: Vec = Vec::from(content); - let v = parse_mode.parse_redis_value(Value::BulkString(content_vec.clone())); - assert_eq!(v, Ok(Arc::from(content_vec))); - - let content: &[u8] = b"1"; - let content_vec: Vec = Vec::from(content); - let v = parse_mode.parse_redis_value(Value::BulkString(content_vec.clone())); - assert_eq!(v, Ok(Arc::from(vec![b'1']))); - let v = parse_mode.parse_redis_value(Value::BulkString(content_vec)); - assert_eq!(v, Ok(Arc::from(vec![1_u16]))); - - assert_eq!( + #[test] + fn test_arc_slice() { + use redis::{FromRedisValue, Value}; + use std::sync::Arc; + + for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { + let v = parse_mode.parse_redis_value(Value::Array(vec![ + Value::BulkString("1".into()), + Value::BulkString("2".into()), + Value::BulkString("3".into()), + ])); + assert_eq!(v, Ok(Arc::from(vec![1i32, 2, 3]))); + + let content: &[u8] = b"\x01\x02\x03\x04"; + let content_vec: Vec = Vec::from(content); + let v = parse_mode.parse_redis_value(Value::BulkString(content_vec.clone())); + assert_eq!(v, Ok(Arc::from(content_vec))); + + let content: &[u8] = b"1"; + let content_vec: Vec = Vec::from(content); + let v = parse_mode.parse_redis_value(Value::BulkString(content_vec.clone())); + assert_eq!(v, Ok(Arc::from(vec![b'1']))); + let v = parse_mode.parse_redis_value(Value::BulkString(content_vec)); + assert_eq!(v, Ok(Arc::from(vec![1_u16]))); + + assert_eq!( Arc::<[i32]>::from_redis_value( &Value::BulkString("just a string".into()) ).unwrap_err().to_string(), "Response was of incompatible type - TypeError: \"Conversion to alloc::sync::Arc<[i32]> failed.\" (response was bulk-string('\"just a string\"'))", ); + } } -} -#[test] -fn test_single_bool_vec() { - use redis::Value; + #[test] + fn test_single_bool_vec() { + use redis::Value; - for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { - let v = parse_mode.parse_redis_value(Value::BulkString("1".into())); + for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { + let v = parse_mode.parse_redis_value(Value::BulkString("1".into())); - assert_eq!(v, Ok(vec![true])); + assert_eq!(v, Ok(vec![true])); + } } -} -#[test] -fn test_single_i32_vec() { - use redis::Value; + #[test] + fn test_single_i32_vec() { + use redis::Value; - for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { - let v = parse_mode.parse_redis_value(Value::BulkString("1".into())); + for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { + let v = parse_mode.parse_redis_value(Value::BulkString("1".into())); - assert_eq!(v, Ok(vec![1i32])); + assert_eq!(v, Ok(vec![1i32])); + } } -} -#[test] -fn test_single_u32_vec() { - use redis::Value; + #[test] + fn test_single_u32_vec() { + use redis::Value; - for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { - let v = parse_mode.parse_redis_value(Value::BulkString("42".into())); + for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { + let v = parse_mode.parse_redis_value(Value::BulkString("42".into())); - assert_eq!(v, Ok(vec![42u32])); + assert_eq!(v, Ok(vec![42u32])); + } } -} -#[test] -fn test_single_string_vec() { - use redis::Value; + #[test] + fn test_single_string_vec() { + use redis::Value; - for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { - let v = parse_mode.parse_redis_value(Value::BulkString("1".into())); - assert_eq!(v, Ok(vec!["1".to_string()])); + for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { + let v = parse_mode.parse_redis_value(Value::BulkString("1".into())); + assert_eq!(v, Ok(vec!["1".to_string()])); + } } -} -#[test] -fn test_tuple() { - use redis::Value; + #[test] + fn test_tuple() { + use redis::Value; - for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { - let v = parse_mode.parse_redis_value(Value::Array(vec![Value::Array(vec![ - Value::BulkString("1".into()), - Value::BulkString("2".into()), - Value::BulkString("3".into()), - ])])); + for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { + let v = parse_mode.parse_redis_value(Value::Array(vec![Value::Array(vec![ + Value::BulkString("1".into()), + Value::BulkString("2".into()), + Value::BulkString("3".into()), + ])])); - assert_eq!(v, Ok(((1i32, 2, 3,),))); + assert_eq!(v, Ok(((1i32, 2, 3,),))); + } } -} -#[test] -fn test_hashmap() { - use fnv::FnvHasher; - use redis::{ErrorKind, Value}; - use std::collections::HashMap; - use std::hash::BuildHasherDefault; - - type Hm = HashMap; - - for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { - let v: Result = parse_mode.parse_redis_value(Value::Array(vec![ - Value::BulkString("a".into()), - Value::BulkString("1".into()), - Value::BulkString("b".into()), - Value::BulkString("2".into()), - Value::BulkString("c".into()), - Value::BulkString("3".into()), - ])); - let mut e: Hm = HashMap::new(); - e.insert("a".into(), 1); - e.insert("b".into(), 2); - e.insert("c".into(), 3); - assert_eq!(v, Ok(e)); - - type Hasher = BuildHasherDefault; - type HmHasher = HashMap; - let v: Result = parse_mode.parse_redis_value(Value::Array(vec![ - Value::BulkString("a".into()), - Value::BulkString("1".into()), - Value::BulkString("b".into()), - Value::BulkString("2".into()), - Value::BulkString("c".into()), - Value::BulkString("3".into()), - ])); - - let fnv = Hasher::default(); - let mut e: HmHasher = HashMap::with_hasher(fnv); - e.insert("a".into(), 1); - e.insert("b".into(), 2); - e.insert("c".into(), 3); - assert_eq!(v, Ok(e)); - - let v: Result = - parse_mode.parse_redis_value(Value::Array(vec![Value::BulkString("a".into())])); - assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + #[test] + fn test_hashmap() { + use fnv::FnvHasher; + use redis::{ErrorKind, Value}; + use std::collections::HashMap; + use std::hash::BuildHasherDefault; + + type Hm = HashMap; + + for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { + let v: Result = parse_mode.parse_redis_value(Value::Array(vec![ + Value::BulkString("a".into()), + Value::BulkString("1".into()), + Value::BulkString("b".into()), + Value::BulkString("2".into()), + Value::BulkString("c".into()), + Value::BulkString("3".into()), + ])); + let mut e: Hm = HashMap::new(); + e.insert("a".into(), 1); + e.insert("b".into(), 2); + e.insert("c".into(), 3); + assert_eq!(v, Ok(e)); + + type Hasher = BuildHasherDefault; + type HmHasher = HashMap; + let v: Result = parse_mode.parse_redis_value(Value::Array(vec![ + Value::BulkString("a".into()), + Value::BulkString("1".into()), + Value::BulkString("b".into()), + Value::BulkString("2".into()), + Value::BulkString("c".into()), + Value::BulkString("3".into()), + ])); + + let fnv = Hasher::default(); + let mut e: HmHasher = HashMap::with_hasher(fnv); + e.insert("a".into(), 1); + e.insert("b".into(), 2); + e.insert("c".into(), 3); + assert_eq!(v, Ok(e)); + + let v: Result = + parse_mode.parse_redis_value(Value::Array(vec![Value::BulkString("a".into())])); + assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + } } -} -#[test] -fn test_bool() { - use redis::{ErrorKind, Value}; + #[test] + fn test_bool() { + use redis::{ErrorKind, Value}; - for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { - let v = parse_mode.parse_redis_value(Value::BulkString("1".into())); - assert_eq!(v, Ok(true)); + for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { + let v = parse_mode.parse_redis_value(Value::BulkString("1".into())); + assert_eq!(v, Ok(true)); - let v = parse_mode.parse_redis_value(Value::BulkString("0".into())); - assert_eq!(v, Ok(false)); + let v = parse_mode.parse_redis_value(Value::BulkString("0".into())); + assert_eq!(v, Ok(false)); - let v: Result = parse_mode.parse_redis_value(Value::BulkString("garbage".into())); - assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + let v: Result = + parse_mode.parse_redis_value(Value::BulkString("garbage".into())); + assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); - let v = parse_mode.parse_redis_value(Value::SimpleString("1".into())); - assert_eq!(v, Ok(true)); + let v = parse_mode.parse_redis_value(Value::SimpleString("1".into())); + assert_eq!(v, Ok(true)); - let v = parse_mode.parse_redis_value(Value::SimpleString("0".into())); - assert_eq!(v, Ok(false)); + let v = parse_mode.parse_redis_value(Value::SimpleString("0".into())); + assert_eq!(v, Ok(false)); - let v: Result = - parse_mode.parse_redis_value(Value::SimpleString("garbage".into())); - assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + let v: Result = + parse_mode.parse_redis_value(Value::SimpleString("garbage".into())); + assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); - let v = parse_mode.parse_redis_value(Value::Okay); - assert_eq!(v, Ok(true)); + let v = parse_mode.parse_redis_value(Value::Okay); + assert_eq!(v, Ok(true)); - let v = parse_mode.parse_redis_value(Value::Nil); - assert_eq!(v, Ok(false)); + let v = parse_mode.parse_redis_value(Value::Nil); + assert_eq!(v, Ok(false)); - let v = parse_mode.parse_redis_value(Value::Int(0)); - assert_eq!(v, Ok(false)); + let v = parse_mode.parse_redis_value(Value::Int(0)); + assert_eq!(v, Ok(false)); - let v = parse_mode.parse_redis_value(Value::Int(42)); - assert_eq!(v, Ok(true)); + let v = parse_mode.parse_redis_value(Value::Int(42)); + assert_eq!(v, Ok(true)); + } } -} -#[cfg(feature = "bytes")] -#[test] -fn test_bytes() { - use bytes::Bytes; - use redis::{ErrorKind, RedisResult, Value}; + #[cfg(feature = "bytes")] + #[test] + fn test_bytes() { + use bytes::Bytes; + use redis::{ErrorKind, RedisResult, Value}; - for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { - let content: &[u8] = b"\x01\x02\x03\x04"; - let content_vec: Vec = Vec::from(content); - let content_bytes = Bytes::from_static(content); + for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { + let content: &[u8] = b"\x01\x02\x03\x04"; + let content_vec: Vec = Vec::from(content); + let content_bytes = Bytes::from_static(content); - let v: RedisResult = parse_mode.parse_redis_value(Value::BulkString(content_vec)); - assert_eq!(v, Ok(content_bytes)); + let v: RedisResult = + parse_mode.parse_redis_value(Value::BulkString(content_vec)); + assert_eq!(v, Ok(content_bytes)); - let v: RedisResult = - parse_mode.parse_redis_value(Value::SimpleString("garbage".into())); - assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + let v: RedisResult = + parse_mode.parse_redis_value(Value::SimpleString("garbage".into())); + assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); - let v: RedisResult = parse_mode.parse_redis_value(Value::Okay); - assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + let v: RedisResult = parse_mode.parse_redis_value(Value::Okay); + assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); - let v: RedisResult = parse_mode.parse_redis_value(Value::Nil); - assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + let v: RedisResult = parse_mode.parse_redis_value(Value::Nil); + assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); - let v: RedisResult = parse_mode.parse_redis_value(Value::Int(0)); - assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + let v: RedisResult = parse_mode.parse_redis_value(Value::Int(0)); + assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); - let v: RedisResult = parse_mode.parse_redis_value(Value::Int(42)); - assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + let v: RedisResult = parse_mode.parse_redis_value(Value::Int(42)); + assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + } } -} -#[cfg(feature = "uuid")] -#[test] -fn test_uuid() { - use std::str::FromStr; + #[cfg(feature = "uuid")] + #[test] + fn test_uuid() { + use std::str::FromStr; - use redis::{ErrorKind, FromRedisValue, RedisResult, Value}; - use uuid::Uuid; + use redis::{ErrorKind, FromRedisValue, RedisResult, Value}; + use uuid::Uuid; - let uuid = Uuid::from_str("abab64b7-e265-4052-a41b-23e1e28674bf").unwrap(); - let bytes = uuid.as_bytes().to_vec(); + let uuid = Uuid::from_str("abab64b7-e265-4052-a41b-23e1e28674bf").unwrap(); + let bytes = uuid.as_bytes().to_vec(); - let v: RedisResult = FromRedisValue::from_redis_value(&Value::BulkString(bytes)); - assert_eq!(v, Ok(uuid)); + let v: RedisResult = FromRedisValue::from_redis_value(&Value::BulkString(bytes)); + assert_eq!(v, Ok(uuid)); - let v: RedisResult = - FromRedisValue::from_redis_value(&Value::SimpleString("garbage".into())); - assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + let v: RedisResult = + FromRedisValue::from_redis_value(&Value::SimpleString("garbage".into())); + assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); - let v: RedisResult = FromRedisValue::from_redis_value(&Value::Okay); - assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + let v: RedisResult = FromRedisValue::from_redis_value(&Value::Okay); + assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); - let v: RedisResult = FromRedisValue::from_redis_value(&Value::Nil); - assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + let v: RedisResult = FromRedisValue::from_redis_value(&Value::Nil); + assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); - let v: RedisResult = FromRedisValue::from_redis_value(&Value::Int(0)); - assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + let v: RedisResult = FromRedisValue::from_redis_value(&Value::Int(0)); + assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); - let v: RedisResult = FromRedisValue::from_redis_value(&Value::Int(42)); - assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); -} + let v: RedisResult = FromRedisValue::from_redis_value(&Value::Int(42)); + assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + } -#[test] -fn test_cstring() { - use redis::{ErrorKind, RedisResult, Value}; - use std::ffi::CString; + #[test] + fn test_cstring() { + use redis::{ErrorKind, RedisResult, Value}; + use std::ffi::CString; - for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { - let content: &[u8] = b"\x01\x02\x03\x04"; - let content_vec: Vec = Vec::from(content); + for parse_mode in [RedisParseMode::Owned, RedisParseMode::Ref] { + let content: &[u8] = b"\x01\x02\x03\x04"; + let content_vec: Vec = Vec::from(content); - let v: RedisResult = parse_mode.parse_redis_value(Value::BulkString(content_vec)); - assert_eq!(v, Ok(CString::new(content).unwrap())); + let v: RedisResult = + parse_mode.parse_redis_value(Value::BulkString(content_vec)); + assert_eq!(v, Ok(CString::new(content).unwrap())); - let v: RedisResult = - parse_mode.parse_redis_value(Value::SimpleString("garbage".into())); - assert_eq!(v, Ok(CString::new("garbage").unwrap())); + let v: RedisResult = + parse_mode.parse_redis_value(Value::SimpleString("garbage".into())); + assert_eq!(v, Ok(CString::new("garbage").unwrap())); - let v: RedisResult = parse_mode.parse_redis_value(Value::Okay); - assert_eq!(v, Ok(CString::new("OK").unwrap())); + let v: RedisResult = parse_mode.parse_redis_value(Value::Okay); + assert_eq!(v, Ok(CString::new("OK").unwrap())); - let v: RedisResult = - parse_mode.parse_redis_value(Value::SimpleString("gar\0bage".into())); - assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + let v: RedisResult = + parse_mode.parse_redis_value(Value::SimpleString("gar\0bage".into())); + assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); - let v: RedisResult = parse_mode.parse_redis_value(Value::Nil); - assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + let v: RedisResult = parse_mode.parse_redis_value(Value::Nil); + assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); - let v: RedisResult = parse_mode.parse_redis_value(Value::Int(0)); - assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + let v: RedisResult = parse_mode.parse_redis_value(Value::Int(0)); + assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); - let v: RedisResult = parse_mode.parse_redis_value(Value::Int(42)); - assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + let v: RedisResult = parse_mode.parse_redis_value(Value::Int(42)); + assert_eq!(v.unwrap_err().kind(), ErrorKind::TypeError); + } } -} -#[test] -fn test_types_to_redis_args() { - use redis::ToRedisArgs; - use std::collections::BTreeMap; - use std::collections::BTreeSet; - use std::collections::HashMap; - use std::collections::HashSet; - - assert!(!5i32.to_redis_args().is_empty()); - assert!(!"abc".to_redis_args().is_empty()); - assert!(!"abc".to_redis_args().is_empty()); - assert!(!String::from("x").to_redis_args().is_empty()); - - assert!(![5, 4] - .iter() - .cloned() - .collect::>() - .to_redis_args() - .is_empty()); - - assert!(![5, 4] - .iter() - .cloned() - .collect::>() - .to_redis_args() - .is_empty()); - - // this can be used on something HMSET - assert!(![("a", 5), ("b", 6), ("C", 7)] - .iter() - .cloned() - .collect::>() - .to_redis_args() - .is_empty()); - - // this can also be used on something HMSET - assert!(![("d", 8), ("e", 9), ("f", 10)] - .iter() - .cloned() - .collect::>() - .to_redis_args() - .is_empty()); -} - -#[test] -fn test_large_usize_array_to_redis_args_and_back() { - use crate::support::encode_value; - use redis::ToRedisArgs; - - let mut array = [0; 1000]; - for (i, item) in array.iter_mut().enumerate() { - *item = i; + #[test] + fn test_types_to_redis_args() { + use redis::ToRedisArgs; + use std::collections::BTreeMap; + use std::collections::BTreeSet; + use std::collections::HashMap; + use std::collections::HashSet; + + assert!(!5i32.to_redis_args().is_empty()); + assert!(!"abc".to_redis_args().is_empty()); + assert!(!"abc".to_redis_args().is_empty()); + assert!(!String::from("x").to_redis_args().is_empty()); + + assert!(![5, 4] + .iter() + .cloned() + .collect::>() + .to_redis_args() + .is_empty()); + + assert!(![5, 4] + .iter() + .cloned() + .collect::>() + .to_redis_args() + .is_empty()); + + // this can be used on something HMSET + assert!(![("a", 5), ("b", 6), ("C", 7)] + .iter() + .cloned() + .collect::>() + .to_redis_args() + .is_empty()); + + // this can also be used on something HMSET + assert!(![("d", 8), ("e", 9), ("f", 10)] + .iter() + .cloned() + .collect::>() + .to_redis_args() + .is_empty()); } - let vec = (&array).to_redis_args(); - assert_eq!(array.len(), vec.len()); + #[test] + fn test_large_usize_array_to_redis_args_and_back() { + use crate::support::encode_value; + use redis::ToRedisArgs; - let value = Value::Array( - vec.iter() - .map(|val| Value::BulkString(val.clone())) - .collect(), - ); - let mut encoded_input = Vec::new(); - encode_value(&value, &mut encoded_input).unwrap(); + let mut array = [0; 1000]; + for (i, item) in array.iter_mut().enumerate() { + *item = i; + } - let new_array: [usize; 1000] = FromRedisValue::from_redis_value(&value).unwrap(); - assert_eq!(new_array, array); -} + let vec = (&array).to_redis_args(); + assert_eq!(array.len(), vec.len()); -#[test] -fn test_large_u8_array_to_redis_args_and_back() { - use crate::support::encode_value; - use redis::ToRedisArgs; + let value = Value::Array( + vec.iter() + .map(|val| Value::BulkString(val.clone())) + .collect(), + ); + let mut encoded_input = Vec::new(); + encode_value(&value, &mut encoded_input).unwrap(); - let mut array: [u8; 1000] = [0; 1000]; - for (i, item) in array.iter_mut().enumerate() { - *item = (i % 256) as u8; + let new_array: [usize; 1000] = FromRedisValue::from_redis_value(&value).unwrap(); + assert_eq!(new_array, array); } - let vec = (&array).to_redis_args(); - assert_eq!(vec.len(), 1); - assert_eq!(array.len(), vec[0].len()); + #[test] + fn test_large_u8_array_to_redis_args_and_back() { + use crate::support::encode_value; + use redis::ToRedisArgs; - let value = Value::Array(vec[0].iter().map(|val| Value::Int(*val as i64)).collect()); - let mut encoded_input = Vec::new(); - encode_value(&value, &mut encoded_input).unwrap(); + let mut array: [u8; 1000] = [0; 1000]; + for (i, item) in array.iter_mut().enumerate() { + *item = (i % 256) as u8; + } - let new_array: [u8; 1000] = FromRedisValue::from_redis_value(&value).unwrap(); - assert_eq!(new_array, array); -} + let vec = (&array).to_redis_args(); + assert_eq!(vec.len(), 1); + assert_eq!(array.len(), vec[0].len()); -#[test] -fn test_large_string_array_to_redis_args_and_back() { - use crate::support::encode_value; - use redis::ToRedisArgs; + let value = Value::Array(vec[0].iter().map(|val| Value::Int(*val as i64)).collect()); + let mut encoded_input = Vec::new(); + encode_value(&value, &mut encoded_input).unwrap(); - let mut array: [String; 1000] = [(); 1000].map(|_| String::new()); - for (i, item) in array.iter_mut().enumerate() { - *item = format!("{i}"); + let new_array: [u8; 1000] = FromRedisValue::from_redis_value(&value).unwrap(); + assert_eq!(new_array, array); } - let vec = (&array).to_redis_args(); - assert_eq!(array.len(), vec.len()); + #[test] + fn test_large_string_array_to_redis_args_and_back() { + use crate::support::encode_value; + use redis::ToRedisArgs; - let value = Value::Array( - vec.iter() - .map(|val| Value::BulkString(val.clone())) - .collect(), - ); - let mut encoded_input = Vec::new(); - encode_value(&value, &mut encoded_input).unwrap(); + let mut array: [String; 1000] = [(); 1000].map(|_| String::new()); + for (i, item) in array.iter_mut().enumerate() { + *item = format!("{i}"); + } - let new_array: [String; 1000] = FromRedisValue::from_redis_value(&value).unwrap(); - assert_eq!(new_array, array); -} + let vec = (&array).to_redis_args(); + assert_eq!(array.len(), vec.len()); -#[test] -fn test_0_length_usize_array_to_redis_args_and_back() { - use crate::support::encode_value; - use redis::ToRedisArgs; + let value = Value::Array( + vec.iter() + .map(|val| Value::BulkString(val.clone())) + .collect(), + ); + let mut encoded_input = Vec::new(); + encode_value(&value, &mut encoded_input).unwrap(); - let array: [usize; 0] = [0; 0]; + let new_array: [String; 1000] = FromRedisValue::from_redis_value(&value).unwrap(); + assert_eq!(new_array, array); + } - let vec = (&array).to_redis_args(); - assert_eq!(array.len(), vec.len()); + #[test] + fn test_0_length_usize_array_to_redis_args_and_back() { + use crate::support::encode_value; + use redis::ToRedisArgs; - let value = Value::Array( - vec.iter() - .map(|val| Value::BulkString(val.clone())) - .collect(), - ); - let mut encoded_input = Vec::new(); - encode_value(&value, &mut encoded_input).unwrap(); + let array: [usize; 0] = [0; 0]; - let new_array: [usize; 0] = FromRedisValue::from_redis_value(&value).unwrap(); - assert_eq!(new_array, array); + let vec = (&array).to_redis_args(); + assert_eq!(array.len(), vec.len()); - let new_array: [usize; 0] = FromRedisValue::from_redis_value(&Value::Nil).unwrap(); - assert_eq!(new_array, array); -} + let value = Value::Array( + vec.iter() + .map(|val| Value::BulkString(val.clone())) + .collect(), + ); + let mut encoded_input = Vec::new(); + encode_value(&value, &mut encoded_input).unwrap(); -#[test] -fn test_attributes() { - use redis::{parse_redis_value, FromRedisValue, Value}; - let bytes: &[u8] = b"*3\r\n:1\r\n:2\r\n|1\r\n+ttl\r\n:3600\r\n:3\r\n"; - let val = parse_redis_value(bytes).unwrap(); - { - // The case user doesn't expect attributes from server - let x: Vec = redis::FromRedisValue::from_redis_value(&val).unwrap(); - assert_eq!(x, vec![1, 2, 3]); + let new_array: [usize; 0] = FromRedisValue::from_redis_value(&value).unwrap(); + assert_eq!(new_array, array); + + let new_array: [usize; 0] = FromRedisValue::from_redis_value(&Value::Nil).unwrap(); + assert_eq!(new_array, array); } - { - // The case user wants raw value from server - let x: Value = FromRedisValue::from_redis_value(&val).unwrap(); - assert_eq!( - x, - Value::Array(vec![ - Value::Int(1), - Value::Int(2), - Value::Attribute { - data: Box::new(Value::Int(3)), - attributes: vec![(Value::SimpleString("ttl".to_string()), Value::Int(3600))] - } - ]) - ) + + #[test] + fn test_attributes() { + use redis::{parse_redis_value, FromRedisValue, Value}; + let bytes: &[u8] = b"*3\r\n:1\r\n:2\r\n|1\r\n+ttl\r\n:3600\r\n:3\r\n"; + let val = parse_redis_value(bytes).unwrap(); + { + // The case user doesn't expect attributes from server + let x: Vec = redis::FromRedisValue::from_redis_value(&val).unwrap(); + assert_eq!(x, vec![1, 2, 3]); + } + { + // The case user wants raw value from server + let x: Value = FromRedisValue::from_redis_value(&val).unwrap(); + assert_eq!( + x, + Value::Array(vec![ + Value::Int(1), + Value::Int(2), + Value::Attribute { + data: Box::new(Value::Int(3)), + attributes: vec![( + Value::SimpleString("ttl".to_string()), + Value::Int(3600) + )] + } + ]) + ) + } } }