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