1 /* 2 * Copyright (C) 2023 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 package com.android.hoststubgen.hosthelper; 17 18 import java.io.PrintStream; 19 import java.lang.reflect.Method; 20 import java.lang.reflect.Modifier; 21 import java.util.Arrays; 22 23 /** 24 * Utilities used in the host side test environment. 25 */ 26 public class HostTestUtils { HostTestUtils()27 private HostTestUtils() { 28 } 29 30 /** 31 * Same as ASM's Type.getInternalName(). Copied here, to avoid having a reference to ASM 32 * in this JAR. 33 */ getInternalName(final Class<?> clazz)34 public static String getInternalName(final Class<?> clazz) { 35 return clazz.getName().replace('.', '/'); 36 } 37 38 public static final String CLASS_INTERNAL_NAME = getInternalName(HostTestUtils.class); 39 40 /** If true, we skip all method call hooks */ 41 private static final boolean SKIP_METHOD_CALL_HOOK = "1".equals(System.getenv( 42 "HOSTTEST_SKIP_METHOD_CALL_HOOK")); 43 44 /** If true, we won't print method call log. */ 45 private static final boolean SKIP_METHOD_LOG = 46 "1".equals(System.getenv("HOSTTEST_SKIP_METHOD_LOG")) 47 || "1".equals(System.getenv("RAVENWOOD_NO_METHOD_LOG")); 48 49 /** If true, we won't print class load log. */ 50 private static final boolean SKIP_CLASS_LOG = "1".equals(System.getenv( 51 "HOSTTEST_SKIP_CLASS_LOG")); 52 53 /** If true, we won't perform non-stub method direct call check. */ 54 private static final boolean SKIP_NON_STUB_METHOD_CHECK = "1".equals(System.getenv( 55 "HOSTTEST_SKIP_NON_STUB_METHOD_CHECK")); 56 57 58 /** 59 * Method call log will be printed to it. 60 */ 61 public static PrintStream logPrintStream = System.out; 62 63 /** 64 * Called from methods with FilterPolicy.Throw. 65 */ onThrowMethodCalled()66 public static void onThrowMethodCalled() { 67 // TODO: Maybe add call tracking? 68 throw new RuntimeException( 69 "This method is not yet supported under the Ravenwood deviceless testing " 70 + "environment; consider requesting support from the API owner or " 71 + "consider using Mockito; more details at go/ravenwood-docs"); 72 } 73 74 private static final Class<?>[] sMethodHookArgTypes = 75 { Class.class, String.class, String.class}; 76 77 /** 78 * Trampoline method for method-call-hook. 79 */ callMethodCallHook( Class<?> methodClass, String methodName, String methodDescriptor, String callbackMethod )80 public static void callMethodCallHook( 81 Class<?> methodClass, 82 String methodName, 83 String methodDescriptor, 84 String callbackMethod 85 ) { 86 if (SKIP_METHOD_CALL_HOOK) { 87 return; 88 } 89 callStaticMethodByName(callbackMethod, "method call hook", sMethodHookArgTypes, 90 methodClass, methodName, methodDescriptor); 91 } 92 93 /** 94 * Simple implementation of method call hook, which just prints the information of the 95 * method. This is just for basic testing. We don't use it in Ravenwood, because this would 96 * be way too noisy as it prints every single method, even trivial ones. (iterator methods, 97 * etc..) 98 * 99 * I can be used as 100 * {@code --default-method-call-hook 101 * com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall}. 102 */ logMethodCall( Class<?> methodClass, String methodName, String methodDescriptor )103 public static void logMethodCall( 104 Class<?> methodClass, 105 String methodName, 106 String methodDescriptor 107 ) { 108 if (SKIP_METHOD_LOG) { 109 return; 110 } 111 logPrintStream.println("# method called: " + methodClass.getCanonicalName() + "." 112 + methodName + methodDescriptor); 113 } 114 115 private static final Class<?>[] sClassLoadHookArgTypes = { Class.class }; 116 117 /** 118 * Called when any top level class (not nested classes) in the impl jar is loaded. 119 * 120 * When HostStubGen inject a class-load hook, it's always a call to this method, with the 121 * actual method name as the second argument. 122 * 123 * This method discovers the hook method with reflections and call it. 124 * 125 * TODO: Add a unit test. 126 */ onClassLoaded(Class<?> loadedClass, String callbackMethod)127 public static void onClassLoaded(Class<?> loadedClass, String callbackMethod) { 128 logPrintStream.println("! Class loaded: " + loadedClass.getCanonicalName() 129 + " calling hook " + callbackMethod); 130 131 callStaticMethodByName( 132 callbackMethod, "class load hook", sClassLoadHookArgTypes, loadedClass); 133 } 134 callStaticMethodByName(String classAndMethodName, String description, Class<?>[] argTypes, Object... args)135 private static void callStaticMethodByName(String classAndMethodName, 136 String description, Class<?>[] argTypes, Object... args) { 137 // Forward the call to callbackMethod. 138 final int lastPeriod = classAndMethodName.lastIndexOf("."); 139 140 if ((lastPeriod) < 0 || (lastPeriod == classAndMethodName.length() - 1)) { 141 throw new HostTestException(String.format( 142 "Unable to find %s: malformed method name \"%s\"", 143 description, 144 classAndMethodName)); 145 } 146 147 final String className = classAndMethodName.substring(0, lastPeriod); 148 final String methodName = classAndMethodName.substring(lastPeriod + 1); 149 150 Class<?> clazz = null; 151 try { 152 clazz = Class.forName(className); 153 } catch (Exception e) { 154 throw new HostTestException(String.format( 155 "Unable to find %s: Class %s not found", 156 description, 157 className), e); 158 } 159 if (!Modifier.isPublic(clazz.getModifiers())) { 160 throw new HostTestException(String.format( 161 "Unable to find %s: Class %s must be public", 162 description, 163 className)); 164 } 165 166 Method method = null; 167 try { 168 method = clazz.getMethod(methodName, argTypes); 169 } catch (Exception e) { 170 throw new HostTestException(String.format( 171 "Unable to find %s: class %s doesn't have method %s" 172 + " Method must be public static, and arg types must be: " 173 + Arrays.toString(argTypes), 174 description, className, methodName), e); 175 } 176 if (!(Modifier.isPublic(method.getModifiers()) 177 && Modifier.isStatic(method.getModifiers()))) { 178 throw new HostTestException(String.format( 179 "Unable to find %s: Method %s in class %s must be public static", 180 description, methodName, className)); 181 } 182 try { 183 method.invoke(null, args); 184 } catch (Exception e) { 185 throw new HostTestException(String.format( 186 "Unable to invoke %s %s.%s", 187 description, className, methodName), e); 188 } 189 } 190 191 /** 192 * I can be used as 193 * {@code --default-class-load-hook 194 * com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded}. 195 * 196 * It logs every loaded class. 197 */ logClassLoaded(Class<?> clazz)198 public static void logClassLoaded(Class<?> clazz) { 199 if (SKIP_CLASS_LOG) { 200 return; 201 } 202 logPrintStream.println("# class loaded: " + clazz.getCanonicalName()); 203 } 204 } 205