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