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