1#!/usr/bin/env python2.7
2
3import argparse
4import datetime
5import os
6import re
7import subprocess
8import sys
9import threading
10import time
11
12QUIET = False
13
14# ANSI escape sequences
15if 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
24else:
25  BOLD = ""
26  RED = ""
27  GREEN = ""
28  YELLOW = ""
29  UNDERLINE = ""
30  ENDCOLOR = ""
31  CLEARLINE = ""
32  STDOUT_IS_TTY = False
33
34def 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
44def 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
59class ExecutionException(Exception):
60  """Thrown to cleanly abort operation."""
61  def __init__(self,*args,**kwargs):
62    Exception.__init__(self,*args,**kwargs)
63
64
65class 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
182def 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
187def 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
237adb = Adb()
238
239def 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
359if __name__ == "__main__":
360  main()
361
362# vim: set ts=2 sw=2 sts=2 expandtab nocindent autoindent:
363