use std::sync::atomic::{AtomicUsize, Ordering};
use crate::database::{AuthLevel, DatabaseConfig, Db, is_transaction_conflict};
mod idp;
pub use idp::{TEST_CLIENT_ID, TestIdentityProvider, TokenSpec};
pub const SERVER_URL_ENV: &str = "TEST_SURREAL_URL";
pub const USER_ENV: &str = "TEST_SURREAL_USER";
pub const PASSWORD_ENV: &str = "TEST_SURREAL_PASS";
pub const DEFAULT_SERVER_URL: &str = crate::database::DEFAULT_URL;
pub const NAMESPACE: &str = "slave_test";
pub struct TestDatabase {
config: DatabaseConfig,
}
impl TestDatabase {
pub fn start() -> Self {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let var =
|key: &str, default: &str| std::env::var(key).unwrap_or_else(|_| default.to_owned());
let config = DatabaseConfig {
url: var(SERVER_URL_ENV, DEFAULT_SERVER_URL),
namespace: NAMESPACE.to_owned(),
database: format!("test_{}_{n}", std::process::id()),
username: Some(var(USER_ENV, "root")),
password: Some(var(PASSWORD_ENV, "root")),
auth_level: AuthLevel::Root,
};
admin(
&config,
&format!("DEFINE DATABASE IF NOT EXISTS {}", config.database),
)
.unwrap_or_else(|err| {
panic!(
"cannot reach the test database server {SERVER_URL_ENV}={}: {err} \
(run `surreal start --user root --pass root` or set {SERVER_URL_ENV})",
config.url
)
});
Self { config }
}
pub fn config(&self) -> &DatabaseConfig {
&self.config
}
pub fn envs(&self) -> Vec<(&'static str, String)> {
self.config.envs()
}
pub async fn connect(&self) -> Db {
crate::database::connect(&self.config)
.await
.expect("connect to the test database")
}
pub async fn migrate(&self, dir: &std::path::Path) -> Db {
let db = self.connect().await;
crate::database::migrations::apply(&db, dir)
.await
.expect("apply migrations");
db
}
}
impl Drop for TestDatabase {
fn drop(&mut self) {
let statement = format!("REMOVE DATABASE IF EXISTS {}", self.config.database);
if let Err(err) = admin(&self.config, &statement) {
eprintln!(
"warning: could not remove test database {}: {err}",
self.config.database
);
}
}
}
fn admin(config: &DatabaseConfig, statement: &str) -> Result<(), String> {
let config = config.clone();
let statement = statement.to_owned();
std::thread::spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|err| err.to_string())?;
runtime.block_on(async {
let mut attempt = 1;
loop {
let result = async {
let db = crate::database::connect(&config).await?;
db.query(statement.as_str()).await?.check()?;
crate::Result::Ok(())
}
.await;
match result {
Err(crate::Error::Database(err))
if is_transaction_conflict(&err) && attempt < 10 =>
{
attempt += 1;
tokio::time::sleep(std::time::Duration::from_millis(20 * attempt as u64))
.await;
}
result => return result.map_err(|err| err.to_string()),
}
}
})
})
.join()
.map_err(|_| "the database thread panicked".to_owned())?
}