| // Copyright 2021 The Bazel Authors. All rights reserved. | |
| // | |
| // Licensed 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 com.google.devtools.build.lib.packages; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static org.junit.Assert.assertThrows; | |
| import com.google.auto.value.AutoValue; | |
| import com.google.common.base.Preconditions; | |
| import com.google.common.collect.BiMap; | |
| import com.google.common.collect.ImmutableBiMap; | |
| import com.google.common.collect.ImmutableMap; | |
| import java.util.AbstractMap; | |
| import java.util.Collection; | |
| import java.util.Iterator; | |
| import java.util.Map; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.JUnit4; | |
| /** Tests for {@link SnapshottableBiMap}. */ | |
| @RunWith(JUnit4.class) | |
| public final class SnapshottableBiMapTest { | |
| // Dummy value type for maps under test. AutoValue for correct hash/equals behavior. | |
| @AutoValue | |
| abstract static class Value { | |
| static Value trackedOf(String name) { | |
| return new AutoValue_SnapshottableBiMapTest_Value(name, true); | |
| } | |
| static Value untrackedOf(String name) { | |
| return new AutoValue_SnapshottableBiMapTest_Value(name, false); | |
| } | |
| static boolean track(Value value) { | |
| return value.tracked(); | |
| } | |
| abstract String name(); | |
| abstract boolean tracked(); | |
| } | |
| private static <E> void verifyCollectionSizeAndContentsInOrder( | |
| Collection<E> collection, Collection<E> expected) { | |
| // Exhaustive testing of a collection's methods; we cannot rely on a minimal usual set of JUnit | |
| // helpers because we want to verify that the collection has valid Collection semantics. | |
| if (expected.isEmpty()) { | |
| assertThat(collection).isEmpty(); | |
| } else { | |
| assertThat(collection).isNotEmpty(); | |
| } | |
| assertThat(collection).hasSize(expected.size()); | |
| assertThat(collection).containsExactlyElementsIn(expected).inOrder(); | |
| for (E entry : expected) { | |
| // JUnit's containsExactlyElementsIn iterates over the collection under test, but doesn't call | |
| // its contains() method. | |
| assertThat(collection).contains(entry); | |
| } | |
| } | |
| private static <K, V> void verifyMapSizeAndContentsInOrder(Map<K, V> map, Map<K, V> expectedMap) { | |
| // Exhaustive testing of a map's methods; we cannot rely on a minimal usual set of JUnit helpers | |
| // because we want to verify that the map has valid Map semantics. | |
| if (expectedMap.isEmpty()) { | |
| assertThat(map).isEmpty(); | |
| } else { | |
| assertThat(map).isNotEmpty(); | |
| } | |
| assertThat(map).hasSize(expectedMap.size()); | |
| assertThat(map).containsExactlyEntriesIn(expectedMap).inOrder(); | |
| for (Map.Entry<K, V> entry : expectedMap.entrySet()) { | |
| assertThat(map.containsKey(entry.getKey())) | |
| .isTrue(); // JUnit's containsKey implementation does not explicitly call map.containsKey | |
| assertThat(map.containsValue(entry.getValue())).isTrue(); | |
| } | |
| verifyCollectionSizeAndContentsInOrder(map.entrySet(), expectedMap.entrySet()); | |
| verifyCollectionSizeAndContentsInOrder(map.keySet(), expectedMap.keySet()); | |
| verifyCollectionSizeAndContentsInOrder(map.values(), expectedMap.values()); | |
| } | |
| @SuppressWarnings("unchecked") // test-only convenience vararg transformation | |
| private static <K, V> void verifyMapSizeAndContentsInOrder( | |
| Map<K, V> map, K key0, V value0, Object... rest) { | |
| ImmutableMap.Builder<K, V> expectedBuilder = ImmutableMap.builder(); | |
| expectedBuilder.put(key0, value0); | |
| Preconditions.checkArgument( | |
| rest.length % 2 == 0, "rest must be a flattened list of key-value pairs"); | |
| for (int i = 0; i < rest.length; i += 2) { | |
| expectedBuilder.put((K) rest[i], (V) rest[i + 1]); | |
| } | |
| Map<K, V> expectedMap = expectedBuilder.build(); | |
| verifyMapSizeAndContentsInOrder(map, expectedMap); | |
| } | |
| private static <K, V> void verifyMapDoesNotContainEntry(Map<K, V> map, K key, V value) { | |
| Map.Entry<K, V> entry = new AbstractMap.SimpleEntry<>(key, value); | |
| // Exhaustive testing of a map's methods; we cannot rely on a minimal usual set of JUnit helpers | |
| // because we want to verify that the map has valid Map semantics. | |
| assertThat(map.containsKey(key)) | |
| .isFalse(); // JUnit's containsKey implementation does not explicitly call map.containsKeys | |
| assertThat(map.containsValue(value)).isFalse(); | |
| assertThat(map.entrySet()).doesNotContain(entry); | |
| assertThat(map.keySet()).doesNotContain(key); | |
| assertThat(map.values()).doesNotContain(value); | |
| } | |
| private static <K, V> void verifyMapIsEmpty(Map<K, V> map) { | |
| verifyMapSizeAndContentsInOrder(map, ImmutableMap.of()); | |
| } | |
| private static <E> void verifyIteratorDoesNotAllowDeletions(Iterator<E> iterator) { | |
| while (iterator.hasNext()) { | |
| iterator.next(); | |
| assertThrows(UnsupportedOperationException.class, iterator::remove); | |
| } | |
| } | |
| private static <K, V> void verifyMapDoesNotAllowDeletions(Map<K, V> map) { | |
| for (Map.Entry<K, V> entry : map.entrySet()) { | |
| K key = entry.getKey(); | |
| V value = entry.getValue(); | |
| assertThrows(UnsupportedOperationException.class, () -> map.remove(key)); | |
| assertThrows(UnsupportedOperationException.class, () -> map.keySet().remove(key)); | |
| assertThrows(UnsupportedOperationException.class, () -> map.values().remove(value)); | |
| assertThrows(UnsupportedOperationException.class, () -> map.entrySet().remove(entry)); | |
| } | |
| verifyIteratorDoesNotAllowDeletions(map.keySet().iterator()); | |
| verifyIteratorDoesNotAllowDeletions(map.values().iterator()); | |
| verifyIteratorDoesNotAllowDeletions(map.entrySet().iterator()); | |
| assertThrows(UnsupportedOperationException.class, map::clear); | |
| } | |
| @SuppressWarnings("unchecked") // test-only convenience vararg transformation | |
| private static <K, V> void verifyBiMapSizeAndContentsInOrder( | |
| BiMap<K, V> bimap, K key0, V value0, Object... rest) { | |
| ImmutableBiMap.Builder<K, V> expectedBuilder = ImmutableBiMap.builder(); | |
| expectedBuilder.put(key0, value0); | |
| Preconditions.checkArgument( | |
| rest.length % 2 == 0, "rest must be a flattened list of key-value pairs"); | |
| for (int i = 0; i < rest.length; i += 2) { | |
| expectedBuilder.put((K) rest[i], (V) rest[i + 1]); | |
| } | |
| BiMap<K, V> expectedBiMap = expectedBuilder.buildOrThrow(); | |
| verifyMapSizeAndContentsInOrder(bimap, expectedBiMap); | |
| verifyMapSizeAndContentsInOrder(bimap.inverse(), expectedBiMap.inverse()); | |
| } | |
| private static <K, V> void verifyBiMapIsEmpty(BiMap<K, V> bimap) { | |
| verifyMapSizeAndContentsInOrder(bimap, ImmutableMap.of()); | |
| verifyMapSizeAndContentsInOrder(bimap.inverse(), ImmutableMap.of()); | |
| } | |
| @Test | |
| public void containsInsertedEntries() { | |
| SnapshottableBiMap<String, Value> map = new SnapshottableBiMap<>(Value::track); | |
| verifyBiMapIsEmpty(map); | |
| Value a = Value.trackedOf("a"); | |
| Value b = Value.untrackedOf("b"); | |
| Value c = Value.trackedOf("c"); | |
| Value z = Value.trackedOf("z"); | |
| map.put("a", a); | |
| verifyBiMapSizeAndContentsInOrder(map, "a", a); | |
| map.put("b", b); | |
| verifyBiMapSizeAndContentsInOrder(map, "a", a, "b", b); | |
| map.put("c", c); | |
| verifyBiMapSizeAndContentsInOrder(map, "a", a, "b", b, "c", c); | |
| // verify that the map's various contains*() methods don't always return true. | |
| verifyMapDoesNotContainEntry(map, "z", z); | |
| } | |
| @Test | |
| public void put_replacesEntries() { | |
| SnapshottableBiMap<String, Value> map = new SnapshottableBiMap<>(Value::track); | |
| Value trackedA = Value.trackedOf("a"); | |
| Value replaceA = Value.trackedOf("replace a"); | |
| Value untrackedB = Value.untrackedOf("b"); | |
| Value replaceB = Value.untrackedOf("b"); | |
| map.put("a", trackedA); | |
| map.put("a", replaceA); | |
| map.put("b", untrackedB); | |
| map.put("b", replaceB); | |
| verifyBiMapSizeAndContentsInOrder(map, "a", replaceA, "b", replaceB); | |
| } | |
| @Test | |
| public void put_nonUniqueValue_illegal() { | |
| SnapshottableBiMap<String, Value> map = new SnapshottableBiMap<>(Value::track); | |
| Value tracked = Value.trackedOf("a"); | |
| Value untracked = Value.untrackedOf("b"); | |
| map.put("a", tracked); | |
| assertThrows(IllegalArgumentException.class, () -> map.put("aa", tracked)); | |
| map.put("b", untracked); | |
| assertThrows(IllegalArgumentException.class, () -> map.put("bb", untracked)); | |
| } | |
| @Test | |
| public void put_replacingUntrackedWithTracked_legal() { | |
| SnapshottableBiMap<String, Value> map = new SnapshottableBiMap<>(Value::track); | |
| Value tracked = Value.trackedOf("a"); | |
| Value untracked = Value.untrackedOf("A"); | |
| map.getTrackedSnapshot(); // start tracking | |
| map.put("a", untracked); | |
| map.put("a", tracked); | |
| verifyBiMapSizeAndContentsInOrder(map, "a", tracked); | |
| } | |
| @Test | |
| public void put_replacingTrackedWithUntracked_illegal() { | |
| SnapshottableBiMap<String, Value> map = new SnapshottableBiMap<>(Value::track); | |
| Value tracked = Value.trackedOf("a"); | |
| Value untracked = Value.untrackedOf("A"); | |
| map.getTrackedSnapshot(); // start tracking | |
| map.put("a", tracked); | |
| assertThrows(IllegalArgumentException.class, () -> map.put("a", untracked)); | |
| } | |
| @Test | |
| @SuppressWarnings("deprecation") // test verifying that deprecated methods don't work | |
| public void deletions_unsupported() { | |
| SnapshottableBiMap<String, Value> map = new SnapshottableBiMap<>(Value::track); | |
| Value value = Value.trackedOf("a"); | |
| Value replacement = Value.trackedOf("replacement a"); | |
| map.put("a", value); | |
| verifyMapDoesNotAllowDeletions(map); | |
| verifyMapDoesNotAllowDeletions(map.inverse()); | |
| assertThrows(UnsupportedOperationException.class, () -> map.forcePut("a", replacement)); | |
| assertThrows(UnsupportedOperationException.class, () -> map.inverse().forcePut(value, "aa")); | |
| } | |
| @Test | |
| public void getUnderlyingBiMap_returnsBiMapSupportingRemove() { | |
| SnapshottableBiMap<String, Value> map = new SnapshottableBiMap<>(Value::track); | |
| Value a = Value.trackedOf("a"); | |
| Value b = Value.untrackedOf("b"); | |
| Value c = Value.trackedOf("c"); | |
| map.put("a", a); | |
| map.put("b", b); | |
| map.put("c", c); | |
| BiMap<String, Value> underlying = map.getUnderlyingBiMap(); | |
| verifyBiMapSizeAndContentsInOrder(underlying, "a", a, "b", b, "c", c); | |
| underlying.remove("a"); | |
| verifyBiMapSizeAndContentsInOrder(underlying, "b", b, "c", c); | |
| } | |
| @Test | |
| public void snapshot_containsExpectedEntries() { | |
| SnapshottableBiMap<String, Value> map = new SnapshottableBiMap<>(Value::track); | |
| Value trackedA = Value.trackedOf("a"); | |
| Value untrackedB = Value.untrackedOf("b"); | |
| Value trackedC = Value.trackedOf("c"); | |
| Value z = Value.trackedOf("z"); | |
| Map<String, Value> snapshot0 = map.getTrackedSnapshot(); | |
| verifyMapIsEmpty(snapshot0); | |
| map.put("a", trackedA); | |
| Map<String, Value> snapshot1 = map.getTrackedSnapshot(); | |
| verifyMapIsEmpty(snapshot0); | |
| verifyMapSizeAndContentsInOrder(snapshot1, "a", trackedA); | |
| map.put("b", untrackedB); | |
| Map<String, Value> snapshot2 = map.getTrackedSnapshot(); | |
| verifyMapIsEmpty(snapshot0); | |
| verifyMapSizeAndContentsInOrder(snapshot1, "a", trackedA); | |
| verifyMapSizeAndContentsInOrder(snapshot2, "a", trackedA); // b is untracked | |
| map.put("c", Value.trackedOf("c")); | |
| Map<String, Value> snapshot3 = map.getTrackedSnapshot(); | |
| verifyMapIsEmpty(snapshot0); | |
| verifyMapSizeAndContentsInOrder(snapshot1, "a", trackedA); // c was added after snapshot | |
| verifyMapSizeAndContentsInOrder(snapshot2, "a", trackedA); | |
| verifyMapSizeAndContentsInOrder(snapshot3, "a", trackedA, "c", trackedC); | |
| // verify that a snapshot's various contains*() methods don't always return true. | |
| verifyMapDoesNotContainEntry(snapshot1, "z", z); | |
| verifyMapDoesNotContainEntry(snapshot2, "z", z); | |
| verifyMapDoesNotContainEntry(snapshot3, "z", z); | |
| } | |
| @Test | |
| public void snapshot_isUnmodifiable() { | |
| SnapshottableBiMap<String, Value> map = new SnapshottableBiMap<>(Value::track); | |
| map.put("a", Value.trackedOf("a")); | |
| map.put("b", Value.untrackedOf("b")); | |
| map.put("c", Value.trackedOf("c")); | |
| Map<String, Value> snapshot = map.getTrackedSnapshot(); | |
| verifyMapDoesNotAllowDeletions(snapshot); | |
| assertThrows( | |
| UnsupportedOperationException.class, () -> snapshot.put("a", Value.trackedOf("replace a"))); | |
| assertThrows( | |
| UnsupportedOperationException.class, () -> snapshot.put("d", Value.trackedOf("d"))); | |
| } | |
| @Test | |
| public void snapshot_containsReplacementsPerformedBeforeSnapshotCreation() { | |
| SnapshottableBiMap<String, Value> map = new SnapshottableBiMap<>(Value::track); | |
| Value trackedA = Value.trackedOf("a"); | |
| Value replacementA = Value.trackedOf("replacement a"); | |
| Value untrackedB = Value.untrackedOf("b"); | |
| Value replacementB = Value.trackedOf("replacement b"); | |
| map.put("a", trackedA); | |
| map.put("b", untrackedB); | |
| verifyMapSizeAndContentsInOrder(map, "a", trackedA, "b", untrackedB); | |
| map.put("a", replacementA); | |
| map.put("b", replacementB); | |
| verifyMapSizeAndContentsInOrder(map, "a", replacementA, "b", replacementB); | |
| Map<String, Value> snapshot = map.getTrackedSnapshot(); | |
| verifyMapSizeAndContentsInOrder(snapshot, "a", replacementA, "b", replacementB); | |
| } | |
| @Test | |
| public void snapshot_afterReplacingEntryInSnapshot_containsReplacement() { | |
| SnapshottableBiMap<String, Value> map = new SnapshottableBiMap<>(Value::track); | |
| Value original = Value.trackedOf("a"); | |
| Value replacement = Value.trackedOf("replacement a"); | |
| map.put("a", original); | |
| Map<String, Value> snapshot = map.getTrackedSnapshot(); | |
| verifyMapSizeAndContentsInOrder(snapshot, "a", original); | |
| map.put("a", replacement); | |
| verifyMapSizeAndContentsInOrder(snapshot, "a", replacement); | |
| } | |
| @Test | |
| public void snapshot_afterReplacingEntryNotInSnapshot_doesNotContainReplacement() { | |
| SnapshottableBiMap<String, Value> map = new SnapshottableBiMap<>(Value::track); | |
| Value untrackedA = Value.untrackedOf("a"); | |
| Value replacementA = Value.trackedOf("replacement a"); | |
| Value trackedB = Value.trackedOf("b"); | |
| Value replacementB = Value.trackedOf("replacement b"); | |
| map.put("a", untrackedA); | |
| Map<String, Value> snapshot = map.getTrackedSnapshot(); | |
| verifyMapIsEmpty(snapshot); | |
| map.put("a", replacementA); | |
| map.put("b", trackedB); | |
| map.put("b", replacementB); | |
| verifyMapSizeAndContentsInOrder(map, "a", replacementA, "b", replacementB); | |
| verifyMapIsEmpty(snapshot); | |
| } | |
| @Test | |
| public void snapshot_containsReplacementEntries_inOriginalKeyInsertionOrder() { | |
| SnapshottableBiMap<String, Value> map = new SnapshottableBiMap<>(Value::track); | |
| Value a = Value.trackedOf("a"); | |
| Value b = Value.trackedOf("b"); | |
| Value replaceB = Value.trackedOf("replacement b"); | |
| Value c = Value.trackedOf("c"); | |
| Value replaceC = Value.trackedOf("replacement c"); | |
| map.put("a", a); | |
| map.put("b", b); | |
| map.put("c", c); | |
| Map<String, Value> snapshot = map.getTrackedSnapshot(); | |
| verifyMapSizeAndContentsInOrder(snapshot, "a", a, "b", b, "c", c); | |
| map.put("c", replaceC); | |
| map.put("b", replaceB); | |
| verifyMapSizeAndContentsInOrder(snapshot, "a", a, "b", replaceB, "c", replaceC); | |
| } | |
| } |