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