• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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