test-with-derive 0.16.1

A library that helps you run tests with conditions
Documentation
use proc_macro_error2::abort_call_site;

#[cfg(feature = "runtime")]
use proc_macro::TokenStream;
#[cfg(feature = "runtime")]
use syn::{parse_macro_input, ItemFn, ReturnType};

pub(crate) fn check_timezone(attr_str: &str) -> (bool, Vec<&str>) {
    let mut incorrect_tzs = vec![];
    let mut match_tz = false;
    let current_tz = chrono::Local::now().offset().local_minus_utc() / 60;

    for tz in attr_str.split(',') {
        let parsed_tz = match tz {
            "NZDT" => Ok(13 * 60),
            "NZST" => Ok(12 * 60),
            "AEDT" => Ok(11 * 60),
            "ACDT" => Ok(10 * 60 + 30),
            "AEST" => Ok(10 * 60),
            "ACST" => Ok(9 * 60 + 30),
            "KST" | "JST" => Ok(9 * 60),
            "HKT" | "WITA" | "AWST" => Ok(8 * 60),
            "PST" => abort_call_site!("PST can be GMT+8 or GMT-8, please use +8 or -8 instead"),
            "WIB" => Ok(7 * 60),
            "CST" => abort_call_site!("PST can be GMT+8 or GMT-6, please use +8 or -6 instead"),
            "5.5" | "+5.5" => Ok(5 * 60 + 30),
            "IST" => abort_call_site!(
                "IST can be GMT+5.5, GMT+2 or GMT+1, please use +5.5, 2 or 1 instead"
            ),
            "PKT" => Ok(5 * 60),
            "EAT" | "EEST" | "IDT" | "MSK" => Ok(3 * 60),
            "CAT" | "EET" | "CEST" | "SAST" => Ok(2 * 60),
            "CET" | "WAT" | "WEST" | "BST" => Ok(60),
            "UTC" | "GMT" | "WET" => Ok(0),
            "NDT" | "-2.5" => Ok(-2 * 60 - 30),
            "NST" | "-3.5" => Ok(-3 * 60 - 30),
            "ADT" => Ok(-3 * 60),
            "AST" | "EDT" => Ok(-4 * 60),
            "EST" | "CDT" => Ok(-5 * 60),
            "MDT" => Ok(-6 * 60),
            "MST" | "PDT" => Ok(-7 * 60),
            "AKDT" => Ok(-8 * 60),
            "HDT" | "AKST" => Ok(-9 * 60),
            "HST" => Ok(-10 * 60),
            _ => tz.parse::<i32>().map(|tz| tz * 60),
        };
        if let Ok(parsed_tz) = parsed_tz {
            match_tz |= current_tz == parsed_tz;
        } else {
            incorrect_tzs.push(tz);
        }
    }
    (match_tz, incorrect_tzs)
}

pub(crate) fn check_tz_condition(attr_str: String) -> (bool, String) {
    let (match_tz, incorrect_tzs) = check_timezone(&attr_str);

    // Generate ignore message
    if incorrect_tzs.len() == 1 {
        (
            false,
            format!("because timezone {} is incorrect", incorrect_tzs[0]),
        )
    } else if incorrect_tzs.len() > 1 {
        (
            false,
            format!(
                "because following timezones are incorrect:\n{}\n",
                incorrect_tzs.join(", ")
            ),
        )
    } else if match_tz {
        (true, String::new())
    } else {
        (
            false,
            format!("because the test case not run in following timezone:\n{attr_str}\n"),
        )
    }
}

