Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can we use Ctor or Fields with a parameterized class? #62

Closed
danking opened this issue Aug 4, 2015 · 9 comments
Closed

Can we use Ctor or Fields with a parameterized class? #62

danking opened this issue Aug 4, 2015 · 9 comments

Comments

@danking
Copy link

danking commented Aug 4, 2015

Can this be made to work?

import com.pholser.junit.quickcheck.ForAll;
import com.pholser.junit.quickcheck.From;
import com.pholser.junit.quickcheck.generator.Ctor;
import org.junit.contrib.theories.Theories;
import org.junit.contrib.theories.Theory;
import org.junit.runner.RunWith;

import static org.junit.Assert.assertTrue;

@RunWith(Theories.class)
public final class FooTest {

    public static class Pair<L, R> {
        private final L l;
        private final R r;

        public Pair(L l, R r) {
            this.l = l;
            this.r = r;
        }

        public L getLeft() {
            return this.l;
        }

        public R getRight() {
            return this.r;
        }
    }

    public static Double magnitude(Pair<Double, Double> p) {
        return p.getLeft() * p.getRight();
    }

    @Theory
    public void testMagnitudeOfPair(@ForAll @From(Ctor.class) Pair<Double, Double> p) {
        assertTrue(true);
    }

}

Currently, with this pom:

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.mycompany.app</groupId>
  <artifactId>my-app</artifactId>
  <version>1</version>
  <dependencies>
         <dependency>
        <groupId>com.pholser</groupId>
        <artifactId>junit-quickcheck-core</artifactId>
        <version>0.5-alpha-3</version>
      </dependency>
      <dependency>
        <groupId>com.pholser</groupId>
        <artifactId>junit-quickcheck-generators</artifactId>
        <version>0.5-alpha-3</version>
      </dependency>
  </dependencies>
</project>

I get this error:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running FooTest
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Tests run: 2, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.179 sec <<< FAILURE!
testMagnitudeOfPair(FooTest)  Time elapsed: 0.133 sec  <<< ERROR!
org.javaruntype.exceptions.TypeValidationException: No variable substitution established for variable "L" in type "L"
    at org.javaruntype.type.TypeUtil.createFromJavaLangReflectType(TypeUtil.java:1081)
    at org.javaruntype.type.TypeRegistry.forJavaLangReflectType(TypeRegistry.java:163)
    at org.javaruntype.type.Types.forJavaLangReflectType(Types.java:1019)
    at com.pholser.junit.quickcheck.internal.generator.GeneratorRepository.token(GeneratorRepository.java:256)
    at com.pholser.junit.quickcheck.internal.generator.GeneratorRepository.generatorFor(GeneratorRepository.java:121)
    at com.pholser.junit.quickcheck.internal.generator.GeneratorRepository.produceGenerator(GeneratorRepository.java:107)
    at com.pholser.junit.quickcheck.generator.Generator.generatorFor(Generator.java:245)
    at com.pholser.junit.quickcheck.generator.Ctor.provideRepository(Ctor.java:89)
    at com.pholser.junit.quickcheck.internal.generator.CompositeGenerator.provideRepository(CompositeGenerator.java:64)
    at com.pholser.junit.quickcheck.internal.generator.GeneratorRepository.produceGenerator(GeneratorRepository.java:108)
    at com.pholser.junit.quickcheck.internal.generator.GenerationContext.decideGenerator(GenerationContext.java:64)
    at com.pholser.junit.quickcheck.internal.generator.GenerationContext.generate(GenerationContext.java:49)
    at com.pholser.junit.quickcheck.internal.generator.RandomTheoryParameterGenerator.generate(RandomTheoryParameterGenerator.java:60)
    at com.pholser.junit.quickcheck.internal.RandomValueSupplier.getValueSources(RandomValueSupplier.java:57)
    at org.junit.contrib.theories.internal.Assignments.potentialsForNextUnassigned(Assignments.java:60)
    at org.junit.contrib.theories.Theories$TheoryAnchor.runWithIncompleteAssignment(Theories.java:147)
    at org.junit.contrib.theories.Theories$TheoryAnchor.runWithAssignment(Theories.java:140)
    at org.junit.contrib.theories.Theories$TheoryAnchor.evaluate(Theories.java:128)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:252)
    at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:141)
    at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:112)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.apache.maven.surefire.util.ReflectionUtils.invokeMethodWithArray(ReflectionUtils.java:189)
    at org.apache.maven.surefire.booter.ProviderFactory$ProviderProxy.invoke(ProviderFactory.java:165)
    at org.apache.maven.surefire.booter.ProviderFactory.invokeProvider(ProviderFactory.java:85)
    at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:115)
    at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:75)


Results :

Tests in error: 
  testMagnitudeOfPair(FooTest): No variable substitution established for variable "L" in type "L"

