1pub mod cluster;
27pub mod sentinel;
28pub mod server;
29pub mod utils;
30
31use std::collections::VecDeque;
32use std::sync::{Arc, Mutex};
33
34use redis::{Cmd, ConnectionLike, ErrorKind, Pipeline, RedisError, RedisResult, Value};
35
36#[cfg(feature = "aio")]
37use futures::{future, FutureExt};
38
39#[cfg(feature = "aio")]
40use redis::{aio::ConnectionLike as AioConnectionLike, RedisFuture};
41
42pub trait IntoRedisValue {
46 fn into_redis_value(self) -> Value;
48}
49
50impl IntoRedisValue for String {
51 fn into_redis_value(self) -> Value {
52 Value::BulkString(self.as_bytes().to_vec())
53 }
54}
55
56impl IntoRedisValue for &str {
57 fn into_redis_value(self) -> Value {
58 Value::BulkString(self.as_bytes().to_vec())
59 }
60}
61
62#[cfg(feature = "bytes")]
63impl IntoRedisValue for bytes::Bytes {
64 fn into_redis_value(self) -> Value {
65 Value::BulkString(self.to_vec())
66 }
67}
68
69impl IntoRedisValue for Vec<u8> {
70 fn into_redis_value(self) -> Value {
71 Value::BulkString(self)
72 }
73}
74
75impl IntoRedisValue for Value {
76 fn into_redis_value(self) -> Value {
77 self
78 }
79}
80
81impl IntoRedisValue for i64 {
82 fn into_redis_value(self) -> Value {
83 Value::Int(self)
84 }
85}
86
87pub trait IntoRedisCmdBytes {
90 fn into_redis_cmd_bytes(self) -> Vec<u8>;
92}
93
94impl IntoRedisCmdBytes for Cmd {
95 fn into_redis_cmd_bytes(self) -> Vec<u8> {
96 self.get_packed_command()
97 }
98}
99
100impl IntoRedisCmdBytes for &Cmd {
101 fn into_redis_cmd_bytes(self) -> Vec<u8> {
102 self.get_packed_command()
103 }
104}
105
106impl IntoRedisCmdBytes for &mut Cmd {
107 fn into_redis_cmd_bytes(self) -> Vec<u8> {
108 self.get_packed_command()
109 }
110}
111
112impl IntoRedisCmdBytes for Pipeline {
113 fn into_redis_cmd_bytes(self) -> Vec<u8> {
114 self.get_packed_pipeline()
115 }
116}
117
118impl IntoRedisCmdBytes for &Pipeline {
119 fn into_redis_cmd_bytes(self) -> Vec<u8> {
120 self.get_packed_pipeline()
121 }
122}
123
124impl IntoRedisCmdBytes for &mut Pipeline {
125 fn into_redis_cmd_bytes(self) -> Vec<u8> {
126 self.get_packed_pipeline()
127 }
128}
129
130pub struct MockCmd {
132 cmd_bytes: Vec<u8>,
133 responses: Result<Vec<Value>, RedisError>,
134}
135
136impl MockCmd {
137 pub fn new<C, V>(cmd: C, response: Result<V, RedisError>) -> Self
140 where
141 C: IntoRedisCmdBytes,
142 V: IntoRedisValue,
143 {
144 MockCmd {
145 cmd_bytes: cmd.into_redis_cmd_bytes(),
146 responses: response.map(|r| vec![r.into_redis_value()]),
147 }
148 }
149
150 pub fn with_values<C, V>(cmd: C, responses: Result<Vec<V>, RedisError>) -> Self
153 where
154 C: IntoRedisCmdBytes,
155 V: IntoRedisValue,
156 {
157 MockCmd {
158 cmd_bytes: cmd.into_redis_cmd_bytes(),
159 responses: responses.map(|xs| xs.into_iter().map(|x| x.into_redis_value()).collect()),
160 }
161 }
162}
163
164#[derive(Clone)]
167pub struct MockRedisConnection {
168 commands: Arc<Mutex<VecDeque<MockCmd>>>,
169}
170
171impl MockRedisConnection {
172 pub fn new<I>(commands: I) -> Self
174 where
175 I: IntoIterator<Item = MockCmd>,
176 {
177 MockRedisConnection {
178 commands: Arc::new(Mutex::new(VecDeque::from_iter(commands))),
179 }
180 }
181}
182
183impl ConnectionLike for MockRedisConnection {
184 fn req_packed_command(&mut self, cmd: &[u8]) -> RedisResult<Value> {
185 let mut commands = self.commands.lock().unwrap();
186 let next_cmd = commands.pop_front().ok_or_else(|| {
187 RedisError::from((
188 ErrorKind::ClientError,
189 "TEST",
190 "unexpected command".to_owned(),
191 ))
192 })?;
193
194 if cmd != next_cmd.cmd_bytes {
195 return Err(RedisError::from((
196 ErrorKind::ClientError,
197 "TEST",
198 format!(
199 "unexpected command: expected={}, actual={}",
200 String::from_utf8(next_cmd.cmd_bytes)
201 .unwrap_or_else(|_| "decode error".to_owned()),
202 String::from_utf8(Vec::from(cmd)).unwrap_or_else(|_| "decode error".to_owned()),
203 ),
204 )));
205 }
206
207 next_cmd
208 .responses
209 .and_then(|values| match values.as_slice() {
210 [value] => Ok(value.clone()),
211 [] => Err(RedisError::from((
212 ErrorKind::ClientError,
213 "no value configured as response",
214 ))),
215 _ => Err(RedisError::from((
216 ErrorKind::ClientError,
217 "multiple values configured as response for command expecting a single value",
218 ))),
219 })
220 }
221
222 fn req_packed_commands(
223 &mut self,
224 cmd: &[u8],
225 _offset: usize,
226 _count: usize,
227 ) -> RedisResult<Vec<Value>> {
228 let mut commands = self.commands.lock().unwrap();
229 let next_cmd = commands.pop_front().ok_or_else(|| {
230 RedisError::from((
231 ErrorKind::ClientError,
232 "TEST",
233 "unexpected command".to_owned(),
234 ))
235 })?;
236
237 if cmd != next_cmd.cmd_bytes {
238 return Err(RedisError::from((
239 ErrorKind::ClientError,
240 "TEST",
241 format!(
242 "unexpected command: expected={}, actual={}",
243 String::from_utf8(next_cmd.cmd_bytes)
244 .unwrap_or_else(|_| "decode error".to_owned()),
245 String::from_utf8(Vec::from(cmd)).unwrap_or_else(|_| "decode error".to_owned()),
246 ),
247 )));
248 }
249
250 next_cmd.responses
251 }
252
253 fn get_db(&self) -> i64 {
254 0
255 }
256
257 fn check_connection(&mut self) -> bool {
258 true
259 }
260
261 fn is_open(&self) -> bool {
262 true
263 }
264}
265
266#[cfg(feature = "aio")]
267impl AioConnectionLike for MockRedisConnection {
268 fn req_packed_command<'a>(&'a mut self, cmd: &'a Cmd) -> RedisFuture<'a, Value> {
269 let packed_cmd = cmd.get_packed_command();
270 let response = <MockRedisConnection as ConnectionLike>::req_packed_command(
271 self,
272 packed_cmd.as_slice(),
273 );
274 future::ready(response).boxed()
275 }
276
277 fn req_packed_commands<'a>(
278 &'a mut self,
279 cmd: &'a Pipeline,
280 offset: usize,
281 count: usize,
282 ) -> RedisFuture<'a, Vec<Value>> {
283 let packed_cmd = cmd.get_packed_pipeline();
284 let response = <MockRedisConnection as ConnectionLike>::req_packed_commands(
285 self,
286 packed_cmd.as_slice(),
287 offset,
288 count,
289 );
290 future::ready(response).boxed()
291 }
292
293 fn get_db(&self) -> i64 {
294 0
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::{MockCmd, MockRedisConnection};
301 use redis::{cmd, pipe, ErrorKind, Value};
302
303 #[test]
304 fn sync_basic_test() {
305 let mut conn = MockRedisConnection::new(vec![
306 MockCmd::new(cmd("SET").arg("foo").arg(42), Ok("")),
307 MockCmd::new(cmd("GET").arg("foo"), Ok(42)),
308 MockCmd::new(cmd("SET").arg("bar").arg("foo"), Ok("")),
309 MockCmd::new(cmd("GET").arg("bar"), Ok("foo")),
310 ]);
311
312 cmd("SET").arg("foo").arg(42).exec(&mut conn).unwrap();
313 assert_eq!(cmd("GET").arg("foo").query(&mut conn), Ok(42));
314
315 cmd("SET").arg("bar").arg("foo").exec(&mut conn).unwrap();
316 assert_eq!(
317 cmd("GET").arg("bar").query(&mut conn),
318 Ok(Value::BulkString(b"foo".as_ref().into()))
319 );
320 }
321
322 #[cfg(feature = "aio")]
323 #[tokio::test]
324 async fn async_basic_test() {
325 let mut conn = MockRedisConnection::new(vec![
326 MockCmd::new(cmd("SET").arg("foo").arg(42), Ok("")),
327 MockCmd::new(cmd("GET").arg("foo"), Ok(42)),
328 MockCmd::new(cmd("SET").arg("bar").arg("foo"), Ok("")),
329 MockCmd::new(cmd("GET").arg("bar"), Ok("foo")),
330 ]);
331
332 cmd("SET")
333 .arg("foo")
334 .arg("42")
335 .exec_async(&mut conn)
336 .await
337 .unwrap();
338 let result: Result<usize, _> = cmd("GET").arg("foo").query_async(&mut conn).await;
339 assert_eq!(result, Ok(42));
340
341 cmd("SET")
342 .arg("bar")
343 .arg("foo")
344 .exec_async(&mut conn)
345 .await
346 .unwrap();
347 let result: Result<Vec<u8>, _> = cmd("GET").arg("bar").query_async(&mut conn).await;
348 assert_eq!(result.as_deref(), Ok(&b"foo"[..]));
349 }
350
351 #[test]
352 fn errors_for_unexpected_commands() {
353 let mut conn = MockRedisConnection::new(vec![
354 MockCmd::new(cmd("SET").arg("foo").arg(42), Ok("")),
355 MockCmd::new(cmd("GET").arg("foo"), Ok(42)),
356 ]);
357
358 cmd("SET").arg("foo").arg(42).exec(&mut conn).unwrap();
359 assert_eq!(cmd("GET").arg("foo").query(&mut conn), Ok(42));
360
361 let err = cmd("SET")
362 .arg("bar")
363 .arg("foo")
364 .exec(&mut conn)
365 .unwrap_err();
366 assert_eq!(err.kind(), ErrorKind::ClientError);
367 assert_eq!(err.detail(), Some("unexpected command"));
368 }
369
370 #[test]
371 fn errors_for_mismatched_commands() {
372 let mut conn = MockRedisConnection::new(vec![
373 MockCmd::new(cmd("SET").arg("foo").arg(42), Ok("")),
374 MockCmd::new(cmd("GET").arg("foo"), Ok(42)),
375 MockCmd::new(cmd("SET").arg("bar").arg("foo"), Ok("")),
376 ]);
377
378 cmd("SET").arg("foo").arg(42).exec(&mut conn).unwrap();
379 let err = cmd("SET")
380 .arg("bar")
381 .arg("foo")
382 .exec(&mut conn)
383 .unwrap_err();
384 assert_eq!(err.kind(), ErrorKind::ClientError);
385 assert!(err.detail().unwrap().contains("unexpected command"));
386 }
387
388 #[test]
389 fn pipeline_basic_test() {
390 let mut conn = MockRedisConnection::new(vec![MockCmd::with_values(
391 pipe().cmd("GET").arg("foo").cmd("GET").arg("bar"),
392 Ok(vec!["hello", "world"]),
393 )]);
394
395 let results: Vec<String> = pipe()
396 .cmd("GET")
397 .arg("foo")
398 .cmd("GET")
399 .arg("bar")
400 .query(&mut conn)
401 .expect("success");
402 assert_eq!(results, vec!["hello", "world"]);
403 }
404
405 #[test]
406 fn pipeline_atomic_test() {
407 let mut conn = MockRedisConnection::new(vec![MockCmd::with_values(
408 pipe().atomic().cmd("GET").arg("foo").cmd("GET").arg("bar"),
409 Ok(vec![Value::Array(
410 vec!["hello", "world"]
411 .into_iter()
412 .map(|x| Value::BulkString(x.as_bytes().into()))
413 .collect(),
414 )]),
415 )]);
416
417 let results: Vec<String> = pipe()
418 .atomic()
419 .cmd("GET")
420 .arg("foo")
421 .cmd("GET")
422 .arg("bar")
423 .query(&mut conn)
424 .expect("success");
425 assert_eq!(results, vec!["hello", "world"]);
426 }
427}