testcontainers/runners/
sync_runner.rs

1use std::sync::{Arc, Mutex, OnceLock, Weak};
2
3use crate::{core::error::Result, Container, ContainerRequest, Image, TestcontainersError};
4
5// We use `Weak` in order not to prevent `Drop` of being called.
6// Instead, we re-create the runtime if it was dropped and asked one more time.
7// This way we provide on `Drop` guarantees and avoid unnecessary instantiation at the same time.
8static ASYNC_RUNTIME: OnceLock<Mutex<Weak<tokio::runtime::Runtime>>> = OnceLock::new();
9
10/// Helper trait to start containers synchronously.
11///
12/// ## Example
13///
14/// ```rust,no_run
15/// use testcontainers::{core::{IntoContainerPort, WaitFor}, runners::SyncRunner, GenericImage};
16///
17/// fn test_redis() {
18///     let container = GenericImage::new("redis", "7.2.4")
19///         .with_exposed_port(6379.tcp())
20///         .with_wait_for(WaitFor::message_on_stdout("Ready to accept connections"))
21///         .start()
22///         .unwrap();
23/// }
24/// ```
25pub trait SyncRunner<I: Image> {
26    /// Starts the container and returns an instance of `Container`.
27    fn start(self) -> Result<Container<I>>;
28
29    /// Pulls the image from the registry.
30    /// Useful if you want to pull the image before starting the container.
31    fn pull_image(self) -> Result<ContainerRequest<I>>;
32}
33
34impl<T, I> SyncRunner<I> for T
35where
36    T: Into<ContainerRequest<I>> + Send,
37    I: Image,
38{
39    fn start(self) -> Result<Container<I>> {
40        let runtime = lazy_sync_runner()?;
41        let async_container = runtime.block_on(super::AsyncRunner::start(self))?;
42
43        Ok(Container::new(runtime, async_container))
44    }
45
46    fn pull_image(self) -> Result<ContainerRequest<I>> {
47        let runtime = lazy_sync_runner()?;
48        runtime.block_on(super::AsyncRunner::pull_image(self))
49    }
50}
51
52pub(crate) fn lazy_sync_runner() -> Result<Arc<tokio::runtime::Runtime>> {
53    let mut guard = ASYNC_RUNTIME
54        .get_or_init(|| Mutex::new(Weak::new()))
55        .lock()
56        .map_err(|e| {
57            TestcontainersError::other(format!("failed to build a runtime for sync-runner: {e}"))
58        })?;
59
60    match guard.upgrade() {
61        Some(runtime) => Ok(runtime),
62        None => {
63            let runtime = Arc::new(
64                // we need to use multi-thread runtime,
65                // because we may spawn background tasks that must keep running regardless of `block_on` calls
66                tokio::runtime::Builder::new_multi_thread()
67                    .thread_name("testcontainers-worker")
68                    .worker_threads(2)
69                    .enable_all()
70                    .build()?,
71            );
72            *guard = Arc::downgrade(&runtime);
73            Ok(runtime)
74        }
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use std::{
81        borrow::Cow,
82        collections::BTreeMap,
83        sync::{Arc, OnceLock},
84    };
85
86    use bollard::models::ContainerInspectResponse;
87    use tokio::runtime::Runtime;
88
89    use super::*;
90    use crate::{
91        core::{client::Client, mounts::Mount, IntoContainerPort, WaitFor},
92        images::generic::GenericImage,
93        runners::SyncBuilder,
94        GenericBuildableImage, ImageExt,
95    };
96
97    static RUNTIME: OnceLock<Runtime> = OnceLock::new();
98
99    fn runtime() -> &'static Runtime {
100        RUNTIME.get_or_init(|| {
101            tokio::runtime::Builder::new_multi_thread()
102                .thread_name("testcontainers-test")
103                .worker_threads(2)
104                .enable_all()
105                .build()
106                .unwrap()
107        })
108    }
109
110    fn docker_client() -> Arc<Client> {
111        runtime().block_on(Client::lazy_client()).unwrap()
112    }
113
114    fn inspect(id: &str) -> ContainerInspectResponse {
115        runtime().block_on(docker_client().inspect(id)).unwrap()
116    }
117
118    fn network_exists(client: &Arc<Client>, name: &str) -> bool {
119        runtime().block_on(client.network_exists(name)).unwrap()
120    }
121
122    fn get_server_container() -> GenericImage {
123        let generic_image = GenericBuildableImage::new("simple_web_server", "latest")
124            // "Dockerfile" is included already, so adding the build context directory is all what is needed
125            .with_file(
126                std::fs::canonicalize("../testimages/simple_web_server").unwrap(),
127                ".",
128            )
129            .build_image()
130            .unwrap();
131        generic_image.with_wait_for(WaitFor::message_on_stdout("server is ready"))
132    }
133
134    #[derive(Default)]
135    struct HelloWorld {
136        mounts: Vec<Mount>,
137        env_vars: BTreeMap<String, String>,
138    }
139
140    impl Image for HelloWorld {
141        fn name(&self) -> &str {
142            "testcontainers/helloworld"
143        }
144
145        fn tag(&self) -> &str {
146            "1.3.0"
147        }
148
149        fn ready_conditions(&self) -> Vec<WaitFor> {
150            vec![WaitFor::message_on_stderr("Ready, listening on")]
151        }
152
153        fn env_vars(
154            &self,
155        ) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
156            Box::new(self.env_vars.iter())
157        }
158
159        fn mounts(&self) -> impl IntoIterator<Item = &Mount> {
160            Box::new(self.mounts.iter())
161        }
162    }
163
164    #[test]
165    fn sync_run_command_should_expose_all_ports_if_no_explicit_mapping_requested(
166    ) -> anyhow::Result<()> {
167        let container = GenericImage::new("testcontainers/helloworld", "1.3.0").start()?;
168
169        let container_details = inspect(container.id());
170        let publish_ports = container_details
171            .host_config
172            .expect("HostConfig")
173            .publish_all_ports
174            .expect("PublishAllPorts");
175        assert!(publish_ports, "publish_all_ports must be `true`");
176        Ok(())
177    }
178
179    #[test]
180    fn sync_run_command_should_map_exposed_port() -> anyhow::Result<()> {
181        let image = get_server_container()
182            .with_exposed_port(5000.tcp())
183            .with_wait_for(WaitFor::seconds(1));
184        let container = image.start()?;
185        let res = container.get_host_port_ipv4(5000.tcp());
186        assert!(res.is_ok());
187        Ok(())
188    }
189
190    #[test]
191    fn sync_run_command_should_expose_only_requested_ports() -> anyhow::Result<()> {
192        let image = GenericImage::new("testcontainers/helloworld", "1.3.0");
193        let container = image
194            .with_mapped_port(124, 456.tcp())
195            .with_mapped_port(556, 888.tcp())
196            .start()?;
197
198        let container_details = inspect(container.id());
199
200        let port_bindings = container_details
201            .host_config
202            .expect("HostConfig")
203            .port_bindings
204            .expect("PortBindings");
205        assert!(port_bindings.contains_key("456/tcp"));
206        assert!(port_bindings.contains_key("888/tcp"));
207        Ok(())
208    }
209
210    #[test]
211    fn sync_rm_command_should_return_error_on_invalid_container() {
212        let res = runtime().block_on(docker_client().rm("!INVALID_NAME_DUE_TO_SYMBOLS!"));
213        assert!(
214            res.is_err(),
215            "should return an error on invalid container name"
216        );
217    }
218
219    #[test]
220    fn sync_run_command_should_include_network() -> anyhow::Result<()> {
221        let image = GenericImage::new("testcontainers/helloworld", "1.3.0");
222        let container = image.with_network("sync-awesome-net-1").start()?;
223
224        let container_details = inspect(container.id());
225        let networks = container_details
226            .network_settings
227            .expect("NetworkSettings")
228            .networks
229            .expect("Networks");
230
231        assert!(
232            networks.contains_key("sync-awesome-net-1"),
233            "Networks is {networks:?}"
234        );
235        Ok(())
236    }
237
238    #[test]
239    fn sync_should_rely_on_network_mode_when_network_is_provided_and_settings_bridge_empty(
240    ) -> anyhow::Result<()> {
241        let web_server = get_server_container().with_wait_for(WaitFor::seconds(1));
242
243        let container = web_server.clone().with_network("bridge").start()?;
244
245        assert!(!container.get_bridge_ip_address()?.to_string().is_empty());
246        Ok(())
247    }
248
249    #[test]
250    fn sync_should_return_error_when_non_bridged_network_selected() -> anyhow::Result<()> {
251        let web_server = get_server_container().with_wait_for(WaitFor::seconds(1));
252
253        let container = web_server.clone().with_network("host").start()?;
254
255        let res = container.get_bridge_ip_address();
256        assert!(res.is_err());
257        Ok(())
258    }
259    #[test]
260    fn sync_run_command_should_include_name() -> anyhow::Result<()> {
261        let image = GenericImage::new("testcontainers/helloworld", "1.3.0");
262        let container = image.with_container_name("sync_hello_container").start()?;
263
264        let container_details = inspect(container.id());
265        let container_name = container_details.name.expect("Name");
266        assert!(container_name.ends_with("sync_hello_container"));
267        Ok(())
268    }
269
270    #[test]
271    fn sync_run_command_with_container_network_should_not_expose_ports() -> anyhow::Result<()> {
272        let _first_container = get_server_container()
273            .with_container_name("the_first_one")
274            .start()?;
275
276        let image = GenericImage::new("testcontainers/helloworld", "1.3.0");
277        image.with_network("container:the_first_one").start()?;
278        Ok(())
279    }
280
281    #[test]
282    fn sync_run_command_should_include_privileged() -> anyhow::Result<()> {
283        let image = GenericImage::new("testcontainers/helloworld", "1.3.0");
284        let container = image.with_privileged(true).start()?;
285        let container_details = inspect(container.id());
286
287        let privileged = container_details
288            .host_config
289            .expect("HostConfig")
290            .privileged
291            .expect("Privileged");
292        assert!(privileged, "privileged must be `true`");
293        Ok(())
294    }
295
296    #[test]
297    fn sync_run_command_should_include_ulimits() -> anyhow::Result<()> {
298        let image = GenericImage::new("testcontainers/helloworld", "1.3.0");
299        let container = image.with_ulimit("nofile", 123, Some(456)).start()?;
300
301        let container_details = inspect(container.id());
302
303        let ulimits = container_details
304            .host_config
305            .expect("HostConfig")
306            .ulimits
307            .expect("Privileged");
308
309        assert_eq!(ulimits.len(), 1);
310        assert_eq!(ulimits[0].name, Some("nofile".into()));
311        assert_eq!(ulimits[0].soft, Some(123));
312        assert_eq!(ulimits[0].hard, Some(456));
313        Ok(())
314    }
315
316    #[test]
317    fn sync_run_command_should_set_shared_memory_size() -> anyhow::Result<()> {
318        let image = GenericImage::new("testcontainers/helloworld", "1.3.0");
319        let container = image.with_shm_size(1_000_000).start()?;
320
321        let container_details = inspect(container.id());
322        let shm_size = container_details
323            .host_config
324            .expect("HostConfig")
325            .shm_size
326            .expect("ShmSize");
327
328        assert_eq!(shm_size, 1_000_000);
329        Ok(())
330    }
331
332    #[test]
333    fn sync_should_create_network_if_image_needs_it_and_drop_it_in_the_end() -> anyhow::Result<()> {
334        {
335            let client = docker_client();
336
337            assert!(!network_exists(&client, "sync-awesome-net"));
338
339            // creating the first container creates the network
340            let _container1: Container<HelloWorld> = HelloWorld::default()
341                .with_network("sync-awesome-net")
342                .start()?;
343            // creating a 2nd container doesn't fail because check if the network exists already
344            let _container2 = HelloWorld::default()
345                .with_network("sync-awesome-net")
346                .start()?;
347
348            assert!(network_exists(&client, "sync-awesome-net"));
349        }
350
351        {
352            let client = docker_client();
353            // original client has been dropped, should clean up networks
354            assert!(!network_exists(&client, "sync-awesome-net"));
355        }
356        Ok(())
357    }
358}