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 
17 package android.cts.statsdatom.lib;
18 
19 import static com.google.common.truth.Truth.assertWithMessage;
20 
21 import com.android.os.AtomsProto;
22 import com.android.os.AtomsProto.AppBreadcrumbReported;
23 import com.android.os.StatsLog;
24 import com.android.tradefed.device.DeviceNotAvailableException;
25 import com.android.tradefed.device.ITestDevice;
26 import com.android.tradefed.log.LogUtil;
27 import com.android.utils.SparseIntArray;
28 
29 import com.google.common.collect.Range;
30 
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.Locale;
34 import java.util.Set;
35 import java.util.function.Function;
36 
37 /**
38  * Contains miscellaneous helper functions that are used in statsd atom tests
39  */
40 public final class AtomTestUtils {
41 
42     public static final int WAIT_TIME_SHORT = 500;
43     public static final int WAIT_TIME_LONG = 1000;
44 
45     public static final long NS_PER_SEC = (long) 1E+9;
46 
47     private static int sShellUid = 2000;
48 
49     /**
50      * Sends an AppBreadcrumbReported atom to statsd. For GaugeMetrics that are added using
51      * ConfigUtils, pulls are triggered when statsd receives an AppBreadcrumbReported atom, so
52      * calling this function is necessary for gauge data to be acquired.
53      *
54      * @param device test device can be retrieved using getDevice()
55      */
sendAppBreadcrumbReportedAtom(ITestDevice device)56     public static void sendAppBreadcrumbReportedAtom(ITestDevice device)
57             throws DeviceNotAvailableException {
58         sendAppBreadcrumbReportedAtom(device,
59                 AppBreadcrumbReported.State.START.ordinal(), /*label=*/ 1);
60     }
61 
62     /**
63      * Sends an AppBreadcrumbReported atom to statsd. For GaugeMetrics that are added using
64      * ConfigUtils, pulls are triggered when statsd receives an AppBreadcrumbReported atom, so
65      * calling this function is necessary for gauge data to be acquired.
66      *
67      * @param device test device can be retrieved using getDevice()
68      * @param state  the breadcrumb atom state field
69      * @param label  the breadcrumb atom label field
70      */
sendAppBreadcrumbReportedAtom(ITestDevice device, int state, int label)71     public static void sendAppBreadcrumbReportedAtom(ITestDevice device, int state, int label)
72             throws DeviceNotAvailableException {
73         String cmd = String.format(Locale.US, "cmd stats log-app-breadcrumb %d %d %d",
74                 sShellUid, label,
75                 state);
76         device.executeShellCommand(cmd);
77     }
78 
79 
80     /**
81      * Asserts that each set of states in {@code stateSets} occurs in {@code data} without assuming
82      * the order of occurrence.
83      *
84      * @param stateSets        A list of set of states, where each set represents an equivalent
85      *                         state of the device for the purpose of CTS.
86      * @param data             list of EventMetricData from statsd, produced by
87      *                         getReportMetricListData()
88      * @param getStateFromAtom expression that takes in an Atom and returns the state it contains
89      */
assertStatesOccurred(List<Set<Integer>> stateSets, List<StatsLog.EventMetricData> data, Function<AtomsProto.Atom, Integer> getStateFromAtom)90     public static void assertStatesOccurred(List<Set<Integer>> stateSets,
91             List<StatsLog.EventMetricData> data,
92             Function<AtomsProto.Atom, Integer> getStateFromAtom) {
93         // Sometimes, there are more events than there are states.
94         // Eg: When the screen turns off, it may go into OFF and then DOZE immediately.
95         final SparseIntArray dataStateCount = new SparseIntArray();
96         final List<Integer> dataStates = new ArrayList<>(data.size());
97         for (StatsLog.EventMetricData emd : data) {
98             final int state = getStateFromAtom.apply(emd.getAtom());
99             dataStates.add(state);
100             dataStateCount.put(state, dataStateCount.get(state, 0) + 1);
101         }
102 
103         assertWithMessage("Number of recorded states must be >= expected states"
104                 + " (dataStates=%s, stateSets=%s)", dataStates, stateSets)
105                 .that(data.size()).isAtLeast(stateSets.size());
106 
107         for (Set<Integer> states : stateSets) {
108             for (int state : states) {
109                 final int count = dataStateCount.get(state);
110                 assertWithMessage("Remaining count of result state (%s)", state)
111                         .that(count).isGreaterThan(0);
112                 dataStateCount.put(state, count - 1);
113             }
114         }
115     }
116 
117     /**
118      * Asserts that each set of states in stateSets occurs at least once in data.
119      * Asserts that the states in data occur in the same order as the sets in stateSets.
120      *
121      * @param stateSets        A list of set of states, where each set represents an equivalent
122      *                         state of the device for the purpose of CTS.
123      * @param data             list of EventMetricData from statsd, produced by
124      *                         getReportMetricListData()
125      * @param wait             expected duration (in ms) between state changes; asserts that the
126      *                         actual wait
127      *                         time was wait/2 <= actual_wait <= 5*wait. Use 0 to ignore this
128      *                         assertion.
129      * @param getStateFromAtom expression that takes in an Atom and returns the state it contains
130      */
assertStatesOccurredInOrder(List<Set<Integer>> stateSets, List<StatsLog.EventMetricData> data, int wait, Function<AtomsProto.Atom, Integer> getStateFromAtom)131     public static void assertStatesOccurredInOrder(List<Set<Integer>> stateSets,
132             List<StatsLog.EventMetricData> data,
133             int wait, Function<AtomsProto.Atom, Integer> getStateFromAtom) {
134         // Sometimes, there are more events than there are states.
135         // Eg: When the screen turns off, it may go into OFF and then DOZE immediately.
136         assertWithMessage("Number of result states").that(data.size()).isAtLeast(stateSets.size());
137         int stateSetIndex = 0; // Tracks which state set we expect the data to be in.
138         for (int dataIndex = 0; dataIndex < data.size(); dataIndex++) {
139             AtomsProto.Atom atom = data.get(dataIndex).getAtom();
140             int state = getStateFromAtom.apply(atom);
141             // If state is in the current state set, we do not assert anything.
142             // If it is not, we expect to have transitioned to the next state set.
143             if (stateSets.get(stateSetIndex).contains(state)) {
144                 // No need to assert anything. Just log it.
145                 LogUtil.CLog.i("The following atom at dataIndex=" + dataIndex + " is "
146                         + "in stateSetIndex " + stateSetIndex + ":\n"
147                         + data.get(dataIndex).getAtom().toString());
148             } else {
149                 stateSetIndex += 1;
150                 LogUtil.CLog.i("Assert that the following atom at dataIndex=" + dataIndex + " is"
151                         + " in stateSetIndex " + stateSetIndex + ":\n"
152                         + data.get(dataIndex).getAtom().toString());
153                 assertWithMessage("Missed first state").that(dataIndex).isNotEqualTo(0);
154                 assertWithMessage("Too many states").that(stateSetIndex)
155                         .isLessThan(stateSets.size());
156                 assertWithMessage(String.format("Is in wrong state (%d)", state))
157                         .that(stateSets.get(stateSetIndex)).contains(state);
158                 if (wait > 0) {
159                     assertTimeDiffBetween(data.get(dataIndex - 1), data.get(dataIndex),
160                             wait / 2, wait * 5);
161                 }
162             }
163         }
164         assertWithMessage("Too few states").that(stateSetIndex).isEqualTo(stateSets.size() - 1);
165     }
166 
167     /**
168      * Asserts that the two events are within the specified range of each other.
169      *
170      * @param d0        the event that should occur first
171      * @param d1        the event that should occur second
172      * @param minDiffMs d0 should precede d1 by at least this amount
173      * @param maxDiffMs d0 should precede d1 by at most this amount
174      */
assertTimeDiffBetween( StatsLog.EventMetricData d0, StatsLog.EventMetricData d1, int minDiffMs, int maxDiffMs)175     public static void assertTimeDiffBetween(
176             StatsLog.EventMetricData d0, StatsLog.EventMetricData d1,
177             int minDiffMs, int maxDiffMs) {
178         long diffMs = (d1.getElapsedTimestampNanos() - d0.getElapsedTimestampNanos()) / 1_000_000;
179         assertWithMessage("Illegal time difference")
180                 .that(diffMs).isIn(Range.closed((long) minDiffMs, (long) maxDiffMs));
181     }
182 
183     // Checks that a timestamp has been truncated to be a multiple of 5 min
assertTimestampIsTruncated(long timestampNs)184     public static void assertTimestampIsTruncated(long timestampNs) {
185         long fiveMinutesInNs = NS_PER_SEC * 5 * 60;
186         assertWithMessage("Timestamp is not truncated")
187                 .that(timestampNs % fiveMinutesInNs).isEqualTo(0);
188     }
189 
190     /**
191      * Removes all elements from data prior to the first occurrence of an element of state. After
192      * this method is called, the first element of data (if non-empty) is guaranteed to be an
193      * element in state.
194      *
195      * @param getStateFromAtom expression that takes in an Atom and returns the state it contains
196      */
popUntilFind(List<StatsLog.EventMetricData> data, Set<Integer> state, Function<AtomsProto.Atom, Integer> getStateFromAtom)197     public static void popUntilFind(List<StatsLog.EventMetricData> data, Set<Integer> state,
198             Function<AtomsProto.Atom, Integer> getStateFromAtom) {
199         int firstStateIdx;
200         for (firstStateIdx = 0; firstStateIdx < data.size(); firstStateIdx++) {
201             AtomsProto.Atom atom = data.get(firstStateIdx).getAtom();
202             if (state.contains(getStateFromAtom.apply(atom))) {
203                 break;
204             }
205         }
206         if (firstStateIdx == 0) {
207             // First first element already is in state, so there's nothing to do.
208             return;
209         }
210         data.subList(0, firstStateIdx).clear();
211     }
212 
213     /**
214      * Removes all elements from data after the last occurrence of an element of state. After this
215      * method is called, the last element of data (if non-empty) is guaranteed to be an element in
216      * state.
217      *
218      * @param getStateFromAtom expression that takes in an Atom and returns the state it contains
219      */
popUntilFindFromEnd(List<StatsLog.EventMetricData> data, Set<Integer> state, Function<AtomsProto.Atom, Integer> getStateFromAtom)220     public static void popUntilFindFromEnd(List<StatsLog.EventMetricData> data, Set<Integer> state,
221             Function<AtomsProto.Atom, Integer> getStateFromAtom) {
222         int lastStateIdx;
223         for (lastStateIdx = data.size() - 1; lastStateIdx >= 0; lastStateIdx--) {
224             AtomsProto.Atom atom = data.get(lastStateIdx).getAtom();
225             if (state.contains(getStateFromAtom.apply(atom))) {
226                 break;
227             }
228         }
229         if (lastStateIdx == data.size() - 1) {
230             // Last element already is in state, so there's nothing to do.
231             return;
232         }
233         data.subList(lastStateIdx + 1, data.size()).clear();
234     }
235 
AtomTestUtils()236     private AtomTestUtils() {
237     }
238 }
239