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}