ConstructorConstructorTest.java
/*
* Copyright (C) 2022 Google Inc.
*
* 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.gson.internal;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertThrows;
import com.google.gson.reflect.TypeToken;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.LinkedBlockingDeque;
import org.junit.Test;
public class ConstructorConstructorTest {
private ConstructorConstructor constructorConstructor =
new ConstructorConstructor(Collections.emptyMap(), true, Collections.emptyList());
private abstract static class AbstractClass {
@SuppressWarnings("unused")
public AbstractClass() {}
}
private interface Interface {}
/**
* Verify that ConstructorConstructor does not try to invoke no-args constructor of abstract
* class.
*/
@Test
public void testGet_AbstractClassNoArgConstructor() {
ObjectConstructor<AbstractClass> constructor =
constructorConstructor.get(TypeToken.get(AbstractClass.class));
var e = assertThrows(RuntimeException.class, () -> constructor.construct());
assertThat(e)
.hasMessageThat()
.isEqualTo(
"Abstract classes can't be instantiated! Adjust the R8 configuration or register an"
+ " InstanceCreator or a TypeAdapter for this type. Class name:"
+ " com.google.gson.internal.ConstructorConstructorTest$AbstractClass\n"
+ "See https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class");
}
@Test
public void testGet_Interface() {
ObjectConstructor<Interface> constructor =
constructorConstructor.get(TypeToken.get(Interface.class));
var e = assertThrows(RuntimeException.class, () -> constructor.construct());
assertThat(e)
.hasMessageThat()
.isEqualTo(
"Interfaces can't be instantiated! Register an InstanceCreator or a TypeAdapter for"
+ " this type. Interface name:"
+ " com.google.gson.internal.ConstructorConstructorTest$Interface");
}
@SuppressWarnings("serial")
private static class CustomSortedSet<E> extends TreeSet<E> {
// Removes default no-args constructor
@SuppressWarnings("unused")
CustomSortedSet(Void v) {}
}
@SuppressWarnings("serial")
private static class CustomSet<E> extends HashSet<E> {
// Removes default no-args constructor
@SuppressWarnings("unused")
CustomSet(Void v) {}
}
@SuppressWarnings("serial")
private static class CustomQueue<E> extends LinkedBlockingDeque<E> {
// Removes default no-args constructor
@SuppressWarnings("unused")
CustomQueue(Void v) {}
}
@SuppressWarnings("serial")
private static class CustomList<E> extends ArrayList<E> {
// Removes default no-args constructor
@SuppressWarnings("unused")
CustomList(Void v) {}
}
/**
* Tests that creation of custom {@code Collection} subclasses without no-args constructor should
* not use default JDK types (which would cause {@link ClassCastException}).
*
* <p>Currently this test is rather contrived because the instances created using Unsafe are not
* usable because their fields are not properly initialized, but assume that user has custom
* classes which would be functional.
*/
@Test
public void testCustomCollectionCreation() {
Class<?>[] collectionTypes = {
CustomSortedSet.class, CustomSet.class, CustomQueue.class, CustomList.class,
};
for (Class<?> collectionType : collectionTypes) {
Object actual =
constructorConstructor
.get(TypeToken.getParameterized(collectionType, Integer.class))
.construct();
assertWithMessage(
"Failed for " + collectionType + "; created instance of " + actual.getClass())
.that(actual)
.isInstanceOf(collectionType);
}
}
private static interface CustomCollectionInterface extends Collection<String> {}
private static interface CustomSetInterface extends Set<String> {}
private static interface CustomListInterface extends List<String> {}
@Test
public void testCustomCollectionInterfaceCreation() {
Class<?>[] interfaces = {
CustomCollectionInterface.class, CustomSetInterface.class, CustomListInterface.class,
};
for (Class<?> interfaceType : interfaces) {
var objectConstructor = constructorConstructor.get(TypeToken.get(interfaceType));
var exception = assertThrows(RuntimeException.class, () -> objectConstructor.construct());
assertThat(exception)
.hasMessageThat()
.isEqualTo(
"Interfaces can't be instantiated! Register an InstanceCreator or a TypeAdapter"
+ " for this type. Interface name: "
+ interfaceType.getName());
}
}
@Test
public void testStringMapCreation() {
// When creating raw Map should use Gson's LinkedTreeMap, assuming keys could be String
Object actual = constructorConstructor.get(TypeToken.get(Map.class)).construct();
assertThat(actual).isInstanceOf(LinkedTreeMap.class);
// When creating a `Map<String, ...>` should use Gson's LinkedTreeMap
actual = constructorConstructor.get(new TypeToken<Map<String, Integer>>() {}).construct();
assertThat(actual).isInstanceOf(LinkedTreeMap.class);
// But when explicitly requesting a JDK `LinkedHashMap<String, ...>` should use LinkedHashMap
actual =
constructorConstructor.get(new TypeToken<LinkedHashMap<String, Integer>>() {}).construct();
assertThat(actual).isInstanceOf(LinkedHashMap.class);
// For all Map types with non-String key, should use JDK LinkedHashMap by default
// This is also done to avoid ClassCastException later, because Gson's LinkedTreeMap requires
// that keys are Comparable
Class<?>[] nonStringTypes = {Integer.class, CharSequence.class, Object.class};
for (Class<?> keyType : nonStringTypes) {
actual =
constructorConstructor
.get(TypeToken.getParameterized(Map.class, keyType, Integer.class))
.construct();
assertWithMessage(
"Failed for key type " + keyType + "; created instance of " + actual.getClass())
.that(actual)
.isInstanceOf(LinkedHashMap.class);
}
}
private enum MyEnum {}
@SuppressWarnings("serial")
private static class CustomEnumMap<K, V> extends EnumMap<MyEnum, V> {
@SuppressWarnings("unused")
CustomEnumMap(Void v) {
super(MyEnum.class);
}
}
@SuppressWarnings("serial")
private static class CustomConcurrentNavigableMap<K, V> extends ConcurrentSkipListMap<K, V> {
// Removes default no-args constructor
@SuppressWarnings("unused")
CustomConcurrentNavigableMap(Void v) {}
}
@SuppressWarnings("serial")
private static class CustomConcurrentMap<K, V> extends ConcurrentHashMap<K, V> {
// Removes default no-args constructor
@SuppressWarnings("unused")
CustomConcurrentMap(Void v) {}
}
@SuppressWarnings("serial")
private static class CustomSortedMap<K, V> extends TreeMap<K, V> {
// Removes default no-args constructor
@SuppressWarnings("unused")
CustomSortedMap(Void v) {}
}
@SuppressWarnings("serial")
private static class CustomLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
// Removes default no-args constructor
@SuppressWarnings("unused")
CustomLinkedHashMap(Void v) {}
}
/**
* Tests that creation of custom {@code Map} subclasses without no-args constructor should not use
* default JDK types (which would cause {@link ClassCastException}).
*
* <p>Currently this test is rather contrived because the instances created using Unsafe are not
* usable because their fields are not properly initialized, but assume that user has custom
* classes which would be functional.
*/
@Test
public void testCustomMapCreation() {
Class<?>[] mapTypes = {
CustomEnumMap.class,
CustomConcurrentNavigableMap.class,
CustomConcurrentMap.class,
CustomSortedMap.class,
CustomLinkedHashMap.class,
};
for (Class<?> mapType : mapTypes) {
Object actual =
constructorConstructor
.get(TypeToken.getParameterized(mapType, String.class, Integer.class))
.construct();
assertWithMessage("Failed for " + mapType + "; created instance of " + actual.getClass())
.that(actual)
.isInstanceOf(mapType);
}
}
private static interface CustomMapInterface extends Map<String, Integer> {}
@Test
public void testCustomMapInterfaceCreation() {
var objectConstructor = constructorConstructor.get(TypeToken.get(CustomMapInterface.class));
var exception = assertThrows(RuntimeException.class, () -> objectConstructor.construct());
assertThat(exception)
.hasMessageThat()
.isEqualTo(
"Interfaces can't be instantiated! Register an InstanceCreator or a TypeAdapter"
+ " for this type. Interface name: "
+ CustomMapInterface.class.getName());
}
}