blob: 8a9a0b653b12d36ade840e2393d2f8bd25142d16 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.collections4.bidimap;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.BulkTest;
import org.apache.commons.collections4.MapIterator;
import org.apache.commons.collections4.iterators.AbstractMapIteratorTest;
import org.apache.commons.collections4.map.AbstractIterableMapTest;
import org.junit.jupiter.api.Test;
/**
* Abstract test class for {@link BidiMap} methods and contracts.
*/
public abstract class AbstractBidiMapTest<K, V> extends AbstractIterableMapTest<K, V> {
public AbstractBidiMapTest(final String testName) {
super(testName);
}
public AbstractBidiMapTest() {
super("Inverse");
}
/**
* Override to create a full {@code BidiMap} other than the default.
*
* @return a full {@code BidiMap} implementation.
*/
@Override
public BidiMap<K, V> makeFullMap() {
return (BidiMap<K, V>) super.makeFullMap();
}
/**
* Override to return the empty BidiMap.
*/
@Override
public abstract BidiMap<K, V> makeObject();
/**
* Override to indicate to AbstractTestMap this is a BidiMap.
*/
@Override
public boolean isAllowDuplicateValues() {
return false;
}
/**
* Override as DualHashBidiMap didn't exist until version 3.
*/
@Override
public String getCompatibilityVersion() {
return "4";
}
// BidiPut
@Test
@SuppressWarnings("unchecked")
public void testBidiPut() {
if (!isPutAddSupported() || !isPutChangeSupported()) {
return;
}
final BidiMap<K, V> map = makeObject();
final BidiMap<V, K> inverse = map.inverseBidiMap();
assertEquals(0, map.size());
assertEquals(map.size(), inverse.size());
map.put((K) "A", (V) "B");
assertEquals(1, map.size());
assertEquals(map.size(), inverse.size());
assertEquals("B", map.get("A"));
assertEquals("A", inverse.get("B"));
map.put((K) "A", (V) "C");
assertEquals(1, map.size());
assertEquals(map.size(), inverse.size());
assertEquals("C", map.get("A"));
assertEquals("A", inverse.get("C"));
map.put((K) "B", (V) "C");
assertEquals(1, map.size());
assertEquals(map.size(), inverse.size());
assertEquals("C", map.get("B"));
assertEquals("B", inverse.get("C"));
map.put((K) "E", (V) "F");
assertEquals(2, map.size());
assertEquals(map.size(), inverse.size());
assertEquals("F", map.get("E"));
assertEquals("E", inverse.get("F"));
}
/**
* Verifies that {@link #map} is still equal to {@link #confirmed}.
* <p>
* This implementation checks the inverse map as well.
*/
@Override
public void verify() {
verifyInverse();
super.verify();
}
public void verifyInverse() {
assertEquals(map.size(), ((BidiMap<K, V>) map).inverseBidiMap().size());
final Map<K, V> map1 = new HashMap<>(map);
final Map<V, K> map2 = new HashMap<>(((BidiMap<K, V>) map).inverseBidiMap());
final Set<K> keys1 = map1.keySet();
final Set<V> keys2 = map2.keySet();
final Collection<V> values1 = map1.values();
final Collection<K> values2 = map2.values();
assertTrue(keys1.containsAll(values2));
assertTrue(values2.containsAll(keys1));
assertTrue(values1.containsAll(keys2));
assertTrue(keys2.containsAll(values1));
}
// testGetKey
@Test
public void testBidiGetKey() {
doTestGetKey(makeFullMap(), getSampleKeys()[0], getSampleValues()[0]);
}
@Test
public void testBidiGetKeyInverse() {
doTestGetKey(
makeFullMap().inverseBidiMap(),
getSampleValues()[0],
getSampleKeys()[0]);
}
private void doTestGetKey(final BidiMap<?, ?> map, final Object key, final Object value) {
assertEquals("Value not found for key.", value, map.get(key));
assertEquals("Key not found for value.", key, map.getKey(value));
}
// testInverse
@Test
public void testBidiInverse() {
final BidiMap<K, V> map = makeFullMap();
final BidiMap<V, K> inverseMap = map.inverseBidiMap();
assertSame(
"Inverse of inverse is not equal to original.",
map,
inverseMap.inverseBidiMap());
assertEquals(
"Value not found for key.",
getSampleKeys()[0],
inverseMap.get(getSampleValues()[0]));
assertEquals(
"Key not found for value.",
getSampleValues()[0],
inverseMap.getKey(getSampleKeys()[0]));
}
@Test
public void testBidiModifyEntrySet() {
if (!isSetValueSupported()) {
return;
}
modifyEntrySet(makeFullMap());
modifyEntrySet(makeFullMap().inverseBidiMap());
}
@SuppressWarnings("unchecked")
private <T> void modifyEntrySet(final BidiMap<?, T> map) {
// Gets first entry
final Map.Entry<?, T> entry = map.entrySet().iterator().next();
// Gets key and value
final Object key = entry.getKey();
final Object oldValue = entry.getValue();
// Sets new value
final Object newValue = "newValue";
entry.setValue((T) newValue);
assertEquals(
"Modifying entrySet did not affect underlying Map.",
newValue,
map.get(key));
assertNull(
"Modifying entrySet did not affect inverse Map.",
map.getKey(oldValue));
}
@Test
public void testBidiClear() {
if (!isRemoveSupported()) {
assertThrows(UnsupportedOperationException.class, () -> makeFullMap().clear());
return;
}
BidiMap<?, ?> map = makeFullMap();
map.clear();
assertTrue("Map was not cleared.", map.isEmpty());
assertTrue("Inverse map was not cleared.", map.inverseBidiMap().isEmpty());
// Tests clear on inverse
map = makeFullMap().inverseBidiMap();
map.clear();
assertTrue("Map was not cleared.", map.isEmpty());
assertTrue("Inverse map was not cleared.", map.inverseBidiMap().isEmpty());
}
@Test
public void testBidiRemove() {
if (!isRemoveSupported()) {
assertThrows(UnsupportedOperationException.class, () -> makeFullMap().remove(getSampleKeys()[0]));
assertThrows(UnsupportedOperationException.class, () -> makeFullMap().removeValue(getSampleValues()[0]));
return;
}
remove(makeFullMap(), getSampleKeys()[0]);
remove(makeFullMap().inverseBidiMap(), getSampleValues()[0]);
removeValue(makeFullMap(), getSampleValues()[0]);
removeValue(makeFullMap().inverseBidiMap(), getSampleKeys()[0]);
assertNull(makeFullMap().removeValue("NotPresent"));
}
private void remove(final BidiMap<?, ?> map, final Object key) {
final Object value = map.remove(key);
assertFalse("Key was not removed.", map.containsKey(key));
assertNull("Value was not removed.", map.getKey(value));
}
private void removeValue(final BidiMap<?, ?> map, final Object value) {
final Object key = map.removeValue(value);
assertFalse("Key was not removed.", map.containsKey(key));
assertNull("Value was not removed.", map.getKey(value));
}
@Test
public void testBidiKeySetValuesOrder() {
resetFull();
final Iterator<K> keys = map.keySet().iterator();
final Iterator<V> values = map.values().iterator();
while (keys.hasNext() && values.hasNext()) {
final K key = keys.next();
final V value = values.next();
assertSame(map.get(key), value);
}
assertFalse(keys.hasNext());
assertFalse(values.hasNext());
}
@Test
public void testBidiRemoveByKeySet() {
if (!isRemoveSupported()) {
return;
}
removeByKeySet(makeFullMap(), getSampleKeys()[0], getSampleValues()[0]);
removeByKeySet(makeFullMap().inverseBidiMap(), getSampleValues()[0], getSampleKeys()[0]);
}
private void removeByKeySet(final BidiMap<?, ?> map, final Object key, final Object value) {
map.remove(key);
assertFalse("Key was not removed.", map.containsKey(key));
assertFalse("Value was not removed.", map.containsValue(value));
assertFalse("Key was not removed from inverse map.", map.inverseBidiMap().containsValue(key));
assertFalse("Value was not removed from inverse map.", map.inverseBidiMap().containsKey(value));
}
@Test
public void testBidiRemoveByEntrySet() {
if (!isRemoveSupported()) {
return;
}
removeByEntrySet(makeFullMap(), getSampleKeys()[0], getSampleValues()[0]);
removeByEntrySet(makeFullMap().inverseBidiMap(), getSampleValues()[0], getSampleKeys()[0]);
}
private void removeByEntrySet(final BidiMap<?, ?> map, final Object key, final Object value) {
final Map<Object, Object> temp = new HashMap<>();
temp.put(key, value);
map.entrySet().remove(temp.entrySet().iterator().next());
assertFalse("Key was not removed.", map.containsKey(key));
assertFalse("Value was not removed.", map.containsValue(value));
assertFalse("Key was not removed from inverse map.", map.inverseBidiMap().containsValue(key));
assertFalse("Value was not removed from inverse map.", map.inverseBidiMap().containsKey(value));
}
/**
* {@inheritDoc}
*/
@Override
public BidiMap<K, V> getMap() {
return (BidiMap<K, V>) super.getMap();
}
@Override
public BulkTest bulkTestMapEntrySet() {
return new TestBidiMapEntrySet();
}
public class TestBidiMapEntrySet extends TestMapEntrySet {
public TestBidiMapEntrySet() {
}
@Test
public void testMapEntrySetIteratorEntrySetValueCrossCheck() {
final K key1 = getSampleKeys()[0];
final K key2 = getSampleKeys()[1];
final V newValue1 = getNewSampleValues()[0];
final V newValue2 = getNewSampleValues()[1];
resetFull();
// explicitly get entries as sample values/keys are connected for some maps
// such as BeanMap
Iterator<Map.Entry<K, V>> it = TestBidiMapEntrySet.this.getCollection().iterator();
final Map.Entry<K, V> entry1 = getEntry(it, key1);
it = TestBidiMapEntrySet.this.getCollection().iterator();
final Map.Entry<K, V> entry2 = getEntry(it, key2);
Iterator<Map.Entry<K, V>> itConfirmed = TestBidiMapEntrySet.this.getConfirmed().iterator();
final Map.Entry<K, V> entryConfirmed1 = getEntry(itConfirmed, key1);
itConfirmed = TestBidiMapEntrySet.this.getConfirmed().iterator();
final Map.Entry<K, V> entryConfirmed2 = getEntry(itConfirmed, key2);
TestBidiMapEntrySet.this.verify();
if (!isSetValueSupported()) {
try {
entry1.setValue(newValue1);
} catch (final UnsupportedOperationException ex) {
}
return;
}
// these checked in superclass
entry1.setValue(newValue1);
entryConfirmed1.setValue(newValue1);
entry2.setValue(newValue2);
entryConfirmed2.setValue(newValue2);
// at this point
// key1=newValue1, key2=newValue2
try {
entry2.setValue(newValue1); // should remove key1
} catch (final IllegalArgumentException ex) {
return; // simplest way of dealing with tricky situation
}
entryConfirmed2.setValue(newValue1);
AbstractBidiMapTest.this.getConfirmed().remove(key1);
assertEquals(newValue1, entry2.getValue());
assertTrue(AbstractBidiMapTest.this.getMap().containsKey(entry2.getKey()));
assertTrue(AbstractBidiMapTest.this.getMap().containsValue(newValue1));
assertEquals(newValue1, AbstractBidiMapTest.this.getMap().get(entry2.getKey()));
assertFalse(AbstractBidiMapTest.this.getMap().containsKey(key1));
assertFalse(AbstractBidiMapTest.this.getMap().containsValue(newValue2));
TestBidiMapEntrySet.this.verify();
// check for ConcurrentModification
it.next(); // if you fail here, maybe you should be throwing an IAE, see above
if (isRemoveSupported()) {
it.remove();
}
}
}
public BulkTest bulkTestInverseMap() {
return new TestInverseBidiMap(this);
}
public class TestInverseBidiMap extends AbstractBidiMapTest<V, K> {
final AbstractBidiMapTest<K, V> main;
public TestInverseBidiMap(final AbstractBidiMapTest<K, V> main) {
this.main = main;
}
@Override
public BidiMap<V, K> makeObject() {
return main.makeObject().inverseBidiMap();
}
@Override
public BidiMap<V, K> makeFullMap() {
return main.makeFullMap().inverseBidiMap();
}
@Override
public V[] getSampleKeys() {
return main.getSampleValues();
}
@Override
public K[] getSampleValues() {
return main.getSampleKeys();
}
@Override
public String getCompatibilityVersion() {
return main.getCompatibilityVersion();
}
@Override
public boolean isAllowNullKey() {
return main.isAllowNullKey();
}
@Override
public boolean isAllowNullValue() {
return main.isAllowNullValue();
}
@Override
public boolean isPutAddSupported() {
return main.isPutAddSupported();
}
@Override
public boolean isPutChangeSupported() {
return main.isPutChangeSupported();
}
@Override
public boolean isSetValueSupported() {
return main.isSetValueSupported();
}
@Override
public boolean isRemoveSupported() {
return main.isRemoveSupported();
}
@Override
protected int getIterationBehaviour() {
return main.getIterationBehaviour();
}
}
public BulkTest bulkTestBidiMapIterator() {
return new TestBidiMapIterator();
}
public class TestBidiMapIterator extends AbstractMapIteratorTest<K, V> {
public TestBidiMapIterator() {
super("TestBidiMapIterator");
}
@Override
public V[] addSetValues() {
return AbstractBidiMapTest.this.getNewSampleValues();
}
@Override
public boolean supportsRemove() {
return AbstractBidiMapTest.this.isRemoveSupported();
}
@Override
public boolean supportsSetValue() {
return AbstractBidiMapTest.this.isSetValueSupported();
}
@Override
public MapIterator<K, V> makeEmptyIterator() {
resetEmpty();
return AbstractBidiMapTest.this.getMap().mapIterator();
}
@Override
public MapIterator<K, V> makeObject() {
resetFull();
return AbstractBidiMapTest.this.getMap().mapIterator();
}
@Override
public BidiMap<K, V> getMap() {
// assumes makeFullMapIterator() called first
return AbstractBidiMapTest.this.getMap();
}
@Override
public Map<K, V> getConfirmedMap() {
// assumes makeFullMapIterator() called first
return AbstractBidiMapTest.this.getConfirmed();
}
@Override
public void verify() {
super.verify();
AbstractBidiMapTest.this.verify();
}
}
@Test
public void testBidiMapIteratorSet() {
final V newValue1 = getOtherValues()[0];
final V newValue2 = getOtherValues()[1];
resetFull();
final BidiMap<K, V> bidi = getMap();
final MapIterator<K, V> it = bidi.mapIterator();
assertTrue(it.hasNext());
final K key1 = it.next();
if (!isSetValueSupported()) {
assertThrows(UnsupportedOperationException.class, () -> it.setValue(newValue1));
return;
}
it.setValue(newValue1);
confirmed.put(key1, newValue1);
assertSame(key1, it.getKey());
assertSame(newValue1, it.getValue());
assertTrue(bidi.containsKey(key1));
assertTrue(bidi.containsValue(newValue1));
assertEquals(newValue1, bidi.get(key1));
verify();
it.setValue(newValue1); // same value - should be OK
confirmed.put(key1, newValue1);
assertSame(key1, it.getKey());
assertSame(newValue1, it.getValue());
assertTrue(bidi.containsKey(key1));
assertTrue(bidi.containsValue(newValue1));
assertEquals(newValue1, bidi.get(key1));
verify();
final K key2 = it.next();
it.setValue(newValue2);
confirmed.put(key2, newValue2);
assertSame(key2, it.getKey());
assertSame(newValue2, it.getValue());
assertTrue(bidi.containsKey(key2));
assertTrue(bidi.containsValue(newValue2));
assertEquals(newValue2, bidi.get(key2));
verify();
// at this point
// key1=newValue1, key2=newValue2
assertThrows(IllegalArgumentException.class, () -> it.setValue(newValue1)); // should remove key1
// below code was previously never executed
// confirmed.put(key2, newValue1);
// AbstractBidiMapTest.this.getConfirmed().remove(key1);
// assertEquals(newValue1, it.getValue());
// assertTrue(bidi.containsKey(it.getKey()));
// assertTrue(bidi.containsValue(newValue1));
// assertEquals(newValue1, bidi.get(it.getKey()));
// assertFalse(bidi.containsKey(key1));
// assertFalse(bidi.containsValue(newValue2));
// verify();
//
// // check for ConcurrentModification
// it.next(); // if you fail here, maybe you should be throwing an IAE, see above
// if (isRemoveSupported()) {
// it.remove();
// }
}
}