1 /*
2  * Copyright (C) 2018 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.cts.devicepolicy.metrics;
17 
18 import static junit.framework.Assert.assertTrue;
19 
20 import com.android.internal.os.StatsdConfigProto.AtomMatcher;
21 import com.android.internal.os.StatsdConfigProto.EventMetric;
22 import com.android.internal.os.StatsdConfigProto.FieldValueMatcher;
23 import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher;
24 import com.android.internal.os.StatsdConfigProto.StatsdConfig;
25 import com.android.os.AtomsProto.Atom;
26 import com.android.os.StatsLog.ConfigMetricsReport;
27 import com.android.os.StatsLog.ConfigMetricsReportList;
28 import com.android.os.StatsLog.EventMetricData;
29 import com.android.os.StatsLog.StatsLogReport;
30 import com.android.tradefed.device.CollectingByteOutputReceiver;
31 import com.android.tradefed.device.DeviceNotAvailableException;
32 import com.android.tradefed.device.ITestDevice;
33 import com.android.tradefed.log.LogUtil.CLog;
34 import com.google.common.io.Files;
35 import com.google.protobuf.InvalidProtocolBufferException;
36 import com.google.protobuf.MessageLite;
37 import com.google.protobuf.Parser;
38 import java.io.File;
39 import java.util.ArrayList;
40 import java.util.Comparator;
41 import java.util.List;
42 import java.util.function.Predicate;
43 import java.util.stream.Collectors;
44 
45 /**
46  * Tests Statsd atoms.
47  * <p/>
48  * Uploads statsd event configs, retrieves logs from host side and validates them
49  * against specified criteria.
50  */
51 class AtomMetricTester {
52     private static final String UPDATE_CONFIG_CMD = "cat %s | cmd stats config update %d";
53     private static final String DUMP_REPORT_CMD =
54             "cmd stats dump-report %d --include_current_bucket --proto";
55     private static final String REMOVE_CONFIG_CMD = "cmd stats config remove %d";
56     /** ID of the config, which evaluates to -1572883457. */
57     private static final long CONFIG_ID = "cts_config".hashCode();
58 
59     private final ITestDevice mDevice;
60 
AtomMetricTester(ITestDevice device)61     AtomMetricTester(ITestDevice device) {
62         mDevice = device;
63     }
64 
cleanLogs()65     void cleanLogs() throws Exception {
66         if (isStatsdDisabled()) {
67             return;
68         }
69         removeConfig(CONFIG_ID);
70         getReportList(); // Clears data.
71     }
72 
createConfigBuilder()73     private static StatsdConfig.Builder createConfigBuilder() {
74         return StatsdConfig.newBuilder().setId(CONFIG_ID)
75                 .addAllowedLogSource("AID_SYSTEM");
76     }
77 
createAndUploadConfig(int atomTag)78     void createAndUploadConfig(int atomTag) throws Exception {
79         StatsdConfig.Builder conf = createConfigBuilder();
80         addAtomEvent(conf, atomTag);
81         uploadConfig(conf);
82     }
83 
uploadConfig(StatsdConfig.Builder config)84     private void uploadConfig(StatsdConfig.Builder config) throws Exception {
85         uploadConfig(config.build());
86     }
87 
uploadConfig(StatsdConfig config)88     private void uploadConfig(StatsdConfig config) throws Exception {
89         CLog.d("Uploading the following config:\n" + config.toString());
90         File configFile = File.createTempFile("statsdconfig", ".config");
91         configFile.deleteOnExit();
92         Files.write(config.toByteArray(), configFile);
93         String remotePath = "/data/local/tmp/" + configFile.getName();
94         mDevice.pushFile(configFile, remotePath);
95         mDevice.executeShellCommand(String.format(UPDATE_CONFIG_CMD, remotePath, CONFIG_ID));
96         mDevice.executeShellCommand("rm " + remotePath);
97     }
98 
removeConfig(long configId)99     private void removeConfig(long configId) throws Exception {
100         mDevice.executeShellCommand(String.format(REMOVE_CONFIG_CMD, configId));
101     }
102 
103     /**
104      * Gets the statsd report and sorts it.
105      * Note that this also deletes that report from statsd.
106      */
getEventMetricDataList()107     List<EventMetricData> getEventMetricDataList() throws Exception {
108         ConfigMetricsReportList reportList = getReportList();
109         return getEventMetricDataList(reportList);
110     }
111 
112     /**
113      * Extracts and sorts the EventMetricData from the given ConfigMetricsReportList (which must
114      * contain a single report).
115      */
getEventMetricDataList(ConfigMetricsReportList reportList)116     private List<EventMetricData> getEventMetricDataList(ConfigMetricsReportList reportList)
117             throws Exception {
118         assertTrue("Expected one report", reportList.getReportsCount() == 1);
119         final ConfigMetricsReport report = reportList.getReports(0);
120         final List<StatsLogReport> metricsList = report.getMetricsList();
121         return metricsList.stream()
122                 .flatMap(statsLogReport -> statsLogReport.getEventMetrics().getDataList().stream())
123                 .sorted(Comparator.comparing(EventMetricData::getElapsedTimestampNanos))
124                 .peek(eventMetricData -> {
125                     CLog.d("Atom at " + eventMetricData.getElapsedTimestampNanos()
126                             + ":\n" + eventMetricData.getAtom().toString());
127                 })
128                 .collect(Collectors.toList());
129     }
130 
131     /** Gets the statsd report. Note that this also deletes that report from statsd. */
getReportList()132     private ConfigMetricsReportList getReportList() throws Exception {
133         try {
134             return getDump(ConfigMetricsReportList.parser(),
135                     String.format(DUMP_REPORT_CMD, CONFIG_ID));
136         } catch (com.google.protobuf.InvalidProtocolBufferException e) {
137             CLog.e("Failed to fetch and parse the statsd output report. "
138                     + "Perhaps there is not a valid statsd config for the requested "
139                     + "uid=" + getHostUid() + ", id=" + CONFIG_ID + ".");
140             throw (e);
141         }
142     }
143 
144     /** Creates a FieldValueMatcher.Builder corresponding to the given field. */
createFvm(int field)145     private static FieldValueMatcher.Builder createFvm(int field) {
146         return FieldValueMatcher.newBuilder().setField(field);
147     }
148 
addAtomEvent(StatsdConfig.Builder conf, int atomTag)149     private void addAtomEvent(StatsdConfig.Builder conf, int atomTag) throws Exception {
150         addAtomEvent(conf, atomTag, new ArrayList<FieldValueMatcher.Builder>());
151     }
152 
153     /**
154      * Adds an event to the config for an atom that matches the given keys.
155      *
156      * @param conf   configuration
157      * @param atomTag atom tag (from atoms.proto)
158      * @param fvms   list of FieldValueMatcher.Builders to attach to the atom. May be null.
159      */
addAtomEvent(StatsdConfig.Builder conf, int atomTag, List<FieldValueMatcher.Builder> fvms)160     private void addAtomEvent(StatsdConfig.Builder conf, int atomTag,
161             List<FieldValueMatcher.Builder> fvms) throws Exception {
162 
163         final String atomName = "Atom" + System.nanoTime();
164         final String eventName = "Event" + System.nanoTime();
165 
166         SimpleAtomMatcher.Builder sam = SimpleAtomMatcher.newBuilder().setAtomId(atomTag);
167         if (fvms != null) {
168             for (FieldValueMatcher.Builder fvm : fvms) {
169                 sam.addFieldValueMatcher(fvm);
170             }
171         }
172         conf.addAtomMatcher(AtomMatcher.newBuilder()
173                 .setId(atomName.hashCode())
174                 .setSimpleAtomMatcher(sam));
175         conf.addEventMetric(EventMetric.newBuilder()
176                 .setId(eventName.hashCode())
177                 .setWhat(atomName.hashCode()));
178     }
179 
180     /**
181      * Removes all elements from data prior to the first occurrence of an element for which
182      * the <code>atomMatcher</code> predicate returns <code>true</code>.
183      * After this method is called, the first element of data (if non-empty) is guaranteed to be
184      * an element in state.
185      *
186      * @param atomMatcher predicate that takes an Atom and returns <code>true</code> if it
187      * fits criteria.
188      */
dropWhileNot(List<EventMetricData> metricData, Predicate<Atom> atomMatcher)189     static void dropWhileNot(List<EventMetricData> metricData, Predicate<Atom> atomMatcher) {
190         int firstStateIdx;
191         for (firstStateIdx = 0; firstStateIdx < metricData.size(); firstStateIdx++) {
192             final Atom atom = metricData.get(firstStateIdx).getAtom();
193             if (atomMatcher.test(atom)) {
194                 break;
195             }
196         }
197         if (firstStateIdx == 0) {
198             // First first element already is in state, so there's nothing to do.
199             return;
200         }
201         metricData.subList(0, firstStateIdx).clear();
202     }
203 
204     /** Returns the UID of the host, which should always either be SHELL (2000) or ROOT (0). */
getHostUid()205     private int getHostUid() throws DeviceNotAvailableException {
206         String strUid = "";
207         try {
208             strUid = mDevice.executeShellCommand("id -u");
209             return Integer.parseInt(strUid.trim());
210         } catch (NumberFormatException e) {
211             CLog.e("Failed to get host's uid via shell command. Found " + strUid);
212             // Fall back to alternative method...
213             if (mDevice.isAdbRoot()) {
214                 return 0; // ROOT
215             } else {
216                 return 2000; // SHELL
217             }
218         }
219     }
220 
221     /**
222      * Execute a shell command on device and get the results of
223      * that as a proto of the given type.
224      *
225      * @param parser A protobuf parser object. e.g. MyProto.parser()
226      * @param command The adb shell command to run. e.g. "dumpsys fingerprint --proto"
227      *
228      * @throws DeviceNotAvailableException If there was a problem communicating with
229      *      the test device.
230      * @throws InvalidProtocolBufferException If there was an error parsing
231      *      the proto. Note that a 0 length buffer is not necessarily an error.
232      */
getDump(Parser<T> parser, String command)233     private <T extends MessageLite> T getDump(Parser<T> parser, String command)
234             throws DeviceNotAvailableException, InvalidProtocolBufferException {
235         final CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
236         mDevice.executeShellCommand(command, receiver);
237         return parser.parseFrom(receiver.getOutput());
238     }
239 
isStatsdDisabled()240     boolean isStatsdDisabled() throws DeviceNotAvailableException {
241         // if ro.statsd.enable doesn't exist, statsd is running by default.
242         if ("false".equals(mDevice.getProperty("ro.statsd.enable"))
243                 && "true".equals(mDevice.getProperty("ro.config.low_ram"))) {
244             CLog.d("Statsd is not enabled on the device");
245             return true;
246         }
247         return false;
248     }
249 }