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.LinkedHashMultiset;
026import com.google.common.collect.Lists;
027import com.google.common.collect.Maps;
028import com.google.common.collect.Multimap;
029import com.google.common.collect.Multiset;
030import java.util.ArrayList;
031import java.util.Collection;
032import java.util.Collections;
033import java.util.Iterator;
034import java.util.List;
035import java.util.Map.Entry;
036import java.util.Set;
037import org.checkerframework.checker.nullness.qual.Nullable;
038
039/**
040 * A series of tests that support asserting that collections cannot be modified, either through
041 * direct or indirect means.
042 *
043 * @author Robert Konigsberg
044 */
045@GwtCompatible
046@ElementTypesAreNonnullByDefault
047public class UnmodifiableCollectionTests {
048
049  public static void assertMapEntryIsUnmodifiable(Entry<?, ?> entry) {
050    try {
051      // fine because the call is going to fail without modifying the entry
052      @SuppressWarnings("unchecked")
053      Entry<?, @Nullable Object> nullableValueEntry = (Entry<?, @Nullable Object>) entry;
054      nullableValueEntry.setValue(null);
055      fail("setValue on unmodifiable Map.Entry succeeded");
056    } catch (UnsupportedOperationException expected) {
057    }
058  }
059
060  /**
061   * Verifies that an Iterator is unmodifiable.
062   *
063   * <p>This test only works with iterators that iterate over a finite set.
064   */
065  public static void assertIteratorIsUnmodifiable(Iterator<?> iterator) {
066    while (iterator.hasNext()) {
067      iterator.next();
068      try {
069        iterator.remove();
070        fail("Remove on unmodifiable iterator succeeded");
071      } catch (UnsupportedOperationException expected) {
072      }
073    }
074  }
075
076  /**
077   * Asserts that two iterators contain elements in tandem.
078   *
079   * <p>This test only works with iterators that iterate over a finite set.
080   */
081  public static void assertIteratorsInOrder(
082      Iterator<?> expectedIterator, Iterator<?> actualIterator) {
083    int i = 0;
084    while (expectedIterator.hasNext()) {
085      Object expected = expectedIterator.next();
086
087      assertTrue(
088          "index " + i + " expected <" + expected + "., actual is exhausted",
089          actualIterator.hasNext());
090
091      Object actual = actualIterator.next();
092      assertEquals("index " + i, expected, actual);
093      i++;
094    }
095    if (actualIterator.hasNext()) {
096      fail("index " + i + ", expected is exhausted, actual <" + actualIterator.next() + ">");
097    }
098  }
099
100  /**
101   * Verifies that a collection is immutable.
102   *
103   * <p>A collection is considered immutable if:
104   *
105   * <ol>
106   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
107   *       underlying contents.
108   *   <li>All methods that return objects that can indirectly mutate the collection throw
109   *       UnsupportedOperationException when those mutators are called.
110   * </ol>
111   *
112   * @param collection the presumed-immutable collection
113   * @param sampleElement an element of the same type as that contained by {@code collection}.
114   *     {@code collection} may or may not have {@code sampleElement} as a member.
115   */
116  public static <E extends @Nullable Object> void assertCollectionIsUnmodifiable(
117      Collection<E> collection, E sampleElement) {
118    Collection<E> siblingCollection = new ArrayList<>();
119    siblingCollection.add(sampleElement);
120
121    Collection<E> copy = new ArrayList<>();
122    copy.addAll(collection);
123
124    try {
125      collection.add(sampleElement);
126      fail("add succeeded on unmodifiable collection");
127    } catch (UnsupportedOperationException expected) {
128    }
129
130    assertCollectionsAreEquivalent(copy, collection);
131
132    try {
133      collection.addAll(siblingCollection);
134      fail("addAll succeeded on unmodifiable collection");
135    } catch (UnsupportedOperationException expected) {
136    }
137    assertCollectionsAreEquivalent(copy, collection);
138
139    try {
140      collection.clear();
141      fail("clear succeeded on unmodifiable collection");
142    } catch (UnsupportedOperationException expected) {
143    }
144    assertCollectionsAreEquivalent(copy, collection);
145
146    assertIteratorIsUnmodifiable(collection.iterator());
147    assertCollectionsAreEquivalent(copy, collection);
148
149    try {
150      collection.remove(sampleElement);
151      fail("remove succeeded on unmodifiable collection");
152    } catch (UnsupportedOperationException expected) {
153    }
154    assertCollectionsAreEquivalent(copy, collection);
155
156    try {
157      collection.removeAll(siblingCollection);
158      fail("removeAll succeeded on unmodifiable collection");
159    } catch (UnsupportedOperationException expected) {
160    }
161    assertCollectionsAreEquivalent(copy, collection);
162
163    try {
164      collection.retainAll(siblingCollection);
165      fail("retainAll succeeded on unmodifiable collection");
166    } catch (UnsupportedOperationException expected) {
167    }
168    assertCollectionsAreEquivalent(copy, collection);
169  }
170
171  /**
172   * Verifies that a set is immutable.
173   *
174   * <p>A set is considered immutable if:
175   *
176   * <ol>
177   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
178   *       underlying contents.
179   *   <li>All methods that return objects that can indirectly mutate the set throw
180   *       UnsupportedOperationException when those mutators are called.
181   * </ol>
182   *
183   * @param set the presumed-immutable set
184   * @param sampleElement an element of the same type as that contained by {@code set}. {@code set}
185   *     may or may not have {@code sampleElement} as a member.
186   */
187  public static <E extends @Nullable Object> void assertSetIsUnmodifiable(
188      Set<E> set, E sampleElement) {
189    assertCollectionIsUnmodifiable(set, sampleElement);
190  }
191
192  /**
193   * Verifies that a multiset is immutable.
194   *
195   * <p>A multiset is considered immutable if:
196   *
197   * <ol>
198   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
199   *       underlying contents.
200   *   <li>All methods that return objects that can indirectly mutate the multiset throw
201   *       UnsupportedOperationException when those mutators are called.
202   * </ol>
203   *
204   * @param multiset the presumed-immutable multiset
205   * @param sampleElement an element of the same type as that contained by {@code multiset}. {@code
206   *     multiset} may or may not have {@code sampleElement} as a member.
207   */
208  public static <E extends @Nullable Object> void assertMultisetIsUnmodifiable(
209      Multiset<E> multiset, E sampleElement) {
210    Multiset<E> copy = LinkedHashMultiset.create(multiset);
211    assertCollectionsAreEquivalent(multiset, copy);
212
213    // Multiset is a collection, so we can use all those tests.
214    assertCollectionIsUnmodifiable(multiset, sampleElement);
215
216    assertCollectionsAreEquivalent(multiset, copy);
217
218    try {
219      multiset.add(sampleElement, 2);
220      fail("add(Object, int) succeeded on unmodifiable collection");
221    } catch (UnsupportedOperationException expected) {
222    }
223    assertCollectionsAreEquivalent(multiset, copy);
224
225    try {
226      multiset.remove(sampleElement, 2);
227      fail("remove(Object, int) succeeded on unmodifiable collection");
228    } catch (UnsupportedOperationException expected) {
229    }
230    assertCollectionsAreEquivalent(multiset, copy);
231
232    try {
233      multiset.removeIf(x -> false);
234      fail("removeIf(Predicate) succeeded on unmodifiable collection");
235    } catch (UnsupportedOperationException expected) {
236    }
237    assertCollectionsAreEquivalent(multiset, copy);
238
239    assertSetIsUnmodifiable(multiset.elementSet(), sampleElement);
240    assertCollectionsAreEquivalent(multiset, copy);
241
242    assertSetIsUnmodifiable(
243        multiset.entrySet(),
244        new Multiset.Entry<E>() {
245          @Override
246          public int getCount() {
247            return 1;
248          }
249
250          @Override
251          public E getElement() {
252            return sampleElement;
253          }
254        });
255    assertCollectionsAreEquivalent(multiset, copy);
256  }
257
258  /**
259   * Verifies that a multimap is immutable.
260   *
261   * <p>A multimap is considered immutable if:
262   *
263   * <ol>
264   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
265   *       underlying contents.
266   *   <li>All methods that return objects that can indirectly mutate the multimap throw
267   *       UnsupportedOperationException when those mutators
268   * </ol>
269   *
270   * @param multimap the presumed-immutable multimap
271   * @param sampleKey a key of the same type as that contained by {@code multimap}. {@code multimap}
272   *     may or may not have {@code sampleKey} as a key.
273   * @param sampleValue a key of the same type as that contained by {@code multimap}. {@code
274   *     multimap} may or may not have {@code sampleValue} as a key.
275   */
276  public static <K extends @Nullable Object, V extends @Nullable Object>
277      void assertMultimapIsUnmodifiable(Multimap<K, V> multimap, K sampleKey, V sampleValue) {
278    List<Entry<K, V>> originalEntries =
279        Collections.unmodifiableList(Lists.newArrayList(multimap.entries()));
280
281    assertMultimapRemainsUnmodified(multimap, originalEntries);
282
283    Collection<V> sampleValueAsCollection = Collections.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(), Maps.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(), Maps.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}