Tests run: 2, Failures: 0, Errors: 1, Skipped: 0

which I assume is the result of a failure to correctly handle the instantiated type parameters. :(

I'd be happy to help write a fix or add functionality, but I don't really know where to start.

@danking
Copy link
Author

danking commented Aug 4, 2015

I get this error if I use Fields.class:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running FooTest
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Tests run: 2, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.174 sec <<< FAILURE!
testMagnitudeOfPair(FooTest)  Time elapsed: 0.111 sec  <<< ERROR!
com.pholser.junit.quickcheck.internal.ReflectionException: com.pholser.junit.quickcheck.internal.ReflectionException: java.lang.InstantiationException: FooTest$Pair
    at com.pholser.junit.quickcheck.internal.Reflection.reflectionException(Reflection.java:235)
    at com.pholser.junit.quickcheck.internal.Reflection.instantiate(Reflection.java:102)
    at com.pholser.junit.quickcheck.internal.ParameterContext.makeGenerator(ParameterContext.java:113)
    at com.pholser.junit.quickcheck.internal.ParameterContext.addGenerators(ParameterContext.java:100)
    at com.pholser.junit.quickcheck.internal.ParameterContext.annotate(ParameterContext.java:76)
    at com.pholser.junit.quickcheck.internal.RandomValueSupplier.getValueSources(RandomValueSupplier.java:56)
    at org.junit.contrib.theories.internal.Assignments.potentialsForNextUnassigned(Assignments.java:60)
    at org.junit.contrib.theories.Theories$TheoryAnchor.runWithIncompleteAssignment(Theories.java:147)
    at org.junit.contrib.theories.Theories$TheoryAnchor.runWithAssignment(Theories.java:140)
    at org.junit.contrib.theories.Theories$TheoryAnchor.evaluate(Theories.java:128)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:252)
    at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:141)
    at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:112)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.apache.maven.surefire.util.ReflectionUtils.invokeMethodWithArray(ReflectionUtils.java:189)
    at org.apache.maven.surefire.booter.ProviderFactory$ProviderProxy.invoke(ProviderFactory.java:165)
    at org.apache.maven.surefire.booter.ProviderFactory.invokeProvider(ProviderFactory.java:85)
    at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:115)
    at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:75)


Results :

Tests in error: 
  testMagnitudeOfPair(FooTest): com.pholser.junit.quickcheck.internal.ReflectionException: java.lang.InstantiationException: FooTest$Pair

Tests run: 2, Failures: 0, Errors: 1, Skipped: 0

@pholser
Copy link
Owner

pholser commented Aug 5, 2015

@danking Thanks for your interest!

You are correct -- Ctor and Fields don't handle target types with unresolved type parameters in their constructors/fields.

Your best bet is to make your own generator for Pair. Then, you can either use @From with your pair generator, or if you don't want to clutter your theories with @From, package your generator in a service loader JAR file and place it on the class path (where junit-quickcheck should pick it up and register it as a generator).

Here's one to get you started:

public class APair extends ComponentizedGenerator<Pair> {
    public APair() {
        super(Pair.class);
    }

    @Override public Pair<?, ?> generate(SourceOfRandomness random, GenerationStatus status) {
        return new Pair<>(
            componentGenerators().get(0).generate(random, status),
            componentGenerators().get(1).generate(random, status));
    }

    @Override public int numberOfNeededComponents() {
        return 2;
    }
}

When junit-quickcheck encounters a theory parameter of type Pair<X, Y> (assuming X and Y are not unresolved type parameters), it should find the APair generator, and then supply it with generators for its "component" types X and Y.

@RunWith(Theories.class)
public class FooTest {
    public static class Pair<L, R> {
        public final L l;
        public final R r;

        public Pair(L l, R r) {
            this.l = l;
            this.r = r;
        }
    }

    public static Double magnitude(Pair<Double, Double> p) {
        return p.getLeft() * p.getRight();
    }

    @Theory public void testMagnitudeOfPair(@ForAll @From(APair.class) Pair<Double, Double> p) {
        assertTrue(true);
    }
}

@danking
Copy link
Author

danking commented Aug 5, 2015

@pholser Nice!

That definitely unblocks me for this.

Would it be difficult for me to modify Ctor to do this directly for arbitrary types because the type parameters are lost by the time Ctor sees the type?

@pholser
Copy link
Owner

pholser commented Aug 6, 2015

@danking Without thinking about it too hard, I believe the benefit-to-effort ratio might be unfavorable to make the changes to Ctor you mention, when compared to that of creating a custom generator that produces instances of classes with type parameters and referring to it using @From or making it available to the generator ServiceLoader.

Ctor and Fields were intended to solve a problem that users encountered with the combinatorial nature of the Theories runner: instead of sampleSize^3 executions of a theory with three parameters, you can create a cheap inline tuple class, change the three-parameter theory to a one-parameter theory, mark the one parameter as @From(Ctor.class), and voila, sampleSize executions. Sometimes, the tuple class helps you discover a missing domain concept; other times, it's purely artificial.

