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         removeConfig(CONFIG_ID);
67         getReportList(); // Clears data.
68     }
69 
createConfigBuilder()70     private static StatsdConfig.Builder createConfigBuilder() {
71         return StatsdConfig.newBuilder().setId(CONFIG_ID)
72                 .addAllowedLogSource("AID_SYSTEM");
73     }
74 
createAndUploadConfig(int atomTag)75     void createAndUploadConfig(int atomTag) throws Exception {
76         StatsdConfig.Builder conf = createConfigBuilder();
77         addAtomEvent(conf, atomTag);
78         uploadConfig(conf);
79     }
80 
uploadConfig(StatsdConfig.Builder config)81     private void uploadConfig(StatsdConfig.Builder config) throws Exception {
82         uploadConfig(config.build());
83     }
84 
uploadConfig(StatsdConfig config)85     private void uploadConfig(StatsdConfig config) throws Exception {
86         CLog.d("Uploading the following config:\n" + config.toString());
87         File configFile = File.createTempFile("statsdconfig", ".config");
88         configFile.deleteOnExit();
89         Files.write(config.toByteArray(), configFile);
90         String remotePath = "/data/local/tmp/" + configFile.getName();
91         mDevice.pushFile(configFile, remotePath);
92         mDevice.executeShellCommand(String.format(UPDATE_CONFIG_CMD, remotePath, CONFIG_ID));
93         mDevice.executeShellCommand("rm " + remotePath);
94     }
95 
removeConfig(long configId)96     private void removeConfig(long configId) throws Exception {
97         mDevice.executeShellCommand(String.format(REMOVE_CONFIG_CMD, configId));
98     }
99 
100     /**
101      * Gets the statsd report and sorts it.
102      * Note that this also deletes that report from statsd.
103      */
getEventMetricDataList()104     List<EventMetricData> getEventMetricDataList() throws Exception {
105         ConfigMetricsReportList reportList = getReportList();
106         return getEventMetricDataList(reportList);
107     }
108 
109     /**
110      * Extracts and sorts the EventMetricData from the given ConfigMetricsReportList (which must
111      * contain a single report).
112      */
getEventMetricDataList(ConfigMetricsReportList reportList)113     private List<EventMetricData> getEventMetricDataList(ConfigMetricsReportList reportList)
114             throws Exception {
115         assertTrue("Expected one report", reportList.getReportsCount() == 1);
116         final ConfigMetricsReport report = reportList.getReports(0);
117         final List<StatsLogReport> metricsList = report.getMetricsList();
118         return metricsList.stream()
119                 .flatMap(statsLogReport -> statsLogReport.getEventMetrics().getDataList().stream())
120                 .sorted(Comparator.comparing(EventMetricData::getElapsedTimestampNanos))
121                 .peek(eventMetricData -> {
122                     CLog.d("Atom at " + eventMetricData.getElapsedTimestampNanos()
123                             + ":\n" + eventMetricData.getAtom().toString());
124                 })
125                 .collect(Collectors.toList());
126     }
127 
128     /** Gets the statsd report. Note that this also deletes that report from statsd. */
getReportList()129     private ConfigMetricsReportList getReportList() throws Exception {
130         try {
131             return getDump(ConfigMetricsReportList.parser(),
132                     String.format(DUMP_REPORT_CMD, CONFIG_ID));
133         } catch (com.google.protobuf.InvalidProtocolBufferException e) {
134             CLog.e("Failed to fetch and parse the statsd output report. "
135                     + "Perhaps there is not a valid statsd config for the requested "
136                     + "uid=" + getHostUid() + ", id=" + CONFIG_ID + ".");
137             throw (e);
138         }
139     }
140 
141     /** Creates a FieldValueMatcher.Builder corresponding to the given field. */
createFvm(int field)142     private static FieldValueMatcher.Builder createFvm(int field) {
143         return FieldValueMatcher.newBuilder().setField(field);
144     }
145 
addAtomEvent(StatsdConfig.Builder conf, int atomTag)146     private void addAtomEvent(StatsdConfig.Builder conf, int atomTag) throws Exception {
147         addAtomEvent(conf, atomTag, new ArrayList<FieldValueMatcher.Builder>());
148     }
149 
150     /**
151      * Adds an event to the config for an atom that matches the given keys.
152      *
153      * @param conf   configuration
154      * @param atomTag atom tag (from atoms.proto)
155      * @param fvms   list of FieldValueMatcher.Builders to attach to the atom. May be null.
156      */
addAtomEvent(StatsdConfig.Builder conf, int atomTag, List<FieldValueMatcher.Builder> fvms)157     private void addAtomEvent(StatsdConfig.Builder conf, int atomTag,
158             List<FieldValueMatcher.Builder> fvms) throws Exception {
159 
160         final String atomName = "Atom" + System.nanoTime();
161         final String eventName = "Event" + System.nanoTime();
162 
163         SimpleAtomMatcher.Builder sam = SimpleAtomMatcher.newBuilder().setAtomId(atomTag);
164         if (fvms != null) {
165             for (FieldValueMatcher.Builder fvm : fvms) {
166                 sam.addFieldValueMatcher(fvm);
167             }
168         }
169         conf.addAtomMatcher(AtomMatcher.newBuilder()
170                 .setId(atomName.hashCode())
171                 .setSimpleAtomMatcher(sam));
172         conf.addEventMetric(EventMetric.newBuilder()
173                 .setId(eventName.hashCode())
174                 .setWhat(atomName.hashCode()));
175     }
176 
177     /**
178      * Removes all elements from data prior to the first occurrence of an element for which
179      * the <code>atomMatcher</code> predicate returns <code>true</code>.
180      * After this method is called, the first element of data (if non-empty) is guaranteed to be
181      * an element in state.
182      *
183      * @param atomMatcher predicate that takes an Atom and returns <code>true</code> if it
184      * fits criteria.
185      */
dropWhileNot(List<EventMetricData> metricData, Predicate<Atom> atomMatcher)186     static void dropWhileNot(List<EventMetricData> metricData, Predicate<Atom> atomMatcher) {
187         int firstStateIdx;
188         for (firstStateIdx = 0; firstStateIdx < metricData.size(); firstStateIdx++) {
189             final Atom atom = metricData.get(firstStateIdx).getAtom();
190             if (atomMatcher.test(atom)) {
191                 break;
192             }
193         }
194         if (firstStateIdx == 0) {
195             // First first element already is in state, so there's nothing to do.
196             return;
197         }
198         metricData.subList(0, firstStateIdx).clear();
199     }
200 
201     /** Returns the UID of the host, which should always either be SHELL (2000) or ROOT (0). */
getHostUid()202     private int getHostUid() throws DeviceNotAvailableException {
203         String strUid = "";
204         try {
205             strUid = mDevice.executeShellCommand("id -u");
206             return Integer.parseInt(strUid.trim());
207         } catch (NumberFormatException e) {
208             CLog.e("Failed to get host's uid via shell command. Found " + strUid);
209             // Fall back to alternative method...
210             if (mDevice.isAdbRoot()) {
211                 return 0; // ROOT
212             } else {
213                 return 2000; // SHELL
214             }
215         }
216     }
217 
218     /**
219      * Execute a shell command on device and get the results of
220      * that as a proto of the given type.
221      *
222      * @param parser A protobuf parser object. e.g. MyProto.parser()
223      * @param command The adb shell command to run. e.g. "dumpsys fingerprint --proto"
224      *
225      * @throws DeviceNotAvailableException If there was a problem communicating with
226      *      the test device.
227      * @throws InvalidProtocolBufferException If there was an error parsing
228      *      the proto. Note that a 0 length buffer is not necessarily an error.
229      */
getDump(Parser<T> parser, String command)230     private <T extends MessageLite> T getDump(Parser<T> parser, String command)
231             throws DeviceNotAvailableException, InvalidProtocolBufferException {
232         final CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
233         mDevice.executeShellCommand(command, receiver);
234         return parser.parseFrom(receiver.getOutput());
235     }
236 }
237