blob: 5ccd9ffad453db2cbca55c23aceff6782e6a44bf [file] [log] [blame]
Joe Onorato186e55c2016-07-14 09:44:14 -07001#!/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: