Merge "Android Open Accessory helper"
diff --git a/libraries/aupt-lib/Android.bp b/libraries/aupt-lib/Android.bp
index 3f8a718..35191f4 100644
--- a/libraries/aupt-lib/Android.bp
+++ b/libraries/aupt-lib/Android.bp
@@ -17,6 +17,7 @@
java_library {
name: "AuptLib",
libs: [
+ "androidx.test.runner",
"ub-uiautomator",
"junit",
"android.test.runner.stubs",
@@ -32,6 +33,7 @@
platform_apis: true,
static_libs: [
+ "androidx.test.runner",
"ub-uiautomator",
"junit",
],
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java b/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java
index c5c986c..2400eea 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java
@@ -31,6 +31,8 @@
import android.test.InstrumentationTestRunner;
import android.util.Log;
+import androidx.test.InstrumentationRegistry;
+
import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestCase;
@@ -114,6 +116,9 @@
/* Test Initialization */
@Override
public void onCreate(Bundle params) {
+ // Support the newer AndroidX test initialization.
+ InstrumentationRegistry.registerInstance(this, params);
+
mParams = params;
// Parse out primitive parameters
diff --git a/libraries/collectors-helper/Android.bp b/libraries/collectors-helper/Android.bp
new file mode 100644
index 0000000..e6c1dc9
--- /dev/null
+++ b/libraries/collectors-helper/Android.bp
@@ -0,0 +1,29 @@
+// Copyright (C) 2018 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.
+
+java_library {
+ name: "collectors-helper",
+ defaults: ["tradefed_errorprone_defaults"],
+
+ srcs: ["src/**/*.java"],
+
+ static_libs: [
+ "platformprotoslite",
+ "statsdprotolite",
+ "androidx.test.runner",
+ "guava",
+ ],
+
+ platform_apis: true,
+}
diff --git a/libraries/collectors-helper/src/com/android/helpers/AppStartupHelper.java b/libraries/collectors-helper/src/com/android/helpers/AppStartupHelper.java
new file mode 100644
index 0000000..b0a9ba0
--- /dev/null
+++ b/libraries/collectors-helper/src/com/android/helpers/AppStartupHelper.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2018 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.helpers;
+
+import android.util.Log;
+
+import com.android.os.AtomsProto.AppStartOccurred;
+import com.android.os.AtomsProto.Atom;
+import com.android.os.StatsLog.EventMetricData;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * AppStartupHelper consist of helper methods to set the app
+ * startup configs in statsd to track the app startup related
+ * performance metrics and retrieve the necessary information from
+ * statsd using the config id.
+ */
+public class AppStartupHelper implements ICollectorHelper<StringBuilder> {
+
+ private static final String LOG_TAG = AppStartupHelper.class.getSimpleName();
+ private static final String COLD_STARTUP = "cold_startup";
+ private static final String WARM_STARTUP = "warm_startup";
+ private static final String HOT_STARTUP = "hot_startup";
+
+ private StatsdHelper mStatsdHelper = new StatsdHelper();
+
+ /**
+ * Set up the app startup statsd config to track the metrics during the app start occurred.
+ */
+ @Override
+ public boolean startCollecting() {
+ Log.i(LOG_TAG, "Adding AppStartOccured config to statsd.");
+ List<Integer> atomIdList = new ArrayList<>();
+ atomIdList.add(Atom.APP_START_OCCURRED_FIELD_NUMBER);
+ return mStatsdHelper.addEventConfig(atomIdList);
+ }
+
+ /**
+ * Collect the app startup metrics tracked during the app startup occurred from the statsd.
+ */
+ @Override
+ public Map<String, StringBuilder> getMetrics() {
+ List<EventMetricData> eventMetricData = mStatsdHelper.getEventMetrics();
+ Map<String, StringBuilder> appStartResultMap = new HashMap<>();
+ for (EventMetricData dataItem : eventMetricData) {
+ AppStartOccurred appStartAtom = dataItem.getAtom().getAppStartOccurred();
+ String pkgName = appStartAtom.getPkgName();
+ String transitionType = appStartAtom.getType().toString();
+ int windowsDrawnMillis = appStartAtom.getWindowsDrawnDelayMillis();
+ Log.i(LOG_TAG, String.format("Pkg Name: %s, Transition Type: %s,"
+ + " WindowDrawnDelayMillis:%s", pkgName, transitionType, windowsDrawnMillis));
+
+ String metricKey = "";
+ switch (appStartAtom.getType()) {
+ case COLD:
+ metricKey = MetricUtility.constructKey(COLD_STARTUP, appStartAtom.getPkgName());
+ break;
+ case WARM:
+ metricKey = MetricUtility.constructKey(WARM_STARTUP, appStartAtom.getPkgName());
+ break;
+ case HOT:
+ metricKey = MetricUtility.constructKey(HOT_STARTUP, appStartAtom.getPkgName());
+ break;
+ case UNKNOWN:
+ break;
+ }
+ if (!metricKey.isEmpty()) {
+ MetricUtility.addMetric(metricKey, windowsDrawnMillis, appStartResultMap);
+ }
+ }
+ return appStartResultMap;
+ }
+
+ /**
+ * Remove the statsd config used to track the app startup metrics.
+ */
+ @Override
+ public boolean stopCollecting() {
+ return mStatsdHelper.removeStatsConfig();
+ }
+}
diff --git a/libraries/collectors-helper/src/com/android/helpers/CpuUsageHelper.java b/libraries/collectors-helper/src/com/android/helpers/CpuUsageHelper.java
new file mode 100644
index 0000000..a2b3b3c
--- /dev/null
+++ b/libraries/collectors-helper/src/com/android/helpers/CpuUsageHelper.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2018 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.helpers;
+
+import android.util.Log;
+
+import com.android.os.AtomsProto.Atom;
+import com.android.os.StatsLog.GaugeBucketInfo;
+import com.android.os.StatsLog.GaugeMetricData;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * CpuUsageHelper consist of helper methods to set the app
+ * cpu usage configs in statsd to track the cpu usage related
+ * metrics and retrieve the necessary information from statsd
+ * using the config id.
+ */
+public class CpuUsageHelper implements ICollectorHelper<Long> {
+
+ private static final String LOG_TAG = CpuUsageHelper.class.getSimpleName();
+ private static final String CPU_USAGE_PKG_UID = "cpu_usage_pkg_or_uid";
+ private static final String CPU_USAGE_FREQ = "cpu_usage_freq";
+ private static final String TOTAL_CPU_USAGE = "total_cpu_usage";
+ private static final String TOTAL_CPU_USAGE_FREQ = "total_cpu_usage_freq";
+ private static final String CLUSTER_ID = "cluster";
+ private static final String FREQ_INDEX = "freq_index";
+ private static final String USER_TIME = "user_time";
+ private static final String SYSTEM_TIME = "system_time";
+
+ private StatsdHelper mStatsdHelper = new StatsdHelper();
+
+
+ @Override
+ public boolean startCollecting() {
+ Log.i(LOG_TAG, "Adding CpuUsage config to statsd.");
+ List<Integer> atomIdList = new ArrayList<>();
+ // Add the atoms to be tracked.
+ atomIdList.add(Atom.CPU_TIME_PER_UID_FIELD_NUMBER);
+ atomIdList.add(Atom.CPU_TIME_PER_FREQ_FIELD_NUMBER);
+
+ return mStatsdHelper.addGaugeConfig(atomIdList);
+ }
+
+
+ @Override
+ public Map<String, Long> getMetrics() {
+ Map<String, Long> cpuUsageFinalMap = new HashMap<>();
+
+ List<GaugeMetricData> gaugeMetricList = mStatsdHelper.getGaugeMetrics();
+ ListMultimap<String, Long> cpuUsageMap = ArrayListMultimap.create();
+
+ for (GaugeMetricData gaugeMetric : gaugeMetricList) {
+ Log.v(LOG_TAG, "Bucket Size: " + gaugeMetric.getBucketInfoCount());
+ for (GaugeBucketInfo gaugeBucketInfo : gaugeMetric.getBucketInfoList()) {
+ for (Atom atom : gaugeBucketInfo.getAtomList()) {
+
+ // Track CPU usage in user time and system time per package or UID
+ if (atom.getCpuTimePerUid().hasUid()) {
+ int uId = atom.getCpuTimePerUid().getUid();
+ String packageName = mStatsdHelper.getPackageName(uId);
+ long userTime = atom.getCpuTimePerUid().getUserTimeMillis();
+ long sysTime = atom.getCpuTimePerUid().getSysTimeMillis();
+ Log.v(LOG_TAG, String.format("Uid:%d, Pkg Name: %s, User_Time: %d,"
+ + " System_Time: %d", uId, packageName, userTime, sysTime));
+
+ // Use the package name if exist for the UID otherwise use the UID.
+ // Note: UID for the apps will be different across the builds.
+
+ // It is possible to have multiple bucket info. Track all the gauge info
+ // and take the difference of the first and last to compute the
+ // final usage.
+ String finalUserTimeKey = MetricUtility.constructKey(CPU_USAGE_PKG_UID,
+ (packageName == null) ? String.valueOf(uId) : packageName,
+ USER_TIME);
+ String finalSystemTimeKey = MetricUtility.constructKey(CPU_USAGE_PKG_UID,
+ (packageName == null) ? String.valueOf(uId) : packageName,
+ SYSTEM_TIME);
+ cpuUsageMap.put(finalUserTimeKey, userTime);
+ cpuUsageMap.put(finalSystemTimeKey, sysTime);
+ }
+
+ // Track cpu usage per cluster_id and freq_index
+ if (atom.getCpuTimePerFreq().hasFreqIndex()) {
+ int clusterId = atom.getCpuTimePerFreq().getCluster();
+ int freqIndex = atom.getCpuTimePerFreq().getFreqIndex();
+ long timeInFreq = atom.getCpuTimePerFreq().getTimeMillis();
+ Log.v(LOG_TAG, String.format("Cluster Id: %d FreqIndex: %d,"
+ + " Time_in_Freq: %d", clusterId, freqIndex, timeInFreq));
+ String finalFreqIndexKey = MetricUtility.constructKey(
+ CPU_USAGE_FREQ, CLUSTER_ID, String.valueOf(clusterId), FREQ_INDEX,
+ String.valueOf(freqIndex));
+ cpuUsageMap.put(finalFreqIndexKey, timeInFreq);
+ }
+
+ }
+ }
+ }
+
+ // Compute the final result map
+ Long totalCpuUsage = 0L;
+ Long totalCpuFreq = 0L;
+ for (String key : cpuUsageMap.keySet()) {
+ List<Long> cpuUsageList = cpuUsageMap.get(key);
+ if (cpuUsageList.size() > 1) {
+ // Compute the total usage by taking the difference of last and first value.
+ Long cpuUsage = cpuUsageList.get(cpuUsageList.size() - 1)
+ - cpuUsageList.get(0);
+ // Add the final result only if the cpu usage is greater than 0.
+ if (cpuUsage > 0) {
+ cpuUsageFinalMap.put(key, cpuUsage);
+ }
+ // Add the CPU time to their respective (usage or frequency) total metric.
+ if (key.startsWith(CPU_USAGE_PKG_UID)) {
+ totalCpuUsage += cpuUsage;
+ } else if (key.startsWith(CPU_USAGE_FREQ)) {
+ totalCpuFreq += cpuUsage;
+ }
+ }
+ }
+ // Put the total results into the final result map.
+ cpuUsageFinalMap.put(TOTAL_CPU_USAGE, totalCpuUsage);
+ cpuUsageFinalMap.put(TOTAL_CPU_USAGE_FREQ, totalCpuFreq);
+ return cpuUsageFinalMap;
+ }
+
+ /**
+ * Remove the statsd config used to track the cpu usage metrics.
+ */
+ @Override
+ public boolean stopCollecting() {
+ return mStatsdHelper.removeStatsConfig();
+ }
+}
diff --git a/libraries/collectors-helper/src/com/android/helpers/CrashHelper.java b/libraries/collectors-helper/src/com/android/helpers/CrashHelper.java
new file mode 100644
index 0000000..495a442
--- /dev/null
+++ b/libraries/collectors-helper/src/com/android/helpers/CrashHelper.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2018 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.helpers;
+
+import android.util.Log;
+
+import com.android.os.AtomsProto.ANROccurred;
+import com.android.os.AtomsProto.AppCrashOccurred;
+import com.android.os.AtomsProto.Atom;
+import com.android.os.StatsLog.EventMetricData;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * CrashHelper consist of helper methods to set the crash collection
+ * configs in statsd to track the crashes happened during the test
+ * and retrieve the necessary information from statsd using the config id.
+ */
+public class CrashHelper implements ICollectorHelper<Integer> {
+
+ private static final String LOG_TAG = CrashHelper.class.getSimpleName();
+ private static final String TOTAL_PREFIX = "total_";
+ private static final String EVENT_JAVA_CRASH = "crash";
+ private static final String EVENT_NATIVE_CRASH = "native_crash";
+ private static final String EVENT_ANR = "anr";
+
+ private StatsdHelper mStatsdHelper = new StatsdHelper();
+
+ /**
+ * Set up the app crash statsd config to track the crash metrics during the test.
+ */
+ @Override
+ public boolean startCollecting() {
+ Log.i(LOG_TAG, "Adding AppCrashOccured config to statsd.");
+ List<Integer> atomIdList = new ArrayList<>();
+ atomIdList.add(Atom.APP_CRASH_OCCURRED_FIELD_NUMBER);
+ atomIdList.add(Atom.ANR_OCCURRED_FIELD_NUMBER);
+ return mStatsdHelper.addEventConfig(atomIdList);
+ }
+
+ /**
+ * Collect the app crash metrics tracked during the test from the statsd.
+ */
+ @Override
+ public Map<String, Integer> getMetrics() {
+ List<EventMetricData> eventMetricData = mStatsdHelper.getEventMetrics();
+ Map<String, Integer> appCrashResultMap = new HashMap<>();
+ // We need this because even if there are no crashes we need to report 0 count
+ // in the dashboard for the total crash, native crash and ANR.
+ appCrashResultMap.put(TOTAL_PREFIX + EVENT_JAVA_CRASH, 0);
+ appCrashResultMap.put(TOTAL_PREFIX + EVENT_NATIVE_CRASH, 0);
+ appCrashResultMap.put(TOTAL_PREFIX + EVENT_ANR, 0);
+ for (EventMetricData dataItem : eventMetricData) {
+ if (dataItem.getAtom().hasAppCrashOccurred()) {
+ AppCrashOccurred appCrashAtom = dataItem.getAtom().getAppCrashOccurred();
+ String eventType = appCrashAtom.getEventType();
+ String pkgName = appCrashAtom.getPackageName();
+ AppCrashOccurred.ForegroundState foregroundState =
+ appCrashAtom.getForegroundState();
+ Log.i(LOG_TAG, String.format("Event Type:%s Pkg Name: %s "
+ + " ForegroundState: %s", eventType, pkgName, foregroundState.toString()));
+
+ // Track the total crash and native crash count.
+ MetricUtility.addMetric(TOTAL_PREFIX + eventType, appCrashResultMap);
+ // Add more detailed crash count key metrics.
+ String detailKey = MetricUtility.constructKey(eventType, pkgName,
+ foregroundState.toString());
+ MetricUtility.addMetric(detailKey, appCrashResultMap);
+ } else if (dataItem.getAtom().hasAnrOccurred()) {
+ ANROccurred anrAtom = dataItem.getAtom().getAnrOccurred();
+ String processName = anrAtom.getProcessName();
+ String reason = anrAtom.getReason();
+ ANROccurred.ForegroundState foregoundState = anrAtom.getForegroundState();
+ Log.i(LOG_TAG,
+ String.format("ANR occurred in process %s due to %s; foregound state is %s",
+ processName, reason, foregoundState.toString()));
+
+ // Track the total ANR count.
+ MetricUtility.addMetric(TOTAL_PREFIX + EVENT_ANR, appCrashResultMap);
+ String detailKey = MetricUtility.constructKey(
+ EVENT_ANR, processName, reason, foregoundState.toString());
+ MetricUtility.addMetric(detailKey, appCrashResultMap);
+ }
+ }
+ return appCrashResultMap;
+ }
+
+ /**
+ * Remove the statsd config used to track the app crash metrics.
+ */
+ @Override
+ public boolean stopCollecting() {
+ return mStatsdHelper.removeStatsConfig();
+ }
+}
diff --git a/libraries/collectors-helper/src/com/android/helpers/ICollectorHelper.java b/libraries/collectors-helper/src/com/android/helpers/ICollectorHelper.java
new file mode 100644
index 0000000..c5b4922
--- /dev/null
+++ b/libraries/collectors-helper/src/com/android/helpers/ICollectorHelper.java
@@ -0,0 +1,22 @@
+package com.android.helpers;
+
+import java.util.Map;
+
+public interface ICollectorHelper<T> {
+
+ /**
+ * This method will have setup to start collecting the metrics.
+ */
+ boolean startCollecting();
+
+ /**
+ * This method will retrieve the metrics.
+ */
+ Map<String, T> getMetrics();
+
+ /**
+ * This method will do the tear down to stop collecting the metrics.
+ */
+ boolean stopCollecting();
+
+}
diff --git a/libraries/collectors-helper/src/com/android/helpers/MetricUtility.java b/libraries/collectors-helper/src/com/android/helpers/MetricUtility.java
new file mode 100644
index 0000000..e251162
--- /dev/null
+++ b/libraries/collectors-helper/src/com/android/helpers/MetricUtility.java
@@ -0,0 +1,47 @@
+package com.android.helpers;
+
+import java.util.Map;
+
+/**
+ * MetricUtility consist of basic utility methods to construct the metrics
+ * reported at the end of the test.
+ */
+public class MetricUtility {
+
+ private static final String KEY_JOIN = "_";
+ private static final String METRIC_SEPARATOR = ",";
+
+ /**
+ * Append the given array of string to construct the final key used to track the metrics.
+ *
+ * @param keys to append using KEY_JOIN
+ */
+ public static String constructKey(String... keys) {
+ return String.join(KEY_JOIN, keys);
+ }
+
+ /**
+ * Add metric to the result map. If metric key already exist append the new metric.
+ *
+ * @param metricKey Unique key to track the metric.
+ * @param metric metric to track.
+ * @param resultMap map of all the metrics.
+ */
+ public static void addMetric(String metricKey, int metric, Map<String,
+ StringBuilder> resultMap) {
+ resultMap.compute(metricKey, (key, value) -> (value == null) ?
+ new StringBuilder().append(metric) : value.append(METRIC_SEPARATOR).append(metric));
+ }
+
+ /**
+ * Add metric to the result map. If metric key already exist increment the value by 1.
+ *
+ * @param metricKey Unique key to track the metric.
+ * @param resultMap map of all the metrics.
+ */
+ public static void addMetric(String metricKey, Map<String,
+ Integer> resultMap) {
+ resultMap.compute(metricKey, (key, value) -> (value == null) ? 1 : value + 1);
+ }
+
+}
diff --git a/libraries/collectors-helper/src/com/android/helpers/StatsdHelper.java b/libraries/collectors-helper/src/com/android/helpers/StatsdHelper.java
new file mode 100644
index 0000000..1333eeb
--- /dev/null
+++ b/libraries/collectors-helper/src/com/android/helpers/StatsdHelper.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2018 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.helpers;
+
+import android.content.Context;
+import android.app.StatsManager;
+import android.app.StatsManager.StatsUnavailableException;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.StatsLog;
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.os.StatsdConfigProto.AtomMatcher;
+import com.android.internal.os.StatsdConfigProto.EventMetric;
+import com.android.internal.os.StatsdConfigProto.FieldFilter;
+import com.android.internal.os.StatsdConfigProto.GaugeMetric;
+import com.android.internal.os.StatsdConfigProto.SimpleAtomMatcher;
+import com.android.internal.os.StatsdConfigProto.StatsdConfig;
+import com.android.internal.os.StatsdConfigProto.TimeUnit;
+import com.android.os.AtomsProto.Atom;
+import com.android.os.StatsLog.ConfigMetricsReport;
+import com.android.os.StatsLog.ConfigMetricsReportList;
+import com.android.os.StatsLog.EventMetricData;
+import com.android.os.StatsLog.GaugeMetricData;
+import com.android.os.StatsLog.StatsLogReport;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * StatsdHelper consist of basic utilities that will be used to setup statsd
+ * config, parse the collected information and remove the statsd config.
+ */
+public class StatsdHelper {
+ private static final String LOG_TAG = StatsdHelper.class.getSimpleName();
+ private static final long MAX_ATOMS = 2000;
+ private static final long METRIC_DELAY_MS = 3000;
+ private long mConfigId = -1;
+ private StatsManager mStatsManager;
+
+ /**
+ * Add simple event configurations using a list of atom ids.
+ *
+ * @param atomIdList uniquely identifies the information that we need to track by statsManager.
+ * @return true if the configuration is added successfully other wise false.
+ */
+ public boolean addEventConfig(List<Integer> atomIdList) {
+ long configId = System.currentTimeMillis();
+ StatsdConfig.Builder statsConfigBuilder = getSimpleSources(configId);
+
+ for (Integer atomId : atomIdList) {
+ int atomUniqueId = getUniqueId();
+ statsConfigBuilder
+ .addEventMetric(
+ EventMetric.newBuilder()
+ .setId(getUniqueId())
+ .setWhat(atomUniqueId))
+ .addAtomMatcher(getSimpleAtomMatcher(atomUniqueId, atomId));
+ }
+ try {
+ getStatsManager().addConfig(configId, statsConfigBuilder.build().toByteArray());
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Not able to setup the event config.", e);
+ return false;
+ }
+ Log.i(LOG_TAG, "Successfully added config with config-id:" + configId);
+ setConfigId(configId);
+ return true;
+ }
+
+ /**
+ * Build gauge metric config based on trigger events (i.e AppBreadCrumbReported).
+ * Whenever the events are triggered via StatsLog.logEvent() collect the gauge metrics.
+ * It doesn't matter what the log event is. It could be 0 or 1.
+ * In order to capture the usage during the test take the difference of gauge metrics
+ * before and after the test.
+ *
+ * @param atomIdList List of atoms to be collected in gauge metrics.
+ * @return if the config is added successfully otherwise false.
+ */
+ public boolean addGaugeConfig(List<Integer> atomIdList) {
+ long configId = System.currentTimeMillis();
+ StatsdConfig.Builder statsConfigBuilder = getSimpleSources(configId);
+ int appBreadCrumbUniqueId = getUniqueId();
+
+ // Needed for collecting gauge metric based on trigger events.
+ statsConfigBuilder.addAtomMatcher(getSimpleAtomMatcher(appBreadCrumbUniqueId,
+ Atom.APP_BREADCRUMB_REPORTED_FIELD_NUMBER));
+
+ for (Integer atomId : atomIdList) {
+ int atomUniqueId = getUniqueId();
+ // Build Gauge metric config.
+ GaugeMetric.Builder gaugeMetric = GaugeMetric.newBuilder()
+ .setId(getUniqueId())
+ .setWhat(atomUniqueId)
+ .setGaugeFieldsFilter(FieldFilter.newBuilder().setIncludeAll(true).build())
+ .setMaxNumGaugeAtomsPerBucket(MAX_ATOMS)
+ .setSamplingType(GaugeMetric.SamplingType.ALL_CONDITION_CHANGES)
+ .setTriggerEvent(appBreadCrumbUniqueId)
+ .setBucket(TimeUnit.CTS);
+
+ // add the gauge config.
+ statsConfigBuilder.addAtomMatcher(getSimpleAtomMatcher(atomUniqueId, atomId))
+ .addGaugeMetric(gaugeMetric.build());
+ }
+
+ try {
+ getStatsManager().addConfig(configId,
+ statsConfigBuilder.build().toByteArray());
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Not able to setup the gauge config.", e);
+ return false;
+ }
+
+ Log.i(LOG_TAG, "Successfully added config with config-id:" + configId);
+ setConfigId(configId);
+ return true;
+ }
+
+ /**
+ * Create simple atom matcher with the given id and the field id.
+ *
+ * @param id
+ * @param fieldId
+ * @return
+ */
+ private AtomMatcher.Builder getSimpleAtomMatcher(int id, int fieldId) {
+ return AtomMatcher.newBuilder()
+ .setId(id)
+ .setSimpleAtomMatcher(SimpleAtomMatcher.newBuilder()
+ .setAtomId(fieldId));
+ }
+
+ /**
+ * List of authorized source that can write the information into statsd.
+ *
+ * @param configId unique id of the configuration tracked by StatsManager.
+ * @return
+ */
+ private static StatsdConfig.Builder getSimpleSources(long configId) {
+ return StatsdConfig.newBuilder().setId(configId)
+ .addAllowedLogSource("AID_ROOT")
+ .addAllowedLogSource("AID_SYSTEM")
+ .addAllowedLogSource("AID_RADIO")
+ .addAllowedLogSource("AID_BLUETOOTH")
+ .addAllowedLogSource("AID_GRAPHICS")
+ .addAllowedLogSource("AID_STATSD")
+ .addAllowedLogSource("AID_INCIENTD");
+ }
+
+ /**
+ * Returns the list of EventMetricData tracked under the config.
+ */
+ public List<EventMetricData> getEventMetrics() {
+ ConfigMetricsReportList reportList = null;
+ List<EventMetricData> eventData = new ArrayList<>();
+ try {
+ if (getConfigId() != -1) {
+ reportList = ConfigMetricsReportList.parser()
+ .parseFrom(getStatsManager().getReports(getConfigId()));
+ }
+ } catch (InvalidProtocolBufferException | StatsUnavailableException se) {
+ Log.e(LOG_TAG, "Retreiving event metrics failed.", se);
+ return eventData;
+ }
+
+ if (reportList != null) {
+ ConfigMetricsReport configReport = reportList.getReports(0);
+ for (StatsLogReport metric : configReport.getMetricsList()) {
+ eventData.addAll(metric.getEventMetrics().getDataList());
+ }
+ }
+ Log.i(LOG_TAG, "Number of events: " + eventData.size());
+ return eventData;
+ }
+
+ /**
+ * Returns the list of GaugeMetric data tracked under the config.
+ */
+ public List<GaugeMetricData> getGaugeMetrics() {
+ // Dump the metric after the test is completed.
+ StatsLog.logEvent(0);
+ SystemClock.sleep(METRIC_DELAY_MS);
+ ConfigMetricsReportList reportList = null;
+ List<GaugeMetricData> gaugeData = new ArrayList<>();
+ try {
+ if (getConfigId() != -1) {
+ reportList = ConfigMetricsReportList.parser()
+ .parseFrom(getStatsManager().getReports(getConfigId()));
+ }
+ } catch (InvalidProtocolBufferException | StatsUnavailableException se) {
+ Log.e(LOG_TAG, "Retreiving gauge metrics failed.", se);
+ return gaugeData;
+ }
+
+ if (reportList != null) {
+ ConfigMetricsReport configReport = reportList.getReports(0);
+ for (StatsLogReport metric : configReport.getMetricsList()) {
+ gaugeData.addAll(metric.getGaugeMetrics().getDataList());
+ }
+ }
+ Log.i(LOG_TAG, "Number of Gauge data: " + gaugeData.size());
+ return gaugeData;
+ }
+
+ /**
+ * Remove the existing config tracked in the statsd.
+ *
+ * @return true if the config is removed successfully otherwise false.
+ */
+ public boolean removeStatsConfig() {
+ Log.i(LOG_TAG, "Removing statsd config-id: " + getConfigId());
+ try {
+ getStatsManager().removeConfig(getConfigId());
+ Log.i(LOG_TAG, "Successfully removed config-id: " + getConfigId());
+ return true;
+ } catch (StatsUnavailableException e) {
+ Log.e(LOG_TAG, String.format("Not able to remove the config-id: %d due to %s ",
+ getConfigId(), e.getMessage()));
+ return false;
+ }
+ }
+
+ /**
+ * StatsManager used to configure, collect and remove the statsd config.
+ *
+ * @return StatsManager
+ */
+ private StatsManager getStatsManager() {
+ if (mStatsManager == null) {
+ mStatsManager = (StatsManager) InstrumentationRegistry.getTargetContext().
+ getSystemService(Context.STATS_MANAGER);
+ }
+ return mStatsManager;
+ }
+
+ /**
+ * Returns the package name for the UID if it is available. Otherwise return null.
+ *
+ * @param uid
+ * @return
+ */
+ public String getPackageName(int uid) {
+ String pkgName = InstrumentationRegistry.getTargetContext().getPackageManager()
+ .getNameForUid(uid);
+ // Remove the UID appended at the end of the package name.
+ if (pkgName != null) {
+ String pkgNameSplit[] = pkgName.split(String.format("\\:%d", uid));
+ return pkgNameSplit[0];
+ }
+ return pkgName;
+ }
+
+ /**
+ * Set the config id tracked in the statsd.
+ */
+ private void setConfigId(long configId) {
+ mConfigId = configId;
+ }
+
+ /**
+ * Return the config id tracked in the statsd.
+ */
+ private long getConfigId() {
+ return mConfigId;
+ }
+
+ /**
+ * Returns the unique id
+ */
+ private int getUniqueId() {
+ return UUID.randomUUID().hashCode();
+ }
+}
diff --git a/libraries/collectors-helper/test/Android.bp b/libraries/collectors-helper/test/Android.bp
new file mode 100644
index 0000000..116906a
--- /dev/null
+++ b/libraries/collectors-helper/test/Android.bp
@@ -0,0 +1,31 @@
+// Copyright (C) 2018 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.
+
+java_library {
+ name: "collectors-helper-test",
+ defaults: ["tradefed_errorprone_defaults"],
+
+ srcs: ["src/**/*.java"],
+
+ static_libs: [
+ "androidx.test.runner",
+ "android-support-test",
+ "collectors-helper",
+ "app-helpers-handheld-interfaces",
+ "junit",
+ "ub-uiautomator",
+ ],
+
+ platform_apis: true,
+}
diff --git a/libraries/collectors-helper/test/src/com/android/helpers/tests/AppStartupHelperTest.java b/libraries/collectors-helper/test/src/com/android/helpers/tests/AppStartupHelperTest.java
new file mode 100644
index 0000000..20605c1
--- /dev/null
+++ b/libraries/collectors-helper/test/src/com/android/helpers/tests/AppStartupHelperTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2018 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.helpers.tests;
+
+import android.os.SystemClock;
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.ICalendarHelper;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.helpers.AppStartupHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Android Unit tests for {@link AppStartupHelper}.
+ *
+ * To run:
+ * Connect to wifi and login to gmail.
+ * atest CollectorsHelperTest:com.android.helpers.tests.AppStartupHelperTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class AppStartupHelperTest {
+
+ // Kill the calendar app.
+ private static final String KILL_TEST_APP_CMD_TEMPLATE = "am force-stop %s";
+ // Package names used for testing
+ private static final String CALENDAR_PKG_NAME = "com.google.android.calendar";
+ private static final String CALCULATOR_PKG_NAME = "com.google.android.calculator";
+ // Key prefixes to store the cold, warm or hot launch time of the calendar app, respectively.
+ private static final String COLD_LAUNCH_KEY_TEMPLATE = "cold_startup_%s";
+ private static final String WARM_LAUNCH_KEY_TEMPLATE = "warm_startup_%s";
+ private static final String HOT_LAUNCH_KEY_TEMPLATE = "hot_startup_%s";
+
+ // A couple of ADB commands to help with the calculator hot launch test.
+ private static final String LUANCH_APP_CMD_TEMPLATE =
+ "monkey -p %s -c android.intent.category.LAUNCHER 1";
+ private static final String KEYEVENT_CMD_TEMPLATE = "input keyevent %s";
+ private static final String KEYCODE_HOME = "KEYCODE_HOME";
+ private static final String KEYCODE_WAKEUP = "KEYCODE_WAKEUP";
+ private static final String KEYCODE_UNLOCK = "KEYCODE_MENU";
+
+ private AppStartupHelper mAppStartupHelper = new AppStartupHelper();
+ private HelperAccessor<ICalendarHelper> mHelper =
+ new HelperAccessor<>(ICalendarHelper.class);
+
+ @Before
+ public void setUp() {
+ mAppStartupHelper = new AppStartupHelper();
+ // Make sure the apps are starting from the clean state.
+ HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CALENDAR_PKG_NAME));
+ HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CALCULATOR_PKG_NAME));
+ }
+
+ /**
+ * Test successfull app launch config.
+ */
+ @Test
+ public void testAppLaunchConfig() throws Exception {
+ assertTrue(mAppStartupHelper.startCollecting());
+ assertTrue(mAppStartupHelper.stopCollecting());
+ }
+
+ /**
+ * Test no error is thrown if there is no app launch.
+ */
+ @Test
+ public void testEmptyAppLaunchMetric() throws Exception {
+ assertTrue(mAppStartupHelper.startCollecting());
+ assertTrue(mAppStartupHelper.getMetrics().isEmpty());
+ assertTrue(mAppStartupHelper.stopCollecting());
+ }
+
+ /**
+ * Test cold launch key.
+ */
+ @Test
+ public void testColdLaunchMetricKey() throws Exception {
+ assertTrue(mAppStartupHelper.startCollecting());
+ mHelper.get().open();
+ Map.Entry<String, StringBuilder> appLaunchEntry = mAppStartupHelper.getMetrics().entrySet()
+ .iterator().next();
+ assertEquals(String.format(COLD_LAUNCH_KEY_TEMPLATE, CALENDAR_PKG_NAME),
+ appLaunchEntry.getKey());
+ assertTrue(mAppStartupHelper.stopCollecting());
+ mHelper.get().exit();
+ }
+
+ /**
+ * Test cold launch metric.
+ */
+ @Test
+ public void testSingleColdLaunchMetric() throws Exception {
+ assertTrue(mAppStartupHelper.startCollecting());
+ mHelper.get().open();
+ assertEquals(1, mAppStartupHelper.getMetrics().size());
+ assertTrue(mAppStartupHelper.stopCollecting());
+ mHelper.get().exit();
+ }
+
+ /**
+ * Test multiple cold launch metric.
+ */
+ @Test
+ public void testMultipleColdLaunchMetric() throws Exception {
+ assertTrue(mAppStartupHelper.startCollecting());
+ mHelper.get().open();
+ SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+ mHelper.get().exit();
+ HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CALENDAR_PKG_NAME));
+ mHelper.get().open();
+ Map.Entry<String, StringBuilder> appLaunchEntry = mAppStartupHelper.getMetrics().entrySet()
+ .iterator().next();
+ assertEquals(2, appLaunchEntry.getValue().toString().split(",").length);
+ assertTrue(mAppStartupHelper.stopCollecting());
+ mHelper.get().exit();
+ }
+
+ /**
+ * Test warm launch metric.
+ */
+ @Test
+ public void testWarmLaunchMetric() throws Exception {
+
+ // Launch the app once and exit it so it resides in memory.
+ mHelper.get().open();
+ SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+ // Press home and clear the cache explicitly.
+ HelperTestUtility.executeShellCommand(String.format(KEYEVENT_CMD_TEMPLATE, KEYCODE_HOME));
+ HelperTestUtility.clearCache();
+ SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+ // Start the collection here to test warm launch.
+ assertTrue(mAppStartupHelper.startCollecting());
+ // Launch the app; a warm launch occurs.
+ mHelper.get().open();
+ SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+ Map<String, StringBuilder> appLaunchMetrics = mAppStartupHelper.getMetrics();
+ String calendarWarmLaunchKey = String.format(WARM_LAUNCH_KEY_TEMPLATE, CALENDAR_PKG_NAME);
+ assertTrue(appLaunchMetrics.keySet().contains(calendarWarmLaunchKey));
+ assertEquals(1, appLaunchMetrics.get(calendarWarmLaunchKey).toString().split(",").length);
+ assertTrue(mAppStartupHelper.stopCollecting());
+ mHelper.get().exit();
+ }
+
+ /**
+ * Test hot launch metric on calculator, which is lightweight enough to trigger a hot launch.
+ */
+ @Test
+ public void testHotLaunchMetric() throws Exception {
+ String calculatorLaunchCommand =
+ String.format(LUANCH_APP_CMD_TEMPLATE, CALCULATOR_PKG_NAME);
+ // Launch the app once and go home so the app resides in memory.
+ HelperTestUtility.executeShellCommand(String.format(KEYEVENT_CMD_TEMPLATE, KEYCODE_WAKEUP));
+ SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+ HelperTestUtility.executeShellCommand(String.format(KEYEVENT_CMD_TEMPLATE, KEYCODE_UNLOCK));
+ SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+ HelperTestUtility.executeShellCommand(calculatorLaunchCommand);
+ SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+ HelperTestUtility.executeShellCommand(String.format(KEYEVENT_CMD_TEMPLATE, KEYCODE_HOME));
+ // Start the collection here to test hot launch.
+ assertTrue(mAppStartupHelper.startCollecting());
+ SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+ // Launch the app; a hot launch occurs.
+ HelperTestUtility.executeShellCommand(calculatorLaunchCommand);
+ SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+ Map<String, StringBuilder> appLaunchMetrics = mAppStartupHelper.getMetrics();
+ String calculatoHotLaunchKey = String.format(HOT_LAUNCH_KEY_TEMPLATE, CALCULATOR_PKG_NAME);
+ assertTrue(appLaunchMetrics.keySet().contains(calculatoHotLaunchKey));
+ assertEquals(1, appLaunchMetrics.get(calculatoHotLaunchKey).toString().split(",").length);
+ assertTrue(mAppStartupHelper.stopCollecting());
+ HelperTestUtility.executeShellCommand(String.format(KEYEVENT_CMD_TEMPLATE, KEYCODE_HOME));
+ SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+ HelperTestUtility.clearApp(String.format(KILL_TEST_APP_CMD_TEMPLATE, CALCULATOR_PKG_NAME));
+ }
+}
+
diff --git a/libraries/collectors-helper/test/src/com/android/helpers/tests/CpuUsageHelperTest.java b/libraries/collectors-helper/test/src/com/android/helpers/tests/CpuUsageHelperTest.java
new file mode 100644
index 0000000..1cf74d3
--- /dev/null
+++ b/libraries/collectors-helper/test/src/com/android/helpers/tests/CpuUsageHelperTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2018 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.helpers.tests;
+
+import android.platform.helpers.HelperAccessor;
+import android.platform.helpers.ICalendarHelper;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.helpers.CpuUsageHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Android Unit tests for {@link CpuUsageHelperTest}.
+ *
+ * To run:
+ * atest CollectorsHelperTest:com.android.helpers.tests.CpuUsageHelperTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class CpuUsageHelperTest {
+
+ // Kill the calendar app.
+ private static final String KILL_TEST_APP_CMD = "am force-stop com.google.android.calendar";
+ // Key prefix used for cpu usage by frequency index.
+ private static final String CPU_USAGE_FREQ_PREFIX = "cpu_usage_freq";
+ // Key prefix used for cpu usage by package name or uid
+ private static final String CPU_USAGE_PKG_UID_PREFIX = "cpu_usage_pkg_or_uid";
+ // Key used for total CPU usage
+ private static final String TOTAL_CPU_USAGE = "total_cpu_usage";
+ // Key used for total CPU usage in frequency buckets
+ private static final String TOTAL_CPU_USAGE_FREQ = "total_cpu_usage_freq";
+
+ private CpuUsageHelper mCpuUsageHelper = new CpuUsageHelper();
+ private HelperAccessor<ICalendarHelper> mHelper =
+ new HelperAccessor<>(ICalendarHelper.class);
+
+ @Before
+ public void setUp() {
+ mCpuUsageHelper = new CpuUsageHelper();
+ }
+
+ /**
+ * Test successfull cpu usage config.
+ */
+ @Test
+ public void testCpuUsageConfig() throws Exception {
+ assertTrue(mCpuUsageHelper.startCollecting());
+ assertTrue(mCpuUsageHelper.stopCollecting());
+ }
+
+ /**
+ * Test cpu usage metrics are collected.
+ */
+ @Test
+ public void testCpuUsageMetrics() throws Exception {
+ assertTrue(mCpuUsageHelper.startCollecting());
+ mHelper.get().open();
+ Map<String, Long> cpuUsage = mCpuUsageHelper.getMetrics();
+ assertTrue(cpuUsage.size() > 0);
+ assertTrue(mCpuUsageHelper.stopCollecting());
+ mHelper.get().exit();
+ }
+
+ /**
+ * Test that at least one cpu usage per pkg or uid and per preq index is collected,
+ * the total usage is collected, and that
+ * the total usage is indeed the sum of the per pkg/uid and frequency usage, respectively.
+ */
+ @Test
+ public void testCpuUsageMetricsKey() throws Exception {
+ // Variables to verify existence of collected metrics.
+ boolean isFreqIndexPresent = false;
+ boolean isPkgorUidPresent = false;
+ boolean isFreqUsed = false;
+ boolean isUIDUsed = false;
+ boolean isTotalCpuUsageEntryPresent = false;
+ boolean isTotalCpuUsageValuePresent = false;
+ boolean isTotalCpuFreqEntryPresent = false;
+ boolean isTotalCpuFreqValuePresent = false;
+ assertTrue(mCpuUsageHelper.startCollecting());
+ mHelper.get().open();
+ // Variables to Verify that the reported usage does sum up to the reported total usage.
+ Long sumCpuUsage = 0L;
+ Long sumCpuUsageFreq = 0L;
+ Long reportedTotalCpuUsage = 0L;
+ Long reportedTotalCpuUsageFreq = 0L;
+ for (Map.Entry<String, Long> cpuUsageEntry : mCpuUsageHelper.getMetrics().entrySet()) {
+ if (cpuUsageEntry.getKey().startsWith(CPU_USAGE_FREQ_PREFIX)) {
+ isFreqIndexPresent = true;
+ if (cpuUsageEntry.getValue() > 0) {
+ isFreqUsed = true;
+ }
+ sumCpuUsageFreq += cpuUsageEntry.getValue();
+ }
+ if (cpuUsageEntry.getKey().startsWith(CPU_USAGE_PKG_UID_PREFIX)) {
+ isPkgorUidPresent = true;
+ if (cpuUsageEntry.getValue() > 0) {
+ isUIDUsed = true;
+ }
+ sumCpuUsage += cpuUsageEntry.getValue();
+ }
+ if (cpuUsageEntry.getKey().equals(TOTAL_CPU_USAGE_FREQ)) {
+ isTotalCpuFreqEntryPresent = true;
+ if (cpuUsageEntry.getValue() > 0) {
+ isTotalCpuFreqValuePresent = true;
+ }
+ reportedTotalCpuUsageFreq = cpuUsageEntry.getValue();
+ }
+ if (cpuUsageEntry.getKey().equals(TOTAL_CPU_USAGE)) {
+ isTotalCpuUsageEntryPresent = true;
+ if (cpuUsageEntry.getValue() > 0) {
+ isTotalCpuUsageValuePresent = true;
+ }
+ reportedTotalCpuUsage = cpuUsageEntry.getValue();
+ }
+ }
+ assertTrue(isFreqIndexPresent && isFreqUsed);
+ assertTrue(isPkgorUidPresent && isUIDUsed);
+ assertTrue(isTotalCpuUsageEntryPresent && isTotalCpuUsageValuePresent);
+ assertTrue(isTotalCpuFreqEntryPresent && isTotalCpuFreqValuePresent);
+ assertEquals(sumCpuUsageFreq, reportedTotalCpuUsageFreq);
+ assertEquals(sumCpuUsage, reportedTotalCpuUsage);
+ assertTrue(mCpuUsageHelper.stopCollecting());
+ mHelper.get().exit();
+ }
+
+}
+
diff --git a/libraries/collectors-helper/test/src/com/android/helpers/tests/CrashHelperTest.java b/libraries/collectors-helper/test/src/com/android/helpers/tests/CrashHelperTest.java
new file mode 100644
index 0000000..125490f
--- /dev/null
+++ b/libraries/collectors-helper/test/src/com/android/helpers/tests/CrashHelperTest.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2018 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.helpers.tests;
+
+import android.os.SystemClock;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.By;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.helpers.CrashHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Android Unit tests for {@link CrashHelper}.
+ *
+ * To run:
+ * atest CollectorsHelperTest:com.android.helpers.tests.CrashHelperTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class CrashHelperTest {
+
+ // Launch Development app BadBehavior activity.
+ private static final String START_APP =
+ "am start -W -n com.android.development/.BadBehaviorActivity";
+ // Kill the Development app
+ private static final String KILL_TEST_APP_CMD = "am force-stop com.android.development";
+ // Package name of the development app.
+ private static final String PKG_NAME = "com.android.development";
+ // Key used to store the crash count.
+ private static final String TOTAL_CRASHES_KEY = "total_crash";
+ // Key used to store the native crash.
+ private static final String TOTAL_NATIVE_CRASHES_KEY = "total_native_crash";
+ // Key used to store the ANR.
+ private static final String TOTAL_ANRS_KEY = "total_anr";
+ // Detailed crash key associated with the package name and the foreground status.
+ private static final String CRASH_PKG_KEY = "crash_com.android.development_FOREGROUND";
+ // Detailed native crash key associated with the package name and the foreground status.
+ private static final String NATIVE_CRASH_PKG_KEY =
+ "native_crash_com.android.development_FOREGROUND";
+ // Detailed event key associated with the ANR: process, reason and foreground status.
+ private static final String ANR_DETAIL_KEY = "anr_com.android.development"
+ + "_executing service com.android.development/.BadBehaviorActivity$BadService"
+ + "_FOREGROUND";
+ // Button id to cause the crash.
+ private static final String CRASH_BTN_NAME = "bad_behavior_crash_main";
+ // Button id to cause the native crash.
+ private static final String NATIVE_CRASH_BTN_NAME = "bad_behavior_crash_native";
+ // Button id to cause the an ANR (not all ANR-related buttons work, but this one does).
+ private static final String ANR_SERVICE_BTN_NAME = "bad_behavior_anr_service";
+ // This delay ensures that an ANR is actually logged.
+ // For details, see BadBehaviorActivity.BadService.
+ private static final int ANR_DELAY = 40000;
+
+ private CrashHelper mCrashHelper = new CrashHelper();
+
+ @Before
+ public void setUp() {
+ mCrashHelper = new CrashHelper();
+ // Make the apps are starting from the clean state.
+ HelperTestUtility.clearApp(KILL_TEST_APP_CMD);
+ }
+
+ /**
+ * Test successfull crash config.
+ */
+ @Test
+ public void testCrashConfig() throws Exception {
+ assertTrue(mCrashHelper.startCollecting());
+ assertTrue(mCrashHelper.stopCollecting());
+ }
+
+ /**
+ * Test no crash metrics.
+ */
+ @Test
+ public void testEmptyCrashMetric() throws Exception {
+ assertTrue(mCrashHelper.startCollecting());
+ Map<String, Integer> crashMap = mCrashHelper.getMetrics();
+ // "crash", "native_crash" and "anr" keys with value 0
+ assertEquals(3, crashMap.size());
+ assertEquals(0, crashMap.get(TOTAL_CRASHES_KEY).intValue());
+ assertEquals(0, crashMap.get(TOTAL_NATIVE_CRASHES_KEY).intValue());
+ assertEquals(0, crashMap.get(TOTAL_ANRS_KEY).intValue());
+ assertTrue(mCrashHelper.stopCollecting());
+
+ }
+
+ /**
+ * Test one crash metric.
+ */
+ @Test
+ public void testCrashMetric() throws Exception {
+ assertTrue(mCrashHelper.startCollecting());
+ HelperTestUtility.executeShellCommand(START_APP);
+ invokeBehavior(CRASH_BTN_NAME);
+ Map<String, Integer> crashMap = mCrashHelper.getMetrics();
+ // An empty ANR key in addition to the crash keys.
+ assertEquals(4, crashMap.size());
+ assertEquals(1, crashMap.get(TOTAL_CRASHES_KEY).intValue());
+ assertEquals(0, crashMap.get(TOTAL_NATIVE_CRASHES_KEY).intValue());
+ assertEquals(1, crashMap.get(CRASH_PKG_KEY).intValue());
+ assertEquals(0, crashMap.get(TOTAL_ANRS_KEY).intValue());
+ assertTrue(mCrashHelper.stopCollecting());
+
+ }
+
+ /**
+ * Test native crash metric.
+ */
+ @Test
+ public void testNativeCrashMetric() throws Exception {
+ assertTrue(mCrashHelper.startCollecting());
+ HelperTestUtility.executeShellCommand(START_APP);
+ invokeBehavior(NATIVE_CRASH_BTN_NAME);
+ Map<String, Integer> crashMap = mCrashHelper.getMetrics();
+ // An empty ANR key in addition to the crash keys.
+ assertEquals(4, crashMap.size());
+ assertEquals(0, crashMap.get(TOTAL_CRASHES_KEY).intValue());
+ assertEquals(1, crashMap.get(TOTAL_NATIVE_CRASHES_KEY).intValue());
+ assertEquals(1, crashMap.get(NATIVE_CRASH_PKG_KEY).intValue());
+ assertEquals(0, crashMap.get(TOTAL_ANRS_KEY).intValue());
+ assertTrue(mCrashHelper.stopCollecting());
+
+ }
+
+ /**
+ * Test ANR metric.
+ */
+ @Test
+ public void testAnrMetric() throws Exception {
+ assertTrue(mCrashHelper.startCollecting());
+ HelperTestUtility.executeShellCommand(START_APP);
+ // The 30100ms sleep time guarantees that an ANR is indeed triggered.
+ // See BadBehaviorActivity.BadService for details.
+ invokeBehavior(ANR_SERVICE_BTN_NAME, ANR_DELAY);
+ Map<String, Integer> crashMap = mCrashHelper.getMetrics();
+ // Two ANR keys and two empty crash keys.
+ assertEquals(4, crashMap.size());
+ assertEquals(1, crashMap.get(TOTAL_ANRS_KEY).intValue());
+ assertEquals(1, crashMap.get(ANR_DETAIL_KEY).intValue());
+ assertEquals(0, crashMap.get(TOTAL_CRASHES_KEY).intValue());
+ assertEquals(0, crashMap.get(TOTAL_NATIVE_CRASHES_KEY).intValue());
+ assertTrue(mCrashHelper.stopCollecting());
+ }
+
+ /**
+ * Test both crash and native crash.
+ */
+ @Test
+ public void testMultipleCrashMetric() throws Exception {
+ assertTrue(mCrashHelper.startCollecting());
+
+ // Invoke the crash
+ HelperTestUtility.executeShellCommand(START_APP);
+ invokeBehavior(CRASH_BTN_NAME);
+
+ // Invoke the native crash
+ HelperTestUtility.executeShellCommand(START_APP);
+ invokeBehavior(NATIVE_CRASH_BTN_NAME);
+
+ // Invoke the ANR.
+ HelperTestUtility.executeShellCommand(START_APP);
+ invokeBehavior(ANR_SERVICE_BTN_NAME, ANR_DELAY);
+
+ Map<String, Integer> crashMap = mCrashHelper.getMetrics();
+ // Two keys for each crash metric, totaling 6.
+ assertEquals(6, crashMap.size());
+ assertEquals(1, crashMap.get(TOTAL_CRASHES_KEY).intValue());
+ assertEquals(1, crashMap.get(CRASH_PKG_KEY).intValue());
+ assertEquals(1, crashMap.get(TOTAL_NATIVE_CRASHES_KEY).intValue());
+ assertEquals(1, crashMap.get(NATIVE_CRASH_PKG_KEY).intValue());
+ assertEquals(1, crashMap.get(TOTAL_ANRS_KEY).intValue());
+ assertEquals(1, crashMap.get(ANR_DETAIL_KEY).intValue());
+ assertTrue(mCrashHelper.stopCollecting());
+ }
+
+ /**
+ * Cause the behavior by clicking on the button in bad behaviour activity.
+ */
+ private void invokeBehavior(String resourceName, int delayAfterAction) {
+ UiObject2 behaviorButton = HelperTestUtility.getUiDevice().findObject(
+ By.res(PKG_NAME, resourceName));
+ behaviorButton.click();
+ // Some actions, e.g. service ANR, requires a delay to register.
+ SystemClock.sleep(delayAfterAction);
+
+ // Dismiss the crash dialog which sometimes appear.
+ UiObject2 closeButton = HelperTestUtility.getUiDevice().findObject(
+ By.res("android", "aerr_close"));
+ if (closeButton != null) {
+ closeButton.click();
+ }
+ SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+ }
+
+ /**
+ * Convenience function for "immediate" actions such as crashes.
+ */
+ private void invokeBehavior(String resourceName) {
+ invokeBehavior(resourceName, 0);
+ }
+}
diff --git a/libraries/collectors-helper/test/src/com/android/helpers/tests/HelperTestUtility.java b/libraries/collectors-helper/test/src/com/android/helpers/tests/HelperTestUtility.java
new file mode 100644
index 0000000..54f94d1
--- /dev/null
+++ b/libraries/collectors-helper/test/src/com/android/helpers/tests/HelperTestUtility.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2018 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.helpers.tests;
+
+import android.os.SystemClock;
+import android.support.test.uiautomator.UiDevice;
+import androidx.test.InstrumentationRegistry;
+
+import java.io.IOException;
+
+public class HelperTestUtility {
+
+ // Clear the cache
+ private static final String CLEAR_CACHE = "echo 3 > /proc/sys/vm/drop_caches";
+ // Delay between actions happening in the device.
+ public static final int ACTION_DELAY = 2000;
+
+ private static UiDevice mDevice;
+
+ /**
+ * Returns the active {@link UiDevice} to interact with.
+ */
+ public static UiDevice getUiDevice() {
+ if (mDevice == null) {
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ }
+ return mDevice;
+ }
+
+ /**
+ * Runs a shell command, {@code cmd}, and returns the output.
+ */
+ public static String executeShellCommand(String cmd) {
+ try {
+ return getUiDevice().executeShellCommand(cmd);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Clears the cache.
+ */
+ public static void clearCache() {
+ SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+ executeShellCommand(CLEAR_CACHE);
+ SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+ }
+
+ /**
+ * Close the test app and clear the cache.
+ * TODO: Replace it with the rules.
+ */
+ public static void clearApp(String killCmd) {
+ SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+ executeShellCommand(killCmd);
+ SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+ executeShellCommand(CLEAR_CACHE);
+ SystemClock.sleep(HelperTestUtility.ACTION_DELAY);
+ }
+
+}
+
diff --git a/libraries/composer/host/Android.bp b/libraries/composer/host/Android.bp
index d8e4f10..aa12779 100644
--- a/libraries/composer/host/Android.bp
+++ b/libraries/composer/host/Android.bp
@@ -12,9 +12,47 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+java_genrule {
+ name: "profile-text-protos",
+ host_supported: true,
+ tools: ["aprotoc"],
+ srcs: [
+ "src/**/profile.proto",
+ "res/*.textpb",
+ ],
+ out: ["profiles.jar"],
+ /*
+ * Loops over all *.textpb files under res/ and serializes them into binary protos with ".pb"
+ * extension using aprotoc. The generated *.pb files are put under an assets/ directory, which
+ * gets packed into a .jar file and ends being under the assets/ directory in the apk package,
+ * which can then be read by the app's asset manager.
+ *
+ * If a profile fails to parse, an error is thrown and the build will fail.
+ */
+ cmd: "out_dir=$$(dirname $(out)) && assets_dir=\"assets\" && mkdir -p $$out_dir/$$assets_dir " +
+ "&& src_protos=($(locations res/*.textpb)) && red='\033[0;31m' && no_color='\033[0m' " +
+ "&& for file in $${src_protos[@]} ; do fname=$$(basename $$file) " +
+ "&& if ! ($(location aprotoc) --encode=composer.profile.Configuration " +
+ "$(location src/**/profile.proto) < $$file > " +
+ "$$out_dir/$$assets_dir/$${fname//.textpb/.pb}) ; then " +
+ "echo \"$${red}Failed to parse profile $$file. See above for errors.$${no_color}\" " +
+ "&& exit 1 ; fi ; done && jar cf $(out) -C $$(dirname $(out)) $$assets_dir",
+}
+
java_library_static {
name: "test-composers",
- srcs: [ "src/**/*.java" ],
+ static_libs: [
+ "junit",
+ "profile-text-protos",
+ ],
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.proto",
+ ],
+ proto: {
+ type: "lite",
+ include_dirs: ["external/protobuf/src"],
+ },
sdk_version: "current",
host_supported: true,
}
diff --git a/libraries/composer/host/res/sample_profile.textpb b/libraries/composer/host/res/sample_profile.textpb
new file mode 100644
index 0000000..a80d08f
--- /dev/null
+++ b/libraries/composer/host/res/sample_profile.textpb
@@ -0,0 +1,19 @@
+scheduled {
+ if_early: IDLE
+}
+scenarios [{
+ at: "00:00:10"
+ journey: "android.platform.test.scenario.calendar.FlingDayPage"
+}, {
+ at: "00:01:00"
+ journey: "android.platform.test.scenario.calendar.FlingWeekPage"
+}, {
+ at: "00:02:00"
+ journey: "android.platform.test.scenario.gmail.OpenApp"
+}, {
+ at: "00:03:00"
+ journey: "android.platform.test.scenario.quicksettings.ToggleWifiOff"
+}, {
+ at: "00:04:00"
+ journey: "android.platform.test.scenario.quicksettings.ToggleWifiOn"
+}]
diff --git a/libraries/composer/host/src/android/host/test/composer/Profile.java b/libraries/composer/host/src/android/host/test/composer/Profile.java
new file mode 100644
index 0000000..51ff7b9
--- /dev/null
+++ b/libraries/composer/host/src/android/host/test/composer/Profile.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2018 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 android.host.test.composer;
+
+import android.host.test.composer.profile.Configuration;
+import android.host.test.composer.profile.Configuration.Scenario;
+import android.host.test.composer.profile.Configuration.Scheduled;
+import android.host.test.composer.profile.Configuration.Scheduled.IfEarly;
+import android.host.test.composer.profile.Configuration.Scheduled.IfLate;
+
+import java.util.Map;
+
+/**
+ * An extension of {@link android.host.test.composer.ProfileBase} for host-side testing.
+ */
+public class Profile extends ProfileBase<Map<String, String>> {
+ @Override
+ protected Configuration getConfigurationArgument(Map<String, String> args) {
+ if (!args.containsKey(PROFILE_OPTION_NAME)) {
+ return null;
+ }
+ return getStubConfiguration();
+ }
+
+ private Configuration getStubConfiguration() {
+ return Configuration.newBuilder()
+ .setScheduled(
+ Scheduled.newBuilder()
+ .setIfEarly(IfEarly.SLEEP)
+ .setIfLate(IfLate.END))
+ .addScenarios(
+ Scenario.newBuilder()
+ .setWeight(0.5)
+ .setJourney("android.platform.test.scenario.calendar.FlingWeekPage")
+ )
+ .addScenarios(
+ Scenario.newBuilder()
+ .setWeight(0.25)
+ .setJourney("android.platform.test.scenario.calendar.FlingDayPage")
+ )
+ .addScenarios(
+ Scenario.newBuilder()
+ .setWeight(0.25)
+ .setJourney("android.platform.test.scenario.calendar.FlingWeekPage")
+ )
+ .build();
+ }
+}
diff --git a/libraries/composer/host/src/android/host/test/composer/ProfileBase.java b/libraries/composer/host/src/android/host/test/composer/ProfileBase.java
new file mode 100644
index 0000000..2424b9a
--- /dev/null
+++ b/libraries/composer/host/src/android/host/test/composer/ProfileBase.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2018 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 android.host.test.composer;
+
+import android.host.test.composer.profile.Configuration;
+import android.host.test.composer.profile.Configuration.Scenario;
+
+import org.junit.runner.Description;
+import org.junit.runner.Runner;
+
+import java.lang.IllegalArgumentException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * A {@link Compose} function base class for taking a profile and returning the tests in the specified sequence.
+ */
+public abstract class ProfileBase<T> implements Compose<T, Runner> {
+ protected static final String PROFILE_OPTION_NAME = "profile";
+ protected static final String PROFILE_EXTENSION = ".pb";
+
+ private static final String LOG_TAG = ProfileBase.class.getSimpleName();
+
+ // Store the configuration to be read by the test runner.
+ private Configuration mConfiguration;
+
+ // Comparator for sorting timstamped CUJs.
+ private static class ScenarioTimestampComparator implements Comparator<Scenario> {
+ public int compare(Scenario s1, Scenario s2) {
+ if (! (s1.hasAt() && s2.hasAt())) {
+ throw new IllegalArgumentException(
+ "Scenarios in scheduled profiles must have timestamps.");
+ }
+ return s1.getAt().compareTo(s2.getAt());
+ }
+ }
+
+ @Override
+ public List<Runner> apply(T args, List<Runner> input) {
+ mConfiguration = getConfigurationArgument(args);
+ if (mConfiguration == null) {
+ return input;
+ }
+ return getTestSequenceFromConfiguration(mConfiguration, input);
+ }
+
+ public boolean isConfigurationLoaded() {
+ return (mConfiguration == null);
+ }
+
+ public Configuration getLoadedConfiguration() {
+ return mConfiguration;
+ }
+
+ protected List<Runner> getTestSequenceFromConfiguration(
+ Configuration config, List<Runner> input) {
+ Map<String, Runner> nameToRunner =
+ input.stream().collect(
+ Collectors.toMap(
+ r -> r.getDescription().getDisplayName(), Function.identity()));
+ logInfo(LOG_TAG, String.format(
+ "Available scenarios: %s",
+ nameToRunner.keySet().stream().collect(Collectors.joining(", "))));
+ List<Scenario> scenarios = new ArrayList<Scenario>(config.getScenariosList());
+ if (config.hasScheduled()) {
+ Collections.sort(scenarios, new ScenarioTimestampComparator());
+ }
+ List<Runner> result = scenarios
+ .stream()
+ .map(Configuration.Scenario::getJourney)
+ .map(
+ journeyName -> {
+ if (nameToRunner.containsKey(journeyName)) {
+ return nameToRunner.get(journeyName);
+ } else {
+ throw new IllegalArgumentException(
+ String.format(
+ "Journey %s in profile does not exist.",
+ journeyName));
+ }
+ })
+ .collect(Collectors.toList());
+ logInfo(LOG_TAG, String.format(
+ "Returned runners: %s",
+ result.stream()
+ .map(Runner::getDescription)
+ .map(Description::getDisplayName)
+ .collect(Collectors.toList())));
+ return result;
+ }
+
+ /**
+ * Parses the arguments, reads the configuration file and returns the Configuraiton object.
+ *
+ * If no profile option is found in the arguments, function should return null, in which case
+ * the input sequence is returned without modification. Otherwise, function should parse the
+ * profile according to the supplied argument and return the Configuration object or throw an
+ * exception if the file is not available or cannot be parsed.
+ */
+ protected abstract Configuration getConfigurationArgument(T args);
+
+ /**
+ * Overrideable, platform-dependent logging function.
+ */
+ protected void logInfo(String tag, String content) {
+ System.out.println(content);
+ }
+}
diff --git a/libraries/composer/host/src/android/host/test/composer/profile.proto b/libraries/composer/host/src/android/host/test/composer/profile.proto
new file mode 100644
index 0000000..c339b2f
--- /dev/null
+++ b/libraries/composer/host/src/android/host/test/composer/profile.proto
@@ -0,0 +1,71 @@
+// Copyright (C) 2018 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.
+
+syntax = "proto2";
+
+package composer.profile;
+
+option java_package = "android.host.test.composer.profile";
+option java_multiple_files = true;
+
+message Configuration {
+ // Types required to build a profile.
+
+ //
+ message Scheduled {
+ //
+ enum IfEarly {
+ IDLE = 1;
+ SLEEP = 2;
+ }
+ //
+ enum IfLate {
+ DELAY = 1;
+ END = 2;
+ }
+ optional IfEarly if_early = 1;
+ optional IfLate if_late = 2;
+ }
+
+ //
+ message Weighted { }
+
+ //
+ message Scenario {
+ oneof schedule {
+ //
+ string at = 1;
+ //
+ double weight = 2;
+ }
+ //
+ optional string journey = 3;
+ //
+ message ExtraArg {
+ optional string key = 1;
+ optional string value = 2;
+ }
+ //
+ repeated ExtraArg extras = 4;
+ //map<string, string> extras = 4;
+ }
+
+ // Field definitions for a profile.
+
+ oneof schedule {
+ Scheduled scheduled = 1;
+ Weighted weighted = 2;
+ }
+ repeated Scenario scenarios = 3;
+}
diff --git a/libraries/composer/host/tests/Android.bp b/libraries/composer/host/tests/Android.bp
index 92d9554..ae35980 100644
--- a/libraries/composer/host/tests/Android.bp
+++ b/libraries/composer/host/tests/Android.bp
@@ -22,6 +22,17 @@
"test-composers",
"truth-prebuilt",
],
+ target: {
+ android: {
+ static_libs: ["mockito-target"]
+ },
+ host: {
+ static_libs: ["mockito", "objenesis"]
+ }
+ },
+ static_libs: [
+ "guava",
+ ]
}
//#####################################
diff --git a/libraries/composer/host/tests/src/android/host/test/composer/ProfileTest.java b/libraries/composer/host/tests/src/android/host/test/composer/ProfileTest.java
new file mode 100644
index 0000000..764218c
--- /dev/null
+++ b/libraries/composer/host/tests/src/android/host/test/composer/ProfileTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2018 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 android.host.test.composer;
+
+import android.host.test.composer.profile.Configuration;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.runner.Runner;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit test the logic for host-side {@link Profile}
+ */
+@RunWith(JUnit4.class)
+public class ProfileTest extends ProfileTestBase<Map<String, String>> {
+ protected class TestableProfile extends ProfileBase<Map<String, String>> {
+ @Override
+ protected Configuration getConfigurationArgument(Map<String, String> args) {
+ return TEST_CONFIGS.get(args.get(PROFILE_OPTION_NAME));
+ }
+ }
+
+ @Override
+ protected ProfileBase<Map<String, String>> getProfile() {
+ return new TestableProfile();
+ }
+
+ @Override
+ protected Map<String, String> getArguments(String configName) {
+ HashMap<String, String> args = new HashMap<>();
+ args.put(PROFILE_OPTION_NAME, configName);
+ return args;
+ }
+}
diff --git a/libraries/composer/host/tests/src/android/host/test/composer/ProfileTestBase.java b/libraries/composer/host/tests/src/android/host/test/composer/ProfileTestBase.java
new file mode 100644
index 0000000..10ed8cf
--- /dev/null
+++ b/libraries/composer/host/tests/src/android/host/test/composer/ProfileTestBase.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2018 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 android.host.test.composer;
+
+import android.host.test.composer.profile.Configuration;
+import android.host.test.composer.profile.Configuration.Scenario;
+import android.host.test.composer.profile.Configuration.Scheduled;
+import android.host.test.composer.profile.Configuration.Scheduled.IfEarly;
+import android.host.test.composer.profile.Configuration.Scheduled.IfLate;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.Description;
+import org.junit.runner.Runner;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * Base class to unit test the logic for {@link Profile}
+ */
+public abstract class ProfileTestBase<T> {
+ protected static final String PROFILE_OPTION_NAME = "profile";
+
+ protected static final String VALID_CONFIG_KEY = "valid_config";
+ protected static final Configuration VALID_CONFIG = Configuration.newBuilder()
+ .setScheduled(
+ Scheduled.newBuilder()
+ .setIfEarly(IfEarly.SLEEP)
+ .setIfLate(IfLate.END))
+ .addScenarios(
+ Scenario.newBuilder()
+ .setAt("00:01:00")
+ .setJourney("android.platform.test.scenario.calendar.FlingWeekPage"))
+ .addScenarios(
+ Scenario.newBuilder()
+ .setAt("00:04:00")
+ .setJourney("android.platform.test.scenario.calendar.FlingDayPage"))
+ .addScenarios(
+ Scenario.newBuilder()
+ .setAt("00:02:00")
+ .setJourney("android.platform.test.scenario.calendar.FlingWeekPage"))
+ .build();
+ private static final String CONFIG_WITH_INVALID_JOURNEY_KEY = "config_with_invalid_journey";
+ protected static final Configuration CONFIG_WITH_INVALID_JOURNEY = Configuration.newBuilder()
+ .setScheduled(
+ Scheduled.newBuilder()
+ .setIfEarly(IfEarly.SLEEP)
+ .setIfLate(IfLate.END))
+ .addScenarios(
+ Scenario.newBuilder()
+ .setAt("00:01:00")
+ .setJourney("android.platform.test.scenario.calendar.FlingWeekPage"))
+ .addScenarios(
+ Scenario.newBuilder()
+ .setAt("00:02:00")
+ .setJourney("invalid"))
+ .build();
+ private static final String CONFIG_WITH_MISSING_TIMESTAMPS_KEY =
+ "config_with_missing_timestamps";
+ protected static final Configuration CONFIG_WITH_MISSING_TIMESTAMPS = Configuration
+ .newBuilder()
+ .setScheduled(
+ Scheduled.newBuilder()
+ .setIfEarly(IfEarly.SLEEP)
+ .setIfLate(IfLate.END))
+ .addScenarios(
+ Scenario.newBuilder()
+ .setAt("00:01:00")
+ .setJourney("android.platform.test.scenario.calendar.FlingWeekPage"))
+ .addScenarios(
+ Scenario.newBuilder()
+ .setWeight(0.1)
+ .setJourney("android.platform.test.scenario.calendar.FlingDayPage"))
+ .build();
+ protected static final ImmutableMap<String, Configuration> TEST_CONFIGS= ImmutableMap.of(
+ VALID_CONFIG_KEY, VALID_CONFIG,
+ CONFIG_WITH_INVALID_JOURNEY_KEY, CONFIG_WITH_INVALID_JOURNEY,
+ CONFIG_WITH_MISSING_TIMESTAMPS_KEY, CONFIG_WITH_MISSING_TIMESTAMPS);
+ private static final ImmutableList<String> AVAILABLE_JOURNEYS = ImmutableList.of(
+ "android.platform.test.scenario.calendar.FlingWeekPage",
+ "android.platform.test.scenario.calendar.FlingDayPage",
+ "android.platform.test.scenario.calendar.FlingSchedulePage");
+
+ private ArrayList<Runner> mMockInput;
+
+ @Rule
+ public ExpectedException exceptionThrown = ExpectedException.none();
+
+ /**
+ * Sets up the input list of mocked runners for test.
+ */
+ @Before
+ public void setUp() {
+ mMockInput = new ArrayList<Runner>();
+ for (String testJourney : AVAILABLE_JOURNEYS) {
+ Runner mockRunner = Mockito.mock(Runner.class);
+ Description mockDescription = Mockito.mock(Description.class);
+ Mockito.when(mockDescription.getDisplayName()).thenReturn(testJourney);
+ Mockito.when(mockRunner.getDescription()).thenReturn(mockDescription);
+ mMockInput.add(mockRunner);
+ }
+ }
+
+ /**
+ * Tests that the returned runners are ordered according to their scheduled timestamps.
+ */
+ @Test
+ public void testProfileOrderingRespected() {
+ ImmutableList<String> expectedJourneyOrder = ImmutableList.of(
+ "android.platform.test.scenario.calendar.FlingWeekPage",
+ "android.platform.test.scenario.calendar.FlingWeekPage",
+ "android.platform.test.scenario.calendar.FlingDayPage");
+
+ List<Runner> output = getProfile().apply(getArguments(VALID_CONFIG_KEY), mMockInput);
+ List<String> outputDescriptions = output.stream().map(r ->
+ r.getDescription().getDisplayName()).collect(Collectors.toList());
+ boolean respected = outputDescriptions.equals(expectedJourneyOrder);
+ System.out.println(outputDescriptions);
+ assertThat(respected).isTrue();
+ }
+
+ /**
+ * Tests that an exception is thrown for profiles with invalid scenario names.
+ */
+ @Test
+ public void testProfileWithInvalidScenarioThrows() {
+ // An exception about nonexistent user journey should be thrown.
+ exceptionThrown.expect(IllegalArgumentException.class);
+ exceptionThrown.expectMessage("does not exist");
+ exceptionThrown.expectMessage("invalid");
+
+ // Attempt to apply a profile with invalid CUJ; the above exception should be thrown.
+ List<Runner> output = getProfile().apply(
+ getArguments(CONFIG_WITH_INVALID_JOURNEY_KEY), mMockInput);
+ }
+
+ /**
+ * Tests that an exception is thrown for profiles with mixed weighted/timestamped scenarios."
+ */
+ @Test
+ public void testScheduledScenarioWithoutTimestampThrows() {
+ // An exception about the absence of a timestamp should be thrown.
+ exceptionThrown.expect(IllegalArgumentException.class);
+ exceptionThrown.expectMessage("must have timestamps");
+ exceptionThrown.expectMessage("scheduled");
+
+ // Attempt to apply a scheduled profile with missing timestamps; the above exception should
+ // be thrown.
+ List<Runner> output = getProfile().apply(
+ getArguments(CONFIG_WITH_MISSING_TIMESTAMPS_KEY), mMockInput);
+ }
+
+ protected abstract ProfileBase<T> getProfile();
+
+ protected abstract T getArguments(String configName);
+}
diff --git a/libraries/composer/platform/Android.bp b/libraries/composer/platform/Android.bp
index 8c2a597..268caba 100644
--- a/libraries/composer/platform/Android.bp
+++ b/libraries/composer/platform/Android.bp
@@ -17,5 +17,5 @@
srcs: [ "src/**/*.java" ],
sdk_version: "current",
libs: [ "androidx.test.runner" ],
- static_libs: ["test-composers"],
+ static_libs: ["test-composers", "guava"],
}
diff --git a/libraries/composer/platform/src/android/platform/test/composer/Profile.java b/libraries/composer/platform/src/android/platform/test/composer/Profile.java
new file mode 100644
index 0000000..b612714
--- /dev/null
+++ b/libraries/composer/platform/src/android/platform/test/composer/Profile.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018 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 android.platform.test.composer;
+
+import android.content.res.AssetManager;
+import android.host.test.composer.profile.Configuration;
+import android.host.test.composer.ProfileBase;
+import android.os.Bundle;
+import android.util.Log;
+import androidx.test.InstrumentationRegistry;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.lang.IllegalArgumentException;
+
+/**
+ * An extension of {@link android.host.test.composer.ProfileBase} for device-side testing.
+ */
+public class Profile extends ProfileBase<Bundle> {
+ /*
+ * {@inheritDocs}
+ *
+ * The configuration should be passed as either the name of a configuration bundled into the APK
+ * or a path to the configuration file.
+ *
+ * TODO(harrytczhang@): Write tests for this logic.
+ */
+ @Override
+ protected Configuration getConfigurationArgument(Bundle args) {
+ // profileValue is either the name of a profile bundled with an APK or a path to a
+ // profile configuration file.
+ String profileValue = args.getString(PROFILE_OPTION_NAME, "");
+ if (profileValue.isEmpty()) {
+ return null;
+ }
+ // Look inside the APK assets for the profile; if this fails, try
+ // using the profile argument as a path to a configuration file.
+ InputStream configStream;
+ try {
+ AssetManager manager = InstrumentationRegistry.getContext().getAssets();
+ String profileName = profileValue + PROFILE_EXTENSION;
+ configStream = manager.open(profileName);
+ } catch (IOException e) {
+ // Try using the profile argument it as a path to a configuration file.
+ try {
+ File configFile = new File(profileValue);
+ if (!configFile.exists()) {
+ throw new IllegalArgumentException(String.format(
+ "Profile %s does not exist.", profileValue));
+ }
+ configStream = new FileInputStream(configFile);
+ } catch (IOException f) {
+ throw new IllegalArgumentException(String.format(
+ "Profile %s cannot be opened.", profileValue));
+ }
+ }
+ try {
+ // Parse the configuration from its input stream and return it.
+ return Configuration.parseFrom(configStream);
+ } catch (IOException e) {
+ throw new IllegalArgumentException(String.format(
+ "Cannot parse profile %s.", profileValue));
+ } finally {
+ try {
+ configStream.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ @Override
+ protected void logInfo(String tag, String content) {
+ Log.i(tag, content);
+ }
+}
diff --git a/libraries/composer/platform/tests/Android.bp b/libraries/composer/platform/tests/Android.bp
index 01b6e07..a3ca3d6 100644
--- a/libraries/composer/platform/tests/Android.bp
+++ b/libraries/composer/platform/tests/Android.bp
@@ -23,6 +23,7 @@
"platform-test-composers",
"test-composers-tests",
"truth-prebuilt",
+ "guava",
],
srcs: [ "src/**/*.java" ],
test_suites: [ "device-tests" ],
diff --git a/libraries/composer/platform/tests/AndroidManifest.xml b/libraries/composer/platform/tests/AndroidManifest.xml
index 1b01092..f023c7a 100644
--- a/libraries/composer/platform/tests/AndroidManifest.xml
+++ b/libraries/composer/platform/tests/AndroidManifest.xml
@@ -19,8 +19,11 @@
<application>
<uses-library android:name="android.test.runner"/>
</application>
+ <uses-permission android:name="android.platform.READ_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.platform.WRITE_EXTERNAL_STORAGE"/>
<instrumentation
android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="android.platform.test.composer.tests"
android:label="Platform Composer Library Tests" />
</manifest>
+
diff --git a/libraries/composer/platform/tests/src/android/platform/test/composer/ProfileTest.java b/libraries/composer/platform/tests/src/android/platform/test/composer/ProfileTest.java
new file mode 100644
index 0000000..83b1849
--- /dev/null
+++ b/libraries/composer/platform/tests/src/android/platform/test/composer/ProfileTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2018 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 android.platform.test.composer;
+
+import android.host.test.composer.profile.Configuration;
+import android.host.test.composer.ProfileBase;
+import android.host.test.composer.ProfileTestBase;
+import android.os.Bundle;
+
+import org.junit.runner.Runner;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit test the logic for device-side {@link Profile}
+ */
+@RunWith(JUnit4.class)
+public class ProfileTest extends ProfileTestBase<Bundle> {
+ protected class TestableProfile extends ProfileBase<Bundle> {
+ @Override
+ protected Configuration getConfigurationArgument(Bundle args) {
+ return TEST_CONFIGS.get(args.getString(PROFILE_OPTION_NAME));
+ }
+ }
+
+ @Override
+ protected ProfileBase<Bundle> getProfile() {
+ return new TestableProfile();
+ }
+
+ @Override
+ protected Bundle getArguments(String configName) {
+ Bundle args = new Bundle();
+ args.putString(PROFILE_OPTION_NAME, configName);
+ return args;
+ }
+}
diff --git a/libraries/device-collectors/src/main/Android.bp b/libraries/device-collectors/src/main/Android.bp
index a32c443..c5134dc 100644
--- a/libraries/device-collectors/src/main/Android.bp
+++ b/libraries/device-collectors/src/main/Android.bp
@@ -25,3 +25,22 @@
sdk_version: "current",
}
+
+// This library enables additional listeners dependent on platform apis.
+java_library {
+ name: "collector-device-lib-platform",
+ defaults: ["tradefed_errorprone_defaults"],
+
+ srcs: [
+ "java/**/*.java",
+ "platform-helpers/**/*.java",
+ ],
+
+ static_libs: [
+ "androidx.test.runner",
+ "collectors-helper",
+ "junit",
+ ],
+
+ platform_apis: true,
+}
diff --git a/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/AppStartupListener.java b/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/AppStartupListener.java
new file mode 100644
index 0000000..a04edec
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/AppStartupListener.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 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 android.device.collectors;
+
+import android.device.collectors.annotations.OptionClass;
+
+import com.android.helpers.AppStartupHelper;
+
+/**
+ * A {@link AppStartupListener} that captures app startup during the test method.
+ *
+ * Do NOT throw exception anywhere in this class. We don't want to halt the test when metrics
+ * collection fails.
+ */
+@OptionClass(alias = "appstartup-collector")
+public class AppStartupListener extends BaseCollectionListener<StringBuilder> {
+ public AppStartupListener() {
+ createHelperInstance(new AppStartupHelper());
+ }
+}
diff --git a/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/BaseCollectionListener.java b/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/BaseCollectionListener.java
new file mode 100644
index 0000000..4177635
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/BaseCollectionListener.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2018 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 android.device.collectors;
+
+import com.android.helpers.ICollectorHelper;
+
+import org.junit.runner.Description;
+
+import java.util.Map;
+
+/**
+ * A {@link BaseCollectionListener} that captures metrics collected during the testing.
+ *
+ * Do NOT throw exception anywhere in this class. We don't want to halt the test when metrics
+ * collection fails.
+ */
+public class BaseCollectionListener<T> extends BaseMetricListener {
+
+ private ICollectorHelper mHelper;
+
+
+ @Override
+ public void onTestStart(DataRecord testData, Description description) {
+ // NO-OP in the failure case for now and proceed with the testing.
+ mHelper.startCollecting();
+ }
+
+ @Override
+ public void onTestEnd(DataRecord testData, Description description) {
+ Map<String, T> metrics = mHelper.getMetrics();
+ for (Map.Entry<String, T> entry : metrics.entrySet()) {
+ testData.addStringMetric(entry.getKey(), entry.getValue().toString());
+ }
+ mHelper.stopCollecting();
+ }
+
+ protected void createHelperInstance(ICollectorHelper helper) {
+ mHelper = helper;
+ }
+
+}
diff --git a/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/CpuUsageListener.java b/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/CpuUsageListener.java
new file mode 100644
index 0000000..591aed8
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/CpuUsageListener.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 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 android.device.collectors;
+
+import android.device.collectors.annotations.OptionClass;
+
+import com.android.helpers.CpuUsageHelper;
+
+/**
+ * A {@link CpuUsageListener} that captures cpu usage during the test method.
+ *
+ * Do NOT throw exception anywhere in this class. We don't want to halt the test when metrics
+ * collection fails.
+ */
+@OptionClass(alias = "cpuusage-collector")
+public class CpuUsageListener extends BaseCollectionListener<Long> {
+ public CpuUsageListener() {
+ createHelperInstance(new CpuUsageHelper());
+ }
+}
+
diff --git a/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/CrashListener.java b/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/CrashListener.java
new file mode 100644
index 0000000..6cde413
--- /dev/null
+++ b/libraries/device-collectors/src/main/platform-helpers/android/device/collectors/CrashListener.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 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 android.device.collectors;
+
+import android.device.collectors.annotations.OptionClass;
+
+import com.android.helpers.CrashHelper;
+
+/**
+ * A {@link CrashListener} that captures crashes during the test method.
+ *
+ * Do NOT throw exception anywhere in this class. We don't want to halt the test when metrics
+ * collection fails.
+ */
+@OptionClass(alias = "crash-collector")
+public class CrashListener extends BaseCollectionListener<Integer> {
+ public CrashListener() {
+ createHelperInstance(new CrashHelper());
+ }
+}
diff --git a/libraries/junitxml/Android.bp b/libraries/junitxml/Android.bp
index b090147..464ea87 100644
--- a/libraries/junitxml/Android.bp
+++ b/libraries/junitxml/Android.bp
@@ -18,4 +18,5 @@
name: "junitxml",
static_libs: ["junit"],
srcs: ["src/**/*.java"],
+ sdk_version: "core_current",
}
diff --git a/libraries/longevity/src/android/longevity/core/LongevitySuite.java b/libraries/longevity/src/android/longevity/core/LongevitySuite.java
index a415faf..1fa46c8 100644
--- a/libraries/longevity/src/android/longevity/core/LongevitySuite.java
+++ b/libraries/longevity/src/android/longevity/core/LongevitySuite.java
@@ -19,6 +19,7 @@
import android.longevity.core.listener.TimeoutTerminator;
import android.host.test.composer.Iterate;
import android.host.test.composer.Shuffle;
+import android.host.test.composer.Profile;
import org.junit.runner.Runner;
import org.junit.runner.notification.RunNotifier;
@@ -78,6 +79,7 @@
private static List<Runner> constructClassRunners(
Class<?> suite, RunnerBuilder builder, Map<String, String> args)
throws InitializationError {
+ // Note: until b/118340229 is resolved, keep the platform class method in sync as necessary.
// Retrieve annotated suite classes.
SuiteClasses annotation = suite.getAnnotation(SuiteClasses.class);
if (annotation == null) {
@@ -86,7 +88,7 @@
}
// Construct and store custom runners for the full suite.
BiFunction<Map<String, String>, List<Runner>, List<Runner>> modifier =
- new Iterate<Runner>().andThen(new Shuffle<Runner>());
+ new Iterate<Runner>().andThen(new Shuffle<Runner>()).andThen(new Profile());
return modifier.apply(args, builder.runners(suite, annotation.value()));
}
diff --git a/libraries/longevity/src/android/longevity/platform/LongevitySuite.java b/libraries/longevity/src/android/longevity/platform/LongevitySuite.java
index a9f54a4..aa88961 100644
--- a/libraries/longevity/src/android/longevity/platform/LongevitySuite.java
+++ b/libraries/longevity/src/android/longevity/platform/LongevitySuite.java
@@ -24,13 +24,19 @@
import android.longevity.platform.listener.TimeoutTerminator;
import android.os.BatteryManager;
import android.os.Bundle;
+import android.platform.test.composer.Iterate;
+import android.platform.test.composer.Shuffle;
+import android.platform.test.composer.Profile;
+import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.test.InstrumentationRegistry;
-import android.util.Log;
+import java.util.function.BiFunction;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import org.junit.runner.Runner;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.RunnerBuilder;
@@ -81,11 +87,31 @@
public LongevitySuite(Class<?> klass, RunnerBuilder builder,
Instrumentation instrumentation, Context context, Bundle arguments)
throws InitializationError {
- super(klass, builder, toMap(arguments));
+ super(klass, constructClassRunners(klass, builder, arguments), toMap(arguments));
mInstrumentation = instrumentation;
mContext = context;
}
+ /**
+ * Constructs the sequence of {@link Runner}s using platform composers.
+ */
+ private static List<Runner> constructClassRunners(
+ Class<?> suite, RunnerBuilder builder, Bundle args)
+ throws InitializationError {
+ // TODO(b/118340229): Refactor to share logic with base class. In the meanwhile, keep the
+ // logic here in sync with the base class.
+ // Retrieve annotated suite classes.
+ SuiteClasses annotation = suite.getAnnotation(SuiteClasses.class);
+ if (annotation == null) {
+ throw new InitializationError(String.format(
+ "Longevity suite, '%s', must have a SuiteClasses annotation", suite.getName()));
+ }
+ // Construct and store custom runners for the full suite.
+ BiFunction<Bundle, List<Runner>, List<Runner>> modifier =
+ new Iterate<Runner>().andThen(new Shuffle<Runner>()).andThen(new Profile());
+ return modifier.apply(args, builder.runners(suite, annotation.value()));
+ }
+
@Override
public void run(final RunNotifier notifier) {
// Register the battery terminator available only on the platform library, if present.
diff --git a/libraries/microbenchmark/Android.bp b/libraries/microbenchmark/Android.bp
index 07943fd..c9b74bd 100644
--- a/libraries/microbenchmark/Android.bp
+++ b/libraries/microbenchmark/Android.bp
@@ -18,6 +18,7 @@
static_libs: [
"androidx.test.runner",
"platform-test-composers",
+ "platform-test-rules",
],
srcs: ["src/**/*.java"],
diff --git a/libraries/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java b/libraries/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java
index 9cd7068..ff1e6bb 100644
--- a/libraries/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java
+++ b/libraries/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java
@@ -17,6 +17,7 @@
import android.os.Bundle;
import android.platform.test.composer.Iterate;
+import android.platform.test.rule.TracePointRule;
import androidx.annotation.VisibleForTesting;
import androidx.test.InstrumentationRegistry;
@@ -37,7 +38,7 @@
* TightMethodRule}s in order to reliably measure a specific test method in isolation. Samples are
* soon to follow.
*/
-public final class Microbenchmark extends BlockJUnit4ClassRunner {
+public class Microbenchmark extends BlockJUnit4ClassRunner {
private Bundle mArguments;
/**
@@ -64,6 +65,8 @@
@Override
protected Statement methodInvoker(FrameworkMethod method, Object test) {
Statement start = super.methodInvoker(method, test);
+ // Wrap the inner-most test method with trace points.
+ start = getTracePointRule().apply(start, describeChild(method));
// Invoke special @TightMethodRules that wrap @Test methods.
List<MethodRule> tightMethodRules = getTestClass()
.getAnnotatedFieldValues(test, TightMethodRule.class, MethodRule.class);
@@ -73,6 +76,19 @@
return start;
}
+ @VisibleForTesting
+ protected TracePointRule getTracePointRule() {
+ return new TracePointRule();
+ }
+
+ /**
+ * Returns a list of repeated {@link FrameworkMethod}s to execute.
+ */
+ @Override
+ protected List<FrameworkMethod> getChildren() {
+ return new Iterate<FrameworkMethod>().apply(mArguments, super.getChildren());
+ }
+
/**
* An annotation for the corresponding tight rules above. These rules are ordered differently
* from standard JUnit {@link Rule}s because they live between {@link Before} and {@link After}
@@ -99,12 +115,4 @@
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface TightMethodRule { }
-
- /**
- * Returns a list of repeated {@link FrameworkMethod}s to execute.
- */
- @Override
- protected List<FrameworkMethod> getChildren() {
- return new Iterate<FrameworkMethod>().apply(mArguments, super.getChildren());
- }
}
diff --git a/libraries/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java b/libraries/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java
index 83897ec..cafd229 100644
--- a/libraries/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java
+++ b/libraries/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java
@@ -20,11 +20,13 @@
import static org.junit.Assert.fail;
import android.os.Bundle;
+import android.platform.test.rule.TracePointRule;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.rules.MethodRule;
+import org.junit.runner.Description;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.RunWith;
@@ -33,6 +35,9 @@
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Unit tests for the {@link Microbenchmark} runner.
*/
@@ -55,43 +60,107 @@
}
/**
- * Tests that {@link TightMethodRule}s are ordered properly.
+ * Tests that {@link TracePointRule} and {@link TightMethodRule}s are properly ordered.
*
- * <p>@Before --> @Tight Before --> @Test --> @Tight After --> @After.
+ * Before --> TightBefore --> Trace (begin) --> Test --> Trace(end) --> TightAfter --> After
*/
@Test
- public void testTightMethodRuleOrder() {
- try {
- Result result = new JUnitCore().run(OrderTest.class);
- assertThat(result.wasSuccessful()).isTrue();
- assertThat(result.getRunCount()).isAtLeast(1);
- } catch (Exception e) {
- fail("This test should not throw.");
+ public void testFeatureExecutionOrder() throws InitializationError {
+ LoggingMicrobenchmark loggingRunner = new LoggingMicrobenchmark(LoggingTest.class);
+ loggingRunner.setOperationLog(new ArrayList<String>());
+ Result result = new JUnitCore().run(loggingRunner);
+ assertThat(result.wasSuccessful()).isTrue();
+ assertThat(loggingRunner.getOperationLog()).containsExactly(
+ "before",
+ "tight before",
+ "begin: testMethod("
+ + "android.platform.test.microbenchmark.MicrobenchmarkTest$LoggingTest)",
+ "test",
+ "end",
+ "tight after",
+ "after")
+ .inOrder();
+ }
+
+ /**
+ * An extensions of the {@link Microbenchmark} runner that logs the start and end of collecting
+ * traces. It also passes the operation log to the provided test {@code Class}, if it is a
+ * {@link LoggingTest}. This is used for ensuring the proper order for evaluating test {@link
+ * Statement}s.
+ */
+ public static class LoggingMicrobenchmark extends Microbenchmark {
+ private List<String> mOperationLog;
+
+ public LoggingMicrobenchmark(Class<?> klass) throws InitializationError {
+ super(klass);
+ }
+
+ LoggingMicrobenchmark(Class<?> klass, Bundle arguments) throws InitializationError {
+ super(klass, arguments);
+ }
+
+ protected Object createTest() throws Exception {
+ Object test = super.createTest();
+ if (test instanceof LoggingTest) {
+ ((LoggingTest)test).setOperationLog(mOperationLog);
+ }
+ return test;
+ }
+
+ void setOperationLog(List<String> log) {
+ mOperationLog = log;
+ }
+
+ List<String> getOperationLog() {
+ return mOperationLog;
+ }
+
+ @Override
+ protected TracePointRule getTracePointRule() {
+ return new LoggingTracePointRule();
+ }
+
+ class LoggingTracePointRule extends TracePointRule {
+ @Override
+ protected void beginSection(String sectionTag) {
+ mOperationLog.add(String.format("begin: %s", sectionTag));
+ }
+
+ @Override
+ protected void endSection() {
+ mOperationLog.add("end");
+ }
}
}
- @RunWith(Microbenchmark.class)
- public static class OrderTest {
+ /**
+ * A test that logs {@link Before}, {@link After}, {@link Test}, and the logging {@link
+ * TightMethodRule} included, used in conjunction with {@link LoggingMicrobenchmark} to
+ * determine all {@link Statement}s are evaluated in the proper order.
+ */
+ public static class LoggingTest {
@Microbenchmark.TightMethodRule
public TightRule orderRule = new TightRule();
- private boolean hasCalledBefore = false;
- private boolean hasCalledAfter = false;
- private boolean hasCalledTest = false;
+ private List<String> mOperationLog;
+
+ void setOperationLog(List<String> log) {
+ mOperationLog = log;
+ }
@Before
public void beforeMethod() {
- hasCalledBefore = true;
+ mOperationLog.add("before");
}
@Test
public void testMethod() {
- hasCalledTest = true;
+ mOperationLog.add("test");
}
@After
public void afterMethod() {
- hasCalledAfter = true;
+ mOperationLog.add("after");
}
class TightRule implements MethodRule {
@@ -100,16 +169,9 @@
return new Statement() {
@Override
public void evaluate() throws Throwable {
- // Tight before statement.
- assertWithMessage("Before was not called before rule evaluation.")
- .that(hasCalledBefore).isTrue();
- // Test method evaluation.
+ mOperationLog.add("tight before");
base.evaluate();
- // Tight after statement.
- assertWithMessage("Test not called before base evaluation.")
- .that(hasCalledTest).isTrue();
- assertWithMessage("After was called before rule evaluation.")
- .that(hasCalledAfter).isFalse();
+ mOperationLog.add("tight after");
}
};
}
diff --git a/tests/jank/jankmicrobenchmark/Android.bp b/tests/jank/jankmicrobenchmark/Android.bp
index 3809958..c32b4df 100644
--- a/tests/jank/jankmicrobenchmark/Android.bp
+++ b/tests/jank/jankmicrobenchmark/Android.bp
@@ -27,6 +27,4 @@
libs: ["android.test.base.stubs"],
sdk_version: "test_current",
-
- test_suites: ["device-tests"],
}
diff --git a/tests/jank/jankmicrobenchmark/AndroidTest.xml b/tests/jank/jankmicrobenchmark/AndroidTest.xml
deleted file mode 100644
index d1523f6..0000000
--- a/tests/jank/jankmicrobenchmark/AndroidTest.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 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.
--->
-<configuration description="Runs Platform Benchmark Jank Tests.">
-
- <target_preparer class="com.google.android.tradefed.targetprep.GoogleDeviceSetup">
- <option name="screen-saver" value="off" />
- <option name="location-gps" value="off" />
- <option name="location-network" value="off" />
- <option name="battery-saver-mode" value="off" />
- <option name="disable-playstore" value="true" />
- </target_preparer>
-
- <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
- <option name="test-file-name" value="JankMicroBenchmarkTests.apk" />
- <option name="test-file-name" value="ApiDemos.apk" />
- </target_preparer>
-
- <option name="post-boot-command" value="setprop debug.hwui.filter_test_overhead true" />
- <option name="test-tag" value="JankMicroBenchmarkTests" />
- <test class="com.android.tradefed.testtype.InstrumentationTest" >
- <option name="package" value="com.android.jankmicrobenchmark.janktests" />
- <option name="runner" value="android.test.InstrumentationTestRunner" />
- <option name="test-timeout" value="1800000" />
- <option name="shell-timeout" value="1800000" />
- </test>
-</configuration>