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.
363 lines
10 KiB
363 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:
|