FlexTradeUKLtd/jfixture

Potentially unintended behavior change of circularDependencyBehaviour().omitSpecimen()

Opened this issue · 0 comments

When upgrading from 2.7.0 to 2.7.2 I got some weird failures.

I narrowed it down to V2.7.0...V2.7.1 range and specifically this commit: 4306daa

Consider the following data structure:

class Container {
    public final @NonNull List<Item> items;

    public Container(@NonNull List<Item> items) {
        this.items = items;
//        this.items = Collections.unmodifiableList(items);
    }
}

class Item {
    public final @NonNull List<Ref> refs;

    public Item(@NonNull List<Ref> refs) {
        this.refs = refs;
//        this.refs = Collections.unmodifiableList(refs);
    }
}

class Ref {
    public final @NonNull Container referenced;

    public Ref(@NonNull Container referenced) {
        this.referenced = referenced;
    }
}

and a test:

import androidx.annotation.NonNull;

import com.flextrade.jfixture.JFixture;
import com.flextrade.jfixture.utility.SpecimenType;

import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Properties;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;

public class OmitSpecimenTest {

    @Test
    public void testRecursionContainer() {
        JFixture fixture = new JFixture();
        fixture.customise().circularDependencyBehaviour().omitSpecimen();

        Container container = fixture.create(Container.class);

        assertNotNull(container);
        assertNotNull(container.items);
        assertEquals(3, container.items.size());
        for (Item item : container.items) {
            assertNotNull(item);
            assertNotNull(item.refs);
            assertEquals(3, item.refs.size());
            for (Ref ref : item.refs) {
                assertNotNull(ref);
                assertNull(ref.referenced); // Container recurses
            }
        }
    }

    @Test
    public void testRecursionList() {
        JFixture fixture = new JFixture();
        fixture.customise().circularDependencyBehaviour().omitSpecimen();

        List<Ref> refs = fixture.create(new SpecimenType<List<Ref>>() { });

        assertEquals(3, refs.size());
        for (Ref ref : refs) {
            assertNotNull(ref);
            assertNotNull(ref.referenced);
            assertNotNull(ref.referenced.items);
            assertEquals(3, ref.referenced.items.size());
            for (Item item : ref.referenced.items) {
                assertNotNull(item);
                switch (jfixtureVersion()) {
                    case "2.7.0":
                        assertNotNull(item.refs);
                        assertEquals(0, item.refs.size()); // Ref recurses
                        break;
                    case "2.7.1":
                    case "2.7.2":
                        assertNull(item.refs); // List<Ref> recurses
                        break;
                    default:
                        fail("Unknown version: " + jfixtureVersion());
                }
            }
        }
    }

    private static String jfixtureVersion() {
        InputStream stream = JFixture.class
                .getResourceAsStream("/META-INF/maven/com.flextrade.jfixture/jfixture/pom.properties");
        try {
            Properties properties = new Properties();
            properties.load(stream);
            stream.close();
            return properties.getProperty("version");
        } catch (IOException ex) {
            // ignore; bad pattern for catch and close, but don't care for repro.
            return ex.toString();
        }
    }
}

Running the above testRecursionContainer test runs fine on all versions, but as you can see the expectation has changed in testRecursionList and it behaves differently on different versions: in 2.7.0 the recursive structure ended up being an empty list, but after 4306daa this became a null list. This wouldn't be a problem, but as you can see the commented (this.refs = Collections.unmodifiableList(refs);) line in the model, passing in a null will crash unmodifiableList and that will in turn cause this exception (only on 2.7.1 and 2.7.2):

