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