1 /*
2  * Copyright (C) 2022 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 android.app.sdksandbox.testutils.testscenario;
18 
19 import android.app.sdksandbox.SandboxedSdk;
20 import android.app.sdksandbox.SandboxedSdkProvider;
21 import android.content.Context;
22 import android.os.Bundle;
23 import android.os.IBinder;
24 import android.os.RemoteException;
25 import android.util.Log;
26 import android.view.View;
27 
28 import androidx.annotation.Nullable;
29 
30 import java.io.PrintWriter;
31 import java.io.StringWriter;
32 import java.lang.annotation.Annotation;
33 import java.lang.reflect.InvocationTargetException;
34 import java.lang.reflect.Method;
35 import java.util.ArrayList;
36 import java.util.List;
37 
38 /**
39  * This class contains all the binding logic needed for a test suite within an SDK. To set up SDK
40  * tests, extend this class. To attach a custom view to your tests, override beforeEachTest() to
41  * return your view (for example a WebView).
42  */
43 public abstract class SdkSandboxTestScenarioRunner extends SandboxedSdkProvider {
44     private static final String TAG = SdkSandboxTestScenarioRunner.class.getName();
45 
46     private Object mTestInstance = this;
47     private @Nullable IBinder mBinder;
48 
49     /**
50      * This method can be used to provide a separate test class to execute tests against. In child
51      * classes, it should be called in the onLoadSdk method before calling super.
52      */
setTestInstance(Object testInstance)53     public void setTestInstance(Object testInstance) {
54         mTestInstance = testInstance;
55     }
56 
57     @Override
getView(Context windowContext, Bundle params, int width, int height)58     public View getView(Context windowContext, Bundle params, int width, int height) {
59         return new View(windowContext);
60     }
61 
62     @Override
onLoadSdk(Bundle params)63     public SandboxedSdk onLoadSdk(Bundle params) {
64         mBinder = params.getBinder(ISdkSandboxTestExecutor.TEST_AUTHOR_DEFINED_BINDER);
65 
66         ISdkSandboxTestExecutor.Stub testExecutor =
67                 new ISdkSandboxTestExecutor.Stub() {
68                     public void cleanOnTestFinish() {
69                         cleanUpOnTestFinish();
70                     }
71 
72                     public List<String> retrieveAnnotatedMethods(String annotationName) {
73                         List<String> annotatedMethods = new ArrayList<>();
74 
75                         try {
76                             Class<? extends Annotation> annotation =
77                                     Class.forName(annotationName).asSubclass(Annotation.class);
78                             Method[] testInstanceMethods =
79                                     mTestInstance.getClass().getDeclaredMethods();
80 
81                             for (final Method method : testInstanceMethods) {
82                                 if (method.isAnnotationPresent(annotation)) {
83                                     annotatedMethods.add(method.getName());
84                                 }
85                             }
86                         } catch (Exception e) {
87                             Log.e(TAG, "Failed to find methods with annotations");
88                         }
89 
90                         return annotatedMethods;
91                     }
92 
93                     public void invokeMethod(
94                             String methodName,
95                             Bundle methodParams,
96                             ISdkSandboxResultCallback resultCallback) {
97                         try {
98                             // We allow test authors to write a test without the bundle parameters
99                             // for convenience.
100                             // We will first look for the test name with a bundle parameter
101                             // if we don't find that, we will load the test without a parameter.
102                             boolean hasParams = true;
103                             Method methodFound =
104                                     findMethodOnTestInstance(
105                                             methodName, /*throwException*/ false, Bundle.class);
106                             if (methodFound == null) {
107                                 hasParams = false;
108                                 methodFound =
109                                         findMethodOnTestInstance(
110                                                 methodName, /*throwException*/ true);
111                             }
112 
113                             invokeMethodOnTestInstance(
114                                     methodFound, hasParams, methodParams, resultCallback);
115                         } catch (NoSuchMethodException error) {
116                             try {
117                                 resultCallback.onError(getStackTrace(error));
118                             } catch (RemoteException e) {
119                                 Log.e(TAG, "Failed to find method and report back");
120                             }
121                         }
122                     }
123                 };
124 
125         return new SandboxedSdk(testExecutor);
126     }
127 
128     @Nullable
getCustomInterface()129     protected IBinder getCustomInterface() {
130         return mBinder;
131     }
132 
findMethodOnTestInstance( String methodName, boolean throwException, Class<?>... parameterTypes)133     private Method findMethodOnTestInstance(
134             String methodName, boolean throwException, Class<?>... parameterTypes)
135             throws NoSuchMethodException {
136         try {
137             return mTestInstance.getClass().getMethod(methodName, parameterTypes);
138         } catch (NoSuchMethodException error) {
139             if (throwException) {
140                 throw error;
141             }
142             return null;
143         }
144     }
145 
invokeMethodOnTestInstance( final Method method, final boolean hasParams, final Bundle params, final ISdkSandboxResultCallback resultCallback)146     private void invokeMethodOnTestInstance(
147             final Method method,
148             final boolean hasParams,
149             final Bundle params,
150             final ISdkSandboxResultCallback resultCallback) {
151         try {
152             if (hasParams) {
153                 method.invoke(mTestInstance, params);
154             } else {
155                 method.invoke(mTestInstance);
156             }
157             resultCallback.onResult();
158         } catch (Exception error) {
159             String errorStackTrace = getStackTrace(error);
160 
161             try {
162                 resultCallback.onError(errorStackTrace);
163             } catch (Exception ex) {
164                 if (error.getCause() instanceof AssertionError) {
165                     Log.e(TAG, "Assertion failed on invoked method " + errorStackTrace);
166                 } else if (error.getCause() instanceof InvocationTargetException) {
167                     Log.e(TAG, "Invocation target failed " + errorStackTrace);
168                 } else if (error.getCause() instanceof NoSuchMethodException) {
169                     Log.e(TAG, "Test method not found " + errorStackTrace);
170                 } else {
171                     Log.e(TAG, "Test execution failed " + errorStackTrace);
172                 }
173             }
174         }
175     }
176 
getStackTrace(Exception error)177     private String getStackTrace(Exception error) {
178         StringWriter errorStackTrace = new StringWriter();
179         PrintWriter errorWriter = new PrintWriter(errorStackTrace);
180         error.getCause().printStackTrace(errorWriter);
181         return errorStackTrace.toString();
182     }
183 
cleanUpOnTestFinish()184     public abstract void cleanUpOnTestFinish();
185 
186 }
187