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 17 package com.android.textclassifier.common.statsd; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import android.app.Instrumentation; 22 import android.app.UiAutomation; 23 import android.os.ParcelFileDescriptor; 24 import android.util.Log; 25 import androidx.test.platform.app.InstrumentationRegistry; 26 import com.android.internal.os.StatsdConfigProto.AtomMatcher; 27 import com.android.internal.os.StatsdConfigProto.EventMetric; 28 import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher; 29 import com.android.internal.os.StatsdConfigProto.StatsdConfig; 30 import com.android.os.AtomsProto.Atom; 31 import com.android.os.StatsLog.AggregatedAtomInfo; 32 import com.android.os.StatsLog.ConfigMetricsReport; 33 import com.android.os.StatsLog.ConfigMetricsReportList; 34 import com.android.os.StatsLog.EventMetricData; 35 import com.android.os.StatsLog.StatsLogReport; 36 import com.google.common.collect.ImmutableList; 37 import com.google.common.io.ByteStreams; 38 import java.io.ByteArrayInputStream; 39 import java.io.FileInputStream; 40 import java.io.FileOutputStream; 41 import java.lang.reflect.Method; 42 import java.util.Comparator; 43 import java.util.List; 44 import java.util.stream.Collectors; 45 import javax.annotation.Nullable; 46 47 /** Util functions to make statsd testing easier by using adb shell cmd stats commands. */ 48 public class StatsdTestUtils { 49 private static final String TAG = "StatsdTestUtils"; 50 StatsdTestUtils()51 private StatsdTestUtils() {} 52 53 /** Push a config which specifies what loggings we are interested in. */ pushConfig(StatsdConfig config)54 public static void pushConfig(StatsdConfig config) throws Exception { 55 String command = String.format("cmd stats config update %s", config.getId()); 56 Log.v(TAG, "pushConfig: " + config); 57 String output = new String(runShellCommand(command, config.toByteArray())); 58 assertThat(output).isEmpty(); 59 } 60 61 /** Adds a atom matcher to capture logs with given atom tag. */ addAtomMatcher(StatsdConfig.Builder builder, int atomTag)62 public static void addAtomMatcher(StatsdConfig.Builder builder, int atomTag) { 63 final String atomName = "Atom" + atomTag; 64 final String eventName = "Event" + atomTag; 65 SimpleAtomMatcher simpleAtomMatcher = SimpleAtomMatcher.newBuilder().setAtomId(atomTag).build(); 66 builder.addAtomMatcher( 67 AtomMatcher.newBuilder() 68 .setId(atomName.hashCode()) 69 .setSimpleAtomMatcher(simpleAtomMatcher)); 70 builder.addEventMetric( 71 EventMetric.newBuilder().setId(eventName.hashCode()).setWhat(atomName.hashCode())); 72 } 73 74 /** 75 * Extracts logged atoms from the report, sorted by logging time, and deletes the saved report. 76 */ getLoggedAtoms(long configId, long timeoutMillis)77 public static ImmutableList<Atom> getLoggedAtoms(long configId, long timeoutMillis) 78 throws Exception { 79 // There is no callback to notify us the log is collected. So we do a wait here. 80 Thread.sleep(timeoutMillis); 81 82 ConfigMetricsReportList reportList = getAndRemoveReportList(configId); 83 assertThat(reportList.getReportsCount()).isEqualTo(1); 84 ConfigMetricsReport report = reportList.getReports(0); 85 List<StatsLogReport> metricsList = report.getMetricsList(); 86 return ImmutableList.copyOf( 87 metricsList.stream() 88 .flatMap(statsLogReport -> statsLogReport.getEventMetrics().getDataList().stream()) 89 .flatMap( 90 eventMetricData -> backfillAggregatedAtomsinEventMetric(eventMetricData).stream()) 91 .sorted(Comparator.comparing(EventMetricData::getElapsedTimestampNanos)) 92 .map(EventMetricData::getAtom) 93 .collect(Collectors.toList())); 94 } 95 96 /** Removes the pushed config file and existing reports. */ cleanup(long configId)97 public static void cleanup(long configId) throws Exception { 98 runShellCommand(String.format("cmd stats config remove %d", configId), /* input= */ null); 99 // Remove existing reports. 100 getAndRemoveReportList(configId); 101 } 102 103 /** 104 * Runs an adb shell command with the provided input and returns the command line output. 105 * 106 * @param cmd the shell command 107 * @param input the content that will be piped to the command stdin. 108 * @return the command output 109 */ runShellCommand(String cmd, @Nullable byte[] input)110 private static byte[] runShellCommand(String cmd, @Nullable byte[] input) throws Exception { 111 Log.v(TAG, "run shell command: " + cmd); 112 Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 113 UiAutomation uiAutomation = instrumentation.getUiAutomation(); 114 Method method = 115 uiAutomation.getClass().getDeclaredMethod("executeShellCommandRw", String.class); 116 ParcelFileDescriptor[] pipes = (ParcelFileDescriptor[]) method.invoke(uiAutomation, cmd); 117 // Write to the input pipe. 118 try (FileOutputStream fos = new ParcelFileDescriptor.AutoCloseOutputStream(pipes[1])) { 119 if (input != null) { 120 fos.write(input); 121 } 122 } 123 // Read from the output pipe. 124 try (FileInputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pipes[0])) { 125 return ByteStreams.toByteArray(inputStream); 126 } 127 } 128 129 /** Gets the statsd report. Note that this also deletes that report from statsd. */ getAndRemoveReportList(long configId)130 private static ConfigMetricsReportList getAndRemoveReportList(long configId) throws Exception { 131 byte[] output = 132 runShellCommand( 133 String.format("cmd stats dump-report %d --include_current_bucket --proto", configId), 134 /*input=*/ null); 135 return ConfigMetricsReportList.parser().parseFrom(new ByteArrayInputStream(output)); 136 } 137 backfillAggregatedAtomsinEventMetric( EventMetricData metricData)138 private static ImmutableList<EventMetricData> backfillAggregatedAtomsinEventMetric( 139 EventMetricData metricData) { 140 if (metricData.hasAtom()) { 141 return ImmutableList.of(metricData); 142 } 143 ImmutableList.Builder<EventMetricData> data = ImmutableList.builder(); 144 AggregatedAtomInfo atomInfo = metricData.getAggregatedAtomInfo(); 145 for (long timestamp : atomInfo.getElapsedTimestampNanosList()) { 146 EventMetricData.Builder newMetricData = EventMetricData.newBuilder(); 147 newMetricData.setAtom(atomInfo.getAtom()); 148 newMetricData.setElapsedTimestampNanos(timestamp); 149 data.add(newMetricData.build()); 150 } 151 return data.build(); 152 } 153 } 154