Skip to content

Commit

Permalink
feat(launcher): add test database initialization helping macros (#1135)
Browse files Browse the repository at this point in the history
* feat(launcher): add test database initialization macros

* fix(launcher): add missing serde dependency to 'database' feature

* chore(launcher): bump to v0.15.0

* chore(metrics-tools): fix cargo clippy
  • Loading branch information
rimrakhimov authored Dec 5, 2024
1 parent f4c6a78 commit a02d7de
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 8 deletions.
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

0 comments on commit a02d7de

Please sign in to comment.