Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a5fe0de1 authored by Guang Zhu's avatar Guang Zhu
Browse files

revamp app compat test harness

- migrate away from deprecated JUnit3 paradigm
- scan dropbox entries for app errors, instead of probing
  task list
- use IActivityController to suppress crash dialogs and record
  detected app errors
- use combined dropbox and activity controller detected errors
  to determine app errors

Bug: 67002148
Test: run harness against apps known to crash
Change-Id: If108cfdc7474a13e24f0d8350a7cbf99e3b51c46
parent 4de7ab5c
Loading
Loading
Loading
Loading
+1 −3
Original line number Original line Diff line number Diff line
@@ -17,9 +17,7 @@ include $(CLEAR_VARS)


# We only want this apk build for tests.
# We only want this apk build for tests.
LOCAL_MODULE_TAGS := tests
LOCAL_MODULE_TAGS := tests

LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
LOCAL_JAVA_LIBRARIES := legacy-android-test
LOCAL_STATIC_JAVA_LIBRARIES := junit
# Include all test java files.
# Include all test java files.
LOCAL_SRC_FILES := \
LOCAL_SRC_FILES := \
	$(call all-java-files-under, src)
	$(call all-java-files-under, src)
+4 −4
Original line number Original line Diff line number Diff line
@@ -15,12 +15,12 @@
-->
-->


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.android.compatibilitytest" >
    package="com.android.compatibilitytest"
    android:sharedUserId="android.uid.system">
    <uses-sdk android:minSdkVersion="21"
    <uses-sdk android:minSdkVersion="21"
              android:targetSdkVersion="21" />
              android:targetSdkVersion="21" />
    <application >
    <application />
        <uses-library android:name="android.test.runner" />
    <uses-permission android:name="android.permission.READ_LOGS" />
    </application>
    <uses-permission android:name="android.permission.REAL_GET_TASKS" />
    <uses-permission android:name="android.permission.REAL_GET_TASKS" />
    <instrumentation
    <instrumentation
        android:name=".AppCompatibilityRunner"
        android:name=".AppCompatibilityRunner"
+167 −74
Original line number Original line Diff line number Diff line
@@ -17,62 +17,91 @@
package com.android.compatibilitytest;
package com.android.compatibilitytest;


import android.app.ActivityManager;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.IActivityController;
import android.app.IActivityManager;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.app.UiAutomation;
import android.app.UiModeManager;
import android.app.UiModeManager;
import android.app.ActivityManager.ProcessErrorStateInfo;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.content.Context;
import android.content.Intent;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Bundle;
import android.test.InstrumentationTestCase;
import android.os.DropBoxManager;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.util.Log;
import android.util.Log;


import junit.framework.Assert;
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.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.List;
import java.util.Map;
import java.util.Set;


/**
/**
 * Application Compatibility Test that launches an application and detects
 * Application Compatibility Test that launches an application and detects
 * crashes.
 * crashes.
 */
 */
public class AppCompatibility extends InstrumentationTestCase {
@RunWith(AndroidJUnit4.class)
public class AppCompatibility {


    private static final String TAG = AppCompatibility.class.getSimpleName();
    private static final String TAG = AppCompatibility.class.getSimpleName();
    private static final String PACKAGE_TO_LAUNCH = "package_to_launch";
    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 APP_LAUNCH_TIMEOUT_MSECS = "app_launch_timeout_ms";
    private static final String WORKSPACE_LAUNCH_TIMEOUT_MSECS = "workspace_launch_timeout_ms";
    private static final String WORKSPACE_LAUNCH_TIMEOUT_MSECS = "workspace_launch_timeout_ms";
    private static final Set<String> DROPBOX_TAGS = new HashSet<>();
    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");
    }


    // time waiting for app to launch
    private int mAppLaunchTimeout = 7000;
    private int mAppLaunchTimeout = 7000;
    // time waiting for launcher home screen to show up
    private int mWorkspaceLaunchTimeout = 2000;
    private int mWorkspaceLaunchTimeout = 2000;


    private Context mContext;
    private Context mContext;
    private ActivityManager mActivityManager;
    private ActivityManager mActivityManager;
    private PackageManager mPackageManager;
    private PackageManager mPackageManager;
    private AppCompatibilityRunner mRunner;
    private Bundle mArgs;
    private Bundle mArgs;
    private Instrumentation mInstrumentation;
    private String mLauncherPackageName;
    private IActivityController mCrashSupressor = new CrashSuppressor();
    private Map<String, List<String>> mAppErrors = new HashMap<>();


