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 crate::register_default_backends();
110 let guard = self.container.start().await?;
111 Ok(MariaDbGuard {
112 guard,
113 username: self.username,
114 password: self.password,
115 database: self.database,
116 })
117 }
118}
119
120impl Default for MariaDbContainer {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126pub struct MariaDbGuard {
128 guard: ContainerGuard,
129 username: String,
130 password: String,
131 database: String,
132}
133
134impl MariaDbGuard {
135 pub fn username(&self) -> &str {
137 &self.username
138 }
139
140 pub fn password(&self) -> &str {
142 &self.password
143 }
144
145 pub fn database_name(&self) -> &str {
147 &self.database
148 }
149
150 pub fn connection_string(&self) -> String {
155 format!(
156 "mysql://{}:{}@{}:{}/{}",
157 self.username,
158 self.password,
159 self.guard.host(),
160 self.guard.get_mapped_port(PORT).unwrap(),
161 self.database,
162 )
163 }
164
165 pub async fn stop(self) -> Result<()> {
167 self.guard.stop().await
168 }
169}
170
171impl std::ops::Deref for MariaDbGuard {
172 type Target = ContainerGuard;
173 fn deref(&self) -> &ContainerGuard {
174 &self.guard
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use rightsize::wait::{WaitStrategy, WaitTarget};
182
183 #[test]
184 fn defaults_are_the_test_trio() {
185 let c = MariaDbContainer::new();
186 assert_eq!(c.username, "test");
187 assert_eq!(c.password, "test");
188 assert_eq!(c.database, "test");
189 }
190
191 #[test]
192 fn builders_override_the_defaults() {
193 let c = MariaDbContainer::new()
194 .with_username("alice")
195 .with_password("s3cret")
196 .with_database("app");
197 assert_eq!(c.username, "alice");
198 assert_eq!(c.password, "s3cret");
199 assert_eq!(c.database, "app");
200 }
201
202 const CAPTURED_LOG: &str = "\
2032026-07-04 8:47:29 0 [Note] mariadbd: ready for connections.
204Version: '11.4.12-MariaDB-ubu2404' socket: '/run/mysqld/mysqld.sock' port: 0 mariadb.org binary distribution
2052026-07-04 8:47:30 0 [Note] Server socket created on IP: '0.0.0.0', port: '3306'.
2062026-07-04 8:47:30 0 [Note] Server socket created on IP: '::', port: '3306'.
2072026-07-04 8:47:30 0 [Note] mariadbd: ready for connections.
208Version: '11.4.12-MariaDB-ubu2404' socket: '/run/mysqld/mysqld.sock' port: 3306 mariadb.org binary distribution";
209
210 struct FakeTarget(std::sync::Mutex<String>);
211 #[async_trait::async_trait]
212 impl WaitTarget for FakeTarget {
213 fn host(&self) -> &str {
214 "127.0.0.1"
215 }
216 fn mapped_port(&self, guest_port: u16) -> u16 {
217 guest_port
218 }
219 fn exposed_guest_ports(&self) -> Vec<u16> {
220 vec![PORT]
221 }
222 async fn current_logs(&self) -> String {
223 self.0.lock().unwrap().clone()
224 }
225 fn describe(&self) -> String {
226 "fake-mariadb".to_string()
227 }
228 }
229
230 #[tokio::test]
233 async fn temp_server_port_zero_line_does_not_signal_ready() {
234 let partial: String = CAPTURED_LOG.lines().take(2).collect::<Vec<_>>().join("\n");
235 let target = FakeTarget(std::sync::Mutex::new(partial));
236 let err = Wait::for_log_message(r".*port: 3306.*mariadb\.org binary distribution.*", 1)
237 .with_startup_timeout(Duration::from_millis(300))
238 .wait_until_ready(&target)
239 .await
240 .expect_err("the temp server's port: 0 line must not signal ready");
241 let _ = err;
242 }
243
244 #[tokio::test]
245 async fn only_the_real_servers_port_3306_line_signals_ready() {
246 let target = FakeTarget(std::sync::Mutex::new(CAPTURED_LOG.to_string()));
247 Wait::for_log_message(r".*port: 3306.*mariadb\.org binary distribution.*", 1)
248 .with_startup_timeout(Duration::from_secs(5))
249 .wait_until_ready(&target)
250 .await
251 .expect("the real server's port: 3306 line must signal ready");
252 }
253}