/* * Copyright (C) 2016 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.reflect; import static com.google.common.base.Preconditions.checkState; import static com.google.common.truth.Truth.assertThat; import com.google.errorprone.annotations.RequiredModifiers; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.Arrays; import java.util.Comparator; import javax.lang.model.element.Modifier; /** * Tester of subtyping relationships between two types. * * Tests should inherit from this class, and declare subtyping relationship with public methods * annotated by {@link TestSubtype}. * * <p>These declaration methods rely on Java static type checking to make sure what we want to * assert as subtypes are really subtypes according to javac. For example: * <pre> {@code * * class MySubtypeTests extends SubtypeTester { * @TestSubtype(suppressGetSubtype = true, suppressGetSupertype = true) * public <T> Iterable<? extends T> listIsSubtypeOfIterable(List<T> list) { * return isSubtype(list); * } * * @TestSubtype * public List<String> intListIsNotSubtypeOfStringList(List<Integer> intList) { * return notSubtype(intList); * } * } * * public void testMySubtypes() throws Exception { * new MySubtypeTests().testAllDeclarations(); * } * }</pre> * * The calls to {@link #isSubtype} and {@link #notSubtype} tells the framework what assertions need * to be made. * * <p>The declaration methods must be public. */ @AndroidIncompatible // only used by android incompatible tests. abstract class SubtypeTester implements Cloneable { /** Annotates a public method that declares subtype assertion. */ @RequiredModifiers(Modifier.PUBLIC) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface TestSubtype { /** Suppresses the assertion on {@link TypeToken#getSubtype}. */ boolean suppressGetSubtype() default false; /** Suppresses the assertion on {@link TypeToken#getSupertype}. */ boolean suppressGetSupertype() default false; } private Method method = null; /** Call this in a {@link TestSubtype} public method asserting subtype relationship. */ final <T> T isSubtype(T sub) { Type returnType = method.getGenericReturnType(); Type paramType = getOnlyParameterType(); TestSubtype spec = method.getAnnotation(TestSubtype.class); assertThat(TypeToken.of(paramType).isSubtypeOf(returnType)) .named("%s is subtype of %s", paramType, returnType) .isTrue(); assertThat(TypeToken.of(returnType).isSupertypeOf(paramType)) .named("%s is supertype of %s", returnType, paramType) .isTrue(); if (!spec.suppressGetSubtype()) { assertThat(getSubtype(returnType, TypeToken.of(paramType).getRawType())) .isEqualTo(paramType); } if (!spec.suppressGetSupertype()) { assertThat(getSupertype(paramType, TypeToken.of(returnType).getRawType())) .isEqualTo(returnType); } return sub; } /** * Call this in a {@link TestSubtype} public method asserting that subtype relationship does not * hold. */ final <X> X notSubtype(@SuppressWarnings("unused") Object sub) { Type returnType = method.getGenericReturnType(); Type paramType = getOnlyParameterType(); TestSubtype spec = method.getAnnotation(TestSubtype.class); assertThat(TypeToken.of(paramType).isSubtypeOf(returnType)) .named("%s is subtype of %s", paramType, returnType) .isFalse(); assertThat(TypeToken.of(returnType).isSupertypeOf(paramType)) .named("%s is supertype of %s", returnType, paramType) .isFalse(); if (!spec.suppressGetSubtype()) { try { assertThat(getSubtype(returnType, TypeToken.of(paramType).getRawType())) .isNotEqualTo(paramType); } catch (IllegalArgumentException notSubtype1) { // The raw class isn't even a subclass. } } if (!spec.suppressGetSupertype()) { try { assertThat(getSupertype(paramType, TypeToken.of(returnType).getRawType())) .isNotEqualTo(returnType); } catch (IllegalArgumentException notSubtype2) { // The raw class isn't even a subclass. } } return null; } final void testAllDeclarations() throws Exception { checkState(method == null); Method[] methods = getClass().getMethods(); Arrays.sort(methods, new Comparator<Method>() { @Override public int compare(Method a, Method b) { return a.getName().compareTo(b.getName()); } }); for (Method method : methods) { if (method.isAnnotationPresent(TestSubtype.class)) { method.setAccessible(true); SubtypeTester tester = (SubtypeTester) clone(); tester.method = method; method.invoke(tester, new Object[] {null}); } } } private Type getOnlyParameterType() { assertThat(method.getGenericParameterTypes()).hasLength(1); return method.getGenericParameterTypes()[0]; } @SuppressWarnings({"rawtypes", "unchecked"}) private static Type getSupertype(Type type, Class<?> superclass) { Class rawType = superclass; return TypeToken.of(type).getSupertype(rawType).getType(); } private static Type getSubtype(Type type, Class<?> subclass) { return TypeToken.of(type).getSubtype(subclass).getType(); } }