testcontainers/runners/
sync_runner.rs1use std::sync::{Arc, Mutex, OnceLock, Weak};
2
3use crate::{core::error::Result, Container, ContainerRequest, Image, TestcontainersError};
4
5static ASYNC_RUNTIME: OnceLock<Mutex<Weak<tokio::runtime::Runtime>>> = OnceLock::new();
9
10pub trait SyncRunner<I: Image> {
26 fn start(self) -> Result<Container<I>>;
28
29 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 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 .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 let _container1: Container<HelloWorld> = HelloWorld::default()
341 .with_network("sync-awesome-net")
342 .start()?;
343 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 assert!(!network_exists(&client, "sync-awesome-net"));
355 }
356 Ok(())
357 }
358}