| #!/usr/bin/env python2.7 |
| |
| import argparse |
| import datetime |
| import os |
| import re |
| import subprocess |
| import sys |
| import threading |
| import time |
| |
| QUIET = False |
| |
| # ANSI escape sequences |
| if sys.stdout.isatty(): |
| BOLD = "\033[1m" |
| RED = "\033[91m" + BOLD |
| GREEN = "\033[92m" + BOLD |
| YELLOW = "\033[93m" + BOLD |
| UNDERLINE = "\033[4m" |
| ENDCOLOR = "\033[0m" |
| CLEARLINE = "\033[K" |
| STDOUT_IS_TTY = True |
| else: |
| BOLD = "" |
| RED = "" |
| GREEN = "" |
| YELLOW = "" |
| UNDERLINE = "" |
| ENDCOLOR = "" |
| CLEARLINE = "" |
| STDOUT_IS_TTY = False |
| |
| def PrintStatus(s): |
| """Prints a bold underlined status message""" |
| sys.stdout.write("\n") |
| sys.stdout.write(BOLD) |
| sys.stdout.write(UNDERLINE) |
| sys.stdout.write(s) |
| sys.stdout.write(ENDCOLOR) |
| sys.stdout.write("\n") |
| |
| |
| def PrintCommand(cmd, env=None): |
| """Prints a bold line of a shell command that is being run""" |
| if not QUIET: |
| sys.stdout.write(BOLD) |
| if env: |
| for k,v in env.iteritems(): |
| if " " in v and "\"" not in v: |
| sys.stdout.write("%s=\"%s\" " % (k, v.replace("\"", "\\\""))) |
| else: |
| sys.stdout.write("%s=%s " % (k, v)) |
| sys.stdout.write(" ".join(cmd)) |
| sys.stdout.write(ENDCOLOR) |
| sys.stdout.write("\n") |
| |
| |
| class ExecutionException(Exception): |
| """Thrown to cleanly abort operation.""" |
| def __init__(self,*args,**kwargs): |
| Exception.__init__(self,*args,**kwargs) |
| |
| |
| class Adb(object): |
| """Encapsulates adb functionality.""" |
| |
| def __init__(self): |
| """Initialize adb.""" |
| self._command = ["adb"] |
| |
| |
| def Exec(self, cmd, stdout=None, stderr=None): |
| """Runs an adb command, and prints that command to stdout. |
| |
| Raises: |
| ExecutionException: if the adb command returned an error. |
| |
| Example: |
| adb.Exec("shell", "ls") will run "adb shell ls" |
| """ |
| cmd = self._command + cmd |
| PrintCommand(cmd) |
| result = subprocess.call(cmd, stdout=stdout, stderr=stderr) |
| if result: |
| raise ExecutionException("adb: %s returned %s" % (cmd, result)) |
| |
| |
| def WaitForDevice(self): |
| """Waits for the android device to be available on usb with adbd running.""" |
| self.Exec(["wait-for-device"]) |
| |
| |
| def Run(self, cmd, stdout=None, stderr=None): |
| """Waits for the device, and then runs a command. |
| |
| Raises: |
| ExecutionException: if the adb command returned an error. |
| |
| Example: |
| adb.Run("shell", "ls") will run "adb shell ls" |
| """ |
| self.WaitForDevice() |
| self.Exec(cmd, stdout=stdout, stderr=stderr) |
| |
| |
| def Get(self, cmd): |
| """Waits for the device, and then runs a command, returning the output. |
| |
| Raises: |
| ExecutionException: if the adb command returned an error. |
| |
| Example: |
| adb.Get(["shell", "ls"]) will run "adb shell ls" |
| """ |
| self.WaitForDevice() |
| cmd = self._command + cmd |
| PrintCommand(cmd) |
| try: |
| text = subprocess.check_output(cmd) |
| return text.strip() |
| except subprocess.CalledProcessError as ex: |
| raise ExecutionException("adb: %s returned %s" % (cmd, ex.returncode)) |
| |
| |
| def Shell(self, cmd, stdout=None, stderr=None): |
| """Runs an adb shell command |
| Args: |
| cmd: The command to run. |
| |
| Raises: |
| ExecutionException: if the adb command returned an error. |
| |
| Example: |
| adb.Shell(["ls"]) will run "adb shell ls" |
| """ |
| cmd = ["shell"] + cmd |
| self.Run(cmd, stdout=stdout, stderr=stderr) |
| |
| |
| def GetProp(self, name): |
| """Gets a system property from the device.""" |
| return self.Get(["shell", "getprop", name]) |
| |
| |
| def Reboot(self): |
| """Reboots the device, and waits for boot to complete.""" |
| # Reboot |
| self.Run(["reboot"]) |
| # Wait until it comes back on adb |
| self.WaitForDevice() |
| # Poll until the system says it's booted |
| while self.GetProp("sys.boot_completed") != "1": |
| time.sleep(2) |
| # Dismiss the keyguard |
| self.Shell(["wm", "dismiss-keyguard"]); |
| |
| def GetBatteryProperties(self): |
| """A dict of the properties from adb shell dumpsys battery""" |
| def ConvertVal(s): |
| if s == "true": |
| return True |
| elif s == "false": |
| return False |
| else: |
| try: |
| return int(s) |
| except ValueError: |
| return s |
| text = self.Get(["shell", "dumpsys", "battery"]) |
| lines = [line.strip() for line in text.split("\n")][1:] |
| lines = [[s.strip() for s in line.split(":", 1)] for line in lines] |
| lines = [(k,ConvertVal(v)) for k,v in lines] |
| return dict(lines) |
| |
| def GetBatteryLevel(self): |
| """Returns the battery level""" |
| return self.GetBatteryProperties()["level"] |
| |
| |
| |
| def CurrentTimestamp(): |
| """Returns the current time in a format suitable for filenames.""" |
| return datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") |
| |
| |
| def ParseOptions(): |
| """Parse the command line options. |
| |
| Returns an argparse options object. |
| """ |
| parser = argparse.ArgumentParser(description="Run monkeys and collect the results.") |
| parser.add_argument("--dir", action="store", |
| help="output directory for results of monkey runs") |
| parser.add_argument("--events", action="store", type=int, default=125000, |
| help="number of events per monkey run") |
| parser.add_argument("-p", action="append", dest="packages", |
| help="package to use (default is a set of system-wide packages") |
| parser.add_argument("--runs", action="store", type=int, default=10000000, |
| help="number of monkey runs to perform") |
| parser.add_argument("--type", choices=["crash", "anr"], |
| help="only stop on errors of the given type (crash or anr)") |
| parser.add_argument("--description", action="store", |
| help="only stop if the error description contains DESCRIPTION") |
| |
| options = parser.parse_args() |
| |
| if not options.dir: |
| options.dir = "monkeys-%s" % CurrentTimestamp() |
| |
| if not options.packages: |
| options.packages = [ |
| "com.google.android.deskclock", |
| "com.android.calculator2", |
| "com.google.android.contacts", |
| "com.android.launcher", |
| "com.google.android.launcher", |
| "com.android.mms", |
| "com.google.android.apps.messaging", |
| "com.android.phone", |
| "com.google.android.dialer", |
| "com.android.providers.downloads.ui", |
| "com.android.settings", |
| "com.google.android.calendar", |
| "com.google.android.GoogleCamera", |
| "com.google.android.apps.photos", |
| "com.google.android.gms", |
| "com.google.android.setupwizard", |
| "com.google.android.googlequicksearchbox", |
| "com.google.android.packageinstaller", |
| "com.google.android.apps.nexuslauncher" |
| ] |
| |
| return options |
| |
| |
| adb = Adb() |
| |
| def main(): |
| """Main entry point.""" |
| |
| def LogcatThreadFunc(): |
| logcatProcess.communicate() |
| |
| options = ParseOptions() |
| |
| # Set up the device a little bit |
| PrintStatus("Setting up the device") |
| adb.Run(["root"]) |
| time.sleep(2) |
| adb.WaitForDevice() |
| adb.Run(["remount"]) |
| time.sleep(2) |
| adb.WaitForDevice() |
| adb.Shell(["echo ro.audio.silent=1 > /data/local.prop"]) |
| adb.Shell(["chmod 644 /data/local.prop"]) |
| |
| # Figure out how many leading zeroes we need. |
| pattern = "%%0%dd" % len(str(options.runs-1)) |
| |
| # Make the output directory |
| if os.path.exists(options.dir) and not os.path.isdir(options.dir): |
| sys.stderr.write("Output directory already exists and is not a directory: %s\n" |
| % options.dir) |
| sys.exit(1) |
| elif not os.path.exists(options.dir): |
| os.makedirs(options.dir) |
| |
| # Run the tests |
| for run in range(1, options.runs+1): |
| PrintStatus("Run %d of %d: %s" % (run, options.runs, |
| datetime.datetime.now().strftime("%A, %B %d %Y %I:%M %p"))) |
| |
| # Reboot and wait for 30 seconds to let the system quiet down so the |
| # log isn't polluted with all the boot completed crap. |
| if True: |
| adb.Reboot() |
| PrintCommand(["sleep", "30"]) |
| time.sleep(30) |
| |
| # Monkeys can outrun the battery, so if it's getting low, pause to |
| # let it charge. |
| if True: |
| targetBatteryLevel = 20 |
| while True: |
| level = adb.GetBatteryLevel() |
| if level > targetBatteryLevel: |
| break |
| print "Battery level is %d%%. Pausing to let it charge above %d%%." % ( |
| level, targetBatteryLevel) |
| time.sleep(60) |
| |
| filebase = os.path.sep.join((options.dir, pattern % run)) |
| bugreportFilename = filebase + "-bugreport.txt" |
| monkeyFilename = filebase + "-monkey.txt" |
| logcatFilename = filebase + "-logcat.txt" |
| htmlFilename = filebase + ".html" |
| |
| monkeyFile = file(monkeyFilename, "w") |
| logcatFile = file(logcatFilename, "w") |
| bugreportFile = None |
| |
| # Clear the log, then start logcat |
| adb.Shell(["logcat", "-c", "-b", "main,system,events,crash"]) |
| cmd = ["adb", "logcat", "-b", "main,system,events,crash"] |
| PrintCommand(cmd) |
| logcatProcess = subprocess.Popen(cmd, stdout=logcatFile, stderr=None) |
| logcatThread = threading.Thread(target=LogcatThreadFunc) |
| logcatThread.start() |
| |
| # Run monkeys |
| cmd = [ |
| "monkey", |
| "-c", "android.intent.category.LAUNCHER", |
| "--ignore-security-exceptions", |
| "--monitor-native-crashes", |
| "-v", "-v", "-v" |
| ] |
| for pkg in options.packages: |
| cmd.append("-p") |
| cmd.append(pkg) |
| if options.type == "anr": |
| cmd.append("--ignore-crashes") |
| cmd.append("--ignore-native-crashes") |
| if options.type == "crash": |
| cmd.append("--ignore-timeouts") |
| if options.description: |
| cmd.append("--match-description") |
| cmd.append("'" + options.description + "'") |
| cmd.append(str(options.events)) |
| try: |
| adb.Shell(cmd, stdout=monkeyFile, stderr=monkeyFile) |
| needReport = False |
| except ExecutionException: |
| # Monkeys failed, take a bugreport |
| bugreportFile = file(bugreportFilename, "w") |
| adb.Shell(["bugreport"], stdout=bugreportFile, stderr=None) |
| needReport = True |
| finally: |
| monkeyFile.close() |
| try: |
| logcatProcess.terminate() |
| except OSError: |
| pass # it must have died on its own |
| logcatThread.join() |
| logcatFile.close() |
| if bugreportFile: |
| bugreportFile.close() |
| |
| if needReport: |
| # Generate the html |
| cmd = ["bugreport", "--monkey", monkeyFilename, "--html", htmlFilename, |
| "--logcat", logcatFilename, bugreportFilename] |
| PrintCommand(cmd) |
| result = subprocess.call(cmd) |
| |
| |
| |
| if __name__ == "__main__": |
| main() |
| |
| # vim: set ts=2 sw=2 sts=2 expandtab nocindent autoindent: |