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