rightsize_modules/
mariadb.rs1use std::time::Duration;
41
42use rightsize::{Container, ContainerGuard, Result, Wait};
43
44const PORT: u16 = 3306;
45
46pub struct MariaDbContainer {
48 container: Container,
49 username: String,
50 password: String,
51 database: String,
52}
53
54impl MariaDbContainer {
55 pub fn new() -> Self {
57 Self::with_image("mariadb:11.4")
58 }
59
60 pub fn with_image(image: &str) -> Self {
62 let username = "test".to_string();
63 let password = "test".to_string();
64 let database = "test".to_string();
65 let container = Container::new(image)
66 .with_exposed_ports(&[PORT])
67 .with_env("MARIADB_USER", &username)
68 .with_env("MARIADB_PASSWORD", &password)
69 .with_env("MARIADB_DATABASE", &database)
70 .with_env("MARIADB_ROOT_PASSWORD", "test")
71 .waiting_for(
75 Wait::for_log_message(r".*port: 3306.*mariadb\.org binary distribution.*", 1)
76 .with_startup_timeout(Duration::from_secs(60)),
77 );
78 Self {
79 container,
80 username,
81 password,
82 database,
83 }
84 }
85
86 pub fn with_username(mut self, username: &str) -> Self {
88 self.username = username.to_string();
89 self.container = self.container.with_env("MARIADB_USER", username);
90 self
91 }
92
93 pub fn with_password(mut self, password: &str) -> Self {
95 self.password = password.to_string();
96 self.container = self.container.with_env("MARIADB_PASSWORD", password);
97 self
98 }
99
100 pub fn with_database(mut self, database: &str) -> Self {
102 self.database = database.to_string();
103 self.container = self.container.with_env("MARIADB_DATABASE", database);
104 self
105 }
106
107 pub async fn start(self) -> Result<MariaDbGuard> {
109 let guard = self.container.start().await?;
110 Ok(MariaDbGuard {
111 guard,
112 username: self.username,
113 password: self.password,
114 database: self.database,
115 })
116 }
117}
118
119impl Default for MariaDbContainer {
120 fn default() -> Self {
121 Self::new()
122 }
123}
124
125pub struct MariaDbGuard {
127 guard: ContainerGuard,
128 username: String,
129 password: String,
130 database: String,
131}
132
133impl MariaDbGuard {
134 pub fn username(&self) -> &str {
136 &self.username
137 }
138
139 pub fn password(&self) -> &str {
141 &self.password
142 }
143
144 pub fn database_name(&self) -> &str {
146 &self.database
147 }
148
149 pub fn connection_string(&self) -> String {
154 format!(
155 "mysql://{}:{}@{}:{}/{}",
156 self.username,
157 self.password,
158 self.guard.host(),
159 self.guard.get_mapped_port(PORT).unwrap(),
160 self.database,
161 )
162 }
163
164 pub async fn stop(self) -> Result<()> {
166 self.guard.stop().await
167 }
168}
169
170impl std::ops::Deref for MariaDbGuard {
171 type Target = ContainerGuard;
172 fn deref(&self) -> &ContainerGuard {
173 &self.guard
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use rightsize::wait::{WaitStrategy, WaitTarget};
181
182 #[test]
183 fn defaults_are_the_test_trio() {
184 let c = MariaDbContainer::new();
185 assert_eq!(c.username, "test");
186 assert_eq!(c.password, "test");
187 assert_eq!(c.database, "test");
188 }
189
190 #[test]
191 fn builders_override_the_defaults() {
192 let c = MariaDbContainer::new()
193 .with_username("alice")
194 .with_password("s3cret")
195 .with_database("app");
196 assert_eq!(c.username, "alice");
197 assert_eq!(c.password, "s3cret");
198 assert_eq!(c.database, "app");
199 }
200
201 const CAPTURED_LOG: &str = "\
2022026-07-04 8:47:29 0 [Note] mariadbd: ready for connections.
203Version: '11.4.12-MariaDB-ubu2404' socket: '/run/mysqld/mysqld.sock' port: 0 mariadb.org binary distribution
2042026-07-04 8:47:30 0 [Note] Server socket created on IP: '0.0.0.0', port: '3306'.
2052026-07-04 8:47:30 0 [Note] Server socket created on IP: '::', port: '3306'.
2062026-07-04 8:47:30 0 [Note] mariadbd: ready for connections.
207Version: '11.4.12-MariaDB-ubu2404' socket: '/run/mysqld/mysqld.sock' port: 3306 mariadb.org binary distribution";
208
209 struct FakeTarget(std::sync::Mutex<String>);
210 #[async_trait::async_trait]
211 impl WaitTarget for FakeTarget {
212 fn host(&self) -> &str {
213 "127.0.0.1"
214 }
215 fn mapped_port(&self, guest_port: u16) -> u16 {
216 guest_port
217 }
218 fn exposed_guest_ports(&self) -> Vec<u16> {
219 vec![PORT]
220 }
221 async fn current_logs(&self) -> String {
222 self.0.lock().unwrap().clone()
223 }
224 fn describe(&self) -> String {
225 "fake-mariadb".to_string()
226 }
227 }
228
229 #[tokio::test]
232 async fn temp_server_port_zero_line_does_not_signal_ready() {
233 let partial: String = CAPTURED_LOG.lines().take(2).collect::<Vec<_>>().join("\n");
234 let target = FakeTarget(std::sync::Mutex::new(partial));
235 let err = Wait::for_log_message(r".*port: 3306.*mariadb\.org binary distribution.*", 1)
236 .with_startup_timeout(Duration::from_millis(300))
237 .wait_until_ready(&target)
238 .await
239 .expect_err("the temp server's port: 0 line must not signal ready");
240 let _ = err;
241 }
242
243 #[tokio::test]
244 async fn only_the_real_servers_port_3306_line_signals_ready() {
245 let target = FakeTarget(std::sync::Mutex::new(CAPTURED_LOG.to_string()));
246 Wait::for_log_message(r".*port: 3306.*mariadb\.org binary distribution.*", 1)
247 .with_startup_timeout(Duration::from_secs(5))
248 .wait_until_ready(&target)
249 .await
250 .expect("the real server's port: 3306 line must signal ready");
251 }
252}