1 /* 2 * Copyright (C) 2007 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.commands.am; 18 19 import static android.app.ActivityManager.INSTR_FLAG_DISABLE_HIDDEN_API_CHECKS; 20 import static android.app.ActivityManager.INSTR_FLAG_DISABLE_TEST_API_CHECKS; 21 import static android.app.ActivityManager.INSTR_FLAG_MOUNT_EXTERNAL_STORAGE_FULL; 22 23 import android.app.IActivityManager; 24 import android.app.IInstrumentationWatcher; 25 import android.app.Instrumentation; 26 import android.app.UiAutomationConnection; 27 import android.content.ComponentName; 28 import android.content.pm.IPackageManager; 29 import android.content.pm.InstrumentationInfo; 30 import android.os.Build; 31 import android.os.Bundle; 32 import android.os.Environment; 33 import android.os.ServiceManager; 34 import android.os.UserHandle; 35 import android.util.AndroidException; 36 import android.util.proto.ProtoOutputStream; 37 import android.view.IWindowManager; 38 39 import java.io.File; 40 import java.io.FileOutputStream; 41 import java.io.IOException; 42 import java.io.InputStreamReader; 43 import java.io.OutputStream; 44 import java.text.SimpleDateFormat; 45 import java.util.ArrayList; 46 import java.util.Collection; 47 import java.util.Collections; 48 import java.util.Date; 49 import java.util.List; 50 import java.util.Locale; 51 52 53 /** 54 * Runs the am instrument command 55 * 56 * Test Result Code: 57 * 1 - Test running 58 * 0 - Test passed 59 * -2 - assertion failure 60 * -1 - other exceptions 61 * 62 * Session Result Code: 63 * -1: Success 64 * other: Failure 65 */ 66 public class Instrument { 67 private static final String TAG = "am"; 68 69 public static final String DEFAULT_LOG_DIR = "instrument-logs"; 70 71 private static final int STATUS_TEST_PASSED = 0; 72 private static final int STATUS_TEST_STARTED = 1; 73 private static final int STATUS_TEST_FAILED_ASSERTION = -1; 74 private static final int STATUS_TEST_FAILED_OTHER = -2; 75 76 private final IActivityManager mAm; 77 private final IPackageManager mPm; 78 private final IWindowManager mWm; 79 80 // Command line arguments 81 public String profileFile = null; 82 public boolean wait = false; 83 public boolean rawMode = false; 84 boolean protoStd = false; // write proto to stdout 85 boolean protoFile = false; // write proto to a file 86 String logPath = null; 87 public boolean noWindowAnimation = false; 88 public boolean disableHiddenApiChecks = false; 89 public boolean disableTestApiChecks = true; 90 public boolean disableIsolatedStorage = false; 91 public String abi = null; 92 public int userId = UserHandle.USER_CURRENT; 93 public Bundle args = new Bundle(); 94 // Required 95 public String componentNameArg; 96 97 /** 98 * Construct the instrument command runner. 99 */ Instrument(IActivityManager am, IPackageManager pm)100 public Instrument(IActivityManager am, IPackageManager pm) { 101 mAm = am; 102 mPm = pm; 103 mWm = IWindowManager.Stub.asInterface(ServiceManager.getService("window")); 104 } 105 106 /** 107 * Base class for status reporting. 108 * 109 * All the methods on this interface are called within the synchronized block 110 * of the InstrumentationWatcher, so calls are in order. However, that means 111 * you must be careful not to do blocking operations because you don't know 112 * exactly the locking dependencies. 113 */ 114 private interface StatusReporter { 115 /** 116 * Status update for tests. 117 */ onInstrumentationStatusLocked(ComponentName name, int resultCode, Bundle results)118 public void onInstrumentationStatusLocked(ComponentName name, int resultCode, 119 Bundle results); 120 121 /** 122 * The tests finished. 123 */ onInstrumentationFinishedLocked(ComponentName name, int resultCode, Bundle results)124 public void onInstrumentationFinishedLocked(ComponentName name, int resultCode, 125 Bundle results); 126 127 /** 128 * @param errorText a description of the error 129 * @param commandError True if the error is related to the commandline, as opposed 130 * to a test failing. 131 */ onError(String errorText, boolean commandError)132 public void onError(String errorText, boolean commandError); 133 } 134 sorted(Collection<String> list)135 private static Collection<String> sorted(Collection<String> list) { 136 final ArrayList<String> copy = new ArrayList<>(list); 137 Collections.sort(copy); 138 return copy; 139 } 140 141 /** 142 * Printer for the 'classic' text based status reporting. 143 */ 144 private class TextStatusReporter implements StatusReporter { 145 private boolean mRawMode; 146 147 /** 148 * Human-ish readable output. 149 * 150 * @param rawMode In "raw mode" (true), all bundles are dumped. 151 * In "pretty mode" (false), if a bundle includes 152 * Instrumentation.REPORT_KEY_STREAMRESULT, just print that. 153 */ TextStatusReporter(boolean rawMode)154 public TextStatusReporter(boolean rawMode) { 155 mRawMode = rawMode; 156 } 157 158 @Override onInstrumentationStatusLocked(ComponentName name, int resultCode, Bundle results)159 public void onInstrumentationStatusLocked(ComponentName name, int resultCode, 160 Bundle results) { 161 // pretty printer mode? 162 String pretty = null; 163 if (!mRawMode && results != null) { 164 pretty = results.getString(Instrumentation.REPORT_KEY_STREAMRESULT); 165 } 166 if (pretty != null) { 167 System.out.print(pretty); 168 } else { 169 if (results != null) { 170 for (String key : sorted(results.keySet())) { 171 System.out.println( 172 "INSTRUMENTATION_STATUS: " + key + "=" + results.get(key)); 173 } 174 } 175 System.out.println("INSTRUMENTATION_STATUS_CODE: " + resultCode); 176 } 177 } 178 179 @Override onInstrumentationFinishedLocked(ComponentName name, int resultCode, Bundle results)180 public void onInstrumentationFinishedLocked(ComponentName name, int resultCode, 181 Bundle results) { 182 // pretty printer mode? 183 String pretty = null; 184 if (!mRawMode && results != null) { 185 pretty = results.getString(Instrumentation.REPORT_KEY_STREAMRESULT); 186 } 187 if (pretty != null) { 188 System.out.println(pretty); 189 } else { 190 if (results != null) { 191 for (String key : sorted(results.keySet())) { 192 System.out.println( 193 "INSTRUMENTATION_RESULT: " + key + "=" + results.get(key)); 194 } 195 } 196 System.out.println("INSTRUMENTATION_CODE: " + resultCode); 197 } 198 } 199 200 @Override onError(String errorText, boolean commandError)201 public void onError(String errorText, boolean commandError) { 202 if (mRawMode) { 203 System.out.println("onError: commandError=" + commandError + " message=" 204 + errorText); 205 } 206 // The regular BaseCommand error printing will print the commandErrors. 207 if (!commandError) { 208 System.out.println(errorText); 209 } 210 } 211 } 212 213 /** 214 * Printer for the protobuf based status reporting. 215 */ 216 private class ProtoStatusReporter implements StatusReporter { 217 218 private File mLog; 219 220 private long mTestStartMs; 221 ProtoStatusReporter()222 ProtoStatusReporter() { 223 if (protoFile) { 224 if (logPath == null) { 225 File logDir = new File(Environment.getLegacyExternalStorageDirectory(), 226 DEFAULT_LOG_DIR); 227 if (!logDir.exists() && !logDir.mkdirs()) { 228 System.err.format("Unable to create log directory: %s\n", 229 logDir.getAbsolutePath()); 230 protoFile = false; 231 return; 232 } 233 SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd-hhmmss-SSS", Locale.US); 234 String fileName = String.format("log-%s.instrumentation_data_proto", 235 format.format(new Date())); 236 mLog = new File(logDir, fileName); 237 } else { 238 mLog = new File(Environment.getLegacyExternalStorageDirectory(), logPath); 239 File logDir = mLog.getParentFile(); 240 if (!logDir.exists() && !logDir.mkdirs()) { 241 System.err.format("Unable to create log directory: %s\n", 242 logDir.getAbsolutePath()); 243 protoFile = false; 244 return; 245 } 246 } 247 if (mLog.exists()) mLog.delete(); 248 } 249 } 250 251 @Override onInstrumentationStatusLocked(ComponentName name, int resultCode, Bundle results)252 public void onInstrumentationStatusLocked(ComponentName name, int resultCode, 253 Bundle results) { 254 final ProtoOutputStream proto = new ProtoOutputStream(); 255 256 final long testStatusToken = proto.start(InstrumentationData.Session.TEST_STATUS); 257 258 proto.write(InstrumentationData.TestStatus.RESULT_CODE, resultCode); 259 writeBundle(proto, InstrumentationData.TestStatus.RESULTS, results); 260 261 if (resultCode == STATUS_TEST_STARTED) { 262 // Logcat -T takes wall clock time (!?) 263 mTestStartMs = System.currentTimeMillis(); 264 } else { 265 if (mTestStartMs > 0) { 266 proto.write(InstrumentationData.TestStatus.LOGCAT, readLogcat(mTestStartMs)); 267 } 268 mTestStartMs = 0; 269 } 270 271 proto.end(testStatusToken); 272 273 outputProto(proto); 274 } 275 276 @Override onInstrumentationFinishedLocked(ComponentName name, int resultCode, Bundle results)277 public void onInstrumentationFinishedLocked(ComponentName name, int resultCode, 278 Bundle results) { 279 final ProtoOutputStream proto = new ProtoOutputStream(); 280 281 final long sessionStatusToken = proto.start(InstrumentationData.Session.SESSION_STATUS); 282 proto.write(InstrumentationData.SessionStatus.STATUS_CODE, 283 InstrumentationData.SESSION_FINISHED); 284 proto.write(InstrumentationData.SessionStatus.RESULT_CODE, resultCode); 285 writeBundle(proto, InstrumentationData.SessionStatus.RESULTS, results); 286 proto.end(sessionStatusToken); 287 288 outputProto(proto); 289 } 290 291 @Override onError(String errorText, boolean commandError)292 public void onError(String errorText, boolean commandError) { 293 final ProtoOutputStream proto = new ProtoOutputStream(); 294 295 final long sessionStatusToken = proto.start(InstrumentationData.Session.SESSION_STATUS); 296 proto.write(InstrumentationData.SessionStatus.STATUS_CODE, 297 InstrumentationData.SESSION_ABORTED); 298 proto.write(InstrumentationData.SessionStatus.ERROR_TEXT, errorText); 299 proto.end(sessionStatusToken); 300 301 outputProto(proto); 302 } 303 writeBundle(ProtoOutputStream proto, long fieldId, Bundle bundle)304 private void writeBundle(ProtoOutputStream proto, long fieldId, Bundle bundle) { 305 final long bundleToken = proto.start(fieldId); 306 307 for (final String key: sorted(bundle.keySet())) { 308 final long entryToken = proto.startRepeatedObject( 309 InstrumentationData.ResultsBundle.ENTRIES); 310 311 proto.write(InstrumentationData.ResultsBundleEntry.KEY, key); 312 313 final Object val = bundle.get(key); 314 if (val instanceof String) { 315 proto.write(InstrumentationData.ResultsBundleEntry.VALUE_STRING, 316 (String)val); 317 } else if (val instanceof Byte) { 318 proto.write(InstrumentationData.ResultsBundleEntry.VALUE_INT, 319 ((Byte)val).intValue()); 320 } else if (val instanceof Double) { 321 proto.write(InstrumentationData.ResultsBundleEntry.VALUE_DOUBLE, (double)val); 322 } else if (val instanceof Float) { 323 proto.write(InstrumentationData.ResultsBundleEntry.VALUE_FLOAT, (float)val); 324 } else if (val instanceof Integer) { 325 proto.write(InstrumentationData.ResultsBundleEntry.VALUE_INT, (int)val); 326 } else if (val instanceof Long) { 327 proto.write(InstrumentationData.ResultsBundleEntry.VALUE_LONG, (long)val); 328 } else if (val instanceof Short) { 329 proto.write(InstrumentationData.ResultsBundleEntry.VALUE_INT, (short)val); 330 } else if (val instanceof Bundle) { 331 writeBundle(proto, InstrumentationData.ResultsBundleEntry.VALUE_BUNDLE, 332 (Bundle)val); 333 } else if (val instanceof byte[]) { 334 proto.write(InstrumentationData.ResultsBundleEntry.VALUE_BYTES, (byte[])val); 335 } 336 337 proto.end(entryToken); 338 } 339 340 proto.end(bundleToken); 341 } 342 outputProto(ProtoOutputStream proto)343 private void outputProto(ProtoOutputStream proto) { 344 byte[] out = proto.getBytes(); 345 if (protoStd) { 346 try { 347 System.out.write(out); 348 System.out.flush(); 349 } catch (IOException ex) { 350 System.err.println("Error writing finished response: "); 351 ex.printStackTrace(System.err); 352 } 353 } 354 if (protoFile) { 355 try (OutputStream os = new FileOutputStream(mLog, true)) { 356 os.write(proto.getBytes()); 357 os.flush(); 358 } catch (IOException ex) { 359 System.err.format("Cannot write to %s:\n", mLog.getAbsolutePath()); 360 ex.printStackTrace(); 361 } 362 } 363 } 364 } 365 366 367 /** 368 * Callbacks from the remote instrumentation instance. 369 */ 370 private class InstrumentationWatcher extends IInstrumentationWatcher.Stub { 371 private final StatusReporter mReporter; 372 373 private boolean mFinished = false; 374 InstrumentationWatcher(StatusReporter reporter)375 public InstrumentationWatcher(StatusReporter reporter) { 376 mReporter = reporter; 377 } 378 379 @Override instrumentationStatus(ComponentName name, int resultCode, Bundle results)380 public void instrumentationStatus(ComponentName name, int resultCode, Bundle results) { 381 synchronized (this) { 382 mReporter.onInstrumentationStatusLocked(name, resultCode, results); 383 notifyAll(); 384 } 385 } 386 387 @Override instrumentationFinished(ComponentName name, int resultCode, Bundle results)388 public void instrumentationFinished(ComponentName name, int resultCode, Bundle results) { 389 synchronized (this) { 390 mReporter.onInstrumentationFinishedLocked(name, resultCode, results); 391 mFinished = true; 392 notifyAll(); 393 } 394 } 395 waitForFinish()396 public boolean waitForFinish() { 397 synchronized (this) { 398 while (!mFinished) { 399 try { 400 if (!mAm.asBinder().pingBinder()) { 401 return false; 402 } 403 wait(1000); 404 } catch (InterruptedException e) { 405 throw new IllegalStateException(e); 406 } 407 } 408 } 409 return true; 410 } 411 } 412 413 /** 414 * Figure out which component they really meant. 415 */ parseComponentName(String cnArg)416 private ComponentName parseComponentName(String cnArg) throws Exception { 417 if (cnArg.contains("/")) { 418 ComponentName cn = ComponentName.unflattenFromString(cnArg); 419 if (cn == null) throw new IllegalArgumentException("Bad component name: " + cnArg); 420 return cn; 421 } else { 422 List<InstrumentationInfo> infos = mPm.queryInstrumentation(null, 0).getList(); 423 424 final int numInfos = infos == null ? 0: infos.size(); 425 ArrayList<ComponentName> cns = new ArrayList<>(); 426 for (int i = 0; i < numInfos; i++) { 427 InstrumentationInfo info = infos.get(i); 428 429 ComponentName c = new ComponentName(info.packageName, info.name); 430 if (cnArg.equals(info.packageName)) { 431 cns.add(c); 432 } 433 } 434 435 if (cns.size() == 0) { 436 throw new IllegalArgumentException("No instrumentation found for: " + cnArg); 437 } else if (cns.size() == 1) { 438 return cns.get(0); 439 } else { 440 StringBuilder cnsStr = new StringBuilder(); 441 final int numCns = cns.size(); 442 for (int i = 0; i < numCns; i++) { 443 cnsStr.append(cns.get(i).flattenToString()); 444 cnsStr.append(", "); 445 } 446 447 // Remove last ", " 448 cnsStr.setLength(cnsStr.length() - 2); 449 450 throw new IllegalArgumentException("Found multiple instrumentations: " 451 + cnsStr.toString()); 452 } 453 } 454 } 455 456 /** 457 * Run the instrumentation. 458 */ run()459 public void run() throws Exception { 460 StatusReporter reporter = null; 461 float[] oldAnims = null; 462 463 try { 464 // Choose which output we will do. 465 if (protoFile || protoStd) { 466 reporter = new ProtoStatusReporter(); 467 } else if (wait) { 468 reporter = new TextStatusReporter(rawMode); 469 } 470 471 // Choose whether we have to wait for the results. 472 InstrumentationWatcher watcher = null; 473 UiAutomationConnection connection = null; 474 if (reporter != null) { 475 watcher = new InstrumentationWatcher(reporter); 476 connection = new UiAutomationConnection(); 477 } 478 479 // Set the window animation if necessary 480 if (noWindowAnimation) { 481 oldAnims = mWm.getAnimationScales(); 482 mWm.setAnimationScale(0, 0.0f); 483 mWm.setAnimationScale(1, 0.0f); 484 mWm.setAnimationScale(2, 0.0f); 485 } 486 487 // Figure out which component we are trying to do. 488 final ComponentName cn = parseComponentName(componentNameArg); 489 490 // Choose an ABI if necessary 491 if (abi != null) { 492 final String[] supportedAbis = Build.SUPPORTED_ABIS; 493 boolean matched = false; 494 for (String supportedAbi : supportedAbis) { 495 if (supportedAbi.equals(abi)) { 496 matched = true; 497 break; 498 } 499 } 500 if (!matched) { 501 throw new AndroidException( 502 "INSTRUMENTATION_FAILED: Unsupported instruction set " + abi); 503 } 504 } 505 506 // Start the instrumentation 507 int flags = 0; 508 if (disableHiddenApiChecks) { 509 flags |= INSTR_FLAG_DISABLE_HIDDEN_API_CHECKS; 510 } 511 if (disableTestApiChecks) { 512 flags |= INSTR_FLAG_DISABLE_TEST_API_CHECKS; 513 } 514 if (disableIsolatedStorage) { 515 flags |= INSTR_FLAG_MOUNT_EXTERNAL_STORAGE_FULL; 516 } 517 if (!mAm.startInstrumentation(cn, profileFile, flags, args, watcher, connection, userId, 518 abi)) { 519 throw new AndroidException("INSTRUMENTATION_FAILED: " + cn.flattenToString()); 520 } 521 522 // If we have been requested to wait, do so until the instrumentation is finished. 523 if (watcher != null) { 524 if (!watcher.waitForFinish()) { 525 reporter.onError("INSTRUMENTATION_ABORTED: System has crashed.", false); 526 return; 527 } 528 } 529 } catch (Exception ex) { 530 // Report failures 531 if (reporter != null) { 532 reporter.onError(ex.getMessage(), true); 533 } 534 535 // And re-throw the exception 536 throw ex; 537 } finally { 538 // Clean up 539 if (oldAnims != null) { 540 mWm.setAnimationScales(oldAnims); 541 } 542 } 543 } 544 readLogcat(long startTimeMs)545 private static String readLogcat(long startTimeMs) { 546 try { 547 // Figure out the timestamp arg for logcat. 548 final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 549 final String timestamp = format.format(new Date(startTimeMs)); 550 551 // Start the process 552 final Process process = new ProcessBuilder() 553 .command("logcat", "-d", "-v threadtime,uid", "-T", timestamp) 554 .start(); 555 556 // Nothing to write. Don't let the command accidentally block. 557 process.getOutputStream().close(); 558 559 // Read the output 560 final StringBuilder str = new StringBuilder(); 561 final InputStreamReader reader = new InputStreamReader(process.getInputStream()); 562 char[] buffer = new char[4096]; 563 int amt; 564 while ((amt = reader.read(buffer, 0, buffer.length)) >= 0) { 565 if (amt > 0) { 566 str.append(buffer, 0, amt); 567 } 568 } 569 570 try { 571 process.waitFor(); 572 } catch (InterruptedException ex) { 573 // We already have the text, drop the exception. 574 } 575 576 return str.toString(); 577 578 } catch (IOException ex) { 579 return "Error reading logcat command:\n" + ex.toString(); 580 } 581 } 582 } 583 584