TAPL: Verifying some interactions with system
Investigation of TAPL failures, especially flakes is complex, partially
because it’s hard to tell whether it’s Launcher who is wrong or the
system.
We need to introduce a framework that looks at Launcher interaction with
the system and reports when interactions deviate from the expected
course, and who made the first wrong step.
This is first, proof-of-concept CL.
It analyzes long-press events. We had multiple cases when long-presses
didn’t happen or happened unexpectedly.
Launcher registers the events, TAPL retrieves and compares against the
sequence of expected regular expressions. This diagnostic is used when
something fails and at the end of public methods.
Change-Id: I07aa3a027267c03422c99c73ccd8808445c55fe8
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 428e647..ab6393a 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -100,6 +100,7 @@
@PortraitLandscape
public void testOverview() throws Exception {
startTestApps();
+ // mLauncher.pressHome() also tests an important case of pressing home while in background.
Overview overview = mLauncher.pressHome().switchToOverview();
assertTrue("Launcher internal state didn't switch to Overview",
isInState(LauncherState.OVERVIEW));
diff --git a/src/com/android/launcher3/testing/TestLogging.java b/src/com/android/launcher3/testing/TestLogging.java
new file mode 100644
index 0000000..024110d
--- /dev/null
+++ b/src/com/android/launcher3/testing/TestLogging.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 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 com.android.launcher3.testing;
+
+import android.util.Log;
+
+import com.android.launcher3.Utilities;
+
+public final class TestLogging {
+ public static synchronized void recordEvent(String event) {
+ if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+ Log.d(TestProtocol.TAPL_EVENTS_TAG, event);
+ }
+ }
+}
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 929315a..01c207f 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -31,6 +31,7 @@
public static final int QUICK_SWITCH_STATE_ORDINAL = 4;
public static final int ALL_APPS_STATE_ORDINAL = 5;
public static final int BACKGROUND_APP_STATE_ORDINAL = 6;
+ public static final String TAPL_EVENTS_TAG = "TaplEvents";
public static String stateOrdinalToString(int ordinal) {
switch (ordinal) {
diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java
index ba1bfa5..bee7853 100644
--- a/src/com/android/launcher3/touch/ItemLongClickListener.java
+++ b/src/com/android/launcher3/touch/ItemLongClickListener.java
@@ -33,6 +33,7 @@
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.folder.Folder;
+import com.android.launcher3.testing.TestLogging;
/**
* Class to handle long-clicks on workspace items and start drag as a result.
@@ -46,6 +47,7 @@
ItemLongClickListener::onAllAppsItemLongClick;
private static boolean onWorkspaceItemLongClick(View v) {
+ TestLogging.recordEvent("onWorkspaceItemLongClick");
Launcher launcher = Launcher.getLauncher(v.getContext());
if (!canStartDrag(launcher)) return false;
if (!launcher.isInState(NORMAL) && !launcher.isInState(OVERVIEW)) return false;
@@ -75,6 +77,7 @@
}
private static boolean onAllAppsItemLongClick(View v) {
+ TestLogging.recordEvent("onAllAppsItemLongClick");
Launcher launcher = Launcher.getLauncher(v.getContext());
if (!canStartDrag(launcher)) return false;
// When we have exited all apps or are in transition, disregard long clicks
diff --git a/src/com/android/launcher3/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
index 66fdc94..61ce046 100644
--- a/src/com/android/launcher3/touch/WorkspaceTouchListener.java
+++ b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
@@ -25,7 +25,6 @@
import android.graphics.PointF;
import android.graphics.Rect;
-import android.util.Log;
import android.view.GestureDetector;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
@@ -37,10 +36,9 @@
import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
-import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.views.OptionsPopupView;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -168,6 +166,7 @@
@Override
public void onLongPress(MotionEvent event) {
+ TestLogging.recordEvent("Workspace.longPress");
if (mLongPressState == STATE_REQUESTED) {
if (canHandleLongPress()) {
mLongPressState = STATE_PENDING_PARENT_INFORM;
@@ -178,9 +177,6 @@
mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.LONGPRESS,
Action.Direction.NONE, ContainerType.WORKSPACE,
mWorkspace.getCurrentPage());
- if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
- Log.d(TestProtocol.PERMANENT_DIAG_TAG, "Opening options popup on long press");
- }
OptionsPopupView.showDefaultOptions(mLauncher, mTouchDownPoint.x, mTouchDownPoint.y);
} else {
cancelLongPress();
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 6cae43d..3758cb8 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -35,6 +35,7 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.touch.ItemLongClickListener;
import com.android.launcher3.uioverrides.WallpaperColorInfo;
import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -90,6 +91,7 @@
@Override
public boolean onLongClick(View v) {
+ TestLogging.recordEvent("Widgets.onLongClick");
if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
if (v instanceof WidgetCell) {
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 2da6344..bf68f71 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -22,10 +22,15 @@
import androidx.test.uiautomator.BySelector;
import androidx.test.uiautomator.UiObject2;
+import java.util.regex.Pattern;
+
/**
* App icon, whether in all apps or in workspace/
*/
public final class AppIcon extends Launchable {
+
+ private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onAllAppsItemLongClick");
+
AppIcon(LauncherInstrumentation launcher, UiObject2 icon) {
super(launcher, icon);
}
@@ -43,6 +48,11 @@
}
@Override
+ protected void addExpectedEventsForLongClick() {
+ mLauncher.expectEvent(LONG_CLICK_EVENT);
+ }
+
+ @Override
protected String getLongPressIndicator() {
return "deep_shortcuts_container";
}
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java b/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
index ba9c10e..597be90 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
@@ -34,6 +34,10 @@
}
@Override
+ protected void addExpectedEventsForLongClick() {
+ }
+
+ @Override
protected String getLongPressIndicator() {
return "drop_target_bar";
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 6881197..9327cb3 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -69,18 +69,24 @@
* Drags an object to the center of homescreen.
*/
public void dragToWorkspace() {
- final Point launchableCenter = getObject().getVisibleCenter();
- final Point displaySize = mLauncher.getRealDisplaySize();
- final int width = displaySize.x / 2;
- Workspace.dragIconToWorkspace(
- mLauncher,
- this,
- new Point(
- launchableCenter.x >= width ?
- launchableCenter.x - width / 2 : launchableCenter.x + width / 2,
- displaySize.y / 2),
- getLongPressIndicator());
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+ final Point launchableCenter = getObject().getVisibleCenter();
+ final Point displaySize = mLauncher.getRealDisplaySize();
+ final int width = displaySize.x / 2;
+ addExpectedEventsForLongClick();
+ Workspace.dragIconToWorkspace(
+ mLauncher,
+ this,
+ new Point(
+ launchableCenter.x >= width
+ ? launchableCenter.x - width / 2
+ : launchableCenter.x + width / 2,
+ displaySize.y / 2),
+ getLongPressIndicator());
+ }
}
+ protected abstract void addExpectedEventsForLongClick();
+
protected abstract String getLongPressIndicator();
}
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index de6fdb1..ed577fd 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -79,6 +79,8 @@
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
@@ -92,6 +94,13 @@
private static final int GESTURE_STEP_MS = 16;
private static long START_TIME = System.currentTimeMillis();
+ static final Pattern LOG_TIME = Pattern.compile(
+ "[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]\\.[0-9][0-9][0-9]");
+
+ static final Pattern EVENT_LOG_ENTRY = Pattern.compile(
+ "(?<time>[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]\\.[0-9][0-9][0-9])"
+ + ".*" + TestProtocol.TAPL_EVENTS_TAG + ": (?<event>.*)");
+
// Types for launcher containers that the user is interacting with. "Background" is a
// pseudo-container corresponding to inactive launcher covered by another app.
public enum ContainerType {
@@ -146,6 +155,11 @@
private Consumer<ContainerType> mOnSettledStateAction;
+ // Not null when we are collecting expected events to compare with actual ones.
+ private List<Pattern> mExpectedEvents;
+
+ private String mTimeBeforeFirstLogEvent;
+
/**
* Constructs the root of TAPL hierarchy. You get all other objects from it.
*/
@@ -299,8 +313,11 @@
public void checkForAnomaly() {
final String anomalyMessage = getAnomalyMessage();
if (anomalyMessage != null) {
- failWithSystemHealth(
- "Tests are broken by a non-Launcher system error: " + anomalyMessage);
+ String message = "Tests are broken by a non-Launcher system error: " + anomalyMessage;
+ log("Hierarchy dump for: " + message);
+ dumpViewHierarchy();
+
+ Assert.fail(formatSystemHealthMessage(message));
}
}
@@ -339,7 +356,7 @@
mOnSettledStateAction = onSettledStateAction;
}
- private String getSystemHealthMessage() {
+ private String formatSystemHealthMessage(String message) {
final String testPackage = getContext().getPackageName();
mInstrumentation.getUiAutomation().grantRuntimePermission(
@@ -347,30 +364,34 @@
mInstrumentation.getUiAutomation().grantRuntimePermission(
testPackage, "android.permission.PACKAGE_USAGE_STATS");
- return mSystemHealthSupplier != null
+ final String systemHealth = mSystemHealthSupplier != null
? mSystemHealthSupplier.apply(START_TIME)
: TestHelpers.getSystemHealthMessage(getContext(), START_TIME);
+
+ if (systemHealth != null) {
+ return message
+ + ",\nperhaps linked to system health problems:\n<<<<<<<<<<<<<<<<<<\n"
+ + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
+ }
+
+ return message;
}
private void fail(String message) {
checkForAnomaly();
- failWithSystemHealth("http://go/tapl : " + getContextDescription() + message +
- " (visible state: " + getVisibleStateMessage() + ")");
- }
-
- private void failWithSystemHealth(String message) {
- final String systemHealth = getSystemHealthMessage();
- if (systemHealth != null) {
- message = message
- + ", perhaps because of system health problems:\n<<<<<<<<<<<<<<<<<<\n"
- + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
- }
-
+ message = "http://go/tapl : " + getContextDescription() + message
+ + " (visible state: " + getVisibleStateMessage() + ")";
log("Hierarchy dump for: " + message);
dumpViewHierarchy();
- Assert.fail(message);
+ final String eventMismatch = getEventMismatchMessage();
+
+ if (eventMismatch != null) {
+ message = message + ",\nhaving produced wrong events:\n " + eventMismatch;
+ }
+
+ Assert.fail(formatSystemHealthMessage(message));
}
private String getContextDescription() {
@@ -582,7 +603,7 @@
try (LauncherInstrumentation.Closable c = addContextLayer(action)) {
mDevice.waitForIdle();
runToState(
- () -> waitForSystemUiObject("home").click(),
+ waitForSystemUiObject("home")::click,
NORMAL_STATE_ORDINAL,
!hasLauncherObject(WORKSPACE_RES_ID)
&& (hasLauncherObject(APPS_RES_ID)
@@ -1099,4 +1120,104 @@
}
return tasks;
}
+
+ private List<String> getEvents() {
+ final ArrayList<String> events = new ArrayList<>();
+ try {
+ final String logcatTimeParameter =
+ mTimeBeforeFirstLogEvent != null ? " -t " + mTimeBeforeFirstLogEvent : "";
+ final String logcatEvents = mDevice.executeShellCommand(
+ "logcat -d --pid=" + getPid() + logcatTimeParameter
+ + " -s " + TestProtocol.TAPL_EVENTS_TAG);
+ final Matcher matcher = EVENT_LOG_ENTRY.matcher(logcatEvents);
+ while (matcher.find()) {
+ final String eventTime = matcher.group("time");
+ if (eventTime.equals(mTimeBeforeFirstLogEvent)) continue;
+
+ events.add(matcher.group("event"));
+ }
+ return events;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void startRecordingEvents() {
+ Assert.assertTrue("Already recording events", mExpectedEvents == null);
+ mExpectedEvents = new ArrayList<>();
+
+ try {
+ final String lastLogLine =
+ mDevice.executeShellCommand("logcat -d --pid=" + getPid() + " -t 1");
+ final Matcher matcher = LOG_TIME.matcher(lastLogLine);
+ mTimeBeforeFirstLogEvent = matcher.find() ? matcher.group().replaceAll(" ", "") : null;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void stopRecordingEvents() {
+ mExpectedEvents = null;
+ }
+
+ Closable eventsCheck() {
+ // Entering events check block.
+ startRecordingEvents();
+
+ return () -> {
+ // Leaving events check block.
+ if (mExpectedEvents == null) {
+ return; // There was a failure. Noo need to report another one.
+ }
+
+ // Wait until Launcher generates expected number of events.
+ final long endTime = SystemClock.uptimeMillis() + WAIT_TIME_MS;
+ while (SystemClock.uptimeMillis() < endTime
+ && getEvents().size() < mExpectedEvents.size()) {
+ SystemClock.sleep(100);
+ }
+
+ final String message = getEventMismatchMessage();
+ if (message != null) {
+ Assert.fail(formatSystemHealthMessage(
+ "http://go/tapl : unexpected event sequence: " + message));
+ }
+ };
+ }
+
+ void expectEvent(Pattern expected) {
+ if (mExpectedEvents != null) mExpectedEvents.add(expected);
+ }
+
+ private String getEventMismatchMessage() {
+ if (mExpectedEvents == null) return null;
+
+ try {
+ final List<String> actual = getEvents();
+
+ for (int i = 0; i < mExpectedEvents.size(); ++i) {
+ if (i >= actual.size()) {
+ return formatEventMismatchMessage("too few actual events", actual, i);
+ }
+ if (!mExpectedEvents.get(i).matcher(actual.get(i)).find()) {
+ return formatEventMismatchMessage("mismatched event", actual, i);
+ }
+ }
+
+ if (actual.size() > mExpectedEvents.size()) {
+ return formatEventMismatchMessage(
+ "too many actual events", actual, mExpectedEvents.size());
+ }
+ } finally {
+ stopRecordingEvents();
+ }
+
+ return null;
+ }
+
+ private String formatEventMismatchMessage(String message, List<String> actual, int position) {
+ return message + ", pos=" + position
+ + ", expected=" + mExpectedEvents
+ + ", actual" + actual;
+ }
}
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/Widget.java b/tests/tapl/com/android/launcher3/tapl/Widget.java
index 1b6d8c4..e0db16d 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widget.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widget.java
@@ -18,10 +18,15 @@
import androidx.test.uiautomator.UiObject2;
+import java.util.regex.Pattern;
+
/**
* Widget in workspace or a widget list.
*/
public final class Widget extends Launchable {
+
+ private static final Pattern LONG_CLICK_EVENT = Pattern.compile("Widgets.onLongClick");
+
Widget(LauncherInstrumentation launcher, UiObject2 icon) {
super(launcher, icon);
}
@@ -30,4 +35,9 @@
protected String getLongPressIndicator() {
return "drop_target_bar";
}
+
+ @Override
+ protected void addExpectedEventsForLongClick() {
+ mLauncher.expectEvent(LONG_CLICK_EVENT);
+ }
}