Skip to content

Commit

Permalink
Merge pull request #627 from blockscout/lok52/launcher-test-utils
Browse files Browse the repository at this point in the history
Add launcher test utils
  • Loading branch information
lok52 authored Oct 16, 2023
2 parents 265d3fa + 02ccfdc commit 1521844
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 8 deletions.
14 changes: 13 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.8.0"
version = "0.9.0"
description = "Allows to launch blazingly fast blockscout rust services"
license = "MIT"
repository = "https://github.com/blockscout/blockscout-rs"
Expand All @@ -17,9 +17,11 @@ anyhow = { version = "1.0", optional = true }
config = { version = "0.13", optional = true }
futures = { version = "0.3", optional = true }
cfg-if = { version = "1.0.0", optional = true }
keccak-hash = { version = "0.10.0", optional = true }
opentelemetry = { version = "0.19", optional = true }
opentelemetry-jaeger = { version = "0.18", features = ["rt-tokio"], optional = true }
prometheus = { version = "0.13", optional = true }
reqwest = { version = "0.11", features = ["json"], optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
tokio = { version = "1", optional = true }
tonic = { version = ">= 0.8, < 0.10", optional = true }
Expand Down Expand Up @@ -85,3 +87,13 @@ database-0_10 = [
"dep:sea-orm-0_10",
"dep:sea-orm-migration-0_10",
]

test-server = [
"launcher",
"dep:reqwest",
]

test-database = [
"database",
"dep:keccak-hash",
]
12 changes: 6 additions & 6 deletions libs/blockscout-service-launcher/src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ use std::str::FromStr;

cfg_if::cfg_if! {
if #[cfg(feature = "database-0_12")] {
use sea_orm_0_12::{ConnectOptions, ConnectionTrait, Database, DatabaseBackend, Statement};
use sea_orm_migration_0_12::MigratorTrait;
pub use sea_orm_0_12::{ConnectOptions, ConnectionTrait, Database, DatabaseBackend, Statement, DatabaseConnection, DbErr};
pub use sea_orm_migration_0_12::MigratorTrait;
} else if #[cfg(feature = "database-0_11")] {
use sea_orm_0_11::{ConnectOptions, ConnectionTrait, Database, DatabaseBackend, Statement};
use sea_orm_migration_0_11::MigratorTrait;
pub use sea_orm_0_11::{ConnectOptions, ConnectionTrait, Database, DatabaseBackend, Statement, DatabaseConnection, DbErr};
pub use sea_orm_migration_0_11::MigratorTrait;
} else if #[cfg(feature = "database-0_10")] {
use sea_orm_0_10::{ConnectOptions, ConnectionTrait, Database, DatabaseBackend, Statement};
use sea_orm_migration_0_10::MigratorTrait;
pub use sea_orm_0_10::{ConnectOptions, ConnectionTrait, Database, DatabaseBackend, Statement, DatabaseConnection, DbErr};
pub use sea_orm_migration_0_10::MigratorTrait;
} else {
compile_error!(
"one of the features ['database-0_12', 'database-0_11', 'database-0_10'] \
Expand Down
6 changes: 6 additions & 0 deletions libs/blockscout-service-launcher/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@ pub mod launcher;

#[cfg(feature = "tracing")]
pub mod tracing;

#[cfg(feature = "test-server")]
pub mod test_server;

#[cfg(feature = "test-database")]
pub mod test_database;
99 changes: 99 additions & 0 deletions libs/blockscout-service-launcher/src/test_database.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use crate::database::{
ConnectionTrait, Database, DatabaseConnection, DbErr, MigratorTrait, Statement,
};
use std::{ops::Deref, sync::Arc};

#[derive(Clone, Debug)]
pub struct TestDbGuard {
conn_with_db: Arc<DatabaseConnection>,
conn_without_db: Arc<DatabaseConnection>,
base_db_url: String,
db_name: String,
}

impl TestDbGuard {
pub async fn new<Migrator: MigratorTrait>(db_name: &str) -> Self {
let base_db_url = std::env::var("DATABASE_URL")
.expect("Database url must be set to initialize a test database");
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.
let db_name = format!("_{:x}", keccak_hash::keccak(db_name));
let mut guard = TestDbGuard {
conn_with_db: Arc::new(DatabaseConnection::Disconnected),
conn_without_db: Arc::new(conn_without_db),
base_db_url,
db_name,
};

guard.init_database().await;
guard.run_migrations::<Migrator>().await;
guard
}

pub fn client(&self) -> Arc<DatabaseConnection> {
self.conn_with_db.clone()
}

pub fn db_url(&self) -> String {
format!("{}/{}", self.base_db_url, self.db_name)
}

async fn init_database(&mut self) {
// Create database
self.drop_database().await;
self.create_database().await;

let db_url = self.db_url();
let conn_with_db = Database::connect(&db_url)
.await
.expect("Connection to postgres (with database) failed");
self.conn_with_db = Arc::new(conn_with_db);
}

pub async fn drop_database(&self) {
Self::drop_database_internal(&self.conn_without_db, &self.db_name)
.await
.expect("Database drop failed");
}

async fn create_database(&self) {
Self::create_database_internal(&self.conn_without_db, &self.db_name)
.await
.expect("Database creation failed");
}

async fn create_database_internal(db: &DatabaseConnection, db_name: &str) -> Result<(), DbErr> {
tracing::info!(name = db_name, "creating database");
db.execute(Statement::from_string(
db.get_database_backend(),
format!("CREATE DATABASE {db_name}"),
))
.await?;
Ok(())
}

async fn drop_database_internal(db: &DatabaseConnection, db_name: &str) -> Result<(), DbErr> {
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)"),
))
.await?;
Ok(())
}

