hamcrest / JavaHamcrest

Java (and original) version of Hamcrest
http://hamcrest.org/
BSD 3-Clause "New" or "Revised" License
2.11k stars 378 forks source link

FR: Matching maps with various type #388

Open jarl-dk opened 2 years ago

jarl-dk commented 2 years ago

Given an actual map like this:

        Map actual = Map.of(
                "k1", Map.of(
                        "k11", "v11",
                        "k12", "v12"),
                "k2", List.of("v21", "v22"),
                "k3", "V3"
        );

I wish/expect to make an assertion with matcher that looks like this:

        assertThat(actual, AllOf.allOf(
                IsMapContaining.hasEntry("k1", AllOf.allOf(
                        IsMapContaining.hasEntry("k11", "v11"),
                        IsMapContaining.hasEntry("k12", "v12"))
                ),
                IsMapContaining.hasEntry("k2", IsIterableContainingInOrder.contains("v21","v22")),
                IsMapContaining.hasEntry("k3", "v3")
                )
        );

But this fails with

    method org.hamcrest.core.AllOf.<T>allOf(java.lang.Iterable<org.hamcrest.Matcher<? super T>>) is not applicable
      (cannot infer type-variable(s) T
        (actual and formal argument lists differ in length))
    method org.hamcrest.core.AllOf.<T>allOf(org.hamcrest.Matcher<? super T>...) is not applicable
      (inferred type does not conform to upper bound(s)
        inferred: java.util.Map<? extends java.lang.String,? extends java.lang.String>
        upper bound(s): java.util.Map<? extends java.lang.String,? extends java.lang.String>,java.util.Map<? extends java.lang.String,? extends org.hamcrest.Matcher<java.lang.Iterable<? extends java.lang.String>>>,java.util.Map<? extends java.lang.String,? extends org.hamcrest.Matcher<java.util.Map<? extends java.lang.String,? extends java.lang.String>>>,java.lang.Object)
    method org.hamcrest.core.AllOf.<T>allOf(org.hamcrest.Matcher<? super T>,org.hamcrest.Matcher<? super T>) is not applicable
      (cannot infer type-variable(s) T
        (actual and formal argument lists differ in length))
    method org.hamcrest.core.AllOf.<T>allOf(org.hamcrest.Matcher<? super T>,org.hamcrest.Matcher<? super T>,org.hamcrest.Matcher<? super T>) is not applicable
      (inferred type does not conform to upper bound(s)
        inferred: java.util.Map<? extends java.lang.String,? extends java.lang.String>
        upper bound(s): java.util.Map<? extends java.lang.String,? extends java.lang.String>,java.util.Map<? extends java.lang.String,? extends org.hamcrest.Matcher<java.lang.Iterable<? extends java.lang.String>>>,java.util.Map<? extends java.lang.String,? extends org.hamcrest.Matcher<java.util.Map<? extends java.lang.String,? extends java.lang.String>>>,java.lang.Object)
    method org.hamcrest.core.AllOf.<T>allOf(org.hamcrest.Matcher<? super T>,org.hamcrest.Matcher<? super T>,org.hamcrest.Matcher<? super T>,org.hamcrest.Matcher<? super T>) is not applicable
      (cannot infer type-variable(s) T
        (actual and formal argument lists differ in length))
    method org.hamcrest.core.AllOf.<T>allOf(org.hamcrest.Matcher<? super T>,org.hamcrest.Matcher<? super T>,org.hamcrest.Matcher<? super T>,org.hamcrest.Matcher<? super T>,org.hamcrest.Matcher<? super T>) is not applicable
      (cannot infer type-variable(s) T
        (actual and formal argument lists differ in length))
    method org.hamcrest.core.AllOf.<T>allOf(org.hamcrest.Matcher<? super T>,org.hamcrest.Matcher<? super T>,org.hamcrest.Matcher<? super T>,org.hamcrest.Matcher<? super T>,org.hamcrest.Matcher<? super T>,org.hamcrest.Matcher<? super T>) is not applicable
      (cannot infer type-variable(s) T
        (actual and formal argument lists differ in length))
