You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
362 lines
10 KiB
362 lines
10 KiB
#!/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:
|
|
|