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 junit.framework.TestCase.assertEquals;
020import static junit.framework.TestCase.assertTrue;
021import static junit.framework.TestCase.fail;
022
023import com.google.common.annotations.GwtCompatible;
024import com.google.common.collect.ArrayListMultimap;
025import com.google.common.collect.Iterators;
026import com.google.common.collect.LinkedHashMultiset;
027import com.google.common.collect.Lists;
028import com.google.common.collect.Maps;
029import com.google.common.collect.Multimap;
030import com.google.common.collect.Multiset;
031import java.util.ArrayList;
032import java.util.Collection;
033import java.util.Collections;
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    // Avoid copy.addAll(collection), which runs afoul of an Android bug in older versions:
124    // http://b.android.com/72073 http://r.android.com/98929
125    Iterators.addAll(copy, collection.iterator());
126
127    try {
128      collection.add(sampleElement);
129      fail("add succeeded on unmodifiable collection");
130    } catch (UnsupportedOperationException expected) {
131    }
132
133    assertCollectionsAreEquivalent(copy, collection);
134
135    try {
136      collection.addAll(siblingCollection);
137      fail("addAll succeeded on unmodifiable collection");
138    } catch (UnsupportedOperationException expected) {
139    }
140    assertCollectionsAreEquivalent(copy, collection);
141
142    try {
143      collection.clear();
144      fail("clear succeeded on unmodifiable collection");
145    } catch (UnsupportedOperationException expected) {
146    }
147    assertCollectionsAreEquivalent(copy, collection);
148
149    assertIteratorIsUnmodifiable(collection.iterator());
150    assertCollectionsAreEquivalent(copy, collection);
151
152    try {
153      collection.remove(sampleElement);
154      fail("remove succeeded on unmodifiable collection");
155    } catch (UnsupportedOperationException expected) {
156    }
157    assertCollectionsAreEquivalent(copy, collection);
158
159    try {
160      collection.removeAll(siblingCollection);
161      fail("removeAll succeeded on unmodifiable collection");
162    } catch (UnsupportedOperationException expected) {
163    }
164    assertCollectionsAreEquivalent(copy, collection);
165
166    try {
167      collection.retainAll(siblingCollection);
168      fail("retainAll succeeded on unmodifiable collection");
169    } catch (UnsupportedOperationException expected) {
170    }
171    assertCollectionsAreEquivalent(copy, collection);
172  }
173
174  /**
175   * Verifies that a set is immutable.
176   *
177   * <p>A set is considered immutable if:
178   *
179   * <ol>
180   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
181   *       underlying contents.
182   *   <li>All methods that return objects that can indirectly mutate the set throw
183   *       UnsupportedOperationException when those mutators are called.
184   * </ol>
185   *
186   * @param set the presumed-immutable set
187   * @param sampleElement an element of the same type as that contained by {@code set}. {@code set}
188   *     may or may not have {@code sampleElement} as a member.
189   */
190  public static <E extends @Nullable Object> void assertSetIsUnmodifiable(
191      Set<E> set, E sampleElement) {
192    assertCollectionIsUnmodifiable(set, sampleElement);
193  }
194
195  /**
196   * Verifies that a multiset is immutable.
197   *
198   * <p>A multiset is considered immutable if:
199   *
200   * <ol>
201   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
202   *       underlying contents.
203   *   <li>All methods that return objects that can indirectly mutate the multiset throw
204   *       UnsupportedOperationException when those mutators are called.
205   * </ol>
206   *
207   * @param multiset the presumed-immutable multiset
208   * @param sampleElement an element of the same type as that contained by {@code multiset}. {@code
209   *     multiset} may or may not have {@code sampleElement} as a member.
210   */
211  public static <E extends @Nullable Object> void assertMultisetIsUnmodifiable(
212      Multiset<E> multiset, E sampleElement) {
213    Multiset<E> copy = LinkedHashMultiset.create(multiset);
214    assertCollectionsAreEquivalent(multiset, copy);
215
216    // Multiset is a collection, so we can use all those tests.
217    assertCollectionIsUnmodifiable(multiset, sampleElement);
218
219    assertCollectionsAreEquivalent(multiset, copy);
220
221    try {
222      multiset.add(sampleElement, 2);
223      fail("add(Object, int) succeeded on unmodifiable collection");
224    } catch (UnsupportedOperationException expected) {
225    }
226    assertCollectionsAreEquivalent(multiset, copy);
227
228    try {
229      multiset.remove(sampleElement, 2);
230      fail("remove(Object, int) succeeded on unmodifiable collection");
231    } catch (UnsupportedOperationException expected) {
232    }
233    assertCollectionsAreEquivalent(multiset, copy);
234
235    try {
236      multiset.removeIf(x -> false);
237      fail("removeIf(Predicate) succeeded on unmodifiable collection");
238    } catch (UnsupportedOperationException expected) {
239    }
240    assertCollectionsAreEquivalent(multiset, copy);
241
242    assertSetIsUnmodifiable(multiset.elementSet(), sampleElement);
243    assertCollectionsAreEquivalent(multiset, copy);
244
245    assertSetIsUnmodifiable(
246        multiset.entrySet(),
247        new Multiset.Entry<E>() {
248          @Override
249          public int getCount() {
250            return 1;
251          }
252
253          @Override
254          public E getElement() {
255            return sampleElement;
256          }
257        });
258    assertCollectionsAreEquivalent(multiset, copy);
259  }
260
261  /**
262   * Verifies that a multimap is immutable.
263   *
264   * <p>A multimap is considered immutable if:
265   *
266   * <ol>
267   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
268   *       underlying contents.
269   *   <li>All methods that return objects that can indirectly mutate the multimap throw
270   *       UnsupportedOperationException when those mutators
271   * </ol>
272   *
273   * @param multimap the presumed-immutable multimap
274   * @param sampleKey a key of the same type as that contained by {@code multimap}. {@code multimap}
275   *     may or may not have {@code sampleKey} as a key.
276   * @param sampleValue a key of the same type as that contained by {@code multimap}. {@code
277   *     multimap} may or may not have {@code sampleValue} as a key.
278   */
279  public static <K extends @Nullable Object, V extends @Nullable Object>
280      void assertMultimapIsUnmodifiable(Multimap<K, V> multimap, K sampleKey, V sampleValue) {
281    List<Entry<K, V>> originalEntries =
282        Collections.unmodifiableList(Lists.newArrayList(multimap.entries()));
283
284    assertMultimapRemainsUnmodified(multimap, originalEntries);
285
286    Collection<V> sampleValueAsCollection = Collections.singleton(sampleValue);
287
288    // Test #clear()
289    try {
290      multimap.clear();
291      fail("clear succeeded on unmodifiable multimap");
292    } catch (UnsupportedOperationException expected) {
293    }
294
295    assertMultimapRemainsUnmodified(multimap, originalEntries);
296
297    // Test asMap().entrySet()
298    assertSetIsUnmodifiable(
299        multimap.asMap().entrySet(), Maps.immutableEntry(sampleKey, sampleValueAsCollection));
300
301    // Test #values()
302
303    assertMultimapRemainsUnmodified(multimap, originalEntries);
304    if (!multimap.isEmpty()) {
305      Collection<V> values = multimap.asMap().entrySet().iterator().next().getValue();
306
307      assertCollectionIsUnmodifiable(values, sampleValue);
308    }
309
310    // Test #entries()
311    assertCollectionIsUnmodifiable(multimap.entries(), Maps.immutableEntry(sampleKey, sampleValue));
312    assertMultimapRemainsUnmodified(multimap, originalEntries);
313
314    // Iterate over every element in the entry set
315    for (Entry<K, V> entry : multimap.entries()) {
316      assertMapEntryIsUnmodifiable(entry);
317    }
318    assertMultimapRemainsUnmodified(multimap, originalEntries);
319
320    // Test #keys()
321    assertMultisetIsUnmodifiable(multimap.keys(), sampleKey);
322    assertMultimapRemainsUnmodified(multimap, originalEntries);
323
324    // Test #keySet()
325    assertSetIsUnmodifiable(multimap.keySet(), sampleKey);
326    assertMultimapRemainsUnmodified(multimap, originalEntries);
327
328    // Test #get()
329    if (!multimap.isEmpty()) {
330      K key = multimap.keySet().iterator().next();
331      assertCollectionIsUnmodifiable(multimap.get(key), sampleValue);
332      assertMultimapRemainsUnmodified(multimap, originalEntries);
333    }
334
335    // Test #put()
336    try {
337      multimap.put(sampleKey, sampleValue);
338      fail("put succeeded on unmodifiable multimap");
339    } catch (UnsupportedOperationException expected) {
340    }
341    assertMultimapRemainsUnmodified(multimap, originalEntries);
342
343    // Test #putAll(K, Collection<V>)
344    try {
345      multimap.putAll(sampleKey, sampleValueAsCollection);
346      fail("putAll(K, Iterable) succeeded on unmodifiable multimap");
347    } catch (UnsupportedOperationException expected) {
348    }
349    assertMultimapRemainsUnmodified(multimap, originalEntries);
350
351    // Test #putAll(Multimap<K, V>)
352    Multimap<K, V> multimap2 = ArrayListMultimap.create();
353    multimap2.put(sampleKey, sampleValue);
354    try {
355      multimap.putAll(multimap2);
356      fail("putAll(Multimap<K, V>) succeeded on unmodifiable multimap");
357    } catch (UnsupportedOperationException expected) {
358    }
359    assertMultimapRemainsUnmodified(multimap, originalEntries);
360
361    // Test #remove()
362    try {
363      multimap.remove(sampleKey, sampleValue);
364      fail("remove succeeded on unmodifiable multimap");
365    } catch (UnsupportedOperationException expected) {
366    }
367    assertMultimapRemainsUnmodified(multimap, originalEntries);
368
369    // Test #removeAll()
370    try {
371      multimap.removeAll(sampleKey);
372      fail("removeAll succeeded on unmodifiable multimap");
373    } catch (UnsupportedOperationException expected) {
374    }
375    assertMultimapRemainsUnmodified(multimap, originalEntries);
376
377    // Test #replaceValues()
378    try {
379      multimap.replaceValues(sampleKey, sampleValueAsCollection);
380      fail("replaceValues succeeded on unmodifiable multimap");
381    } catch (UnsupportedOperationException expected) {
382    }
383    assertMultimapRemainsUnmodified(multimap, originalEntries);
384
385    // Test #asMap()
386    try {
387      multimap.asMap().remove(sampleKey);
388      fail("asMap().remove() succeeded on unmodifiable multimap");
389    } catch (UnsupportedOperationException expected) {
390    }
391    assertMultimapRemainsUnmodified(multimap, originalEntries);
392
393    if (!multimap.isEmpty()) {
394      K presentKey = multimap.keySet().iterator().next();
395      try {
396        multimap.asMap().get(presentKey).remove(sampleValue);
397        fail("asMap().get().remove() succeeded on unmodifiable multimap");
398      } catch (UnsupportedOperationException expected) {
399      }
400      assertMultimapRemainsUnmodified(multimap, originalEntries);
401
402      try {
403        multimap.asMap().values().iterator().next().remove(sampleValue);
404        fail("asMap().values().iterator().next().remove() succeeded on unmodifiable multimap");
405      } catch (UnsupportedOperationException expected) {
406      }
407
408      try {
409        ((Collection<?>) multimap.asMap().values().toArray()[0]).clear();
410        fail("asMap().values().toArray()[0].clear() succeeded on unmodifiable multimap");
411      } catch (UnsupportedOperationException expected) {
412      }
413    }
414
415    assertCollectionIsUnmodifiable(multimap.values(), sampleValue);
416    assertMultimapRemainsUnmodified(multimap, originalEntries);
417  }
418
419  private static <E extends @Nullable Object> void assertCollectionsAreEquivalent(
420      Collection<E> expected, Collection<E> actual) {
421    assertIteratorsInOrder(expected.iterator(), actual.iterator());
422  }
423
424  private static <K extends @Nullable Object, V extends @Nullable Object>
425      void assertMultimapRemainsUnmodified(Multimap<K, V> expected, List<Entry<K, V>> actual) {
426    assertIteratorsInOrder(expected.entries().iterator(), actual.iterator());
427  }
428}