This is an automated email from the ASF dual-hosted git repository. spmallette pushed a commit to branch gvalue in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit b990a4abe6213c0a8d0fec7d6bde532db325e015 Author: Stephen Mallette <[email protected]> AuthorDate: Wed Aug 28 11:29:18 2024 -0400 wip - refactoring, tests, comments --- .../language/grammar/TraversalMethodVisitor.java | 2 +- .../tinkerpop/gremlin/process/traversal/P.java | 8 +- .../gremlin/process/traversal/step/GValue.java | 73 +++++------- .../process/traversal/step/filter/CoinStep.java | 4 - .../process/traversal/step/map/GraphStep.java | 4 +- .../process/traversal/step/map/VertexStep.java | 2 +- .../traversal/step/sideEffect/InjectStep.java | 2 +- .../optimization/InlineFilterStrategy.java | 2 +- .../tinkerpop/gremlin/util/CollectionUtil.java | 23 +++- .../gremlin/process/traversal/step/GValueTest.java | 131 +++++++++++++++++++++ .../tinkerpop/gremlin/util/CollectionUtilTest.java | 110 +++++++++++++++++ 11 files changed, 301 insertions(+), 60 deletions(-) diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java index ce010e326e..4d28a0b8e9 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalMethodVisitor.java @@ -765,7 +765,7 @@ public class TraversalMethodVisitor extends TraversalRootVisitor<GraphTraversal> // if any are GValue then they all need to be GValue to call hasLabel if (literalOrVar instanceof GValue || Arrays.stream(literalOrVars).anyMatch(lov -> lov instanceof GValue)) { literalOrVar = GValue.of(literalOrVar); - literalOrVars = Arrays.stream(literalOrVars).map(GValue::of).toArray(); + literalOrVars = GValue.ensureGValues(literalOrVars); } // since we normalized above to gvalue or literal we can just test the first arg for gvalue-ness diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java index 622a3c3e3a..a878bbb839 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java @@ -82,13 +82,7 @@ public class P<V> implements Predicate<V>, Serializable, Cloneable { } else { // this might be a bunch of GValue that need to be resolved. zomg if (this.value instanceof List) { - return this.biPredicate.test(testValue, (V) ((List) this.value).stream().map(o -> { - if (o instanceof GValue) { - return ((GValue) o).get(); - } else { - return o; - } - }).collect(Collectors.toList())); + return this.biPredicate.test(testValue, (V) ((List) this.value).stream().map(GValue::valueOf).collect(Collectors.toList())); } else { return this.biPredicate.test(testValue, this.value); } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/GValue.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/GValue.java index 0b1e88b15c..cdc5aa6972 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/GValue.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/GValue.java @@ -57,47 +57,6 @@ public class GValue<V> implements Cloneable, Serializable { this.value = value; } - /** - * The elements in object array argument are examined to see if they are {@link GValue} objects. If they are, they - * are preserved as is. If they are not then they are wrapped in a {@link GValue} object. - */ - public static <T> GValue<T>[] convertToGValues(final Object[] args) { - return Stream.of(args).map(id -> { - if (id instanceof GValue) - return (GValue<?>) id; - else - return of(id); - }).toArray(GValue[]::new); - } - - /** - * Converts {@link GValue} objects arguments to their values to prevent them from leaking to the Graph API. - * Providers extending from this step should use this method to get actual values to prevent any {@link GValue} - * objects from leaking to the Graph API. - */ - public static Object[] resolveToValues(final List<GValue<?>> gvalues) { - // convert gvalues to array - final Object[] newIds = new Object[gvalues.size()]; - int i = 0; - for (final GValue<?> gvalue : gvalues) { - newIds[i++] = gvalue.get(); - } - return newIds; - } - - /** - * Converts {@link GValue} objects argument array to their values to prevent them from leaking to the Graph API. - * Providers extending from this step should use this method to get actual values to prevent any {@link GValue} - * objects from leaking to the Graph API. - */ - public static Object[] resolveToValues(final GValue<?>[] gvalues) { - final Object[] newIds = new Object[gvalues.length]; - for (int i = 0; i < gvalues.length; i++) { - newIds[i] = gvalues[i].get(); - } - return newIds; - } - /** * Determines if the value held by this object was defined as a variable or a literal value. Literal values simply * have no name. @@ -437,4 +396,36 @@ public class GValue<V> implements Cloneable, Serializable { public static boolean instanceOfNumber(final Object o) { return o instanceof Number || (o instanceof GValue && ((GValue) o).getType().isNumeric()); } + + /** + * The elements in object array argument are examined to see if they are {@link GValue} objects. If they are, they + * are preserved as is. If they are not then they are wrapped in a {@link GValue} object. + */ + public static <T> GValue<T>[] ensureGValues(final Object[] args) { + return Stream.of(args).map(GValue::of).toArray(GValue[]::new); + } + + /** + * Converts {@link GValue} objects argument array to their values to prevent them from leaking to the Graph API. + * Providers extending from this step should use this method to get actual values to prevent any {@link GValue} + * objects from leaking to the Graph API. + */ + public static Object[] resolveToValues(final GValue<?>[] gvalues) { + final Object[] values = new Object[gvalues.length]; + for (int i = 0; i < gvalues.length; i++) { + values[i] = gvalues[i].get(); + } + return values; + } + + /** + * Takes an argument that is either a {@code GValue} or an object and if the former returns the child object and + * if the latter returns the object itself. + */ + public static Object valueOf(final Object either) { + if (either instanceof GValue) + return ((GValue<?>) either).get(); + else + return either; + } } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/CoinStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/CoinStep.java index 33b3c80944..5fd63ee3de 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/CoinStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/filter/CoinStep.java @@ -39,10 +39,6 @@ public final class CoinStep<S> extends FilterStep<S> implements Seedable { private final Random random = new Random(); private final GValue<Double> probability; - /** - * @deprecated As of release 3.7.3, replaced by {@link #CoinStep(Traversal.Admin, GValue)} - */ - @Deprecated public CoinStep(final Traversal.Admin traversal, final double probability) { super(traversal); this.probability = GValue.ofDouble(probability); diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/GraphStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/GraphStep.java index f65ecb7e81..c009588864 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/GraphStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/GraphStep.java @@ -70,7 +70,7 @@ public class GraphStep<S, E extends Element> extends AbstractStep<S, E> implemen this.returnClass = returnClass; // if ids is a single collection like g.V(['a','b','c']), then unroll it into an array of ids - this.ids = GValue.convertToGValues(tryUnrollSingleCollectionArgument(ids)); + this.ids = GValue.ensureGValues(tryUnrollSingleCollectionArgument(ids)); this.isStart = isStart; @@ -169,7 +169,7 @@ public class GraphStep<S, E extends Element> extends AbstractStep<S, E> implemen this.legacyLogicForPassingNoIds = newIds.length == 1 && ((newIds[0] instanceof List && ((List) newIds[0]).isEmpty()) || (newIds[0] instanceof GValue && ((GValue) newIds[0]).getType().isCollection() && ((List) ((GValue) newIds[0]).get()).isEmpty())); - final GValue[] gvalues = GValue.convertToGValues(tryUnrollSingleCollectionArgument(newIds)); + final GValue[] gvalues = GValue.ensureGValues(tryUnrollSingleCollectionArgument(newIds)); this.ids = ArrayUtils.addAll(this.ids, gvalues); } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/VertexStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/VertexStep.java index 896934668d..b6230e0538 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/VertexStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/VertexStep.java @@ -54,7 +54,7 @@ public class VertexStep<E extends Element> extends FlatMapStep<Vertex, E> implem private final Class<E> returnClass; public VertexStep(final Traversal.Admin traversal, final Class<E> returnClass, final Direction direction, final String... edgeLabels) { - this(traversal, returnClass, direction, GValue.convertToGValues(edgeLabels)); + this(traversal, returnClass, direction, GValue.ensureGValues(edgeLabels)); } public VertexStep(final Traversal.Admin traversal, final Class<E> returnClass, final Direction direction, final GValue<String>... edgeLabels) { diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/InjectStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/InjectStep.java index 73bf40b199..fa27d5af92 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/InjectStep.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/InjectStep.java @@ -32,7 +32,7 @@ public final class InjectStep<S> extends StartStep<S> { @SafeVarargs public InjectStep(final Traversal.Admin traversal, final S... injections) { super(traversal); - this.injections = GValue.convertToGValues(injections); + this.injections = GValue.ensureGValues(injections); this.start = new ArrayIterator<>(GValue.resolveToValues(this.injections)); } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/InlineFilterStrategy.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/InlineFilterStrategy.java index ad0ccf395f..e8b154fef6 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/InlineFilterStrategy.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/InlineFilterStrategy.java @@ -158,7 +158,7 @@ public final class InlineFilterStrategy extends AbstractTraversalStrategy<Traver } } if (!edgeLabels.isEmpty()) { - final VertexStep<Edge> newVertexStep = new VertexStep<>(traversal, Edge.class, previousStep.getDirection(), GValue.convertToGValues(edgeLabels.toArray())); + final VertexStep<Edge> newVertexStep = new VertexStep<>(traversal, Edge.class, previousStep.getDirection(), GValue.ensureGValues(edgeLabels.toArray())); TraversalHelper.replaceStep(previousStep, newVertexStep, traversal); TraversalHelper.copyLabels(previousStep, newVertexStep, false); if (step.getHasContainers().isEmpty()) { diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/util/CollectionUtil.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/util/CollectionUtil.java index 61d9d7573d..27d41d8e18 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/util/CollectionUtil.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/util/CollectionUtil.java @@ -32,22 +32,36 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +/** + * Utility class for working with collections and arrays. + */ public final class CollectionUtil { - private CollectionUtil() { - } + private CollectionUtil() { } + /** + * Converts varargs to a {@code List}. + */ public static <E> List<E> asList(final E... elements) { return new ArrayList<>(Arrays.asList(elements)); } + /** + * Converts varargs to a {@code Set}. + */ public static <E> LinkedHashSet<E> asSet(final E... elements) { return asSet(Arrays.asList(elements)); } + /** + * Converts {@code Collection} to a {@code Set}. + */ public static <E> LinkedHashSet<E> asSet(final Collection<E> elements) { return new LinkedHashSet<>(elements); } + /** + * Converts varargs to a {@code Map} where the elements are key/value pairs. + */ public static <K,V> LinkedHashMap<K,V> asMap(final Object... elements) { final LinkedHashMap<K,V> map = new LinkedHashMap<>(); for (int i = 0; i < elements.length; i+=2) { @@ -58,6 +72,11 @@ public final class CollectionUtil { return map; } + /** + * Clones a given {@code ConcurrentHashMap} by creating a new map and copying all entries from the original map. + * If the value of an entry is a {@code Set} or an {@code ArrayList}, a deep copy of the value is created. + * Otherwise, the value is copied as is. + */ public static <K,V> ConcurrentHashMap<K,V> clone(final ConcurrentHashMap<K,V> map) { final ConcurrentHashMap<K, V> result = new ConcurrentHashMap<>(map.size()); diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/GValueTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/GValueTest.java index f6aff53c8d..625bfaeb5a 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/GValueTest.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/GValueTest.java @@ -34,9 +34,11 @@ import java.util.Set; import org.junit.Test; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.mockito.Mockito.mock; @@ -475,4 +477,133 @@ public class GValueTest { public void valueInstanceOfNumericShouldReturnFalseForNullObject() { assertThat(GValue.valueInstanceOfNumeric(null), is(false)); } + + @Test + public void shouldConvertObjectArrayToGValues() { + final Object[] input = {1, "string", true}; + final GValue<?>[] expected = {GValue.of(1), GValue.of("string"), GValue.of(true)}; + final GValue<?>[] result = GValue.ensureGValues(input); + assertArrayEquals(expected, result); + } + + @Test + public void shouldPreserveExistingGValuesInArray() { + final GValue<Integer> gValue = GValue.of(123); + final Object[] input = {gValue, "string"}; + final GValue<?>[] expected = {gValue, GValue.of("string")}; + final GValue<?>[] result = GValue.ensureGValues(input); + assertArrayEquals(expected, result); + } + + @Test + public void shouldHandleEmptyArray() { + final Object[] input = {}; + final GValue<?>[] expected = {}; + final GValue<?>[] result = GValue.ensureGValues(input); + assertArrayEquals(expected, result); + } + + @Test + public void shouldHandleArrayWithNullValues() { + final Object[] input = {null, "string"}; + final GValue<?>[] expected = {GValue.of(null), GValue.of("string")}; + final GValue<?>[] result = GValue.ensureGValues(input); + assertArrayEquals(expected, result); + } + + @Test + public void shouldResolveGValuesToValues() { + final GValue<?>[] input = {GValue.of(1), GValue.of("string"), GValue.of(true)}; + final Object[] expected = {1, "string", true}; + final Object[] result = GValue.resolveToValues(input); + assertArrayEquals(expected, result); + } + + @Test + public void shouldHandleEmptyGValuesArray() { + final GValue<?>[] input = {}; + final Object[] expected = {}; + final Object[] result = GValue.resolveToValues(input); + assertArrayEquals(expected, result); + } + + @Test + public void shouldHandleGValuesArrayWithNullValues() { + final GValue<?>[] input = {GValue.of(null), GValue.of("string")}; + final Object[] expected = {null, "string"}; + final Object[] result = GValue.resolveToValues(input); + assertArrayEquals(expected, result); + } + + @Test + public void shouldHandleMixedTypeGValuesArray() { + final GValue<?>[] input = {GValue.of(1), GValue.of("string"), GValue.of(true), GValue.of(123.45)}; + final Object[] expected = {1, "string", true, 123.45}; + final Object[] result = GValue.resolveToValues(input); + assertArrayEquals(expected, result); + } + + @Test + public void equalsShouldReturnTrueForSameObject() { + final GValue<Integer> gValue = GValue.of(123); + assertEquals(true, gValue.equals(gValue)); + } + + @Test + public void equalsShouldReturnFalseForNull() { + final GValue<Integer> gValue = GValue.of(123); + assertEquals(false, gValue.equals(null)); + } + + @Test + public void equalsShouldReturnFalseForDifferentClass() { + final GValue<Integer> gValue = GValue.of(123); + assertEquals(false, gValue.equals("string")); + } + + @Test + public void equalsShouldReturnTrueForEqualGValues() { + final GValue<Integer> gValue1 = GValue.of(123); + final GValue<Integer> gValue2 = GValue.of(123); + assertEquals(true, gValue1.equals(gValue2)); + } + + @Test + public void equalsShouldReturnFalseForDifferentNames() { + final GValue<Integer> gValue1 = GValue.of("name1", 123); + final GValue<Integer> gValue2 = GValue.of("name2", 123); + assertEquals(false, gValue1.equals(gValue2)); + } + + @Test + public void equalsShouldReturnFalseForDifferentTypes() { + final GValue<Integer> gValue1 = GValue.of(123); + final GValue<String> gValue2 = GValue.of("123"); + assertEquals(false, gValue1.equals(gValue2)); + } + + @Test + public void equalsShouldReturnFalseForDifferentValues() { + final GValue<Integer> gValue1 = GValue.of(123); + final GValue<Integer> gValue2 = GValue.of(456); + assertEquals(false, gValue1.equals(gValue2)); + } + + @Test + public void valueOfShouldReturnGValueValue() { + final GValue<Integer> gValue = GValue.of(123); + assertEquals(123, GValue.valueOf(gValue)); + } + + @Test + public void valueOfShouldReturnObjectAsIs() { + final String value = "test"; + assertEquals("test", GValue.valueOf(value)); + } + + @Test + public void valueOfShouldReturnNullForNullInput() { + assertNull(GValue.valueOf(null)); + assertNull(null); + } } \ No newline at end of file diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/util/CollectionUtilTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/util/CollectionUtilTest.java index 73bd39b415..7b938c1a22 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/util/CollectionUtilTest.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/util/CollectionUtilTest.java @@ -22,10 +22,20 @@ import org.apache.tinkerpop.gremlin.AssertHelper; import org.junit.Test; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; public class CollectionUtilTest { @@ -70,4 +80,104 @@ public class CollectionUtilTest { final ConcurrentHashMap<?, ?> cloned = CollectionUtil.clone(source); assertTrue(source.equals(cloned)); } + + @Test + public void shouldCloneEmptyConcurrentHashMap() { + final ConcurrentHashMap<String, String> source = new ConcurrentHashMap<>(); + final ConcurrentHashMap<?, ?> cloned = CollectionUtil.clone(source); + assertTrue(source.equals(cloned)); + } + + @Test + public void shouldCloneConcurrentHashMapWithMixedTypes() { + final ConcurrentHashMap<String, Object> source = new ConcurrentHashMap<>(); + source.put("key1", "value1"); + source.put("key2", new ArrayList<>(Arrays.asList("a", "b"))); + source.put("key3", new HashSet<>(Arrays.asList("x", "y"))); + + final ConcurrentHashMap<?, ?> cloned = CollectionUtil.clone(source); + assertTrue(source.equals(cloned)); + } + + @Test + public void shouldAddFirstWhenBothArgumentsNull() { + String[] result = CollectionUtil.addFirst(null, null, String.class); + assertArrayEquals(new String[]{null}, result); + } + + @Test + public void shouldAddFirstWhenArrayNull() { + String[] result = CollectionUtil.addFirst(null, "element", String.class); + assertArrayEquals(new String[]{"element"}, result); + } + + @Test + public void shoulAddFirstWhenNeitherArgumentNull() { + Integer[] array = {1, 2, 3}; + Integer[] result = CollectionUtil.addFirst(array, 0, Integer.class); + assertArrayEquals(new Integer[]{0, 1, 2, 3}, result); + } + + @Test + public void shouldAddFirstWhenEmptyArray() { + String[] array = {}; + String[] result = CollectionUtil.addFirst(array, "element", String.class); + assertArrayEquals(new String[]{"element"}, result); + } + + @Test + public void shouldAddFirstWhenIntegers() { + Integer[] array = {1, 2, 3}; + Integer[] result = CollectionUtil.addFirst(array, 0, Integer.class); + assertArrayEquals(new Integer[]{0, 1, 2, 3}, result); + } + + @Test + public void shouldConvertVarargsToList() { + List<String> result = CollectionUtil.asList("a", "b", "c"); + assertEquals(Arrays.asList("a", "b", "c"), result); + } + + @Test + public void shouldConvertEmptyVarargsToList() { + List<String> result = CollectionUtil.asList(); + assertEquals(Collections.emptyList(), result); + } + + @Test + public void shouldConvertVarargsToSet() { + Set<String> result = CollectionUtil.asSet("a", "b", "c"); + assertEquals(new LinkedHashSet<>(Arrays.asList("a", "b", "c")), result); + } + + @Test + public void shouldConvertEmptyVarargsToSet() { + Set<String> result = CollectionUtil.asSet(); + assertEquals(new LinkedHashSet<>(), result); + } + + @Test + public void shouldConvertCollectionToSet() { + Collection<String> collection = Arrays.asList("a", "b", "c"); + Set<String> result = CollectionUtil.asSet(collection); + assertEquals(new LinkedHashSet<>(collection), result); + } + + @Test + public void shouldConvertVarargsToMap() { + Map<String, String> result = CollectionUtil.asMap("key1", "value1", "key2", "value2"); + Map<String, String> expected = new LinkedHashMap<>(); + expected.put("key1", "value1"); + expected.put("key2", "value2"); + assertEquals(expected, result); + } + + @Test + public void shouldConvertVarargsToMapWithOddNumberOfElements() { + Map<String, String> result = CollectionUtil.asMap("key1", "value1", "key2"); + Map<String, String> expected = new LinkedHashMap<>(); + expected.put("key1", "value1"); + expected.put("key2", null); + assertEquals(expected, result); + } }
