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