Home | History | Annotate | Download | only in applaunchtest
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.applaunchtest;
     18 
     19 import android.app.ActivityManager;
     20 import android.app.ActivityManager.ProcessErrorStateInfo;
     21 import android.content.ComponentName;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.pm.PackageManager;
     25 import android.content.pm.ResolveInfo;
     26 import android.test.InstrumentationTestCase;
     27 import android.util.Log;
     28 
     29 import java.util.ArrayList;
     30 import java.util.Collection;
     31 import java.util.Iterator;
     32 import java.util.LinkedHashSet;
     33 import java.util.List;
     34 import java.util.Set;
     35 
     36 /**
     37  * Simple tests that launches a specified app, and waits for a configurable amount of time for
     38  * crashes and ANRs.
     39  * <p/>
     40  * If no crashes occur, test is considered passed.
     41  * <p/>
     42  * Derived from frameworks/base/tests/SmokeTests/... . TODO: consider refactoring to share code
     43  */
     44 public class AppLaunchTest extends InstrumentationTestCase {
     45 
     46     private static final String TAG = "AppLaunchTest";
     47 
     48     private ActivityManager mActivityManager;
     49     private PackageManager mPackageManager;
     50     private String mPackageName;
     51     private Context mContext;
     52     private long mWaitTime;
     53 
     54     /**
     55      * {@inheritDoc}
     56      */
     57     @Override
     58     public void setUp() throws Exception {
     59         super.setUp();
     60         mContext = getInstrumentation().getTargetContext();
     61         assertNotNull("failed to get context", mContext);
     62 
     63         mActivityManager = (ActivityManager)
     64                 mContext.getSystemService(Context.ACTIVITY_SERVICE);
     65         mPackageManager = mContext.getPackageManager();
     66         assertNotNull("failed to get activity manager", mActivityManager);
     67         assertNotNull("failed to get package manager", mPackageManager);
     68 
     69         assertTrue("Unexpected runner: AppLaunchRunner must be used",
     70                 getInstrumentation() instanceof AppLaunchRunner);
     71         AppLaunchRunner runner = (AppLaunchRunner)getInstrumentation();
     72         mPackageName = runner.getAppPackageName();
     73         mWaitTime  = runner.getAppWaitTime();
     74         assertNotNull("package name to launch was not provided", mPackageName);
     75         assertNotNull("time to wait for app launch was not provided", mWaitTime);
     76     }
     77 
     78     /**
     79      * A test that runs Launcher-launchable activity for given package name and verifies that no
     80      * ANRs or crashes happened while doing so.
     81      */
     82     public void testLaunchActivity() throws Exception {
     83         final Set<ProcessError> errSet = new LinkedHashSet<ProcessError>();
     84 
     85         ResolveInfo app = getLauncherActivity(mPackageName, mPackageManager);
     86         assertNotNull(String.format("Could not find launchable activity for %s", mPackageName),
     87                 app);
     88         final Collection<ProcessError> errProcs = runOneActivity(app, mWaitTime);
     89         if (errProcs != null) {
     90             errSet.addAll(errProcs);
     91          }
     92 
     93         if (!errSet.isEmpty()) {
     94             fail(String.format("Detected %d errors on launch of app %s:\n%s", errSet.size(),
     95                     mPackageName, reportWrappedListContents(errSet)));
     96         }
     97     }
     98 
     99     /**
    100      * A method to run the specified Activity and return a {@link Collection} of the Activities that
    101      * were in an error state, as listed by {@link ActivityManager.getProcessesInErrorState()}.
    102      * <p />
    103      * The method will launch the app, wait for waitTime seconds, check for apps in the error state
    104      * and then return.
    105      */
    106     public Collection<ProcessError> runOneActivity(ResolveInfo app, long appLaunchWait) {
    107 
    108         Log.i(TAG, String.format("Running activity %s/%s", app.activityInfo.packageName,
    109                 app.activityInfo.name));
    110 
    111         // We check for any Crash or ANR dialogs that are already up, and we ignore them.  This is
    112         // so that we don't report crashes that were caused by prior apps.
    113         final Collection<ProcessError> preErrProcs =
    114                 ProcessError.fromCollection(mActivityManager.getProcessesInErrorState());
    115 
    116         // launch app, and waitfor it to start/settle
    117         final Intent intent = intentForActivity(app);
    118         mContext.startActivity(intent);
    119         try {
    120             Thread.sleep(appLaunchWait);
    121         } catch (InterruptedException e) {
    122             // ignore
    123         }
    124 
    125         // TODO: inject event to see if app is responding. The smoke tests press 'Home', but
    126         // we don't want to do that here because we want to take screenshot on app launch
    127 
    128         // See if there are any errors.  We wait until down here to give ANRs as much time as
    129         // possible to occur.
    130         final Collection<ProcessError> errProcs =
    131                 ProcessError.fromCollection(mActivityManager.getProcessesInErrorState());
    132 
    133         // Distinguish the asynchronous crashes/ANRs from the synchronous ones by checking the
    134         // crash package name against the package name for {@code app}
    135         if (errProcs != null) {
    136             Iterator<ProcessError> errIter = errProcs.iterator();
    137             while (errIter.hasNext()) {
    138                 ProcessError err = errIter.next();
    139                 if (!packageMatches(app, err)) {
    140                     // crash in another package. Just log it for now
    141                     Log.w(TAG, String.format("Detected crash in %s when launching %s",
    142                             err.info.processName, app.activityInfo.packageName));
    143                     errIter.remove();
    144                 }
    145             }
    146         }
    147         // Take the difference between the remaining current error processes and the ones that were
    148         // present when we started.  The result is guaranteed to be:
    149         // 1) Errors that are pertinent to this app's package
    150         // 2) Errors that are pertinent to this particular app invocation
    151         if (errProcs != null && preErrProcs != null) {
    152             errProcs.removeAll(preErrProcs);
    153         }
    154 
    155         return errProcs;
    156     }
    157 
    158     /**
    159      * A helper function that checks whether the specified error could have been caused by the
    160      * specified app.
    161      *
    162      * @param app The app to check against
    163      * @param err The error that we're considering
    164      */
    165     private static boolean packageMatches(ResolveInfo app, ProcessError err) {
    166         final String appPkg = app.activityInfo.packageName;
    167         final String errPkg = err.info.processName;
    168         Log.d(TAG, String.format("packageMatches(%s, %s)", appPkg, errPkg));
    169         return appPkg.equals(errPkg);
    170     }
    171 
    172     /**
    173      * A helper function to get the launchable activity for the given package name.
    174      */
    175     static ResolveInfo getLauncherActivity(String packageName, PackageManager pm) {
    176         final Intent launchable = new Intent(Intent.ACTION_MAIN);
    177         launchable.addCategory(Intent.CATEGORY_LAUNCHER);
    178         launchable.setPackage(packageName);
    179         return pm.resolveActivity(launchable, 0);
    180     }
    181 
    182     /**
    183      * A helper function to create an {@link Intent} to run, given a {@link ResolveInfo} specifying
    184      * an activity to be launched.
    185      */
    186     static Intent intentForActivity(ResolveInfo app) {
    187         final ComponentName component = new ComponentName(app.activityInfo.packageName,
    188                 app.activityInfo.name);
    189         final Intent intent = new Intent(Intent.ACTION_MAIN);
    190         intent.setComponent(component);
    191         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    192         intent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
    193         return intent;
    194     }
    195 
    196     /**
    197      * Report error reports for {@link ProcessErrorStateInfo} instances that are wrapped inside of
    198      * {@link ProcessError} instances.  Just unwraps and calls
    199      * {@see reportListContents(Collection<ProcessErrorStateInfo>)}.
    200      */
    201     static String reportWrappedListContents(Collection<ProcessError> errList) {
    202         List<ProcessErrorStateInfo> newList = new ArrayList<ProcessErrorStateInfo>(errList.size());
    203         for (ProcessError err : errList) {
    204             newList.add(err.info);
    205         }
    206         return reportListContents(newList);
    207     }
    208 
    209     /**
    210      * This helper function will dump the actual error reports.
    211      *
    212      * @param errList The error report containing one or more error records.
    213      * @return Returns a string containing all of the errors.
    214      */
    215     private static String reportListContents(Collection<ProcessErrorStateInfo> errList) {
    216         if (errList == null) return null;
    217 
    218         StringBuilder builder = new StringBuilder();
    219 
    220         Iterator<ProcessErrorStateInfo> iter = errList.iterator();
    221         while (iter.hasNext()) {
    222             ProcessErrorStateInfo entry = iter.next();
    223 
    224             String condition;
    225             switch (entry.condition) {
    226             case ActivityManager.ProcessErrorStateInfo.CRASHED:
    227                 condition = "a CRASH";
    228                 break;
    229             case ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING:
    230                 condition = "an ANR";
    231                 break;
    232             default:
    233                 condition = "an unknown error";
    234                 break;
    235             }
    236 
    237             builder.append(String.format("Process %s encountered %s (%s)", entry.processName,
    238                     condition, entry.shortMsg));
    239             if (entry.condition == ActivityManager.ProcessErrorStateInfo.CRASHED) {
    240                 builder.append(String.format(" with stack trace:\n%s\n", entry.stackTrace));
    241             }
    242             builder.append("\n");
    243         }
    244         return builder.toString();
    245     }
    246 
    247     /**
    248      * A {@link ProcessErrorStateInfo} wrapper class that hashes how we want (so that equivalent
    249      * crashes are considered equal).
    250      */
    251     static class ProcessError {
    252         public final ProcessErrorStateInfo info;
    253 
    254         public ProcessError(ProcessErrorStateInfo newInfo) {
    255             info = newInfo;
    256         }
    257 
    258         public static Collection<ProcessError> fromCollection(Collection<ProcessErrorStateInfo> in)
    259                 {
    260             if (in == null) {
    261                 return null;
    262             }
    263 
    264             List<ProcessError> out = new ArrayList<ProcessError>(in.size());
    265             for (ProcessErrorStateInfo info : in) {
    266                 out.add(new ProcessError(info));
    267             }
    268             return out;
    269         }
    270 
    271         private boolean strEquals(String a, String b) {
    272             if ((a == null) && (b == null)) {
    273                 return true;
    274             } else if ((a == null) || (b == null)) {
    275                 return false;
    276             } else {
    277                 return a.equals(b);
    278             }
    279         }
    280 
    281         @Override
    282         public boolean equals(Object other) {
    283             if (other == null) return false;
    284             if (!(other instanceof ProcessError)) return false;
    285             ProcessError peOther = (ProcessError) other;
    286 
    287             return (info.condition == peOther.info.condition)
    288                     && strEquals(info.longMsg, peOther.info.longMsg)
    289                     && (info.pid == peOther.info.pid)
    290                     && strEquals(info.processName, peOther.info.processName)
    291                     && strEquals(info.shortMsg, peOther.info.shortMsg)
    292                     && strEquals(info.stackTrace, peOther.info.stackTrace)
    293                     && strEquals(info.tag, peOther.info.tag)
    294                     && (info.uid == peOther.info.uid);
    295         }
    296 
    297         private int hash(Object obj) {
    298             if (obj == null) {
    299                 return 13;
    300             } else {
    301                 return obj.hashCode();
    302             }
    303         }
    304 
    305         @Override
    306         public int hashCode() {
    307             int code = 17;
    308             code += info.condition;
    309             code *= hash(info.longMsg);
    310             code += info.pid;
    311             code *= hash(info.processName);
    312             code *= hash(info.shortMsg);
    313             code *= hash(info.stackTrace);
    314             code *= hash(info.tag);
    315             code += info.uid;
    316             return code;
    317         }
    318     }
    319 }
    320