• 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 
17 package android.platform.test.ravenwood;
18 
19 import static android.os.Process.FIRST_APPLICATION_UID;
20 import static android.os.UserHandle.SYSTEM;
21 import static android.platform.test.ravenwood.RavenwoodSystemServer.ANDROID_PACKAGE_NAME;
22 
23 import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_EMPTY_RESOURCES_APK;
24 import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_INST_RESOURCE_APK;
25 import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_RESOURCE_APK;
26 import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_RUNTIME_PATH_JAVA_SYSPROP;
27 import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERSION_JAVA_SYSPROP;
28 import static com.android.ravenwood.common.RavenwoodCommonUtils.getRavenwoodRuntimePath;
29 import static com.android.ravenwood.common.RavenwoodCommonUtils.parseNullableInt;
30 import static com.android.ravenwood.common.RavenwoodCommonUtils.withDefault;
31 
32 import static org.junit.Assert.assertNotNull;
33 import static org.junit.Assert.assertThrows;
34 import static org.junit.Assert.assertTrue;
35 import static org.mockito.ArgumentMatchers.any;
36 import static org.mockito.Mockito.doAnswer;
37 import static org.mockito.Mockito.mock;
38 
39 import android.annotation.Nullable;
40 import android.app.ActivityManager;
41 import android.app.AppCompatCallbacks;
42 import android.app.Instrumentation;
43 import android.app.ResourcesManager;
44 import android.app.UiAutomation;
45 import android.content.Context;
46 import android.content.pm.ApplicationInfo;
47 import android.content.res.Resources;
48 import android.graphics.Typeface;
49 import android.icu.util.ULocale;
50 import android.os.Binder;
51 import android.os.Build;
52 import android.os.Build.VERSION_CODES;
53 import android.os.Bundle;
54 import android.os.HandlerThread;
55 import android.os.Looper;
56 import android.os.Message;
57 import android.os.Process_ravenwood;
58 import android.os.ServiceManager;
59 import android.os.ServiceManager.ServiceNotFoundException;
60 import android.os.SystemProperties;
61 import android.provider.DeviceConfig_host;
62 import android.system.ErrnoException;
63 import android.system.Os;
64 import android.util.Log;
65 import android.util.Log_ravenwood;
66 import android.view.DisplayAdjustments;
67 
68 import androidx.test.platform.app.InstrumentationRegistry;
69 
70 import com.android.hoststubgen.hosthelper.HostTestUtils;
71 import com.android.internal.annotations.GuardedBy;
72 import com.android.internal.os.RuntimeInit;
73 import com.android.ravenwood.RavenwoodRuntimeNative;
74 import com.android.ravenwood.RavenwoodRuntimeState;
75 import com.android.ravenwood.common.RavenwoodCommonUtils;
76 import com.android.ravenwood.common.SneakyThrow;
77 import com.android.server.LocalServices;
78 import com.android.server.compat.PlatformCompat;
79 
80 import org.junit.AssumptionViolatedException;
81 import org.junit.internal.management.ManagementFactory;
82 import org.junit.runner.Description;
83 
84 import java.io.File;
85 import java.io.IOException;
86 import java.io.PrintStream;
87 import java.util.Collections;
88 import java.util.Comparator;
89 import java.util.HashMap;
90 import java.util.Locale;
91 import java.util.Map;
92 import java.util.Objects;
93 import java.util.Random;
94 import java.util.Set;
95 import java.util.concurrent.Executors;
96 import java.util.concurrent.ScheduledExecutorService;
97 import java.util.concurrent.ScheduledFuture;
98 import java.util.concurrent.TimeUnit;
99 import java.util.concurrent.atomic.AtomicReference;
100 import java.util.function.Supplier;
101 import java.util.stream.Collectors;
102 
103 /**
104  * Responsible for initializing and the environment.
105  */
106 public class RavenwoodRuntimeEnvironmentController {
107     private static final String TAG = com.android.ravenwood.common.RavenwoodCommonUtils.TAG;
108 
RavenwoodRuntimeEnvironmentController()109     private RavenwoodRuntimeEnvironmentController() {
110     }
111 
112     private static final PrintStream sStdOut = System.out;
113     @SuppressWarnings("UnusedVariable")
114     private static final PrintStream sStdErr = System.err;
115 
116     private static final String MAIN_THREAD_NAME = "Ravenwood:Main";
117     private static final String TESTS_THREAD_NAME = "Ravenwood:Test";
118 
119     private static final String LIBRAVENWOOD_INITIALIZER_NAME = "ravenwood_initializer";
120     private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime";
121 
122     private static final String ANDROID_LOG_TAGS = "ANDROID_LOG_TAGS";
123     private static final String RAVENWOOD_ANDROID_LOG_TAGS = "RAVENWOOD_" + ANDROID_LOG_TAGS;
124 
125     static volatile Thread sTestThread;
126     static volatile Thread sMainThread;
127 
128     /**
129      * When enabled, attempt to dump all thread stacks just before we hit the
130      * overall Tradefed timeout, to aid in debugging deadlocks.
131      *
132      * Note, this timeout will _not_ stop the test, as there isn't really a clean way to do it.
133      * It'll merely print stacktraces.
134      */
135     private static final boolean ENABLE_TIMEOUT_STACKS =
136             !"0".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS"));
137 
138     private static final boolean TOLERATE_LOOPER_ASSERTS =
139             !"0".equals(System.getenv("RAVENWOOD_TOLERATE_LOOPER_ASSERTS"));
140 
141     static final int DEFAULT_TIMEOUT_SECONDS = 10;
142     private static final int TIMEOUT_MILLIS = getTimeoutSeconds() * 1000;
143 
getTimeoutSeconds()144     static int getTimeoutSeconds() {
145         var e = System.getenv("RAVENWOOD_TIMEOUT_SECONDS");
146         if (e == null || e.isEmpty()) {
147             return DEFAULT_TIMEOUT_SECONDS;
148         }
149         return Integer.parseInt(e);
150     }
151 
152 
153     private static final ScheduledExecutorService sTimeoutExecutor =
154             Executors.newScheduledThreadPool(1, (Runnable r) -> {
155                 Thread t = Executors.defaultThreadFactory().newThread(r);
156                 t.setName("Ravenwood:TimeoutMonitor");
157                 t.setDaemon(true);
158                 return t;
159             });
160 
161     private static volatile ScheduledFuture<?> sPendingTimeout;
162 
163     /**
164      * When enabled, attempt to detect uncaught exceptions from background threads.
165      */
166     private static final boolean ENABLE_UNCAUGHT_EXCEPTION_DETECTION =
167             !"0".equals(System.getenv("RAVENWOOD_ENABLE_UNCAUGHT_EXCEPTION_DETECTION"));
168 
169     private static final boolean DIE_ON_UNCAUGHT_EXCEPTION = true;
170 
171     /**
172      * When set, an unhandled exception was discovered (typically on a background thread), and we
173      * capture it here to ensure it's reported as a test failure.
174      */
175     private static final AtomicReference<Throwable> sPendingUncaughtException =
176             new AtomicReference<>();
177 
178     // TODO: expose packCallingIdentity function in libbinder and use it directly
179     // See: packCallingIdentity in frameworks/native/libs/binder/IPCThreadState.cpp
packBinderIdentityToken( boolean hasExplicitIdentity, int callingUid, int callingPid)180     private static long packBinderIdentityToken(
181             boolean hasExplicitIdentity, int callingUid, int callingPid) {
182         long res = ((long) callingUid << 32) | callingPid;
183         if (hasExplicitIdentity) {
184             res |= (0x1 << 30);
185         } else {
186             res &= ~(0x1 << 30);
187         }
188         return res;
189     }
190 
191     /** Map from path -> resources. */
192     private static final HashMap<File, Resources> sCachedResources = new HashMap<>();
193     private static Set<String> sAdoptedPermissions = Collections.emptySet();
194 
195     private static final Object sInitializationLock = new Object();
196 
197     @GuardedBy("sInitializationLock")
198     private static boolean sInitialized = false;
199 
200     @GuardedBy("sInitializationLock")
201     private static Throwable sExceptionFromGlobalInit;
202 
203     private static final int DEFAULT_TARGET_SDK_LEVEL = VERSION_CODES.CUR_DEVELOPMENT;
204     private static final String DEFAULT_PACKAGE_NAME = "com.android.ravenwoodtests.defaultname";
205 
206     private static final int sMyPid = new Random().nextInt(100, 32768);
207     private static int sTargetSdkLevel;
208     private static String sTestPackageName;
209     private static String sTargetPackageName;
210     private static Instrumentation sInstrumentation;
211     private static final long sCallingIdentity =
212             packBinderIdentityToken(false, FIRST_APPLICATION_UID, sMyPid);
213 
214     /**
215      * Initialize the global environment.
216      */
globalInitOnce()217     public static void globalInitOnce() {
218         sTestThread = Thread.currentThread();
219         Thread.currentThread().setName(TESTS_THREAD_NAME);
220         synchronized (sInitializationLock) {
221             if (!sInitialized) {
222                 // globalInitOnce() is called from class initializer, which cause
223                 // this method to be called recursively,
224                 sInitialized = true;
225 
226                 // This is the first call.
227                 final long start = System.currentTimeMillis();
228                 try {
229                     globalInitInner();
230                 } catch (Throwable th) {
231                     Log.e(TAG, "globalInit() failed", th);
232 
233                     sExceptionFromGlobalInit = th;
234                     SneakyThrow.sneakyThrow(th);
235                 }
236                 final long end = System.currentTimeMillis();
237                 // TODO Show user/system time too
238                 Log.e(TAG, "globalInit() took " + (end - start) + "ms");
239             } else {
240                 // Subsequent calls. If the first call threw, just throw the same error, to prevent
241                 // the test from running.
242                 if (sExceptionFromGlobalInit != null) {
243                     Log.e(TAG, "globalInit() failed re-throwing the same exception",
244                             sExceptionFromGlobalInit);
245 
246                     SneakyThrow.sneakyThrow(sExceptionFromGlobalInit);
247                 }
248             }
249         }
250     }
251 
globalInitInner()252     private static void globalInitInner() throws IOException {
253         // We haven't initialized liblog yet, so directly write to System.out here.
254         RavenwoodCommonUtils.log(TAG, "globalInitInner()");
255 
256         if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
257             Thread.setDefaultUncaughtExceptionHandler(
258                     RavenwoodRuntimeEnvironmentController::reportUncaughtExceptions);
259         }
260 
261         // Some process-wide initialization:
262         // - maybe redirect stdout/stderr
263         // - override native system property functions
264         var lib = RavenwoodCommonUtils.getJniLibraryPath(LIBRAVENWOOD_INITIALIZER_NAME);
265         System.load(lib);
266         RavenwoodRuntimeNative.reloadNativeLibrary(lib);
267 
268         // Redirect stdout/stdin to the Log API.
269         RuntimeInit.redirectLogStreams();
270 
271         dumpCommandLineArgs();
272         dumpEnvironment();
273         dumpJavaProperties();
274         dumpOtherInfo();
275 
276         System.setProperty(RAVENWOOD_VERSION_JAVA_SYSPROP, "1");
277         var runtimePath = getRavenwoodRuntimePath();
278         System.setProperty(RAVENWOOD_RUNTIME_PATH_JAVA_SYSPROP, runtimePath);
279 
280         Log.i(TAG, "PWD=" + System.getProperty("user.dir"));
281         Log.i(TAG, "RuntimePath=" + runtimePath);
282 
283         // Make sure libravenwood_runtime is loaded.
284         System.load(RavenwoodCommonUtils.getJniLibraryPath(RAVENWOOD_NATIVE_RUNTIME_NAME));
285 
286         Log_ravenwood.setLogLevels(getLogTags());
287         Log_ravenwood.onRavenwoodRuntimeNativeReady();
288 
289         // Do the basic set up for the android sysprops.
290         RavenwoodSystemProperties.initialize();
291 
292         // Set ICU data file
293         String icuData = RavenwoodCommonUtils.getRavenwoodRuntimePath()
294                 + "ravenwood-data/"
295                 + RavenwoodRuntimeNative.getIcuDataName()
296                 + ".dat";
297         RavenwoodRuntimeNative.setSystemProperty("ro.icu.data.path", icuData);
298 
299         // Enable all log levels for native logging, until we'll have a way to change the native
300         // side log level at runtime.
301         // Do this after loading RAVENWOOD_NATIVE_RUNTIME_NAME (which backs Os.setenv()),
302         // before loadFrameworkNativeCode() (which uses $ANDROID_LOG_TAGS).
303         // This would also prevent libbase from crashing the process (b/381112373) because
304         // the string format it accepts is very limited.
305         try {
306             Os.setenv("ANDROID_LOG_TAGS", "*:v", true);
307         } catch (ErrnoException e) {
308             throw new RuntimeException(e);
309         }
310 
311         // Make sure libandroid_runtime is loaded.
312         RavenwoodNativeLoader.loadFrameworkNativeCode();
313 
314         // Start method logging.
315         RavenwoodMethodCallLogger.enable(sStdOut);
316 
317         // Touch some references early to ensure they're <clinit>'ed
318         Objects.requireNonNull(Build.TYPE);
319         Objects.requireNonNull(Build.VERSION.SDK);
320 
321         // Fonts can only be initialized once
322         Typeface.init();
323         Typeface.loadPreinstalledSystemFontMap();
324         Typeface.loadNativeSystemFonts();
325 
326         // This will let AndroidJUnit4 use the original runner.
327         System.setProperty("android.junit.runner",
328                 "androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner");
329 
330         loadRavenwoodProperties();
331 
332         assertMockitoVersion();
333 
334         Log.i(TAG, "TargetPackageName=" + sTargetPackageName);
335         Log.i(TAG, "TestPackageName=" + sTestPackageName);
336         Log.i(TAG, "TargetSdkLevel=" + sTargetSdkLevel);
337 
338         RavenwoodRuntimeState.sUid = FIRST_APPLICATION_UID;
339         RavenwoodRuntimeState.sPid = sMyPid;
340         RavenwoodRuntimeState.sTargetSdkLevel = sTargetSdkLevel;
341 
342         ServiceManager.init$ravenwood();
343         LocalServices.removeAllServicesForTest();
344 
345         ActivityManager.init$ravenwood(SYSTEM.getIdentifier());
346 
347         final var main = new HandlerThread(MAIN_THREAD_NAME);
348         sMainThread = main;
349         main.start();
350         Looper.setMainLooperForTest(main.getLooper());
351 
352         final boolean isSelfInstrumenting =
353                 Objects.equals(sTestPackageName, sTargetPackageName);
354 
355         // This will load the resources from the apk set to `resource_apk` in the build file.
356         // This is supposed to be the "target app"'s resources.
357         final Supplier<Resources> targetResourcesLoader = () -> {
358             var file = new File(RAVENWOOD_RESOURCE_APK);
359             return loadResources(file.exists() ? file : null);
360         };
361 
362         // Set up test context's (== instrumentation context's) resources.
363         // If the target package name == test package name, then we use the main resources.
364         final Supplier<Resources> instResourcesLoader;
365         if (isSelfInstrumenting) {
366             instResourcesLoader = targetResourcesLoader;
367         } else {
368             instResourcesLoader = () -> {
369                 var file = new File(RAVENWOOD_INST_RESOURCE_APK);
370                 return loadResources(file.exists() ? file : null);
371             };
372         }
373 
374         var instContext = new RavenwoodContext(
375                 sTestPackageName, main, instResourcesLoader);
376         var targetContext = new RavenwoodContext(
377                 sTargetPackageName, main, targetResourcesLoader);
378 
379         // Set up app context.
380         var appContext = new RavenwoodContext(sTargetPackageName, main, targetResourcesLoader);
381         appContext.setApplicationContext(appContext);
382         if (isSelfInstrumenting) {
383             instContext.setApplicationContext(appContext);
384             targetContext.setApplicationContext(appContext);
385         } else {
386             // When instrumenting into another APK, the test context doesn't have an app context.
387             targetContext.setApplicationContext(appContext);
388         }
389 
390         final Supplier<Resources> systemResourcesLoader = () -> loadResources(null);
391 
392         var systemServerContext =
393                 new RavenwoodContext(ANDROID_PACKAGE_NAME, main, systemResourcesLoader);
394 
395         var instArgs = Bundle.EMPTY;
396         RavenwoodUtils.runOnMainThreadSync(() -> {
397             try {
398                 // TODO We should get the instrumentation class name from the build file or
399                 // somewhere.
400                 var InstClass = Class.forName("android.app.Instrumentation");
401                 sInstrumentation = (Instrumentation) InstClass.getConstructor().newInstance();
402                 sInstrumentation.basicInit(instContext, targetContext, null);
403                 sInstrumentation.onCreate(instArgs);
404             } catch (Exception e) {
405                 SneakyThrow.sneakyThrow(e);
406             }
407         });
408         InstrumentationRegistry.registerInstance(sInstrumentation, instArgs);
409 
410         RavenwoodSystemServer.init(systemServerContext);
411 
412         initializeCompatIds();
413     }
414 
415     /**
416      * Get log tags from environmental variable.
417      */
418     @Nullable
getLogTags()419     private static String getLogTags() {
420         var logTags = System.getenv(RAVENWOOD_ANDROID_LOG_TAGS);
421         if (logTags == null) {
422             logTags = System.getenv(ANDROID_LOG_TAGS);
423         }
424         return logTags;
425     }
426 
loadRavenwoodProperties()427     private static void loadRavenwoodProperties() {
428         var props = RavenwoodSystemProperties.readProperties("ravenwood.properties");
429 
430         sTargetSdkLevel = withDefault(
431                 parseNullableInt(props.get("targetSdkVersionInt")), DEFAULT_TARGET_SDK_LEVEL);
432         sTargetPackageName = withDefault(props.get("packageName"), DEFAULT_PACKAGE_NAME);
433         sTestPackageName = withDefault(props.get("instPackageName"), sTargetPackageName);
434 
435         // TODO(b/377765941) Read them from the manifest too?
436     }
437 
438     /**
439      * Partially reset and initialize before each test class invocation
440      */
initForRunner()441     public static void initForRunner() {
442         var targetContext = sInstrumentation.getTargetContext();
443         var instContext = sInstrumentation.getContext();
444         // We need to recreate the mock UiAutomation for each test class, because sometimes tests
445         // will call Mockito.framework().clearInlineMocks() after execution.
446         sInstrumentation.basicInit(instContext, targetContext, createMockUiAutomation());
447 
448         // Reset some global state
449         Process_ravenwood.reset();
450         DeviceConfig_host.reset();
451         Binder.restoreCallingIdentity(sCallingIdentity);
452 
453         SystemProperties.clearChangeCallbacksForTest();
454 
455         maybeThrowPendingUncaughtException();
456     }
457 
458     /**
459      * Called when a test method is about to be started.
460      */
enterTestMethod(Description description)461     public static void enterTestMethod(Description description) {
462         // TODO(b/375272444): this is a hacky workaround to ensure binder identity
463         Binder.restoreCallingIdentity(sCallingIdentity);
464 
465         scheduleTimeout();
466     }
467 
468     /**
469      * Called when a test method finished.
470      */
exitTestMethod(Description description)471     public static void exitTestMethod(Description description) {
472         cancelTimeout();
473         maybeThrowPendingUncaughtException();
474     }
475 
scheduleTimeout()476     private static void scheduleTimeout() {
477         if (!ENABLE_TIMEOUT_STACKS) {
478             return;
479         }
480         cancelTimeout();
481 
482         sPendingTimeout = sTimeoutExecutor.schedule(
483                 RavenwoodRuntimeEnvironmentController::onTestTimedOut,
484                 TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
485     }
486 
cancelTimeout()487     private static void cancelTimeout() {
488         if (!ENABLE_TIMEOUT_STACKS) {
489             return;
490         }
491         var pt = sPendingTimeout;
492         if (pt != null) {
493             pt.cancel(false);
494         }
495     }
496 
initializeCompatIds()497     private static void initializeCompatIds() {
498         // Set up compat-IDs for the app side.
499         // TODO: Inside the system server, all the compat-IDs should be enabled,
500         // Due to the `AppCompatCallbacks.install(new long[0], new long[0])` call in
501         // SystemServer.
502 
503         // Compat framework only uses the package name and the target SDK level.
504         ApplicationInfo appInfo = new ApplicationInfo();
505         appInfo.packageName = sTargetPackageName;
506         appInfo.targetSdkVersion = sTargetSdkLevel;
507 
508         PlatformCompat platformCompat = null;
509         try {
510             platformCompat = (PlatformCompat) ServiceManager.getServiceOrThrow(
511                     Context.PLATFORM_COMPAT_SERVICE);
512         } catch (ServiceNotFoundException e) {
513             throw new RuntimeException(e);
514         }
515 
516         var disabledChanges = platformCompat.getDisabledChanges(appInfo);
517         var loggableChanges = platformCompat.getLoggableChanges(appInfo);
518 
519         AppCompatCallbacks.install(disabledChanges, loggableChanges);
520     }
521 
522     /**
523      * Load {@link Resources} from an APK, with cache.
524      */
loadResources(@ullable File apkPath)525     private static Resources loadResources(@Nullable File apkPath) {
526         var cached = sCachedResources.get(apkPath);
527         if (cached != null) {
528             return cached;
529         }
530 
531         var fileToLoad = apkPath != null ? apkPath : new File(RAVENWOOD_EMPTY_RESOURCES_APK);
532 
533         assertTrue("File " + fileToLoad + " doesn't exist.", fileToLoad.isFile());
534 
535         final String path = fileToLoad.getAbsolutePath();
536         final var emptyPaths = new String[0];
537 
538         ResourcesManager.getInstance().initializeApplicationPaths(path, emptyPaths);
539 
540         final var ret = ResourcesManager.getInstance().getResources(null, path,
541                 emptyPaths, emptyPaths, emptyPaths,
542                 emptyPaths, null, null,
543                 new DisplayAdjustments().getCompatibilityInfo(),
544                 RavenwoodRuntimeEnvironmentController.class.getClassLoader(), null);
545 
546         assertNotNull(ret);
547 
548         sCachedResources.put(apkPath, ret);
549         return ret;
550     }
551 
552     /**
553      * Return if an exception is benign and okay to continue running the main looper even
554      * if we detect it.
555      */
isThrowableBenign(Throwable th)556     private static boolean isThrowableBenign(Throwable th) {
557         return th instanceof AssertionError || th instanceof AssumptionViolatedException;
558     }
559 
dispatchMessage(Message msg)560     static void dispatchMessage(Message msg) {
561         try {
562             msg.getTarget().dispatchMessage(msg);
563         } catch (Throwable th) {
564             var desc = String.format("Detected %s on looper thread %s", th.getClass().getName(),
565                     Thread.currentThread());
566             sStdErr.println(desc);
567             if (TOLERATE_LOOPER_ASSERTS && isThrowableBenign(th)) {
568                 sStdErr.printf("*** Continuing the test because it's %s ***\n",
569                         th.getClass().getSimpleName());
570                 var e = new Exception(desc, th);
571                 sPendingUncaughtException.compareAndSet(null, e);
572                 return;
573             }
574             throw th;
575         }
576     }
577 
578     /**
579      * A callback when a test class finishes its execution, mostly only for debugging.
580      */
exitTestClass()581     public static void exitTestClass() {
582         maybeThrowPendingUncaughtException();
583     }
584 
logTestRunner(String label, Description description)585     public static void logTestRunner(String label, Description description) {
586         // This message string carefully matches the exact format emitted by on-device tests, to
587         // aid developers in debugging raw text logs
588         Log.e("TestRunner", label + ": " + description.getMethodName()
589                 + "(" + description.getTestClass().getName() + ")");
590     }
591 
maybeThrowPendingUncaughtException()592     private static void maybeThrowPendingUncaughtException() {
593         final Throwable pending = sPendingUncaughtException.getAndSet(null);
594         if (pending != null) {
595             throw new IllegalStateException("Found an uncaught exception", pending);
596         }
597     }
598 
599     /**
600      * Prints the stack trace from all threads.
601      */
onTestTimedOut()602     private static void onTestTimedOut() {
603         sStdErr.println("********* SLOW TEST DETECTED ********");
604         dumpStacks(null, null);
605     }
606 
607     private static final Object sDumpStackLock = new Object();
608 
609     /**
610      * Prints the stack trace from all threads.
611      */
dumpStacks( @ullable Thread exceptionThread, @Nullable Throwable throwable)612     private static void dumpStacks(
613             @Nullable Thread exceptionThread, @Nullable Throwable throwable) {
614         cancelTimeout();
615         synchronized (sDumpStackLock) {
616             final PrintStream out = sStdErr;
617             out.println("-----BEGIN ALL THREAD STACKS-----");
618 
619             var stacks = Thread.getAllStackTraces();
620             var threads = stacks.keySet().stream().sorted(
621                     Comparator.comparingLong(Thread::getId)).collect(Collectors.toList());
622 
623             // Put the test and the main thread at the top.
624             var testThread = sTestThread;
625             var mainThread = sMainThread;
626             if (mainThread != null) {
627                 threads.remove(mainThread);
628                 threads.add(0, mainThread);
629             }
630             if (testThread != null) {
631                 threads.remove(testThread);
632                 threads.add(0, testThread);
633             }
634             // Put the exception thread at the top.
635             // Also inject the stacktrace from the exception.
636             if (exceptionThread != null) {
637                 threads.remove(exceptionThread);
638                 threads.add(0, exceptionThread);
639                 stacks.put(exceptionThread, throwable.getStackTrace());
640             }
641             for (var th : threads) {
642                 out.println();
643 
644                 out.print("Thread");
645                 if (th == exceptionThread) {
646                     out.print(" [** EXCEPTION THREAD **]");
647                 }
648                 out.print(": " + th.getName() + " / " + th);
649                 out.println();
650 
651                 for (StackTraceElement e :  stacks.get(th)) {
652                     out.println("\tat " + e);
653                 }
654             }
655             out.println("-----END ALL THREAD STACKS-----");
656         }
657     }
658 
659     private static final String MOCKITO_ERROR = "FATAL: Unsupported Mockito detected!"
660             + " Your test or its dependencies use one of the \"mockito-target-*\""
661             + " modules as static library, which is unusable on host side."
662             + " Please switch over to use \"mockito-ravenwood-prebuilt\" as shared library, or"
663             + " as a last resort, set `ravenizer: { strip_mockito: true }` in your test module.";
664 
665     /**
666      * Assert the Mockito version at runtime to ensure no incorrect Mockito classes are loaded.
667      */
assertMockitoVersion()668     private static void assertMockitoVersion() {
669         // DexMaker should not exist
670         assertThrows(
671                 MOCKITO_ERROR,
672                 ClassNotFoundException.class,
673                 () -> Class.forName("com.android.dx.DexMaker"));
674         // Mockito 2 should not exist
675         assertThrows(
676                 MOCKITO_ERROR,
677                 ClassNotFoundException.class,
678                 () -> Class.forName("org.mockito.Matchers"));
679     }
680 
makeDefaultThrowMock(Class<T> clazz)681     static <T> T makeDefaultThrowMock(Class<T> clazz) {
682         return mock(clazz, inv -> {
683             HostTestUtils.onThrowMethodCalled();
684             return null;
685         });
686     }
687 
688     // TODO: use the real UiAutomation class instead of a mock
createMockUiAutomation()689     private static UiAutomation createMockUiAutomation() {
690         sAdoptedPermissions = Collections.emptySet();
691         var mock = makeDefaultThrowMock(UiAutomation.class);
692         doAnswer(inv -> {
693             sAdoptedPermissions = UiAutomation.ALL_PERMISSIONS;
694             return null;
695         }).when(mock).adoptShellPermissionIdentity();
696         doAnswer(inv -> {
697             if (inv.getArgument(0) == null) {
698                 sAdoptedPermissions = UiAutomation.ALL_PERMISSIONS;
699             } else {
700                 sAdoptedPermissions = (Set) Set.of(inv.getArguments());
701             }
702             return null;
703         }).when(mock).adoptShellPermissionIdentity(any());
704         doAnswer(inv -> {
705             sAdoptedPermissions = Collections.emptySet();
706             return null;
707         }).when(mock).dropShellPermissionIdentity();
708         doAnswer(inv -> sAdoptedPermissions).when(mock).getAdoptedShellPermissions();
709         return mock;
710     }
711 
dumpCommandLineArgs()712     private static void dumpCommandLineArgs() {
713         Log.v(TAG, "JVM arguments:");
714 
715         // Note, we use the wrapper in JUnit4, not the actual class (
716         // java.lang.management.ManagementFactory), because we can't see the later at the build
717         // because this source file is compiled for the device target, where ManagementFactory
718         // doesn't exist.
719         var args = ManagementFactory.getRuntimeMXBean().getInputArguments();
720 
721         for (var arg : args) {
722             Log.v(TAG, "  " + arg);
723         }
724     }
725 
reportUncaughtExceptions(Thread th, Throwable e)726     private static void reportUncaughtExceptions(Thread th, Throwable e) {
727         sStdErr.printf("Uncaught exception detected: %s: %s\n",
728                 th, RavenwoodCommonUtils.getStackTraceString(e));
729 
730         doBugreport(th, e, DIE_ON_UNCAUGHT_EXCEPTION);
731     }
732 
doBugreport( @ullable Thread exceptionThread, @Nullable Throwable throwable, boolean killSelf)733     private static void doBugreport(
734             @Nullable Thread exceptionThread, @Nullable Throwable throwable,
735             boolean killSelf) {
736         // TODO: Print more information
737         dumpStacks(exceptionThread, throwable);
738         if (killSelf) {
739             System.exit(13);
740         }
741     }
742 
dumpJavaProperties()743     private static void dumpJavaProperties() {
744         Log.v(TAG, "JVM properties:");
745         dumpMap(System.getProperties());
746     }
747 
dumpEnvironment()748     private static void dumpEnvironment() {
749         Log.v(TAG, "Environment:");
750         dumpMap(System.getenv());
751     }
752 
dumpMap(Map<?, ?> map)753     private static void dumpMap(Map<?, ?> map) {
754         for (var key : map.keySet().stream().sorted().toList()) {
755             Log.v(TAG, "  " + key + "=" + map.get(key));
756         }
757     }
dumpOtherInfo()758     private static void dumpOtherInfo() {
759         Log.v(TAG, "Other key information:");
760         var jloc = Locale.getDefault();
761         Log.v(TAG, "  java.util.Locale=" + jloc + " / " + jloc.toLanguageTag());
762         var uloc = ULocale.getDefault();
763         Log.v(TAG, "  android.icu.util.ULocale=" + uloc + " / " + uloc.toLanguageTag());
764 
765         var jtz = java.util.TimeZone.getDefault();
766         Log.v(TAG, "  java.util.TimeZone=" + jtz.getDisplayName() + " / " + jtz);
767 
768         var itz = android.icu.util.TimeZone.getDefault();
769         Log.v(TAG, "  android.icu.util.TimeZone="  + itz.getDisplayName() + " / " + itz);
770     }
771 }
772