From 390a974e68c47abc68a52cc676b5438fdb8c9376 Mon Sep 17 00:00:00 2001 From: Lorenzo Delgado Date: Wed, 14 Feb 2024 16:27:33 +0100 Subject: [PATCH] fix: env macro multiline support --- src/lib.rs | 162 +++++++++++++++++++++++++++++++++++++++++++++++++-- src/utils.rs | 55 +++++++++++++++++ 2 files changed, 211 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 984d9cb..675b812 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,7 +68,7 @@ use syn::{Item, ItemStruct, ItemType}; #[cfg(feature = "executable")] use which::which; -use crate::utils::{fn_macro, is_module, mod_macro}; +use crate::utils::{fn_macro, is_module, mod_macro, sanitize_env_vars_attr}; mod utils; @@ -130,14 +130,20 @@ pub fn env(attr: TokenStream, stream: TokenStream) -> TokenStream { } fn check_env_condition(attr_str: String) -> (bool, String) { - let var_names: Vec<&str> = attr_str.split(',').collect(); + let var_names = sanitize_env_vars_attr(&attr_str); + + // Check if the environment variables are set let mut missing_vars = vec![]; - for var in var_names.iter() { - if std::env::var(var).is_err() { - missing_vars.push(var.to_string()); + for name in var_names { + if std::env::var(name).is_err() { + missing_vars.push(name.to_string()); } } - let ignore_msg = if missing_vars.len() == 1 { + + // Generate ignore message + let ignore_msg = if missing_vars.is_empty() { + String::new() + } else if missing_vars.len() == 1 { format!("because variable {} not found", missing_vars[0]) } else { format!( @@ -145,6 +151,7 @@ fn check_env_condition(attr_str: String) -> (bool, String) { missing_vars.join(", ") ) }; + (missing_vars.is_empty(), ignore_msg) } @@ -2520,3 +2527,146 @@ pub fn runtime_ignore_if(attr: TokenStream, stream: TokenStream) -> TokenStream } .into() } + +#[cfg(test)] +mod tests { + use super::check_env_condition; + + mod env_macro { + use super::*; + + #[test] + fn single_env_var_should_be_not_set() { + //* Given + let env_var = "A_RIDICULOUS_ENV_VAR_NAME_THAT_SHOULD_NOT_BE_SET"; + + // The `test_with::env()` macro arguments + let attr_str = env_var.to_string(); + + //* When + let (is_ok, ignore_msg) = check_env_condition(attr_str); + + //* Then + // Assert if the test should be ignored + assert!(!is_ok); + // Assert the ignore message should contain only the missing env var names + assert!(ignore_msg.contains(env_var)); + } + + #[test] + fn multiple_env_vars_should_not_be_set() { + //* Given + let env_var1 = "A_RIDICULOUS_ENV_VAR_NAME_THAT_SHOULD_NOT_BE_SET"; + let env_var2 = "ANOTHER_RIDICULOUS_ENV_VAR_NAME_THAT_SHOULD_NOT_BE_SET"; + + // The `test_with::env()` macro arguments + let attr_str = format!("{}, {}", env_var1, env_var2); + + //* When + let (is_ok, ignore_msg) = check_env_condition(attr_str); + + //* Then + // Assert if the test should be ignored + assert!(!is_ok); + // Assert the ignore message should contain only the missing env var names + assert!(ignore_msg.contains(env_var1)); + assert!(ignore_msg.contains(env_var2)); + } + + #[test] + fn single_env_var_should_be_set() { + //* Given + let env_var = "PATH"; + + // The `test_with::env()` macro arguments + let attr_str = env_var.to_string(); + + //* When + let (is_ok, ignore_msg) = check_env_condition(attr_str); + + //* Then + // Assert if the test should be ignored + assert!(is_ok); + // Assert the ignore message should contain only the missing env var names + assert!(!ignore_msg.contains(env_var)); + } + + /// Test the `test_with::env()` macro should parse the attribute string correctly + /// when the attribute string contains multiple env vars containing spaces and newlines. + /// + /// ```no_run + /// #[test_with::env( + /// PATH, + /// HOME + /// )] + /// #[test] + /// fn some_test() {} + #[test] + fn multiple_env_vars_should_be_set() { + //* Given + let env_var1 = "PATH"; + let env_var2 = "HOME"; + + // The `test_with::env()` macro arguments + let attr_str = format!("\t{},\n\t{}\n", env_var1, env_var2); + + //* When + let (is_ok, ignore_msg) = check_env_condition(attr_str); + + //* Then + // Assert if the test should be ignored + assert!(is_ok); + // Assert the ignore message should contain only the missing env var names + assert!(!ignore_msg.contains(env_var1)); + assert!(!ignore_msg.contains(env_var2)); + } + + /// Test the `test_with::env()` macro should parse the attribute string correctly + /// when the attribute string contains multiple env vars and one of them is not set. + #[test] + fn multiple_env_vars_but_one_is_not_set() { + //* Given + let env_var1 = "PATH"; + let env_var2 = "HOME"; + let env_var3 = "A_RIDICULOUS_ENV_VAR_NAME_THAT_SHOULD_NOT_BE_SET"; + + // The `test_with::env()` macro arguments + let attr_str = format!("{}, {}, {}", env_var1, env_var2, env_var3); + + //* When + let (is_ok, ignore_msg) = check_env_condition(attr_str); + + //* Then + // Assert if the test should be ignored + assert!(!is_ok); + // Assert the ignore message should contain only the missing env var names + assert!(!ignore_msg.contains(env_var1)); + assert!(!ignore_msg.contains(env_var2)); + assert!(ignore_msg.contains(env_var3)); + } + + /// Test the `test_with::env()` macro should parse the attribute string correctly + /// when the attribute string contains multiple env vars and various of them are not set. + #[test] + fn multiple_env_vars_and_various_not_set() { + //* Given + let env_var1 = "PATH"; + let env_var2 = "A_RIDICULOUS_ENV_VAR_NAME_THAT_SHOULD_NOT_BE_SET"; + let env_var3 = "ANOHTER_RIDICULOUS_ENV_VAR_NAME_THAT_SHOULD_NOT_BE_SET"; + + // The `test_with::env()` macro arguments + let attr_str = format!("{}, {}, {}", env_var1, env_var2, env_var3); + + //* When + let (is_ok, ignore_msg) = check_env_condition(attr_str); + + //* Then + // Assert if the test should be ignored + assert!(!is_ok); + // Assert the ignore message should contain only the missing env var names + assert!(!ignore_msg.contains(env_var1)); + assert!(ignore_msg.contains(env_var2)); + assert!(ignore_msg.contains(env_var3)); + } + } +} diff --git a/src/utils.rs b/src/utils.rs index 29168f5..ba82194 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -257,3 +257,58 @@ pub(crate) fn mod_macro( abort_call_site!("should use on mod with context") } } + +/// Sanitize the attribute string to remove any leading or trailing whitespace +/// and split the string into an iterator of individual environment variable names. +pub fn sanitize_env_vars_attr(attr_str: &str) -> impl Iterator { + attr_str.split(',').map(str::trim) +} + +#[cfg(test)] +mod tests { + use super::sanitize_env_vars_attr; + + #[test] + fn sanitize_single_env_var() { + //* Given + let env_var = "FOO"; + + let attr_str = env_var.to_string(); + + //* When + let result = sanitize_env_vars_attr(&attr_str).collect::>(); + + //* Then + assert_eq!(result, vec!["FOO"]); + } + + #[test] + fn sanitize_multiple_env_vars() { + //* Given + let env_var1 = "FOO"; + let env_var2 = "BAR"; + let env_var3 = "BAZ"; + + let attr_str = format!("\t{},\n\t{},\n\t{}", env_var1, env_var2, env_var3); + + //* When + let result = sanitize_env_vars_attr(&attr_str).collect::>(); + + //* Then + assert_eq!(result, vec!["FOO", "BAR", "BAZ"]); + } + + #[test] + fn sanitize_env_vars_with_whitespace() { + //* Given + let env_var = "FOO BAR"; + + let attr_str = env_var.to_string(); + + //* When + let result = sanitize_env_vars_attr(&attr_str).collect::>(); + + //* Then + assert_eq!(result, vec!["FOO BAR"]); + } +}