#[cfg(feature = "runtime")]
pub(crate) fn runtime_timezone(attr: TokenStream, stream: TokenStream) -> TokenStream {
    let attr_str = attr.to_string();
    let ItemFn {
        attrs,
        vis,
        sig,
        block,
    } = parse_macro_input!(stream as ItemFn);
    let syn::Signature { ident, .. } = sig.clone();
    let check_ident = syn::Ident::new(&format!("_check_{ident}"), proc_macro2::Span::call_site());

    let check_fn = match (&sig.asyncness, &sig.output) {
        (Some(_), ReturnType::Default) => quote::quote! {
            async fn #check_ident() -> Result<test_with::Completion, test_with::Failed> {
                let mut incorrect_tzs = vec![];
                let mut match_tz = false;
                let current_tz = test_with::chrono::Local::now().offset().local_minus_utc() / 60;
                for tz in #attr_str.split(',') {
                    if let Ok(parsed_tz) = tz.parse::<i32>() {
                        match_tz |= current_tz == parsed_tz;
                    } else {
                        incorrect_tzs.push(tz);
                    }
                }

                if match_tz && incorrect_tzs.is_empty() {
                        #ident().await;
                        Ok(test_with::Completion::Completed)
                } else if incorrect_tzs.len() == 1 {
                    Ok(test_with::Completion::ignored_with(format!("because timezone {} is incorrect", incorrect_tzs[0])))
                } else if incorrect_tzs.len() > 1 {
                    Ok(test_with::Completion::ignored_with(format!("because following timezones are incorrect:\n{:?}\n", incorrect_tzs)))
                } else {
                    Ok(test_with::Completion::ignored_with(format!("because the test case not run in following timezone:\n{}\n", #attr_str)))
                }
            }
        },
        (Some(_), ReturnType::Type(_, _)) => quote::quote! {
            async fn #check_ident() -> Result<test_with::Completion, test_with::Failed> {
                let mut incorrect_tzs = vec![];
                let mut match_tz = false;
                let current_tz = test_with::chrono::Local::now().offset().local_minus_utc() / 60;
                for tz in #attr_str.split(',') {
                    if let Ok(parsed_tz) = tz.parse::<i32>() {
                        match_tz |= current_tz == parsed_tz;
                    } else {
                        incorrect_tzs.push(tz);
                    }
                }

                if match_tz && incorrect_tzs.is_empty() {
                        if let Err(e) = #ident().await {
                            Err(format!("{e:?}").into())
                        } else {
                            Ok(test_with::Completion::Completed)
                        }
                } else if incorrect_tzs.len() == 1 {
                    Ok(test_with::Completion::ignored_with(format!("because timezone {} is incorrect", incorrect_tzs[0])))
                } else if incorrect_tzs.len() > 1 {
                    Ok(test_with::Completion::ignored_with(format!("because following timezones are incorrect:\n{:?}\n", incorrect_tzs)))
                } else {
                    Ok(test_with::Completion::ignored_with(format!("because the test case not run in following timezone:\n{}\n", #attr_str)))
                }
            }
        },
        (None, _) => quote::quote! {
            fn #check_ident() -> Result<test_with::Completion, test_with::Failed> {
                let mut incorrect_tzs = vec![];
                let mut match_tz = false;
                let current_tz = test_with::chrono::Local::now().offset().local_minus_utc() / 60;
                for tz in #attr_str.split(',') {
                    if let Ok(parsed_tz) = tz.parse::<i32>() {
                        match_tz |= current_tz == parsed_tz;
                    } else {
                        incorrect_tzs.push(tz);
                    }
                }

                if match_tz && incorrect_tzs.is_empty() {
                        #ident();
                        Ok(test_with::Completion::Completed)
                } else if incorrect_tzs.len() == 1 {
                    Ok(test_with::Completion::ignored_with(format!("because timezone {} is incorrect", incorrect_tzs[0])))
                } else if incorrect_tzs.len() > 1 {
                    Ok(test_with::Completion::ignored_with(format!("because following timezones are incorrect:\n{:?}\n", incorrect_tzs)))
                } else {
                    Ok(test_with::Completion::ignored_with(format!("because the test case not run in following timezone:\n{}\n", #attr_str)))
                }
            }
        },
    };

    quote::quote! {
        #check_fn
        #(#attrs)*
        #vis #sig #block
    }
    .into()
}