    @Override
    @Before
    public void setUp() throws Exception {
    public void setUp() throws Exception {
        super.setUp();
        mInstrumentation = InstrumentationRegistry.getInstrumentation();
        mRunner = (AppCompatibilityRunner) getInstrumentation();
        mContext = InstrumentationRegistry.getTargetContext();
        assertNotNull("Could not fetch InstrumentationTestRunner.", mRunner);
        mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);

        mContext = mRunner.getTargetContext();
        Assert.assertNotNull("Could not get the Context", mContext);

        mActivityManager = (ActivityManager)
                mContext.getSystemService(Context.ACTIVITY_SERVICE);
        Assert.assertNotNull("Could not get Activity Manager", mActivityManager);

        mPackageManager = mContext.getPackageManager();
        mPackageManager = mContext.getPackageManager();
        Assert.assertNotNull("Missing Package Manager", mPackageManager);
        mArgs = InstrumentationRegistry.getArguments();


        mArgs = mRunner.getBundle();
        // 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.
        // Parse optional inputs.
        String appLaunchTimeoutMsecs = mArgs.getString(APP_LAUNCH_TIMEOUT_MSECS);
        String appLaunchTimeoutMsecs = mArgs.getString(APP_LAUNCH_TIMEOUT_MSECS);
@@ -83,13 +112,20 @@ public class AppCompatibility extends InstrumentationTestCase {
        if (workspaceLaunchTimeoutMsecs != null) {
        if (workspaceLaunchTimeoutMsecs != null) {
            mWorkspaceLaunchTimeout = Integer.parseInt(workspaceLaunchTimeoutMsecs);
            mWorkspaceLaunchTimeout = Integer.parseInt(workspaceLaunchTimeoutMsecs);
        }
        }
        getInstrumentation().getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_0);
        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);
    }
    }


    @Override
    @After
    protected void tearDown() throws Exception {
    public void tearDown() throws Exception {
        getInstrumentation().getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE);
        // unset activity controller
        super.tearDown();
        IActivityManager.Stub.asInterface(ServiceManager.checkService(Context.ACTIVITY_SERVICE))
            .setActivityController(null, false);
        mInstrumentation.getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE);
    }
    }


    /**
    /**
@@ -98,6 +134,7 @@ public class AppCompatibility extends InstrumentationTestCase {
     *
     *
     * @throws Exception
     * @throws Exception
     */
     */
    @Test
    public void testAppStability() throws Exception {
    public void testAppStability() throws Exception {
        String packageName = mArgs.getString(PACKAGE_TO_LAUNCH);
        String packageName = mArgs.getString(PACKAGE_TO_LAUNCH);
        if (packageName != null) {
        if (packageName != null) {
@@ -107,13 +144,23 @@ public class AppCompatibility extends InstrumentationTestCase {
                Log.w(TAG, String.format("Skipping %s; no launch intent", packageName));
                Log.w(TAG, String.format("Skipping %s; no launch intent", packageName));
                return;
                return;
            }
            }
            ProcessErrorStateInfo err = launchActivity(packageName, intent);
            long startTime = System.currentTimeMillis();
            // Make sure there are no errors when launching the application,
            launchActivity(packageName, intent);
            // otherwise raise an
            // exception with the first error encountered.
            assertNull(getStackTrace(err), err);
            try {
            try {
                assertTrue("App crashed after launch.", processStillUp(packageName));
                checkDropbox(startTime, packageName);
                if (mAppErrors.containsKey(packageName)) {
                    StringBuilder message = new StringBuilder("Error detected for package: ")
                            .append(packageName);
                    for (String err : mAppErrors.get(packageName)) {
                        message.append("\n\n");
                        message.append(err);
                    }
                    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 {
            } finally {
                returnHome();
                returnHome();
            }
            }
@@ -124,31 +171,30 @@ public class AppCompatibility extends InstrumentationTestCase {
    }
    }


    /**
    /**
     * Gets the stack trace for the error.
     * 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 in {@link ProcessErrorStateInfo} to parse.
     * @param processName the process name to check for
     * @return {@link String} the long message of the error.
     */
     */
    private String getStackTrace(ProcessErrorStateInfo in) {
    private void checkDropbox(long startTime, String processName) {
        if (in == null) {
        DropBoxManager dropbox = (DropBoxManager) mContext
            return null;
                .getSystemService(Context.DROPBOX_SERVICE);
        } else {
        DropBoxManager.Entry entry = null;
            return in.stackTrace;
        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();
     * Returns the process name that the package is going to use.
            } finally {
     *
                entry.close();
     * @param packageName name of the package
            }
     * @return process name of the package
     */
    private String getProcessName(String packageName) {
        try {
            PackageInfo pi = mPackageManager.getPackageInfo(packageName, 0);
            return pi.applicationInfo.processName;
        } catch (NameNotFoundException e) {
            return packageName;
        }
        }
    }
    }


@@ -166,8 +212,7 @@ public class AppCompatibility extends InstrumentationTestCase {
    }
    }


    private Intent getLaunchIntentForPackage(String packageName) {
    private Intent getLaunchIntentForPackage(String packageName) {
        UiModeManager umm = (UiModeManager)
        UiModeManager umm = (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE);
                getInstrumentation().getContext().getSystemService(Context.UI_MODE_SERVICE);
        boolean isLeanback = umm.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
        boolean isLeanback = umm.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
        Intent intent = null;
        Intent intent = null;
        if (isLeanback) {
        if (isLeanback) {
@@ -186,35 +231,32 @@ public class AppCompatibility extends InstrumentationTestCase {
     * @return {@link Collection} of {@link ProcessErrorStateInfo} detected
     * @return {@link Collection} of {@link ProcessErrorStateInfo} detected
     *         during the app launch.
     *         during the app launch.
     */
     */
    private ProcessErrorStateInfo launchActivity(String packageName, Intent intent) {
    private void launchActivity(String packageName, Intent intent) {
        Log.d(TAG, String.format("launching package \"%s\" with intent: %s",
        Log.d(TAG, String.format("launching package \"%s\" with intent: %s",
                packageName, intent.toString()));
                packageName, intent.toString()));


        String processName = getProcessName(packageName);

        // Launch Activity
        // Launch Activity
        mContext.startActivity(intent);
        mContext.startActivity(intent);


        try {
        try {
            // artificial delay: in case app crashes after doing some work during launch
            Thread.sleep(mAppLaunchTimeout);
            Thread.sleep(mAppLaunchTimeout);
        } catch (InterruptedException e) {
        } catch (InterruptedException e) {
            // ignore
            // ignore
        }
        }

        // See if there are any errors. We wait until down here to give ANRs as much time as
        // possible to occur.
        final Collection<ProcessErrorStateInfo> postErr =
                mActivityManager.getProcessesInErrorState();

        if (postErr == null) {
            return null;
        }
        for (ProcessErrorStateInfo error : postErr) {
            if (error.processName.equals(processName)) {
                return error;
    }
    }

    private void addProcessError(String processName, String errorType, String errorInfo) {
        // parse out the package name if necessary, for apps with multiple proceses
        String pkgName = processName.split(":", 2)[0];
        List<String> errors;
        if (mAppErrors.containsKey(pkgName)) {
            errors = mAppErrors.get(pkgName);
        }  else {
            errors = new ArrayList<>();
        }
        }
        return null;
        errors.add(String.format("type: %s details:\n%s", errorType, errorInfo));
        mAppErrors.put(pkgName, errors);
    }
    }


    /**
    /**
@@ -233,4 +275,55 @@ public class AppCompatibility extends InstrumentationTestCase {
        }
        }
        return false;
        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;
        }
    }
}
}
+3 −16
Original line number Original line Diff line number Diff line
@@ -16,20 +16,7 @@


package com.android.compatibilitytest;
package com.android.compatibilitytest;


import android.os.Bundle;
import android.support.test.runner.AndroidJUnitRunner;
import android.test.InstrumentationTestRunner;


public class AppCompatibilityRunner extends InstrumentationTestRunner {
// empty subclass to maintain backwards compatibility on host-side harness

public class AppCompatibilityRunner extends AndroidJUnitRunner {}
    private Bundle mArgs;

    @Override
    public void onCreate(Bundle args) {
        super.onCreate(args);
        mArgs = args;
    }

    public Bundle getBundle() {
        return mArgs;
    }
}