testcontainers/runners/
async_builder.rs1use 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]
15impl<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}