jqwik-team / jqwik

Property-Based Testing on the JUnit Platform

Home Page:http://jqwik.net

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Type arguments are missing in some situations

lmartelli opened this issue · comments

I have an ArbitraryProvider implementation use in a context of a hierarchy of abstract inner classes of an abstract test class like this (I will provide for a complete minimal "working" example, but this should give you a rough picture) :

public class AbstractCrudControllerTest {
    public abstract class EndpointWithParamsTest<PARAMS> extends SecuredEndpointTest {
        protected final Function<PARAMS, ResultActions> endpoint;
        protected final Authority requiredAuthority;

        public EndpointWithParamsTest(Authority requiredAuthority, Function<PARAMS, ResultActions> endpoint) {
            this.requiredAuthority = requiredAuthority;
            this.endpoint = endpoint;
        }

        @Property
        void without_authority_returns_forbidden(@ForAll Authority authority, @ForAll PARAMS params) {
            givenAuthority(authority).isNot(requiredAuthority);
            endpoint.apply(params).andExpect(status().isForbidden());
        }
    }

    public abstract class FilteredAndPagedEndpointTest<FILTERS> extends EndpointWithParamsTest<FilteredAndPagedParams<FILTERS>> {
        public FilteredAndPagedEndpointTest(Authority requiredAuthority) {
            super(requiredAuthority, AbstractCrudControllerTest.this::getPage);
        }

        @Property
        void returns_requested_page(@ForAll List<T> expectedEntities, @ForAll FilteredAndPagedParams<FILTERS> params) {
            can_get_page(params, expectedEntities);
        }
    }
}

@Domain(MyDomain.class)
class ConfigurationEntrepotControllerTest extends AbstractCrudControllerTest {
    @Group
    class GetConfigurations extends FilteredAndPagedEndpointTest<ConfigurationEntrepot.Filters> {
        public GetConfigurations() {
            super(VOIR_CONFIGURATION_ENTREPOT);
        }
}

@Domain(DomainContext.Global.class)
public class MyDomain extends DomainContextBase {
    public class FilteredAndPagedParamsArbitraryProvider implements ArbitraryProvider {
        @Override
        public boolean canProvideFor(TypeUsage targetType) {
            return targetType.isOfType(FilteredAndPagedParams.class);
        }

        @Override
        public Set<Arbitrary<?>> provideFor(TypeUsage targetType, SubtypeProvider subtypeProvider) {
            TypeUsage innerType = targetType.getTypeArguments().get(0);
            var innerTypeProviders = subtypeProvider.apply(innerType);
            if (innerTypeProviders.isEmpty())
                throw new RuntimeException("Cannot find any Arbitrary provider for " + innerType.getType().getTypeName());
            return seq(innerTypeProviders)
                .map(arbitrary ->
                    combine(pageRequests(), arbitrary)
                        .as(FilteredAndPagedParams::new))
                .collect(Collectors.toSet());
        }
    }
}

When without_authority_returns_forbidden() is run for Group GetConfigurations, provideFor() fails with java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0 because the type arguments are missing.

Quite a complicated setup you have there ;-) Still trying to wrap my head around it.

What is the actual value of targetType in the failing case?

And yes, a reproducible example would help me figure out the problem more easily.

Quite a complicated setup you have there ;-)

Agreed !

Still trying to wrap my head around it.

What is the actual value of targetType in the failing case?

And yes, a reproducible example would help me figure out the problem more easily.

I've created a simple test project here : https://github.com/lmartelli/jqwik-issue-492
It's all in one file (see below).
Inner2 group runs fine with value set to various types.
But Inner3 fails :

provideFor net.jqwik.issue_492.BugTest$GenericType<java.lang.String>
targetType=@net.jqwik.api.ForAll(value="", supplier=net.jqwik.api.ArbitrarySupplier$NONE.class) GenericType
timestamp = 2023-06-20T21:44:35.438978394, Inner3:genericProperty = 
  java.lang.IndexOutOfBoundsException:
    Index 0 out of bounds for length 0
package net.jqwik.issue_492;

import net.jqwik.api.*;
import net.jqwik.api.domains.Domain;
import net.jqwik.api.domains.DomainContext;
import net.jqwik.api.domains.DomainContextBase;
import net.jqwik.api.providers.ArbitraryProvider;
import net.jqwik.api.providers.TypeUsage;

import java.util.Set;
import java.util.stream.Collectors;

import static org.assertj.core.api.Assertions.assertThat;

@Domain(BugTest.ConfigurationDomain.class)
@PropertyDefaults(tries = 10)
public class BugTest {
    public abstract class Inner1<PARAMS> {
        @Property
        void genericProperty(@ForAll PARAMS params) {
            System.out.println("params = " + params);
            assertThat(params).isNotNull();
        }
    }

    @Group
    public class Inner2<T> extends Inner1<GenericType<T>> {
    }

    public record GenericType<T>(T value) {
    }

    @Group
    class Inner3 extends Inner2<String> {
    }

    @Domain(DomainContext.Global.class)
    static class ConfigurationDomain extends DomainContextBase {
        public class GenericTypeArbitraryProvider implements ArbitraryProvider {
            @Override
            public boolean canProvideFor(TypeUsage targetType) {
                return targetType.isOfType(BugTest.GenericType.class);
            }

            @Override
            public Set<Arbitrary<?>> provideFor(TypeUsage targetType, SubtypeProvider subtypeProvider) {
                System.out.println("provideFor " + targetType.getType().getTypeName());
                System.out.println("targetType=" + targetType);
                TypeUsage innerType = targetType.getTypeArguments().get(0);
                System.out.println("TypeArgument = " + innerType.getType());
                var innerTypeProviders = subtypeProvider.apply(innerType);
                if (innerTypeProviders.isEmpty())
                    throw new RuntimeException("Cannot find any Arbitrary provider for " + innerType.getType().getTypeName());
                return innerTypeProviders.stream()
                        .map(arbitrary -> arbitrary.map(BugTest.GenericType::new))
                        .collect(Collectors.toSet());
            }
        }
    }
}

Judging by the amount of generics, https://github.com/harawata/typeparameterresolver might be relevant

Judging by the amount of generics, https://github.com/harawata/typeparameterresolver might be relevant

I've just tried it on my use case (lmartelli/jqwik-issue-492@de206e3) but it does not seem to provide more information.

Thanks @lmartelli for the reproducing example. It's really helpfully and it strongly suggests a bug in jqwik's type resolution to me. I'll put the bug as first thing on my list for 1.7.5 since 1.7.4 will have to be released soonish in order to get the micronaut extension on the road.

Next on my list for 1.8.0

Should be fixed.
@lmartelli You can try out 1.8.0-SNAPCHAR

Reopen if it does not work for you.