use std::sync::{
OnceLock,
atomic::{AtomicU32, Ordering},
};
use toasty::{Db, migration::MigrationSet, schema::ModelSet};
pub const TEST_DATABASE_URL: &str = "TABLO_TEST_DATABASE_URL";
pub async fn test_db(models: ModelSet) -> Db {
let mut builder = Db::builder();
builder.models(models);
let url = match std::env::var(TEST_DATABASE_URL) {
Ok(url) => {
builder.table_name_prefix(&table_prefix());
url
}
Err(_) => "sqlite::memory:".to_string(),
};
let db = builder.connect(&url).await.expect("connect");
db.push_schema().await.expect("push_schema");
db
}
pub async fn migrated_db(models: ModelSet, migrations: &MigrationSet) -> Db {
let url = match std::env::var(TEST_DATABASE_URL) {
Ok(url) => isolated(models.clone(), &url).await,
Err(_) => "sqlite::memory:".to_string(),
};
let mut builder = Db::builder();
builder.models(models);
let db = builder.connect(&url).await.expect("connect");
migrations.apply(&db).await.expect("apply the migrations");
db
}
async fn isolated(models: ModelSet, url: &str) -> String {
let (scheme, _) = url.split_once(':').unwrap_or((url, ""));
let create = match scheme {
"postgres" | "postgresql" => "SCHEMA",
"mysql" | "mariadb" => "DATABASE",
_ => return url.to_string(),
};
let name = table_prefix().trim_end_matches('_').to_string();
let mut builder = Db::builder();
builder.models(models);
let mut admin = builder.connect(url).await.expect("connect");
toasty::sql::statement(format!("CREATE {create} {name}"))
.exec(&mut admin)
.await
.expect("create the test's namespace");
if create == "SCHEMA" {
let separator = if url.contains('?') { '&' } else { '?' };
format!("{url}{separator}options=-c%20search_path%3D{name}")
} else {
with_database(url, &name)
}
}
fn with_database(url: &str, name: &str) -> String {
let (base, query) = match url.split_once('?') {
Some((base, query)) => (base, Some(query)),
None => (url, None),
};
let authority_start = base.find("://").map_or(0, |at| at + 3);
let path_start = base[authority_start..]
.find('/')
.map_or(base.len(), |at| authority_start + at);
let mut out = format!("{}/{name}", &base[..path_start]);
if let Some(query) = query {
out.push('?');
out.push_str(query);
}
out
}
fn table_prefix() -> String {
static RUN: OnceLock<u32> = OnceLock::new();
static NEXT: AtomicU32 = AtomicU32::new(0);
let run = RUN.get_or_init(|| {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
now.as_secs() as u32 ^ now.subsec_nanos()
});
let call = NEXT.fetch_add(1, Ordering::Relaxed);
format!("t{run:08x}{call}_")
}
#[cfg(test)]
mod tests {
use super::with_database;
#[test]
fn a_mysql_url_names_the_tests_database() {
assert_eq!(
with_database("mysql://root:pw@localhost:3306/tablo", "t1"),
"mysql://root:pw@localhost:3306/t1"
);
assert_eq!(
with_database("mysql://localhost?ssl-mode=disabled", "t1"),
"mysql://localhost/t1?ssl-mode=disabled"
);
}
}