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