Skip to main content

testcontainers/runners/
async_builder.rs

1use async_trait::async_trait;
2
3use crate::{
4    core::{build::build_options::BuildImageOptions, client::Client, error::Result},
5    BuildableImage,
6};
7
8#[async_trait]
9pub trait AsyncBuilder<B: BuildableImage> {
10    async fn build_image(self) -> Result<B::Built>;
11    async fn build_image_with(self, options: BuildImageOptions) -> Result<B::Built>;
12}
13
14#[async_trait]
15/// Helper trait to build Docker images asynchronously from [`BuildableImage`] instances.
16///
17/// Provides an asynchronous interface for building custom Docker images within test environments.
18/// This trait is automatically implemented for any type that implements [`BuildableImage`] + [`Send`].
19///
20/// # Example
21///
22/// ```rust,no_run
23/// use testcontainers::{core::WaitFor, runners::AsyncBuilder, runners::AsyncRunner, GenericBuildableImage};
24///
25/// #[test]
26/// async fn test_custom_image() -> anyhow::Result<()> {
27///     let image = GenericBuildableImage::new("my-test-app", "latest")
28///         .with_dockerfile_string("FROM alpine:latest\nRUN echo 'hello'")
29///         .build_image()?.await;
30///     // Use the built image in containers
31///     let container = image
32///         .with_wait_for(WaitFor::message_on_stdout("Hello from test!"))
33///         .start()?.await;
34///
35///     Ok(())
36/// }
37/// ```
38impl<T> AsyncBuilder<T> for T
39where
40    T: BuildableImage + Send,
41{
42    async fn build_image(self) -> Result<T::Built> {
43        self.build_image_with(BuildImageOptions::default()).await
44    }
45
46    async fn build_image_with(self, options: BuildImageOptions) -> Result<T::Built> {
47        let client = Client::lazy_client().await?;
48
49        let build_context = self.build_context();
50        let descriptor = self.descriptor();
51
52        client
53            .build_image(&descriptor, &build_context, options)
54            .await?;
55
56        Ok(self.into_image())
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use crate::{
63        core::{BuildImageOptions, WaitFor},
64        runners::{AsyncBuilder, AsyncRunner},
65        GenericBuildableImage,
66    };
67
68    #[tokio::test]
69    async fn build_image_and_run() -> anyhow::Result<()> {
70        let _ = pretty_env_logger::try_init();
71
72        let image = GenericBuildableImage::new("hello-tc", "latest")
73            .with_dockerfile_string(
74                r#"FROM alpine:latest
75COPY --chmod=0755 hello.sh /sbin/hello
76ENTRYPOINT ["/sbin/hello"]
77"#,
78            )
79            .with_data(
80                r#"#!/bin/sh
81echo "hello from hello-tc""#,
82                "./hello.sh",
83            )
84            .build_image()
85            .await?;
86
87        let _container = image
88            .with_wait_for(WaitFor::message_on_stdout("hello from hello-tc"))
89            .start()
90            .await?;
91
92        Ok(())
93    }
94
95    #[tokio::test]
96    async fn build_image_with_options() -> anyhow::Result<()> {
97        let _ = pretty_env_logger::try_init();
98
99        let image = GenericBuildableImage::new("hello-tc-with-options", "test")
100            .with_dockerfile_string(
101                r#"FROM alpine:latest
102COPY --chmod=0755 hello.sh /sbin/hello
103ENTRYPOINT ["/sbin/hello"]
104"#,
105            )
106            .with_data(
107                r#"#!/bin/sh
108echo "hello from build_image_with_options""#,
109                "./hello.sh",
110            )
111            .build_image_with(BuildImageOptions::new().with_no_cache(true))
112            .await?;
113
114        let _container = image
115            .with_wait_for(WaitFor::message_on_stdout(
116                "hello from build_image_with_options",
117            ))
118            .start()
119            .await?;
120
121        Ok(())
122    }
123
124    #[tokio::test]
125    async fn build_image_skip_if_exists() -> anyhow::Result<()> {
126        let _ = pretty_env_logger::try_init();
127
128        let image1 = GenericBuildableImage::new("hello-tc-skip", "test")
129            .with_dockerfile_string(
130                r#"FROM alpine:latest
131COPY --chmod=0755 hello.sh /sbin/hello
132ENTRYPOINT ["/sbin/hello"]
133"#,
134            )
135            .with_data(
136                r#"#!/bin/sh
137echo "hello from skip test""#,
138                "./hello.sh",
139            )
140            .build_image_with(BuildImageOptions::new())
141            .await?;
142
143        let _container1 = image1
144            .with_wait_for(WaitFor::message_on_stdout("hello from skip test"))
145            .start()
146            .await?;
147
148        let image2 = GenericBuildableImage::new("hello-tc-skip", "test")
149            .with_dockerfile_string(
150                r#"FROM alpine:latest
151COPY --chmod=0755 hello.sh /sbin/hello
152ENTRYPOINT ["/sbin/hello"]
153"#,
154            )
155            .with_data(
156                r#"#!/bin/sh
157echo "hello from skip test""#,
158                "./hello.sh",
159            )
160            .build_image_with(BuildImageOptions::new().with_skip_if_exists(true))
161            .await?;
162
163        let _container2 = image2
164            .with_wait_for(WaitFor::message_on_stdout("hello from skip test"))
165            .start()
166            .await?;
167
168        Ok(())
169    }
170
171    #[tokio::test]
172    async fn build_image_parallel_with_skip_if_exists() -> anyhow::Result<()> {
173        let _ = pretty_env_logger::try_init();
174
175        let build_task = || async {
176            GenericBuildableImage::new("hello-tc-parallel", "test")
177                .with_dockerfile_string(
178                    r#"FROM alpine:latest
179COPY --chmod=0755 hello.sh /sbin/hello
180ENTRYPOINT ["/sbin/hello"]
181"#,
182                )
183                .with_data(
184                    r#"#!/bin/sh
185echo "hello from parallel test""#,
186                    "./hello.sh",
187                )
188                .build_image_with(BuildImageOptions::new().with_skip_if_exists(true))
189                .await
190        };
191
192        let (result1, result2, result3) = tokio::join!(build_task(), build_task(), build_task());
193
194        let image1 = result1?;
195        let image2 = result2?;
196        let image3 = result3?;
197
198        let _container1 = image1
199            .with_wait_for(WaitFor::message_on_stdout("hello from parallel test"))
200            .start()
201            .await?;
202
203        let _container2 = image2
204            .with_wait_for(WaitFor::message_on_stdout("hello from parallel test"))
205            .start()
206            .await?;
207
208        let _container3 = image3
209            .with_wait_for(WaitFor::message_on_stdout("hello from parallel test"))
210            .start()
211            .await?;
212
213        Ok(())
214    }
215
216    #[tokio::test]
217    async fn build_image_with_build_args() -> anyhow::Result<()> {
218        let _ = pretty_env_logger::try_init();
219
220        let image = GenericBuildableImage::new("hello-tc-buildargs", "test")
221            .with_dockerfile_string(
222                r#"FROM alpine:latest
223ARG VERSION=unknown
224ARG BUILD_DATE=unknown
225RUN echo "Building with VERSION=${VERSION} DATE=${BUILD_DATE}" > /build_info.txt
226COPY --chmod=0755 hello.sh /sbin/hello
227ENTRYPOINT ["/sbin/hello"]
228"#,
229            )
230            .with_data(
231                r#"#!/bin/sh
232cat /build_info.txt"#,
233                "./hello.sh",
234            )
235            .build_image_with(
236                BuildImageOptions::new()
237                    .with_build_arg("VERSION", "1.0.0")
238                    .with_build_arg("BUILD_DATE", "2024-10-25"),
239            )
240            .await?;
241
242        let _container = image
243            .with_wait_for(WaitFor::message_on_stdout("VERSION=1.0.0"))
244            .start()
245            .await?;
246
247        Ok(())
248    }
249}