Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.compatibility.common.util;
     18 
     19 import java.lang.reflect.InvocationTargetException;
     20 import java.lang.reflect.Method;
     21 import java.util.ArrayList;
     22 import java.util.Arrays;
     23 import java.util.List;
     24 
     25 import org.junit.AssumptionViolatedException;
     26 
     27 /**
     28  * Resolves methods provided by the BusinessLogicService and invokes them
     29  */
     30 public abstract class BusinessLogicExecutor {
     31 
     32     protected static final String LOG_TAG = "BusinessLogicExecutor";
     33 
     34     /** String representations of the String class and String[] class */
     35     protected static final String STRING_CLASS = "java.lang.String";
     36     protected static final String STRING_ARRAY_CLASS = "[Ljava.lang.String;";
     37 
     38     /**
     39      * Execute a business logic condition.
     40      * @param method the name of the method to invoke. Must include fully qualified name of the
     41      * enclosing class, followed by '.', followed by the name of the method
     42      * @param args the string arguments to supply to the method
     43      * @return the return value of the method invoked
     44      * @throws RuntimeException when failing to resolve or invoke the condition method
     45      */
     46     public boolean executeCondition(String method, String... args) {
     47         try {
     48             return (Boolean) invokeMethod(method, args);
     49         } catch (ClassNotFoundException | IllegalAccessException | InstantiationException |
     50                 InvocationTargetException | NoSuchMethodException e) {
     51             throw new RuntimeException(String.format(
     52                     "BusinessLogic: Failed to invoke condition method %s with args: %s", method,
     53                     Arrays.toString(args)), e);
     54         }
     55     }
     56 
     57     /**
     58      * Execute a business logic action.
     59      * @param method the name of the method to invoke. Must include fully qualified name of the
     60      * enclosing class, followed by '.', followed by the name of the method
     61      * @param args the string arguments to supply to the method
     62      * @throws RuntimeException when failing to resolve or invoke the action method
     63      */
     64     public void executeAction(String method, String... args) {
     65         try {
     66             invokeMethod(method, args);
     67         } catch (ClassNotFoundException | IllegalAccessException | InstantiationException |
     68                 NoSuchMethodException e) {
     69             throw new RuntimeException(String.format(
     70                     "BusinessLogic: Failed to invoke action method %s with args: %s", method,
     71                     Arrays.toString(args)), e);
     72         } catch (InvocationTargetException e) {
     73             // This action throws an exception, so throw the original exception (e.g.
     74             // AssertionFailedError) for a more readable stacktrace.
     75             Throwable t = e.getCause();
     76             if (AssumptionViolatedException.class.isInstance(t)) {
     77                 // This is an assumption failure (registered as a "pass") so don't wrap this
     78                 // throwable in a RuntimeException
     79                 throw (AssumptionViolatedException) t;
     80             } else {
     81                 RuntimeException re = new RuntimeException(t.getMessage(), t.getCause());
     82                 re.setStackTrace(t.getStackTrace());
     83                 throw re;
     84             }
     85         }
     86     }
     87 
     88     /**
     89      * Execute a business logic method.
     90      * @param method the name of the method to invoke. Must include fully qualified name of the
     91      * enclosing class, followed by '.', followed by the name of the method
     92      * @param args the string arguments to supply to the method
     93      * @return the return value of the method invoked (type Boolean if method is a condition)
     94      * @throws RuntimeException when failing to resolve or invoke the method
     95      */
     96     protected Object invokeMethod(String method, String... args) throws ClassNotFoundException,
     97             IllegalAccessException, InstantiationException, InvocationTargetException,
     98             NoSuchMethodException {
     99         // Method names served by the BusinessLogic service should assume format
    100         // classname.methodName, but also handle format classname#methodName since test names use
    101         // this format
    102         int index = (method.indexOf('#') == -1) ? method.lastIndexOf('.') : method.indexOf('#');
    103         if (index == -1) {
    104             throw new RuntimeException(String.format("BusinessLogic: invalid method name "
    105                     + "\"%s\". Method string must include fully qualified class name. "
    106                     + "For example, \"com.android.packagename.ClassName.methodName\".", method));
    107         }
    108         String className = method.substring(0, index);
    109         Class cls = Class.forName(className);
    110         Object obj = cls.getDeclaredConstructor().newInstance();
    111         if (getTestObject() != null && cls.isAssignableFrom(getTestObject().getClass())) {
    112             // The given method is a member of the test class, use the known test class instance
    113             obj = getTestObject();
    114         }
    115         ResolvedMethod rm = getResolvedMethod(cls, method.substring(index + 1), args);
    116         return rm.invoke(obj);
    117     }
    118 
    119     /**
    120      * Log information with whichever logging mechanism is available to the instance. This varies
    121      * from host-side to device-side, so implementations are left to subclasses.
    122      * See {@link String.format(String, Object...)} for parameter information.
    123      */
    124     public abstract void logInfo(String format, Object... args);
    125 
    126     /**
    127      * Get the test object. This method is left abstract, since non-abstract subclasses will set
    128      * the test object in the constructor.
    129      * @return the test case instance
    130      */
    131     protected abstract Object getTestObject();
    132 
    133     /**
    134      * Get the method and list of arguments corresponding to the class, method name, and proposed
    135      * argument values, in the form of a {@link ResolvedMethod} object. This object stores all
    136      * information required to successfully invoke the method. getResolvedMethod is left abstract,
    137      * since argument types differ between device-side (e.g. Context) and host-side
    138      * (e.g. ITestDevice) implementations of this class.
    139      * @param cls the Class to which the method belongs
    140      * @param methodName the name of the method to invoke
    141      * @param args the string arguments to use when invoking the method
    142      * @return a {@link ResolvedMethod}
    143      * @throws ClassNotFoundException
    144      */
    145     protected abstract ResolvedMethod getResolvedMethod(Class cls, String methodName,
    146             String... args) throws ClassNotFoundException;
    147 
    148     /**
    149      * Retrieve all methods within a class that match a given name
    150      * @param cls the class
    151      * @param name the method name
    152      * @return a list of method objects
    153      */
    154     protected List<Method> getMethodsWithName(Class cls, String name) {
    155         List<Method> methodList = new ArrayList<>();
    156         for (Method m : cls.getMethods()) {
    157             if (name.equals(m.getName())) {
    158                 methodList.add(m);
    159             }
    160         }
    161         return methodList;
    162     }
    163 
    164     /**
    165      * Helper class for storing a method object, and a list of arguments to use when invoking the
    166      * method. The class is also equipped with an "invoke" method for convenience.
    167      */
    168     protected static class ResolvedMethod {
    169         private Method mMethod;
    170         List<Object> mArgs;
    171 
    172         public ResolvedMethod(Method method) {
    173             mMethod = method;
    174             mArgs = new ArrayList<>();
    175         }
    176 
    177         /** Add an argument to the argument list for this instance */
    178         public void addArg(Object arg) {
    179             mArgs.add(arg);
    180         }
    181 
    182         /** Invoke the stored method with the stored args on a given object */
    183         public Object invoke(Object instance) throws IllegalAccessException,
    184                 InvocationTargetException {
    185             return mMethod.invoke(instance, mArgs.toArray());
    186         }
    187     }
    188 }
    189