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}