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.monkey; 18 19 import com.android.ddmlib.CollectingOutputReceiver; 20 import com.android.ddmlib.IShellOutputReceiver; 21 import com.android.loganalysis.item.BugreportItem; 22 import com.android.loganalysis.item.MiscKernelLogItem; 23 import com.android.loganalysis.parser.BugreportParser; 24 import com.android.loganalysis.parser.KernelLogParser; 25 import com.android.tradefed.config.Option; 26 import com.android.tradefed.config.Option.Importance; 27 import com.android.tradefed.device.DeviceNotAvailableException; 28 import com.android.tradefed.device.ITestDevice; 29 import com.android.tradefed.invoker.TestInformation; 30 import com.android.tradefed.log.LogUtil.CLog; 31 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 32 import com.android.tradefed.result.ByteArrayInputStreamSource; 33 import com.android.tradefed.result.DeviceFileReporter; 34 import com.android.tradefed.result.FailureDescription; 35 import com.android.tradefed.result.FileInputStreamSource; 36 import com.android.tradefed.result.ITestInvocationListener; 37 import com.android.tradefed.result.InputStreamSource; 38 import com.android.tradefed.result.LogDataType; 39 import com.android.tradefed.result.TestDescription; 40 import com.android.tradefed.testtype.IDeviceTest; 41 import com.android.tradefed.testtype.IRemoteTest; 42 import com.android.tradefed.util.ArrayUtil; 43 import com.android.tradefed.util.Bugreport; 44 import com.android.tradefed.util.CircularAtraceUtil; 45 import com.android.tradefed.util.FileUtil; 46 import com.android.tradefed.util.IRunUtil; 47 import com.android.tradefed.util.RunUtil; 48 import com.android.tradefed.util.StreamUtil; 49 50 import org.junit.Assert; 51 52 import java.io.BufferedReader; 53 import java.io.File; 54 import java.io.FileReader; 55 import java.io.IOException; 56 import java.util.ArrayList; 57 import java.util.Collection; 58 import java.util.Date; 59 import java.util.HashMap; 60 import java.util.HashSet; 61 import java.util.LinkedHashMap; 62 import java.util.LinkedList; 63 import java.util.List; 64 import java.util.Map; 65 import java.util.Random; 66 import java.util.concurrent.TimeUnit; 67 68 /** Runner for stress tests which use the monkey command. */ 69 public class MonkeyBase implements IDeviceTest, IRemoteTest { 70 71 public static final String MONKEY_LOG_NAME = "monkey_log"; 72 public static final String BUGREPORT_NAME = "bugreport"; 73 74 /** Allow a 15 second buffer between the monkey run time and the delta uptime. */ 75 public static final long UPTIME_BUFFER = 15 * 1000; 76 77 private static final String DEVICE_ALLOWLIST_PATH = "/data/local/tmp/monkey_allowlist.txt"; 78 79 /** 80 * am command template to launch app intent with same action, category and task flags as if user 81 * started it from the app's launcher icon 82 */ 83 private static final String LAUNCH_APP_CMD = 84 "am start -W -n '%s' -a android.intent.action.MAIN -c android.intent.category.LAUNCHER" 85 + " -f 0x10200000"; 86 87 private static final String NULL_UPTIME = "0.00"; 88 89 /** 90 * Helper to run a monkey command with an absolute timeout. 91 * 92 * <p>This is used so that the command can be stopped after a set timeout, since the timeout 93 * that {@link ITestDevice#executeShellCommand(String, IShellOutputReceiver, long, TimeUnit, 94 * int)} takes applies to the time between output, not the overall time of the command. 95 */ 96 private class CommandHelper { 97 private DeviceNotAvailableException mException = null; 98 private String mOutput = null; 99 runCommand(final ITestDevice device, final String command, long timeout)100 public void runCommand(final ITestDevice device, final String command, long timeout) 101 throws DeviceNotAvailableException { 102 final CollectingOutputReceiver receiver = new CollectingOutputReceiver(); 103 Thread t = 104 new Thread() { 105 @Override 106 public void run() { 107 try { 108 device.executeShellCommand(command, receiver); 109 } catch (DeviceNotAvailableException e) { 110 mException = e; 111 } 112 } 113 }; 114 115 t.start(); 116 117 try { 118 t.join(timeout); 119 } catch (InterruptedException e) { 120 // Ignore and log. The thread should terminate once receiver.cancel() is called. 121 CLog.e("Thread was interrupted while running %s", command); 122 } 123 124 mOutput = receiver.getOutput(); 125 receiver.cancel(); 126 127 if (mException != null) { 128 throw mException; 129 } 130 } 131 getOutput()132 public String getOutput() { 133 return mOutput; 134 } 135 } 136 137 @Option(name = "package", description = "Package name to send events to. May be repeated.") 138 private Collection<String> mPackages = new LinkedList<>(); 139 140 @Option( 141 name = "exclude-package", 142 description = 143 "Substring of package names to exclude from " 144 + "the package list. May be repeated.", 145 importance = Importance.IF_UNSET) 146 private Collection<String> mExcludePackages = new HashSet<>(); 147 148 @Option(name = "category", description = "App Category. May be repeated.") 149 private Collection<String> mCategories = new LinkedList<>(); 150 151 @Option(name = "option", description = "Option to pass to monkey command. May be repeated.") 152 private Collection<String> mOptions = new LinkedList<>(); 153 154 @Option( 155 name = "launch-extras-int", 156 description = 157 "Launch int extras. May be repeated. Format: --launch-extras-i key value." 158 + " Note: this will be applied to all components.") 159 private Map<String, Integer> mIntegerExtras = new HashMap<>(); 160 161 @Option( 162 name = "launch-extras-str", 163 description = 164 "Launch string extras. May be repeated. Format: --launch-extras-s key value." 165 + " Note: this will be applied to all components.") 166 private Map<String, String> mStringExtras = new HashMap<>(); 167 168 @Option( 169 name = "target-count", 170 description = "Target number of events to send.", 171 importance = Importance.ALWAYS) 172 private int mTargetCount = 125000; 173 174 @Option(name = "random-seed", description = "Random seed to use for the monkey.") 175 private Long mRandomSeed = null; 176 177 @Option( 178 name = "throttle", 179 description = 180 "How much time to wait between sending successive " 181 + "events, in msecs. Default is 0ms.") 182 private long mThrottle = 0; 183 184 @Option( 185 name = "ignore-crashes", 186 description = "Monkey should keep going after encountering " + "an app crash") 187 private boolean mIgnoreCrashes = false; 188 189 @Option( 190 name = "ignore-timeout", 191 description = "Monkey should keep going after encountering " + "an app timeout (ANR)") 192 private boolean mIgnoreTimeouts = false; 193 194 @Option( 195 name = "reboot-device", 196 description = "Reboot device before running monkey. Defaults " + "to true.") 197 private boolean mRebootDevice = true; 198 199 @Option(name = "idle-time", description = "How long to sleep before running monkey, in secs") 200 private int mIdleTimeSecs = 5 * 60; 201 202 @Option( 203 name = "monkey-arg", 204 description = 205 "Extra parameters to pass onto monkey. Key/value " 206 + "pairs should be passed as key:value. May be repeated.") 207 private Collection<String> mMonkeyArgs = new LinkedList<>(); 208 209 @Option( 210 name = "use-pkg-allowlist-file", 211 description = 212 "Whether to use the monkey " 213 + "--pkg-whitelist-file option to work around cmdline length limits") 214 private boolean mUseAllowlistFile = false; 215 216 @Option( 217 name = "per-event-timeout", 218 description = 219 "A per event timeout in ms, for determining the total timeout for " 220 + "monkey run together with throttle and target event injection count.") 221 private long mPerEventTimeout = 100; 222 223 @Option( 224 name = "warmup-component", 225 description = 226 "Component name of app to launch for \"warming up\" before monkey test, will" 227 + " be used in an intent together with standard flags and parameters as" 228 + " launched from Launcher. May be repeated") 229 private List<String> mLaunchComponents = new ArrayList<>(); 230 231 /** @deprecated b/139751666 */ 232 @Deprecated 233 @Option(name = "retry-on-failure", description = "Retry the test on failure") 234 private boolean mRetryOnFailure = false; 235 236 // FIXME: Remove this once traces.txt is no longer needed. 237 @Option( 238 name = "upload-file-pattern", 239 description = 240 "File glob of on-device files to upload " 241 + "if found. Takes two arguments: the glob, and the file type " 242 + "(text/xml/zip/gzip/png/unknown). May be repeated.") 243 private Map<String, LogDataType> mUploadFilePatterns = new LinkedHashMap<>(); 244 245 @Option(name = "screenshot", description = "Take a device screenshot on monkey completion") 246 private boolean mScreenshot = false; 247 248 @Option( 249 name = "ignore-security-exceptions", 250 description = "Ignore SecurityExceptions while injecting events") 251 private boolean mIgnoreSecurityExceptions = true; 252 253 @Option( 254 name = "collect-atrace", 255 description = "Enable a continuous circular buffer to collect atrace information") 256 private boolean mAtraceEnabled = false; 257 258 private ITestDevice mTestDevice = null; 259 private BugreportItem mBugreport = null; 260 261 /** {@inheritDoc} */ 262 @Override run(TestInformation testInfo, ITestInvocationListener listener)263 public void run(TestInformation testInfo, ITestInvocationListener listener) 264 throws DeviceNotAvailableException { 265 Assert.assertNotNull(getDevice()); 266 267 TestDescription id = new TestDescription(getClass().getCanonicalName(), "monkey"); 268 long startTime = System.currentTimeMillis(); 269 270 listener.testRunStarted(getClass().getCanonicalName(), 1); 271 listener.testStarted(id); 272 273 try { 274 runMonkey(listener); 275 } catch (Exception | AssertionError e) { 276 listener.testRunFailed(FailureDescription.create(e.getMessage())); 277 } finally { 278 listener.testEnded(id, new HashMap<String, Metric>()); 279 listener.testRunEnded( 280 System.currentTimeMillis() - startTime, new HashMap<String, Metric>()); 281 } 282 } 283 284 /** Returns the command that should be used to launch the app, */ getAppCmdWithExtras()285 private String getAppCmdWithExtras() { 286 String extras = ""; 287 for (Map.Entry<String, String> sEntry : mStringExtras.entrySet()) { 288 extras += String.format(" -e %s %s", sEntry.getKey(), sEntry.getValue()); 289 } 290 for (Map.Entry<String, Integer> sEntry : mIntegerExtras.entrySet()) { 291 extras += String.format(" --ei %s %d", sEntry.getKey(), sEntry.getValue()); 292 } 293 return LAUNCH_APP_CMD + extras; 294 } 295 296 /** Run the monkey one time. */ runMonkey(ITestInvocationListener listener)297 protected void runMonkey(ITestInvocationListener listener) throws DeviceNotAvailableException { 298 ITestDevice device = getDevice(); 299 if (mRebootDevice) { 300 CLog.v("Rebooting device prior to running Monkey"); 301 device.reboot(); 302 } else { 303 CLog.v("Pre-run reboot disabled; skipping..."); 304 } 305 306 if (mIdleTimeSecs > 0) { 307 CLog.i("Sleeping for %d seconds to allow device to settle...", mIdleTimeSecs); 308 getRunUtil().sleep(mIdleTimeSecs * 1000); 309 CLog.i("Done sleeping."); 310 } 311 312 // launch the list of apps that needs warm-up 313 for (String componentName : mLaunchComponents) { 314 getDevice().executeShellCommand(String.format(getAppCmdWithExtras(), componentName)); 315 // give it some more time to settle down 316 getRunUtil().sleep(5000); 317 } 318 319 if (mUseAllowlistFile) { 320 // Use \r\n for new lines on the device. 321 String allowlist = ArrayUtil.join("\r\n", setSubtract(mPackages, mExcludePackages)); 322 device.pushString(allowlist.toString(), DEVICE_ALLOWLIST_PATH); 323 } 324 325 // Generate the monkey command to run, given the options 326 String command = buildMonkeyCommand(); 327 CLog.i( 328 "About to run monkey with at %d minute timeout: %s", 329 TimeUnit.MILLISECONDS.toMinutes(getMonkeyTimeoutMs()), command); 330 331 StringBuilder outputBuilder = new StringBuilder(); 332 CommandHelper commandHelper = new CommandHelper(); 333 334 long start = System.currentTimeMillis(); 335 long duration = 0; 336 Date dateAfter = null; 337 String uptimeAfter = NULL_UPTIME; 338 FileInputStreamSource atraceStream = null; 339 340 // Generate the monkey log prefix, which includes the device uptime 341 outputBuilder.append( 342 String.format( 343 "# %s - device uptime = %s: Monkey command used " 344 + "for this test:\nadb shell %s\n\n", 345 new Date().toString(), getUptime(), command)); 346 347 // Start atrace before running the monkey command, but after reboot 348 if (mAtraceEnabled) { 349 CircularAtraceUtil.startTrace(getDevice(), null, 10); 350 } 351 352 try { 353 onMonkeyStart(); 354 commandHelper.runCommand(mTestDevice, command, getMonkeyTimeoutMs()); 355 } finally { 356 // Wait for device to recover if it's not online. If it hasn't recovered, ignore. 357 try { 358 mTestDevice.waitForDeviceOnline(); 359 mTestDevice.enableAdbRoot(); 360 duration = System.currentTimeMillis() - start; 361 dateAfter = new Date(); 362 uptimeAfter = getUptime(); 363 onMonkeyFinish(); 364 takeScreenshot(listener, "screenshot"); 365 366 if (mAtraceEnabled) { 367 atraceStream = CircularAtraceUtil.endTrace(getDevice()); 368 } 369 370 mBugreport = takeBugreport(listener, BUGREPORT_NAME); 371 // FIXME: Remove this once traces.txt is no longer needed. 372 takeTraces(listener); 373 } finally { 374 // @@@ DO NOT add anything that requires device interaction into this block @@@ 375 // @@@ logging that no longer requires device interaction MUST be in this block @@@ 376 outputBuilder.append(commandHelper.getOutput()); 377 if (dateAfter == null) { 378 dateAfter = new Date(); 379 } 380 381 // Generate the monkey log suffix, which includes the device uptime. 382 outputBuilder.append( 383 String.format( 384 "\n# %s - device uptime = %s: Monkey command " 385 + "ran for: %d:%02d (mm:ss)\n", 386 dateAfter.toString(), 387 uptimeAfter, 388 duration / 1000 / 60, 389 duration / 1000 % 60)); 390 listener.testLog( 391 MONKEY_LOG_NAME, 392 LogDataType.MONKEY_LOG, 393 new ByteArrayInputStreamSource(outputBuilder.toString().getBytes())); 394 395 if (mAtraceEnabled) { 396 listener.testLog("circular-atrace", LogDataType.TEXT, atraceStream); 397 } 398 StreamUtil.cancel(atraceStream); 399 } 400 } 401 402 // Extra logs for what was found 403 if (mBugreport != null && mBugreport.getLastKmsg() != null) { 404 List<MiscKernelLogItem> kernelErrors = 405 mBugreport.getLastKmsg().getMiscEvents(KernelLogParser.KERNEL_ERROR); 406 List<MiscKernelLogItem> kernelResets = 407 mBugreport.getLastKmsg().getMiscEvents(KernelLogParser.KERNEL_ERROR); 408 CLog.d( 409 "Found %d kernel errors and %d kernel resets in last kmsg", 410 kernelErrors.size(), kernelResets.size()); 411 for (int i = 0; i < kernelErrors.size(); i++) { 412 String stack = kernelErrors.get(i).getStack(); 413 if (stack != null) { 414 CLog.d("Kernel Error #%d: %s", i + 1, stack.split("\n")[0].trim()); 415 } 416 } 417 for (int i = 0; i < kernelResets.size(); i++) { 418 String stack = kernelResets.get(i).getStack(); 419 if (stack != null) { 420 CLog.d("Kernel Reset #%d: %s", i + 1, stack.split("\n")[0].trim()); 421 } 422 } 423 } 424 } 425 426 /** A hook to allow subclasses to perform actions just before the monkey starts. */ onMonkeyStart()427 protected void onMonkeyStart() { 428 // empty 429 } 430 431 /** A hook to allow sublaccess to perform actions just after the monkey finished. */ onMonkeyFinish()432 protected void onMonkeyFinish() { 433 // empty 434 } 435 436 /** 437 * If enabled, capture a screenshot and send it to a listener. 438 * 439 * @throws DeviceNotAvailableException 440 */ takeScreenshot(ITestInvocationListener listener, String screenshotName)441 protected void takeScreenshot(ITestInvocationListener listener, String screenshotName) 442 throws DeviceNotAvailableException { 443 if (mScreenshot) { 444 try (InputStreamSource screenshot = mTestDevice.getScreenshot("JPEG")) { 445 listener.testLog(screenshotName, LogDataType.JPEG, screenshot); 446 } 447 } 448 } 449 450 /** Capture a bugreport and send it to a listener. */ takeBugreport(ITestInvocationListener listener, String bugreportName)451 protected BugreportItem takeBugreport(ITestInvocationListener listener, String bugreportName) { 452 Bugreport bugreport = mTestDevice.takeBugreport(); 453 if (bugreport == null) { 454 CLog.e("Could not take bugreport"); 455 return null; 456 } 457 bugreport.log(bugreportName, listener); 458 File main = null; 459 InputStreamSource is = null; 460 try { 461 main = bugreport.getMainFile(); 462 if (main == null) { 463 CLog.e("Bugreport has no main file"); 464 return null; 465 } 466 return new BugreportParser().parse(new BufferedReader(new FileReader(main))); 467 } catch (IOException e) { 468 CLog.e("Could not process bugreport"); 469 CLog.e(e); 470 return null; 471 } finally { 472 StreamUtil.close(bugreport); 473 StreamUtil.cancel(is); 474 FileUtil.deleteFile(main); 475 } 476 } 477 takeTraces(ITestInvocationListener listener)478 protected void takeTraces(ITestInvocationListener listener) { 479 DeviceFileReporter dfr = new DeviceFileReporter(mTestDevice, listener); 480 dfr.addPatterns(mUploadFilePatterns); 481 try { 482 dfr.run(); 483 } catch (DeviceNotAvailableException e) { 484 // Log but don't throw 485 CLog.e( 486 "Device %s became unresponsive while pulling files", 487 mTestDevice.getSerialNumber()); 488 } 489 } 490 491 /** 492 * A helper method to build a monkey command given the specified arguments. 493 * 494 * <p>Actual output argument order is: {@code monkey [-p PACKAGE]... [-c CATEGORY]... 495 * [--OPTION]... -s SEED -v -v -v COUNT} 496 * 497 * @return a {@link String} containing the command with the arguments assembled in the proper 498 * order. 499 */ buildMonkeyCommand()500 protected String buildMonkeyCommand() { 501 List<String> cmdList = new LinkedList<>(); 502 cmdList.add("monkey"); 503 504 if (!mUseAllowlistFile) { 505 for (String pkg : setSubtract(mPackages, mExcludePackages)) { 506 cmdList.add("-p"); 507 cmdList.add(pkg); 508 } 509 } 510 511 for (String cat : mCategories) { 512 cmdList.add("-c"); 513 cmdList.add(cat); 514 } 515 516 if (mIgnoreSecurityExceptions) { 517 cmdList.add("--ignore-security-exceptions"); 518 } 519 520 if (mThrottle >= 1) { 521 cmdList.add("--throttle"); 522 cmdList.add(Long.toString(mThrottle)); 523 } 524 if (mIgnoreCrashes) { 525 cmdList.add("--ignore-crashes"); 526 } 527 if (mIgnoreTimeouts) { 528 cmdList.add("--ignore-timeouts"); 529 } 530 531 if (mUseAllowlistFile) { 532 cmdList.add("--pkg-whitelist-file"); 533 cmdList.add(DEVICE_ALLOWLIST_PATH); 534 } 535 536 for (String arg : mMonkeyArgs) { 537 String[] args = arg.split(":"); 538 cmdList.add(String.format("--%s", args[0])); 539 if (args.length > 1) { 540 cmdList.add(args[1]); 541 } 542 } 543 544 cmdList.addAll(mOptions); 545 546 cmdList.add("-s"); 547 if (mRandomSeed == null) { 548 // Pick a number that is random, but in a small enough range that some seeds are likely 549 // to be repeated 550 cmdList.add(Long.toString(new Random().nextInt(1000))); 551 } else { 552 cmdList.add(Long.toString(mRandomSeed)); 553 } 554 555 // verbose 556 cmdList.add("-v"); 557 cmdList.add("-v"); 558 cmdList.add("-v"); 559 cmdList.add(Integer.toString(mTargetCount)); 560 561 return ArrayUtil.join(" ", cmdList); 562 } 563 564 /** 565 * Get a {@link String} containing the number seconds since the device was booted. 566 * 567 * <p>{@code NULL_UPTIME} is returned if the device becomes unresponsive. Used in the monkey log 568 * prefix and suffix. 569 */ getUptime()570 protected String getUptime() { 571 try { 572 // make two attempts to get valid uptime 573 for (int i = 0; i < 2; i++) { 574 // uptime will typically have a format like "5278.73 1866.80". Use the first one 575 // (which is wall-time) 576 String uptime = mTestDevice.executeShellCommand("cat /proc/uptime").split(" ")[0]; 577 try { 578 Float.parseFloat(uptime); 579 // if this parsed, its a valid uptime 580 return uptime; 581 } catch (NumberFormatException e) { 582 CLog.w( 583 "failed to get valid uptime from %s. Received: '%s'", 584 mTestDevice.getSerialNumber(), uptime); 585 } 586 } 587 } catch (DeviceNotAvailableException e) { 588 CLog.e( 589 "Device %s became unresponsive while getting the uptime.", 590 mTestDevice.getSerialNumber()); 591 } 592 return NULL_UPTIME; 593 } 594 595 /** 596 * Perform set subtraction between two {@link Collection} objects. 597 * 598 * <p>The return value will consist of all of the elements of {@code keep}, excluding the 599 * elements that are also in {@code exclude}. Exposed for unit testing. 600 * 601 * @param keep the minuend in the subtraction 602 * @param exclude the subtrahend 603 * @return the collection of elements in {@code keep} that are not also in {@code exclude}. If 604 * {@code keep} is an ordered {@link Collection}, the remaining elements in the return value 605 * will remain in their original order. 606 */ setSubtract(Collection<String> keep, Collection<String> exclude)607 static Collection<String> setSubtract(Collection<String> keep, Collection<String> exclude) { 608 if (exclude.isEmpty()) { 609 return keep; 610 } 611 612 Collection<String> output = new ArrayList<>(keep); 613 output.removeAll(exclude); 614 return output; 615 } 616 617 /** Get {@link IRunUtil} to use. Exposed for unit testing. */ getRunUtil()618 IRunUtil getRunUtil() { 619 return RunUtil.getDefault(); 620 } 621 622 /** {@inheritDoc} */ 623 @Override setDevice(ITestDevice device)624 public void setDevice(ITestDevice device) { 625 mTestDevice = device; 626 } 627 628 /** {@inheritDoc} */ 629 @Override getDevice()630 public ITestDevice getDevice() { 631 return mTestDevice; 632 } 633 634 /** Get the monkey timeout in milliseconds */ getMonkeyTimeoutMs()635 protected long getMonkeyTimeoutMs() { 636 return (mPerEventTimeout + mThrottle) * mTargetCount; 637 } 638 } 639