Merge "Keep ViewCaptureRule logic self-contained." into udc-dev am: 9678d9470c

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Launcher3/+/23402041

Change-Id: Ibe589891f05b710ff3f11bc8b0ca013830aebf66
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Stefan Andonian
2023-05-25 23:36:58 +00:00
committed by Automerger Merge Worker
4 changed files with 81 additions and 64 deletions

View File

@@ -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),

View File

@@ -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)

View File

@@ -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) {
}

View File

@@ -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<SafeCloseable>()
override fun apply(base: Statement, description: Description): Statement {
val testWatcherStatement = super.apply(base, description)
return object : Statement() {
override fun evaluate() {
val windowListenerCloseables = mutableListOf<SafeCloseable>()
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<Application>()) {
registerActivityLifecycleCallbacks(lifecycleCallbacks)
try {
testWatcherStatement.evaluate()
} finally {
unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
}
val application = ApplicationProvider.getApplicationContext<Application>()
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) }
}
}