001/* 002 * Copyright (C) 2016 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.testers; 018 019import static com.google.common.collect.testing.Helpers.assertEqualIgnoringOrder; 020import static com.google.common.collect.testing.Helpers.copyToSet; 021import static com.google.common.collect.testing.Helpers.mapEntry; 022import static com.google.common.collect.testing.features.CollectionFeature.KNOWN_ORDER; 023import static com.google.common.collect.testing.features.CollectionSize.SEVERAL; 024import static com.google.common.collect.testing.features.CollectionSize.ZERO; 025import static com.google.common.collect.testing.features.MapFeature.SUPPORTS_PUT; 026import static org.junit.Assert.assertThrows; 027 028import com.google.common.annotations.GwtCompatible; 029import com.google.common.collect.testing.AbstractMapTester; 030import com.google.common.collect.testing.SampleElements; 031import com.google.common.collect.testing.features.CollectionFeature; 032import com.google.common.collect.testing.features.CollectionSize; 033import com.google.common.collect.testing.features.MapFeature; 034import com.google.common.collect.testing.testers.TestExceptions.SomeUncheckedException; 035import java.util.ArrayList; 036import java.util.List; 037import java.util.Map; 038import java.util.Map.Entry; 039import java.util.Objects; 040import java.util.Set; 041import java.util.concurrent.atomic.AtomicReference; 042import java.util.function.BiFunction; 043import org.jspecify.annotations.Nullable; 044 045/** 046 * A generic JUnit test which tests {@code replaceAll()} operations on a map. Can't be invoked 047 * directly; please see {@link com.google.common.collect.testing.MapTestSuiteBuilder}. 048 * 049 * @author Louis Wasserman 050 */ 051@GwtCompatible 052public class MapReplaceAllTester<K, V> extends AbstractMapTester<K, V> { 053 private SampleElements<K> keys() { 054 return new SampleElements<>(k0(), k1(), k2(), k3(), k4()); 055 } 056 057 private SampleElements<V> values() { 058 return new SampleElements<>(v0(), v1(), v2(), v3(), v4()); 059 } 060 061 @MapFeature.Require(SUPPORTS_PUT) 062 public void testReplaceAllRotate() { 063 getMap() 064 .replaceAll( 065 (K k, V v) -> { 066 int index = keys().asList().indexOf(k); 067 return values().asList().get(index + 1); 068 }); 069 List<Entry<K, V>> expectedEntries = new ArrayList<>(); 070 for (Entry<K, V> entry : getSampleEntries()) { 071 int index = keys().asList().indexOf(entry.getKey()); 072 expectedEntries.add(mapEntry(entry.getKey(), values().asList().get(index + 1))); 073 } 074 expectContents(expectedEntries); 075 } 076 077 @MapFeature.Require(SUPPORTS_PUT) 078 @CollectionFeature.Require(KNOWN_ORDER) 079 public void testReplaceAllPreservesOrder() { 080 getMap() 081 .replaceAll( 082 (K k, V v) -> { 083 int index = keys().asList().indexOf(k); 084 return values().asList().get(index + 1); 085 }); 086 List<Entry<K, V>> orderedEntries = getOrderedElements(); 087 int index = 0; 088 for (K key : getMap().keySet()) { 089 assertEquals(orderedEntries.get(index).getKey(), key); 090 index++; 091 } 092 } 093 094 @MapFeature.Require(SUPPORTS_PUT) 095 @CollectionSize.Require(SEVERAL) 096 public void testReplaceAllThrowsMidwayThroughNewValue() { 097 doTestReplaceAllThrowsMidwayThrough((k, v) -> v3()); 098 } 099 100 @MapFeature.Require(SUPPORTS_PUT) 101 @CollectionSize.Require(SEVERAL) 102 public void testReplaceAllThrowsMidwayThroughExistingValue() { 103 doTestReplaceAllThrowsMidwayThrough( 104 (k, v) -> { 105 int index = keys().asList().indexOf(k); 106 return values().asList().get((index + 1) % getNumEntries()); 107 }); 108 } 109 110 private void doTestReplaceAllThrowsMidwayThrough(BiFunction<K, V, V> functionForFirstEntry) { 111 Map<K, V> map = getMap(); 112 113 /* 114 * We set up a stateful function that replaces the first value it encounters (and stores the new 115 * entry) and then throws afterward. 116 * 117 * We expect either: 118 * 119 * - The first value is replaced in the map, and everything else is untouched. 120 * 121 * - The entire map is untouched because the map either computes all replacements before making 122 * changes or else rolls back changes in case of an exception. 123 */ 124 125 AtomicReference<@Nullable Entry<K, V>> firstReplacement = new AtomicReference<>(); 126 assertThrows( 127 SomeUncheckedException.class, 128 () -> 129 map.replaceAll( 130 (k, v) -> { 131 if (firstReplacement.get() == null) { 132 V newValue = functionForFirstEntry.apply(k, v); 133 firstReplacement.set(mapEntry(k, newValue)); 134 return newValue; 135 } 136 throw new SomeUncheckedException(); 137 })); 138 Entry<K, V> replacement = firstReplacement.get(); 139 if (Objects.equals(map.get(replacement.getKey()), replacement.getValue())) { 140 expectReplacement(replacement); 141 } else { 142 expectUnchanged(); 143 } 144 145 List<V> expectedValues = new ArrayList<>(); 146 for (Entry<K, V> entry : map.entrySet()) { 147 expectedValues.add(entry.getValue()); 148 } 149 assertEqualIgnoringOrder(expectedValues, map.values()); 150 assertEquals(expectedValues.size(), map.values().size()); 151 for (V value : values().asList()) { 152 if (expectedValues.contains(value)) { 153 assertTrue("Should contain value " + value, map.containsValue(value)); 154 assertTrue("values() should contain " + value, map.values().contains(value)); 155 } else { 156 expectMissingValues(value); 157 assertFalse("values() should not contain " + value, map.values().contains(value)); 158 } 159 } 160 161 if (map.values() instanceof Set) { 162 Set<V> values = (Set<V>) getMap().values(); 163 assertEquals( 164 "values() Set should not contain duplicate values", 165 expectedValues.size(), 166 copyToSet(expectedValues).size()); 167 assertEquals(copyToSet(expectedValues), values); 168 } 169 } 170 171 @MapFeature.Require(absent = SUPPORTS_PUT) 172 @CollectionSize.Require(absent = ZERO) 173 public void testReplaceAll_unsupported() { 174 assertThrows( 175 UnsupportedOperationException.class, 176 () -> 177 getMap() 178 .replaceAll( 179 (K k, V v) -> { 180 int index = keys().asList().indexOf(k); 181 return values().asList().get(index + 1); 182 })); 183 expectUnchanged(); 184 } 185 186 @MapFeature.Require(absent = SUPPORTS_PUT) 187 @CollectionSize.Require(ZERO) 188 public void testReplaceAll_unsupportedByEmptyCollection() { 189 try { 190 getMap() 191 .replaceAll( 192 (K k, V v) -> { 193 int index = keys().asList().indexOf(k); 194 return values().asList().get(index + 1); 195 }); 196 } catch (UnsupportedOperationException tolerated) { 197 } 198 expectUnchanged(); 199 } 200 201 @MapFeature.Require(absent = SUPPORTS_PUT) 202 public void testReplaceAll_unsupportedNoOpFunction() { 203 try { 204 getMap().replaceAll((K k, V v) -> v); 205 } catch (UnsupportedOperationException tolerated) { 206 } 207 expectUnchanged(); 208 } 209}