java.lang.UnsupportedOperationException: JFixture was unable to create an instance of com.example.Item
Most likely because it has no public constructor, is an abstract or non-public type or has no static factory methods.
If this isn't the case it's likely that all constructors and factory methods on the type have thrown an exception.
To view any thrown exceptions just add the Tracing Customisation to the JFixture instance, e.g. fixture.customise(new TracingCustomisation(System.out));

		java.util.List<com.example.Ref> --> 
		com.example.Ref --> 
		public com.example.Ref(com.example.Container) --> 
		com.example.Container --> 
		public com.example.Container(java.util.List) --> 
		java.util.List<com.example.Item> --> 
		com.example.Item --> 
		public com.example.Item(java.util.List) --> 
		com.example.Item

	at com.flextrade.jfixture.behaviours.noresolution.ThrowingNoResolutionHandler.handleNoResolution(ThrowingNoResolutionHandler.java:9)
	at com.flextrade.jfixture.behaviours.noresolution.NoResolutionGuard.handleNoResolutionForRequest(NoResolutionGuard.java:52)
	at com.flextrade.jfixture.behaviours.noresolution.NoResolutionGuard.create(NoResolutionGuard.java:29)
	at com.flextrade.jfixture.behaviours.specimentype.SpecimenTypeInjector.create(SpecimenTypeInjector.java:26)
	at com.flextrade.jfixture.behaviours.recursion.RecursionGuard.create(RecursionGuard.java:33)
	at com.flextrade.jfixture.SpecimenBuilderContext.resolve(SpecimenBuilderContext.java:13)
	at com.flextrade.jfixture.builders.MultipleSpecimenRelay.buildArrayList(MultipleSpecimenRelay.java:33)
	at com.flextrade.jfixture.builders.MultipleSpecimenRelay.create(MultipleSpecimenRelay.java:26)
	at com.flextrade.jfixture.builders.CompositeBuilder.create(CompositeBuilder.java:26)
	at com.flextrade.jfixture.behaviours.autoproperty.AutoPropertyBuilder.create(AutoPropertyBuilder.java:22)
	at com.flextrade.jfixture.behaviours.noresolution.NoResolutionGuard.create(NoResolutionGuard.java:27)
	at com.flextrade.jfixture.behaviours.specimentype.SpecimenTypeInjector.create(SpecimenTypeInjector.java:26)
	at com.flextrade.jfixture.behaviours.recursion.RecursionGuard.create(RecursionGuard.java:33)
	at com.flextrade.jfixture.SpecimenBuilderContext.resolve(SpecimenBuilderContext.java:13)
	at com.flextrade.jfixture.builders.IterableRelay.create(IterableRelay.java:34)
	at com.flextrade.jfixture.builders.CompositeBuilder.create(CompositeBuilder.java:26)
	at com.flextrade.jfixture.behaviours.autoproperty.AutoPropertyBuilder.create(AutoPropertyBuilder.java:22)
	at com.flextrade.jfixture.behaviours.noresolution.NoResolutionGuard.create(NoResolutionGuard.java:27)
	at com.flextrade.jfixture.behaviours.specimentype.SpecimenTypeInjector.create(SpecimenTypeInjector.java:26)
	at com.flextrade.jfixture.behaviours.recursion.RecursionGuard.create(RecursionGuard.java:33)
	at com.flextrade.jfixture.SpecimenBuilderContext.resolve(SpecimenBuilderContext.java:13)
	at com.flextrade.jfixture.utility.ParameterUtils.convertTypesToInstances(ParameterUtils.java:28)
	at com.flextrade.jfixture.utility.ParameterUtils.getConstructorArguments(ParameterUtils.java:16)
	at com.flextrade.jfixture.builders.GenericConstructorRelay.create(GenericConstructorRelay.java:27)
	at com.flextrade.jfixture.builders.CompositeBuilder.create(CompositeBuilder.java:26)
	at com.flextrade.jfixture.behaviours.autoproperty.AutoPropertyBuilder.create(AutoPropertyBuilder.java:22)
	at com.flextrade.jfixture.behaviours.noresolution.NoResolutionGuard.create(NoResolutionGuard.java:27)
	at com.flextrade.jfixture.behaviours.specimentype.SpecimenTypeInjector.create(SpecimenTypeInjector.java:26)
	at com.flextrade.jfixture.behaviours.recursion.RecursionGuard.create(RecursionGuard.java:33)
	at com.flextrade.jfixture.SpecimenBuilderContext.resolve(SpecimenBuilderContext.java:13)
	at com.flextrade.jfixture.builders.ClassToConstructorRelay.create(ClassToConstructorRelay.java:51)
	at com.flextrade.jfixture.builders.CompositeBuilder.create(CompositeBuilder.java:26)
	at com.flextrade.jfixture.behaviours.autoproperty.AutoPropertyBuilder.create(AutoPropertyBuilder.java:22)
	at com.flextrade.jfixture.behaviours.noresolution.NoResolutionGuard.create(NoResolutionGuard.java:27)
	at com.flextrade.jfixture.behaviours.specimentype.SpecimenTypeInjector.create(SpecimenTypeInjector.java:26)
	at com.flextrade.jfixture.behaviours.recursion.RecursionGuard.create(RecursionGuard.java:33)
	at com.flextrade.jfixture.SpecimenBuilderContext.resolve(SpecimenBuilderContext.java:13)
	at com.flextrade.jfixture.utility.ParameterUtils.convertTypesToInstances(ParameterUtils.java:28)
	at com.flextrade.jfixture.utility.ParameterUtils.getConstructorArguments(ParameterUtils.java:16)
	at com.flextrade.jfixture.builders.GenericConstructorRelay.create(GenericConstructorRelay.java:27)
	at com.flextrade.jfixture.builders.CompositeBuilder.create(CompositeBuilder.java:26)
	at com.flextrade.jfixture.behaviours.autoproperty.AutoPropertyBuilder.create(AutoPropertyBuilder.java:22)
	at com.flextrade.jfixture.behaviours.noresolution.NoResolutionGuard.create(NoResolutionGuard.java:27)
	at com.flextrade.jfixture.behaviours.specimentype.SpecimenTypeInjector.create(SpecimenTypeInjector.java:26)
	at com.flextrade.jfixture.behaviours.recursion.RecursionGuard.create(RecursionGuard.java:33)
	at com.flextrade.jfixture.SpecimenBuilderContext.resolve(SpecimenBuilderContext.java:13)
	at com.flextrade.jfixture.builders.ClassToConstructorRelay.create(ClassToConstructorRelay.java:51)
	at com.flextrade.jfixture.builders.CompositeBuilder.create(CompositeBuilder.java:26)
	at com.flextrade.jfixture.behaviours.autoproperty.AutoPropertyBuilder.create(AutoPropertyBuilder.java:22)
	at com.flextrade.jfixture.behaviours.noresolution.NoResolutionGuard.create(NoResolutionGuard.java:27)
	at com.flextrade.jfixture.behaviours.specimentype.SpecimenTypeInjector.create(SpecimenTypeInjector.java:26)
	at com.flextrade.jfixture.behaviours.recursion.RecursionGuard.create(RecursionGuard.java:33)
	at com.flextrade.jfixture.SpecimenBuilderContext.resolve(SpecimenBuilderContext.java:13)
	at com.flextrade.jfixture.builders.MultipleSpecimenRelay.buildArrayList(MultipleSpecimenRelay.java:33)
	at com.flextrade.jfixture.builders.MultipleSpecimenRelay.create(MultipleSpecimenRelay.java:26)
	at com.flextrade.jfixture.builders.CompositeBuilder.create(CompositeBuilder.java:26)
	at com.flextrade.jfixture.behaviours.autoproperty.AutoPropertyBuilder.create(AutoPropertyBuilder.java:22)
	at com.flextrade.jfixture.behaviours.noresolution.NoResolutionGuard.create(NoResolutionGuard.java:27)
	at com.flextrade.jfixture.behaviours.specimentype.SpecimenTypeInjector.create(SpecimenTypeInjector.java:26)
	at com.flextrade.jfixture.behaviours.recursion.RecursionGuard.create(RecursionGuard.java:33)
	at com.flextrade.jfixture.SpecimenBuilderContext.resolve(SpecimenBuilderContext.java:13)
	at com.flextrade.jfixture.builders.IterableRelay.create(IterableRelay.java:34)
	at com.flextrade.jfixture.builders.CompositeBuilder.create(CompositeBuilder.java:26)
	at com.flextrade.jfixture.behaviours.autoproperty.AutoPropertyBuilder.create(AutoPropertyBuilder.java:22)
	at com.flextrade.jfixture.behaviours.noresolution.NoResolutionGuard.create(NoResolutionGuard.java:27)
	at com.flextrade.jfixture.behaviours.specimentype.SpecimenTypeInjector.create(SpecimenTypeInjector.java:26)
	at com.flextrade.jfixture.behaviours.recursion.RecursionGuard.create(RecursionGuard.java:33)
	at com.flextrade.jfixture.JFixture.create(JFixture.java:82)
	at com.flextrade.jfixture.JFixture.create(JFixture.java:73)
	at com.example.OmitSpecimentTest.testRecursionList(OmitSpecimentTest.java:28)

So it looks like the fix of #33 introduced a behavior change that may have not been intended. Although it looks logical as the recursion now actually matches the original type (see List<Ref> recurses instead of Ref recurses in comment in the test).