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 android.os.SystemClock; 19 20 import com.android.launcher3.testing.shared.TestProtocol; 21 22 import java.util.ArrayList; 23 import java.util.HashMap; 24 import java.util.List; 25 import java.util.Map; 26 import java.util.regex.Pattern; 27 28 /** 29 * Utility class to verify expected events. 30 */ 31 public class LogEventChecker { 32 33 private final LauncherInstrumentation mLauncher; 34 35 // Map from an event sequence name to an ordered list of expected events in that sequence. 36 private final ListMap<Pattern> mExpectedEvents = new ListMap<>(); 37 38 private LogExclusionRule mLogExclusionRule = null; 39 LogEventChecker(LauncherInstrumentation launcher)40 LogEventChecker(LauncherInstrumentation launcher) { 41 mLauncher = launcher; 42 } 43 start()44 boolean start() { 45 mExpectedEvents.clear(); 46 return mLauncher.getTestInfo(TestProtocol.REQUEST_START_EVENT_LOGGING) != null; 47 } 48 expectPattern(String sequence, Pattern pattern)49 void expectPattern(String sequence, Pattern pattern) { 50 mExpectedEvents.add(sequence, pattern); 51 } 52 setLogExclusionRule(LogExclusionRule logExclusionRule)53 void setLogExclusionRule(LogExclusionRule logExclusionRule) { 54 mLogExclusionRule = logExclusionRule; 55 } 56 57 // Waits for the expected number of events and returns them. finishSync(long waitForExpectedCountMs)58 private ListMap<String> finishSync(long waitForExpectedCountMs) { 59 final long startTime = SystemClock.uptimeMillis(); 60 // Event strings with '/' separating the sequence and the event. 61 ArrayList<String> rawEvents; 62 63 while (true) { 64 rawEvents = mLauncher.getTestInfo(TestProtocol.REQUEST_GET_TEST_EVENTS) 65 .getStringArrayList(TestProtocol.TEST_INFO_RESPONSE_FIELD); 66 if (rawEvents == null) return null; 67 68 final int expectedCount = mExpectedEvents.entrySet() 69 .stream().mapToInt(e -> e.getValue().size()).sum(); 70 if (rawEvents.size() >= expectedCount 71 || SystemClock.uptimeMillis() > startTime + waitForExpectedCountMs) { 72 break; 73 } 74 SystemClock.sleep(100); 75 } 76 77 finishNoWait(); 78 79 // Parse raw events into a map. 80 final ListMap<String> eventSequences = new ListMap<>(); 81 for (String rawEvent : rawEvents) { 82 final String[] split = rawEvent.split("/"); 83 if (mLogExclusionRule == null || !mLogExclusionRule.shouldExclude(split[1])) { 84 eventSequences.add(split[0], split[1]); 85 } 86 } 87 return eventSequences; 88 } 89 finishNoWait()90 void finishNoWait() { 91 mLauncher.getTestInfo(TestProtocol.REQUEST_STOP_EVENT_LOGGING); 92 } 93 verify(long waitForExpectedCountMs)94 String verify(long waitForExpectedCountMs) { 95 final ListMap<String> actualEvents = finishSync(waitForExpectedCountMs); 96 if (actualEvents == null) return "null event sequences because launcher likely died"; 97 98 return lowLevelMismatchDiagnostics(actualEvents); 99 } 100 lowLevelMismatchDiagnostics(ListMap<String> actualEvents)101 private String lowLevelMismatchDiagnostics(ListMap<String> actualEvents) { 102 final StringBuilder sb = new StringBuilder(); 103 boolean hasMismatches = false; 104 for (Map.Entry<String, List<Pattern>> expectedEvents : mExpectedEvents.entrySet()) { 105 String sequence = expectedEvents.getKey(); 106 107 List<String> actual = new ArrayList<>(actualEvents.getNonNull(sequence)); 108 final int mismatchPosition = getMismatchPosition(expectedEvents.getValue(), actual); 109 hasMismatches = hasMismatches || mismatchPosition != -1; 110 formatSequenceWithMismatch( 111 sb, 112 sequence, 113 expectedEvents.getValue(), 114 actual, 115 mismatchPosition); 116 } 117 // Check for unexpected event sequences in the actual data. 118 for (String actualNamedSequence : actualEvents.keySet()) { 119 if (!mExpectedEvents.containsKey(actualNamedSequence)) { 120 hasMismatches = true; 121 formatSequenceWithMismatch( 122 sb, 123 actualNamedSequence, 124 new ArrayList<>(), 125 actualEvents.get(actualNamedSequence), 126 0); 127 } 128 } 129 130 return hasMismatches ? "Mismatching events: " + sb.toString() : null; 131 } 132 133 // If the list of actual events matches the list of expected events, returns -1, otherwise 134 // the position of the mismatch. getMismatchPosition(List<Pattern> expected, List<String> actual)135 private static int getMismatchPosition(List<Pattern> expected, List<String> actual) { 136 for (int i = 0; i < expected.size(); ++i) { 137 if (i >= actual.size() 138 || !expected.get(i).matcher(actual.get(i)).find()) { 139 return i; 140 } 141 } 142 143 if (actual.size() > expected.size()) return expected.size(); 144 145 return -1; 146 } 147 formatSequenceWithMismatch( StringBuilder sb, String sequenceName, List<Pattern> expected, List<String> actualEvents, int mismatchPosition)148 private static void formatSequenceWithMismatch( 149 StringBuilder sb, 150 String sequenceName, 151 List<Pattern> expected, 152 List<String> actualEvents, 153 int mismatchPosition) { 154 sb.append("\n>> SEQUENCE " + sequenceName + " - " 155 + (mismatchPosition == -1 ? "MATCH" : "MISMATCH")); 156 sb.append("\n EXPECTED:"); 157 formatEventListWithMismatch(sb, expected, mismatchPosition); 158 sb.append("\n ACTUAL:"); 159 formatEventListWithMismatch(sb, actualEvents, mismatchPosition); 160 } 161 formatEventListWithMismatch(StringBuilder sb, List events, int position)162 private static void formatEventListWithMismatch(StringBuilder sb, List events, int position) { 163 for (int i = 0; i < events.size(); ++i) { 164 sb.append("\n | "); 165 sb.append(i == position ? "---> " : " "); 166 sb.append(events.get(i).toString()); 167 } 168 if (position == events.size()) sb.append("\n | ---> (end)"); 169 } 170 171 private static class ListMap<T> extends HashMap<String, List<T>> { 172 add(String key, T value)173 void add(String key, T value) { 174 getNonNull(key).add(value); 175 } 176 getNonNull(String key)177 List<T> getNonNull(String key) { 178 List<T> list = get(key); 179 if (list == null) { 180 list = new ArrayList<>(); 181 put(key, list); 182 } 183 return list; 184 } 185 } 186 187 interface LogExclusionRule { shouldExclude(String event)188 boolean shouldExclude(String event); 189 } 190 } 191