Joe Onorato | 186e55c | 2016-07-14 09:44:14 -0700 | [diff] [blame] | 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: |