reuse db container between tests
Container is now never stopped. Since there's only ever going to be one, this is probably fine.
This commit is contained in:
parent
ee36efecad
commit
741268ef1e
1 changed files with 163 additions and 155 deletions
|
@ -1,11 +1,13 @@
|
||||||
use anyhow::Result;
|
use anyhow::{bail, Result};
|
||||||
use bollard::query_parameters::CreateImageOptionsBuilder;
|
use bollard::query_parameters::{CreateImageOptionsBuilder, ListContainersOptionsBuilder};
|
||||||
use bollard::secret::{
|
use bollard::secret::{
|
||||||
ContainerCreateBody, ContainerInspectResponse, ContainerState, CreateImageInfo, Health,
|
ContainerCreateBody, ContainerInspectResponse, ContainerState, CreateImageInfo, Health,
|
||||||
HealthConfig, HealthStatusEnum, HostConfig, PortBinding,
|
HealthConfig, HealthStatusEnum, HostConfig, PortBinding,
|
||||||
};
|
};
|
||||||
use bollard::Docker;
|
use bollard::Docker;
|
||||||
use futures_util::{FutureExt, StreamExt as _};
|
use futures_util::{FutureExt, StreamExt as _};
|
||||||
|
use rand::distr::slice::Choose;
|
||||||
|
use rand::{rng, Rng};
|
||||||
use sqlx::migrate::MigrateDatabase;
|
use sqlx::migrate::MigrateDatabase;
|
||||||
use sqlx::{Connection, PgConnection};
|
use sqlx::{Connection, PgConnection};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
@ -39,7 +41,6 @@ impl TestServer {
|
||||||
|
|
||||||
// TODO: allow per-test DBs in some cases
|
// TODO: allow per-test DBs in some cases
|
||||||
// let db = TestDb::spawn().await;
|
// let db = TestDb::spawn().await;
|
||||||
// TODO: share test server between test file, somehow?
|
|
||||||
let db = get_shared_db().await;
|
let db = get_shared_db().await;
|
||||||
let url = dbg!(db.get_url());
|
let url = dbg!(db.get_url());
|
||||||
|
|
||||||
|
@ -97,11 +98,6 @@ impl TestServer {
|
||||||
self.server_task_handle.abort();
|
self.server_task_handle.abort();
|
||||||
let _ = self.server_task_handle.await;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,14 +105,12 @@ impl TestServer {
|
||||||
const TEST_DB_IMAGE_NAME: &str = "postgres";
|
const TEST_DB_IMAGE_NAME: &str = "postgres";
|
||||||
const TEST_DB_SUPERUSER: &str = "postgres";
|
const TEST_DB_SUPERUSER: &str = "postgres";
|
||||||
const TEST_DB_SUPERUSER_PASS: &str = "password";
|
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 {
|
pub struct TestDb {
|
||||||
docker: Docker,
|
|
||||||
// image_id: String,
|
|
||||||
container: bollard::secret::ContainerInspectResponse,
|
container: bollard::secret::ContainerInspectResponse,
|
||||||
|
user: String,
|
||||||
|
pass: String,
|
||||||
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestDb {
|
impl TestDb {
|
||||||
|
@ -129,126 +123,148 @@ impl TestDb {
|
||||||
|
|
||||||
trace!("version: {version:?}");
|
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
|
let container_id;
|
||||||
if let Ok(image) = docker.inspect_image(TEST_DB_IMAGE_NAME).await {
|
if let Some(container) = found_containers.pop() {
|
||||||
image_id = Some(image.id.unwrap());
|
container_id = container.id.unwrap();
|
||||||
}
|
} else {
|
||||||
|
// build container
|
||||||
|
let mut image_id = None;
|
||||||
|
|
||||||
// build docker image from docker file
|
// check for image
|
||||||
// let mut image_id = None;
|
if let Ok(image) = docker.inspect_image(TEST_DB_IMAGE_NAME).await {
|
||||||
// {
|
image_id = Some(image.id.unwrap());
|
||||||
// let filename = "Dockerfile.db";
|
}
|
||||||
// let image_options = bollard::query_parameters::BuildImageOptionsBuilder::default()
|
|
||||||
// .dockerfile(filename)
|
|
||||||
// .rm(true)
|
|
||||||
// .build();
|
|
||||||
|
|
||||||
// let archive_bytes = {
|
// build docker image from docker file
|
||||||
// let mut archive = tar::Builder::new(Vec::new());
|
// let mut image_id = None;
|
||||||
// archive.append_path(filename).unwrap();
|
// {
|
||||||
// archive.into_inner().unwrap()
|
// 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(
|
// let archive_bytes = {
|
||||||
// image_options,
|
// let mut archive = tar::Builder::new(Vec::new());
|
||||||
// None,
|
// archive.append_path(filename).unwrap();
|
||||||
// Some(http_body_util::Either::Left(http_body_util::Full::new(
|
// archive.into_inner().unwrap()
|
||||||
// archive_bytes.into(),
|
// };
|
||||||
// ))),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// while let Some(msg) = image_build_stream.next().await {
|
// let mut image_build_stream = docker.build_image(
|
||||||
// info!("Message: {msg:?}");
|
// image_options,
|
||||||
|
// None,
|
||||||
|
// Some(http_body_util::Either::Left(http_body_util::Full::new(
|
||||||
|
// archive_bytes.into(),
|
||||||
|
// ))),
|
||||||
|
// );
|
||||||
|
|
||||||
// if let Ok(BuildInfo {
|
// while let Some(msg) = image_build_stream.next().await {
|
||||||
// aux: Some(ImageId { id: Some(id) }),
|
// info!("Message: {msg:?}");
|
||||||
// ..
|
|
||||||
// }) = msg
|
|
||||||
// {
|
|
||||||
// trace!("Image ID: {id}");
|
|
||||||
// image_id = Some(id);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// let image_id = image_id.expect("get image id for built docker image");
|
|
||||||
|
|
||||||
// pull image
|
// if let Ok(BuildInfo {
|
||||||
if image_id.is_none() {
|
// aux: Some(ImageId { id: Some(id) }),
|
||||||
let image_opts = CreateImageOptionsBuilder::new()
|
// ..
|
||||||
.from_image(TEST_DB_IMAGE_NAME)
|
// }) = msg
|
||||||
.build();
|
// {
|
||||||
|
// 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");
|
// pull image
|
||||||
let mut image_create_stream = docker.create_image(Some(image_opts), None, None);
|
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!(?image_opts, "pull image");
|
||||||
trace!("Message: {msg:?}");
|
let mut image_create_stream = docker.create_image(Some(image_opts), None, None);
|
||||||
|
|
||||||
if let Ok(CreateImageInfo { id: Some(id), .. }) = msg {
|
while let Some(msg) = image_create_stream.next().await {
|
||||||
trace!("Image ID: {id}");
|
trace!("Message: {msg:?}");
|
||||||
image_id = Some(id);
|
|
||||||
|
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
|
// create and start docker container
|
||||||
let container_id;
|
{
|
||||||
{
|
let container_config = ContainerCreateBody {
|
||||||
let container_config = ContainerCreateBody {
|
image: Some(image_id.clone()),
|
||||||
image: Some(image_id.clone()),
|
exposed_ports: Some([("5432/tcp".to_string(), [].into())].into()),
|
||||||
exposed_ports: Some([("5432/tcp".to_string(), [].into())].into()),
|
host_config: Some(HostConfig {
|
||||||
host_config: Some(HostConfig {
|
port_bindings: Some(
|
||||||
port_bindings: Some(
|
[(
|
||||||
[(
|
"5432/tcp".to_string(),
|
||||||
"5432/tcp".to_string(),
|
Some(vec![PortBinding {
|
||||||
Some(vec![PortBinding {
|
host_ip: Some("127.0.0.1".to_string()),
|
||||||
host_ip: Some("127.0.0.1".to_string()),
|
host_port: None, // auto-assign
|
||||||
host_port: None, // auto-assign
|
}]),
|
||||||
}]),
|
)]
|
||||||
)]
|
.into(),
|
||||||
.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()
|
..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");
|
trace!("create container");
|
||||||
bollard::secret::ContainerCreateResponse {
|
bollard::secret::ContainerCreateResponse {
|
||||||
id: container_id,
|
id: container_id,
|
||||||
..
|
..
|
||||||
} = docker
|
} = docker
|
||||||
.create_container(
|
.create_container(
|
||||||
None::<bollard::query_parameters::CreateContainerOptions>,
|
None::<bollard::query_parameters::CreateContainerOptions>,
|
||||||
container_config,
|
container_config,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
trace!("start container");
|
trace!("start container");
|
||||||
docker
|
docker
|
||||||
.start_container(
|
.start_container(
|
||||||
&container_id,
|
&container_id,
|
||||||
None::<bollard::query_parameters::StartContainerOptions>,
|
None::<bollard::query_parameters::StartContainerOptions>,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait for container to be started
|
// wait for container to be started
|
||||||
|
@ -284,30 +300,48 @@ impl TestDb {
|
||||||
sleep(Duration::from_secs(2)).await;
|
sleep(Duration::from_secs(2)).await;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let lowercase_alpha = Choose::new(b"abcdefghijklmnopqrstuvwxyz").unwrap();
|
||||||
|
|
||||||
let db = TestDb {
|
let db = TestDb {
|
||||||
docker,
|
|
||||||
// image_id,
|
|
||||||
container,
|
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
|
// setup app db
|
||||||
{
|
{
|
||||||
let mut conn = PgConnection::connect(&db.get_superuser_url())
|
let mut conn = PgConnection::connect(&dbg!(db.get_superuser_url()))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// create application user
|
// create application user
|
||||||
// Note: In general, string formtting a query is bad practice, but it's required here.
|
// Note: In general, string formtting a query is bad practice, but it's required here.
|
||||||
sqlx::query(&format!(
|
dbg!(
|
||||||
"CREATE USER {TEST_DB_APP_USER} WITH PASSWORD '{TEST_DB_APP_PASS}';"
|
sqlx::query(&dbg!(format!(
|
||||||
))
|
"CREATE USER {} WITH PASSWORD '{}';",
|
||||||
.execute(&mut conn)
|
db.user, db.pass
|
||||||
.await
|
)))
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// grant privs to app user
|
// grant privs to app user
|
||||||
// Note: In general, string formtting a query is bad practice, but it's required here.
|
// 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)
|
.execute(&mut conn)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -348,7 +382,10 @@ impl TestDb {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let host_ip = binding.host_ip.as_ref().unwrap().clone();
|
let host_ip = binding.host_ip.as_ref().unwrap().clone();
|
||||||
let host_port = binding.host_port.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.
|
/// 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();
|
let host_port = binding.host_port.as_ref().unwrap().clone();
|
||||||
format!("postgres://{TEST_DB_SUPERUSER}:{TEST_DB_SUPERUSER_PASS}@{host_ip}:{host_port}/postgres")
|
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::<bollard::container::StopContainerOptions>,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
self.docker
|
|
||||||
.remove_container(
|
|
||||||
&self.container.id.as_deref().unwrap(),
|
|
||||||
None::<bollard::query_parameters::RemoveContainerOptions>,
|
|
||||||
)
|
|
||||||
.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::<bollard::query_parameters::RemoveImageOptions>,
|
|
||||||
// None,
|
|
||||||
// )
|
|
||||||
// .await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue