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