Anyway, I didn't think too hard about the kinds of classes we might use with Ctor or Fields -- generic types with unresolved type parameters weren't on the radar. 8^(

I don't want to dissuade you from attempting such an enhancement, however. Basically, for a theory parameter marked with @ForAll, junit-quickcheck's parameter supplier gets invoked. That supplier has to decide what kind of generator should produce values for the parameter, and any of its components. You'll want to start by looking at class RandomValueSupplier and work your way down.

Do you think it'd be beneficial to have more documentation around creating custom generators for more complex types? If so, would you create a separate issue for this?

Thanks again! I hope you're enjoying using junit-quickcheck.

@danking
Copy link
Author

danking commented Aug 6, 2015

I certainly am enjoying it! Haskell is my first love and one of my favorite aspects of it was quickcheck. I really appreciate the work you've put into this 😀.

I'll try to carve time out this weekend to look into it; I'll come back for more docs if I need them 😉.

Thanks for your detailed responses!

@danking danking closed this as completed Aug 6, 2015
@vlsi
Copy link
Contributor

vlsi commented Sep 18, 2019

I've came across the need of something that is suggested here.

My use case is listed here: #239

If Ctor was able to automatically associate generic type parameters with the relevant constructor arguments, then I could lift generator-specific (e.g. test-specific) configuration to closer the test code.

Sample code:

    public static class ManifestBuilder<
        Main extends Map<String, String>,
        Sections extends Map<String, ? extends Map<String, String>>
        > implements Callable<Manifest> {
        private final Main main;
        private final Sections sections;

        public ManifestBuilder(Main main, Sections sections) {
            this.main = main;
            this.sections = sections;
        }

        @Override
        public Manifest call() {
            // actually build the manifest from the components
            DefaultManifest m = new DefaultManifest(null);
            m.attributes(mainAttributes);
            for (Map.Entry<String, HashMap<String, String>> sections : sections.entrySet()) {
                m.attributes(sections.getValue(), sections.getKey());
            }
            return m;
        }
    }

Test code:

    @Property
    public void manifestTheory(
        @From(Ctor.class)
            GeneratorDescriptors.ManifestBuilder<
                        @FromField(value = GeneratorDescriptors.class, field = "manifestAttributes")
                        HashMap<String, String>,
                        @Size(min=0, max=5) HashMap<@ManifestValue String,
                        @FromField(value = GeneratorDescriptors.class, field = "manifestAttributes")
                        HashMap<String, String>>
                    > builder) {
        Manifest manifest = builder.call();
        // test manifest
    }

On top of that it would be great to handle the following (== consider annotations from the type placeholders like SimpleAttrs):

    public <SimpleAtts extends
        @FromField(value = GeneratorDescriptors.class, field = "manifestAttributes")
            HashMap<String, String>> void
        manifestTheory2(
        @From(Ctor.class)
           ManifestBuilder<
                        SimpleAtts,
                        @Size(min=0, max=6) HashMap<@ManifestValue String, SimpleAtts>
                    > builder) {
    }

Unfortunately generic variables do not seem to work 100% (see #240 )

@pholser , what do you think?

My example seems to be "self sufficient", and it seems there's no need to implement a generator for ManifestBuilder. A single Ctor or alike would be enough, and it would be really great if the very same Ctor supported shrink out of the box.

That would simplify making complex user-defined objects.

@vlsi
Copy link
Contributor

vlsi commented Sep 18, 2019

Just to clarify: ManifestBuilder might seem to be excessive, however it allows to keep the original implementation (which is DefaultManifest in my case) intact, and the use of ManifestBuilder enables to lift some types to generics (when required), so those types are visible to quickcheck, and that enables to attach quickcheck annotations to them.

The price is I have to explicitly call builder.call() method in my test code, however I think it is not that bad.

@vlsi
Copy link
Contributor

vlsi commented Sep 18, 2019

A bit of a problem for shrinking in Ctor is List<T> doShrink(SourceOfRandomness random, T larger).
In other words, Ctor can't really recover the original values it passed to the constructor.

So let's consider improved Fields:

    public static class ManifestBuilder<
        MainAttrs extends HashMap<String, String>,
        ...
        > {
        public MainAttrs mainAttributes;
        ...

Then @From(Fields.class) ManifestBuilder<@Size(min=1, max=4) HashMap<String, String>> builder could infer the actual types of the fields, and doShrink can just grab the values of the fields and try shrinking them.

@pholser
Copy link
Owner

pholser commented Oct 8, 2019

@visi Certainly will investigate. Seems reasonable for Fields to allow shrink attempts, if the generators for the constituent fields can shrink.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants