/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.elasticsearch.cluster.serialization; import org.elasticsearch.cluster.AbstractDiffable; import org.elasticsearch.cluster.Diff; import org.elasticsearch.cluster.DiffableUtils; import org.elasticsearch.cluster.DiffableUtils.MapDiff; import org.elasticsearch.common.collect.ImmutableOpenIntMap; import org.elasticsearch.common.collect.ImmutableOpenMap; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.test.ESTestCase; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import static java.util.Collections.emptyMap; import static java.util.Collections.unmodifiableMap; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.nullValue; public class DiffableTests extends ESTestCase { public void testJKDMapDiff() throws IOException { new JdkMapDriver<TestDiffable>() { @Override protected boolean diffableValues() { return true; } @Override protected TestDiffable createValue(Integer key, boolean before) { return new TestDiffable(String.valueOf(before ? key : key + 1)); } @Override protected MapDiff diff(Map<Integer, TestDiffable> before, Map<Integer, TestDiffable> after) { return DiffableUtils.diff(before, after, keySerializer); } @Override protected MapDiff readDiff(StreamInput in) throws IOException { return useProtoForDiffableSerialization ? DiffableUtils.readJdkMapDiff(in, keySerializer, TestDiffable::readFrom, TestDiffable::readDiffFrom) : DiffableUtils.readJdkMapDiff(in, keySerializer, diffableValueSerializer()); } }.execute(); new JdkMapDriver<String>() { @Override protected boolean diffableValues() { return false; } @Override protected String createValue(Integer key, boolean before) { return String.valueOf(before ? key : key + 1); } @Override protected MapDiff diff(Map<Integer, String> before, Map<Integer, String> after) { return DiffableUtils.diff(before, after, keySerializer, nonDiffableValueSerializer()); } @Override protected MapDiff readDiff(StreamInput in) throws IOException { return DiffableUtils.readJdkMapDiff(in, keySerializer, nonDiffableValueSerializer()); } }.execute(); } public void testImmutableOpenMapDiff() throws IOException { new ImmutableOpenMapDriver<TestDiffable>() { @Override protected boolean diffableValues() { return true; } @Override protected TestDiffable createValue(Integer key, boolean before) { return new TestDiffable(String.valueOf(before ? key : key + 1)); } @Override protected MapDiff diff(ImmutableOpenMap<Integer, TestDiffable> before, ImmutableOpenMap<Integer, TestDiffable> after) { return DiffableUtils.diff(before, after, keySerializer); } @Override protected MapDiff readDiff(StreamInput in) throws IOException { return useProtoForDiffableSerialization ? DiffableUtils.readImmutableOpenMapDiff(in, keySerializer, TestDiffable::readFrom, TestDiffable::readDiffFrom) : DiffableUtils.readImmutableOpenMapDiff(in, keySerializer, diffableValueSerializer()); } }.execute(); new ImmutableOpenMapDriver<String>() { @Override protected boolean diffableValues() { return false; } @Override protected String createValue(Integer key, boolean before) { return String.valueOf(before ? key : key + 1); } @Override protected MapDiff diff(ImmutableOpenMap<Integer, String> before, ImmutableOpenMap<Integer, String> after) { return DiffableUtils.diff(before, after, keySerializer, nonDiffableValueSerializer()); } @Override protected MapDiff readDiff(StreamInput in) throws IOException { return DiffableUtils.readImmutableOpenMapDiff(in, keySerializer, nonDiffableValueSerializer()); } }.execute(); } public void testImmutableOpenIntMapDiff() throws IOException { new ImmutableOpenIntMapDriver<TestDiffable>() { @Override protected boolean diffableValues() { return true; } @Override protected TestDiffable createValue(Integer key, boolean before) { return new TestDiffable(String.valueOf(before ? key : key + 1)); } @Override protected MapDiff diff(ImmutableOpenIntMap<TestDiffable> before, ImmutableOpenIntMap<TestDiffable> after) { return DiffableUtils.diff(before, after, keySerializer); } @Override protected MapDiff readDiff(StreamInput in) throws IOException { return useProtoForDiffableSerialization ? DiffableUtils.readImmutableOpenIntMapDiff(in, keySerializer, TestDiffable::readFrom, TestDiffable::readDiffFrom) : DiffableUtils.readImmutableOpenIntMapDiff(in, keySerializer, diffableValueSerializer()); } }.execute(); new ImmutableOpenIntMapDriver<String>() { @Override protected boolean diffableValues() { return false; } @Override protected String createValue(Integer key, boolean before) { return String.valueOf(before ? key : key + 1); } @Override protected MapDiff diff(ImmutableOpenIntMap<String> before, ImmutableOpenIntMap<String> after) { return DiffableUtils.diff(before, after, keySerializer, nonDiffableValueSerializer()); } @Override protected MapDiff readDiff(StreamInput in) throws IOException { return DiffableUtils.readImmutableOpenIntMapDiff(in, keySerializer, nonDiffableValueSerializer()); } }.execute(); } /** * Class that abstracts over specific map implementation type and value kind (Diffable or not) * @param <T> map type * @param <V> value type */ public abstract class MapDriver<T, V> { protected final Set<Integer> keys = randomPositiveIntSet(); protected final Set<Integer> keysToRemove = new HashSet<>(randomSubsetOf(randomInt(keys.size()), keys.toArray(new Integer[keys.size()]))); protected final Set<Integer> keysThatAreNotRemoved = Sets.difference(keys, keysToRemove); protected final Set<Integer> keysToOverride = new HashSet<>(randomSubsetOf(randomInt(keysThatAreNotRemoved.size()), keysThatAreNotRemoved.toArray(new Integer[keysThatAreNotRemoved.size()]))); protected final Set<Integer> keysToAdd = Sets.difference(randomPositiveIntSet(), keys); // make sure keysToAdd does not contain elements in keys protected final Set<Integer> keysUnchanged = Sets.difference(keysThatAreNotRemoved, keysToOverride); protected final DiffableUtils.KeySerializer<Integer> keySerializer = randomBoolean() ? DiffableUtils.getIntKeySerializer() : DiffableUtils.getVIntKeySerializer(); protected final boolean useProtoForDiffableSerialization = randomBoolean(); private Set<Integer> randomPositiveIntSet() { int maxSetSize = randomInt(6); Set<Integer> result = new HashSet<>(); for (int i = 0; i < maxSetSize; i++) { // due to duplicates, set size can be smaller than maxSetSize result.add(randomIntBetween(0, 100)); } return result; } /** * whether we operate on {@link org.elasticsearch.cluster.Diffable} values */ protected abstract boolean diffableValues(); /** * functions that determines value in "before" or "after" map based on key */ protected abstract V createValue(Integer key, boolean before); /** * creates map based on JDK-based map */ protected abstract T createMap(Map<Integer, V> values); /** * calculates diff between two maps */ protected abstract MapDiff<Integer, V, T> diff(T before, T after); /** * reads diff of maps from stream */ protected abstract MapDiff<Integer, V, T> readDiff(StreamInput in) throws IOException; /** * gets element at key "key" in map "map" */ protected abstract V get(T map, Integer key); /** * returns size of given map */ protected abstract int size(T map); /** * executes the actual test */ public void execute() throws IOException { logger.debug("Keys in 'before' map: {}", keys); logger.debug("Keys to remove: {}", keysToRemove); logger.debug("Keys to override: {}", keysToOverride); logger.debug("Keys to add: {}", keysToAdd); logger.debug("--> creating 'before' map"); Map<Integer, V> before = new HashMap<>(); for (Integer key : keys) { before.put(key, createValue(key, true)); } T beforeMap = createMap(before); logger.debug("--> creating 'after' map"); Map<Integer, V> after = new HashMap<>(); after.putAll(before); for (Integer key : keysToRemove) { after.remove(key); } for (Integer key : keysToOverride) { after.put(key, createValue(key, false)); } for (Integer key : keysToAdd) { after.put(key, createValue(key, false)); } T afterMap = createMap(unmodifiableMap(after)); MapDiff<Integer, V, T> diffMap = diff(beforeMap, afterMap); // check properties of diffMap assertThat(new HashSet(diffMap.getDeletes()), equalTo(keysToRemove)); if (diffableValues()) { assertThat(diffMap.getDiffs().keySet(), equalTo(keysToOverride)); for (Integer key : keysToOverride) { assertThat(diffMap.getDiffs().get(key).apply(get(beforeMap, key)), equalTo(get(afterMap, key))); } assertThat(diffMap.getUpserts().keySet(), equalTo(keysToAdd)); for (Integer key : keysToAdd) { assertThat(diffMap.getUpserts().get(key), equalTo(get(afterMap, key))); } } else { assertThat(diffMap.getDiffs(), equalTo(emptyMap())); Set<Integer> keysToAddAndOverride = Sets.union(keysToAdd, keysToOverride); assertThat(diffMap.getUpserts().keySet(), equalTo(keysToAddAndOverride)); for (Integer key : keysToAddAndOverride) { assertThat(diffMap.getUpserts().get(key), equalTo(get(afterMap, key))); } } if (randomBoolean()) { logger.debug("--> serializing diff"); BytesStreamOutput out = new BytesStreamOutput(); diffMap.writeTo(out); StreamInput in = out.bytes().streamInput(); logger.debug("--> reading diff back"); diffMap = readDiff(in); } T appliedDiffMap = diffMap.apply(beforeMap); // check properties of appliedDiffMap assertThat(size(appliedDiffMap), equalTo(keys.size() - keysToRemove.size() + keysToAdd.size())); for (Integer key : keysToRemove) { assertThat(get(appliedDiffMap, key), nullValue()); } for (Integer key : keysUnchanged) { assertThat(get(appliedDiffMap, key), equalTo(get(beforeMap, key))); } for (Integer key : keysToOverride) { assertThat(get(appliedDiffMap, key), not(equalTo(get(beforeMap, key)))); assertThat(get(appliedDiffMap, key), equalTo(get(afterMap, key))); } for (Integer key : keysToAdd) { assertThat(get(appliedDiffMap, key), equalTo(get(afterMap, key))); } } } abstract class JdkMapDriver<V> extends MapDriver<Map<Integer, V>, V> { @Override protected Map<Integer, V> createMap(Map values) { return values; } @Override protected V get(Map<Integer, V> map, Integer key) { return map.get(key); } @Override protected int size(Map<Integer, V> map) { return map.size(); } } abstract class ImmutableOpenMapDriver<V> extends MapDriver<ImmutableOpenMap<Integer, V>, V> { @Override protected ImmutableOpenMap<Integer, V> createMap(Map values) { return ImmutableOpenMap.<Integer, V>builder().putAll(values).build(); } @Override protected V get(ImmutableOpenMap<Integer, V> map, Integer key) { return map.get(key); } @Override protected int size(ImmutableOpenMap<Integer, V> map) { return map.size(); } } abstract class ImmutableOpenIntMapDriver<V> extends MapDriver<ImmutableOpenIntMap<V>, V> { @Override protected ImmutableOpenIntMap<V> createMap(Map values) { return ImmutableOpenIntMap.<V>builder().putAll(values).build(); } @Override protected V get(ImmutableOpenIntMap<V> map, Integer key) { return map.get(key); } @Override protected int size(ImmutableOpenIntMap<V> map) { return map.size(); } } private static <K> DiffableUtils.DiffableValueSerializer<K, TestDiffable> diffableValueSerializer() { return new DiffableUtils.DiffableValueSerializer<K, TestDiffable>() { @Override public TestDiffable read(StreamInput in, K key) throws IOException { return new TestDiffable(in.readString()); } @Override public Diff<TestDiffable> readDiff(StreamInput in, K key) throws IOException { return AbstractDiffable.readDiffFrom(TestDiffable::readFrom, in); } }; } private static <K> DiffableUtils.NonDiffableValueSerializer<K, String> nonDiffableValueSerializer() { return new DiffableUtils.NonDiffableValueSerializer<K, String>() { @Override public void write(String value, StreamOutput out) throws IOException { out.writeString(value); } @Override public String read(StreamInput in, K key) throws IOException { return in.readString(); } }; } public static class TestDiffable extends AbstractDiffable<TestDiffable> { private final String value; public TestDiffable(String value) { this.value = value; } public String value() { return value; } public static TestDiffable readFrom(StreamInput in) throws IOException { return new TestDiffable(in.readString()); } public static Diff<TestDiffable> readDiffFrom(StreamInput in) throws IOException { return readDiffFrom(TestDiffable::readFrom, in); } @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(value); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TestDiffable that = (TestDiffable) o; return !(value != null ? !value.equals(that.value) : that.value != null); } @Override public int hashCode() { return value != null ? value.hashCode() : 0; } } }