use crate::{
ClientError, Error, ErrorKind, RedisError, RedisErrorKind, Result,
client::BatchPreparedCommand,
commands::{
ClientKillOptions, ConnectionCommands, GenericCommands, ListCommands, StringCommands,
},
resp::cmd,
tests::{get_default_config, get_test_client, get_test_client_with_config},
};
use bytes::Bytes;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn unknown_command() -> Result<()> {
let client = get_test_client().await?;
let result = client.send::<()>(cmd("UNKNOWN").arg("arg"), None).await;
assert!(matches!(
result.unwrap_err().kind(),
ErrorKind::Redis(RedisError {
kind: RedisErrorKind::Err,
description
}) if description.starts_with("unknown command 'UNKNOWN'")
));
Ok(())
}
#[test]
fn moved_error() {
let raw_error = b"MOVED 3999 127.0.0.1:6381";
let error = RedisError::try_from(&raw_error[..]);
println!("error: {error:?}");
assert!(matches!(
error,
Ok(RedisError {
kind: RedisErrorKind::Moved { hash_slot: 3999, address: (host, 6381) },
description
}) if description.is_empty() && host == "127.0.0.1"
));
}
#[test]
fn ask_error() {
let raw_error = b"ASK 3999 127.0.0.1:6381";
let error = RedisError::try_from(&raw_error[..]);
assert!(matches!(
error,
Ok(RedisError {
kind: RedisErrorKind::Ask { hash_slot: 3999, address: (host, 6381) },
description
}) if description.is_empty() && host == "127.0.0.1"
));
}
#[test]
fn moved_error_ipv6() {
let raw_error = b"MOVED 3999 2001:db8::1:6380";
let error = RedisError::try_from(&raw_error[..]);
println!("error: {error:?}");
assert!(matches!(
error,
Ok(RedisError {
kind: RedisErrorKind::Moved { hash_slot: 3999, address: (host, 6380) },
description
}) if description.is_empty() && host == "2001:db8::1"
));
}
#[test]
fn an_error_carries_no_command_until_one_is_attached() {
let error = Error::from(ErrorKind::Timeout);
assert!(matches!(error.kind(), ErrorKind::Timeout));
assert_eq!(None, error.command());
assert!(error.context().is_none());
assert_eq!(ErrorKind::Timeout.to_string(), error.to_string());
}
#[test]
fn attaching_a_command_names_it_in_the_context_and_the_message() {
let error = Error::from(ErrorKind::Timeout).with_command(Bytes::from_static(b"BLMPOP"));
assert_eq!(Some("BLMPOP"), error.command());
assert_eq!("BLMPOP", error.context().unwrap().command());
assert!(
error.to_string().contains("BLMPOP"),
"the rendered message must name the command, got {error}"
);
assert!(matches!(error.kind(), ErrorKind::Timeout));
}
#[test]
fn the_innermost_command_wins() {
let error = Error::from(ErrorKind::Timeout)
.with_command(Bytes::from_static(b"GET"))
.with_command(Bytes::from_static(b"SET"));
assert_eq!(Some("GET"), error.command());
}
#[tokio::test]
#[serial]
async fn reconnection() -> Result<()> {
let mut config = get_default_config()?;
config.connection_name = "regular".to_string();
let regular_client = get_test_client_with_config(config).await?;
let mut config = get_default_config()?;
config.connection_name = "killer".to_string();
let killer_client = get_test_client_with_config(config).await?;
let client_id = regular_client.client_id().await?;
killer_client
.client_kill(ClientKillOptions::default().id(client_id))
.await?;
let result = regular_client.set("key", "value").await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
#[serial]
async fn kill_on_write() -> Result<()> {
use crate::client::ReconnectionConfig;
let mut config = get_default_config()?;
config.reconnection = ReconnectionConfig::new_constant(0, 100);
let client = get_test_client_with_config(config).await?;
let result = client
.send::<()>(
cmd("SET")
.arg("key1")
.arg("value1")
.kill_connection_on_write(3),
Some(true),
)
.await;
assert!(result.is_ok());
let result = client
.send::<()>(
cmd("SET")
.arg("key2")
.arg("value2")
.kill_connection_on_write(2),
Some(true),
)
.await;
assert!(result.is_ok());
let result = client
.send::<()>(
cmd("SET")
.arg("key3")
.arg("value3")
.kill_connection_on_write(2),
Some(false),
)
.await;
assert!(result.is_err());
Ok(())
}
#[test]
fn the_error_type_keeps_its_bounds() {
const fn assert_bounds<T: std::error::Error + Send + Sync + Clone + 'static>() {}
assert_bounds::<Error>();
let boxed: Box<dyn std::error::Error + Send + Sync> =
Box::new(Error::from(ErrorKind::Timeout).with_command(Bytes::from_static(b"GET")));
assert!(boxed.to_string().contains("GET"));
}
#[tokio::test]
#[serial]
async fn a_server_error_names_the_command_that_drew_it() -> Result<()> {
let client = get_test_client().await?;
client.del("a_list_key").await?;
client.lpush("a_list_key", "value").await?;
let result: Result<String> = client.get("a_list_key").await;
let error = result.expect_err("GET on a list must be refused by the server");
assert!(
matches!(error.kind(), ErrorKind::Redis(e) if e.kind == RedisErrorKind::WrongType),
"expected WRONGTYPE, got {error:?}"
);
assert_eq!(Some("GET"), error.command());
Ok(())
}
fn redis(kind: RedisErrorKind) -> Error {
Error::from(ErrorKind::Redis(RedisError {
kind,
description: String::new(),
}))
}
fn client(client_error: ClientError) -> Error {
Error::from(ErrorKind::Client(client_error))
}
fn io() -> Error {
Error::from(ErrorKind::IO(std::sync::Arc::new(std::io::Error::new(
std::io::ErrorKind::ConnectionReset,
"reset",
))))
}
#[test]
fn a_connection_error_is_told_from_a_command_error() {
assert!(io().is_connection_error());
assert!(Error::from(ErrorKind::EOF).is_connection_error());
assert!(Error::from(ErrorKind::DisconnectedByPeer).is_connection_error());
assert!(client(ClientError::CannotParseInteger).is_connection_error());
assert!(client(ClientError::UnknownRespTag('@')).is_connection_error());
assert!(!redis(RedisErrorKind::WrongType).is_connection_error());
assert!(!client(ClientError::MismatchedKeySlots).is_connection_error());
assert!(!client(ClientError::CannotParseBytes).is_connection_error());
assert!(!Error::from(ErrorKind::Timeout).is_connection_error());
assert!(!Error::from(ErrorKind::Aborted).is_connection_error());
}
#[test]
fn a_timeout_is_its_own_class() {
assert!(Error::from(ErrorKind::Timeout).is_timeout());
assert!(!io().is_timeout());
assert!(!redis(RedisErrorKind::TryAgain).is_timeout());
assert!(!Error::from(ErrorKind::Timeout).is_server_error());
assert!(!Error::from(ErrorKind::Timeout).is_connection_error());
}
#[test]
fn a_server_error_is_a_reply_the_server_chose_to_send() {
assert!(redis(RedisErrorKind::WrongType).is_server_error());
assert!(redis(RedisErrorKind::Other).is_server_error());
assert!(!io().is_server_error());
assert!(!client(ClientError::CannotParseInteger).is_server_error());
}
#[test]
fn a_retryable_error_covers_every_transient_layer() {
assert!(io().is_retryable());
assert!(Error::from(ErrorKind::EOF).is_retryable());
assert!(Error::from(ErrorKind::Timeout).is_retryable());
assert!(redis(RedisErrorKind::TryAgain).is_retryable());
assert!(redis(RedisErrorKind::ClusterDown).is_retryable());
assert!(redis(RedisErrorKind::MasterDown).is_retryable());
assert!(redis(RedisErrorKind::NoMasterLink).is_retryable());
assert!(!redis(RedisErrorKind::WrongType).is_retryable());
assert!(!redis(RedisErrorKind::NoAuth).is_retryable());
assert!(!redis(RedisErrorKind::Err).is_retryable());
assert!(!client(ClientError::MismatchedKeySlots).is_retryable());
assert!(!Error::from(ErrorKind::Aborted).is_retryable());
}
#[tokio::test]
#[serial]
async fn a_failing_command_inside_a_transaction_names_itself() -> Result<()> {
let client = get_test_client().await?;
client.del("a_list_for_tx").await?;
client.lpush("a_list_for_tx", "value").await?;
let mut transaction = client.create_transaction();
transaction.set("tx_ok_key", "value").forget();
transaction.get::<String>("a_list_for_tx").queue();
let result: Result<String> = transaction.execute().await;
let error = result.expect_err("GET on a list must be refused inside the transaction");
assert_eq!(
Some("GET"),
error.command(),
"the failing command must name itself, not the head of the batch: {error:?}"
);
Ok(())
}