jarl-dk commented 2 years ago

I thnk this is related to

tumbarumba commented 1 month ago

I was looking at this issue, and I'm struggling to work out how to fix this. I tried to recreate the example as described above, but pulling out all the individual matchers so that I could inspect the exact types. Here's what I ended up with:

public void testNestedMatchers() {
    String k1 = "k1";
    Map<String, String> v1 = Map.of(
            "k11", "v11",
            "k12", "v12");
    String k2 = "k2";
    List<String> v2 = List.of("v21", "v22");
    String k3 = "k3";
    String v3 = "v3";
    Map<String, Object> actual = Map.of(
            k1, v1,
            k2, v2,
            k3, v3
    );

    Matcher<? super String> k1Matcher = equalTo("k1");
    assertThat(k1, k1Matcher);
    Matcher<Map<? extends String, ?>> kv11Matcher = hasEntry("k11", "v11");
    assertThat(v1, kv11Matcher);
    Matcher<Map<? extends String, ?>> kv12Matcher = hasEntry("k12", "v12");
    assertThat(v1, kv12Matcher);
    Matcher<Map<? extends String, ? extends String>> v1Matcher = allOf(kv11Matcher, kv12Matcher);
    assertThat(v1, v1Matcher);
    Matcher<? super Map<? extends String, ?>> kv1Matcher = hasEntry(k1Matcher, v1Matcher);
    // ^ error: incompatible types: inference variable V has incompatible bounds
    //        Matcher<? super Map<? extends String, ?>> kv1Matcher = hasEntry(k1Matcher, v1Matcher);
    //                                                                       ^
    //    upper bounds: Map<? extends String,? extends String>,Object
    //    lower bounds: Object
    //  where V,K are type-variables:
    //    V extends Object declared in method <K,V>hasEntry(Matcher<? super K>,Matcher<? super V>)
    //    K extends Object declared in method <K,V>hasEntry(Matcher<? super K>,Matcher<? super V>)
    Matcher<Iterable<? extends String>> v2Matcher = contains("v21", "v22");
    assertThat(v2, v2Matcher);
    Matcher<Map<? extends String, ?>> kv2Matcher = hasEntry(equalTo("k2"), v2Matcher);
    // ^ error: incompatible types: inference variable V has incompatible bounds
    //        Matcher<Map<? extends String, ?>> kv2Matcher = hasEntry(equalTo("k2"), v2Matcher);
    //                                                               ^
    //    equality constraints: Object
    //    upper bounds: Iterable<? extends String>,Object
    //  where V,K are type-variables:
    //    V extends Object declared in method <K,V>hasEntry(Matcher<? super K>,Matcher<? super V>)
    //    K extends Object declared in method <K,V>hasEntry(Matcher<? super K>,Matcher<? super V>)
    assertThat(actual, kv2Matcher);
    Matcher<Map<? extends String, ?>> kv3Matcher = hasEntry("k3", "v3");
    assertThat(actual, kv3Matcher);
    Matcher<Map<? extends String, ?>> fullMatcher = allOf(kv1Matcher, kv2Matcher, kv3Matcher);
    assertThat(actual, fullMatcher);
}

Expanding out the types like this, I get the compilation errors as noted in the comments above. This is the definition of hasEntry:

public static <K, V> Matcher<Map<? extends K, ? extends V>> hasEntry(Matcher<? super K> keyMatcher, Matcher<? super V> valueMatcher) {
    return new IsMapContaining<>(keyMatcher, valueMatcher);
}

To me, it looks like this is following the PECS rule (see https://stackoverflow.com/questions/2723397/what-is-pecs-producer-extends-consumer-super). In this instance, the method arguments are acting as consumers of values (hence the super), while the matcher as a whole is acting against a collection from which values are produced (hence the extends).

I recently put in a fix for the IsIterableContaining matcher without a problem using this guide, but this doesn't seem to be the same issue. I'm thinking that the extra complexity of having the 2 type variables (K and V) doesn't give enough constraints to the compiler.

Does anyone have any suggestions?