diff --git a/fixture-monkey/src/main/java/com/navercorp/fixturemonkey/adapter/projection/ValueProjectionAssembler.java b/fixture-monkey/src/main/java/com/navercorp/fixturemonkey/adapter/projection/ValueProjectionAssembler.java index 00749cb036..7e55b3f054 100644 --- a/fixture-monkey/src/main/java/com/navercorp/fixturemonkey/adapter/projection/ValueProjectionAssembler.java +++ b/fixture-monkey/src/main/java/com/navercorp/fixturemonkey/adapter/projection/ValueProjectionAssembler.java @@ -18,6 +18,7 @@ package com.navercorp.fixturemonkey.adapter.projection; +import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; @@ -297,8 +298,17 @@ private CombinableArbitrary assembleNode( ); applyDecomposeResult(decomposeResult, state); if (decomposeResult.hasEarlyReturn()) { + Object earlyValue = decomposeResult.getEarlyReturnValue(); + + // Apply wildcard overrides to container elements when a wildcard has higher order + if (isCurrentTypeContainer && earlyValue != null && !state.wildcardEntries.isEmpty()) { + earlyValue = applyWildcardOverridesToContainer( + earlyValue, currentPath, parentOrder, state + ); + } + return wrapValueWithFiltersAndCustomizers( - decomposeResult.getEarlyReturnValue(), + earlyValue, currentPath, currentRawType, state @@ -577,6 +587,7 @@ private CombinableArbitrary assembleNode( result = options.getDefaultArbitraryGenerator().generate(context); } + result = applyFilters(result, currentPath, currentRawType, state); result = applyCustomizers(result, currentPath, state); @@ -1970,9 +1981,9 @@ private boolean hasFieldLevelTypeSelectorSiblings(PathExpression rootTypePath, A return null; } if (container.getClass().isArray()) { - int length = java.lang.reflect.Array.getLength(container); + int length = Array.getLength(container); if (index >= 0 && index < length) { - return java.lang.reflect.Array.get(container, index); + return Array.get(container, index); } return null; } @@ -2098,6 +2109,66 @@ private static void applyDecomposeResult(DecomposeResult result, AssemblyState s } } + /** + * Applies wildcard overrides to container elements when a wildcard has a higher order + * than the container's own value. This handles the case where a container value is + * returned via earlyReturn (all decomposed elements match), but a wildcard like + * {@code $.values[*].values[*]} should override individual elements. + */ + private Object applyWildcardOverridesToContainer( + Object container, + PathExpression containerPath, + ValueOrder containerOrder, + AssemblyState state + ) { + if (container instanceof List) { + List list = (List)container; + List<@Nullable Object> result = null; + for (int i = 0; i < list.size(); i++) { + PathExpression elementPath = containerPath.index(i); + for (Map.Entry entry : state.wildcardEntries) { + if (entry.getKey().matches(elementPath) + && entry.getValue().order.compareTo(containerOrder) > 0) { + if (result == null) { + result = new ArrayList<>(list); + } + result.set(i, resolveLazyValue(entry.getValue().value, false, state)); + break; + } + } + } + return result != null ? result : container; + } else if (container.getClass().isArray()) { + int length = Array.getLength(container); + boolean modified = false; + for (int i = 0; i < length; i++) { + PathExpression elementPath = containerPath.index(i); + for (Map.Entry entry : state.wildcardEntries) { + if (entry.getKey().matches(elementPath) + && entry.getValue().order.compareTo(containerOrder) > 0) { + if (!modified) { + Class componentType = container.getClass().getComponentType(); + if (componentType == null) { + break; + } + Object copy = Array.newInstance(componentType, length); + //noinspection SuspiciousSystemArraycopy + System.arraycopy(container, 0, copy, 0, length); + container = copy; + modified = true; + } + Object resolved = resolveLazyValue(entry.getValue().value, false, state); + if (resolved != null) { + Array.set(container, i, resolved); + } + break; + } + } + } + } + return container; + } + private @Nullable Object resolveLazyValue(@Nullable Object value, boolean isFromRegister, AssemblyState state) { if (!(value instanceof LazyValueHolder)) { return value; diff --git a/fixture-monkey/src/test/java/com/navercorp/fixturemonkey/adapter/ContainerAdapterTest.java b/fixture-monkey/src/test/java/com/navercorp/fixturemonkey/adapter/ContainerAdapterTest.java index f6a231372d..713fbf6525 100644 --- a/fixture-monkey/src/test/java/com/navercorp/fixturemonkey/adapter/ContainerAdapterTest.java +++ b/fixture-monkey/src/test/java/com/navercorp/fixturemonkey/adapter/ContainerAdapterTest.java @@ -31,7 +31,10 @@ import com.navercorp.fixturemonkey.FixtureMonkey; import com.navercorp.fixturemonkey.api.type.TypeReference; import com.navercorp.fixturemonkey.test.FixtureMonkeyTestSpecs.EnumObject; +import com.navercorp.fixturemonkey.test.FixtureMonkeyTestSpecs.ListListStringObject; import com.navercorp.fixturemonkey.test.FixtureMonkeyTestSpecs.MapEntryWrapper; +import com.navercorp.fixturemonkey.test.FixtureMonkeyTestSpecs.MapStringListObject; +import com.navercorp.fixturemonkey.test.FixtureMonkeyTestSpecs.OptionalListStringObject; import com.navercorp.fixturemonkey.test.FixtureMonkeyTestSpecs.SimpleObject; import com.navercorp.fixturemonkey.test.FixtureMonkeyTestSpecs.TwoEnum; @@ -148,4 +151,52 @@ void sampleMapEntryWrapper() { then(actual.getComplexEntry().getValue()).isNotNull(); } + @Property + void sampleNestedListList() { + // when + ListListStringObject actual = SUT.giveMeBuilder(ListListStringObject.class) + .size("values", 2) + .size("values[*]", 2) + .sample(); + + // then + then(actual.getValues()).hasSize(2); + for (List inner : actual.getValues()) { + then(inner).hasSize(2); + for (String s : inner) { + then(s).isNotNull(); + } + } + } + + @Property + void sampleMapWithListValue() { + // when + MapStringListObject actual = SUT.giveMeBuilder(MapStringListObject.class) + .size("values", 2) + .sample(); + + // then + then(actual.getValues()).hasSize(2); + for (Map.Entry> entry : actual.getValues().entrySet()) { + then(entry.getKey()).isNotNull(); + then(entry.getValue()).isNotNull(); + } + } + + @Property + void sampleOptionalWithList() { + // when + OptionalListStringObject actual = SUT.giveMeBuilder(OptionalListStringObject.class) + .size("value", 2) + .sample(); + + // then + then(actual.getValue()).isPresent(); + then(actual.getValue().get()).hasSize(2); + for (String s : actual.getValue().get()) { + then(s).isNotNull(); + } + } + } diff --git a/fixture-monkey/src/test/java/com/navercorp/fixturemonkey/adapter/ThenApplyOrderingAdapterTest.java b/fixture-monkey/src/test/java/com/navercorp/fixturemonkey/adapter/ThenApplyOrderingAdapterTest.java index db2299ea30..d8018a4f01 100644 --- a/fixture-monkey/src/test/java/com/navercorp/fixturemonkey/adapter/ThenApplyOrderingAdapterTest.java +++ b/fixture-monkey/src/test/java/com/navercorp/fixturemonkey/adapter/ThenApplyOrderingAdapterTest.java @@ -21,6 +21,8 @@ import static org.assertj.core.api.BDDAssertions.then; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import net.jqwik.api.Property; @@ -29,7 +31,10 @@ import com.navercorp.fixturemonkey.FixtureMonkey; import com.navercorp.fixturemonkey.test.FixtureMonkeyTestSpecs.ComplexObject; import com.navercorp.fixturemonkey.test.FixtureMonkeyTestSpecs.DoubleNestedStringListWrapper; +import com.navercorp.fixturemonkey.test.FixtureMonkeyTestSpecs.ListListStringObject; +import com.navercorp.fixturemonkey.test.FixtureMonkeyTestSpecs.NestedStringArrayWrapper; import com.navercorp.fixturemonkey.test.FixtureMonkeyTestSpecs.NestedStringListWrapper; +import com.navercorp.fixturemonkey.test.FixtureMonkeyTestSpecs.StringArrayWrapper; import com.navercorp.fixturemonkey.test.FixtureMonkeyTestSpecs.StringListWrapper; @PropertyDefaults(tries = 10) @@ -129,7 +134,7 @@ void nestedThenApplyWithSizeAndElement() { then(actual.getStrList().get(1)).isEqualTo("second"); } - @Property(seed = "784162401591375688") + @Property void nestedThenApplySetAfterNestedApply() { // when ComplexObject actual = SUT.giveMeBuilder(ComplexObject.class) @@ -729,7 +734,7 @@ void setDecomposedMultipleIndicesThenApplyWildcardSize() { then(actual.get(1).getValues().get(1)).isEqualTo("y"); } - @Property(tries = 1000) + @Property void setDecomposedThenApplyWildcardSetOverride() { // given List innerList = new ArrayList<>(); @@ -740,6 +745,7 @@ void setDecomposedThenApplyWildcardSetOverride() { // when List actual = SUT.giveMeBuilder(NestedStringListWrapper.class) .set("values[0].values", innerList) + .set("values[1].values", Arrays.asList("a", "b")) .thenApply((obj, builder) -> builder.size("values", 2).size("values[*].values", 2).set("values[*].values[*]", "override") ) @@ -921,4 +927,184 @@ void thenApplySizeThenSetNullContainer() { // then then(actual.getStrList()).isNull(); } + + @Property + void setDecomposedThenApplyWildcardSetOverrideWithoutSizeChange() { + // given + List innerList = Arrays.asList("1", "2", "3"); + + // when + List actual = SUT.giveMeBuilder(NestedStringListWrapper.class) + .size("values", 2) + .size("values[*].values", 3) + .set("values[0].values", innerList) + .set("values[1].values", innerList) + .thenApply((obj, builder) -> + builder.set("values[*].values[*]", "override") + ) + .sample() + .getValues(); + + // then + then(actual).hasSize(2); + for (int j = 0; j < 2; j++) { + then(actual.get(j).getValues()).hasSize(3); + for (int k = 0; k < 3; k++) { + then(actual.get(j).getValues().get(k)).isEqualTo("override"); + } + } + } + + @Property + void setDecomposedArrayThenApplyWildcardSetOverride() { + // given + StringArrayWrapper wrapper = new StringArrayWrapper(); + wrapper.setValues(new String[]{"1", "2", "3"}); + + // when + List actual = SUT.giveMeBuilder(NestedStringArrayWrapper.class) + .set("values", Collections.singletonList(wrapper)) + .thenApply((obj, builder) -> + builder.size("values", 2) + .size("values[*].values", 2) + .set("values[*].values[*]", "override") + ) + .sample() + .getValues(); + + // then + then(actual).hasSize(2); + for (int j = 0; j < 2; j++) { + then(actual.get(j).getValues()).hasSize(2); + for (int k = 0; k < 2; k++) { + then(actual.get(j).getValues()[k]).isEqualTo("override"); + } + } + } + + @Property + void setDecomposedAfterThenApplyWildcardSetOverride() { + // given + List innerList = Arrays.asList("a", "b"); + + // when + List actual = SUT.giveMeBuilder(NestedStringListWrapper.class) + .thenApply((obj, builder) -> + builder.set("values[*].values[*]", "override") + ) + .size("values", 2) + .size("values[*].values", 2) + .set("values[0].values", innerList) + .sample() + .getValues(); + + // then + then(actual).hasSize(2); + then(actual.get(0).getValues()).hasSize(2); + then(actual.get(0).getValues().get(0)).isEqualTo("a"); + then(actual.get(0).getValues().get(1)).isEqualTo("b"); + } + + @Property + void setDecomposedObjectThenApplyWildcardSetOverride() { + // when + StringListWrapper stringListWrapper = new StringListWrapper(); + stringListWrapper.setValues(Collections.singletonList("1")); + List actual = SUT.giveMeBuilder(NestedStringListWrapper.class) + .set("values", Collections.singletonList(stringListWrapper)) + .thenApply((obj, builder) -> + builder.size("values", 2) + .size("values[*].values", 2) + .set("values[*].values[*]", "override") + ) + .sample() + .getValues(); + + // then + then(actual).hasSize(2); + then(actual.get(0).getValues()).hasSize(2); + then(actual.get(0).getValues().get(0)).isEqualTo("override"); + then(actual.get(0).getValues().get(1)).isEqualTo("override"); + then(actual.get(1).getValues()).hasSize(2); + then(actual.get(1).getValues().get(0)).isEqualTo("override"); + then(actual.get(1).getValues().get(1)).isEqualTo("override"); + } + + @Property + void setDecomposedNestedContainerThenApplyWildcardSetOverride() { + // given + List> values = Arrays.asList( + Arrays.asList("a", "b"), + Arrays.asList("c", "d") + ); + + // when + List> actual = SUT.giveMeBuilder(ListListStringObject.class) + .set("values", values) + .thenApply((obj, builder) -> + builder.size("values", 2) + .size("values[*]", 2) + .set("values[*][*]", "override") + ) + .sample() + .getValues(); + + // then + then(actual).hasSize(2); + for (int i = 0; i < 2; i++) { + then(actual.get(i)).hasSize(2); + then(actual.get(i).get(0)).isEqualTo("override"); + then(actual.get(i).get(1)).isEqualTo("override"); + } + } + + @Property + void listListStringSizeAndSet() { + // when + List> actual = SUT.giveMeBuilder(ListListStringObject.class) + .size("values", 2) + .size("values[*]", 2) + .set("values[*][*]", "test") + .sample() + .getValues(); + + // then + then(actual).hasSize(2); + for (int i = 0; i < 2; i++) { + then(actual.get(i)).hasSize(2); + then(actual.get(i).get(0)).isEqualTo("test"); + then(actual.get(i).get(1)).isEqualTo("test"); + } + } + + @Property + void listListStringSetThenSize() { + // given + FixtureMonkey fm = FixtureMonkey.builder() + .defaultNotNull(true) + .plugin(new JavaNodeTreeAdapterPlugin()) + .build(); + + List> values = Arrays.asList( + Arrays.asList("a", "b"), + Arrays.asList("c", "d") + ); + + // when + List> actual = fm.giveMeBuilder(ListListStringObject.class) + .set("values", values) + .size("values", 2) + .size("values[*]", 2) + .set("values[*][*]", "override") + .sample() + .getValues(); + + // then + then(actual).hasSize(2); + for (int i = 0; i < 2; i++) { + then(actual.get(i)).hasSize(2); + then(actual.get(i).get(0)).isEqualTo("override"); + then(actual.get(i).get(1)).isEqualTo("override"); + } + } } diff --git a/fixture-monkey/src/test/java/com/navercorp/fixturemonkey/test/FixtureMonkeyTestSpecs.java b/fixture-monkey/src/test/java/com/navercorp/fixturemonkey/test/FixtureMonkeyTestSpecs.java index f061fa2527..70163f5c38 100644 --- a/fixture-monkey/src/test/java/com/navercorp/fixturemonkey/test/FixtureMonkeyTestSpecs.java +++ b/fixture-monkey/src/test/java/com/navercorp/fixturemonkey/test/FixtureMonkeyTestSpecs.java @@ -334,6 +334,19 @@ public static class GenericSimpleChild extends GenericValue { String childValue; } + @Data + public static class StringArrayWrapper { + + private String[] values; + } + + @Setter + @Getter + public static class NestedStringArrayWrapper { + + List values; + } + @Setter @Getter public static class NestedStringListWrapper { @@ -348,6 +361,27 @@ public static class DoubleNestedStringListWrapper { List values; } + @Setter + @Getter + public static class ListListStringObject { + + List> values; + } + + @Setter + @Getter + public static class MapStringListObject { + + Map> values; + } + + @Setter + @Getter + public static class OptionalListStringObject { + + Optional> value; + } + @Getter public static class ConstructorJavaTypeObject { diff --git a/object-farm-api/src/main/java/com/navercorp/objectfarm/api/input/GenericTypeResolverConverter.java b/object-farm-api/src/main/java/com/navercorp/objectfarm/api/input/GenericTypeResolverConverter.java index 25f216621a..84bf2dad51 100644 --- a/object-farm-api/src/main/java/com/navercorp/objectfarm/api/input/GenericTypeResolverConverter.java +++ b/object-farm-api/src/main/java/com/navercorp/objectfarm/api/input/GenericTypeResolverConverter.java @@ -156,7 +156,10 @@ public static List extractCollectionTypeArguments(Collection collect return Collections.emptyList(); } - JvmType elementType = new JavaType(firstElement.getClass()); + List elementTypeArgs = extractTypeArgumentsFromValue(firstElement); + JvmType elementType = elementTypeArgs.isEmpty() + ? new JavaType(firstElement.getClass()) + : new JavaType(firstElement.getClass(), elementTypeArgs, Collections.emptyList()); return Collections.singletonList(elementType); } @@ -180,8 +183,14 @@ public static List extractMapTypeArguments(Map map) { } List typeArguments = new ArrayList<>(); - typeArguments.add(new JavaType(key.getClass())); - typeArguments.add(new JavaType(entryValue.getClass())); + List keyTypeArgs = extractTypeArgumentsFromValue(key); + typeArguments.add(keyTypeArgs.isEmpty() + ? new JavaType(key.getClass()) + : new JavaType(key.getClass(), keyTypeArgs, Collections.emptyList())); + List valueTypeArgs = extractTypeArgumentsFromValue(entryValue); + typeArguments.add(valueTypeArgs.isEmpty() + ? new JavaType(entryValue.getClass()) + : new JavaType(entryValue.getClass(), valueTypeArgs, Collections.emptyList())); return typeArguments; } diff --git a/object-farm-api/src/main/java/com/navercorp/objectfarm/api/input/ValueAnalyzer.java b/object-farm-api/src/main/java/com/navercorp/objectfarm/api/input/ValueAnalyzer.java index 1d6e6a4736..aaa89d990d 100644 --- a/object-farm-api/src/main/java/com/navercorp/objectfarm/api/input/ValueAnalyzer.java +++ b/object-farm-api/src/main/java/com/navercorp/objectfarm/api/input/ValueAnalyzer.java @@ -182,6 +182,14 @@ private ValueAnalysisResult analyzeInternalDeep( containerSizeResolvers.addAll(nestedResult.getContainerSizeResolvers()); interfaceResolvers.addAll(nestedResult.getInterfaceResolvers()); genericTypeResolvers.addAll(nestedResult.getGenericTypeResolvers()); + } else if (fieldValue != null && requiresInterfaceResolver(fieldValue)) { + PathResolver interfaceResolver = InterfaceResolverConverter.fromValue( + fieldPath, + fieldValue + ); + if (interfaceResolver != null) { + interfaceResolvers.add(interfaceResolver); + } } } } diff --git a/object-farm-api/src/main/java/com/navercorp/objectfarm/api/tree/PathResolverContext.java b/object-farm-api/src/main/java/com/navercorp/objectfarm/api/tree/PathResolverContext.java index 751a192cbe..c515cbccd3 100644 --- a/object-farm-api/src/main/java/com/navercorp/objectfarm/api/tree/PathResolverContext.java +++ b/object-farm-api/src/main/java/com/navercorp/objectfarm/api/tree/PathResolverContext.java @@ -207,11 +207,13 @@ public Optional findWildcardContainerSizeResolver(PathExp * @return an Optional containing the matching resolver, or empty if none match */ public Optional findInterfaceResolver(PathExpression path) { - return interfaceResolvers - .stream() - .filter(r -> r.matches(path)) - .map(PathResolver::getCustomizer) - .findFirst(); + InterfaceResolver last = null; + for (PathResolver r : interfaceResolvers) { + if (r.matches(path)) { + last = r.getCustomizer(); + } + } + return Optional.ofNullable(last); } /** @@ -221,11 +223,13 @@ public Optional findInterfaceResolver(PathExpression path) { * @return an Optional containing the matching resolver, or empty if none match */ public Optional findGenericTypeResolver(PathExpression path) { - return genericTypeResolvers - .stream() - .filter(r -> r.matches(path)) - .map(PathResolver::getCustomizer) - .findFirst(); + GenericTypeResolver last = null; + for (PathResolver r : genericTypeResolvers) { + if (r.matches(path)) { + last = r.getCustomizer(); + } + } + return Optional.ofNullable(last); } /**