Use testcontainers for Rust PostgreSQL tests
This commit is contained in:
@@ -27,3 +27,4 @@ uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
crank-mapping = { path = "../crank-mapping" }
|
||||
crank-test-support = { path = "../crank-test-support" }
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
use std::env;
|
||||
|
||||
use crank_community_mcp::session::{PostgresTransportSessionStore, TransportSessionStore};
|
||||
use crank_registry::PostgresPoolConfig;
|
||||
use sqlx::{
|
||||
Executor,
|
||||
postgres::{PgConnectOptions, PgPoolOptions},
|
||||
};
|
||||
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
use uuid::Uuid;
|
||||
|
||||
fn timestamp(value: &str) -> time::OffsetDateTime {
|
||||
time::OffsetDateTime::parse(value, &Rfc3339).unwrap()
|
||||
@@ -21,23 +15,8 @@ fn truncate_to_micros(value: OffsetDateTime) -> OffsetDateTime {
|
||||
|
||||
#[tokio::test]
|
||||
async fn postgres_transport_sessions_survive_store_reconnect() {
|
||||
let database_url = env::var("TEST_DATABASE_URL")
|
||||
.unwrap_or_else(|_| "postgres://crank:crank@127.0.0.1:15432/crank".to_owned());
|
||||
let admin_pool = PgPoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect(&database_url)
|
||||
.await
|
||||
.unwrap();
|
||||
let schema = format!("test_mcp_transport_{}", Uuid::now_v7().simple());
|
||||
|
||||
admin_pool
|
||||
.execute(sqlx::query(&format!("create schema {schema}")))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let connect_options = format!("{database_url}?options=-csearch_path%3D{schema}")
|
||||
.parse::<PgConnectOptions>()
|
||||
.unwrap();
|
||||
let database_url = crank_test_support::postgres_schema_url("test_mcp_transport").await;
|
||||
let connect_options = database_url.parse::<PgConnectOptions>().unwrap();
|
||||
let pool_config = PostgresPoolConfig::default();
|
||||
let store_a = PostgresTransportSessionStore::connect_with_options_and_pool_config(
|
||||
connect_options.clone(),
|
||||
@@ -80,23 +59,8 @@ async fn postgres_transport_sessions_survive_store_reconnect() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn postgres_transport_sessions_evict_expired_rows_on_read() {
|
||||
let database_url = env::var("TEST_DATABASE_URL")
|
||||
.unwrap_or_else(|_| "postgres://crank:crank@127.0.0.1:15432/crank".to_owned());
|
||||
let admin_pool = PgPoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect(&database_url)
|
||||
.await
|
||||
.unwrap();
|
||||
let schema = format!("test_mcp_transport_{}", Uuid::now_v7().simple());
|
||||
|
||||
admin_pool
|
||||
.execute(sqlx::query(&format!("create schema {schema}")))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let connect_options = format!("{database_url}?options=-csearch_path%3D{schema}")
|
||||
.parse::<PgConnectOptions>()
|
||||
.unwrap();
|
||||
let database_url = crank_test_support::postgres_schema_url("test_mcp_transport").await;
|
||||
let connect_options = database_url.parse::<PgConnectOptions>().unwrap();
|
||||
let store = PostgresTransportSessionStore::connect_with_options_and_pool_config(
|
||||
connect_options.clone(),
|
||||
PostgresPoolConfig::default(),
|
||||
|
||||
@@ -17,4 +17,5 @@ time.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
crank-test-support = { path = "../crank-test-support" }
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -1009,11 +1009,7 @@ fn usage_bucket_sql(bucket: crate::model::UsageBucket) -> &'static str {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
env,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crank_core::{
|
||||
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApiKeyHeaderAuthConfig,
|
||||
@@ -1917,21 +1913,13 @@ mod tests {
|
||||
|
||||
impl TestDatabase {
|
||||
async fn new() -> Self {
|
||||
let database_url = env::var("TEST_DATABASE_URL")
|
||||
.expect("TEST_DATABASE_URL must point to a reachable PostgreSQL database");
|
||||
let database_url = crank_test_support::postgres_database_url().await.to_owned();
|
||||
let admin_pool = PgPoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect(&database_url)
|
||||
.await
|
||||
.unwrap();
|
||||
let schema = format!(
|
||||
"test_registry_{}_{}",
|
||||
std::process::id(),
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
);
|
||||
let schema = crank_test_support::unique_schema_name("test_registry");
|
||||
|
||||
admin_pool
|
||||
.execute(sqlx::query(&format!("create schema {schema}")))
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "crank-test-support"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
sqlx.workspace = true
|
||||
testcontainers.workspace = true
|
||||
testcontainers-modules.workspace = true
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
uuid.workspace = true
|
||||
@@ -0,0 +1,110 @@
|
||||
use std::{
|
||||
process::{Command, Stdio},
|
||||
sync::{Mutex, OnceLock},
|
||||
};
|
||||
|
||||
use sqlx::{Connection, Executor, PgConnection};
|
||||
use testcontainers::{Container, ImageExt, runners::SyncRunner};
|
||||
use testcontainers_modules::postgres::Postgres;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
static POSTGRES: OnceCell<TestPostgres> = OnceCell::const_new();
|
||||
static CLEANUP_REGISTERED: OnceLock<()> = OnceLock::new();
|
||||
static CONTAINER_IDS: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
|
||||
struct TestPostgres {
|
||||
_container: Container<Postgres>,
|
||||
database_url: String,
|
||||
}
|
||||
|
||||
pub async fn postgres_database_url() -> &'static str {
|
||||
&test_postgres().await.database_url
|
||||
}
|
||||
|
||||
pub async fn postgres_schema_url(prefix: &str) -> String {
|
||||
let database_url = postgres_database_url().await;
|
||||
let schema = unique_schema_name(prefix);
|
||||
let mut connection = PgConnection::connect(database_url)
|
||||
.await
|
||||
.expect("test PostgreSQL container must accept admin connections");
|
||||
|
||||
connection
|
||||
.execute(sqlx::query(&format!("create schema {schema}")))
|
||||
.await
|
||||
.expect("test PostgreSQL schema must be created");
|
||||
|
||||
format!("{database_url}?options=-csearch_path%3D{schema}")
|
||||
}
|
||||
|
||||
pub fn unique_schema_name(prefix: &str) -> String {
|
||||
let normalized_prefix: String = prefix
|
||||
.chars()
|
||||
.map(|ch| {
|
||||
if ch.is_ascii_alphanumeric() || ch == '_' {
|
||||
ch.to_ascii_lowercase()
|
||||
} else {
|
||||
'_'
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
format!("{}_{}", normalized_prefix, uuid::Uuid::now_v7().simple())
|
||||
}
|
||||
|
||||
async fn test_postgres() -> &'static TestPostgres {
|
||||
POSTGRES
|
||||
.get_or_init(|| async {
|
||||
tokio::task::spawn_blocking(start_postgres)
|
||||
.await
|
||||
.expect("test PostgreSQL container startup task must complete")
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
fn start_postgres() -> TestPostgres {
|
||||
let container = Postgres::default()
|
||||
.with_db_name("crank")
|
||||
.with_user("crank")
|
||||
.with_password("crank")
|
||||
.with_tag("16-alpine")
|
||||
.start()
|
||||
.expect("test PostgreSQL container must start");
|
||||
register_container_cleanup(container.id().to_owned());
|
||||
let port = container
|
||||
.get_host_port_ipv4(5432)
|
||||
.expect("test PostgreSQL port must be mapped");
|
||||
|
||||
TestPostgres {
|
||||
_container: container,
|
||||
database_url: format!("postgres://crank:crank@127.0.0.1:{port}/crank"),
|
||||
}
|
||||
}
|
||||
|
||||
fn register_container_cleanup(container_id: String) {
|
||||
CLEANUP_REGISTERED.get_or_init(|| unsafe {
|
||||
libc::atexit(cleanup_test_containers);
|
||||
});
|
||||
CONTAINER_IDS
|
||||
.lock()
|
||||
.expect("test container id registry must be available")
|
||||
.push(container_id);
|
||||
}
|
||||
|
||||
extern "C" fn cleanup_test_containers() {
|
||||
let container_ids = CONTAINER_IDS
|
||||
.lock()
|
||||
.map(|ids| ids.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
if container_ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = Command::new("docker")
|
||||
.arg("rm")
|
||||
.arg("-f")
|
||||
.args(container_ids)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
}
|
||||
Reference in New Issue
Block a user