diff --git a/googletest/src/matchers/matches_pattern.rs b/googletest/src/matchers/matches_pattern.rs index fdcdfade..1ae2cdde 100644 --- a/googletest/src/matchers/matches_pattern.rs +++ b/googletest/src/matchers/matches_pattern.rs @@ -246,7 +246,8 @@ /// # .unwrap(); /// ``` /// -/// One can also match enum values: +/// The macro also allows matching on specific enum values and supports wildcard +/// patterns like `MyEnum::Case(_)`. /// /// ``` /// # use googletest::prelude::*; @@ -260,11 +261,16 @@ /// verify_that!(MyEnum::A(123), matches_pattern!(&MyEnum::A(eq(123))))?; // Passes /// # Ok(()) /// # } +/// # fn should_pass_with_wildcard() -> Result<()> { +/// verify_that!(MyEnum::A(123), matches_pattern!(MyEnum::A(_)))?; // Passes +/// # Ok(()) +/// # } /// # fn should_fail() -> Result<()> { /// verify_that!(MyEnum::B, matches_pattern!(&MyEnum::A(eq(123))))?; // Fails - wrong enum variant /// # Ok(()) /// # } /// # should_pass().unwrap(); +/// # should_pass_with_wildcard().unwrap(); /// # should_fail().unwrap_err(); /// ``` /// diff --git a/googletest/tests/matches_pattern_test.rs b/googletest/tests/matches_pattern_test.rs index c01a74f9..35fb9559 100644 --- a/googletest/tests/matches_pattern_test.rs +++ b/googletest/tests/matches_pattern_test.rs @@ -639,6 +639,112 @@ fn generates_correct_failure_output_when_enum_variant_without_field_is_matched() const EXPECTED: &str = "is & AnEnum :: A"; verify_that!(result, err(displays_as(contains_substring(EXPECTED)))) } + +#[test] +fn has_failure_when_wrong_enum_variant_is_matched_non_exhaustively() -> Result<()> { + #[allow(dead_code)] + #[derive(Debug)] + enum AnEnum { + Variant1(i8), + Variant2, + } + let actual: AnEnum = AnEnum::Variant2; + + let result = verify_that!(actual, matches_pattern!(&AnEnum::Variant1(..))); + + const EXPECTED: &str = indoc!( + " + Expected: is & AnEnum :: Variant1(..) + Actual: Variant2, + which is not & AnEnum :: Variant1(..) + " + ); + verify_that!(result, err(displays_as(contains_substring(EXPECTED)))) +} + +#[test] +fn has_failure_when_wrong_enum_variant_is_matched_with_underscore() -> Result<()> { + #[allow(dead_code)] + #[derive(Debug)] + enum AnEnum { + Variant1(i8), + Variant2, + } + let actual: AnEnum = AnEnum::Variant2; + + let result = verify_that!(actual, matches_pattern!(&AnEnum::Variant1(_))); + + const EXPECTED: &str = indoc!( + " + Expected: is & AnEnum :: Variant1(_) + Actual: Variant2, + which is not & AnEnum :: Variant1(_) + " + ); + verify_that!(result, err(displays_as(contains_substring(EXPECTED)))) +} + +#[test] +fn has_failure_when_wrong_enum_variant_is_matched_with_value() -> Result<()> { + #[allow(dead_code)] + #[derive(Debug)] + enum AnEnum { + Variant1(i8), + Variant2, + } + let actual: AnEnum = AnEnum::Variant2; + + let result = verify_that!(actual, matches_pattern!(&AnEnum::Variant1(123))); + + const EXPECTED: &str = indoc!( + " + Expected: is & AnEnum :: Variant1 which has field `0`, which is equal to 123 + Actual: Variant2, + which has the wrong enum variant `Variant2` + " + ); + verify_that!(result, err(displays_as(contains_substring(EXPECTED)))) +} + +#[test] +fn matches_enum_struct_field_with_mutliple_variants() -> Result<()> { + #[allow(dead_code)] + #[derive(Debug)] + enum AnEnum { + Variant1(i8), + Variant2, + } + let actual: AnEnum = AnEnum::Variant2; + + verify_that!(actual, matches_pattern!(&AnEnum::Variant2)) +} + +#[test] +fn matches_enum_struct_field_with_multiple_variants_non_exhaustive() -> Result<()> { + #[allow(dead_code)] + #[derive(Debug)] + enum AnEnum { + Variant1(i8), + Variant2, + } + let actual: AnEnum = AnEnum::Variant1(123); + + verify_that!(actual, matches_pattern!(&AnEnum::Variant1(..))) +} + +#[test] +fn matches_enum_struct_field_with_multiple_variants_with_wildcard() -> Result<()> { + #[allow(dead_code)] + #[derive(Debug)] + enum AnEnum { + Variant1(i8), + Variant2, + } + let actual: AnEnum = AnEnum::Variant1(123); + + verify_that!(actual, matches_pattern!(&AnEnum::Variant1(_))) +} + #[test] fn matches_enum_with_field() -> Result<()> { #[derive(Debug)] diff --git a/googletest_macro/src/matches_pattern.rs b/googletest_macro/src/matches_pattern.rs index 983d03d8..8e22a307 100644 --- a/googletest_macro/src/matches_pattern.rs +++ b/googletest_macro/src/matches_pattern.rs @@ -167,43 +167,70 @@ fn parse_tuple_pattern_args( let (patterns, dot_dot) = parse_list_terminated_pattern::.parse2(group_content)?; let field_count = patterns.len(); - let field_patterns = patterns + let field_patterns: Vec<_> = patterns .into_iter() .enumerate() .filter_map(|(index, maybe_pattern)| maybe_pattern.0.map(|pattern| (index, pattern))) .map(|(index, TupleFieldPattern { ref_token, matcher })| { let index = syn::Index::from(index); quote! { googletest::matchers::field!(#struct_name.#index, #ref_token #matcher) } - }); + }) + .collect(); - let matcher = quote! { - googletest::matchers::__internal_unstable_do_not_depend_on_these::is( - stringify!(#struct_name), - all!( #(#field_patterns),* ) - ) - }; + if field_patterns.is_empty() { + // It is possible that the logic above didn't generate any field matchers + // (e.g., for patterns like `Some(_)`). + // In this case we verify that the enum has the correct case, but don't + // verify the payload. + #[allow(clippy::manual_repeat_n)] + // `repeat_n` is not available on the Rust MSRV that we support in OSS + let ignored_fields = std::iter::repeat(quote! { _ }) + .take(field_count) + .chain(dot_dot.map(ToTokens::into_token_stream)); + let full_pattern = quote! { #struct_name ( #(#ignored_fields),* ) }; - // Do a match to ensure: - // - Fields are exhaustively listed unless the pattern ended with `..`. - // - `UNDEFINED_SYMBOL(..)` fails to compile. - let empty_fields = std::iter::repeat(quote! { _ }) - .take(field_count) - .chain(dot_dot.map(ToTokens::into_token_stream)); - Ok(quote! { - googletest::matchers::__internal_unstable_do_not_depend_on_these::compile_assert_and_match( - |actual| { - // Exhaustively check that all field names are specified. - match actual { - #struct_name ( #(#empty_fields),* ) => (), - // The pattern below is unreachable if the type is a struct (as opposed to - // an enum). Since the macro can't know which it is, we always include it - // and just tell the compiler not to complain. - #[allow(unreachable_patterns)] - _ => {}, - } - }, - #matcher) - }) + Ok(quote! { + googletest::matchers::__internal_unstable_do_not_depend_on_these::pattern_only( + |actual| { matches!(actual, #full_pattern) }, + concat!("is ", stringify!(#full_pattern)), + concat!("is not ", stringify!(#full_pattern)) + ) + }) + } else { + // We have created at least one field matcher. Each field matcher will verify + // not only its part of the payload, but also that the enum has the + // correct case. + let matcher = quote! { + googletest::matchers::__internal_unstable_do_not_depend_on_these::is( + stringify!(#struct_name), + all!( #(#field_patterns),* ) + ) + }; + + // Do a match to ensure: + // - Fields are exhaustively listed unless the pattern ended with `..`. + // - `UNDEFINED_SYMBOL(..)` fails to compile. + #[allow(clippy::manual_repeat_n)] + // `repeat_n` is not available on the Rust MSRV that we support in OSS + let empty_fields = std::iter::repeat(quote! { _ }) + .take(field_count) + .chain(dot_dot.map(ToTokens::into_token_stream)); + Ok(quote! { + googletest::matchers::__internal_unstable_do_not_depend_on_these::compile_assert_and_match( + |actual| { + // Exhaustively check that all field names are specified. + match actual { + #struct_name ( #(#empty_fields),* ) => (), + // The pattern below is unreachable if the type is a struct (as opposed to + // an enum). Since the macro can't know which it is, we always include it + // and just tell the compiler not to complain. + #[allow(unreachable_patterns)] + _ => {}, + } + }, + #matcher) + }) + } } ////////////////////////////////////////////////////////////////////////////////