Added collector for runtime restarts.
Bug: 115906683
Test: RuntimeRestartCollectorTest.java(added); MetricUtilTest.java
(updated)
Change-Id: Iaa111663d54a26e20f508dece1eae817bd09e11a
diff --git a/src/com/android/tradefed/device/metric/RuntimeRestartCollector.java b/src/com/android/tradefed/device/metric/RuntimeRestartCollector.java
new file mode 100644
index 0000000..16e28c1
--- /dev/null
+++ b/src/com/android/tradefed/device/metric/RuntimeRestartCollector.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.device.metric;
+
+import static com.android.tradefed.util.proto.TfMetricProtoUtil.stringToMetric;
+
+import com.android.os.AtomsProto.Atom;
+import com.android.os.StatsLog.EventMetricData;
+import com.android.os.StatsLog.StatsdStatsReport;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.util.statsd.ConfigUtil;
+import com.android.tradefed.util.statsd.MetricUtil;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Iterables;
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * Collector that collects timestamps of runtime restarts (system server crashes) during the test
+ * run, if any.
+ *
+ * <p>Outputs results in counts, wall clock time in seconds and in HH:mm:ss format, and system
+ * uptime in nanoseconds and HH:mm:ss format.
+ *
+ * <p>This collector uses two sources for system server crashes:
+ *
+ * <p>
+ *
+ * <ol>
+ * <li>The system_restart_sec list from StatsdStatsReport, which is a rolling list of 20
+ * timestamps when the system server crashes, in seconds, with newer crashes appended to the
+ * end (when the list fills up, older timestamps fall off the beginning).
+ * <li>The AppCrashOccurred statsd atom, where a system server crash shows up as a system_server
+ * process crash (this behavior is documented in the statsd atoms.proto definition). The event
+ * metric gives the device uptime when the crash occurs.
+ * </ol>
+ *
+ * <p>Both can be useful information, as the former makes it easy to correlate timestamps in logs,
+ * while the latter serves as a longevity metric.
+ */
+@OptionClass(alias = "runtime-restart-collector")
+public class RuntimeRestartCollector extends BaseDeviceMetricCollector {
+ private static final String METRIC_SEP = "-";
+ public static final String METRIC_PREFIX = "runtime-restart";
+ public static final String METRIC_SUFFIX_COUNT = "count";
+ public static final String METRIC_SUFFIX_SYSTEM_TIMESTAMP_SECS = "timestamps_secs";
+ public static final String METRIC_SUFFIX_SYSTEM_TIMESTAMP_FORMATTED = "timestamps_str";
+ public static final String METRIC_SUFFIX_UPTIME_NANOS = "uptime_nanos";
+ public static final String METRIC_SUFFIX_UPTIME_FORMATTED = "uptime_str";
+ public static final SimpleDateFormat TIME_FORMATTER = new SimpleDateFormat("HH:mm:ss");
+ public static final String SYSTEM_SERVER_KEYWORD = "system_server";
+
+ private List<ITestDevice> mTestDevices;
+ // Map to store the runtime restart timestamps for each device, keyed by device serial numbers.
+ private Map<String, List<Integer>> mExistingTimestamps = new HashMap<>();
+ // Map to store the statsd config IDs for each device, keyed by device serial numbers.
+ private Map<String, Long> mDeviceConfigIds = new HashMap<>();
+ // Flag variable to indicate whether including the device serial in the metrics is needed.
+ // Device serial is included when there are more than one device under test.
+ private boolean mIncludeDeviceSerial = false;
+
+ /**
+ * Store the existing timestamps of system server restarts prior to the test run as statsd keeps
+ * a running log of them, and push the config to collect app crashes.
+ */
+ @Override
+ public void onTestRunStart(DeviceMetricData runData) {
+ mTestDevices = getDevices();
+ mIncludeDeviceSerial = (mTestDevices.size() > 1);
+ for (ITestDevice device : mTestDevices) {
+ // Pull statsd metadata (StatsdStatsReport) from the device and keep track of existing
+ // runtime restart timestamps.
+ try {
+ List<Integer> existingTimestamps =
+ getStatsdMetadata(device).getSystemRestartSecList();
+ mExistingTimestamps.put(device.getSerialNumber(), existingTimestamps);
+ } catch (DeviceNotAvailableException | InvalidProtocolBufferException e) {
+ // Error is not thrown as we still want to process the other devices.
+ CLog.e(
+ "Failed to get statsd metadata from device %s. Exception: %s.",
+ device.getSerialNumber(), e);
+ }
+
+ // Register statsd config to collect app crashes.
+ try {
+ mDeviceConfigIds.put(
+ device.getSerialNumber(),
+ pushStatsConfig(
+ device, Arrays.asList(Atom.APP_CRASH_OCCURRED_FIELD_NUMBER)));
+ } catch (DeviceNotAvailableException | IOException e) {
+ // Error is not thrown as we still want to push the config to other devices.
+ CLog.e(
+ "Failed to push statsd config to device %s. Exception: %s.",
+ device.getSerialNumber(), e);
+ }
+ }
+ }
+
+ /**
+ * Pull the timestamps at the end of the test run and report the difference with existing ones,
+ * if any.
+ */
+ @Override
+ public void onTestRunEnd(
+ DeviceMetricData runData, final Map<String, Metric> currentRunMetrics) {
+ for (ITestDevice device : mTestDevices) {
+ // Pull statsd metadata again to look at the changes of system server crash timestamps
+ // during the test run, and report them.
+ // A copy of the list is created as the list returned by the proto is not modifiable.
+ List<Integer> updatedTimestamps = new ArrayList<>();
+ try {
+ updatedTimestamps.addAll(getStatsdMetadata(device).getSystemRestartSecList());
+ } catch (DeviceNotAvailableException | InvalidProtocolBufferException e) {
+ // Error is not thrown as we still want to process the other devices.
+ CLog.e(
+ "Failed to get statsd metadata from device %s. Exception: %s.",
+ device.getSerialNumber(), e);
+ continue;
+ }
+ if (!mExistingTimestamps.containsKey(device.getSerialNumber())) {
+ CLog.e("No prior state recorded for device %s.", device.getSerialNumber());
+ addStatsdStatsBasedMetrics(
+ currentRunMetrics, updatedTimestamps, device.getSerialNumber());
+ } else {
+ try {
+ int lastTimestampBeforeTestRun =
+ Iterables.getLast(mExistingTimestamps.get(device.getSerialNumber()));
+ // If the last timestamp is not found, lastIndexOf(...) + 1 returns 0 which is
+ // what is needed in that situation.
+ addStatsdStatsBasedMetrics(
+ currentRunMetrics,
+ getAllValuesAfter(lastTimestampBeforeTestRun, updatedTimestamps),
+ device.getSerialNumber());
+ } catch (NoSuchElementException e) {
+ // The exception occurs when no prior runtime restarts had been recorded. It is
+ // thrown from Iterables.getLast().
+ addStatsdStatsBasedMetrics(
+ currentRunMetrics, updatedTimestamps, device.getSerialNumber());
+ }
+ }
+ }
+
+ // Pull the AppCrashOccurred event metric reports and report the system server crashes
+ // within.
+ for (ITestDevice device : mTestDevices) {
+ if (!mDeviceConfigIds.containsKey(device.getSerialNumber())) {
+ CLog.e("No config ID is associated with device %s.", device.getSerialNumber());
+ continue;
+ }
+ long configId = mDeviceConfigIds.get(device.getSerialNumber());
+ List<Long> uptimeListNanos = new ArrayList<>();
+ try {
+ List<EventMetricData> metricData = getEventMetricData(device, configId);
+ uptimeListNanos.addAll(
+ metricData
+ .stream()
+ .filter(d -> d.hasElapsedTimestampNanos())
+ .filter(d -> d.hasAtom())
+ .filter(d -> d.getAtom().hasAppCrashOccurred())
+ .filter(d -> d.getAtom().getAppCrashOccurred().hasProcessName())
+ .filter(
+ d ->
+ SYSTEM_SERVER_KEYWORD.equals(
+ d.getAtom()
+ .getAppCrashOccurred()
+ .getProcessName()))
+ .map(d -> d.getElapsedTimestampNanos())
+ .collect(Collectors.toList()));
+ } catch (DeviceNotAvailableException e) {
+ // Error is not thrown as we still want to process other devices.
+ CLog.e(
+ "Failed to retrieve event metric data from device %s. Exception: %s.",
+ device.getSerialNumber(), e);
+ }
+ addAtomBasedMetrics(currentRunMetrics, uptimeListNanos, device.getSerialNumber());
+ try {
+ removeConfig(device, configId);
+ } catch (DeviceNotAvailableException e) {
+ // Error is not thrown as we still want to remove the config from other devices.
+ CLog.e(
+ "Failed to remove statsd config from device %s. Exception: %s.",
+ device.getSerialNumber(), e);
+ }
+ }
+ }
+
+ /** Helper method to add metrics from StatsdStatsReport according to timestamps. */
+ private void addStatsdStatsBasedMetrics(
+ final Map<String, Metric> metrics, List<Integer> timestampsSecs, String serial) {
+ // If there are runtime restarts, add a comma-separated list of timestamps.
+ if (!timestampsSecs.isEmpty()) {
+ // Store both the raw timestamp and the formatted, more readable version.
+
+ String timestampMetricKey =
+ createMetricKey(METRIC_SUFFIX_SYSTEM_TIMESTAMP_SECS, serial);
+ metrics.put(
+ timestampMetricKey,
+ stringToMetric(
+ timestampsSecs
+ .stream()
+ .map(t -> String.valueOf(t))
+ .collect(Collectors.joining(","))));
+
+ String formattedTimestampMetricKey =
+ createMetricKey(METRIC_SUFFIX_SYSTEM_TIMESTAMP_FORMATTED, serial);
+ metrics.put(
+ formattedTimestampMetricKey,
+ stringToMetric(
+ timestampsSecs
+ .stream()
+ .map(t -> timestampToHoursMinutesSeconds(t))
+ .collect(Collectors.joining(","))));
+ }
+ }
+
+ /** Helper method to add metrics from the AppCrashOccurred atoms according to timestamps. */
+ private void addAtomBasedMetrics(
+ final Map<String, Metric> metrics, List<Long> timestampsNanos, String serial) {
+ // Always add a count of system server crashes, regardless of whether there are any.
+ // The atom-based count is used as the statsd-metadata-based one tops out at 20.
+ String countMetricKey = createMetricKey(METRIC_SUFFIX_COUNT, serial);
+ metrics.put(countMetricKey, stringToMetric(String.valueOf(timestampsNanos.size())));
+ // If there are runtime restarts, add a comma-separated list of device uptime timestamps.
+ if (!timestampsNanos.isEmpty()) {
+ // Store both the raw timestamp and the formatted, more readable version.
+
+ String uptimeNanosMetricKey = createMetricKey(METRIC_SUFFIX_UPTIME_NANOS, serial);
+ metrics.put(
+ uptimeNanosMetricKey,
+ stringToMetric(
+ timestampsNanos
+ .stream()
+ .map(t -> String.valueOf(t))
+ .collect(Collectors.joining(","))));
+
+ String formattedUptimeMetricKey =
+ createMetricKey(METRIC_SUFFIX_UPTIME_FORMATTED, serial);
+ metrics.put(
+ formattedUptimeMetricKey,
+ stringToMetric(
+ timestampsNanos
+ .stream()
+ .map(t -> nanosToHoursMinutesSeconds(t))
+ .collect(Collectors.joining(","))));
+ }
+ }
+
+ private String createMetricKey(String suffix, String serial) {
+ return mIncludeDeviceSerial
+ ? String.join(METRIC_SEP, METRIC_PREFIX, serial, suffix)
+ : String.join(METRIC_SEP, METRIC_PREFIX, suffix);
+ }
+
+ /**
+ * Forwarding logic to {@link ConfigUtil} for testing as it is a static utility class.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ protected long pushStatsConfig(ITestDevice device, List<Integer> eventAtomIds)
+ throws IOException, DeviceNotAvailableException {
+ return ConfigUtil.pushStatsConfig(device, eventAtomIds);
+ }
+
+ /**
+ * Forwarding logic to {@link ConfigUtil} for testing as it is a static utility class.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ protected void removeConfig(ITestDevice device, long configId)
+ throws DeviceNotAvailableException {
+ ConfigUtil.removeConfig(device, configId);
+ }
+
+ /**
+ * Forwarding logic to {@link MetricUtil} for testing as it is a static utility class.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ protected StatsdStatsReport getStatsdMetadata(ITestDevice device)
+ throws DeviceNotAvailableException, InvalidProtocolBufferException {
+ return MetricUtil.getStatsdMetadata(device);
+ }
+
+ /**
+ * Forwarding logic to {@link MetricUtil} for testing as it is a static utility class.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ protected List<EventMetricData> getEventMetricData(ITestDevice device, long configId)
+ throws DeviceNotAvailableException {
+ return MetricUtil.getEventMetricData(device, configId);
+ }
+
+ /**
+ * Helper method to convert nanoseconds to HH:mm:ss. {@link SimpleDateFormat} is not used here
+ * as the input is not a real epoch-based timestamp.
+ */
+ private String nanosToHoursMinutesSeconds(long nanos) {
+ long hours = TimeUnit.NANOSECONDS.toHours(nanos);
+ nanos -= TimeUnit.HOURS.toNanos(hours);
+ long minutes = TimeUnit.NANOSECONDS.toMinutes(nanos);
+ nanos -= TimeUnit.MINUTES.toNanos(minutes);
+ long seconds = TimeUnit.NANOSECONDS.toSeconds(nanos);
+ return String.format("%02d:%02d:%02d", hours, minutes, seconds);
+ }
+
+ /** Helper method to format an epoch-based timestamp in seconds to HH:mm:ss. */
+ private String timestampToHoursMinutesSeconds(int seconds) {
+ return TIME_FORMATTER.format(new Date(TimeUnit.SECONDS.toMillis(seconds)));
+ }
+
+ /** Helper method to get all values in a list that appear after all occurrences of target. */
+ private List<Integer> getAllValuesAfter(int target, List<Integer> l) {
+ return l.subList(l.lastIndexOf(target) + 1, l.size());
+ }
+}
diff --git a/src/com/android/tradefed/util/statsd/MetricUtil.java b/src/com/android/tradefed/util/statsd/MetricUtil.java
index f7220a7..b0464d0 100644
--- a/src/com/android/tradefed/util/statsd/MetricUtil.java
+++ b/src/com/android/tradefed/util/statsd/MetricUtil.java
@@ -18,6 +18,7 @@
import com.android.os.StatsLog.ConfigMetricsReport;
import com.android.os.StatsLog.ConfigMetricsReportList;
import com.android.os.StatsLog.EventMetricData;
+import com.android.os.StatsLog.StatsdStatsReport;
import com.android.os.StatsLog.StatsLogReport;
import com.android.tradefed.device.CollectingByteOutputReceiver;
import com.android.tradefed.device.DeviceNotAvailableException;
@@ -26,6 +27,7 @@
import com.android.tradefed.result.ByteArrayInputStreamSource;
import com.android.tradefed.result.InputStreamSource;
+import com.google.common.annotations.VisibleForTesting;
import com.google.protobuf.InvalidProtocolBufferException;
import java.util.ArrayList;
@@ -34,8 +36,12 @@
/** Utility class for pulling metrics from pushed statsd configurations. */
public class MetricUtil {
+ @VisibleForTesting
static final String DUMP_REPORT_CMD_TEMPLATE =
"cmd stats dump-report %s --include_current_bucket --proto";
+ // The command is documented in frameworks/base/cmds/statsd/src/StatsService.cpp.
+ @VisibleForTesting
+ static final String DUMP_STATSD_METADATA_CMD = "dumpsys stats --metadata --proto";
/** Get statsd event metrics data from the device using the statsd config id. */
public static List<EventMetricData> getEventMetricData(ITestDevice device, long configId)
@@ -45,13 +51,16 @@
CLog.d("No stats report collected.");
return new ArrayList<EventMetricData>();
}
- ConfigMetricsReport report = reports.getReports(0);
List<EventMetricData> data = new ArrayList<>();
- for (StatsLogReport metric : report.getMetricsList()) {
- data.addAll(metric.getEventMetrics().getDataList());
+ // Usually, there is only one report. However, a runtime restart will generate a new report
+ // for the same config, resulting in multiple reports. Their data is independent and can
+ // simply be concatenated together.
+ for (ConfigMetricsReport report : reports.getReportsList()) {
+ for (StatsLogReport metric : report.getMetricsList()) {
+ data.addAll(metric.getEventMetrics().getDataList());
+ }
}
data.sort(Comparator.comparing(EventMetricData::getElapsedTimestampNanos));
-
CLog.d("Received EventMetricDataList as following:\n");
for (EventMetricData d : data) {
CLog.d("Atom at %d:\n%s", d.getElapsedTimestampNanos(), d.getAtom().toString());
@@ -65,6 +74,14 @@
return new ByteArrayInputStreamSource(getReportByteArray(device, configId));
}
+ /** Get statsd metadata which also contains system server crash information. */
+ public static StatsdStatsReport getStatsdMetadata(ITestDevice device)
+ throws DeviceNotAvailableException, InvalidProtocolBufferException {
+ final CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
+ device.executeShellCommand(DUMP_STATSD_METADATA_CMD, receiver);
+ return StatsdStatsReport.parser().parseFrom(receiver.getOutput());
+ }
+
/** Get the report list proto from the device for the given {@code configId}. */
private static ConfigMetricsReportList getReportList(ITestDevice device, long configId)
throws DeviceNotAvailableException {
@@ -86,6 +103,4 @@
String.format(DUMP_REPORT_CMD_TEMPLATE, String.valueOf(configId)), receiver);
return receiver.getOutput();
}
-
-
}
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index bedcfbb..1f130b3 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -99,6 +99,7 @@
import com.android.tradefed.device.metric.PerfettoPullerMetricCollectorTest;
import com.android.tradefed.device.metric.ProcessMaxMemoryCollectorTest;
import com.android.tradefed.device.metric.RebootReasonCollectorTest;
+import com.android.tradefed.device.metric.RuntimeRestartCollectorTest;
import com.android.tradefed.device.metric.ScheduleMultipleDeviceMetricCollectorTest;
import com.android.tradefed.device.metric.ScheduledDeviceMetricCollectorTest;
import com.android.tradefed.device.metric.ScreenshotOnFailureCollectorTest;
@@ -469,6 +470,7 @@
PerfettoPullerMetricCollectorTest.class,
ProcessMaxMemoryCollectorTest.class,
RebootReasonCollectorTest.class,
+ RuntimeRestartCollectorTest.class,
ScheduledDeviceMetricCollectorTest.class,
ScheduleMultipleDeviceMetricCollectorTest.class,
ScreenshotOnFailureCollectorTest.class,
diff --git a/tests/src/com/android/tradefed/device/metric/RuntimeRestartCollectorTest.java b/tests/src/com/android/tradefed/device/metric/RuntimeRestartCollectorTest.java
new file mode 100644
index 0000000..c626eca
--- /dev/null
+++ b/tests/src/com/android/tradefed/device/metric/RuntimeRestartCollectorTest.java
@@ -0,0 +1,614 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.device.metric;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import com.android.os.AtomsProto.Atom;
+import com.android.os.AtomsProto.AppCrashOccurred;
+import com.android.os.StatsLog.EventMetricData;
+import com.android.os.StatsLog.StatsdStatsReport;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.ITestInvocationListener;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+/** Unit tests for {@link RuntimeRestartCollector}. */
+@RunWith(JUnit4.class)
+public class RuntimeRestartCollectorTest {
+ private static final String DEVICE_SERIAL_1 = "device_serial_1";
+ private static final String DEVICE_SERIAL_2 = "device_serial_2";
+
+ private static final long CONFIG_ID_1 = 1;
+ private static final long CONFIG_ID_2 = 2;
+
+ private static final int TIMESTAMP_1_SECS = 1554764010;
+ private static final String TIMESTAMP_1_STR =
+ RuntimeRestartCollector.TIME_FORMATTER.format(
+ new Date(TimeUnit.SECONDS.toMillis(TIMESTAMP_1_SECS)));
+ private static final int TIMESTAMP_2_SECS = 1554764135;
+ private static final String TIMESTAMP_2_STR =
+ RuntimeRestartCollector.TIME_FORMATTER.format(
+ new Date(TimeUnit.SECONDS.toMillis(TIMESTAMP_2_SECS)));
+
+ private static final long UPTIME_1_NANOS =
+ TimeUnit.HOURS.toNanos(1) + TimeUnit.MINUTES.toNanos(2) + TimeUnit.SECONDS.toNanos(3);
+ private static final String UPTIME_1_STR = "01:02:03";
+ private static final long UPTIME_2_NANOS =
+ TimeUnit.HOURS.toNanos(1) + TimeUnit.MINUTES.toNanos(4) + TimeUnit.SECONDS.toNanos(8);
+ private static final String UPTIME_2_STR = "01:04:08";
+
+ private static final EventMetricData RUNTIME_RESTART_DATA_1 =
+ EventMetricData.newBuilder()
+ .setElapsedTimestampNanos(UPTIME_1_NANOS)
+ .setAtom(
+ Atom.newBuilder()
+ .setAppCrashOccurred(
+ AppCrashOccurred.newBuilder()
+ .setProcessName(
+ RuntimeRestartCollector
+ .SYSTEM_SERVER_KEYWORD)))
+ .build();
+ private static final EventMetricData RUNTIME_RESTART_DATA_2 =
+ RUNTIME_RESTART_DATA_1.toBuilder().setElapsedTimestampNanos(UPTIME_2_NANOS).build();
+ private static final EventMetricData NOT_RUNTIME_RESTART_DATA =
+ EventMetricData.newBuilder()
+ .setElapsedTimestampNanos(111)
+ .setAtom(
+ Atom.newBuilder()
+ .setAppCrashOccurred(
+ AppCrashOccurred.newBuilder()
+ .setProcessName("not_system_server")))
+ .build();
+
+ @Spy private RuntimeRestartCollector mCollector;
+ @Mock private IInvocationContext mContext;
+ @Mock private ITestInvocationListener mListener;
+
+ @Before
+ public void setUp() throws Exception {
+ initMocks(this);
+ }
+
+ /**
+ * Test that the collector makes the correct calls to the statsd utilities for a single device.
+ *
+ * <p>During testRunStarted() it should push the config and pull the statsd metadata. During
+ * testRunEnded() it should pull the event metric data, remove the statsd config, and pull the
+ * statsd metadata again.
+ */
+ @Test
+ public void testStatsdInteractions_singleDevice() throws Exception {
+ ITestDevice testDevice = mockTestDevice(DEVICE_SERIAL_1);
+ doReturn(Arrays.asList(testDevice)).when(mContext).getDevices();
+ doReturn(CONFIG_ID_1)
+ .when(mCollector)
+ .pushStatsConfig(any(ITestDevice.class), any(List.class));
+ doReturn(new ArrayList<EventMetricData>())
+ .when(mCollector)
+ .getEventMetricData(any(ITestDevice.class), anyLong());
+ doReturn(StatsdStatsReport.newBuilder().build())
+ .when(mCollector)
+ .getStatsdMetadata(any(ITestDevice.class));
+
+ mCollector.init(mContext, mListener);
+
+ mCollector.testRunStarted("test run", 1);
+ verify(mCollector, times(1))
+ .pushStatsConfig(
+ eq(testDevice),
+ argThat(l -> l.contains(Atom.APP_CRASH_OCCURRED_FIELD_NUMBER)));
+ verify(mCollector, times(1)).getStatsdMetadata(eq(testDevice));
+
+ mCollector.testRunEnded(0, new HashMap<String, Metric>());
+ verify(mCollector, times(1)).getEventMetricData(eq(testDevice), eq(CONFIG_ID_1));
+ verify(mCollector, times(1)).removeConfig(eq(testDevice), eq(CONFIG_ID_1));
+ verify(mCollector, times(2)).getStatsdMetadata(eq(testDevice));
+ }
+
+ /**
+ * Test that the collector makes the correct calls to the statsd utilities for multiple devices.
+ *
+ * <p>During testRunStarted() it should push the config and pull the statsd metadata for each
+ * device. During testRunEnded() it should pull the event metric data, remove the statsd config,
+ * and pull the statsd metadata again for each device.
+ */
+ @Test
+ public void testStatsdInteractions_multiDevice() throws Exception {
+ ITestDevice testDevice1 = mockTestDevice(DEVICE_SERIAL_1);
+ ITestDevice testDevice2 = mockTestDevice(DEVICE_SERIAL_2);
+ doReturn(Arrays.asList(testDevice1, testDevice2)).when(mContext).getDevices();
+ doReturn(CONFIG_ID_1).when(mCollector).pushStatsConfig(eq(testDevice1), any(List.class));
+ doReturn(CONFIG_ID_2).when(mCollector).pushStatsConfig(eq(testDevice2), any(List.class));
+ doReturn(new ArrayList<EventMetricData>())
+ .when(mCollector)
+ .getEventMetricData(any(ITestDevice.class), anyLong());
+ doReturn(StatsdStatsReport.newBuilder().build())
+ .when(mCollector)
+ .getStatsdMetadata(any(ITestDevice.class));
+
+ mCollector.init(mContext, mListener);
+
+ mCollector.testRunStarted("test run", 1);
+ verify(mCollector, times(1))
+ .pushStatsConfig(
+ eq(testDevice1),
+ argThat(l -> l.contains(Atom.APP_CRASH_OCCURRED_FIELD_NUMBER)));
+ verify(mCollector, times(1))
+ .pushStatsConfig(
+ eq(testDevice2),
+ argThat(l -> l.contains(Atom.APP_CRASH_OCCURRED_FIELD_NUMBER)));
+ verify(mCollector, times(1)).getStatsdMetadata(eq(testDevice1));
+ verify(mCollector, times(1)).getStatsdMetadata(eq(testDevice2));
+
+ mCollector.testRunEnded(0, new HashMap<String, Metric>());
+ verify(mCollector, times(1)).getEventMetricData(eq(testDevice1), eq(CONFIG_ID_1));
+ verify(mCollector, times(1)).getEventMetricData(eq(testDevice2), eq(CONFIG_ID_2));
+ verify(mCollector, times(1)).removeConfig(eq(testDevice1), eq(CONFIG_ID_1));
+ verify(mCollector, times(1)).removeConfig(eq(testDevice2), eq(CONFIG_ID_2));
+ verify(mCollector, times(2)).getStatsdMetadata(eq(testDevice1));
+ verify(mCollector, times(2)).getStatsdMetadata(eq(testDevice2));
+ }
+
+ /**
+ * Test that the collector collects a count of zero and no timestamps when no runtime restarts
+ * occur during the test run and there were no prior runtime restarts.
+ */
+ @Test
+ public void testAddingMetrics_noRuntimeRestart_noPriorRuntimeRestart() throws Exception {
+ ITestDevice testDevice = mockTestDevice(DEVICE_SERIAL_1);
+ doReturn(Arrays.asList(testDevice)).when(mContext).getDevices();
+ doReturn(CONFIG_ID_1)
+ .when(mCollector)
+ .pushStatsConfig(any(ITestDevice.class), any(List.class));
+ doReturn(new ArrayList<EventMetricData>())
+ .when(mCollector)
+ .getEventMetricData(any(ITestDevice.class), anyLong());
+ doReturn(StatsdStatsReport.newBuilder().build(), StatsdStatsReport.newBuilder().build())
+ .when(mCollector)
+ .getStatsdMetadata(any(ITestDevice.class));
+
+ HashMap<String, Metric> runMetrics = new HashMap<>();
+ mCollector.init(mContext, mListener);
+ mCollector.testRunStarted("test run", 1);
+ mCollector.testRunEnded(0, runMetrics);
+
+ // A count should always be present, and should be zero in this case.
+ // If a count is not present, .findFirst().get() throws and the test will fail.
+ int count = getCount(runMetrics);
+ Assert.assertEquals(0, count);
+ // No other metric keys should be present as there is no data.
+ ensureNoMetricWithKeySuffix(
+ runMetrics, RuntimeRestartCollector.METRIC_SUFFIX_SYSTEM_TIMESTAMP_SECS);
+ ensureNoMetricWithKeySuffix(
+ runMetrics, RuntimeRestartCollector.METRIC_SUFFIX_SYSTEM_TIMESTAMP_FORMATTED);
+ ensureNoMetricWithKeySuffix(runMetrics, RuntimeRestartCollector.METRIC_SUFFIX_UPTIME_NANOS);
+ ensureNoMetricWithKeySuffix(
+ runMetrics, RuntimeRestartCollector.METRIC_SUFFIX_UPTIME_FORMATTED);
+ }
+
+ /**
+ * Test that the collector collects a count of zero and no timestamps when no runtime restarts
+ * and there were prior runtime restarts.
+ */
+ @Test
+ public void testAddingMetrics_noRuntimeRestart_withPriorRuntimeRestart() throws Exception {
+ ITestDevice testDevice = mockTestDevice(DEVICE_SERIAL_1);
+ doReturn(Arrays.asList(testDevice)).when(mContext).getDevices();
+ doReturn(CONFIG_ID_1)
+ .when(mCollector)
+ .pushStatsConfig(any(ITestDevice.class), any(List.class));
+ doReturn(new ArrayList<EventMetricData>())
+ .when(mCollector)
+ .getEventMetricData(any(ITestDevice.class), anyLong());
+ doReturn(
+ StatsdStatsReport.newBuilder()
+ .addAllSystemRestartSec(Arrays.asList(1, 2))
+ .build(),
+ StatsdStatsReport.newBuilder()
+ .addAllSystemRestartSec(Arrays.asList(1, 2))
+ .build())
+ .when(mCollector)
+ .getStatsdMetadata(any(ITestDevice.class));
+
+ HashMap<String, Metric> runMetrics = new HashMap<>();
+ mCollector.init(mContext, mListener);
+ mCollector.testRunStarted("test run", 1);
+ mCollector.testRunEnded(0, runMetrics);
+
+ // A count should always be present, and should be zero in this case.
+ // If a count is not present, .findFirst().get() throws and the test will fail.
+ int count = getCount(runMetrics);
+ Assert.assertEquals(0, count);
+ // No other metric keys should be present as there is no data.
+ ensureNoMetricWithKeySuffix(
+ runMetrics, RuntimeRestartCollector.METRIC_SUFFIX_SYSTEM_TIMESTAMP_SECS);
+ ensureNoMetricWithKeySuffix(
+ runMetrics, RuntimeRestartCollector.METRIC_SUFFIX_SYSTEM_TIMESTAMP_FORMATTED);
+ ensureNoMetricWithKeySuffix(runMetrics, RuntimeRestartCollector.METRIC_SUFFIX_UPTIME_NANOS);
+ ensureNoMetricWithKeySuffix(
+ runMetrics, RuntimeRestartCollector.METRIC_SUFFIX_UPTIME_FORMATTED);
+ }
+
+ /**
+ * Test that the collector collects counts and timestamps correctly when there are runtime
+ * restarts and there were no prior runtime restarts.
+ */
+ @Test
+ public void testAddingMetrics_withRuntimeRestart_noPriorRuntimeRestart() throws Exception {
+ ITestDevice testDevice = mockTestDevice(DEVICE_SERIAL_1);
+ doReturn(Arrays.asList(testDevice)).when(mContext).getDevices();
+ doReturn(CONFIG_ID_1)
+ .when(mCollector)
+ .pushStatsConfig(any(ITestDevice.class), any(List.class));
+ doReturn(Arrays.asList(RUNTIME_RESTART_DATA_1, RUNTIME_RESTART_DATA_2))
+ .when(mCollector)
+ .getEventMetricData(any(ITestDevice.class), anyLong());
+ doReturn(
+ StatsdStatsReport.newBuilder().build(),
+ StatsdStatsReport.newBuilder()
+ .addAllSystemRestartSec(
+ Arrays.asList(TIMESTAMP_1_SECS, TIMESTAMP_2_SECS))
+ .build())
+ .when(mCollector)
+ .getStatsdMetadata(any(ITestDevice.class));
+
+ HashMap<String, Metric> runMetrics = new HashMap<>();
+ mCollector.init(mContext, mListener);
+ mCollector.testRunStarted("test run", 1);
+ mCollector.testRunEnded(0, runMetrics);
+
+ // Count should be two.
+ int count = getCount(runMetrics);
+ Assert.assertEquals(2, count);
+ // There should be two timestamps that match TIMESTAMP_1_SECS and TIMESTAMP_2_SECS
+ // respectively.
+ List<Integer> timestampSecs =
+ getIntMetricValuesByKeySuffix(
+ runMetrics, RuntimeRestartCollector.METRIC_SUFFIX_SYSTEM_TIMESTAMP_SECS);
+ Assert.assertEquals(Arrays.asList(TIMESTAMP_1_SECS, TIMESTAMP_2_SECS), timestampSecs);
+ // There should be two timestamp strings that match TIMESTAMP_1_STR and TIMESTAMP_2_STR
+ // respectively.
+ List<String> timestampStrs =
+ getStringMetricValuesByKeySuffix(
+ runMetrics,
+ RuntimeRestartCollector.METRIC_SUFFIX_SYSTEM_TIMESTAMP_FORMATTED);
+ Assert.assertEquals(Arrays.asList(TIMESTAMP_1_STR, TIMESTAMP_2_STR), timestampStrs);
+ // There should be two uptime timestsmps that match UPTIME_1_NANOS and UPTIME_2_NANOS
+ // respectively.
+ List<Long> uptimeNanos =
+ getLongMetricValuesByKeySuffix(
+ runMetrics, RuntimeRestartCollector.METRIC_SUFFIX_UPTIME_NANOS);
+ Assert.assertEquals(Arrays.asList(UPTIME_1_NANOS, UPTIME_2_NANOS), uptimeNanos);
+ // There should be two uptime timestamp strings that match UPTIME_1_STR and UPTIME_2_STR
+ // respectively.
+ List<String> uptimeStrs =
+ getStringMetricValuesByKeySuffix(
+ runMetrics, RuntimeRestartCollector.METRIC_SUFFIX_UPTIME_FORMATTED);
+ Assert.assertEquals(Arrays.asList(UPTIME_1_STR, UPTIME_2_STR), uptimeStrs);
+ }
+
+ /**
+ * Test that the collector collects counts and metrics correctly when there are runtime restarts
+ * and there were prior runtime restarts.
+ */
+ @Test
+ public void testAddingMetrics_withRuntimeRestart_withPriorRuntimeRestart() throws Exception {
+ ITestDevice testDevice = mockTestDevice(DEVICE_SERIAL_1);
+ doReturn(Arrays.asList(testDevice)).when(mContext).getDevices();
+ doReturn(CONFIG_ID_1)
+ .when(mCollector)
+ .pushStatsConfig(any(ITestDevice.class), any(List.class));
+ doReturn(Arrays.asList(RUNTIME_RESTART_DATA_1, RUNTIME_RESTART_DATA_2))
+ .when(mCollector)
+ .getEventMetricData(any(ITestDevice.class), anyLong());
+ doReturn(
+ StatsdStatsReport.newBuilder()
+ .addAllSystemRestartSec(Arrays.asList(1, 2, 3))
+ .build(),
+ StatsdStatsReport.newBuilder()
+ .addAllSystemRestartSec(
+ Arrays.asList(2, 3, TIMESTAMP_1_SECS, TIMESTAMP_2_SECS))
+ .build())
+ .when(mCollector)
+ .getStatsdMetadata(any(ITestDevice.class));
+
+ HashMap<String, Metric> runMetrics = new HashMap<>();
+ mCollector.init(mContext, mListener);
+ mCollector.testRunStarted("test run", 1);
+ mCollector.testRunEnded(0, runMetrics);
+
+ // Count should be two.
+ int count = getCount(runMetrics);
+ Assert.assertEquals(2, count);
+ // There should be two timestamps that match TIMESTAMP_1_SECS and TIMESTAMP_2_SECS
+ // respectively.
+ List<Integer> timestampSecs =
+ getIntMetricValuesByKeySuffix(
+ runMetrics, RuntimeRestartCollector.METRIC_SUFFIX_SYSTEM_TIMESTAMP_SECS);
+ Assert.assertEquals(Arrays.asList(TIMESTAMP_1_SECS, TIMESTAMP_2_SECS), timestampSecs);
+ // There should be two timestamp strings that match TIMESTAMP_1_STR and TIMESTAMP_2_STR
+ // respectively.
+ List<String> timestampStrs =
+ getStringMetricValuesByKeySuffix(
+ runMetrics,
+ RuntimeRestartCollector.METRIC_SUFFIX_SYSTEM_TIMESTAMP_FORMATTED);
+ Assert.assertEquals(Arrays.asList(TIMESTAMP_1_STR, TIMESTAMP_2_STR), timestampStrs);
+ // There should be two uptime timestsmps that match UPTIME_1_NANOS and UPTIME_2_NANOS
+ // respectively.
+ List<Long> uptimeNanos =
+ getLongMetricValuesByKeySuffix(
+ runMetrics, RuntimeRestartCollector.METRIC_SUFFIX_UPTIME_NANOS);
+ Assert.assertEquals(Arrays.asList(UPTIME_1_NANOS, UPTIME_2_NANOS), uptimeNanos);
+ // There should be two uptime timestamp strings that match UPTIME_1_STR and UPTIME_2_STR
+ // respectively.
+ List<String> uptimeStrs =
+ getStringMetricValuesByKeySuffix(
+ runMetrics, RuntimeRestartCollector.METRIC_SUFFIX_UPTIME_FORMATTED);
+ Assert.assertEquals(Arrays.asList(UPTIME_1_STR, UPTIME_2_STR), uptimeStrs);
+ }
+
+ /**
+ * Test that the {@link AppCrashOccurred}-based collection only collects runtime restarts (i.e.
+ * system server crashes) and not other crashes.
+ */
+ @Test
+ public void testAddingMetrics_withRuntimeRestart_reportsSystemServerCrashesOnly()
+ throws Exception {
+ ITestDevice testDevice = mockTestDevice(DEVICE_SERIAL_1);
+ doReturn(Arrays.asList(testDevice)).when(mContext).getDevices();
+ doReturn(CONFIG_ID_1)
+ .when(mCollector)
+ .pushStatsConfig(any(ITestDevice.class), any(List.class));
+ doReturn(
+ Arrays.asList(
+ RUNTIME_RESTART_DATA_1,
+ NOT_RUNTIME_RESTART_DATA,
+ RUNTIME_RESTART_DATA_2))
+ .when(mCollector)
+ .getEventMetricData(any(ITestDevice.class), anyLong());
+ doReturn(
+ StatsdStatsReport.newBuilder()
+ .addAllSystemRestartSec(Arrays.asList(1, 2, 3))
+ .build(),
+ StatsdStatsReport.newBuilder()
+ .addAllSystemRestartSec(
+ Arrays.asList(2, 3, TIMESTAMP_1_SECS, TIMESTAMP_2_SECS))
+ .build())
+ .when(mCollector)
+ .getStatsdMetadata(any(ITestDevice.class));
+
+ HashMap<String, Metric> runMetrics = new HashMap<>();
+ mCollector.init(mContext, mListener);
+ mCollector.testRunStarted("test run", 1);
+ mCollector.testRunEnded(0, runMetrics);
+
+ // We only check for the uptime timestamps coming from the AppCrashOccurred atoms.
+
+ // There should be two uptime timestamps that match UPTIME_1_NANOS and UPTIME_2_NANOS
+ // respectively.
+ List<Long> uptimeNanos =
+ getLongMetricValuesByKeySuffix(
+ runMetrics, RuntimeRestartCollector.METRIC_SUFFIX_UPTIME_NANOS);
+ Assert.assertEquals(Arrays.asList(UPTIME_1_NANOS, UPTIME_2_NANOS), uptimeNanos);
+ // There should be two uptime timestamp strings that match UPTIME_1_STR and UPTIME_2_STR
+ // respectively.
+ List<String> uptimeStrs =
+ getStringMetricValuesByKeySuffix(
+ runMetrics, RuntimeRestartCollector.METRIC_SUFFIX_UPTIME_FORMATTED);
+ Assert.assertEquals(Arrays.asList(UPTIME_1_STR, UPTIME_2_STR), uptimeStrs);
+ }
+
+ /**
+ * Test that the collector reports counts based on the {@link AppCrashOccurred} results when it
+ * disagrees with info from statsd metadata.
+ */
+ @Test
+ public void testAddingMetrics_withRuntimeRestart_useAtomResultsForCount() throws Exception {
+ ITestDevice testDevice = mockTestDevice(DEVICE_SERIAL_1);
+ doReturn(Arrays.asList(testDevice)).when(mContext).getDevices();
+ // Two data points from the AppCrashOccurred data.
+ doReturn(CONFIG_ID_1)
+ .when(mCollector)
+ .pushStatsConfig(any(ITestDevice.class), any(List.class));
+ doReturn(Arrays.asList(RUNTIME_RESTART_DATA_1, RUNTIME_RESTART_DATA_2))
+ .when(mCollector)
+ .getEventMetricData(any(ITestDevice.class), anyLong());
+ // Data from statsd metadata only has one timestamp.
+ doReturn(
+ StatsdStatsReport.newBuilder()
+ .addAllSystemRestartSec(Arrays.asList())
+ .build(),
+ StatsdStatsReport.newBuilder()
+ .addAllSystemRestartSec(Arrays.asList(TIMESTAMP_1_SECS))
+ .build())
+ .when(mCollector)
+ .getStatsdMetadata(any(ITestDevice.class));
+
+ HashMap<String, Metric> runMetrics = new HashMap<>();
+ mCollector.init(mContext, mListener);
+ mCollector.testRunStarted("test run", 1);
+ mCollector.testRunEnded(0, runMetrics);
+
+ // Count should be two as in the stubbed EventMetricDataResults, even though statsd metadata
+ // only reported one timestamp.
+ int count = getCount(runMetrics);
+ Assert.assertEquals(2, count);
+ }
+
+ /**
+ * Test that the device serial number is included in the metric key when multiple devices are
+ * under test.
+ */
+ @Test
+ public void testAddingMetrics_includesSerialForMultipleDevices() throws Exception {
+ ITestDevice testDevice1 = mockTestDevice(DEVICE_SERIAL_1);
+ ITestDevice testDevice2 = mockTestDevice(DEVICE_SERIAL_2);
+ doReturn(Arrays.asList(testDevice1, testDevice2)).when(mContext).getDevices();
+ doReturn(CONFIG_ID_1).when(mCollector).pushStatsConfig(eq(testDevice1), any(List.class));
+ doReturn(CONFIG_ID_2).when(mCollector).pushStatsConfig(eq(testDevice2), any(List.class));
+ doReturn(new ArrayList<EventMetricData>())
+ .when(mCollector)
+ .getEventMetricData(any(ITestDevice.class), anyLong());
+ doReturn(StatsdStatsReport.newBuilder().build())
+ .when(mCollector)
+ .getStatsdMetadata(any(ITestDevice.class));
+
+ HashMap<String, Metric> runMetrics = new HashMap<>();
+ mCollector.init(mContext, mListener);
+ mCollector.testRunStarted("test run", 1);
+ mCollector.testRunEnded(0, runMetrics);
+
+ // Check that the count metrics contain the serial numbers for the two devices,
+ // respectively.
+ List<String> countMetricKeys =
+ runMetrics
+ .keySet()
+ .stream()
+ .filter(
+ key ->
+ hasPrefixAndSuffix(
+ key,
+ RuntimeRestartCollector.METRIC_PREFIX,
+ RuntimeRestartCollector.METRIC_SUFFIX_COUNT))
+ .collect(Collectors.toList());
+ // One key for each device.
+ Assert.assertEquals(2, countMetricKeys.size());
+ Assert.assertTrue(
+ countMetricKeys
+ .stream()
+ .anyMatch(
+ key ->
+ (key.contains(DEVICE_SERIAL_1)
+ && (!key.contains(DEVICE_SERIAL_2)))));
+ Assert.assertTrue(
+ countMetricKeys
+ .stream()
+ .anyMatch(
+ key ->
+ (key.contains(DEVICE_SERIAL_2)
+ && (!key.contains(DEVICE_SERIAL_1)))));
+ }
+
+ /** Helper method to get count from metrics. */
+ private static int getCount(Map<String, Metric> metrics) {
+ return Integer.parseInt(
+ metrics.entrySet()
+ .stream()
+ .filter(
+ entry ->
+ hasPrefixAndSuffix(
+ entry.getKey(),
+ RuntimeRestartCollector.METRIC_PREFIX,
+ RuntimeRestartCollector.METRIC_SUFFIX_COUNT))
+ .map(entry -> entry.getValue())
+ .findFirst()
+ .get()
+ .getMeasurements()
+ .getSingleString());
+ }
+
+ /** Helper method to check that no metric key with the given suffix is in the metrics. */
+ private static void ensureNoMetricWithKeySuffix(Map<String, Metric> metrics, String suffix) {
+ Assert.assertTrue(
+ metrics.keySet()
+ .stream()
+ .noneMatch(
+ key ->
+ hasPrefixAndSuffix(
+ key,
+ RuntimeRestartCollector.METRIC_PREFIX,
+ suffix)));
+ }
+
+ /**
+ * Helper method to find a comma-separated String metric value by its key suffix, and return its
+ * values as a list.
+ */
+ private static List<String> getStringMetricValuesByKeySuffix(
+ Map<String, Metric> metrics, String suffix) {
+ return metrics.entrySet()
+ .stream()
+ .filter(
+ entry ->
+ hasPrefixAndSuffix(
+ entry.getKey(),
+ RuntimeRestartCollector.METRIC_PREFIX,
+ suffix))
+ .map(entry -> entry.getValue().getMeasurements().getSingleString().split(","))
+ .flatMap(arr -> Arrays.stream(arr))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Helper method to find a comma-separated int metric value by its key suffix, and return its
+ * values as a list.
+ */
+ private static List<Integer> getIntMetricValuesByKeySuffix(
+ Map<String, Metric> metrics, String suffix) {
+ return getStringMetricValuesByKeySuffix(metrics, suffix)
+ .stream()
+ .map(val -> Integer.valueOf(val))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Helper method to find a comma-separated long metric value by its key suffix, and return its
+ * values as a list.
+ */
+ private static List<Long> getLongMetricValuesByKeySuffix(
+ Map<String, Metric> metrics, String suffix) {
+ return getStringMetricValuesByKeySuffix(metrics, suffix)
+ .stream()
+ .map(val -> Long.valueOf(val))
+ .collect(Collectors.toList());
+ }
+
+ private static boolean hasPrefixAndSuffix(String target, String prefix, String suffix) {
+ return target.startsWith(prefix) && target.endsWith(suffix);
+ }
+
+ private ITestDevice mockTestDevice(String serial) {
+ ITestDevice device = mock(ITestDevice.class);
+ doReturn(serial).when(device).getSerialNumber();
+ return device;
+ }
+}
diff --git a/tests/src/com/android/tradefed/util/statsd/MetricUtilTest.java b/tests/src/com/android/tradefed/util/statsd/MetricUtilTest.java
index 53ccadd..9182e6f 100644
--- a/tests/src/com/android/tradefed/util/statsd/MetricUtilTest.java
+++ b/tests/src/com/android/tradefed/util/statsd/MetricUtilTest.java
@@ -17,9 +17,10 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.matches;
+import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.matches;
import com.android.os.AtomsProto.Atom;
import com.android.os.AtomsProto.BleScanStateChanged;
@@ -27,11 +28,14 @@
import com.android.os.StatsLog.ConfigMetricsReport;
import com.android.os.StatsLog.ConfigMetricsReportList;
import com.android.os.StatsLog.EventMetricData;
+import com.android.os.StatsLog.StatsdStatsReport;
import com.android.os.StatsLog.StatsLogReport;
import com.android.tradefed.device.CollectingByteOutputReceiver;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
+import com.google.protobuf.InvalidProtocolBufferException;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -41,7 +45,6 @@
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
-import java.io.IOException;
import java.util.List;
/** Unit tests for {@link MetricUtil} */
@@ -49,7 +52,7 @@
public class MetricUtilTest {
// A config id used for testing.
private static final long CONFIG_ID = 11;
- // Two metrics for a sample report.
+ // Three metrics for a sample report.
private static final long METRIC_1_NANOS = 222;
private static final Atom METRIC_1_ATOM =
Atom.newBuilder().setBleScanStateChanged(BleScanStateChanged.newBuilder()).build();
@@ -66,8 +69,16 @@
.setElapsedTimestampNanos(METRIC_2_NANOS)
.setAtom(METRIC_2_ATOM)
.build();
+ // The third metric has the same atom as the first one for the "two reports on the same atom"
+ // test case.
+ private static final long METRIC_3_NANOS = 333;
+ private static final EventMetricData METRIC_3_DATA =
+ EventMetricData.newBuilder()
+ .setElapsedTimestampNanos(METRIC_3_NANOS)
+ .setAtom(METRIC_1_ATOM)
+ .build();
// A sample metrics report.
- private static final ConfigMetricsReportList SAMPLE_REPORT_LIST_PROTO =
+ private static final ConfigMetricsReportList SINGLE_REPORT_LIST_PROTO =
ConfigMetricsReportList.newBuilder()
.addReports(
ConfigMetricsReport.newBuilder()
@@ -79,6 +90,46 @@
.addData(METRIC_1_DATA)
.addData(METRIC_2_DATA))))
.build();
+ // A sample metrics report list with multiple reports on different atoms.
+ private static final ConfigMetricsReportList MULTIPLE_REPORT_LIST_PROTO_DIFFERENT_ATOMS =
+ ConfigMetricsReportList.newBuilder()
+ .addReports(
+ ConfigMetricsReport.newBuilder()
+ .addMetrics(
+ StatsLogReport.newBuilder()
+ .setEventMetrics(
+ StatsLogReport.EventMetricDataWrapper
+ .newBuilder()
+ .addData(METRIC_1_DATA))))
+ .addReports(
+ ConfigMetricsReport.newBuilder()
+ .addMetrics(
+ StatsLogReport.newBuilder()
+ .setEventMetrics(
+ StatsLogReport.EventMetricDataWrapper
+ .newBuilder()
+ .addData(METRIC_2_DATA))))
+ .build();
+ // A sample metrics report list with multiple reports on the same atom.
+ private static final ConfigMetricsReportList MULTIPLE_REPORT_LIST_PROTO_SAME_ATOM =
+ ConfigMetricsReportList.newBuilder()
+ .addReports(
+ ConfigMetricsReport.newBuilder()
+ .addMetrics(
+ StatsLogReport.newBuilder()
+ .setEventMetrics(
+ StatsLogReport.EventMetricDataWrapper
+ .newBuilder()
+ .addData(METRIC_1_DATA))))
+ .addReports(
+ ConfigMetricsReport.newBuilder()
+ .addMetrics(
+ StatsLogReport.newBuilder()
+ .setEventMetrics(
+ StatsLogReport.EventMetricDataWrapper
+ .newBuilder()
+ .addData(METRIC_3_DATA))))
+ .build();
// Implementation of Mockito's {@link Answer<T>} interface to write bytes to the
// {@link CollectingByteOutputReceiver} when mocking {@link ITestDevice#executeShellCommand}.
@@ -106,10 +157,10 @@
/** Test that a non-empty ConfigMetricsReportList is parsed correctly. */
@Test
- public void testNonEmptyMetricReportList() throws DeviceNotAvailableException, IOException {
- byte[] sampleReportBytes = SAMPLE_REPORT_LIST_PROTO.toByteArray();
+ public void testNonEmptyMetricReportList() throws DeviceNotAvailableException {
+ byte[] sampleReportBytes = SINGLE_REPORT_LIST_PROTO.toByteArray();
// Mock executeShellCommand to supply the passed-in CollectingByteOutputReceiver with
- // SAMPLE_REPORT_LIST_PROTO.
+ // SINGLE_REPORT_LIST_PROTO.
doAnswer(new ExecuteShellCommandWithOutputAnswer(sampleReportBytes))
.when(mTestDevice)
.executeShellCommand(
@@ -131,7 +182,7 @@
/** Test that an empty list is returned for an empty ConfigMetricsReportList proto. */
@Test
- public void testEmptyMetricReportList() throws DeviceNotAvailableException, IOException {
+ public void testEmptyMetricReportList() throws DeviceNotAvailableException {
byte[] emptyReportBytes = ConfigMetricsReport.newBuilder().build().toByteArray();
// Mock executeShellCommand to supply the passed-in CollectingByteOutputReceiver with the
// empty report list proto.
@@ -148,9 +199,66 @@
assertThat(data.size()).comparesEqualTo(0);
}
+ /**
+ * Test that the utility can process multiple {@link ConfigMetricsReport} in the {@link
+ * ConfigMetricsReportList} that contain metrics from different atoms.
+ */
+ @Test
+ public void testMultipleReportsInReportList_differentAtoms()
+ throws DeviceNotAvailableException {
+ byte[] sampleReportBytes = MULTIPLE_REPORT_LIST_PROTO_DIFFERENT_ATOMS.toByteArray();
+ // Mock executeShellCommand to supply the passed-in CollectingByteOutputReceiver with
+ // SINGLE_REPORT_LIST_PROTO.
+ doAnswer(new ExecuteShellCommandWithOutputAnswer(sampleReportBytes))
+ .when(mTestDevice)
+ .executeShellCommand(
+ matches(
+ String.format(
+ MetricUtil.DUMP_REPORT_CMD_TEMPLATE,
+ String.valueOf(CONFIG_ID))),
+ any(CollectingByteOutputReceiver.class));
+ List<EventMetricData> data = MetricUtil.getEventMetricData(mTestDevice, CONFIG_ID);
+ // Resulting list should have two metrics.
+ assertThat(data.size()).comparesEqualTo(2);
+ // The first metric should correspond to METRIC_2_* as its timestamp is earlier.
+ assertThat(data.get(0).getElapsedTimestampNanos()).comparesEqualTo(METRIC_2_NANOS);
+ assertThat(data.get(0).getAtom().hasBleScanResultReceived()).isTrue();
+ // The second metric should correspond to METRIC_1_*.
+ assertThat(data.get(1).getElapsedTimestampNanos()).comparesEqualTo(METRIC_1_NANOS);
+ assertThat(data.get(1).getAtom().hasBleScanStateChanged()).isTrue();
+ }
+
+ /**
+ * Test that the utility can process multiple {@link ConfigMetricsReport} in the {@link
+ * ConfigMetricsReportList} that contain metrics from the same atom.
+ */
+ @Test
+ public void testMultipleReportsInReportList_sameAtom() throws DeviceNotAvailableException {
+ byte[] sampleReportBytes = MULTIPLE_REPORT_LIST_PROTO_SAME_ATOM.toByteArray();
+ // Mock executeShellCommand to supply the passed-in CollectingByteOutputReceiver with
+ // SINGLE_REPORT_LIST_PROTO.
+ doAnswer(new ExecuteShellCommandWithOutputAnswer(sampleReportBytes))
+ .when(mTestDevice)
+ .executeShellCommand(
+ matches(
+ String.format(
+ MetricUtil.DUMP_REPORT_CMD_TEMPLATE,
+ String.valueOf(CONFIG_ID))),
+ any(CollectingByteOutputReceiver.class));
+ List<EventMetricData> data = MetricUtil.getEventMetricData(mTestDevice, CONFIG_ID);
+ // Resulting list should have two metrics.
+ assertThat(data.size()).comparesEqualTo(2);
+ // The first metric should correspond to METRIC_1_* as its timestamp is earlier.
+ assertThat(data.get(0).getElapsedTimestampNanos()).comparesEqualTo(METRIC_1_NANOS);
+ assertThat(data.get(0).getAtom().hasBleScanStateChanged()).isTrue();
+ // The second metric should correspond to METRIC_3_*.
+ assertThat(data.get(1).getElapsedTimestampNanos()).comparesEqualTo(METRIC_3_NANOS);
+ assertThat(data.get(1).getAtom().hasBleScanStateChanged()).isTrue();
+ }
+
/** Test that invalid proto from the dump report command causes an exception. */
@Test
- public void testInvalidDumpedReportThrows() throws DeviceNotAvailableException, IOException {
+ public void testInvalidDumpedReportThrows() throws DeviceNotAvailableException {
byte[] invalidProtoBytes = "not a proto".getBytes();
// Mock executeShellCommand to supply the passed-in CollectingByteOutputReceiver with the
// invalid proto bytes.
@@ -169,4 +277,34 @@
// Expected behavior, no action required.
}
}
+
+ /** Test that the utility can correctly retrieve statsd metadata. */
+ @Test
+ public void testGettingStatsdStats()
+ throws DeviceNotAvailableException, InvalidProtocolBufferException {
+ doAnswer(
+ new ExecuteShellCommandWithOutputAnswer(
+ StatsdStatsReport.newBuilder().build().toByteArray()))
+ .when(mTestDevice)
+ .executeShellCommand(
+ eq(MetricUtil.DUMP_STATSD_METADATA_CMD),
+ any(CollectingByteOutputReceiver.class));
+ assertThat(MetricUtil.getStatsdMetadata(mTestDevice)).isNotNull();
+ }
+
+ /** Test that the utility throws when the retrieved statsd metatdata is not valid. */
+ @Test
+ public void testInvalidStatsdStatsThrows() throws DeviceNotAvailableException {
+ doAnswer(new ExecuteShellCommandWithOutputAnswer("not a proto".getBytes()))
+ .when(mTestDevice)
+ .executeShellCommand(
+ eq(MetricUtil.DUMP_STATSD_METADATA_CMD),
+ any(CollectingByteOutputReceiver.class));
+ try {
+ MetricUtil.getStatsdMetadata(mTestDevice);
+ fail("An exception should be thrown for the invalid proto data.");
+ } catch (InvalidProtocolBufferException e) {
+ // Expected behavior, no action required.
+ }
+ }
}