async fn run_migrations<Migrator: MigratorTrait>(&self) {
Migrator::up(self.conn_with_db.as_ref(), None)
.await
.expect("Database migration failed");
}
}

impl Deref for TestDbGuard {
type Target = DatabaseConnection;
fn deref(&self) -> &Self::Target {
&self.conn_with_db
}
}
128 changes: 128 additions & 0 deletions libs/blockscout-service-launcher/src/test_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use crate::launcher::ServerSettings;
use reqwest::Url;
use std::{
future::Future,
net::{SocketAddr, TcpListener},
str::FromStr,
time::Duration,
};
use tokio::time::timeout;

fn get_free_port() -> u16 {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
listener.local_addr().unwrap().port()
}

pub fn get_test_server_settings() -> (ServerSettings, Url) {
let mut server = ServerSettings::default();
let port = get_free_port();
server.http.addr = SocketAddr::from_str(&format!("127.0.0.1:{port}")).unwrap();
server.grpc.enabled = false;
let base = Url::parse(&format!("http://{}", server.http.addr)).unwrap();
(server, base)
}

pub async fn init_server<F, R>(run: F, base: &Url)
where
F: FnOnce() -> R + Send + 'static,
R: Future<Output = Result<(), anyhow::Error>> + Send,
{
tokio::spawn(async move { run().await });

let client = reqwest::Client::new();
let health_endpoint = base.join("health").unwrap();

let wait_health_check = async {
loop {
if let Ok(_response) = client
.get(health_endpoint.clone())
.query(&[("service", "")])
.send()
.await
{
break;
}
}
};
// Wait for the server to start
if (timeout(Duration::from_secs(10), wait_health_check).await).is_err() {
panic!("Server did not start in time");
}
}

async fn send_annotated_request<Response: for<'a> serde::Deserialize<'a>>(
url: &Url,
route: &str,
method: reqwest::Method,
payload: Option<&impl serde::Serialize>,
annotation: Option<&str>,
) -> Response {
let annotation = annotation.map(|v| format!("({v}) ")).unwrap_or_default();

let mut request = reqwest::Client::new().request(method, url.join(route).unwrap());
if let Some(p) = payload {
request = request.json(p);
};
let response = request
.send()
.await
.unwrap_or_else(|_| panic!("{annotation}Failed to send request"));

// Assert that status code is success
if !response.status().is_success() {
let status = response.status();
let message = response.text().await.expect("Read body as text");
panic!("({annotation})Invalid status code (success expected). Status: {status}. Message: {message}")
}

response
.json()
.await
.unwrap_or_else(|_| panic!("({annotation})Response deserialization failed"))
}

pub async fn send_annotated_post_request<Response: for<'a> serde::Deserialize<'a>>(
url: &Url,
route: &str,
payload: &impl serde::Serialize,
annotation: &str,
) -> Response {
send_annotated_request(
url,
route,
reqwest::Method::POST,
Some(payload),
Some(annotation),
)
.await
}

pub async fn send_post_request<Response: for<'a> serde::Deserialize<'a>>(
url: &Url,
route: &str,
payload: &impl serde::Serialize,
) -> Response {
send_annotated_request(url, route, reqwest::Method::POST, Some(payload), None).await
}

pub async fn send_annotated_get_request<Response: for<'a> serde::Deserialize<'a>>(
url: &Url,
route: &str,
annotation: &str,
) -> Response {
send_annotated_request(
url,
route,
reqwest::Method::GET,
None::<&()>,
Some(annotation),
)
.await
}

pub async fn send_get_request<Response: for<'a> serde::Deserialize<'a>>(
url: &Url,
route: &str,
) -> Response {
send_annotated_request(url, route, reqwest::Method::GET, None::<&()>, None).await
}
2 changes: 1 addition & 1 deletion libs/solidity-metadata/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ impl<'b> Decode<'b, DecodeContext> for MetadataHash {
for _ in 0..number_of_elements {
// try to parse the key
match d.str() {
Ok(s) if s == "solc" => {
Ok("solc") => {
if solc.is_some() {
// duplicate keys are not allowed in CBOR (RFC 8949)
return Err(Error::custom(ParseMetadataHashError::DuplicateKeys));
Expand Down

0 comments on commit 1521844

Please sign in to comment.