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}