| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.monkey; |
| |
| import com.android.ddmlib.CollectingOutputReceiver; |
| import com.android.ddmlib.IShellOutputReceiver; |
| import com.android.ddmlib.testrunner.TestIdentifier; |
| import com.android.loganalysis.item.MonkeyLogItem; |
| import com.android.loganalysis.parser.MonkeyLogParser; |
| import com.android.tradefed.config.Option; |
| import com.android.tradefed.config.Option.Importance; |
| import com.android.tradefed.device.DeviceNotAvailableException; |
| import com.android.tradefed.device.ITestDevice; |
| import com.android.tradefed.log.LogUtil.CLog; |
| import com.android.tradefed.result.ByteArrayInputStreamSource; |
| import com.android.tradefed.result.DeviceFileReporter; |
| import com.android.tradefed.result.ITestInvocationListener; |
| import com.android.tradefed.result.InputStreamSource; |
| import com.android.tradefed.result.LogDataType; |
| import com.android.tradefed.testtype.IDeviceTest; |
| import com.android.tradefed.testtype.IRemoteTest; |
| import com.android.tradefed.testtype.IRetriableTest; |
| import com.android.tradefed.util.ArrayUtil; |
| import com.android.tradefed.util.IRunUtil; |
| import com.android.tradefed.util.RunUtil; |
| |
| import junit.framework.Assert; |
| |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Random; |
| |
| /** |
| * Runner for stress tests which use the monkey command. |
| */ |
| public class MonkeyBase implements IDeviceTest, IRemoteTest, IRetriableTest { |
| |
| public static final String MONKEY_LOG_NAME = "monkey_log"; |
| public static final String BUGREPORT_NAME = "bugreport"; |
| |
| /** |
| * Allow a 15 second buffer between the monkey run time and the delta uptime. |
| */ |
| public static final long UPTIME_BUFFER = 15 * 1000; |
| |
| private static final String DEVICE_WHITELIST_PATH = "/data/local/tmp/monkey_whitelist.txt"; |
| |
| /** |
| * am command template to launch app intent with same action, category and task flags as if user |
| * started it from the app's launcher icon |
| */ |
| private static final String LAUNCH_APP_CMD = "am start -W -n '%s' " + |
| "-a android.intent.action.MAIN -c android.intent.category.LAUNCHER -f 0x10200000"; |
| |
| /** |
| * Helper to run a monkey command with an absolute timeout. |
| * <p> |
| * This is used so that the command can be stopped after a set timeout, since the timeout that |
| * {@link ITestDevice#executeShellCommand(String, IShellOutputReceiver, int, int)} takes applies |
| * to the time between output, not the overall time of the command. |
| * </p> |
| */ |
| private class CommandHelper { |
| private DeviceNotAvailableException mException = null; |
| private String mOutput = null; |
| |
| public void runCommand(final ITestDevice device, final String command, long timeout) |
| throws DeviceNotAvailableException { |
| final CollectingOutputReceiver receiver = new CollectingOutputReceiver(); |
| Thread t = new Thread() { |
| @Override |
| public void run() { |
| try { |
| device.executeShellCommand(command, receiver); |
| } catch (DeviceNotAvailableException e) { |
| mException = e; |
| } |
| } |
| }; |
| |
| t.start(); |
| |
| try { |
| t.join(timeout); |
| } catch (InterruptedException e) { |
| // Ignore and log. The thread should terminate once receiver.cancel() is called. |
| CLog.e("Thread was interrupted while running %s", command); |
| } |
| |
| mOutput = receiver.getOutput(); |
| receiver.cancel(); |
| |
| if (mException != null) { |
| throw mException; |
| } |
| } |
| |
| public String getOutput() { |
| return mOutput; |
| } |
| } |
| |
| @Option(name = "package", description = "Package name to send events to. May be repeated.") |
| private Collection<String> mPackages = new LinkedList<String>(); |
| |
| @Option(name = "exclude-package", description = "Substring of package names to exclude from " + |
| "the package list. May be repeated.", importance = Importance.IF_UNSET) |
| private Collection<String> mExcludePackages = new HashSet<String>(); |
| |
| @Option(name = "category", description = "App Category. May be repeated.") |
| private Collection<String> mCategories = new LinkedList<String>(); |
| |
| @Option(name = "option", description = "Option to pass to monkey command. May be repeated.") |
| private Collection<String> mOptions = new LinkedList<String>(); |
| |
| @Option(name = "target-count", description = "Target number of events to send.") |
| private int mTargetCount = 125000; |
| |
| @Option(name = "random-seed", description = "Random seed to use for the monkey.") |
| private Long mRandomSeed = null; |
| |
| @Option(name = "throttle", description = "How much time to wait between sending successive " + |
| "events, in msecs. Default is 0ms.") |
| private int mThrottle = 0; |
| |
| @Option(name = "ignore-crashes", description = "Monkey should keep going after encountering " + |
| "an app crash") |
| private boolean mIgnoreCrashes = false; |
| |
| @Option(name = "ignore-timeout", description = "Monkey should keep going after encountering " + |
| "an app timeout (ANR)") |
| private boolean mIgnoreTimeouts = false; |
| |
| @Option(name = "reboot-device", description = "Reboot device before running monkey. Defaults " + |
| "to true.") |
| private boolean mRebootDevice = true; |
| |
| @Option(name = "idle-time", description = "How long to sleep before running monkey, in secs") |
| private int mIdleTimeSecs = 5 * 60; |
| |
| @Option(name = "monkey-arg", description = "Extra parameters to pass onto monkey. Key/value " + |
| "pairs should be passed as key:value. May be repeated.") |
| private Collection<String> mMonkeyArgs = new LinkedList<String>(); |
| |
| @Option(name = "use-pkg-whitelist-file", description = "Whether to use the monkey " + |
| "--pkg-whitelist-file option to work around cmdline length limits") |
| private boolean mUseWhitelistFile = false; |
| |
| @Option(name = "monkey-timeout", description = "How long to wait for the monkey to " + |
| "complete, in minutes. Default is 4 hours.") |
| private int mMonkeyTimeout = 4 * 60; |
| |
| @Option(name = "warmup-component", description = "Component name of app to launch for " + |
| "\"warming up\" before monkey test, will be used in an intent together with standard " + |
| "flags and parameters as launched from Launcher. May be repeated") |
| private List<String> mLaunchComponents = new ArrayList<String>(); |
| |
| @Option(name = "retry-on-failure", description = "Retry the test on failure") |
| private boolean mRetryOnFailure = false; |
| |
| // FIXME: Remove this once traces.txt is no longer needed. |
| @Option(name = "upload-file-pattern", description = "File glob of on-device files to upload " + |
| "if found. Takes two arguments: the glob, and the file type " + |
| "(text/xml/zip/gzip/png/unknown). May be repeated.") |
| private Map<String, LogDataType> mUploadFilePatterns = new LinkedHashMap<String, LogDataType>(); |
| |
| @Option(name = "screenshot", description = "Take a device screenshot on monkey completion") |
| private boolean mScreenshot = false; |
| |
| private ITestDevice mTestDevice = null; |
| private MonkeyLogItem mMonkeyLog = null; |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { |
| Assert.assertNotNull(getDevice()); |
| |
| TestIdentifier id = new TestIdentifier(getClass().getCanonicalName(), "monkey"); |
| long startTime = System.currentTimeMillis(); |
| |
| listener.testRunStarted(getClass().getCanonicalName(), 1); |
| listener.testStarted(id); |
| |
| try { |
| runMonkey(listener); |
| } finally { |
| Map<String, String> empty = Collections.emptyMap(); |
| listener.testEnded(id, empty); |
| listener.testRunEnded(System.currentTimeMillis() - startTime, empty); |
| } |
| } |
| |
| /** |
| * Run the monkey one time and return a {@link MonkeyLogItem} for the run. |
| */ |
| protected void runMonkey(ITestInvocationListener listener) throws DeviceNotAvailableException { |
| ITestDevice device = getDevice(); |
| if (mRebootDevice) { |
| CLog.v("Rebooting device prior to running Monkey"); |
| device.reboot(); |
| } else { |
| CLog.v("Pre-run reboot disabled; skipping..."); |
| } |
| |
| if (mIdleTimeSecs > 0) { |
| CLog.i("Sleeping for %d seconds to allow device to settle...", mIdleTimeSecs); |
| getRunUtil().sleep(mIdleTimeSecs * 1000); |
| CLog.i("Done sleeping."); |
| } |
| |
| // launch the list of apps that needs warm-up |
| for (String componentName : mLaunchComponents) { |
| getDevice().executeShellCommand(String.format(LAUNCH_APP_CMD, componentName)); |
| // give it some more time to settle down |
| getRunUtil().sleep(5000); |
| } |
| |
| if (mUseWhitelistFile) { |
| // Use \r\n for new lines on the device. |
| String whitelist = ArrayUtil.join("\r\n", setSubtract(mPackages, mExcludePackages)); |
| device.pushString(whitelist.toString(), DEVICE_WHITELIST_PATH); |
| } |
| |
| // Generate the monkey command to run, given the options |
| String command = buildMonkeyCommand(); |
| CLog.i("About to run monkey with at %d minute timeout: %s", mMonkeyTimeout, command); |
| |
| StringBuilder outputBuilder = new StringBuilder(); |
| CommandHelper commandHelper = new CommandHelper(); |
| long duration = 0; |
| |
| // Generate the monkey log prefix, which includes the device uptime |
| outputBuilder.append(String.format("# %s - device uptime = %s: Monkey command used " + |
| "for this test:\nadb shell %s\n\n", new Date().toString(), getUptime(), command)); |
| |
| try { |
| long start = System.currentTimeMillis(); |
| commandHelper.runCommand(mTestDevice, command, getMonkeyTimeoutMs()); |
| duration = System.currentTimeMillis() - start; |
| } finally { |
| outputBuilder.append(commandHelper.getOutput()); |
| |
| // Generate the monkey log suffix, which includes the device uptime. |
| outputBuilder.append(String.format("\n# %s - device uptime = %s: Monkey command ran " + |
| "for: %d:%02d (mm:ss)\n", new Date().toString(), getUptime(), |
| duration / 1000 / 60, duration / 1000 % 60)); |
| |
| // Wait for device to recover if it's not online. If it hasn't recovered, ignore. |
| try { |
| mTestDevice.waitForDeviceOnline(2 * 60 * 1000); |
| } catch (DeviceNotAvailableException e) { |
| CLog.w("Device %s not available after 2 minutes.", mTestDevice.getSerialNumber()); |
| } |
| |
| takeScreenshot(listener, "screenshot"); |
| |
| takeBugreport(listener, BUGREPORT_NAME); |
| // FIXME: Remove this once traces.txt is no longer needed. |
| takeTraces(listener); |
| mMonkeyLog = createMonkeyLog(listener, MONKEY_LOG_NAME, outputBuilder.toString()); |
| } |
| |
| checkResults(); |
| } |
| |
| /** |
| * If enabled, capture a screenshot and send it to a listener. |
| * @throws DeviceNotAvailableException |
| */ |
| protected void takeScreenshot(ITestInvocationListener listener, String screenshotName) |
| throws DeviceNotAvailableException { |
| if (mScreenshot) { |
| InputStreamSource screenshot = mTestDevice.getScreenshot(); |
| try { |
| listener.testLog(screenshotName, LogDataType.PNG, screenshot); |
| } finally { |
| screenshot.cancel(); |
| } |
| } |
| } |
| |
| /** |
| * Capture a bugreport and send it to a listener. |
| */ |
| protected void takeBugreport(ITestInvocationListener listener, String bugreportName) { |
| InputStreamSource bugreport = mTestDevice.getBugreport(); |
| try { |
| listener.testLog(bugreportName, LogDataType.BUGREPORT, bugreport); |
| } finally { |
| bugreport.cancel(); |
| } |
| } |
| |
| protected void takeTraces(ITestInvocationListener listener) { |
| DeviceFileReporter dfr = new DeviceFileReporter(mTestDevice, listener); |
| dfr.addPatterns(mUploadFilePatterns); |
| try { |
| dfr.run(); |
| } catch (DeviceNotAvailableException e) { |
| // Log but don't throw |
| CLog.e("Device %s became unresponsive while pulling files", |
| mTestDevice.getSerialNumber()); |
| } |
| } |
| |
| /** |
| * Create the monkey log, parse it, and send it to a listener. |
| */ |
| protected MonkeyLogItem createMonkeyLog(ITestInvocationListener listener, String monkeyLogName, |
| String log) { |
| |
| InputStreamSource source = new ByteArrayInputStreamSource(log.getBytes()); |
| try { |
| listener.testLog(monkeyLogName, LogDataType.MONKEY_LOG, source); |
| return new MonkeyLogParser().parse(new BufferedReader(new InputStreamReader( |
| source.createInputStream()))); |
| } catch (IOException e) { |
| CLog.e("Could not parse monkey log"); |
| return null; |
| } finally { |
| source.cancel(); |
| } |
| } |
| |
| /** |
| * A helper method to build a monkey command given the specified arguments. |
| * <p> |
| * Actual output argument order is: |
| * {@code monkey [-p PACKAGE]... [-c CATEGORY]... [--OPTION]... -s SEED -v -v -v COUNT} |
| * </p> |
| * |
| * @return a {@link String} containing the command with the arguments assembled in the proper |
| * order. |
| */ |
| protected String buildMonkeyCommand() { |
| List<String> cmdList = new LinkedList<String>(); |
| cmdList.add("monkey"); |
| |
| if (!mUseWhitelistFile) { |
| for (String pkg : setSubtract(mPackages, mExcludePackages)) { |
| cmdList.add("-p"); |
| cmdList.add(pkg); |
| } |
| } |
| |
| for (String cat : mCategories) { |
| cmdList.add("-c"); |
| cmdList.add(cat); |
| } |
| |
| cmdList.add("--ignore-security-exceptions"); |
| |
| if (mThrottle >= 1) { |
| cmdList.add("--throttle"); |
| cmdList.add(Integer.toString(mThrottle)); |
| } |
| if (mIgnoreCrashes) { |
| cmdList.add("--ignore-crashes"); |
| } |
| if (mIgnoreTimeouts) { |
| cmdList.add("--ignore-timeouts"); |
| } |
| |
| if (mUseWhitelistFile) { |
| cmdList.add("--pkg-whitelist-file"); |
| cmdList.add(DEVICE_WHITELIST_PATH); |
| } |
| |
| for (String arg : mMonkeyArgs) { |
| String[] args = arg.split(":"); |
| cmdList.add(String.format("--%s", args[0])); |
| if (args.length > 1) { |
| cmdList.add(args[1]); |
| } |
| } |
| |
| cmdList.addAll(mOptions); |
| |
| cmdList.add("-s"); |
| if (mRandomSeed == null) { |
| // Pick a number that is random, but in a small enough range that some seeds are likely |
| // to be repeated |
| cmdList.add(Long.toString(new Random().nextInt(1000))); |
| } else { |
| cmdList.add(Long.toString(mRandomSeed)); |
| } |
| |
| // verbose |
| cmdList.add("-v"); |
| cmdList.add("-v"); |
| cmdList.add("-v"); |
| cmdList.add(Integer.toString(mTargetCount)); |
| |
| return ArrayUtil.join(" ", cmdList); |
| } |
| |
| /** |
| * Get a {@link String} containing the number seconds since the device was booted. |
| * <p> |
| * {@code "0.00"} is returned if the device becomes unresponsive. Used in the monkey log prefix |
| * and suffix. |
| * </p> |
| */ |
| protected String getUptime() { |
| try { |
| // make two attempts to get valid uptime |
| for (int i = 0; i < 2; i++) { |
| // uptime will typically have a format like "5278.73 1866.80". Use the first one |
| // (which is wall-time) |
| String uptime = mTestDevice.executeShellCommand("cat /proc/uptime").split(" ")[0]; |
| try { |
| Float.parseFloat(uptime); |
| // if this parsed, its a valid uptime |
| return uptime; |
| } catch (NumberFormatException e) { |
| CLog.w("failed to get valid uptime from %s. Received: '%s'", |
| mTestDevice.getSerialNumber(), uptime); |
| } |
| } |
| } catch (DeviceNotAvailableException e) { |
| CLog.e("Device %s became unresponsive while getting the uptime.", |
| mTestDevice.getSerialNumber()); |
| } |
| return "0.00"; |
| } |
| |
| /** |
| * Perform set subtraction between two {@link Collection} objects. |
| * <p> |
| * The return value will consist of all of the elements of {@code keep}, excluding the elements |
| * that are also in {@code exclude}. Exposed for unit testing. |
| * </p> |
| * |
| * @param keep the minuend in the subtraction |
| * @param exclude the subtrahend |
| * @return the collection of elements in {@code keep} that are not also in {@code exclude}. If |
| * {@code keep} is an ordered {@link Collection}, the remaining elements in the return value |
| * will remain in their original order. |
| */ |
| static Collection<String> setSubtract(Collection<String> keep, Collection<String> exclude) { |
| if (exclude.isEmpty()) { |
| return keep; |
| } |
| |
| Collection<String> output = new ArrayList<String>(keep); |
| output.removeAll(exclude); |
| return output; |
| } |
| |
| /** |
| * Get {@link IRunUtil} to use. Exposed for unit testing. |
| */ |
| IRunUtil getRunUtil() { |
| return RunUtil.getDefault(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void setDevice(ITestDevice device) { |
| mTestDevice = device; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public ITestDevice getDevice() { |
| return mTestDevice; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @return {@code false} if retry-on-failure is not set, if the monkey ran to completion, |
| * crashed in an understood way, or if there were no packages to run, {@code true} otherwise. |
| */ |
| @Override |
| public boolean isRetriable() { |
| return mRetryOnFailure; |
| } |
| |
| /** |
| * Check the results and return if valid or throw an assertion error if not valid. |
| */ |
| private void checkResults() { |
| if (!isRetriable()) { |
| return; |
| } |
| |
| Assert.assertNotNull("Monkey log is null", mMonkeyLog); |
| |
| // If there are no activities, retrying the test won't matter. |
| if (mMonkeyLog.getNoActivities()) { |
| return; |
| } |
| |
| Assert.assertNotNull("Start uptime is missing", mMonkeyLog.getStartUptimeDuration()); |
| Assert.assertNotNull("Stop uptime is missing", mMonkeyLog.getStopUptimeDuration()); |
| Assert.assertNotNull("Total duration is missing", mMonkeyLog.getTotalDuration()); |
| |
| long startUptime = mMonkeyLog.getStartUptimeDuration(); |
| long stopUptime = mMonkeyLog.getStopUptimeDuration(); |
| long totalDuration = mMonkeyLog.getTotalDuration(); |
| |
| Assert.assertTrue("Uptime failure", |
| stopUptime - startUptime > totalDuration - UPTIME_BUFFER); |
| |
| // False count |
| Assert.assertFalse("False count", mMonkeyLog.getIsFinished() && |
| mMonkeyLog.getTargetCount() - mMonkeyLog.getIntermediateCount() > 100); |
| |
| // Monkey finished or crashed, so don't fail |
| if (mMonkeyLog.getIsFinished() || mMonkeyLog.getFinalCount() != null) { |
| return; |
| } |
| |
| // Missing count |
| Assert.fail("Missing count"); |
| } |
| |
| /** |
| * Get the monkey timeout in milliseconds |
| */ |
| protected long getMonkeyTimeoutMs() { |
| return mMonkeyTimeout * 60 * 1000; |
| } |
| } |