/*
* Copyright (C) 2010 The Android Open Source Project
*
* 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.android.layoutlib.bridge;
import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
import com.android.tools.layoutlib.create.CreateInfo;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import junit.framework.TestCase;
/**
* Tests that native delegate classes implement all the required methods.
*
* This looks at {@link CreateInfo#DELEGATE_CLASS_NATIVES} to get the list of classes that
* have their native methods reimplemented through a delegate.
*
* Since the reimplemented methods are not native anymore, we look for the annotation
* {@link LayoutlibDelegate}, and look for a matching method in the delegate (named the same
* as the modified class with _Delegate added as a suffix).
* If the original native method is not static, then we make sure the delegate method also
* include the original class as first parameter (to access "this").
*
*/
public class TestDelegates extends TestCase {
private List<String> mErrors = new ArrayList<String>();
public void testNativeDelegates() {
final String[] classes = CreateInfo.DELEGATE_CLASS_NATIVES;
mErrors.clear();
for (String clazz : classes) {
loadAndCompareClasses(clazz, clazz + "_Delegate");
}
assertTrue(getErrors(), mErrors.isEmpty());
}
public void testMethodDelegates() {
final String[] methods = CreateInfo.DELEGATE_METHODS;
mErrors.clear();
for (String methodName : methods) {
// extract the class name
String className = methodName.substring(0, methodName.indexOf('#'));
String targetClassName = className.replace('$', '_') + "_Delegate";
loadAndCompareClasses(className, targetClassName);
}
assertTrue(getErrors(), mErrors.isEmpty());
}
private void loadAndCompareClasses(String originalClassName, String delegateClassName) {
// load the classes
try {
ClassLoader classLoader = TestDelegates.class.getClassLoader();
Class<?> originalClass = classLoader.loadClass(originalClassName);
Class<?> delegateClass = classLoader.loadClass(delegateClassName);
compare(originalClass, delegateClass);
} catch (ClassNotFoundException e) {
mErrors.add("Failed to load class: " + e.getMessage());
} catch (SecurityException e) {
mErrors.add("Failed to load class: " + e.getMessage());
}
}
private void compare(Class<?> originalClass, Class<?> delegateClass) throws SecurityException {
List<Method> checkedDelegateMethods = new ArrayList<Method>();
// loop on the methods of the original class, and for the ones that are annotated
// with @LayoutlibDelegate, look for a matching method in the delegate class.
// The annotation is automatically added by layoutlib_create when it replace a method
// by a call to a delegate
Method[] originalMethods = originalClass.getDeclaredMethods();
for (Method originalMethod : originalMethods) {
// look for methods that are delegated: they have the LayoutlibDelegate annotation
if (originalMethod.getAnnotation(LayoutlibDelegate.class) == null) {
continue;
}
// get the signature.
Class<?>[] parameters = originalMethod.getParameterTypes();
// if the method is not static, then the class is added as the first parameter
// (for "this")
if ((originalMethod.getModifiers() & Modifier.STATIC) == 0) {
Class<?>[] newParameters = new Class<?>[parameters.length + 1];
newParameters[0] = originalClass;
System.arraycopy(parameters, 0, newParameters, 1, parameters.length);
parameters = newParameters;
}
// if the original class is an inner class that's not static, then
// we add this on the enclosing class at the beginning
if (originalClass.getEnclosingClass() != null &&
(originalClass.getModifiers() & Modifier.STATIC) == 0) {
Class<?>[] newParameters = new Class<?>[parameters.length + 1];
newParameters[0] = originalClass.getEnclosingClass();
System.arraycopy(parameters, 0, newParameters, 1, parameters.length);
parameters = newParameters;
}
try {
// try to load the method with the given parameter types.
Method delegateMethod = delegateClass.getDeclaredMethod(originalMethod.getName(),
parameters);
// check the return type of the methods match.
if (delegateMethod.getReturnType() != originalMethod.getReturnType()) {
mErrors.add(
String.format("Delegate method %1$s.%2$s does not match the " +
"corresponding framework method which returns %3$s",
delegateClass.getName(),
getMethodName(delegateMethod),
originalMethod.getReturnType().getName()));
}
// check that the method has the annotation
if (delegateMethod.getAnnotation(LayoutlibDelegate.class) == null) {
mErrors.add(
String.format("Delegate method %1$s for class %2$s does not have the " +
"@LayoutlibDelegate annotation",
delegateMethod.getName(),
originalClass.getName()));
}
// check that the method is static
if ((delegateMethod.getModifiers() & Modifier.STATIC) != Modifier.STATIC) {
mErrors.add(
String.format(
"Delegate method %1$s for class %2$s is not static",
delegateMethod.getName(),
originalClass.getName())
);
}
// add the method as checked.
checkedDelegateMethods.add(delegateMethod);
} catch (NoSuchMethodException e) {
String name = getMethodName(originalMethod, parameters);
mErrors.add(String.format("Missing %1$s.%2$s", delegateClass.getName(), name));
}
}
// look for dead (delegate) code.
// This looks for all methods in the delegate class, and if they have the
// @LayoutlibDelegate annotation, make sure they have been previously found as a
// match for a method in the original class.
// If not, this means the method is a delegate for a method that either doesn't exist
// anymore or is not delegated anymore.
Method[] delegateMethods = delegateClass.getDeclaredMethods();
for (Method delegateMethod : delegateMethods) {
// look for methods that are delegates: they have the LayoutlibDelegate annotation
if (delegateMethod.getAnnotation(LayoutlibDelegate.class) == null) {
continue;
}
if (!checkedDelegateMethods.contains(delegateMethod)) {
mErrors.add(String.format(
"Delegate method %1$s.%2$s is not used anymore and must be removed",
delegateClass.getName(),
getMethodName(delegateMethod)));
}
}
}
private String getMethodName(Method method) {
return getMethodName(method, method.getParameterTypes());
}
private String getMethodName(Method method, Class<?>[] parameters) {
// compute a full class name that's long but not too long.
StringBuilder sb = new StringBuilder(method.getName() + "(");
for (int j = 0; j < parameters.length; j++) {
Class<?> theClass = parameters[j];
int dimensions = 0;
while (theClass.isArray()) {
dimensions++;
theClass = theClass.getComponentType();
}
sb.append(theClass.getName());
for (int i = 0; i < dimensions; i++) {
sb.append("[]");
}
if (j < (parameters.length - 1)) {
sb.append(",");
}
}
sb.append(")");
return sb.toString();
}
private String getErrors() {
StringBuilder s = new StringBuilder();
for (String error : mErrors) {
s.append(error).append('\n');
}
return s.toString();
}
}