I am building a project loosely following Luca Palmieri's Zero to Production in Rust. For extra practice (and maybe masochism?), I decided I wanted to enforce 100% code coverage. The following struct is covered in the setup of my integration tests:
#[derive(serde::Deserialize, Clone)]
pub struct DatabaseSettings {
pub username: String,
pub password: SecretString,
pub host: String,
pub port: u16,
pub database_name: String,
pub require_ssl: bool,
}
pub async fn spawn_app() -> TestApp {
LazyLock::force(&TRACING);
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to random port");
let port = listener.local_addr().unwrap().port();
let address = format!("http://127.0.0.1:{}", port);
let mut configuration = get_configuration().expect("Failed to read configuration.");
configuration.database.database_name = Uuid::new_v4().to_string();
let connection_pool = configure_database(&configuration.database).await;
let server = run(listener, connection_pool.clone()).expect("Failed to bind address");
let _ = tokio::spawn(server);
TestApp {
address,
db_pool: connection_pool,
}
}
pub fn get_configuration() -> Result<Settings, config::ConfigError> {
let base_path = std::env::current_dir().expect("Failed to determine the current directory");
let configuration_directory = base_path.join("configuration");
let environment: Environment = std::env::var("APP_ENVIRONMENT")
.unwrap_or_else(|_| "local".into())
.try_into()
.expect("Failed to parse APP_ENVIRONMENT");
let environment_filename = format!("{}.yaml", environment.as_str());
let settings = config::Config::builder()
.add_source(config::File::from(
configuration_directory.join("base.yaml"),
))
.add_source(config::File::from(
configuration_directory.join(environment_filename),
))
.add_source(
config::Environment::with_prefix("APP")
.prefix_separator("_")
.separator("__"),
)
.build()?;
settings.try_deserialize::<Settings>()
}
pub async fn configure_database(config: &DatabaseSettings) -> PgPool {
let maintenance_settings = DatabaseSettings {
database_name: "postgres".to_string(),
username: "postgres".to_string(),
password: SecretString::from("postgres"),
..config.clone()
};
let mut connection = PgConnection::connect_with(&maintenance_settings.connect_options())
.await
.expect("Failed to connect to Postgres.");
connection
.execute(format!(r#"CREATE DATABASE "{}";"#, config.database_name).as_str())
.await
.expect("Failed to create database.");
let connection_pool = PgPool::connect_with(config.connect_options())
.await
.expect("Failed to connect to Postgres.");
sqlx::migrate!("./migrations")
.run(&connection_pool)
.await
.expect("Failed to migrate the database.");
connection_pool
}
However, to run the database in production, I set the port via an environment variable, and for that I use deserialize_number_from_string
from serde_aux
:
#[derive(serde::Deserialize, Clone)]
pub struct DatabaseSettings {
pub username: String,
pub password: SecretString,
pub host: String,
#[serde(deserialize_with = "deserialize_number_from_string")] // <--ADDED THIS
pub port: u16,
pub database_name: String,
pub require_ssl: bool,
}
For some reason, when I add that trait to the port, lcov shows a coverage gap on the DatabaseSettings
struct. Even if I add #[cfg_attr(coverage_nightly, coverage(off))]
on top of the struct and run cargo llvm-cov --text --all-features --workspace --output-dir coverage --ignore-filename-regex main.rs --fail-under-lines 100
, the coverage is not excluded.
My questions are the following:
- Why did adding the
serde_aux
helper cause a coverage gap? - Why doesn't
#[cfg_attr(coverage_nightly, coverage(off))]
ignore the coverage?
Thanks!