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