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