Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/env python2.7
      2 
      3 import argparse
      4 import datetime
      5 import os
      6 import re
      7 import subprocess
      8 import sys
      9 import threading
     10 import time
     11 
     12 QUIET = False
     13 
     14 # ANSI escape sequences
     15 if sys.stdout.isatty():
     16   BOLD = "\033[1m"
     17   RED = "\033[91m" + BOLD
     18   GREEN = "\033[92m" + BOLD
     19   YELLOW = "\033[93m" + BOLD
     20   UNDERLINE = "\033[4m"
     21   ENDCOLOR = "\033[0m"
     22   CLEARLINE = "\033[K"
     23   STDOUT_IS_TTY = True
     24 else:
     25   BOLD = ""
     26   RED = ""
     27   GREEN = ""
     28   YELLOW = ""
     29   UNDERLINE = ""
     30   ENDCOLOR = ""
     31   CLEARLINE = ""
     32   STDOUT_IS_TTY = False
     33 
     34 def PrintStatus(s):
     35   """Prints a bold underlined status message"""
     36   sys.stdout.write("\n")
     37   sys.stdout.write(BOLD)
     38   sys.stdout.write(UNDERLINE)
     39   sys.stdout.write(s)
     40   sys.stdout.write(ENDCOLOR)
     41   sys.stdout.write("\n")
     42 
     43 
     44 def PrintCommand(cmd, env=None):
     45   """Prints a bold line of a shell command that is being run"""
     46   if not QUIET:
     47     sys.stdout.write(BOLD)
     48     if env:
     49       for k,v in env.iteritems():
     50         if " " in v and "\"" not in v:
     51           sys.stdout.write("%s=\"%s\" " % (k, v.replace("\"", "\\\"")))
     52         else:
     53           sys.stdout.write("%s=%s " % (k, v))
     54     sys.stdout.write(" ".join(cmd))
     55     sys.stdout.write(ENDCOLOR)
     56     sys.stdout.write("\n")
     57 
     58 
     59 class ExecutionException(Exception):
     60   """Thrown to cleanly abort operation."""
     61   def __init__(self,*args,**kwargs):
     62     Exception.__init__(self,*args,**kwargs)
     63 
     64 
     65 class Adb(object):
     66   """Encapsulates adb functionality."""
     67 
     68   def __init__(self):
     69     """Initialize adb."""
     70     self._command = ["adb"]
     71 
     72 
     73   def Exec(self, cmd, stdout=None, stderr=None):
     74     """Runs an adb command, and prints that command to stdout.
     75 
     76       Raises:
     77         ExecutionException: if the adb command returned an error.
     78 
     79       Example:
     80         adb.Exec("shell", "ls") will run "adb shell ls"
     81     """
     82     cmd = self._command + cmd
     83     PrintCommand(cmd)
     84     result = subprocess.call(cmd, stdout=stdout, stderr=stderr)
     85     if result:
     86       raise ExecutionException("adb: %s returned %s" % (cmd, result))
     87 
     88 
     89   def WaitForDevice(self):
     90     """Waits for the android device to be available on usb with adbd running."""
     91     self.Exec(["wait-for-device"])
     92 
     93 
     94   def Run(self, cmd, stdout=None, stderr=None):
     95     """Waits for the device, and then runs a command.
     96 
     97       Raises:
     98         ExecutionException: if the adb command returned an error.
     99 
    100       Example:
    101         adb.Run("shell", "ls") will run "adb shell ls"
    102     """
    103     self.WaitForDevice()
    104     self.Exec(cmd, stdout=stdout, stderr=stderr)
    105 
    106 
    107   def Get(self, cmd):
    108     """Waits for the device, and then runs a command, returning the output.
    109 
    110       Raises:
    111         ExecutionException: if the adb command returned an error.
    112 
    113       Example:
    114         adb.Get(["shell", "ls"]) will run "adb shell ls"
    115     """
    116     self.WaitForDevice()
    117     cmd = self._command + cmd
    118     PrintCommand(cmd)
    119     try:
    120       text = subprocess.check_output(cmd)
    121       return text.strip()
    122     except subprocess.CalledProcessError as ex:
    123       raise ExecutionException("adb: %s returned %s" % (cmd, ex.returncode))
    124 
    125 
    126   def Shell(self, cmd, stdout=None, stderr=None):
    127     """Runs an adb shell command
    128       Args:
    129         cmd: The command to run.
    130 
    131       Raises:
    132         ExecutionException: if the adb command returned an error.
    133 
    134       Example:
    135         adb.Shell(["ls"]) will run "adb shell ls"
    136     """
    137     cmd = ["shell"] + cmd
    138     self.Run(cmd, stdout=stdout, stderr=stderr)
    139 
    140 
    141   def GetProp(self, name):
    142     """Gets a system property from the device."""
    143     return self.Get(["shell", "getprop", name])
    144 
    145 
    146   def Reboot(self):
    147     """Reboots the device, and waits for boot to complete."""
    148     # Reboot
    149     self.Run(["reboot"])
    150     # Wait until it comes back on adb
    151     self.WaitForDevice()
    152     # Poll until the system says it's booted
    153     while self.GetProp("sys.boot_completed") != "1":
    154       time.sleep(2)
    155     # Dismiss the keyguard
    156     self.Shell(["wm", "dismiss-keyguard"]);
    157 
    158   def GetBatteryProperties(self):
    159     """A dict of the properties from adb shell dumpsys battery"""
    160     def ConvertVal(s):
    161       if s == "true":
    162         return True
    163       elif s == "false":
    164         return False
    165       else:
    166         try:
    167           return int(s)
    168         except ValueError:
    169           return s
    170     text = self.Get(["shell", "dumpsys", "battery"])
    171     lines = [line.strip() for line in text.split("\n")][1:]
    172     lines = [[s.strip() for s in line.split(":", 1)] for line in lines]
    173     lines = [(k,ConvertVal(v)) for k,v in lines]
    174     return dict(lines)
    175 
    176   def GetBatteryLevel(self):
    177     """Returns the battery level"""
    178     return self.GetBatteryProperties()["level"]
    179 
    180 
    181 
    182 def CurrentTimestamp():
    183   """Returns the current time in a format suitable for filenames."""
    184   return datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
    185 
    186 
    187 def ParseOptions():
    188   """Parse the command line options.
    189 
    190     Returns an argparse options object.
    191   """
    192   parser = argparse.ArgumentParser(description="Run monkeys and collect the results.")
    193   parser.add_argument("--dir", action="store",
    194                       help="output directory for results of monkey runs")
    195   parser.add_argument("--events", action="store", type=int, default=125000,
    196                       help="number of events per monkey run")
    197   parser.add_argument("-p", action="append", dest="packages",
    198                       help="package to use (default is a set of system-wide packages")
    199   parser.add_argument("--runs", action="store", type=int, default=10000000,
    200                       help="number of monkey runs to perform")
    201   parser.add_argument("--type", choices=["crash", "anr"],
    202                       help="only stop on errors of the given type (crash or anr)")
    203   parser.add_argument("--description", action="store",
    204                       help="only stop if the error description contains DESCRIPTION")
    205 
    206   options = parser.parse_args()
    207   
    208   if not options.dir:
    209     options.dir = "monkeys-%s" % CurrentTimestamp()
    210 
    211   if not options.packages:
    212     options.packages = [
    213         "com.google.android.deskclock",
    214         "com.android.calculator2",
    215         "com.google.android.contacts",
    216         "com.android.launcher",
    217         "com.google.android.launcher",
    218         "com.android.mms",
    219         "com.google.android.apps.messaging",
    220         "com.android.phone",
    221         "com.google.android.dialer",
    222         "com.android.providers.downloads.ui",
    223         "com.android.settings",
    224         "com.google.android.calendar",
    225         "com.google.android.GoogleCamera",
    226         "com.google.android.apps.photos",
    227         "com.google.android.gms",
    228         "com.google.android.setupwizard",
    229         "com.google.android.googlequicksearchbox",
    230         "com.google.android.packageinstaller",
    231         "com.google.android.apps.nexuslauncher"
    232       ]
    233 
    234   return options
    235 
    236 
    237 adb = Adb()
    238 
    239 def main():
    240   """Main entry point."""
    241 
    242   def LogcatThreadFunc():
    243     logcatProcess.communicate()
    244 
    245   options = ParseOptions()
    246 
    247   # Set up the device a little bit
    248   PrintStatus("Setting up the device")
    249   adb.Run(["root"])
    250   time.sleep(2)
    251   adb.WaitForDevice()
    252   adb.Run(["remount"])
    253   time.sleep(2)
    254   adb.WaitForDevice()
    255   adb.Shell(["echo ro.audio.silent=1 > /data/local.prop"])
    256   adb.Shell(["chmod 644 /data/local.prop"])
    257 
    258   # Figure out how many leading zeroes we need.
    259   pattern = "%%0%dd" % len(str(options.runs-1))
    260 
    261   # Make the output directory
    262   if os.path.exists(options.dir) and not os.path.isdir(options.dir):
    263     sys.stderr.write("Output directory already exists and is not a directory: %s\n"
    264         % options.dir)
    265     sys.exit(1)
    266   elif not os.path.exists(options.dir):
    267     os.makedirs(options.dir)
    268 
    269   # Run the tests
    270   for run in range(1, options.runs+1):
    271     PrintStatus("Run %d of %d: %s" % (run, options.runs,
    272         datetime.datetime.now().strftime("%A, %B %d %Y %I:%M %p")))
    273 
    274     # Reboot and wait for 30 seconds to let the system quiet down so the
    275     # log isn't polluted with all the boot completed crap.
    276     if True:
    277       adb.Reboot()
    278       PrintCommand(["sleep", "30"])
    279       time.sleep(30)
    280 
    281     # Monkeys can outrun the battery, so if it's getting low, pause to
    282     # let it charge.
    283     if True:
    284       targetBatteryLevel = 20
    285       while True:
    286         level = adb.GetBatteryLevel()
    287         if level > targetBatteryLevel:
    288           break
    289         print "Battery level is %d%%.  Pausing to let it charge above %d%%." % (
    290             level, targetBatteryLevel)
    291         time.sleep(60)
    292 
    293     filebase = os.path.sep.join((options.dir, pattern % run))
    294     bugreportFilename = filebase + "-bugreport.txt"
    295     monkeyFilename = filebase + "-monkey.txt"
    296     logcatFilename = filebase + "-logcat.txt"
    297     htmlFilename = filebase + ".html"
    298 
    299     monkeyFile = file(monkeyFilename, "w")
    300     logcatFile = file(logcatFilename, "w")
    301     bugreportFile = None
    302 
    303     # Clear the log, then start logcat
    304     adb.Shell(["logcat", "-c", "-b", "main,system,events,crash"])
    305     cmd = ["adb", "logcat", "-b", "main,system,events,crash"]
    306     PrintCommand(cmd)
    307     logcatProcess = subprocess.Popen(cmd, stdout=logcatFile, stderr=None)
    308     logcatThread = threading.Thread(target=LogcatThreadFunc)
    309     logcatThread.start()
    310 
    311     # Run monkeys
    312     cmd = [
    313         "monkey",
    314         "-c", "android.intent.category.LAUNCHER",
    315         "--ignore-security-exceptions",
    316         "--monitor-native-crashes",
    317         "-v", "-v", "-v"
    318       ]
    319     for pkg in options.packages:
    320       cmd.append("-p")
    321       cmd.append(pkg)
    322     if options.type == "anr":
    323       cmd.append("--ignore-crashes")
    324       cmd.append("--ignore-native-crashes")
    325     if options.type == "crash":
    326       cmd.append("--ignore-timeouts")
    327     if options.description:
    328       cmd.append("--match-description")
    329       cmd.append("'" + options.description + "'")
    330     cmd.append(str(options.events))
    331     try:
    332       adb.Shell(cmd, stdout=monkeyFile, stderr=monkeyFile)
    333       needReport = False
    334     except ExecutionException:
    335       # Monkeys failed, take a bugreport
    336       bugreportFile = file(bugreportFilename, "w")
    337       adb.Shell(["bugreport"], stdout=bugreportFile, stderr=None)
    338       needReport = True
    339     finally:
    340       monkeyFile.close()
    341       try:
    342         logcatProcess.terminate()
    343       except OSError:
    344         pass # it must have died on its own
    345       logcatThread.join()
    346       logcatFile.close()
    347       if bugreportFile:
    348         bugreportFile.close()
    349     
    350     if needReport:
    351       # Generate the html
    352       cmd = ["bugreport", "--monkey", monkeyFilename, "--html", htmlFilename,
    353           "--logcat", logcatFilename, bugreportFilename]
    354       PrintCommand(cmd)
    355       result = subprocess.call(cmd)
    356 
    357 
    358 
    359 if __name__ == "__main__":
    360   main()
    361 
    362 # vim: set ts=2 sw=2 sts=2 expandtab nocindent autoindent:
    363