aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkarenluo <karenluo@google.com>2019-11-13 20:10:41 -0400
committerkarenluo <karenluo@google.com>2019-11-13 21:02:50 -0400
commitfd50cd7fe8008a532c2d87e4560ff987d416afe3 (patch)
tree85982193c8ea02ae6edffec433c00605c93899a6
parent19bffac0507ffe7df41f3d906b25f59ddf27aecf (diff)
downloadcsuite-fd50cd7fe8008a532c2d87e4560ff987d416afe3.tar.gz
Add a launch instrumentation app to C-Suite
Add a JUnit 4 instrumentation test that launches an installed app and checks for crashes. The test is installed and run by csuite-harness. Test: make csuite, csuite-tradefed, run launch Change-Id: Icad6ffe80c09b01862fb8f657fa22d5c3e772b81
-rw-r--r--instrumentation/launch/Android.bp26
-rw-r--r--instrumentation/launch/src/main/AndroidManifest.xml29
-rw-r--r--instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibility.java362
-rw-r--r--instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibilityRunner.java22
4 files changed, 439 insertions, 0 deletions
diff --git a/instrumentation/launch/Android.bp b/instrumentation/launch/Android.bp
new file mode 100644
index 0000000..afe90eb
--- /dev/null
+++ b/instrumentation/launch/Android.bp
@@ -0,0 +1,26 @@
+// Copyright (C) 2012 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.
+
+android_test {
+ name: "csuite-launch-instrumentation",
+ static_libs: ["androidx.test.rules"],
+ // Include all test java files.
+ srcs: ["src/**/*.java"],
+ platform_apis: true,
+ certificate: "platform",
+ manifest: "src/main/AndroidManifest.xml",
+ test_suites: [
+ "csuite"
+ ],
+}
diff --git a/instrumentation/launch/src/main/AndroidManifest.xml b/instrumentation/launch/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c9f71c2
--- /dev/null
+++ b/instrumentation/launch/src/main/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.compatibilitytest"
+ android:sharedUserId="android.uid.system">
+ <uses-sdk android:minSdkVersion="21"
+ android:targetSdkVersion="21" />
+ <application />
+ <uses-permission android:name="android.permission.READ_LOGS" />
+ <uses-permission android:name="android.permission.REAL_GET_TASKS" />
+ <instrumentation
+ android:name=".AppCompatibilityRunner"
+ android:targetPackage="com.android.compatibilitytest"
+ android:label="App Compatibility Test Runner" />
+</manifest>
diff --git a/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibility.java b/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibility.java
new file mode 100644
index 0000000..3e146e1
--- /dev/null
+++ b/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibility.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2012 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.compatibilitytest;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.ProcessErrorStateInfo;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.IActivityController;
+import android.app.IActivityManager;
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.app.UiModeManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.DropBoxManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Application Compatibility Test that launches an application and detects
+ * crashes.
+ */
+@RunWith(AndroidJUnit4.class)
+public final class AppCompatibility {
+
+ private static final String TAG = AppCompatibility.class.getSimpleName();
+ private static final String PACKAGE_TO_LAUNCH = "package_to_launch";
+ private static final String APP_LAUNCH_TIMEOUT_MSECS = "app_launch_timeout_ms";
+ private static final String WORKSPACE_LAUNCH_TIMEOUT_MSECS = "workspace_launch_timeout_ms";
+ private static final Set<String> DROPBOX_TAGS = new HashSet<>();
+ private static final int MAX_CRASH_SNIPPET_LINES = 20;
+ private static final int MAX_NUM_CRASH_SNIPPET = 3;
+
+ // time waiting for app to launch
+ private int mAppLaunchTimeout = 7000;
+ // time waiting for launcher home screen to show up
+ private int mWorkspaceLaunchTimeout = 2000;
+
+ private Context mContext;
+ private ActivityManager mActivityManager;
+ private PackageManager mPackageManager;
+ private Bundle mArgs;
+ private Instrumentation mInstrumentation;
+ private String mLauncherPackageName;
+ private IActivityController mCrashSupressor = new CrashSuppressor();
+ private Map<String, List<String>> mAppErrors = new HashMap<>();
+
+ static {
+ DROPBOX_TAGS.add("SYSTEM_TOMBSTONE");
+ DROPBOX_TAGS.add("system_app_anr");
+ DROPBOX_TAGS.add("system_app_native_crash");
+ DROPBOX_TAGS.add("system_app_crash");
+ DROPBOX_TAGS.add("data_app_anr");
+ DROPBOX_TAGS.add("data_app_native_crash");
+ DROPBOX_TAGS.add("data_app_crash");
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ mContext = InstrumentationRegistry.getTargetContext();
+ mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
+ mPackageManager = mContext.getPackageManager();
+ mArgs = InstrumentationRegistry.getArguments();
+
+ // resolve launcher package name
+ Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME);
+ ResolveInfo resolveInfo = mPackageManager.resolveActivity(
+ intent, PackageManager.MATCH_DEFAULT_ONLY);
+ mLauncherPackageName = resolveInfo.activityInfo.packageName;
+ Assert.assertNotNull("failed to resolve package name for launcher", mLauncherPackageName);
+ Log.v(TAG, "Using launcher package name: " + mLauncherPackageName);
+
+ // Parse optional inputs.
+ String appLaunchTimeoutMsecs = mArgs.getString(APP_LAUNCH_TIMEOUT_MSECS);
+ if (appLaunchTimeoutMsecs != null) {
+ mAppLaunchTimeout = Integer.parseInt(appLaunchTimeoutMsecs);
+ }
+ String workspaceLaunchTimeoutMsecs = mArgs.getString(WORKSPACE_LAUNCH_TIMEOUT_MSECS);
+ if (workspaceLaunchTimeoutMsecs != null) {
+ mWorkspaceLaunchTimeout = Integer.parseInt(workspaceLaunchTimeoutMsecs);
+ }
+ mInstrumentation.getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_0);
+
+ // set activity controller to suppress crash dialogs and collects them by process name
+ mAppErrors.clear();
+ IActivityManager.Stub.asInterface(ServiceManager.checkService(Context.ACTIVITY_SERVICE))
+ .setActivityController(mCrashSupressor, false);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ // unset activity controller
+ IActivityManager.Stub.asInterface(ServiceManager.checkService(Context.ACTIVITY_SERVICE))
+ .setActivityController(null, false);
+ mInstrumentation.getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE);
+ }
+
+ /**
+ * Actual test case that launches the package and throws an exception on the
+ * first error.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testAppStability() throws Exception {
+ String packageName = mArgs.getString(PACKAGE_TO_LAUNCH);
+ if (packageName != null) {
+ Log.d(TAG, "Launching app " + packageName);
+ Intent intent = getLaunchIntentForPackage(packageName);
+ if (intent == null) {
+ Log.w(TAG, String.format("Skipping %s; no launch intent", packageName));
+ return;
+ }
+ long startTime = System.currentTimeMillis();
+ launchActivity(packageName, intent);
+ try {
+ checkDropbox(startTime, packageName);
+ if (mAppErrors.containsKey(packageName)) {
+ StringBuilder message = new StringBuilder("Error(s) detected for package: ")
+ .append(packageName);
+ List<String> errors = mAppErrors.get(packageName);
+ for (int i = 0; i < MAX_NUM_CRASH_SNIPPET && i < errors.size(); i++) {
+ String err = errors.get(i);
+ message.append("\n\n");
+ // limit the size of each crash snippet
+ message.append(truncate(err, MAX_CRASH_SNIPPET_LINES));
+ }
+ if (errors.size() > MAX_NUM_CRASH_SNIPPET) {
+ message.append(String.format("\n... %d more errors omitted ...",
+ errors.size() - MAX_NUM_CRASH_SNIPPET));
+ }
+ Assert.fail(message.toString());
+ }
+ // last check: see if app process is still running
+ Assert.assertTrue("app package \"" + packageName + "\" no longer found in running "
+ + "tasks, but no explicit crashes were detected; check logcat for details",
+ processStillUp(packageName));
+ } finally {
+ returnHome();
+ }
+ } else {
+ Log.d(TAG, "Missing argument, use " + PACKAGE_TO_LAUNCH +
+ " to specify the package to launch");
+ }
+ }
+
+ /**
+ * Truncate the text to at most the specified number of lines, and append a marker at the end
+ * when truncated
+ * @param text
+ * @param maxLines
+ * @return
+ */
+ private static String truncate(String text, int maxLines) {
+ String[] lines = text.split("\\r?\\n");
+ StringBuilder ret = new StringBuilder();
+ for (int i = 0; i < maxLines && i < lines.length; i++) {
+ ret.append(lines[i]);
+ ret.append('\n');
+ }
+ if (lines.length > maxLines) {
+ ret.append("... ");
+ ret.append(lines.length - maxLines);
+ ret.append(" more lines truncated ...\n");
+ }
+ return ret.toString();
+ }
+
+ /**
+ * Check dropbox for entries of interest regarding the specified process
+ * @param startTime if not 0, only check entries with timestamp later than the start time
+ * @param processName the process name to check for
+ */
+ private void checkDropbox(long startTime, String processName) {
+ DropBoxManager dropbox = (DropBoxManager) mContext
+ .getSystemService(Context.DROPBOX_SERVICE);
+ DropBoxManager.Entry entry = null;
+ while (null != (entry = dropbox.getNextEntry(null, startTime))) {
+ try {
+ // only check entries with tag that's of interest
+ String tag = entry.getTag();
+ if (DROPBOX_TAGS.contains(tag)) {
+ String content = entry.getText(4096);
+ if (content != null) {
+ if (content.contains(processName)) {
+ addProcessError(processName, "dropbox:" + tag, content);
+ }
+ }
+ }
+ startTime = entry.getTimeMillis();
+ } finally {
+ entry.close();
+ }
+ }
+ }
+
+ private void returnHome() {
+ Intent homeIntent = new Intent(Intent.ACTION_MAIN);
+ homeIntent.addCategory(Intent.CATEGORY_HOME);
+ homeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // Send the "home" intent and wait 2 seconds for us to get there
+ mContext.startActivity(homeIntent);
+ try {
+ Thread.sleep(mWorkspaceLaunchTimeout);
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ }
+
+ private Intent getLaunchIntentForPackage(String packageName) {
+ UiModeManager umm = (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE);
+ boolean isLeanback = umm.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
+ Intent intent = null;
+ if (isLeanback) {
+ intent = mPackageManager.getLeanbackLaunchIntentForPackage(packageName);
+ } else {
+ intent = mPackageManager.getLaunchIntentForPackage(packageName);
+ }
+ return intent;
+ }
+
+ /**
+ * Launches and activity and queries for errors.
+ *
+ * @param packageName {@link String} the package name of the application to
+ * launch.
+ * @return {@link Collection} of {@link ProcessErrorStateInfo} detected
+ * during the app launch.
+ */
+ private void launchActivity(String packageName, Intent intent) {
+ Log.d(TAG, String.format("launching package \"%s\" with intent: %s",
+ packageName, intent.toString()));
+
+ // Launch Activity
+ mContext.startActivity(intent);
+
+ try {
+ // artificial delay: in case app crashes after doing some work during launch
+ Thread.sleep(mAppLaunchTimeout);
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ }
+
+ private void addProcessError(String processName, String errorType, String errorInfo) {
+ // parse out the package name if necessary, for apps with multiple processes
+ String pkgName = processName.split(":", 2)[0];
+ List<String> errors;
+ if (mAppErrors.containsKey(pkgName)) {
+ errors = mAppErrors.get(pkgName);
+ } else {
+ errors = new ArrayList<>();
+ }
+ errors.add(String.format("### Type: %s, Details:\n%s", errorType, errorInfo));
+ mAppErrors.put(pkgName, errors);
+ }
+
+ /**
+ * Determine if a given package is still running.
+ *
+ * @param packageName {@link String} package to look for
+ * @return True if package is running, false otherwise.
+ */
+ private boolean processStillUp(String packageName) {
+ @SuppressWarnings("deprecation")
+ List<RunningTaskInfo> infos = mActivityManager.getRunningTasks(100);
+ for (RunningTaskInfo info : infos) {
+ if (info.baseActivity.getPackageName().equals(packageName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * An {@link IActivityController} that instructs framework to kill processes hitting crashes
+ * directly without showing crash dialogs
+ *
+ */
+ private class CrashSuppressor extends IActivityController.Stub {
+
+ @Override
+ public boolean activityStarting(Intent intent, String pkg) throws RemoteException {
+ Log.d(TAG, "activity starting: " + intent.getComponent().toShortString());
+ return true;
+ }
+
+ @Override
+ public boolean activityResuming(String pkg) throws RemoteException {
+ Log.d(TAG, "activity resuming: " + pkg);
+ return true;
+ }
+
+ @Override
+ public boolean appCrashed(String processName, int pid, String shortMsg, String longMsg,
+ long timeMillis, String stackTrace) throws RemoteException {
+ Log.d(TAG, "app crash: " + processName);
+ addProcessError(processName, "crash", stackTrace);
+ // don't show dialog
+ return false;
+ }
+
+ @Override
+ public int appEarlyNotResponding(String processName, int pid, String annotation)
+ throws RemoteException {
+ // ignore
+ return 0;
+ }
+
+ @Override
+ public int appNotResponding(String processName, int pid, String processStats)
+ throws RemoteException {
+ Log.d(TAG, "app ANR: " + processName);
+ addProcessError(processName, "ANR", processStats);
+ // don't show dialog
+ return -1;
+ }
+
+ @Override
+ public int systemNotResponding(String msg) throws RemoteException {
+ // ignore
+ return -1;
+ }
+ }
+}
diff --git a/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibilityRunner.java b/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibilityRunner.java
new file mode 100644
index 0000000..960ea49
--- /dev/null
+++ b/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibilityRunner.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2012, 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.compatibilitytest;
+
+import androidx.test.runner.AndroidJUnitRunner;
+
+// empty subclass to maintain backwards compatibility on host-side harness
+public class AppCompatibilityRunner extends AndroidJUnitRunner {}