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