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