diff options
author | 2024-09-26 22:59:40 +0000 | |
---|---|---|
committer | 2024-09-26 22:59:40 +0000 | |
commit | 983461633b96db0bc58205a657edeffad3ce4080 (patch) | |
tree | 8df76979756f92a675ea35ac852917a2369d152f /ravenwood/junit-impl-src | |
parent | 6f7665370b368b3f4164cdce501ae224b3351c9b (diff) |
Cherry-pick Ravenwood "core" code
- Copied f/b/r and f/b/t/h
- Ported files under f/b/core, only what needed to for run-ravenwood-tests.sh to pass
- Local changes because of missing resoucres support
- Added @DisabledOnRavenwood(reason="AOSP is missing resources support") to tests under f/b/r that depends on resources
bivalentinst and servicestest
- Added try-catch around ResourcesManager.setInstance()
Flag: EXEMPT host test change only
Bug: 292141694
Test: $ANDROID_BUILD_TOP/frameworks/base/ravenwood/scripts/run-ravenwood-tests.sh
Merged-in: I8a9b8374be3ae052ba4f152eb43af20d0871597f
Change-Id: Iefd574dbded8c4ab2e244c4918c26641364a3432
Diffstat (limited to 'ravenwood/junit-impl-src')
10 files changed, 1463 insertions, 330 deletions
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java new file mode 100644 index 000000000000..478bead1354f --- /dev/null +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.platform.test.ravenwood; + +import static org.junit.Assert.fail; + +import android.os.Bundle; +import android.platform.test.ravenwood.RavenwoodAwareTestRunner.Order; +import android.platform.test.ravenwood.RavenwoodAwareTestRunner.Scope; +import android.platform.test.ravenwood.RavenwoodTestStats.Result; +import android.util.Log; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.runner.Description; +import org.junit.runners.model.TestClass; + +/** + * Provide hook points created by {@link RavenwoodAwareTestRunner}. + * + * States are associated with each {@link RavenwoodAwareTestRunner} are stored in + * {@link RavenwoodRunnerState}, rather than as members of {@link RavenwoodAwareTestRunner}. + * See its javadoc for the reasons. + * + * All methods in this class must be called from the test main thread. + */ +public class RavenwoodAwareTestRunnerHook { + private static final String TAG = RavenwoodAwareTestRunner.TAG; + + private RavenwoodAwareTestRunnerHook() { + } + + /** + * Called before any code starts. Internally it will only initialize the environment once. + */ + public static void performGlobalInitialization() { + RavenwoodRuntimeEnvironmentController.globalInitOnce(); + } + + /** + * Called when a runner starts, before the inner runner gets a chance to run. + */ + public static void onRunnerInitializing(RavenwoodAwareTestRunner runner, TestClass testClass) { + Log.i(TAG, "onRunnerInitializing: testClass=" + testClass.getJavaClass() + + " runner=" + runner); + + // This is needed to make AndroidJUnit4ClassRunner happy. + InstrumentationRegistry.registerInstance(null, Bundle.EMPTY); + } + + /** + * Called when a whole test class is skipped. + */ + public static void onClassSkipped(Description description) { + Log.i(TAG, "onClassSkipped: description=" + description); + RavenwoodTestStats.getInstance().onClassSkipped(description); + } + + /** + * Called before the inner runner starts. + */ + public static void onBeforeInnerRunnerStart( + RavenwoodAwareTestRunner runner, Description description) throws Throwable { + Log.v(TAG, "onBeforeInnerRunnerStart: description=" + description); + + // Prepare the environment before the inner runner starts. + runner.mState.enterTestClass(description); + } + + /** + * Called after the inner runner finished. + */ + public static void onAfterInnerRunnerFinished( + RavenwoodAwareTestRunner runner, Description description) throws Throwable { + Log.v(TAG, "onAfterInnerRunnerFinished: description=" + description); + + RavenwoodTestStats.getInstance().onClassFinished(description); + runner.mState.exitTestClass(); + } + + /** + * Called before a test / class. + * + * Return false if it should be skipped. + */ + public static boolean onBefore(RavenwoodAwareTestRunner runner, Description description, + Scope scope, Order order) throws Throwable { + Log.v(TAG, "onBefore: description=" + description + ", " + scope + ", " + order); + + if (scope == Scope.Instance && order == Order.Outer) { + // Start of a test method. + runner.mState.enterTestMethod(description); + } + + final var classDescription = runner.mState.getClassDescription(); + + // Class-level annotations are checked by the runner already, so we only check + // method-level annotations here. + if (scope == Scope.Instance && order == Order.Outer) { + if (!RavenwoodEnablementChecker.shouldEnableOnRavenwood( + description, true)) { + RavenwoodTestStats.getInstance().onTestFinished( + classDescription, description, Result.Skipped); + return false; + } + } + return true; + } + + /** + * Called after a test / class. + * + * Return false if the exception should be ignored. + */ + public static boolean onAfter(RavenwoodAwareTestRunner runner, Description description, + Scope scope, Order order, Throwable th) { + Log.v(TAG, "onAfter: description=" + description + ", " + scope + ", " + order + ", " + th); + + final var classDescription = runner.mState.getClassDescription(); + + if (scope == Scope.Instance && order == Order.Outer) { + // End of a test method. + runner.mState.exitTestMethod(); + RavenwoodTestStats.getInstance().onTestFinished(classDescription, description, + th == null ? Result.Passed : Result.Failed); + } + + // If RUN_DISABLED_TESTS is set, and the method did _not_ throw, make it an error. + if (RavenwoodRule.private$ravenwood().isRunningDisabledTests() + && scope == Scope.Instance && order == Order.Outer) { + + boolean isTestEnabled = RavenwoodEnablementChecker.shouldEnableOnRavenwood( + description, false); + if (th == null) { + // Test passed. Is the test method supposed to be enabled? + if (isTestEnabled) { + // Enabled and didn't throw, okay. + return true; + } else { + // Disabled and didn't throw. We should report it. + fail("Test wasn't included under Ravenwood, but it actually " + + "passed under Ravenwood; consider updating annotations"); + return true; // unreachable. + } + } else { + // Test failed. + if (isTestEnabled) { + // Enabled but failed. We should throw the exception. + return true; + } else { + // Disabled and failed. Expected. Don't throw. + return false; + } + } + } + return true; + } + + /** + * Called by {@link RavenwoodAwareTestRunner} to see if it should run a test class or not. + */ + public static boolean shouldRunClassOnRavenwood(Class<?> clazz) { + return RavenwoodEnablementChecker.shouldRunClassOnRavenwood(clazz, true); + } + + /** + * Called by RavenwoodRule. + */ + public static void onRavenwoodRuleEnter(RavenwoodAwareTestRunner runner, + Description description, RavenwoodRule rule) throws Throwable { + Log.v(TAG, "onRavenwoodRuleEnter: description=" + description); + + runner.mState.enterRavenwoodRule(rule); + } + + + /** + * Called by RavenwoodRule. + */ + public static void onRavenwoodRuleExit(RavenwoodAwareTestRunner runner, + Description description, RavenwoodRule rule) throws Throwable { + Log.v(TAG, "onRavenwoodRuleExit: description=" + description); + + runner.mState.exitRavenwoodRule(rule); + } +} diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodConfigState.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodConfigState.java new file mode 100644 index 000000000000..3535cb2b1b79 --- /dev/null +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodConfigState.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.platform.test.ravenwood; + +import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_EMPTY_RESOURCES_APK; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.annotation.Nullable; +import android.app.ResourcesManager; +import android.content.res.Resources; +import android.view.DisplayAdjustments; + +import java.io.File; +import java.util.HashMap; + +/** + * Used to store various states associated with {@link RavenwoodConfig} that's inly needed + * in junit-impl. + * + * We don't want to put it in junit-src to avoid having to recompile all the downstream + * dependencies after changing this class. + * + * All members must be called from the runner's main thread. + */ +public class RavenwoodConfigState { + private static final String TAG = "RavenwoodConfigState"; + + private final RavenwoodConfig mConfig; + + public RavenwoodConfigState(RavenwoodConfig config) { + mConfig = config; + } + + /** Map from path -> resources. */ + private final HashMap<File, Resources> mCachedResources = new HashMap<>(); + + /** + * Load {@link Resources} from an APK, with cache. + */ + public Resources loadResources(@Nullable File apkPath) { + var cached = mCachedResources.get(apkPath); + if (cached != null) { + return cached; + } + + var fileToLoad = apkPath != null ? apkPath : new File(RAVENWOOD_EMPTY_RESOURCES_APK); + + assertTrue("File " + fileToLoad + " doesn't exist.", fileToLoad.isFile()); + + final String path = fileToLoad.getAbsolutePath(); + final var emptyPaths = new String[0]; + + ResourcesManager.getInstance().initializeApplicationPaths(path, emptyPaths); + + final var ret = ResourcesManager.getInstance().getResources(null, path, + emptyPaths, emptyPaths, emptyPaths, + emptyPaths, null, null, + new DisplayAdjustments().getCompatibilityInfo(), + RavenwoodRuntimeEnvironmentController.class.getClassLoader(), null); + + assertNotNull(ret); + + mCachedResources.put(apkPath, ret); + return ret; + } +} diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java index 1dd5e1ddd630..239c8061b757 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java @@ -16,8 +16,13 @@ package android.platform.test.ravenwood; +import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_RESOURCE_APK; + import android.content.ClipboardManager; import android.content.Context; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.content.res.Resources.Theme; import android.hardware.ISerialManager; import android.hardware.SerialManager; import android.os.Handler; @@ -31,11 +36,18 @@ import android.ravenwood.example.RedManager; import android.util.ArrayMap; import android.util.Singleton; +import com.android.internal.annotations.GuardedBy; + +import java.io.File; +import java.io.IOException; import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Supplier; public class RavenwoodContext extends RavenwoodBaseContext { + private static final String TAG = "Ravenwood"; + + private final Object mLock = new Object(); private final String mPackageName; private final HandlerThread mMainThread; @@ -44,15 +56,31 @@ public class RavenwoodContext extends RavenwoodBaseContext { private final ArrayMap<Class<?>, String> mClassToName = new ArrayMap<>(); private final ArrayMap<String, Supplier<?>> mNameToFactory = new ArrayMap<>(); + private final File mFilesDir; + private final File mCacheDir; + private final Supplier<Resources> mResourcesSupplier; + + private RavenwoodContext mAppContext; + + @GuardedBy("mLock") + private Resources mResources; + + @GuardedBy("mLock") + private Resources.Theme mTheme; + private void registerService(Class<?> serviceClass, String serviceName, Supplier<?> serviceSupplier) { mClassToName.put(serviceClass, serviceName); mNameToFactory.put(serviceName, serviceSupplier); } - public RavenwoodContext(String packageName, HandlerThread mainThread) { + public RavenwoodContext(String packageName, HandlerThread mainThread, + Supplier<Resources> resourcesSupplier) throws IOException { mPackageName = packageName; mMainThread = mainThread; + mResourcesSupplier = resourcesSupplier; + mFilesDir = createTempDir(packageName + "_files-dir"); + mCacheDir = createTempDir(packageName + "_cache-dir"); // Services provided by a typical shipping device registerService(ClipboardManager.class, @@ -85,6 +113,11 @@ public class RavenwoodContext extends RavenwoodBaseContext { } } + void cleanUp() { + deleteDir(mFilesDir); + deleteDir(mCacheDir); + } + @Override public String getSystemServiceName(Class<?> serviceClass) { // TODO: pivot to using SystemServiceRegistry @@ -100,34 +133,35 @@ public class RavenwoodContext extends RavenwoodBaseContext { @Override public Looper getMainLooper() { Objects.requireNonNull(mMainThread, - "Test must request setProvideMainThread() via RavenwoodRule"); + "Test must request setProvideMainThread() via RavenwoodConfig"); return mMainThread.getLooper(); } @Override public Handler getMainThreadHandler() { Objects.requireNonNull(mMainThread, - "Test must request setProvideMainThread() via RavenwoodRule"); + "Test must request setProvideMainThread() via RavenwoodConfig"); return mMainThread.getThreadHandler(); } @Override public Executor getMainExecutor() { Objects.requireNonNull(mMainThread, - "Test must request setProvideMainThread() via RavenwoodRule"); + "Test must request setProvideMainThread() via RavenwoodConfig"); return mMainThread.getThreadExecutor(); } @Override public String getPackageName() { return Objects.requireNonNull(mPackageName, - "Test must request setPackageName() via RavenwoodRule"); + "Test must request setPackageName() (or setTargetPackageName())" + + " via RavenwoodConfig"); } @Override public String getOpPackageName() { return Objects.requireNonNull(mPackageName, - "Test must request setPackageName() via RavenwoodRule"); + "Test must request setPackageName() via RavenwoodConfig"); } @Override @@ -150,6 +184,61 @@ public class RavenwoodContext extends RavenwoodBaseContext { return Context.DEVICE_ID_DEFAULT; } + @Override + public File getFilesDir() { + return mFilesDir; + } + + @Override + public File getCacheDir() { + return mCacheDir; + } + + @Override + public boolean deleteFile(String name) { + File f = new File(name); + return f.delete(); + } + + @Override + public Resources getResources() { + synchronized (mLock) { + if (mResources == null) { + mResources = mResourcesSupplier.get(); + } + return mResources; + } + } + + @Override + public AssetManager getAssets() { + return getResources().getAssets(); + } + + @Override + public Theme getTheme() { + synchronized (mLock) { + if (mTheme == null) { + mTheme = getResources().newTheme(); + } + return mTheme; + } + } + + @Override + public String getPackageResourcePath() { + return new File(RAVENWOOD_RESOURCE_APK).getAbsolutePath(); + } + + public void setApplicationContext(RavenwoodContext appContext) { + mAppContext = appContext; + } + + @Override + public Context getApplicationContext() { + return mAppContext; + } + /** * Wrap the given {@link Supplier} to become memoized. * @@ -175,4 +264,26 @@ public class RavenwoodContext extends RavenwoodBaseContext { public interface ThrowingSupplier<T> { T get() throws Exception; } + + + static File createTempDir(String prefix) throws IOException { + // Create a temp file, delete it and recreate it as a directory. + final File dir = File.createTempFile(prefix + "-", ""); + dir.delete(); + dir.mkdirs(); + return dir; + } + + static void deleteDir(File dir) { + File[] children = dir.listFiles(); + if (children != null) { + for (File child : children) { + if (child.isDirectory()) { + deleteDir(child); + } else { + child.delete(); + } + } + } + } } diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodEnablementChecker.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodEnablementChecker.java new file mode 100644 index 000000000000..77275c445dd9 --- /dev/null +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodEnablementChecker.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.platform.test.ravenwood; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.platform.test.annotations.DisabledOnRavenwood; +import android.platform.test.annotations.EnabledOnRavenwood; +import android.platform.test.annotations.IgnoreUnderRavenwood; + +import org.junit.runner.Description; + +/** + * Calculates which tests need to be executed on Ravenwood. + */ +public class RavenwoodEnablementChecker { + private static final String TAG = "RavenwoodDisablementChecker"; + + private RavenwoodEnablementChecker() { + } + + /** + * Determine if the given {@link Description} should be enabled when running on the + * Ravenwood test environment. + * + * A more specific method-level annotation always takes precedence over any class-level + * annotation, and an {@link EnabledOnRavenwood} annotation always takes precedence over + * an {@link DisabledOnRavenwood} annotation. + */ + public static boolean shouldEnableOnRavenwood(Description description, + boolean takeIntoAccountRunDisabledTestsFlag) { + // First, consult any method-level annotations + if (description.isTest()) { + Boolean result = null; + + // Stopgap for http://g/ravenwood/EPAD-N5ntxM + if (description.getMethodName().endsWith("$noRavenwood")) { + result = false; + } else if (description.getAnnotation(EnabledOnRavenwood.class) != null) { + result = true; + } else if (description.getAnnotation(DisabledOnRavenwood.class) != null) { + result = false; + } else if (description.getAnnotation(IgnoreUnderRavenwood.class) != null) { + result = false; + } + if (result != null) { + if (takeIntoAccountRunDisabledTestsFlag + && RavenwoodRule.private$ravenwood().isRunningDisabledTests()) { + result = !shouldStillIgnoreInProbeIgnoreMode( + description.getTestClass(), description.getMethodName()); + } + } + if (result != null) { + return result; + } + } + + // Otherwise, consult any class-level annotations + return shouldRunClassOnRavenwood(description.getTestClass(), + takeIntoAccountRunDisabledTestsFlag); + } + + public static boolean shouldRunClassOnRavenwood(@NonNull Class<?> testClass, + boolean takeIntoAccountRunDisabledTestsFlag) { + boolean result = true; + if (testClass.getAnnotation(EnabledOnRavenwood.class) != null) { + result = true; + } else if (testClass.getAnnotation(DisabledOnRavenwood.class) != null) { + result = false; + } else if (testClass.getAnnotation(IgnoreUnderRavenwood.class) != null) { + result = false; + } + if (!result) { + if (takeIntoAccountRunDisabledTestsFlag + && RavenwoodRule.private$ravenwood().isRunningDisabledTests()) { + result = !shouldStillIgnoreInProbeIgnoreMode(testClass, null); + } + } + return result; + } + + /** + * Check if a test should _still_ disabled even if {@code RUN_DISABLED_TESTS} + * is true, using {@code REALLY_DISABLED_PATTERN}. + * + * This only works on tests, not on classes. + */ + static boolean shouldStillIgnoreInProbeIgnoreMode( + @NonNull Class<?> testClass, @Nullable String methodName) { + if (RavenwoodRule.private$ravenwood().getReallyDisabledPattern().pattern().isEmpty()) { + return false; + } + + final var fullname = testClass.getName() + (methodName != null ? "#" + methodName : ""); + + System.out.println("XXX=" + fullname); + + if (RavenwoodRule.private$ravenwood().getReallyDisabledPattern().matcher(fullname).find()) { + System.out.println("Still ignoring " + fullname); + return true; + } + return false; + } +} diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodNativeLoader.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodNativeLoader.java new file mode 100644 index 000000000000..e5486117e7f2 --- /dev/null +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodNativeLoader.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.platform.test.ravenwood; + +import com.android.ravenwood.common.RavenwoodCommonUtils; + +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * We use this class to load libandroid_runtime. + * In the future, we may load other native libraries. + */ +public final class RavenwoodNativeLoader { + public static final String CORE_NATIVE_CLASSES = "core_native_classes"; + public static final String ICU_DATA_PATH = "icu.data.path"; + public static final String KEYBOARD_PATHS = "keyboard_paths"; + public static final String GRAPHICS_NATIVE_CLASSES = "graphics_native_classes"; + + public static final String LIBANDROID_RUNTIME_NAME = "android_runtime"; + + /** + * Classes with native methods that are backed by libandroid_runtime. + * + * See frameworks/base/core/jni/platform/host/HostRuntime.cpp + */ + private static final Class<?>[] sLibandroidClasses = { + android.util.Log.class, + android.os.Parcel.class, + android.os.Binder.class, + android.content.res.ApkAssets.class, + android.content.res.AssetManager.class, + android.content.res.StringBlock.class, + android.content.res.XmlBlock.class, + }; + + /** + * Classes with native methods that are backed by libhwui. + * + * See frameworks/base/libs/hwui/apex/LayoutlibLoader.cpp + */ + private static final Class<?>[] sLibhwuiClasses = { + android.graphics.Interpolator.class, + android.graphics.Matrix.class, + android.graphics.Path.class, + android.graphics.Color.class, + android.graphics.ColorSpace.class, + }; + + /** + * Extra strings needed to pass to register_android_graphics_classes(). + * + * `android.graphics.Graphics` is not actually a class, so we just hardcode it here. + */ + public final static String[] GRAPHICS_EXTRA_INIT_PARAMS = new String[] { + "android.graphics.Graphics" + }; + + private RavenwoodNativeLoader() { + } + + private static void log(String message) { + System.out.println("RavenwoodNativeLoader: " + message); + } + + private static void log(String fmt, Object... args) { + log(String.format(fmt, args)); + } + + private static void ensurePropertyNotSet(String key) { + if (System.getProperty(key) != null) { + throw new RuntimeException("System property \"" + key + "\" is set unexpectedly"); + } + } + + private static void setProperty(String key, String value) { + System.setProperty(key, value); + log("Property set: %s=\"%s\"", key, value); + } + + private static void dumpSystemProperties() { + for (var prop : System.getProperties().entrySet()) { + log(" %s=\"%s\"", prop.getKey(), prop.getValue()); + } + } + + /** + * libandroid_runtime uses Java's system properties to decide what JNI methods to set up. + * Set up these properties and load the native library + */ + public static void loadFrameworkNativeCode() { + if ("1".equals(System.getenv("RAVENWOOD_DUMP_PROPERTIES"))) { + log("Java system properties:"); + dumpSystemProperties(); + } + + // Make sure these properties are not set. + ensurePropertyNotSet(CORE_NATIVE_CLASSES); + ensurePropertyNotSet(ICU_DATA_PATH); + ensurePropertyNotSet(KEYBOARD_PATHS); + ensurePropertyNotSet(GRAPHICS_NATIVE_CLASSES); + + // Build the property values + final var joiner = Collectors.joining(","); + final var libandroidClasses = + Arrays.stream(sLibandroidClasses).map(Class::getName).collect(joiner); + final var libhwuiClasses = Stream.concat( + Arrays.stream(sLibhwuiClasses).map(Class::getName), + Arrays.stream(GRAPHICS_EXTRA_INIT_PARAMS) + ).collect(joiner); + + // Load the libraries + setProperty(CORE_NATIVE_CLASSES, libandroidClasses); + setProperty(GRAPHICS_NATIVE_CLASSES, libhwuiClasses); + log("Loading " + LIBANDROID_RUNTIME_NAME + " for '" + libandroidClasses + "' and '" + + libhwuiClasses + "'"); + RavenwoodCommonUtils.loadJniLibrary(LIBANDROID_RUNTIME_NAME); + } +} diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java deleted file mode 100644 index 4357f2b8660a..000000000000 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.platform.test.ravenwood; - -import static org.junit.Assert.assertFalse; - -import android.app.ActivityManager; -import android.app.Instrumentation; -import android.os.Build; -import android.os.Bundle; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.ServiceManager; -import android.util.Log; - -import androidx.test.platform.app.InstrumentationRegistry; - -import com.android.internal.os.RuntimeInit; -import com.android.server.LocalServices; - -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.Description; -import org.junit.runner.RunWith; -import org.junit.runners.model.Statement; - -import java.io.PrintStream; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -public class RavenwoodRuleImpl { - private static final String MAIN_THREAD_NAME = "RavenwoodMain"; - - /** - * When enabled, attempt to dump all thread stacks just before we hit the - * overall Tradefed timeout, to aid in debugging deadlocks. - */ - private static final boolean ENABLE_TIMEOUT_STACKS = false; - private static final int TIMEOUT_MILLIS = 9_000; - - private static final ScheduledExecutorService sTimeoutExecutor = - Executors.newScheduledThreadPool(1); - - private static ScheduledFuture<?> sPendingTimeout; - - /** - * When enabled, attempt to detect uncaught exceptions from background threads. - */ - private static final boolean ENABLE_UNCAUGHT_EXCEPTION_DETECTION = false; - - /** - * When set, an unhandled exception was discovered (typically on a background thread), and we - * capture it here to ensure it's reported as a test failure. - */ - private static final AtomicReference<Throwable> sPendingUncaughtException = - new AtomicReference<>(); - - private static final Thread.UncaughtExceptionHandler sUncaughtExceptionHandler = - (thread, throwable) -> { - // Remember the first exception we discover - sPendingUncaughtException.compareAndSet(null, throwable); - }; - - public static void init(RavenwoodRule rule) { - if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) { - maybeThrowPendingUncaughtException(false); - Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler); - } - - RuntimeInit.redirectLogStreams(); - - android.os.Process.init$ravenwood(rule.mUid, rule.mPid); - android.os.Binder.init$ravenwood(); -// android.os.SystemProperties.init$ravenwood( -// rule.mSystemProperties.getValues(), -// rule.mSystemProperties.getKeyReadablePredicate(), -// rule.mSystemProperties.getKeyWritablePredicate()); - setSystemProperties(rule.mSystemProperties); - - ServiceManager.init$ravenwood(); - LocalServices.removeAllServicesForTest(); - - ActivityManager.init$ravenwood(rule.mCurrentUser); - - final HandlerThread main; - if (rule.mProvideMainThread) { - main = new HandlerThread(MAIN_THREAD_NAME); - main.start(); - Looper.setMainLooperForTest(main.getLooper()); - } else { - main = null; - } - - rule.mContext = new RavenwoodContext(rule.mPackageName, main); - rule.mInstrumentation = new Instrumentation(); - rule.mInstrumentation.basicInit(rule.mContext); - InstrumentationRegistry.registerInstance(rule.mInstrumentation, Bundle.EMPTY); - - RavenwoodSystemServer.init(rule); - - if (ENABLE_TIMEOUT_STACKS) { - sPendingTimeout = sTimeoutExecutor.schedule(RavenwoodRuleImpl::dumpStacks, - TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - } - - // Touch some references early to ensure they're <clinit>'ed - Objects.requireNonNull(Build.TYPE); - Objects.requireNonNull(Build.VERSION.SDK); - } - - public static void reset(RavenwoodRule rule) { - if (ENABLE_TIMEOUT_STACKS) { - sPendingTimeout.cancel(false); - } - - RavenwoodSystemServer.reset(rule); - - InstrumentationRegistry.registerInstance(null, Bundle.EMPTY); - rule.mInstrumentation = null; - rule.mContext = null; - - if (rule.mProvideMainThread) { - Looper.getMainLooper().quit(); - Looper.clearMainLooperForTest(); - } - - ActivityManager.reset$ravenwood(); - - LocalServices.removeAllServicesForTest(); - ServiceManager.reset$ravenwood(); - - setSystemProperties(RavenwoodSystemProperties.DEFAULT_VALUES); - android.os.Binder.reset$ravenwood(); - android.os.Process.reset$ravenwood(); - - if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) { - maybeThrowPendingUncaughtException(true); - } - } - - public static void logTestRunner(String label, Description description) { - // This message string carefully matches the exact format emitted by on-device tests, to - // aid developers in debugging raw text logs - Log.e("TestRunner", label + ": " + description.getMethodName() - + "(" + description.getTestClass().getName() + ")"); - } - - private static void dumpStacks() { - final PrintStream out = System.err; - out.println("-----BEGIN ALL THREAD STACKS-----"); - final Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces(); - for (Map.Entry<Thread, StackTraceElement[]> stack : stacks.entrySet()) { - out.println(); - Thread t = stack.getKey(); - out.println(t.toString() + " ID=" + t.getId()); - for (StackTraceElement e : stack.getValue()) { - out.println("\tat " + e); - } - } - out.println("-----END ALL THREAD STACKS-----"); - } - - /** - * If there's a pending uncaught exception, consume and throw it now. Typically used to - * report an exception on a background thread as a failure for the currently running test. - */ - private static void maybeThrowPendingUncaughtException(boolean duringReset) { - final Throwable pending = sPendingUncaughtException.getAndSet(null); - if (pending != null) { - if (duringReset) { - throw new IllegalStateException( - "Found an uncaught exception during this test", pending); - } else { - throw new IllegalStateException( - "Found an uncaught exception before this test started", pending); - } - } - } - - public static void validate(Statement base, Description description, - boolean enableOptionalValidation) { - validateTestRunner(base, description, enableOptionalValidation); - validateTestAnnotations(base, description, enableOptionalValidation); - } - - private static void validateTestRunner(Statement base, Description description, - boolean shouldFail) { - final var testClass = description.getTestClass(); - final var runWith = testClass.getAnnotation(RunWith.class); - if (runWith == null) { - return; - } - - // Due to build dependencies, we can't directly refer to androidx classes here, - // so just check the class name instead. - if (runWith.value().getCanonicalName().equals("androidx.test.runner.AndroidJUnit4")) { - var message = "Test " + testClass.getCanonicalName() + " uses deprecated" - + " test runner androidx.test.runner.AndroidJUnit4." - + " Switch to androidx.test.ext.junit.runners.AndroidJUnit4."; - if (shouldFail) { - Assert.fail(message); - } else { - System.err.println("Warning: " + message); - } - } - } - - /** - * @return if a method has any of annotations. - */ - private static boolean hasAnyAnnotations(Method m, Class<? extends Annotation>... annotations) { - for (var anno : annotations) { - if (m.getAnnotation(anno) != null) { - return true; - } - } - return false; - } - - private static void validateTestAnnotations(Statement base, Description description, - boolean enableOptionalValidation) { - final var testClass = description.getTestClass(); - - final var message = new StringBuilder(); - - boolean hasErrors = false; - for (Method m : collectMethods(testClass)) { - if (Modifier.isPublic(m.getModifiers()) && m.getName().startsWith("test")) { - if (!hasAnyAnnotations(m, Test.class, Before.class, After.class, - BeforeClass.class, AfterClass.class)) { - message.append("\nMethod " + m.getName() + "() doesn't have @Test"); - hasErrors = true; - } - } - if ("setUp".equals(m.getName())) { - if (!hasAnyAnnotations(m, Before.class)) { - message.append("\nMethod " + m.getName() + "() doesn't have @Before"); - hasErrors = true; - } - if (!Modifier.isPublic(m.getModifiers())) { - message.append("\nMethod " + m.getName() + "() must be public"); - hasErrors = true; - } - } - if ("tearDown".equals(m.getName())) { - if (!hasAnyAnnotations(m, After.class)) { - message.append("\nMethod " + m.getName() + "() doesn't have @After"); - hasErrors = true; - } - if (!Modifier.isPublic(m.getModifiers())) { - message.append("\nMethod " + m.getName() + "() must be public"); - hasErrors = true; - } - } - } - assertFalse("Problem(s) detected in class " + testClass.getCanonicalName() + ":" - + message, hasErrors); - } - - /** - * Collect all (public or private or any) methods in a class, including inherited methods. - */ - private static List<Method> collectMethods(Class<?> clazz) { - var ret = new ArrayList<Method>(); - collectMethods(clazz, ret); - return ret; - } - - private static void collectMethods(Class<?> clazz, List<Method> result) { - // Class.getMethods() only return public methods, so we need to use getDeclaredMethods() - // instead, and recurse. - for (var m : clazz.getDeclaredMethods()) { - result.add(m); - } - if (clazz.getSuperclass() != null) { - collectMethods(clazz.getSuperclass(), result); - } - } - - /** - * Set the current configuration to the actual SystemProperties. - */ - public static void setSystemProperties(RavenwoodSystemProperties ravenwoodSystemProperties) { - var clone = new RavenwoodSystemProperties(ravenwoodSystemProperties, true); - - android.os.SystemProperties.init$ravenwood( - clone.getValues(), - clone.getKeyReadablePredicate(), - clone.getKeyWritablePredicate()); - } -} diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java new file mode 100644 index 000000000000..03513ab0a2af --- /dev/null +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.platform.test.ravenwood; + +import static com.android.ravenwood.common.RavenwoodCommonUtils.ensureIsPublicMember; + +import static org.junit.Assert.fail; + +import android.annotation.Nullable; + +import com.android.internal.annotations.GuardedBy; +import com.android.ravenwood.common.RavenwoodRuntimeException; + +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.runner.Description; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.WeakHashMap; + +/** + * Used to store various states associated with the current test runner that's inly needed + * in junit-impl. + * + * We don't want to put it in junit-src to avoid having to recompile all the downstream + * dependencies after changing this class. + * + * All members must be called from the runner's main thread. + */ +public final class RavenwoodRunnerState { + private static final String TAG = "RavenwoodRunnerState"; + + @GuardedBy("sStates") + private static final WeakHashMap<RavenwoodAwareTestRunner, RavenwoodRunnerState> sStates = + new WeakHashMap<>(); + + private final RavenwoodAwareTestRunner mRunner; + + /** + * Ctor. + */ + public RavenwoodRunnerState(RavenwoodAwareTestRunner runner) { + mRunner = runner; + } + + private Description mClassDescription; + private Description mMethodDescription; + + private RavenwoodConfig mCurrentConfig; + private RavenwoodRule mCurrentRule; + private boolean mHasRavenwoodRule; + + public Description getClassDescription() { + return mClassDescription; + } + + public void enterTestClass(Description classDescription) throws IOException { + mClassDescription = classDescription; + + mHasRavenwoodRule = hasRavenwoodRule(mRunner.getTestClass().getJavaClass()); + mCurrentConfig = extractConfiguration(mRunner.getTestClass().getJavaClass()); + + if (mCurrentConfig != null) { + RavenwoodRuntimeEnvironmentController.init(mCurrentConfig); + } + } + + public void exitTestClass() { + if (mCurrentConfig != null) { + try { + RavenwoodRuntimeEnvironmentController.reset(); + } finally { + mClassDescription = null; + } + } + } + + public void enterTestMethod(Description description) { + mMethodDescription = description; + } + + public void exitTestMethod() { + mMethodDescription = null; + } + + public void enterRavenwoodRule(RavenwoodRule rule) throws IOException { + if (!mHasRavenwoodRule) { + fail("If you have a RavenwoodRule in your test, make sure the field type is" + + " RavenwoodRule so Ravenwood can detect it."); + } + if (mCurrentConfig != null) { + fail("RavenwoodConfig and RavenwoodRule cannot be used in the same class." + + " Suggest migrating to RavenwoodConfig."); + } + if (mCurrentRule != null) { + fail("Multiple nesting RavenwoodRule's are detected in the same class," + + " which is not supported."); + } + mCurrentRule = rule; + RavenwoodRuntimeEnvironmentController.init(rule.getConfiguration()); + } + + public void exitRavenwoodRule(RavenwoodRule rule) { + if (mCurrentRule != rule) { + return; // This happens if the rule did _not_ take effect somehow. + } + + try { + RavenwoodRuntimeEnvironmentController.reset(); + } finally { + mCurrentRule = null; + } + } + + /** + * @return a configuration from a test class, if any. + */ + @Nullable + private RavenwoodConfig extractConfiguration(Class<?> testClass) { + var field = findConfigurationField(testClass); + if (field == null) { + if (mHasRavenwoodRule) { + // Should be handled by RavenwoodRule + return null; + } + + // If no RavenwoodConfig and no RavenwoodRule, return a default config + return new RavenwoodConfig.Builder().build(); + } + if (mHasRavenwoodRule) { + fail("RavenwoodConfig and RavenwoodRule cannot be used in the same class." + + " Suggest migrating to RavenwoodConfig."); + } + + try { + return (RavenwoodConfig) field.get(null); + } catch (IllegalAccessException e) { + throw new RavenwoodRuntimeException("Failed to fetch from the configuration field", e); + } + } + + /** + * @return true if the current target class (or its super classes) has any @Rule / @ClassRule + * fields of type RavenwoodRule. + * + * Note, this check won't detect cases where a Rule is of type + * {@link TestRule} and still be a {@link RavenwoodRule}. But that'll be detected at runtime + * as a failure, in {@link #enterRavenwoodRule}. + */ + private static boolean hasRavenwoodRule(Class<?> testClass) { + for (var field : testClass.getDeclaredFields()) { + if (!field.isAnnotationPresent(Rule.class) + && !field.isAnnotationPresent(ClassRule.class)) { + continue; + } + if (field.getType().equals(RavenwoodRule.class)) { + return true; + } + } + // JUnit supports rules as methods, so we need to check them too. + for (var method : testClass.getDeclaredMethods()) { + if (!method.isAnnotationPresent(Rule.class) + && !method.isAnnotationPresent(ClassRule.class)) { + continue; + } + if (method.getReturnType().equals(RavenwoodRule.class)) { + return true; + } + } + // Look into the super class. + if (!testClass.getSuperclass().equals(Object.class)) { + return hasRavenwoodRule(testClass.getSuperclass()); + } + return false; + } + + /** + * Find and return a field with @RavenwoodConfig.Config, which must be of type + * RavenwoodConfig. + */ + @Nullable + private static Field findConfigurationField(Class<?> testClass) { + Field foundField = null; + + for (var field : testClass.getDeclaredFields()) { + final var hasAnot = field.isAnnotationPresent(RavenwoodConfig.Config.class); + final var isType = field.getType().equals(RavenwoodConfig.class); + + if (hasAnot) { + if (isType) { + // Good, use this field. + if (foundField != null) { + fail(String.format( + "Class %s has multiple fields with %s", + testClass.getCanonicalName(), + "@RavenwoodConfig.Config")); + } + // Make sure it's static public + ensureIsPublicMember(field, true); + + foundField = field; + } else { + fail(String.format( + "Field %s.%s has %s but type is not %s", + testClass.getCanonicalName(), + field.getName(), + "@RavenwoodConfig.Config", + "RavenwoodConfig")); + return null; // unreachable + } + } else { + if (isType) { + fail(String.format( + "Field %s.%s does not have %s but type is %s", + testClass.getCanonicalName(), + field.getName(), + "@RavenwoodConfig.Config", + "RavenwoodConfig")); + return null; // unreachable + } else { + // Unrelated field, ignore. + continue; + } + } + } + if (foundField != null) { + return foundField; + } + if (!testClass.getSuperclass().equals(Object.class)) { + return findConfigurationField(testClass.getSuperclass()); + } + return null; + } +} diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java new file mode 100644 index 000000000000..40b14dbe6bfd --- /dev/null +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.platform.test.ravenwood; + +import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_INST_RESOURCE_APK; +import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_RESOURCE_APK; +import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING; +import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERSION_JAVA_SYSPROP; + +import android.app.ActivityManager; +import android.app.Instrumentation; +import android.app.ResourcesManager; +import android.content.res.Resources; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.ServiceManager; +import android.system.ErrnoException; +import android.system.Os; +import android.util.Log; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.internal.os.RuntimeInit; +import com.android.ravenwood.common.RavenwoodCommonUtils; +import com.android.ravenwood.common.RavenwoodRuntimeException; +import com.android.ravenwood.common.SneakyThrow; +import com.android.server.LocalServices; + +import org.junit.runner.Description; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +/** + * Responsible for initializing and de-initializing the environment, according to a + * {@link RavenwoodConfig}. + */ +public class RavenwoodRuntimeEnvironmentController { + private static final String TAG = "RavenwoodRuntimeEnvironmentController"; + + private RavenwoodRuntimeEnvironmentController() { + } + + private static final String MAIN_THREAD_NAME = "RavenwoodMain"; + + /** + * When enabled, attempt to dump all thread stacks just before we hit the + * overall Tradefed timeout, to aid in debugging deadlocks. + */ + private static final boolean ENABLE_TIMEOUT_STACKS = + "1".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS")); + + private static final int TIMEOUT_MILLIS = 9_000; + + private static final ScheduledExecutorService sTimeoutExecutor = + Executors.newScheduledThreadPool(1); + + private static ScheduledFuture<?> sPendingTimeout; + + private static long sOriginalIdentityToken = -1; + + /** + * When enabled, attempt to detect uncaught exceptions from background threads. + */ + private static final boolean ENABLE_UNCAUGHT_EXCEPTION_DETECTION = + "1".equals(System.getenv("RAVENWOOD_ENABLE_UNCAUGHT_EXCEPTION_DETECTION")); + + /** + * When set, an unhandled exception was discovered (typically on a background thread), and we + * capture it here to ensure it's reported as a test failure. + */ + private static final AtomicReference<Throwable> sPendingUncaughtException = + new AtomicReference<>(); + + private static final Thread.UncaughtExceptionHandler sUncaughtExceptionHandler = + (thread, throwable) -> { + // Remember the first exception we discover + sPendingUncaughtException.compareAndSet(null, throwable); + }; + + // TODO: expose packCallingIdentity function in libbinder and use it directly + // See: packCallingIdentity in frameworks/native/libs/binder/IPCThreadState.cpp + private static long packBinderIdentityToken( + boolean hasExplicitIdentity, int callingUid, int callingPid) { + long res = ((long) callingUid << 32) | callingPid; + if (hasExplicitIdentity) { + res |= (0x1 << 30); + } else { + res &= ~(0x1 << 30); + } + return res; + } + + private static RavenwoodConfig sConfig; + private static boolean sInitialized = false; + + /** + * Initialize the global environment. + */ + public static void globalInitOnce() { + if (sInitialized) { + return; + } + sInitialized = true; + + // We haven't initialized liblog yet, so directly write to System.out here. + RavenwoodCommonUtils.log(TAG, "globalInit()"); + + // Do the basic set up for the android sysprops. + setSystemProperties(RavenwoodSystemProperties.DEFAULT_VALUES); + + // Make sure libandroid_runtime is loaded. + RavenwoodNativeLoader.loadFrameworkNativeCode(); + + // Redirect stdout/stdin to liblog. + RuntimeInit.redirectLogStreams(); + + if (RAVENWOOD_VERBOSE_LOGGING) { + RavenwoodCommonUtils.log(TAG, "Force enabling verbose logging"); + try { + Os.setenv("ANDROID_LOG_TAGS", "*:v", true); + } catch (ErrnoException e) { + // Shouldn't happen. + } + } + + System.setProperty(RAVENWOOD_VERSION_JAVA_SYSPROP, "1"); + // This will let AndroidJUnit4 use the original runner. + System.setProperty("android.junit.runner", + "androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner"); + } + + /** + * Initialize the environment. + */ + public static void init(RavenwoodConfig config) throws IOException { + if (RAVENWOOD_VERBOSE_LOGGING) { + Log.i(TAG, "init() called here", new RuntimeException("STACKTRACE")); + } + try { + initInner(config); + } catch (Exception th) { + Log.e(TAG, "init() failed", th); + reset(); + SneakyThrow.sneakyThrow(th); + } + } + + private static void initInner(RavenwoodConfig config) throws IOException { + if (sConfig != null) { + throw new RavenwoodRuntimeException("Internal error: init() called without reset()"); + } + sConfig = config; + if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) { + maybeThrowPendingUncaughtException(false); + Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler); + } + + android.os.Process.init$ravenwood(config.mUid, config.mPid); + sOriginalIdentityToken = Binder.clearCallingIdentity(); + Binder.restoreCallingIdentity(packBinderIdentityToken(false, config.mUid, config.mPid)); + setSystemProperties(config.mSystemProperties); + + ServiceManager.init$ravenwood(); + LocalServices.removeAllServicesForTest(); + + ActivityManager.init$ravenwood(config.mCurrentUser); + + final HandlerThread main; + if (config.mProvideMainThread) { + main = new HandlerThread(MAIN_THREAD_NAME); + main.start(); + Looper.setMainLooperForTest(main.getLooper()); + } else { + main = null; + } + + final boolean isSelfInstrumenting = + Objects.equals(config.mTestPackageName, config.mTargetPackageName); + + // This will load the resources from the apk set to `resource_apk` in the build file. + // This is supposed to be the "target app"'s resources. + final Supplier<Resources> targetResourcesLoader = () -> { + var file = new File(RAVENWOOD_RESOURCE_APK); + return config.mState.loadResources(file.exists() ? file : null); + }; + + // Set up test context's (== instrumentation context's) resources. + // If the target package name == test package name, then we use the main resources. + final Supplier<Resources> instResourcesLoader; + if (isSelfInstrumenting) { + instResourcesLoader = targetResourcesLoader; + } else { + instResourcesLoader = () -> { + var file = new File(RAVENWOOD_INST_RESOURCE_APK); + return config.mState.loadResources(file.exists() ? file : null); + }; + } + + var instContext = new RavenwoodContext( + config.mTestPackageName, main, instResourcesLoader); + var targetContext = new RavenwoodContext( + config.mTargetPackageName, main, targetResourcesLoader); + + // Set up app context. + var appContext = new RavenwoodContext( + config.mTargetPackageName, main, targetResourcesLoader); + appContext.setApplicationContext(appContext); + if (isSelfInstrumenting) { + instContext.setApplicationContext(appContext); + targetContext.setApplicationContext(appContext); + } else { + // When instrumenting into another APK, the test context doesn't have an app context. + targetContext.setApplicationContext(appContext); + } + config.mInstContext = instContext; + config.mTargetContext = targetContext; + + // Prepare other fields. + config.mInstrumentation = new Instrumentation(); + config.mInstrumentation.basicInit(config.mInstContext, config.mTargetContext); + InstrumentationRegistry.registerInstance(config.mInstrumentation, Bundle.EMPTY); + + RavenwoodSystemServer.init(config); + + if (ENABLE_TIMEOUT_STACKS) { + sPendingTimeout = sTimeoutExecutor.schedule( + RavenwoodRuntimeEnvironmentController::dumpStacks, + TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + } + + // Touch some references early to ensure they're <clinit>'ed + Objects.requireNonNull(Build.TYPE); + Objects.requireNonNull(Build.VERSION.SDK); + } + + /** + * De-initialize. + */ + public static void reset() { + if (RAVENWOOD_VERBOSE_LOGGING) { + Log.i(TAG, "reset() called here", new RuntimeException("STACKTRACE")); + } + if (sConfig == null) { + throw new RavenwoodRuntimeException("Internal error: reset() already called"); + } + var config = sConfig; + sConfig = null; + + if (ENABLE_TIMEOUT_STACKS) { + sPendingTimeout.cancel(false); + } + + RavenwoodSystemServer.reset(config); + + InstrumentationRegistry.registerInstance(null, Bundle.EMPTY); + config.mInstrumentation = null; + if (config.mInstContext != null) { + ((RavenwoodContext) config.mInstContext).cleanUp(); + } + if (config.mTargetContext != null) { + ((RavenwoodContext) config.mTargetContext).cleanUp(); + } + config.mInstContext = null; + config.mTargetContext = null; + + if (config.mProvideMainThread) { + Looper.getMainLooper().quit(); + Looper.clearMainLooperForTest(); + } + + ActivityManager.reset$ravenwood(); + + LocalServices.removeAllServicesForTest(); + ServiceManager.reset$ravenwood(); + + setSystemProperties(RavenwoodSystemProperties.DEFAULT_VALUES); + if (sOriginalIdentityToken != -1) { + Binder.restoreCallingIdentity(sOriginalIdentityToken); + } + android.os.Process.reset$ravenwood(); + + try { + ResourcesManager.setInstance(null); // Better structure needed. + } catch (Exception e) { + // AOSP-CHANGE: AOSP doesn't support resources yet. + } + + if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) { + maybeThrowPendingUncaughtException(true); + } + } + + public static void logTestRunner(String label, Description description) { + // This message string carefully matches the exact format emitted by on-device tests, to + // aid developers in debugging raw text logs + Log.e("TestRunner", label + ": " + description.getMethodName() + + "(" + description.getTestClass().getName() + ")"); + } + + private static void dumpStacks() { + final PrintStream out = System.err; + out.println("-----BEGIN ALL THREAD STACKS-----"); + final Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces(); + for (Map.Entry<Thread, StackTraceElement[]> stack : stacks.entrySet()) { + out.println(); + Thread t = stack.getKey(); + out.println(t.toString() + " ID=" + t.getId()); + for (StackTraceElement e : stack.getValue()) { + out.println("\tat " + e); + } + } + out.println("-----END ALL THREAD STACKS-----"); + } + + /** + * If there's a pending uncaught exception, consume and throw it now. Typically used to + * report an exception on a background thread as a failure for the currently running test. + */ + private static void maybeThrowPendingUncaughtException(boolean duringReset) { + final Throwable pending = sPendingUncaughtException.getAndSet(null); + if (pending != null) { + if (duringReset) { + throw new IllegalStateException( + "Found an uncaught exception during this test", pending); + } else { + throw new IllegalStateException( + "Found an uncaught exception before this test started", pending); + } + } + } + + /** + * Set the current configuration to the actual SystemProperties. + */ + public static void setSystemProperties(RavenwoodSystemProperties ravenwoodSystemProperties) { + var clone = new RavenwoodSystemProperties(ravenwoodSystemProperties, true); + + android.os.SystemProperties.init$ravenwood( + clone.getValues(), + clone.getKeyReadablePredicate(), + clone.getKeyWritablePredicate()); + } +} diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java index cd6b61df392f..3946dd8471b0 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java @@ -61,19 +61,19 @@ public class RavenwoodSystemServer { private static TimingsTraceAndSlog sTimings; private static SystemServiceManager sServiceManager; - public static void init(RavenwoodRule rule) { + public static void init(RavenwoodConfig config) { // Avoid overhead if no services required - if (rule.mServicesRequired.isEmpty()) return; + if (config.mServicesRequired.isEmpty()) return; sStartedServices = new ArraySet<>(); sTimings = new TimingsTraceAndSlog(); - sServiceManager = new SystemServiceManager(rule.mContext); + sServiceManager = new SystemServiceManager(config.mInstContext); sServiceManager.setStartInfo(false, SystemClock.elapsedRealtime(), SystemClock.uptimeMillis()); LocalServices.addService(SystemServiceManager.class, sServiceManager); - startServices(rule.mServicesRequired); + startServices(config.mServicesRequired); sServiceManager.sealStartedServices(); // TODO: expand to include additional boot phases when relevant @@ -81,7 +81,7 @@ public class RavenwoodSystemServer { sServiceManager.startBootPhase(sTimings, SystemService.PHASE_BOOT_COMPLETED); } - public static void reset(RavenwoodRule rule) { + public static void reset(RavenwoodConfig config) { // TODO: consider introducing shutdown boot phases LocalServices.removeServiceForTest(SystemServiceManager.class); diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java new file mode 100644 index 000000000000..428eb57f20bf --- /dev/null +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.platform.test.ravenwood; + +import android.util.Log; + +import org.junit.runner.Description; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + +/** + * Collect test result stats and write them into a CSV file containing the test results. + * + * The output file is created as `/tmp/Ravenwood-stats_[TEST-MODULE=NAME]_[TIMESTAMP].csv`. + * A symlink to the latest result will be created as + * `/tmp/Ravenwood-stats_[TEST-MODULE=NAME]_latest.csv`. + */ +public class RavenwoodTestStats { + private static final String TAG = "RavenwoodTestStats"; + private static final String HEADER = "Module,Class,ClassDesc,Passed,Failed,Skipped"; + + private static RavenwoodTestStats sInstance; + + /** + * @return a singleton instance. + */ + public static RavenwoodTestStats getInstance() { + if (sInstance == null) { + sInstance = new RavenwoodTestStats(); + } + return sInstance; + } + + /** + * Represents a test result. + */ + public enum Result { + Passed, + Failed, + Skipped, + } + + private final File mOutputFile; + private final PrintWriter mOutputWriter; + private final String mTestModuleName; + + public final Map<Description, Map<Description, Result>> mStats = new HashMap<>(); + + /** Ctor */ + public RavenwoodTestStats() { + mTestModuleName = guessTestModuleName(); + + var basename = "Ravenwood-stats_" + mTestModuleName + "_"; + + // Get the current time + LocalDateTime now = LocalDateTime.now(); + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"); + + var tmpdir = System.getProperty("java.io.tmpdir"); + mOutputFile = new File(tmpdir, basename + now.format(fmt) + ".csv"); + + try { + mOutputWriter = new PrintWriter(mOutputFile); + } catch (IOException e) { + throw new RuntimeException("Failed to crete logfile. File=" + mOutputFile, e); + } + + // Crete the "latest" symlink. + Path symlink = Paths.get(tmpdir, basename + "latest.csv"); + try { + if (Files.exists(symlink)) { + Files.delete(symlink); + } + Files.createSymbolicLink(symlink, Paths.get(mOutputFile.getName())); + + } catch (IOException e) { + throw new RuntimeException("Failed to crete logfile. File=" + mOutputFile, e); + } + + Log.i(TAG, "Test result stats file: " + mOutputFile); + + // Print the header. + mOutputWriter.println(HEADER); + mOutputWriter.flush(); + } + + private String guessTestModuleName() { + // Assume the current directory name is the test module name. + File cwd; + try { + cwd = new File(".").getCanonicalFile(); + } catch (IOException e) { + throw new RuntimeException("Failed to get the current directory", e); + } + return cwd.getName(); + } + + private void addResult(Description classDescription, Description methodDescription, + Result result) { + mStats.compute(classDescription, (classDesc, value) -> { + if (value == null) { + value = new HashMap<>(); + } + value.put(methodDescription, result); + return value; + }); + } + + /** + * Call it when a test class is skipped. + */ + public void onClassSkipped(Description classDescription) { + addResult(classDescription, Description.EMPTY, Result.Skipped); + onClassFinished(classDescription); + } + + /** + * Call it when a test method is finished. + */ + public void onTestFinished(Description classDescription, Description testDescription, + Result result) { + addResult(classDescription, testDescription, result); + } + + /** + * Call it when a test class is finished. + */ + public void onClassFinished(Description classDescription) { + int passed = 0; + int skipped = 0; + int failed = 0; + var stats = mStats.get(classDescription); + if (stats == null) { + return; + } + for (var e : stats.values()) { + switch (e) { + case Passed: passed++; break; + case Skipped: skipped++; break; + case Failed: failed++; break; + } + } + + var testClass = extractTestClass(classDescription); + + mOutputWriter.printf("%s,%s,%s,%d,%d,%d\n", + mTestModuleName, (testClass == null ? "?" : testClass.getCanonicalName()), + classDescription, passed, failed, skipped); + mOutputWriter.flush(); + } + + /** + * Try to extract the class from a description, which is needed because + * ParameterizedAndroidJunit4's description doesn't contain a class. + */ + private Class<?> extractTestClass(Description desc) { + if (desc.getTestClass() != null) { + return desc.getTestClass(); + } + // Look into the children. + for (var child : desc.getChildren()) { + var fromChild = extractTestClass(child); + if (fromChild != null) { + return fromChild; + } + } + return null; + } +} |