Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(launcher): add test database initialization helping macros #1135

Merged
merged 7 commits into from
Dec 5, 2024
3 changes: 2 additions & 1 deletion libs/blockscout-service-launcher/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "blockscout-service-launcher"
version = "0.14.0"
version = "0.15.0"
description = "Allows to launch blazingly fast blockscout rust services"
license = "MIT"
repository = "https://github.com/blockscout/blockscout-rs"
Expand Down Expand Up @@ -89,6 +89,7 @@ tracing = [
database = [
"dep:anyhow",
"dep:cfg-if",
"dep:serde",
"dep:tracing",
"dep:url",
]
Expand Down
133 changes: 128 additions & 5 deletions libs/blockscout-service-launcher/src/test_database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ use crate::database::{
};
use std::{ops::Deref, sync::Arc};

/// Postgres supports maximum 63 symbols.
/// All exceeding symbols are truncated by the database.
const MAX_DATABASE_NAME_LEN: usize = 63;

/// A length of the hex encoded hash of database name
/// when the original exceeds [`MAX_DATABASE_NAME_LEN`]
const HASH_SUFFIX_STRING_LEN: usize = 8;

#[derive(Clone, Debug)]
pub struct TestDbGuard {
conn_with_db: Arc<DatabaseConnection>,
Expand All @@ -20,9 +28,7 @@ impl TestDbGuard {
let conn_without_db = Database::connect(&base_db_url)
.await
.expect("Connection to postgres (without database) failed");
// We use a hash, as the name itself may be quite long and be trimmed.
// Postgres DB name should be 63 symbols max.
let db_name = format!("_{:x}", keccak_hash::keccak(db_name))[..63].to_string();
let db_name = Self::preprocess_database_name(db_name);
let mut guard = TestDbGuard {
conn_with_db: Arc::new(DatabaseConnection::Disconnected),
conn_without_db: Arc::new(conn_without_db),
Expand All @@ -35,6 +41,35 @@ impl TestDbGuard {
guard
}

/// Creates a new test database helper with a unique name.
///
/// This function initializes a test database, where the database name is constructed
/// as a concatenation of the provided `prefix_name`, `file`, `line`, and `column` arguments.
/// It ensures that the generated database name is unique to the location in the code
/// where this function is called.
///
/// # Arguments
///
/// - `prefix_name`: A custom prefix for the database name.
/// - `file`: The file name where this function is invoked. Must be the result of the `file!` macro.
/// - `line`: The line number where this function is invoked. Must be the result of the `line!` macro.
/// - `column`: The column number where this function is invoked. Must be the result of the `column!` macro.
///
/// # Example
///
/// ```text
/// let db_guard = TestDbGuard::new_with_metadata::<Migrator>("test_db", file!(), line!(), column!()).await;
/// ```
pub async fn new_with_metadata<Migrator: MigratorTrait>(
prefix_name: &str,
file: &str,
line: u32,
column: u32,
) -> Self {
let db_name = format!("{prefix_name}_{file}_{line}_{column}");
Self::new::<Migrator>(db_name.as_str()).await
}

pub fn client(&self) -> Arc<DatabaseConnection> {
self.conn_with_db.clone()
}
Expand Down Expand Up @@ -71,7 +106,7 @@ impl TestDbGuard {
tracing::info!(name = db_name, "creating database");
db.execute(Statement::from_string(
db.get_database_backend(),
format!("CREATE DATABASE {db_name}"),
format!("CREATE DATABASE \"{db_name}\""),
))
.await?;
Ok(())
Expand All @@ -81,7 +116,7 @@ impl TestDbGuard {
tracing::info!(name = db_name, "dropping database");
db.execute(Statement::from_string(
db.get_database_backend(),
format!("DROP DATABASE IF EXISTS {db_name} WITH (FORCE)"),
format!("DROP DATABASE IF EXISTS \"{db_name}\" WITH (FORCE)"),
))
.await?;
Ok(())
Expand All @@ -92,6 +127,21 @@ impl TestDbGuard {
.await
.expect("Database migration failed");
}

/// Strips given database name if the one is too long to be supported.
/// To differentiate the resultant name with other similar prefixes,
/// a 4-bytes hash of the original name is added at the end.
fn preprocess_database_name(name: &str) -> String {
if name.len() <= MAX_DATABASE_NAME_LEN {
return name.to_string();
}

let hash = &format!("{:x}", keccak_hash::keccak(name))[..HASH_SUFFIX_STRING_LEN];
format!(
"{}-{hash}",
&name[..MAX_DATABASE_NAME_LEN - HASH_SUFFIX_STRING_LEN - 1]
)
}
}

impl Deref for TestDbGuard {
Expand All @@ -100,3 +150,76 @@ impl Deref for TestDbGuard {
&self.conn_with_db
}
}

/// Generates a unique database name for use in tests.
///
/// This macro creates a database name based on the file name, line number, and column number
/// of the macro invocation. Optionally, a custom prefix can be appended for added specificity,
/// which is useful in scenarios like parameterized tests.
///
/// For more details on usage and examples, see the [`database!`](macro.database.html) macro.
///
/// # Arguments
///
/// - `custom_prefix` (optional): A custom string to append to the database name.
#[macro_export]
macro_rules! database_name {
() => {
format!("{}_{}_{}", file!(), line!(), column!())
};
($custom_prefix:expr) => {
format!("{}_{}_{}_{}", $custom_prefix, file!(), line!(), column!())
};
}
pub use database_name;

/// Initializes a test database for use in tests.
///
/// This macro simplifies setting up a database by automatically generating a database name
/// based on the location where the function is defined. It eliminates the need to manually
/// specify the test case name for the database name.
///
/// # Usage
///
/// The macro can be used within a test as follows:
/// ```text
/// use blockscout_service_launcher::test_database::database;
///
/// #[tokio::test]
/// async fn test() {
/// let db_guard = database!(migration_crate);
/// // Perform operations with the database...
/// }
/// ```
///
/// The `migration_crate` parameter refers to the migration crate associated with the database.
///
/// # Parameterized Tests
///
/// **Note:** When using this macro with [`rstest` parameterized test cases](https://docs.rs/rstest/latest/rstest/attr.rstest.html#test-parametrized-cases),
/// the same database name will be used for all test cases. To avoid conflicts, you need to provide
/// a meaningful prefix explicitly, as demonstrated below:
///
/// ```text
/// #[tokio::test]
/// async fn test_with_prefix() {
/// let db_guard = database!(migration_crate, "custom_prefix");
/// // Perform operations with the database...
/// }
/// ```
#[macro_export]
macro_rules! database {
($migration_crate:ident) => {{
$crate::test_database::TestDbGuard::new::<$migration_crate::Migrator>(
&$crate::test_database::database_name!(),
)
.await
}};
($migration_crate:ident, $custom_prefix:expr) => {{
$crate::test_database::TestDbGuard::new::<$migration_crate::Migrator>(
$crate::test_database::database_name!($custom_prefix),
)
.await
}};
}
pub use database;
4 changes: 2 additions & 2 deletions libs/metrics-tools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub struct Interval<'a> {
discarded: bool,
}

impl<'a> Interval<'a> {
impl Interval<'_> {
/// Get current time of the interval without recording.
pub fn elapsed_from_start(&self) -> Duration {
self.start_time.elapsed()
Expand All @@ -62,7 +62,7 @@ impl<'a> Interval<'a> {
}
}

impl<'a> Drop for Interval<'a> {
impl Drop for Interval<'_> {
fn drop(&mut self) {
if !self.discarded {
self.recorder.add_time(self.elapsed_from_start())
Expand Down
Loading