• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.tapl;
17 
18 import static com.android.launcher3.testing.TestProtocol.SEQUENCE_MAIN;
19 import static com.android.launcher3.testing.TestProtocol.SEQUENCE_PILFER;
20 import static com.android.launcher3.testing.TestProtocol.SEQUENCE_TIS;
21 
22 import android.os.SystemClock;
23 
24 import com.android.launcher3.testing.TestProtocol;
25 
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.regex.Pattern;
31 
32 /**
33  * Utility class to verify expected events.
34  */
35 public class LogEventChecker {
36 
37     private final LauncherInstrumentation mLauncher;
38 
39     // Map from an event sequence name to an ordered list of expected events in that sequence.
40     private final ListMap<Pattern> mExpectedEvents = new ListMap<>();
41 
LogEventChecker(LauncherInstrumentation launcher)42     LogEventChecker(LauncherInstrumentation launcher) {
43         mLauncher = launcher;
44     }
45 
start()46     boolean start() {
47         mExpectedEvents.clear();
48         return mLauncher.getTestInfo(TestProtocol.REQUEST_START_EVENT_LOGGING) != null;
49     }
50 
expectPattern(String sequence, Pattern pattern)51     void expectPattern(String sequence, Pattern pattern) {
52         mExpectedEvents.add(sequence, pattern);
53     }
54 
55     // Waits for the expected number of events and returns them.
finishSync(long waitForExpectedCountMs)56     private ListMap<String> finishSync(long waitForExpectedCountMs) {
57         final long startTime = SystemClock.uptimeMillis();
58         // Event strings with '/' separating the sequence and the event.
59         ArrayList<String> rawEvents;
60 
61         while (true) {
62             rawEvents = mLauncher.getTestInfo(TestProtocol.REQUEST_GET_TEST_EVENTS)
63                     .getStringArrayList(TestProtocol.TEST_INFO_RESPONSE_FIELD);
64             if (rawEvents == null) return null;
65 
66             final int expectedCount = mExpectedEvents.entrySet()
67                     .stream().mapToInt(e -> e.getValue().size()).sum();
68             if (rawEvents.size() >= expectedCount
69                     || SystemClock.uptimeMillis() > startTime + waitForExpectedCountMs) {
70                 break;
71             }
72             SystemClock.sleep(100);
73         }
74 
75         finishNoWait();
76 
77         // Parse raw events into a map.
78         final ListMap<String> eventSequences = new ListMap<>();
79         for (String rawEvent : rawEvents) {
80             final String[] split = rawEvent.split("/");
81             eventSequences.add(split[0], split[1]);
82         }
83         return eventSequences;
84     }
85 
finishNoWait()86     void finishNoWait() {
87         mLauncher.getTestInfo(TestProtocol.REQUEST_STOP_EVENT_LOGGING);
88     }
89 
verify(long waitForExpectedCountMs, boolean successfulGesture)90     String verify(long waitForExpectedCountMs, boolean successfulGesture) {
91         final ListMap<String> actualEvents = finishSync(waitForExpectedCountMs);
92         if (actualEvents == null) return "null event sequences because launcher likely died";
93 
94         final String lowLevelDiags = lowLevelMismatchDiagnostics(actualEvents);
95         // If we have a sequence mismatch for a successful gesture, we want to provide all low-level
96         // details.
97         if (successfulGesture) {
98             return lowLevelDiags;
99         }
100 
101         final String sequenceMismatchInEnglish = highLevelMismatchDiagnostics(actualEvents);
102 
103         if (sequenceMismatchInEnglish != null) {
104             LauncherInstrumentation.log(lowLevelDiags);
105             return "Hint: " + sequenceMismatchInEnglish;
106         } else {
107             return lowLevelDiags;
108         }
109     }
110 
lowLevelMismatchDiagnostics(ListMap<String> actualEvents)111     private String lowLevelMismatchDiagnostics(ListMap<String> actualEvents) {
112         final StringBuilder sb = new StringBuilder();
113         boolean hasMismatches = false;
114         for (Map.Entry<String, List<Pattern>> expectedEvents : mExpectedEvents.entrySet()) {
115             String sequence = expectedEvents.getKey();
116 
117             List<String> actual = new ArrayList<>(actualEvents.getNonNull(sequence));
118             final int mismatchPosition = getMismatchPosition(expectedEvents.getValue(), actual);
119             hasMismatches = hasMismatches || mismatchPosition != -1;
120             formatSequenceWithMismatch(
121                     sb,
122                     sequence,
123                     expectedEvents.getValue(),
124                     actual,
125                     mismatchPosition);
126         }
127         // Check for unexpected event sequences in the actual data.
128         for (String actualNamedSequence : actualEvents.keySet()) {
129             if (!mExpectedEvents.containsKey(actualNamedSequence)) {
130                 hasMismatches = true;
131                 formatSequenceWithMismatch(
132                         sb,
133                         actualNamedSequence,
134                         new ArrayList<>(),
135                         actualEvents.get(actualNamedSequence),
136                         0);
137             }
138         }
139 
140         return hasMismatches ? "Mismatching events: " + sb.toString() : null;
141     }
142 
highLevelMismatchDiagnostics(ListMap<String> actualEvents)143     private String highLevelMismatchDiagnostics(ListMap<String> actualEvents) {
144         if (!mExpectedEvents.getNonNull(SEQUENCE_TIS).isEmpty()
145                 && actualEvents.getNonNull(SEQUENCE_TIS).isEmpty()) {
146             return "TouchInteractionService didn't receive any of the touch events sent by the "
147                     + "test";
148         }
149         if (getMismatchPosition(mExpectedEvents.getNonNull(SEQUENCE_TIS),
150                 actualEvents.getNonNull(SEQUENCE_TIS)) != -1) {
151             // If TIS has a mismatch that we can't convert to high-level diags, don't convert
152             // other sequences either.
153             return null;
154         }
155 
156         if (mExpectedEvents.getNonNull(SEQUENCE_PILFER).size() == 1
157                 && actualEvents.getNonNull(SEQUENCE_PILFER).isEmpty()) {
158             return "Launcher didn't detect the navigation gesture sent by the test";
159         }
160         if (mExpectedEvents.getNonNull(SEQUENCE_PILFER).isEmpty()
161                 && actualEvents.getNonNull(SEQUENCE_PILFER).size() == 1) {
162             return "Launcher detected a navigation gesture, but the test didn't send one";
163         }
164         if (getMismatchPosition(mExpectedEvents.getNonNull(SEQUENCE_PILFER),
165                 actualEvents.getNonNull(SEQUENCE_PILFER)) != -1) {
166             // If Pilfer has a mismatch that we can't convert to high-level diags, don't analyze
167             // other sequences.
168             return null;
169         }
170 
171         if (!mExpectedEvents.getNonNull(SEQUENCE_MAIN).isEmpty()
172                 && actualEvents.getNonNull(SEQUENCE_MAIN).isEmpty()) {
173             return "None of the touch or keyboard events sent by the test was received by "
174                     + "Launcher's main thread";
175         }
176         return null;
177     }
178 
179     // If the list of actual events matches the list of expected events, returns -1, otherwise
180     // the position of the mismatch.
getMismatchPosition(List<Pattern> expected, List<String> actual)181     private static int getMismatchPosition(List<Pattern> expected, List<String> actual) {
182         for (int i = 0; i < expected.size(); ++i) {
183             if (i >= actual.size()
184                     || !expected.get(i).matcher(actual.get(i)).find()) {
185                 return i;
186             }
187         }
188 
189         if (actual.size() > expected.size()) return expected.size();
190 
191         return -1;
192     }
193 
formatSequenceWithMismatch( StringBuilder sb, String sequenceName, List<Pattern> expected, List<String> actualEvents, int mismatchPosition)194     private static void formatSequenceWithMismatch(
195             StringBuilder sb,
196             String sequenceName,
197             List<Pattern> expected,
198             List<String> actualEvents,
199             int mismatchPosition) {
200         sb.append("\n>> SEQUENCE " + sequenceName + " - "
201                 + (mismatchPosition == -1 ? "MATCH" : "MISMATCH"));
202         sb.append("\n  EXPECTED:");
203         formatEventListWithMismatch(sb, expected, mismatchPosition);
204         sb.append("\n  ACTUAL:");
205         formatEventListWithMismatch(sb, actualEvents, mismatchPosition);
206     }
207 
formatEventListWithMismatch(StringBuilder sb, List events, int position)208     private static void formatEventListWithMismatch(StringBuilder sb, List events, int position) {
209         for (int i = 0; i < events.size(); ++i) {
210             sb.append("\n  | ");
211             sb.append(i == position ? "---> " : "     ");
212             sb.append(events.get(i).toString());
213         }
214         if (position == events.size()) sb.append("\n  | ---> (end)");
215     }
216 
217     private static class ListMap<T> extends HashMap<String, List<T>> {
218 
add(String key, T value)219         void add(String key, T value) {
220             getNonNull(key).add(value);
221         }
222 
getNonNull(String key)223         List<T> getNonNull(String key) {
224             List<T> list = get(key);
225             if (list == null) {
226                 list = new ArrayList<>();
227                 put(key, list);
228             }
229             return list;
230         }
231     }
232 }
233