testcontainers_modules/valkey/
mod.rs

1use std::{borrow::Cow, collections::BTreeMap};
2
3use testcontainers::{
4    core::{ContainerPort, WaitFor},
5    CopyDataSource, CopyToContainer, Image,
6};
7
8const NAME: &str = "valkey/valkey";
9const TAG: &str = "8.0.2-alpine";
10
11/// Default port (6379) on which Valkey is exposed
12pub const VALKEY_PORT: ContainerPort = ContainerPort::Tcp(6379);
13
14/// Module to work with [`Valkey`] inside of tests.
15/// Valkey is a high-performance data structure server that primarily serves key/value workloads.
16///
17/// Starts an instance of Valkey based on the official [`Valkey docker image`].
18///
19/// By default, Valkey is exposed on Port 6379 ([`VALKEY_PORT`]), just like Redis, and has no access control.
20/// Currently, for communication with Valkey we can still use redis library.
21///
22/// # Example
23/// ```
24/// use redis::Commands;
25/// use testcontainers_modules::{
26///     testcontainers::runners::SyncRunner,
27///     valkey::{Valkey, VALKEY_PORT},
28/// };
29///
30/// let valkey_instance = Valkey::default().start().unwrap();
31/// let host_ip = valkey_instance.get_host().unwrap();
32/// let host_port = valkey_instance.get_host_port_ipv4(VALKEY_PORT).unwrap();
33///
34/// let url = format!("redis://{host_ip}:{host_port}");
35/// let client = redis::Client::open(url.as_ref()).unwrap();
36/// let mut con = client.get_connection().unwrap();
37///
38/// con.set::<_, _, ()>("my_key", 42).unwrap();
39/// let result: i64 = con.get("my_key").unwrap();
40/// ```
41///
42/// [`Valkey`]: https://valkey.io/
43/// [`Valeky docker image`]: https://hub.docker.com/r/valkey/valkey
44/// [`VALKEY_PORT`]: super::VALKEY_PORT
45#[derive(Debug, Default, Clone)]
46pub struct Valkey {
47    env_vars: BTreeMap<String, String>,
48    tag: Option<String>,
49    copy_to_container: Vec<CopyToContainer>,
50}
51
52impl Valkey {
53    /// Create a new Valkey instance with the latest image.
54    ///
55    /// # Example
56    /// ```
57    /// use testcontainers_modules::{
58    ///     testcontainers::runners::SyncRunner,
59    ///     valkey::{Valkey, VALKEY_PORT},
60    /// };
61    ///
62    /// let valkey_instance = Valkey::latest().start().unwrap();
63    /// ```
64    pub fn latest() -> Self {
65        Self {
66            tag: Some("latest".to_string()),
67            ..Default::default()
68        }
69    }
70
71    /// Add extra flags by passing additional start arguments.
72    ///
73    /// # Example
74    /// ```
75    /// use testcontainers_modules::{
76    ///     testcontainers::runners::SyncRunner,
77    ///     valkey::{Valkey, VALKEY_PORT},
78    /// };
79    ///
80    /// let valkey_instance = Valkey::default()
81    ///     .with_valkey_extra_flags("--maxmemory 2mb")
82    ///     .start()
83    ///     .unwrap();
84    /// ```
85    pub fn with_valkey_extra_flags(self, valkey_extra_flags: &str) -> Self {
86        let mut env_vars = self.env_vars;
87        env_vars.insert(
88            "VALKEY_EXTRA_FLAGS".to_string(),
89            valkey_extra_flags.to_string(),
90        );
91        Self {
92            env_vars,
93            tag: self.tag,
94            copy_to_container: self.copy_to_container,
95        }
96    }
97
98    /// Add custom valkey configuration.
99    ///
100    /// # Example
101    /// ```
102    /// use testcontainers_modules::{
103    ///     testcontainers::runners::SyncRunner,
104    ///     valkey::{Valkey, VALKEY_PORT},
105    /// };
106    ///
107    /// let valkey_instance = Valkey::default()
108    ///     .with_valkey_conf("maxmemory 2mb".to_string().into_bytes())
109    ///     .start()
110    ///     .unwrap();
111    /// ```
112    pub fn with_valkey_conf(self, valky_conf: impl Into<CopyDataSource>) -> Self {
113        let mut copy_to_container = self.copy_to_container;
114        copy_to_container.push(CopyToContainer::new(
115            valky_conf.into(),
116            "/usr/local/etc/valkey/valkey.conf",
117        ));
118        Self {
119            env_vars: self.env_vars,
120            tag: self.tag,
121            copy_to_container,
122        }
123    }
124}
125
126impl Image for Valkey {
127    fn name(&self) -> &str {
128        NAME
129    }
130
131    fn tag(&self) -> &str {
132        self.tag.as_deref().unwrap_or(TAG)
133    }
134
135    fn ready_conditions(&self) -> Vec<WaitFor> {
136        vec![WaitFor::message_on_stdout("Ready to accept connections")]
137    }
138
139    fn env_vars(
140        &self,
141    ) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
142        &self.env_vars
143    }
144
145    fn copy_to_sources(&self) -> impl IntoIterator<Item = &CopyToContainer> {
146        &self.copy_to_container
147    }
148
149    fn cmd(&self) -> impl IntoIterator<Item = impl Into<Cow<'_, str>>> {
150        if !self.copy_to_container.is_empty() {
151            vec!["valkey-server", "/usr/local/etc/valkey/valkey.conf"]
152        } else {
153            Vec::new()
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use std::collections::HashMap;
161
162    use redis::Commands;
163    use testcontainers::Image;
164
165    use crate::{
166        testcontainers::runners::SyncRunner,
167        valkey::{Valkey, TAG, VALKEY_PORT},
168    };
169
170    #[test]
171    fn valkey_fetch_an_integer() -> Result<(), Box<dyn std::error::Error + 'static>> {
172        let _ = pretty_env_logger::try_init();
173        let node = Valkey::default().start()?;
174
175        let tag = node.image().tag.clone();
176        assert_eq!(None, tag);
177        let tag_from_method = node.image().tag();
178        assert_eq!(TAG, tag_from_method);
179        assert_eq!(0, node.image().copy_to_container.len());
180
181        let host_ip = node.get_host()?;
182        let host_port = node.get_host_port_ipv4(VALKEY_PORT)?;
183        let url = format!("redis://{host_ip}:{host_port}");
184        let client = redis::Client::open(url.as_ref()).unwrap();
185        let mut con = client.get_connection().unwrap();
186
187        con.set::<_, _, ()>("my_key", 42).unwrap();
188        let result: i64 = con.get("my_key").unwrap();
189        assert_eq!(42, result);
190        Ok(())
191    }
192
193    #[test]
194    fn valkey_latest() -> Result<(), Box<dyn std::error::Error + 'static>> {
195        let _ = pretty_env_logger::try_init();
196        let node = Valkey::latest().start()?;
197
198        let tag = node.image().tag.clone();
199        assert_eq!(Some("latest".to_string()), tag);
200        let tag_from_method = node.image().tag();
201        assert_eq!("latest", tag_from_method);
202        assert_eq!(0, node.image().copy_to_container.len());
203
204        let host_ip = node.get_host()?;
205        let host_port = node.get_host_port_ipv4(VALKEY_PORT)?;
206        let url = format!("redis://{host_ip}:{host_port}");
207        let client = redis::Client::open(url.as_ref()).unwrap();
208        let mut con = client.get_connection().unwrap();
209
210        con.set::<_, _, ()>("my_key", 42).unwrap();
211        let result: i64 = con.get("my_key").unwrap();
212        assert_eq!(42, result);
213        Ok(())
214    }
215
216    #[test]
217    fn valkey_extra_flags() -> Result<(), Box<dyn std::error::Error + 'static>> {
218        let _ = pretty_env_logger::try_init();
219        let node = Valkey::default()
220            .with_valkey_extra_flags("--maxmemory 2mb")
221            .start()?;
222        let tag = node.image().tag.clone();
223        assert_eq!(None, tag);
224        let tag_from_method = node.image().tag();
225        assert_eq!(TAG, tag_from_method);
226        assert_eq!(0, node.image().copy_to_container.len());
227
228        let host_ip = node.get_host()?;
229        let host_port = node.get_host_port_ipv4(VALKEY_PORT)?;
230        let url = format!("redis://{host_ip}:{host_port}");
231
232        let client = redis::Client::open(url.as_ref()).unwrap();
233        let mut con = client.get_connection().unwrap();
234        let max_memory: HashMap<String, isize> = redis::cmd("CONFIG")
235            .arg("GET")
236            .arg("maxmemory")
237            .query(&mut con)
238            .unwrap();
239        let max = *max_memory.get("maxmemory").unwrap();
240        assert_eq!(2097152, max);
241        Ok(())
242    }
243
244    #[test]
245    fn valkey_conf() -> Result<(), Box<dyn std::error::Error + 'static>> {
246        let _ = pretty_env_logger::try_init();
247        let node = Valkey::default()
248            .with_valkey_conf("maxmemory 2mb".to_string().into_bytes())
249            .start()?;
250        let tag = node.image().tag.clone();
251        assert_eq!(None, tag);
252        let tag_from_method = node.image().tag();
253        assert_eq!(TAG, tag_from_method);
254        assert_eq!(1, node.image().copy_to_container.len());
255
256        let host_ip = node.get_host()?;
257        let host_port = node.get_host_port_ipv4(VALKEY_PORT)?;
258        let url = format!("redis://{host_ip}:{host_port}");
259
260        let client = redis::Client::open(url.as_ref()).unwrap();
261        let mut con = client.get_connection().unwrap();
262        let max_memory: HashMap<String, isize> = redis::cmd("CONFIG")
263            .arg("GET")
264            .arg("maxmemory")
265            .query(&mut con)
266            .unwrap();
267        let max = *max_memory.get("maxmemory").unwrap();
268        assert_eq!(2097152, max);
269        Ok(())
270    }
271}