From eec7a9d90f920bbea38d8001c1a8fe01d0917af3 Mon Sep 17 00:00:00 2001 From: Stefan Andonian Date: Tue, 23 May 2023 23:44:39 +0000 Subject: [PATCH] Keep ViewCaptureRule logic self-contained. This will make it easier for other apps / processes to integrate the ViewCapture logic into their integrated testing frameworks. Bug: 270158224 Test: Verified that a zip file was generated properly and was able to be loaded into go/web-hv properly. Change-Id: Ib3e4a0b60497937b750126590071884882b22917 --- .../quickstep/FallbackRecentsTest.java | 5 +- .../launcher3/ui/AbstractLauncherUiTest.java | 5 +- .../launcher3/util/rule/FailureWatcher.java | 24 +--- .../launcher3/util/rule/ViewCaptureRule.kt | 111 ++++++++++++------ 4 files changed, 81 insertions(+), 64 deletions(-) diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java index 97e34c5f10..dbe4402812 100644 --- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java +++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java @@ -116,12 +116,11 @@ public class FallbackRecentsTest { Utilities.enableRunningInTestHarnessForTests(); } - final ViewCaptureRule viewCaptureRule = new ViewCaptureRule(); mOrderSensitiveRules = RuleChain .outerRule(new SamplerRule()) .around(new NavigationModeSwitchRule(mLauncher)) - .around(viewCaptureRule) - .around(new FailureWatcher(mDevice, mLauncher, viewCaptureRule.getViewCapture())); + .around(new ViewCaptureRule()) + .around(new FailureWatcher(mDevice, mLauncher)); mOtherLauncherActivity = context.getPackageManager().queryIntentActivities( getHomeIntentInPackage(context), diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java index d7c4ae3857..5bd28d85a3 100644 --- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java +++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java @@ -216,11 +216,10 @@ public abstract class AbstractLauncherUiTest { } protected TestRule getRulesInsideActivityMonitor() { - final ViewCaptureRule viewCaptureRule = new ViewCaptureRule(); final RuleChain inner = RuleChain .outerRule(new PortraitLandscapeRunner(this)) - .around(viewCaptureRule) - .around(new FailureWatcher(mDevice, mLauncher, viewCaptureRule.getViewCapture())); + .around(new ViewCaptureRule()) + .around(new FailureWatcher(mDevice, mLauncher)); return TestHelpers.isInLauncherProcess() ? RuleChain.outerRule(ShellCommandRule.setDefaultLauncher()).around(inner) diff --git a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java index 7ca6a06ed2..6b11fd6af4 100644 --- a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java +++ b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java @@ -6,12 +6,8 @@ import android.os.FileUtils; import android.os.ParcelFileDescriptor.AutoCloseInputStream; import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.test.core.app.ApplicationProvider; import androidx.test.uiautomator.UiDevice; -import com.android.app.viewcapture.ViewCapture; import com.android.launcher3.tapl.LauncherInstrumentation; import com.android.launcher3.ui.AbstractLauncherUiTest; @@ -32,14 +28,10 @@ public class FailureWatcher extends TestWatcher { private static boolean sSavedBugreport = false; final private UiDevice mDevice; private final LauncherInstrumentation mLauncher; - @NonNull - private final ViewCapture mViewCapture; - public FailureWatcher(UiDevice device, LauncherInstrumentation launcher, - @NonNull ViewCapture viewCapture) { + public FailureWatcher(UiDevice device, LauncherInstrumentation launcher) { mDevice = device; mLauncher = launcher; - mViewCapture = viewCapture; } @Override @@ -71,7 +63,7 @@ public class FailureWatcher extends TestWatcher { @Override protected void failed(Throwable e, Description description) { - onError(mLauncher, description, e, mViewCapture); + onError(mLauncher, description, e); } static File diagFile(Description description, String prefix, String ext) { @@ -82,12 +74,6 @@ public class FailureWatcher extends TestWatcher { public static void onError(LauncherInstrumentation launcher, Description description, Throwable e) { - onError(launcher, description, e, null); - } - - private static void onError(LauncherInstrumentation launcher, Description description, - Throwable e, @Nullable ViewCapture viewCapture) { - final File sceenshot = diagFile(description, "TestScreenshot", "png"); final File hierarchy = diagFile(description, "Hierarchy", "zip"); @@ -102,12 +88,6 @@ public class FailureWatcher extends TestWatcher { out.putNextEntry(new ZipEntry("visible_windows.zip")); dumpCommand("cmd window dump-visible-window-views", out); out.closeEntry(); - - if (viewCapture != null) { - out.putNextEntry(new ZipEntry("FS/data/misc/wmtrace/failed_test.vc")); - viewCapture.dumpTo(out, ApplicationProvider.getApplicationContext()); - out.closeEntry(); - } } catch (Exception ignored) { } diff --git a/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt b/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt index 0c6553998d..f3fff35c90 100644 --- a/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt +++ b/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt @@ -19,62 +19,101 @@ import android.app.Activity import android.app.Application import android.media.permission.SafeCloseable import android.os.Bundle +import android.util.Log +import androidx.annotation.AnyThread import androidx.test.core.app.ApplicationProvider import com.android.app.viewcapture.SimpleViewCapture import com.android.app.viewcapture.ViewCapture.MAIN_EXECUTOR import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter -import org.junit.rules.TestRule +import java.io.File +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import org.junit.rules.TestWatcher import org.junit.runner.Description import org.junit.runners.model.Statement +private const val TAG = "ViewCaptureRule" + /** * This JUnit TestRule registers a listener for activity lifecycle events to attach a ViewCapture * instance that other test rules use to dump the timelapse hierarchy upon an error during a test. * * This rule will not work in OOP tests that don't have access to the activity under test. */ -class ViewCaptureRule : TestRule { - val viewCapture = SimpleViewCapture("test-view-capture") +class ViewCaptureRule : TestWatcher() { + private val viewCapture = SimpleViewCapture("test-view-capture") + private val windowListenerCloseables = mutableListOf() override fun apply(base: Statement, description: Description): Statement { + val testWatcherStatement = super.apply(base, description) + return object : Statement() { override fun evaluate() { - val windowListenerCloseables = mutableListOf() - - val lifecycleCallbacks = - object : ActivityLifecycleCallbacksAdapter { - override fun onActivityCreated(activity: Activity, bundle: Bundle?) { - super.onActivityCreated(activity, bundle) - windowListenerCloseables.add( - viewCapture.startCapture( - activity.window.decorView, - "${description.testClass?.simpleName}.${description.methodName}" - ) - ) - } - - override fun onActivityDestroyed(activity: Activity) { - super.onActivityDestroyed(activity) - viewCapture.stopCapture(activity.window.decorView) - } + val lifecycleCallbacks = createLifecycleCallbacks(description) + with(ApplicationProvider.getApplicationContext()) { + registerActivityLifecycleCallbacks(lifecycleCallbacks) + try { + testWatcherStatement.evaluate() + } finally { + unregisterActivityLifecycleCallbacks(lifecycleCallbacks) } - - val application = ApplicationProvider.getApplicationContext() - application.registerActivityLifecycleCallbacks(lifecycleCallbacks) - - try { - base.evaluate() - } finally { - application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks) - - // Clean up ViewCapture references here rather than in onActivityDestroyed so - // test code can access view hierarchy capture. onActivityDestroyed would delete - // view capture data before FailureWatcher could output it as a test artifact. - // This is on the main thread to avoid a race condition where the onDrawListener - // is removed while onDraw is running, resulting in an IllegalStateException. - MAIN_EXECUTOR.execute { windowListenerCloseables.onEach(SafeCloseable::close) } } } } } + + private fun createLifecycleCallbacks(description: Description) = + object : ActivityLifecycleCallbacksAdapter { + override fun onActivityCreated(activity: Activity, bundle: Bundle?) { + super.onActivityCreated(activity, bundle) + windowListenerCloseables.add( + viewCapture.startCapture( + activity.window.decorView, + "${description.testClass?.simpleName}.${description.methodName}" + ) + ) + } + + override fun onActivityDestroyed(activity: Activity) { + super.onActivityDestroyed(activity) + viewCapture.stopCapture(activity.window.decorView) + } + } + + override fun succeeded(description: Description) = cleanup() + + /** If the test fails, this function will output the ViewCapture information. */ + override fun failed(e: Throwable, description: Description) { + super.failed(e, description) + + val testName = "${description.testClass.simpleName}.${description.methodName}" + val application: Application = ApplicationProvider.getApplicationContext() + val zip = File(application.filesDir, "ViewCapture-$testName.zip") + + ZipOutputStream(FileOutputStream(zip)).use { + it.putNextEntry(ZipEntry("FS/data/misc/wmtrace/failed_test.vc")) + viewCapture.dumpTo(it, ApplicationProvider.getApplicationContext()) + it.closeEntry() + } + cleanup() + + Log.d( + TAG, + "Failed $testName due to ${e::class.java.simpleName}.\n" + + "\tUse go/web-hv to open dump file: \n\t\t${zip.absolutePath}" + ) + } + + /** + * Clean up ViewCapture references can't happen in onActivityDestroyed otherwise view + * hierarchies would be erased before they could be outputted. + * + * This is on the main thread to avoid a race condition where the onDrawListener is removed + * while onDraw is running, resulting in an IllegalStateException. + */ + @AnyThread + private fun cleanup() { + MAIN_EXECUTOR.execute { windowListenerCloseables.onEach(SafeCloseable::close) } + } }