skipif 0.1.0

Turn test cases into no-ops with _SKIPPED appended to their name based on compile time conditions.
Documentation
// Copyright 2024 Steve Wayne Warren
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use proc_macro_error::abort;
use syn::{
    parenthesized,
    parse::{Parse, ParseStream},
    parse2,
    punctuated::Punctuated,
    Ident, Result, Token,
};

#[derive(Debug, PartialEq, Eq)]
pub(crate) struct CallExpr {
    pub(crate) method: Ident,
    pub(crate) args: Vec<Ident>,
}

#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Arg {
    CallExpr(CallExpr),
}

impl Parse for Arg {
    fn parse(stream: ParseStream<'_>) -> Result<Self> {
        let method = stream.parse()?;
        let content;
        parenthesized!(content in stream);
        let args: Vec<Ident> = Punctuated::<Ident, Token![,]>::parse_separated_nonempty(&content)?
            .into_iter()
            .collect();
        Ok(Self::CallExpr(CallExpr { method, args }))
    }
}

#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct SkipIfArgs {
    pub(crate) elements: Vec<Arg>,
}

impl Parse for SkipIfArgs {
    fn parse(stream: ParseStream<'_>) -> Result<Self> {
        let elements: Vec<Arg> = Punctuated::<Arg, Token![,]>::parse_separated_nonempty(stream)?
            .into_iter()
            .collect();
        Ok(Self { elements })
    }
}

impl SkipIfArgs {
    pub(crate) fn parse(ts: proc_macro2::TokenStream) -> Self {
        match parse2::<Self>(ts) {
            Ok(args) => args,
            Err(e) => {
                abort!(e.span(), e)
            }
        }
    }
}

#[cfg(test)]
mod test {
    use proc_macro2::TokenStream;
    use quote::quote;
    use rstest::rstest;

    use super::*;
    use crate::test::*;

    #[rstest]
    #[case::single_call_arg(
        quote!(missing(ENV_VAR)),
        skip_if_args(vec![call_expr("missing", vec!["ENV_VAR"])]),
    )]
    #[case::multiple_call_args(
        quote!(missing(ENV_VAR1, ENV_VAR2, ENV_VAR3)),
        skip_if_args(vec![call_expr(
            "missing",
            vec!["ENV_VAR1", "ENV_VAR2", "ENV_VAR3"],
        )]),
    )]
    #[case::arbitrary_call_iden(
        quote!(meow(ARG), docker_container(NAME)),
        skip_if_args(vec![
            call_expr("meow", vec!["ARG"]),
            call_expr("docker_container", vec!["NAME"]),
        ]),
    )]
    #[case::multiple_instances_of_same_arg(
        quote!(meow(ARG), meow(ARG)),
        skip_if_args(vec![
            call_expr("meow", vec!["ARG"]),
            call_expr("meow", vec!["ARG"]),
        ]),
    )]
    fn parse_skipif_args(#[case] ts: TokenStream, #[case] expected: SkipIfArgs) -> Result<()> {
        let parsed = parse2::<SkipIfArgs>(ts)?;

        assert_eq!(expected, parsed);

        Ok(())
    }
}