001/*
002 * Copyright (C) 2007 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.google.common.collect.testing.google;
018
019import static com.google.common.collect.Maps.immutableEntry;
020import static java.util.Collections.singleton;
021import static java.util.Collections.unmodifiableList;
022import static junit.framework.TestCase.assertEquals;
023import static junit.framework.TestCase.assertTrue;
024import static junit.framework.TestCase.fail;
025
026import com.google.common.annotations.GwtCompatible;
027import com.google.common.collect.ArrayListMultimap;
028import com.google.common.collect.LinkedHashMultiset;
029import com.google.common.collect.Lists;
030import com.google.common.collect.Multimap;
031import com.google.common.collect.Multiset;
032import java.util.ArrayList;
033import java.util.Collection;
034import java.util.Iterator;
035import java.util.List;
036import java.util.Map.Entry;
037import java.util.Set;
038import org.checkerframework.checker.nullness.qual.Nullable;
039
040/**
041 * A series of tests that support asserting that collections cannot be modified, either through
042 * direct or indirect means.
043 *
044 * @author Robert Konigsberg
045 */
046@GwtCompatible
047@ElementTypesAreNonnullByDefault
048public class UnmodifiableCollectionTests {
049
050  public static void assertMapEntryIsUnmodifiable(Entry<?, ?> entry) {
051    try {
052      // fine because the call is going to fail without modifying the entry
053      @SuppressWarnings("unchecked")
054      Entry<?, @Nullable Object> nullableValueEntry = (Entry<?, @Nullable Object>) entry;
055      nullableValueEntry.setValue(null);
056      fail("setValue on unmodifiable Map.Entry succeeded");
057    } catch (UnsupportedOperationException expected) {
058    }
059  }
060
061  /**
062   * Verifies that an Iterator is unmodifiable.
063   *
064   * <p>This test only works with iterators that iterate over a finite set.
065   */
066  public static void assertIteratorIsUnmodifiable(Iterator<?> iterator) {
067    while (iterator.hasNext()) {
068      iterator.next();
069      try {
070        iterator.remove();
071        fail("Remove on unmodifiable iterator succeeded");
072      } catch (UnsupportedOperationException expected) {
073      }
074    }
075  }
076
077  /**
078   * Asserts that two iterators contain elements in tandem.
079   *
080   * <p>This test only works with iterators that iterate over a finite set.
081   */
082  public static void assertIteratorsInOrder(
083      Iterator<?> expectedIterator, Iterator<?> actualIterator) {
084    int i = 0;
085    while (expectedIterator.hasNext()) {
086      Object expected = expectedIterator.next();
087
088      assertTrue(
089          "index " + i + " expected <" + expected + "., actual is exhausted",
090          actualIterator.hasNext());
091
092      Object actual = actualIterator.next();
093      assertEquals("index " + i, expected, actual);
094      i++;
095    }
096    if (actualIterator.hasNext()) {
097      fail("index " + i + ", expected is exhausted, actual <" + actualIterator.next() + ">");
098    }
099  }
100
101  /**
102   * Verifies that a collection is immutable.
103   *
104   * <p>A collection is considered immutable if:
105   *
106   * <ol>
107   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
108   *       underlying contents.
109   *   <li>All methods that return objects that can indirectly mutate the collection throw
110   *       UnsupportedOperationException when those mutators are called.
111   * </ol>
112   *
113   * @param collection the presumed-immutable collection
114   * @param sampleElement an element of the same type as that contained by {@code collection}.
115   *     {@code collection} may or may not have {@code sampleElement} as a member.
116   */
117  public static <E extends @Nullable Object> void assertCollectionIsUnmodifiable(
118      Collection<E> collection, E sampleElement) {
119    Collection<E> siblingCollection = new ArrayList<>();
120    siblingCollection.add(sampleElement);
121
122    Collection<E> copy = new ArrayList<>();
123    copy.addAll(collection);
124
125    try {
126      collection.add(sampleElement);
127      fail("add succeeded on unmodifiable collection");
128    } catch (UnsupportedOperationException expected) {
129    }
130
131    assertCollectionsAreEquivalent(copy, collection);
132
133    try {
134      collection.addAll(siblingCollection);
135      fail("addAll succeeded on unmodifiable collection");
136    } catch (UnsupportedOperationException expected) {
137    }
138    assertCollectionsAreEquivalent(copy, collection);
139
140    try {
141      collection.clear();
142      fail("clear succeeded on unmodifiable collection");
143    } catch (UnsupportedOperationException expected) {
144    }
145    assertCollectionsAreEquivalent(copy, collection);
146
147    assertIteratorIsUnmodifiable(collection.iterator());
148    assertCollectionsAreEquivalent(copy, collection);
149
150    try {
151      collection.remove(sampleElement);
152      fail("remove succeeded on unmodifiable collection");
153    } catch (UnsupportedOperationException expected) {
154    }
155    assertCollectionsAreEquivalent(copy, collection);
156
157    try {
158      collection.removeAll(siblingCollection);
159      fail("removeAll succeeded on unmodifiable collection");
160    } catch (UnsupportedOperationException expected) {
161    }
162    assertCollectionsAreEquivalent(copy, collection);
163
164    try {
165      collection.retainAll(siblingCollection);
166      fail("retainAll succeeded on unmodifiable collection");
167    } catch (UnsupportedOperationException expected) {
168    }
169    assertCollectionsAreEquivalent(copy, collection);
170  }
171
172  /**
173   * Verifies that a set is immutable.
174   *
175   * <p>A set is considered immutable if:
176   *
177   * <ol>
178   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
179   *       underlying contents.
180   *   <li>All methods that return objects that can indirectly mutate the set throw
181   *       UnsupportedOperationException when those mutators are called.
182   * </ol>
183   *
184   * @param set the presumed-immutable set
185   * @param sampleElement an element of the same type as that contained by {@code set}. {@code set}
186   *     may or may not have {@code sampleElement} as a member.
187   */
188  public static <E extends @Nullable Object> void assertSetIsUnmodifiable(
189      Set<E> set, E sampleElement) {
190    assertCollectionIsUnmodifiable(set, sampleElement);
191  }
192
193  /**
194   * Verifies that a multiset is immutable.
195   *
196   * <p>A multiset is considered immutable if:
197   *
198   * <ol>
199   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
200   *       underlying contents.
201   *   <li>All methods that return objects that can indirectly mutate the multiset throw
202   *       UnsupportedOperationException when those mutators are called.
203   * </ol>
204   *
205   * @param multiset the presumed-immutable multiset
206   * @param sampleElement an element of the same type as that contained by {@code multiset}. {@code
207   *     multiset} may or may not have {@code sampleElement} as a member.
208   */
209  public static <E extends @Nullable Object> void assertMultisetIsUnmodifiable(
210      Multiset<E> multiset, E sampleElement) {
211    Multiset<E> copy = LinkedHashMultiset.create(multiset);
212    assertCollectionsAreEquivalent(multiset, copy);
213
214    // Multiset is a collection, so we can use all those tests.
215    assertCollectionIsUnmodifiable(multiset, sampleElement);
216
217    assertCollectionsAreEquivalent(multiset, copy);
218
219    try {
220      multiset.add(sampleElement, 2);
221      fail("add(Object, int) succeeded on unmodifiable collection");
222    } catch (UnsupportedOperationException expected) {
223    }
224    assertCollectionsAreEquivalent(multiset, copy);
225
226    try {
227      multiset.remove(sampleElement, 2);
228      fail("remove(Object, int) succeeded on unmodifiable collection");
229    } catch (UnsupportedOperationException expected) {
230    }
231    assertCollectionsAreEquivalent(multiset, copy);
232
233    try {
234      multiset.removeIf(x -> false);
235      fail("removeIf(Predicate) succeeded on unmodifiable collection");
236    } catch (UnsupportedOperationException expected) {
237    }
238    assertCollectionsAreEquivalent(multiset, copy);
239
240    assertSetIsUnmodifiable(multiset.elementSet(), sampleElement);
241    assertCollectionsAreEquivalent(multiset, copy);
242
243    assertSetIsUnmodifiable(
244        multiset.entrySet(),
245        new Multiset.Entry<E>() {
246          @Override
247          public int getCount() {
248            return 1;
249          }
250
251          @Override
252          public E getElement() {
253            return sampleElement;
254          }
255        });
256    assertCollectionsAreEquivalent(multiset, copy);
257  }
258
259  /**
260   * Verifies that a multimap is immutable.
261   *
262   * <p>A multimap is considered immutable if:
263   *
264   * <ol>
265   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
266   *       underlying contents.
267   *   <li>All methods that return objects that can indirectly mutate the multimap throw
268   *       UnsupportedOperationException when those mutators
269   * </ol>
270   *
271   * @param multimap the presumed-immutable multimap
272   * @param sampleKey a key of the same type as that contained by {@code multimap}. {@code multimap}
273   *     may or may not have {@code sampleKey} as a key.
274   * @param sampleValue a key of the same type as that contained by {@code multimap}. {@code
275   *     multimap} may or may not have {@code sampleValue} as a key.
276   */
277  public static <K extends @Nullable Object, V extends @Nullable Object>
278      void assertMultimapIsUnmodifiable(Multimap<K, V> multimap, K sampleKey, V sampleValue) {
279    List<Entry<K, V>> originalEntries = unmodifiableList(Lists.newArrayList(multimap.entries()));
280
281    assertMultimapRemainsUnmodified(multimap, originalEntries);
282
283    Collection<V> sampleValueAsCollection = singleton(sampleValue);
284
285    // Test #clear()
286    try {
287      multimap.clear();
288      fail("clear succeeded on unmodifiable multimap");
289    } catch (UnsupportedOperationException expected) {
290    }
291
292    assertMultimapRemainsUnmodified(multimap, originalEntries);
293
294    // Test asMap().entrySet()
295    assertSetIsUnmodifiable(
296        multimap.asMap().entrySet(), immutableEntry(sampleKey, sampleValueAsCollection));
297
298    // Test #values()
299
300    assertMultimapRemainsUnmodified(multimap, originalEntries);
301    if (!multimap.isEmpty()) {
302      Collection<V> values = multimap.asMap().entrySet().iterator().next().getValue();
303
304      assertCollectionIsUnmodifiable(values, sampleValue);
305    }
306
307    // Test #entries()
308    assertCollectionIsUnmodifiable(multimap.entries(), immutableEntry(sampleKey, sampleValue));
309    assertMultimapRemainsUnmodified(multimap, originalEntries);
310
311    // Iterate over every element in the entry set
312    for (Entry<K, V> entry : multimap.entries()) {
313      assertMapEntryIsUnmodifiable(entry);
314    }
315    assertMultimapRemainsUnmodified(multimap, originalEntries);
316
317    // Test #keys()
318    assertMultisetIsUnmodifiable(multimap.keys(), sampleKey);
319    assertMultimapRemainsUnmodified(multimap, originalEntries);
320
321    // Test #keySet()
322    assertSetIsUnmodifiable(multimap.keySet(), sampleKey);
323    assertMultimapRemainsUnmodified(multimap, originalEntries);
324
325    // Test #get()
326    if (!multimap.isEmpty()) {
327      K key = multimap.keySet().iterator().next();
328      assertCollectionIsUnmodifiable(multimap.get(key), sampleValue);
329      assertMultimapRemainsUnmodified(multimap, originalEntries);
330    }
331
332    // Test #put()
333    try {
334      multimap.put(sampleKey, sampleValue);
335      fail("put succeeded on unmodifiable multimap");
336    } catch (UnsupportedOperationException expected) {
337    }
338    assertMultimapRemainsUnmodified(multimap, originalEntries);
339
340    // Test #putAll(K, Collection<V>)
341    try {
342      multimap.putAll(sampleKey, sampleValueAsCollection);
343      fail("putAll(K, Iterable) succeeded on unmodifiable multimap");
344    } catch (UnsupportedOperationException expected) {
345    }
346    assertMultimapRemainsUnmodified(multimap, originalEntries);
347
348    // Test #putAll(Multimap<K, V>)
349    Multimap<K, V> multimap2 = ArrayListMultimap.create();
350    multimap2.put(sampleKey, sampleValue);
351    try {
352      multimap.putAll(multimap2);
353      fail("putAll(Multimap<K, V>) succeeded on unmodifiable multimap");
354    } catch (UnsupportedOperationException expected) {
355    }
356    assertMultimapRemainsUnmodified(multimap, originalEntries);
357
358    // Test #remove()
359    try {
360      multimap.remove(sampleKey, sampleValue);
361      fail("remove succeeded on unmodifiable multimap");
362    } catch (UnsupportedOperationException expected) {
363    }
364    assertMultimapRemainsUnmodified(multimap, originalEntries);
365
366    // Test #removeAll()
367    try {
368      multimap.removeAll(sampleKey);
369      fail("removeAll succeeded on unmodifiable multimap");
370    } catch (UnsupportedOperationException expected) {
371    }
372    assertMultimapRemainsUnmodified(multimap, originalEntries);
373
374    // Test #replaceValues()
375    try {
376      multimap.replaceValues(sampleKey, sampleValueAsCollection);
377      fail("replaceValues succeeded on unmodifiable multimap");
378    } catch (UnsupportedOperationException expected) {
379    }
380    assertMultimapRemainsUnmodified(multimap, originalEntries);
381
382    // Test #asMap()
383    try {
384      multimap.asMap().remove(sampleKey);
385      fail("asMap().remove() succeeded on unmodifiable multimap");
386    } catch (UnsupportedOperationException expected) {
387    }
388    assertMultimapRemainsUnmodified(multimap, originalEntries);
389
390    if (!multimap.isEmpty()) {
391      K presentKey = multimap.keySet().iterator().next();
392      try {
393        multimap.asMap().get(presentKey).remove(sampleValue);
394        fail("asMap().get().remove() succeeded on unmodifiable multimap");
395      } catch (UnsupportedOperationException expected) {
396      }
397      assertMultimapRemainsUnmodified(multimap, originalEntries);
398
399      try {
400        multimap.asMap().values().iterator().next().remove(sampleValue);
401        fail("asMap().values().iterator().next().remove() succeeded on unmodifiable multimap");
402      } catch (UnsupportedOperationException expected) {
403      }
404
405      try {
406        ((Collection<?>) multimap.asMap().values().toArray()[0]).clear();
407        fail("asMap().values().toArray()[0].clear() succeeded on unmodifiable multimap");
408      } catch (UnsupportedOperationException expected) {
409      }
410    }
411
412    assertCollectionIsUnmodifiable(multimap.values(), sampleValue);
413    assertMultimapRemainsUnmodified(multimap, originalEntries);
414  }
415
416  private static <E extends @Nullable Object> void assertCollectionsAreEquivalent(
417      Collection<E> expected, Collection<E> actual) {
418    assertIteratorsInOrder(expected.iterator(), actual.iterator());
419  }
420
421  private static <K extends @Nullable Object, V extends @Nullable Object>
422      void assertMultimapRemainsUnmodified(Multimap<K, V> expected, List<Entry<K, V>> actual) {
423    assertIteratorsInOrder(expected.entries().iterator(), actual.iterator());
424  }
425}