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;
025import static org.junit.Assert.assertThrows;
026
027import com.google.common.annotations.GwtCompatible;
028import com.google.common.collect.ArrayListMultimap;
029import com.google.common.collect.LinkedHashMultiset;
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    // 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    assertThrows(UnsupportedOperationException.class, () -> nullableValueEntry.setValue(null));
056  }
057
058  /**
059   * Verifies that an Iterator is unmodifiable.
060   *
061   * <p>This test only works with iterators that iterate over a finite set.
062   */
063  public static void assertIteratorIsUnmodifiable(Iterator<?> iterator) {
064    while (iterator.hasNext()) {
065      iterator.next();
066      assertThrows(UnsupportedOperationException.class, () -> iterator.remove());
067    }
068  }
069
070  /**
071   * Asserts that two iterators contain elements in tandem.
072   *
073   * <p>This test only works with iterators that iterate over a finite set.
074   */
075  public static void assertIteratorsInOrder(
076      Iterator<?> expectedIterator, Iterator<?> actualIterator) {
077    int i = 0;
078    while (expectedIterator.hasNext()) {
079      Object expected = expectedIterator.next();
080
081      assertTrue(
082          "index " + i + " expected <" + expected + "., actual is exhausted",
083          actualIterator.hasNext());
084
085      Object actual = actualIterator.next();
086      assertEquals("index " + i, expected, actual);
087      i++;
088    }
089    if (actualIterator.hasNext()) {
090      fail("index " + i + ", expected is exhausted, actual <" + actualIterator.next() + ">");
091    }
092  }
093
094  /**
095   * Verifies that a collection is immutable.
096   *
097   * <p>A collection is considered immutable if:
098   *
099   * <ol>
100   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
101   *       underlying contents.
102   *   <li>All methods that return objects that can indirectly mutate the collection throw
103   *       UnsupportedOperationException when those mutators are called.
104   * </ol>
105   *
106   * @param collection the presumed-immutable collection
107   * @param sampleElement an element of the same type as that contained by {@code collection}.
108   *     {@code collection} may or may not have {@code sampleElement} as a member.
109   */
110  public static <E extends @Nullable Object> void assertCollectionIsUnmodifiable(
111      Collection<E> collection, E sampleElement) {
112    Collection<E> siblingCollection = new ArrayList<>();
113    siblingCollection.add(sampleElement);
114
115    Collection<E> copy = new ArrayList<>();
116    copy.addAll(collection);
117
118    assertThrows(UnsupportedOperationException.class, () -> collection.add(sampleElement));
119    assertCollectionsAreEquivalent(copy, collection);
120
121    assertThrows(UnsupportedOperationException.class, () -> collection.addAll(siblingCollection));
122    assertCollectionsAreEquivalent(copy, collection);
123
124    assertThrows(UnsupportedOperationException.class, () -> collection.clear());
125    assertCollectionsAreEquivalent(copy, collection);
126
127    assertIteratorIsUnmodifiable(collection.iterator());
128    assertCollectionsAreEquivalent(copy, collection);
129
130    assertThrows(UnsupportedOperationException.class, () -> collection.remove(sampleElement));
131    assertCollectionsAreEquivalent(copy, collection);
132
133    assertThrows(
134        UnsupportedOperationException.class, () -> collection.removeAll(siblingCollection));
135    assertCollectionsAreEquivalent(copy, collection);
136
137    assertThrows(
138        UnsupportedOperationException.class, () -> collection.retainAll(siblingCollection));
139    assertCollectionsAreEquivalent(copy, collection);
140  }
141
142  /**
143   * Verifies that a set is immutable.
144   *
145   * <p>A set is considered immutable if:
146   *
147   * <ol>
148   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
149   *       underlying contents.
150   *   <li>All methods that return objects that can indirectly mutate the set throw
151   *       UnsupportedOperationException when those mutators are called.
152   * </ol>
153   *
154   * @param set the presumed-immutable set
155   * @param sampleElement an element of the same type as that contained by {@code set}. {@code set}
156   *     may or may not have {@code sampleElement} as a member.
157   */
158  public static <E extends @Nullable Object> void assertSetIsUnmodifiable(
159      Set<E> set, E sampleElement) {
160    assertCollectionIsUnmodifiable(set, sampleElement);
161  }
162
163  /**
164   * Verifies that a multiset is immutable.
165   *
166   * <p>A multiset is considered immutable if:
167   *
168   * <ol>
169   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
170   *       underlying contents.
171   *   <li>All methods that return objects that can indirectly mutate the multiset throw
172   *       UnsupportedOperationException when those mutators are called.
173   * </ol>
174   *
175   * @param multiset the presumed-immutable multiset
176   * @param sampleElement an element of the same type as that contained by {@code multiset}. {@code
177   *     multiset} may or may not have {@code sampleElement} as a member.
178   */
179  public static <E extends @Nullable Object> void assertMultisetIsUnmodifiable(
180      Multiset<E> multiset, E sampleElement) {
181    Multiset<E> copy = LinkedHashMultiset.create(multiset);
182    assertCollectionsAreEquivalent(multiset, copy);
183
184    // Multiset is a collection, so we can use all those tests.
185    assertCollectionIsUnmodifiable(multiset, sampleElement);
186
187    assertCollectionsAreEquivalent(multiset, copy);
188
189    assertThrows(UnsupportedOperationException.class, () -> multiset.add(sampleElement, 2));
190    assertCollectionsAreEquivalent(multiset, copy);
191
192    assertThrows(UnsupportedOperationException.class, () -> multiset.remove(sampleElement, 2));
193    assertCollectionsAreEquivalent(multiset, copy);
194
195    assertThrows(UnsupportedOperationException.class, () -> multiset.removeIf(x -> false));
196    assertCollectionsAreEquivalent(multiset, copy);
197
198    assertSetIsUnmodifiable(multiset.elementSet(), sampleElement);
199    assertCollectionsAreEquivalent(multiset, copy);
200
201    assertSetIsUnmodifiable(
202        multiset.entrySet(),
203        new Multiset.Entry<E>() {
204          @Override
205          public int getCount() {
206            return 1;
207          }
208
209          @Override
210          public E getElement() {
211            return sampleElement;
212          }
213        });
214    assertCollectionsAreEquivalent(multiset, copy);
215  }
216
217  /**
218   * Verifies that a multimap is immutable.
219   *
220   * <p>A multimap is considered immutable if:
221   *
222   * <ol>
223   *   <li>All its mutation methods result in UnsupportedOperationException, and do not change the
224   *       underlying contents.
225   *   <li>All methods that return objects that can indirectly mutate the multimap throw
226   *       UnsupportedOperationException when those mutators
227   * </ol>
228   *
229   * @param multimap the presumed-immutable multimap
230   * @param sampleKey a key of the same type as that contained by {@code multimap}. {@code multimap}
231   *     may or may not have {@code sampleKey} as a key.
232   * @param sampleValue a key of the same type as that contained by {@code multimap}. {@code
233   *     multimap} may or may not have {@code sampleValue} as a key.
234   */
235  public static <K extends @Nullable Object, V extends @Nullable Object>
236      void assertMultimapIsUnmodifiable(Multimap<K, V> multimap, K sampleKey, V sampleValue) {
237    List<Entry<K, V>> originalEntries = unmodifiableList(new ArrayList<>(multimap.entries()));
238
239    assertMultimapRemainsUnmodified(multimap, originalEntries);
240
241    Collection<V> sampleValueAsCollection = singleton(sampleValue);
242
243    // Test #clear()
244    assertThrows(UnsupportedOperationException.class, () -> multimap.clear());
245
246    assertMultimapRemainsUnmodified(multimap, originalEntries);
247
248    // Test asMap().entrySet()
249    assertSetIsUnmodifiable(
250        multimap.asMap().entrySet(), immutableEntry(sampleKey, sampleValueAsCollection));
251
252    // Test #values()
253
254    assertMultimapRemainsUnmodified(multimap, originalEntries);
255    if (!multimap.isEmpty()) {
256      Collection<V> values = multimap.asMap().entrySet().iterator().next().getValue();
257
258      assertCollectionIsUnmodifiable(values, sampleValue);
259    }
260
261    // Test #entries()
262    assertCollectionIsUnmodifiable(multimap.entries(), immutableEntry(sampleKey, sampleValue));
263    assertMultimapRemainsUnmodified(multimap, originalEntries);
264
265    // Iterate over every element in the entry set
266    for (Entry<K, V> entry : multimap.entries()) {
267      assertMapEntryIsUnmodifiable(entry);
268    }
269    assertMultimapRemainsUnmodified(multimap, originalEntries);
270
271    // Test #keys()
272    assertMultisetIsUnmodifiable(multimap.keys(), sampleKey);
273    assertMultimapRemainsUnmodified(multimap, originalEntries);
274
275    // Test #keySet()
276    assertSetIsUnmodifiable(multimap.keySet(), sampleKey);
277    assertMultimapRemainsUnmodified(multimap, originalEntries);
278
279    // Test #get()
280    if (!multimap.isEmpty()) {
281      K key = multimap.keySet().iterator().next();
282      assertCollectionIsUnmodifiable(multimap.get(key), sampleValue);
283      assertMultimapRemainsUnmodified(multimap, originalEntries);
284    }
285
286    // Test #put()
287    assertThrows(UnsupportedOperationException.class, () -> multimap.put(sampleKey, sampleValue));
288    assertMultimapRemainsUnmodified(multimap, originalEntries);
289
290    // Test #putAll(K, Collection<V>)
291    assertThrows(
292        UnsupportedOperationException.class,
293        () -> multimap.putAll(sampleKey, sampleValueAsCollection));
294    assertMultimapRemainsUnmodified(multimap, originalEntries);
295
296    // Test #putAll(Multimap<K, V>)
297    Multimap<K, V> multimap2 = ArrayListMultimap.create();
298    multimap2.put(sampleKey, sampleValue);
299    assertThrows(UnsupportedOperationException.class, () -> multimap.putAll(multimap2));
300    assertMultimapRemainsUnmodified(multimap, originalEntries);
301
302    // Test #remove()
303    assertThrows(
304        UnsupportedOperationException.class, () -> multimap.remove(sampleKey, sampleValue));
305    assertMultimapRemainsUnmodified(multimap, originalEntries);
306
307    // Test #removeAll()
308    assertThrows(UnsupportedOperationException.class, () -> multimap.removeAll(sampleKey));
309    assertMultimapRemainsUnmodified(multimap, originalEntries);
310
311    // Test #replaceValues()
312    assertThrows(
313        UnsupportedOperationException.class,
314        () -> multimap.replaceValues(sampleKey, sampleValueAsCollection));
315    assertMultimapRemainsUnmodified(multimap, originalEntries);
316
317    // Test #asMap()
318    assertThrows(UnsupportedOperationException.class, () -> multimap.asMap().remove(sampleKey));
319    assertMultimapRemainsUnmodified(multimap, originalEntries);
320
321    if (!multimap.isEmpty()) {
322      K presentKey = multimap.keySet().iterator().next();
323      assertThrows(
324          UnsupportedOperationException.class,
325          () -> multimap.asMap().get(presentKey).remove(sampleValue));
326      assertMultimapRemainsUnmodified(multimap, originalEntries);
327
328      assertThrows(
329          UnsupportedOperationException.class,
330          () -> multimap.asMap().values().iterator().next().remove(sampleValue));
331
332      assertThrows(
333          UnsupportedOperationException.class,
334          () -> ((Collection<?>) multimap.asMap().values().toArray()[0]).clear());
335    }
336
337    assertCollectionIsUnmodifiable(multimap.values(), sampleValue);
338    assertMultimapRemainsUnmodified(multimap, originalEntries);
339  }
340
341  private static <E extends @Nullable Object> void assertCollectionsAreEquivalent(
342      Collection<E> expected, Collection<E> actual) {
343    assertIteratorsInOrder(expected.iterator(), actual.iterator());
344  }
345
346  private static <K extends @Nullable Object, V extends @Nullable Object>
347      void assertMultimapRemainsUnmodified(Multimap<K, V> expected, List<Entry<K, V>> actual) {
348    assertIteratorsInOrder(expected.entries().iterator(), actual.iterator());
349  }
350
351  /**
352   * Useless constructor for a class of static utility methods.
353   *
354   * @deprecated Do not instantiate this utility class.
355   */
356  @Deprecated
357  public UnmodifiableCollectionTests() {}
358}