/* * Copyright (C) 2008 The Guava Authors * * Licensed 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 com.google.common.collect.testing; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.testing.features.MapFeature.ALLOWS_NULL_KEYS; import static com.google.common.collect.testing.features.MapFeature.ALLOWS_NULL_VALUES; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.testing.features.CollectionFeature; import com.google.common.collect.testing.features.CollectionSize; import com.google.common.collect.testing.features.Feature; import com.google.common.collect.testing.features.MapFeature; import com.google.common.reflect.Reflection; import java.io.Serializable; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; import org.checkerframework.checker.nullness.qual.Nullable; /** * Tests {@link MapTestSuiteBuilder} by using it against maps that have various negative behaviors. * * @author George van den Driessche */ public final class MapTestSuiteBuilderTests extends TestCase { private MapTestSuiteBuilderTests() {} public static Test suite() { TestSuite suite = new TestSuite(MapTestSuiteBuilderTests.class.getSimpleName()); suite.addTest(testsForHashMapNullKeysForbidden()); suite.addTest(testsForHashMapNullValuesForbidden()); suite.addTest(testsForSetUpTearDown()); return suite; } private abstract static class WrappedHashMapGenerator extends TestStringMapGenerator { @Override protected final Map create(Entry[] entries) { HashMap map = Maps.newHashMap(); for (Entry entry : entries) { map.put(entry.getKey(), entry.getValue()); } return wrap(map); } abstract Map wrap(HashMap map); } private static TestSuite wrappedHashMapTests( WrappedHashMapGenerator generator, String name, Feature... features) { List> featuresList = Lists.newArrayList(features); Collections.addAll( featuresList, MapFeature.GENERAL_PURPOSE, CollectionFeature.SUPPORTS_ITERATOR_REMOVE, CollectionSize.ANY); return MapTestSuiteBuilder.using(generator) .named(name) .withFeatures(featuresList) .createTestSuite(); } // TODO: consider being null-hostile in these tests private static Test testsForHashMapNullKeysForbidden() { return wrappedHashMapTests( new WrappedHashMapGenerator() { @Override Map wrap(final HashMap map) { if (map.containsKey(null)) { throw new NullPointerException(); } return new AbstractMap() { @Override public Set> entrySet() { return map.entrySet(); } @Override public @Nullable String put(String key, String value) { checkNotNull(key); return map.put(key, value); } }; } }, "HashMap w/out null keys", ALLOWS_NULL_VALUES); } private static Test testsForHashMapNullValuesForbidden() { return wrappedHashMapTests( new WrappedHashMapGenerator() { @Override Map wrap(final HashMap map) { if (map.containsValue(null)) { throw new NullPointerException(); } return new AbstractMap() { @Override public Set> entrySet() { return new EntrySet(); } @Override public int hashCode() { return map.hashCode(); } @Override public boolean equals(@Nullable Object o) { return map.equals(o); } @Override public String toString() { return map.toString(); } @Override public @Nullable String remove(Object key) { return map.remove(key); } @Override public boolean remove(Object key, Object value) { return map.remove(key, value); } class EntrySet extends AbstractSet> { @Override public Iterator> iterator() { return new Iterator>() { final Iterator> iterator = map.entrySet().iterator(); @Override public void remove() { iterator.remove(); } @Override public boolean hasNext() { return iterator.hasNext(); } @Override public Entry next() { return transform(iterator.next()); } private Entry transform(final Entry next) { return new Entry() { @Override public String setValue(String value) { checkNotNull(value); return next.setValue(value); } @Override public String getValue() { return next.getValue(); } @Override public String getKey() { return next.getKey(); } @Override public boolean equals(@Nullable Object obj) { return next.equals(obj); } @Override public int hashCode() { return next.hashCode(); } }; } }; } @Override public int size() { return map.size(); } @Override public boolean remove(Object o) { return map.entrySet().remove(o); } @Override public boolean removeIf(Predicate> filter) { return map.entrySet().removeIf(filter); } @Override public boolean containsAll(Collection c) { return map.entrySet().containsAll(c); } @Override public boolean removeAll(Collection c) { return map.entrySet().removeAll(c); } @Override public boolean retainAll(Collection c) { return map.entrySet().retainAll(c); } @Override public int hashCode() { return map.entrySet().hashCode(); } @Override public boolean equals(@Nullable Object o) { return map.entrySet().equals(o); } @Override public String toString() { return map.entrySet().toString(); } } @Override public @Nullable String put(String key, String value) { checkNotNull(value); return map.put(key, value); } }; } }, "HashMap w/out null values", ALLOWS_NULL_KEYS); } /** * Map generator that verifies that {@code setUp()} methods are called in all the test cases. The * {@code setUpRan} parameter is set true by the {@code setUp} that every test case is supposed to * have registered, and set false by the {@code tearDown}. We use a dynamic proxy to intercept all * of the {@code Map} method calls and check that {@code setUpRan} is true. */ private static class CheckSetUpHashMapGenerator extends WrappedHashMapGenerator { private final AtomicBoolean setUpRan; CheckSetUpHashMapGenerator(AtomicBoolean setUpRan) { this.setUpRan = setUpRan; } @Override Map wrap(HashMap map) { @SuppressWarnings("unchecked") Map proxy = Reflection.newProxy(Map.class, new CheckSetUpInvocationHandler(map, setUpRan)); return proxy; } } /** * Intercepts calls to a {@code Map} to check that {@code setUpRan} is true when they happen. Then * forwards the calls to the underlying {@code Map}. */ private static class CheckSetUpInvocationHandler implements InvocationHandler, Serializable { private final Map map; private final AtomicBoolean setUpRan; CheckSetUpInvocationHandler(Map map, AtomicBoolean setUpRan) { this.map = map; this.setUpRan = setUpRan; } @Override public Object invoke(Object target, Method method, Object[] args) throws Throwable { assertTrue("setUp should have run", setUpRan.get()); try { return method.invoke(map, args); } catch (InvocationTargetException e) { throw e.getCause(); } catch (IllegalAccessException e) { throw newLinkageError(e); } } } /** Verifies that {@code setUp} and {@code tearDown} are called in all map test cases. */ private static Test testsForSetUpTearDown() { final AtomicBoolean setUpRan = new AtomicBoolean(); Runnable setUp = new Runnable() { @Override public void run() { assertFalse("previous tearDown should have run before setUp", setUpRan.getAndSet(true)); } }; Runnable tearDown = new Runnable() { @Override public void run() { assertTrue("setUp should have run", setUpRan.getAndSet(false)); } }; return MapTestSuiteBuilder.using(new CheckSetUpHashMapGenerator(setUpRan)) .named("setUpTearDown") .withFeatures( MapFeature.GENERAL_PURPOSE, MapFeature.ALLOWS_NULL_KEYS, MapFeature.ALLOWS_NULL_VALUES, CollectionFeature.SERIALIZABLE, CollectionFeature.SUPPORTS_ITERATOR_REMOVE, CollectionSize.ANY) .withSetUp(setUp) .withTearDown(tearDown) .createTestSuite(); } private static LinkageError newLinkageError(Throwable cause) { LinkageError error = new LinkageError(cause.toString()); error.initCause(cause); return error; } }