From 741268ef1edb3772e9ead386314a4732b1cd0599 Mon Sep 17 00:00:00 2001 From: azdle Date: Tue, 22 Jul 2025 12:08:37 -0500 Subject: [PATCH] reuse db container between tests Container is now never stopped. Since there's only ever going to be one, this is probably fine. --- tests/fixture/mod.rs | 318 ++++++++++++++++++++++--------------------- 1 file changed, 163 insertions(+), 155 deletions(-) diff --git a/tests/fixture/mod.rs b/tests/fixture/mod.rs index 5a4ef64..405c233 100644 --- a/tests/fixture/mod.rs +++ b/tests/fixture/mod.rs @@ -1,11 +1,13 @@ -use anyhow::Result; -use bollard::query_parameters::CreateImageOptionsBuilder; +use anyhow::{bail, Result}; +use bollard::query_parameters::{CreateImageOptionsBuilder, ListContainersOptionsBuilder}; use bollard::secret::{ ContainerCreateBody, ContainerInspectResponse, ContainerState, CreateImageInfo, Health, HealthConfig, HealthStatusEnum, HostConfig, PortBinding, }; use bollard::Docker; use futures_util::{FutureExt, StreamExt as _}; +use rand::distr::slice::Choose; +use rand::{rng, Rng}; use sqlx::migrate::MigrateDatabase; use sqlx::{Connection, PgConnection}; use std::net::SocketAddr; @@ -39,7 +41,6 @@ impl TestServer { // TODO: allow per-test DBs in some cases // let db = TestDb::spawn().await; - // TODO: share test server between test file, somehow? let db = get_shared_db().await; let url = dbg!(db.get_url()); @@ -97,11 +98,6 @@ impl TestServer { self.server_task_handle.abort(); let _ = self.server_task_handle.await; - // Shutdown server if this is the last one. - if let Some(db) = Arc::into_inner(self.db) { - db.stop().await; - } - Ok(()) } } @@ -109,14 +105,12 @@ impl TestServer { const TEST_DB_IMAGE_NAME: &str = "postgres"; const TEST_DB_SUPERUSER: &str = "postgres"; const TEST_DB_SUPERUSER_PASS: &str = "password"; -const TEST_DB_APP_USER: &str = "app"; -const TEST_DB_APP_PASS: &str = "apppass"; -const TEST_DB_APP_NAME: &str = "ztoa"; pub struct TestDb { - docker: Docker, - // image_id: String, container: bollard::secret::ContainerInspectResponse, + user: String, + pass: String, + name: String, } impl TestDb { @@ -129,126 +123,148 @@ impl TestDb { trace!("version: {version:?}"); - let mut image_id = None; + // check for existing container + let mut found_containers = docker + .list_containers(Some( + ListContainersOptionsBuilder::new() + .filters( + &([( + "label".to_string(), + vec!["zero-to-axum_test-db".to_string()], + )] + .into()), + ) + .build(), + )) + .await + .unwrap(); - // check for image - if let Ok(image) = docker.inspect_image(TEST_DB_IMAGE_NAME).await { - image_id = Some(image.id.unwrap()); - } + let container_id; + if let Some(container) = found_containers.pop() { + container_id = container.id.unwrap(); + } else { + // build container + let mut image_id = None; - // build docker image from docker file - // let mut image_id = None; - // { - // let filename = "Dockerfile.db"; - // let image_options = bollard::query_parameters::BuildImageOptionsBuilder::default() - // .dockerfile(filename) - // .rm(true) - // .build(); + // check for image + if let Ok(image) = docker.inspect_image(TEST_DB_IMAGE_NAME).await { + image_id = Some(image.id.unwrap()); + } - // let archive_bytes = { - // let mut archive = tar::Builder::new(Vec::new()); - // archive.append_path(filename).unwrap(); - // archive.into_inner().unwrap() - // }; + // build docker image from docker file + // let mut image_id = None; + // { + // let filename = "Dockerfile.db"; + // let image_options = bollard::query_parameters::BuildImageOptionsBuilder::default() + // .dockerfile(filename) + // .rm(true) + // .build(); - // let mut image_build_stream = docker.build_image( - // image_options, - // None, - // Some(http_body_util::Either::Left(http_body_util::Full::new( - // archive_bytes.into(), - // ))), - // ); + // let archive_bytes = { + // let mut archive = tar::Builder::new(Vec::new()); + // archive.append_path(filename).unwrap(); + // archive.into_inner().unwrap() + // }; - // while let Some(msg) = image_build_stream.next().await { - // info!("Message: {msg:?}"); + // let mut image_build_stream = docker.build_image( + // image_options, + // None, + // Some(http_body_util::Either::Left(http_body_util::Full::new( + // archive_bytes.into(), + // ))), + // ); - // if let Ok(BuildInfo { - // aux: Some(ImageId { id: Some(id) }), - // .. - // }) = msg - // { - // trace!("Image ID: {id}"); - // image_id = Some(id); - // } - // } - // } - // let image_id = image_id.expect("get image id for built docker image"); + // while let Some(msg) = image_build_stream.next().await { + // info!("Message: {msg:?}"); - // pull image - if image_id.is_none() { - let image_opts = CreateImageOptionsBuilder::new() - .from_image(TEST_DB_IMAGE_NAME) - .build(); + // if let Ok(BuildInfo { + // aux: Some(ImageId { id: Some(id) }), + // .. + // }) = msg + // { + // trace!("Image ID: {id}"); + // image_id = Some(id); + // } + // } + // } + // let image_id = image_id.expect("get image id for built docker image"); - trace!(?image_opts, "pull image"); - let mut image_create_stream = docker.create_image(Some(image_opts), None, None); + // pull image + if image_id.is_none() { + let image_opts = CreateImageOptionsBuilder::new() + .from_image(TEST_DB_IMAGE_NAME) + .build(); - while let Some(msg) = image_create_stream.next().await { - trace!("Message: {msg:?}"); + trace!(?image_opts, "pull image"); + let mut image_create_stream = docker.create_image(Some(image_opts), None, None); - if let Ok(CreateImageInfo { id: Some(id), .. }) = msg { - trace!("Image ID: {id}"); - image_id = Some(id); + while let Some(msg) = image_create_stream.next().await { + trace!("Message: {msg:?}"); + + if let Ok(CreateImageInfo { id: Some(id), .. }) = msg { + trace!("Image ID: {id}"); + image_id = Some(id); + } } } - } - let image_id = image_id.expect("get image id for built docker image"); + let image_id = image_id.expect("get image id for built docker image"); - // create and start docker container - let container_id; - { - let container_config = ContainerCreateBody { - image: Some(image_id.clone()), - exposed_ports: Some([("5432/tcp".to_string(), [].into())].into()), - host_config: Some(HostConfig { - port_bindings: Some( - [( - "5432/tcp".to_string(), - Some(vec![PortBinding { - host_ip: Some("127.0.0.1".to_string()), - host_port: None, // auto-assign - }]), - )] - .into(), - ), + // create and start docker container + { + let container_config = ContainerCreateBody { + image: Some(image_id.clone()), + exposed_ports: Some([("5432/tcp".to_string(), [].into())].into()), + host_config: Some(HostConfig { + port_bindings: Some( + [( + "5432/tcp".to_string(), + Some(vec![PortBinding { + host_ip: Some("127.0.0.1".to_string()), + host_port: None, // auto-assign + }]), + )] + .into(), + ), + ..Default::default() + }), + env: Some(vec![ + format!("POSTGRES_USER={TEST_DB_SUPERUSER}"), + format!("POSTGRES_PASSWORD={TEST_DB_SUPERUSER_PASS}"), + ]), + healthcheck: Some(HealthConfig { + test: Some(vec!["pg_isready -U postgres || exit 1".to_string()]), + // nano seconds + interval: Some(1 * 1000 * 1000 * 1000), + timeout: Some(5 * 1000 * 1000 * 1000), + retries: Some(5 * 1000 * 1000 * 1000), + ..Default::default() + }), + labels: Some([("zero-to-axum_test-db".to_string(), String::new())].into()), ..Default::default() - }), - env: Some(vec![ - format!("POSTGRES_USER={TEST_DB_SUPERUSER}"), - format!("POSTGRES_PASSWORD={TEST_DB_SUPERUSER_PASS}"), - ]), - healthcheck: Some(HealthConfig { - test: Some(vec!["pg_isready -U postgres || exit 1".to_string()]), - // nano seconds - interval: Some(1 * 1000 * 1000 * 1000), - timeout: Some(5 * 1000 * 1000 * 1000), - retries: Some(5 * 1000 * 1000 * 1000), - ..Default::default() - }), - ..Default::default() - }; + }; - trace!("create container"); - bollard::secret::ContainerCreateResponse { - id: container_id, - .. - } = docker - .create_container( - None::, - container_config, - ) - .await - .unwrap(); + trace!("create container"); + bollard::secret::ContainerCreateResponse { + id: container_id, + .. + } = docker + .create_container( + None::, + container_config, + ) + .await + .unwrap(); - trace!("start container"); - docker - .start_container( - &container_id, - None::, - ) - .await - .unwrap(); + trace!("start container"); + docker + .start_container( + &container_id, + None::, + ) + .await + .unwrap(); + } } // wait for container to be started @@ -284,30 +300,48 @@ impl TestDb { sleep(Duration::from_secs(2)).await; }; + let lowercase_alpha = Choose::new(b"abcdefghijklmnopqrstuvwxyz").unwrap(); + let db = TestDb { - docker, - // image_id, container, + user: rng() + .sample_iter(lowercase_alpha) + .take(16) + .map(|i| char::from(*i)) + .collect(), + pass: rng() + .sample_iter(lowercase_alpha) + .take(32) + .map(|i| char::from(*i)) + .collect(), + name: rng() + .sample_iter(lowercase_alpha) + .take(8) + .map(|i| char::from(*i)) + .collect(), }; // setup app db { - let mut conn = PgConnection::connect(&db.get_superuser_url()) + let mut conn = PgConnection::connect(&dbg!(db.get_superuser_url())) .await .unwrap(); // create application user // Note: In general, string formtting a query is bad practice, but it's required here. - sqlx::query(&format!( - "CREATE USER {TEST_DB_APP_USER} WITH PASSWORD '{TEST_DB_APP_PASS}';" - )) - .execute(&mut conn) - .await + dbg!( + sqlx::query(&dbg!(format!( + "CREATE USER {} WITH PASSWORD '{}';", + db.user, db.pass + ))) + .execute(&mut conn) + .await + ) .unwrap(); // grant privs to app user // Note: In general, string formtting a query is bad practice, but it's required here. - sqlx::query(&format!("ALTER USER {TEST_DB_APP_USER} CREATEDB;")) + sqlx::query(&format!("ALTER USER {} CREATEDB;", db.user)) .execute(&mut conn) .await .unwrap(); @@ -348,7 +382,10 @@ impl TestDb { .unwrap(); let host_ip = binding.host_ip.as_ref().unwrap().clone(); let host_port = binding.host_port.as_ref().unwrap().clone(); - format!("postgres://{TEST_DB_APP_USER}:{TEST_DB_APP_PASS}@{host_ip}:{host_port}/{TEST_DB_APP_NAME}") + format!( + "postgres://{}:{}@{host_ip}:{host_port}/{}", + self.user, self.pass, self.name + ) } /// Get the superuser-authenticated URL for accessing the `postgres` db from the host. @@ -372,33 +409,4 @@ impl TestDb { let host_port = binding.host_port.as_ref().unwrap().clone(); format!("postgres://{TEST_DB_SUPERUSER}:{TEST_DB_SUPERUSER_PASS}@{host_ip}:{host_port}/postgres") } - - pub async fn stop(&self) { - self.docker - .stop_container( - &self.container.id.as_deref().unwrap(), - #[allow(deprecated)] // is deprecated, but also required - None::, - ) - .await - .unwrap(); - - self.docker - .remove_container( - &self.container.id.as_deref().unwrap(), - None::, - ) - .await - .unwrap(); - - // // TODO: images seem like they might be reused, this is probably a bad idea, but it seems to work? - // let _ = self - // .docker - // .remove_image( - // &self.image_id, - // None::, - // None, - // ) - // .await; - } }