blob: 2ff487a86e6e51707632c262a1b48260e45ea5d3 [file] [log] [blame]
jschung25850f02020-06-17 14:38:11 +09001# Copyright 2020 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Stress test utility for repeating actions repeatedly on android devices.
16
17Configures multiple devices to simultaneously run through the same set of
18actions over and over, while keeping logs from various sources. Primarily
19designed for playing audio to the devices and scanning their log output for
20events, while running other adb commands in between.
21"""
22from __future__ import absolute_import
23from __future__ import division
24from __future__ import print_function
25
26import datetime
27from email import encoders
28from email.mime import text
29import email.mime.base as base
30import email.mime.multipart as multipart
31import logging
32import mimetypes
33import os
34import platform
35import re
36import shlex
37import signal
38import smtplib
39import socket
40import subprocess
41import sys
42import tempfile
43import threading
44import time
45import uuid
46import wave
47from absl import app
48from absl import flags
49import pexpect
50import queue
51import stress_test_common
52import stress_test_pb2
53from google.protobuf import text_format
54
55_SUMMARY_LINES = "-" * 73
56
57if sys.platform.startswith("win"):
58 pexpect = None
59
60_SUMMARY_COLUMNS = (
61 "| Event Type | Event Count | Consecutive no event |")
62_SUMMARY_COL_FORMATT = "|%-25.25s|% 22d|% 22d|"
63
64FLAGS = flags.FLAGS
65flags.DEFINE_string("notification_address", "",
66 "Email address where to send notification events. Will "
67 "default to $USER@google.com if not provided. No emails "
68 "will be sent if suppress_notification_emails is True.")
69flags.DEFINE_bool("suppress_notification_emails", False,
70 "Prevents emails from being sent as notifications if True.")
71flags.DEFINE_string("test_name", None,
72 "Name of stress test to run. For example, if you set this "
73 "to 'dsp_trigger_sw_rejection', the stress test in "
74 "'stress_test.dsp_trigger_sw_rejection.ascii_proto' will "
75 "be loaded and executed.")
76# flags.mark_flag_as_required("test_name")
77flags.DEFINE_string("output_root", "./",
78 "Path where directory should be generated containing all "
79 "logs from devices and moved files.")
80flags.DEFINE_integer("num_iterations", None,
81 "If set to a positive number, the number of iterations of "
82 "the stress test to run. Otherwise, the test runs "
83 "forever.")
84flags.DEFINE_list("devices", [],
85 "Serial numbers of devices that should be included in the "
86 "stress test. If empty, all devices will be used.")
87flags.DEFINE_integer("print_summary_every_n", 10,
88 "Prints the summary to the log file every n iterations.")
89
90flags.DEFINE_string("email_sender_address", "",
91 "Account to use for sending notification emails.")
92flags.DEFINE_string("email_sender_password", "",
93 "Password to use for notification email account.")
94flags.DEFINE_string("email_smtp_server", "smtp.gmail.com",
95 "SMTP server to use for sending notification emails.")
96flags.DEFINE_integer("email_smtp_port", 465,
97 "Port to use for the notification SMTP server.")
98flags.DEFINE_integer("device_settle_time", 5,
99 "Time to wait for devices to settle.")
100flags.DEFINE_bool("use_sox", platform.system() != "Windows",
101 "Use sox for playback, otherwise, attempt to use platform "
102 "specific features.")
103flags.DEFINE_bool("attach_bugreport", True,
104 "Attach bugreport to email if test failed.")
105flags.DEFINE_bool("delete_data_dir", False,
106 "If true, code will delete all the files generated by this "
107 "test at the end.")
108
109if platform.system().startswith("CYGWIN"):
110 FLAGS.device_settle_time = 30
111
112
113def QueueWorker(worker_queue):
114 while True:
115 work = worker_queue.get()
116 try:
117 work()
118 except: # pylint:disable=bare-except
119 logging.exception("Exception in worker queue - task remains uncompleted.")
120 worker_queue.task_done()
121
122
123def SendNotificationEmail(subject, body, bugreport=None):
124 """Sends an email with the specified subject and body.
125
126 Also attach bugreport if bugreport location is provided as argument
127
128 Args:
129 subject: Subject of the email.
130 body: Body of the email.
131 bugreport: If provided, it will be attach to the email.
132 """
133 if FLAGS.suppress_notification_emails:
134 logging.info("Email with subject '%s' has been suppressed", subject)
135 return
136 try:
137 # Assemble the message to send.
138 recpient_address = FLAGS.notification_address
139 message = multipart.MIMEMultipart("alternative")
140 message["From"] = "Stress Test on %s" % socket.gethostname()
141 message["To"] = recpient_address
142 message["Subject"] = subject
143 message.attach(text.MIMEText(body, "plain"))
144 message.attach(text.MIMEText("<pre>%s</pre>" % body, "html"))
145
146 if FLAGS.attach_bugreport and bugreport:
147 # buildozer: disable=unused-variable
148 ctype, _ = mimetypes.guess_type(bugreport)
149 maintype, subtype = ctype.split("/", 1)
150 with open(bugreport, "rb") as fp:
151 att = base.MIMEBase(maintype, subtype)
152 att.set_payload(fp.read())
153 encoders.encode_base64(att)
154 att.add_header("Content-Disposition", "attachment", filename=bugreport)
155 message.attach(att)
156
157 # Send the message from our special account.
158 server = smtplib.SMTP_SSL(FLAGS.email_smtp_server, FLAGS.email_smtp_port)
159 server.login(FLAGS.email_sender_address, FLAGS.email_sender_password)
160 server.sendmail(FLAGS.email_sender_address, recpient_address,
161 message.as_string())
162 server.quit()
163 logging.info("Email with subject '%s' has been sent", subject)
164 except: # pylint:disable=bare-except
165 logging.exception("Failed to send notification email")
166
167
168class ProcessLogger(threading.Thread):
169
170 class EventScanner(object):
171
172 def __init__(self, name, process_name, regexes):
173 """Struct to store the data about an event.
174
175 Args:
176 name: Name of event.
177 process_name: Name of the process that is being logged.
178 regexes: An iteratable of regex strings that indicate an event has
179 happened.
180 """
181
182 self.name = name
183 self.process_name = process_name
184 self.searches = [re.compile(regex).search for regex in regexes]
185 self.count = 0
186
187 def ScanForEvent(self, line, lock=None):
188 """Checks the line for matches. If found, updates the internal counter."""
189
190 for search in self.searches:
191 if search(line.decode("utf-8")):
192 # Grab the lock (if provided), update the counter, and release it.
193 if lock: lock.acquire()
194 self.count += 1
195 if lock: lock.release()
196 logging.info("Event '%s' detected on %s", self.name,
197 self.process_name)
198
199 def __init__(self, name, command, output, events,
200 restart_process, repeats_output_when_opened):
201 """Threaded class that monitors processes for events, and logs output.
202
203 Args:
204 name: The name of the process being logged.
205 command: A list of arguments to be passed to the subprocess to execute.
206 output: Name of output file to write process stdout to. If blank or None,
207 will not be generated.
208 events: An iterable of LoggingEventConfigs to look for in the output.
209 restart_process: Restart the process if it terminates by itself. This
210 should typically be true, but false for processes that only should be
211 run once and have their output logged.
212 repeats_output_when_opened: Set to true if the process will repeat the
213 output of a previous call when it is restarted. This will prevent
214 duplicate lines from being logged.
215 """
216 super(ProcessLogger, self).__init__()
217 self.name = name
218 self.command = command
219 self.restart_process = restart_process
220 self.repeats_output_when_opened = repeats_output_when_opened
221 self.process = None
222 self.lock = threading.Lock()
223 self.looking = False
224
225 # Compile the list of regexes that we're supposed to be looking for.
226 self.events = []
227 for event in events:
228 self.events.append(ProcessLogger.EventScanner(event.name, self.name,
229 event.regex))
230
231 if output:
232 stress_test_common.MakeDirsIfNeeded(os.path.dirname(output))
jschung2d7ae102020-09-15 18:20:54 +0900233 self.output_fp = open(output, "w", encoding="utf-8")
jschung25850f02020-06-17 14:38:11 +0900234 logging.info("Logging device info to %s", output)
235 else:
236 self.output_fp = None
237
238 def GetEventCountsSinceLastCall(self):
239 """Returns the counts of all events since this method was last called."""
240 event_map = {}
241 self.lock.acquire()
242 for event in self.events:
243 event_map[event.name] = event.count
244 event.count = 0
245 self.lock.release()
246 return event_map
247
248 def run(self):
249 last_line = None
250 should_log = True
251 first_run = True
jschung551597f2020-09-25 10:03:27 +0900252 skip_exception_line = False
jschung25850f02020-06-17 14:38:11 +0900253 self.lock.acquire()
254 last_run_time = 0
255 while self.restart_process:
256 self.lock.release()
257 if not first_run:
258 logging.info("Restarting process %s", "".join(str(self.command)))
259 time_since_last_run = datetime.datetime.now() - last_run_time
260 if time_since_last_run.total_seconds() < 1.0:
261 needed_delay = 1.0 - time_since_last_run.total_seconds()
262 logging.info("Delaying for %.2f seconds", needed_delay)
263 time.sleep(needed_delay)
264 else:
265 first_run = False
266
267 try:
268 if pexpect:
269 self.process = pexpect.spawn(" ".join(self.command), timeout=None)
270 output_source = self.process
271 else:
272 self.process = subprocess.Popen(self.command, stdout=subprocess.PIPE)
273 output_source = self.process.stdout
274 last_run_time = datetime.datetime.now()
275 for line in output_source:
276 # If the process we're logging likes to repeat its output, we need to
277 # look for the last line we saw before we start doing anything with
278 # these lines anymore.
279 if self.repeats_output_when_opened:
280 if not should_log:
281 if last_line == line:
282 should_log = True
283 continue
jschung551597f2020-09-25 10:03:27 +0900284 elif skip_exception_line:
285 # ignore the last line which caused UnicodeEncodeError
286 skip_exception_line = False
287 continue
jschung25850f02020-06-17 14:38:11 +0900288
289 if self.output_fp:
jschunge3f93fa2020-09-15 00:10:56 +0900290 self.output_fp.write(line.decode("utf-8", "backslashreplace").rstrip())
jschung25850f02020-06-17 14:38:11 +0900291 self.output_fp.write("\n")
292
293 # Loop through all events we're watching for, to see if they occur on
294 # this line. If they do, update the fact that we've seen this event.
295 for event in self.events:
296 if self.looking:
297 event.ScanForEvent(line, lock=self.lock)
298 last_line = line
jschung551597f2020-09-25 10:03:27 +0900299 except UnicodeEncodeError:
300 logging.exception("UnicodeEncodeError on running logger process")
301 skip_exception_line = True
jschung25850f02020-06-17 14:38:11 +0900302 except: # pylint:disable=bare-except
303 logging.exception("Exception encountered running process")
304 finally:
305 if pexpect:
306 self.process.terminate()
307 else:
308 self.process.send_signal(signal.SIGTERM)
309 should_log = False
310 self.lock.acquire()
jschung551597f2020-09-25 10:03:27 +0900311
jschung25850f02020-06-17 14:38:11 +0900312 self.lock.release()
313 if pexpect:
314 if self.process.exitstatus is not None:
315 logging.info("Process finished - exit code %d", self.process.exitstatus)
316 else:
317 logging.info("Process finished - signal code %d",
318 self.process.signalstatus)
319 else:
320 if self.process.returncode is not None:
321 logging.info("Process finished - return code %d",
322 self.process.returncode)
323 else:
324 logging.info("Process finished - no return code")
325
326 def StopLogging(self):
327 if self.process:
328 self.lock.acquire()
329 self.restart_process = False
330 self.lock.release()
331
332 if pexpect:
333 self.process.kill(signal.SIGHUP)
334 self.process.kill(signal.SIGINT)
335 else:
336 self.process.send_signal(signal.SIGTERM)
337
338
339class Device(object):
340
341 SECONDS_TO_SLEEP_DURING_ROOT = 0.5
342
343 def __init__(self, serial_number, output_root, test_events, expected_result):
344 """Responsible for monitoring a specific device, and pulling files from it.
345
346 The actual work of the constructor will be handled asynchronously, you must
347 call WaitForTasks() before using the device.
348
349 Args:
350 serial_number: The device serial number.
351 output_root: The directory where to output log files/anything pulled from
352 the device.
353 test_events: The events (with conditions) that come from the StressTest
354 that should be evaluated at every iteration, along with a list of
355 actions to take when one of these events occur. For example, if there
356 have not been any detected hotword triggers, a bugreport can be
357 generated.
358 expected_result: Expected event count to pass the test.
359 """
360 self.serial_number = serial_number
361 self.output_root = output_root
362 self.cmd_string_replacements = {}
363 self.iteration = 0
364 self.cmd_string_replacements["iteration"] = 0
365 self.cmd_string_replacements["serial_number"] = serial_number
366 self.cmd_string_replacements["output_root"] = output_root
367 self.name = None
368 self.process_loggers = []
369 self.event_log = stress_test_pb2.EventLog()
370 self.cnt_per_iteration = expected_result
371
372 # Prepare the work queue, and offload the rest of the init into it.
373 self.work_queue = queue.Queue()
374 self.worker = threading.Thread(target=QueueWorker, args=[self.work_queue])
375 self.worker.daemon = True
376 self.worker.name = self.name
377 self.worker.start()
378 self.abort_requested = False
379 self.remove_device = False
380 self.test_events = test_events
381
382 self.work_queue.put(self.__init_async__)
383
384 def __init_async__(self):
385 # Get the device type, and append it to the serial number.
386 self.device_type = self.Command(["shell", "getprop",
387 "ro.product.name"]).strip().decode("utf-8")
388 self.name = "%s_%s" % (self.device_type, self.serial_number)
389 self.worker.name = self.name
390 self.cmd_string_replacements["device"] = self.name
391 logging.info("Setting up device %s", self.name)
392
393 config = stress_test_common.LoadDeviceConfig(self.device_type,
394 self.serial_number)
395
396 # Get the device ready.
397 self.Root()
398
399 # Run any setup commands.
400 for cmd in config.setup_command:
401 result = self.Command(
402 shlex.split(cmd % self.cmd_string_replacements)).strip()
403 if result:
404 for line in result.splitlines():
405 logging.info(line)
406
407 self.files_to_move = config.file_to_move
408
409 self.event_names = set([event.name for event in config.event])
410 self.event_counter = {name: 0 for name in self.event_names}
411 self.iterations_since_event = {name: 0 for name in self.event_names}
412
413 for file_to_watch in config.file_to_watch:
414 # Are there any events that match up with this file?
415 events = [x for x in config.event if x.source == file_to_watch.source]
416
417 if file_to_watch.source == "LOGCAT":
418 command = [
419 "adb", "-s", self.serial_number, "logcat", "-v", "usec", ""
420 ]
421 command.extend(["%s:S" % tag for tag in config.tag_to_suppress])
422 name = "logcat_" + self.serial_number
423 else:
424 command = [
425 "adb", "-s", self.serial_number, "shell",
426 "while : ; do cat %s 2>&1; done" % file_to_watch.source
427 ]
428 name = "%s_%s" % (os.path.basename(
429 file_to_watch.source), self.serial_number)
430
431 process_logger = ProcessLogger(
432 name, command, os.path.join(
433 self.output_root,
434 file_to_watch.destination % self.cmd_string_replacements),
435 events, True, file_to_watch.repeats_output_on_open)
436 self.process_loggers.append(process_logger)
437 process_logger.start()
438
439 # Add any of the background processes.
440 for daemon_process in config.daemon_process:
441 # Are there any events that match up with this file?
442 events = [x for x in config.event if x.source == daemon_process.name]
443 command = shlex.split(
444 daemon_process.command % self.cmd_string_replacements)
445 if daemon_process.destination:
446 output = os.path.join(
447 self.output_root,
448 daemon_process.destination % self.cmd_string_replacements)
449 else:
450 output = None
451 name = "%s_%s" % (daemon_process.name, self.serial_number)
452 process_logger = ProcessLogger(name, command, output, events,
453 daemon_process.restart,
454 daemon_process.repeats_output_on_open)
455 self.process_loggers.append(process_logger)
456 process_logger.start()
457
458 # Build up the list of events we can actually process.
459 self.__UpdateEventCounters(number_of_iterations=0)
460 test_events = self.test_events
461 self.test_events = []
462 for event in test_events:
463 try:
464 eval(event.condition, # pylint:disable=eval-used
465 {"__builtins__": None}, self.__ValuesInEval())
466 self.test_events.append(event)
467 except Exception as err: # pylint:disable=broad-except
468 logging.error("Test event %s is not compatible with %s", event.name,
469 self.name)
470 logging.error(str(err))
471 # Make sure that device specific events don't have conditions.
472 self.device_events = []
473 for event in config.test_event:
474 if not event.name:
475 logging.error("Device %s test event is missing a name", self.name)
476 continue
477 if event.condition:
478 self.test_events.append(event)
479 else:
480 self.device_events.append(event)
481
482 def StartLookingForEvents(self):
483 """Starts all child ProcessLoggers to start looking for events."""
484 for process_logger in self.process_loggers:
485 process_logger.looking = True
486
487 def __ValuesInEval(self):
488 values_in_eval = {key: value for key, value
489 in list(self.event_counter.items())}
490 for key, value in list(self.iterations_since_event.items()):
491 values_in_eval["iterations_since_%s" % key] = value
492 return values_in_eval
493
494 def __GetExpectedEventCount(self, event):
495 if event == "logcat_iteration":
496 return -1
497 try:
498 event_cnt = getattr(self.cnt_per_iteration, event)
499 except AttributeError:
500 event_cnt = -1
501 logging.exception("%s is not an attribute of expected_result", event)
502 return event_cnt
503
504 def __UpdateEventCounters(self, number_of_iterations=1):
505 # Update the event counters
506 visited_events = set()
507 error_log = []
508 for process_logger in self.process_loggers:
509 events = process_logger.GetEventCountsSinceLastCall()
510 for event, count in list(events.items()):
511 # Print log when there is any missed event
512 expected_count = self.__GetExpectedEventCount(event)
513
514 if expected_count > 0:
515 if count > expected_count * number_of_iterations:
516 logging.info(
517 "[STRESS_TEST] In iteration %d, got duplicated %s : %d",
518 self.iteration, self.name, count)
519 logging.info("[STRESS_TEST] Will count only : %d",
520 expected_count * number_of_iterations)
521 count = expected_count * number_of_iterations
522
523 if count:
524 self.event_counter[event] += count
525 visited_events.add(event)
526
527 if expected_count >= 0:
528 if expected_count * number_of_iterations != count:
529 error_log.append(
530 _SUMMARY_COL_FORMATT %
531 (event, count, expected_count * number_of_iterations))
532
533 # Go clear all the events that weren't consecutive.
534 for event in self.iterations_since_event:
535 if event in visited_events:
536 self.iterations_since_event[event] = 0
537 else:
538 self.iterations_since_event[event] += number_of_iterations
539
540 if error_log:
541 logging.info(_SUMMARY_LINES)
542 logging.info(" iteration %d : Something wrong in %s.",
543 self.iteration, self.name)
544 logging.info(_SUMMARY_LINES)
545 logging.info(_SUMMARY_COLUMNS)
546 logging.info(_SUMMARY_LINES)
547 for line in error_log:
548 logging.info(line)
549 logging.info(_SUMMARY_LINES)
550
551 def ProcessEvents(self):
552 """Updates the event_counter and iterations_since_event maps."""
553 self.work_queue.put(self.__ProcessEventsAsync)
554
555 def __ProcessEventsAsync(self):
556 # Move any files to the local machine that should be moved.
557 if self.files_to_move:
558 for file_to_move in self.files_to_move:
559 try:
560 self.Command(["pull", file_to_move.source, file_to_move.destination])
561 except: # pylint:disable=bare-except
562 logging.exception("Failed to pull %s", file_to_move.source)
563
564 self.__UpdateEventCounters()
565
566 for event in self.test_events:
567 if eval(event.condition, # pylint:disable=eval-used
568 {"__builtins__": None}, self.__ValuesInEval()):
569 logging.info("Condition has been met for event '%s'", event.name)
570 # Write the updated event log.
571 event_log_details = self.event_log.event.add()
572 event_log_details.iteration = self.iteration
573 event_log_details.name = event.name
574 with open(os.path.join(self.output_root,
575 "%s_event_log.ascii_proto" % self.name),
576 "w") as fp:
577 text_format.PrintMessage(self.event_log, fp)
578
579 # Do whatever other actions that are part of the event.
580 self.__ProcessEventActionQueue(event)
581
582 # Run any device specific actions for this event.
583 for device_event in self.device_events:
584 if device_event.name == event.name:
585 self.__ProcessEventActionQueue(device_event)
586
587 # Set up the next iteration.
588 self.iteration += 1
589 self.cmd_string_replacements["iteration"] = self.iteration
590
591 def __ProcessEventActionQueue(self, event):
592 bugreport = None
593 for action in event.action:
594 if action == "BUGREPORT":
595 bugreport = self.TakeBugReport()
596 elif action.startswith("DUMPSYS "):
597 self.CaptureDumpsys(action[action.find(" ") + 1:])
598 elif action == "NOTIFY":
599 SendNotificationEmail(
600 "%s had event '%s' occur" % (self.name, event.name),
601 "\n".join(["Current Summary:"] + self.GetSummaryLines()), bugreport)
602 elif action == "REMOVE_DEVICE":
603 logging.info("Removing %s from the test", self.serial_number)
604 self.remove_device = True
605 elif action == "ABORT":
606 logging.info("Abort requested")
607 self.abort_requested = True
608 else:
609 action %= self.cmd_string_replacements
610 logging.info("Running command %s on %s", action, self.name)
611 result = self.Command(shlex.split(action)).strip()
612 if result:
613 for line in result.splitlines():
614 logging.info(line)
615
616 def Root(self):
617 self.Command(["root"])
618 time.sleep(Device.SECONDS_TO_SLEEP_DURING_ROOT)
619 self.Command(["wait-for-device"])
620 time.sleep(Device.SECONDS_TO_SLEEP_DURING_ROOT)
621
622 def Stop(self):
623 """Stops all file loggers attached to this device."""
624 for process_logger in self.process_loggers:
625 process_logger.StopLogging()
626 self.process_loggers = []
627
628 def Join(self):
629 for process_logger in self.process_loggers:
630 process_logger.join()
631 self.WaitForTasks()
632
633 def AsyncCommand(self, command, log_output=False):
634 self.work_queue.put(
635 lambda: self.__AsyncCommand(command, log_output=log_output))
636
637 def __AsyncCommand(self, command, log_output=False):
638 result = self.Command(command).strip()
639 if result and log_output:
640 for line in result.splitlines():
641 logging.info(line.decode("utf-8"))
642
643 def Command(self, command):
644 """Runs the provided command on this device."""
645 if command[0] in {"bugreport", "root", "wait-for-device", "shell",
646 "logcat"}:
647 return subprocess.check_output(
648 ["adb", "-s", self.serial_number] + command)
649 elif command[0] == "DUMPSYS":
650 self.CaptureDumpsys(command[1])
651 return ""
652 elif command[0] == "pull":
653 try:
654 files = subprocess.check_output(
655 ["adb", "-s", self.serial_number, "shell", "ls", command[1]]
656 ).strip().splitlines()
657 except subprocess.CalledProcessError:
658 return ""
659 if len(files) == 1 and "No such file or directory" in files[0]:
660 return ""
661 for source_file in files:
662 destination = os.path.join(self.output_root,
663 command[2] % self.cmd_string_replacements)
664 stress_test_common.MakeDirsIfNeeded(os.path.dirname(destination))
665 logging.info("Moving %s from %s to %s", source_file, self.name,
666 destination)
667 subprocess.check_output(["adb", "-s", self.serial_number, "pull",
668 source_file, destination])
669 if FLAGS.delete_data_dir:
670 subprocess.check_output([
671 "adb", "-s", self.serial_number, "shell", "rm", "-rf", source_file
672 ])
673 return ""
674 else:
675 return subprocess.check_output(command)
676
677 def TakeBugReport(self):
678 logging.info("Capturing bugreport on %s", self.name)
679 bugreport = os.path.join(self.output_root,
680 "%s_bugreport_iteration_%06d.zip" %
681 (self.name, self.iteration))
682 sdk = int(self.Command(
683 ["shell", "getprop", "ro.build.version.sdk"]).strip())
684 if sdk >= 24: # SDK 24 = Android N
685 with open(bugreport, "w") as bugreport_fp:
686 bugreport_fp.write(self.Command(["bugreport", bugreport]))
687 else:
688 bugreport_txt = os.path.join(self.output_root,
689 "%s_bugreport_iteration_%06d.txt" %
690 (self.name, self.iteration))
691 with open(bugreport_txt, "w") as bugreport_fp:
692 bugreport_fp.write(self.Command(["bugreport"]))
693 self.Command(["zip", bugreport, bugreport_txt])
694
695 self.Command(["pull", "/data/anr/traces.txt",
696 "%s_traces_iteration_%06d.txt" % (self.name, self.iteration)])
697 self.Command(["pull", "/data/anr/traces.txt.bugreport",
698 "%s_traces_iteration_%06d.txt.bugreport" % (self.name,
699 self.iteration)])
700 return bugreport
701
702 def CaptureDumpsys(self, dumpsys_unit):
703 logging.info("Taking dumpsys %s on %s", dumpsys_unit, self.name)
704 stress_test_common.MakeDirsIfNeeded(os.path.join(self.output_root,
705 self.name))
706 with open(os.path.join(self.output_root, self.name,
707 "%s_%06d.txt" % (dumpsys_unit, self.iteration)),
708 "w") as dumpsys_fp:
709 dumpsys_fp.write(self.Command(["shell", "dumpsys", dumpsys_unit]))
710
711 def WaitForTasks(self):
712 self.work_queue.join()
713
714 def GetSummaryLines(self):
715 lines = [
716 "Device {}".format(self.name),
717 _SUMMARY_LINES, _SUMMARY_COLUMNS, _SUMMARY_LINES
718 ]
719 for event, count in sorted(self.event_counter.items()):
720 lines.append(_SUMMARY_COL_FORMATT % (
721 event, count, self.iterations_since_event[event]))
722 lines.append(_SUMMARY_LINES)
723 return lines
724
725
726def RunAsyncCommand(devices, command):
727 """Helper function for running async commands on many devices."""
728 for device in devices:
729 device.AsyncCommand(command)
730 for device in devices:
731 device.WaitForTasks()
732
733
734class StressTest(object):
735 """Manages dispatching commands to devices/playing audio and events."""
736
737 def __init__(self, output_root, test_name):
738 self.output_root = output_root
739 self.devices = []
740 self.test_name = test_name
741 config = stress_test_pb2.StressTestConfig()
742 config_contents = stress_test_common.GetResourceContents(
743 os.path.join(stress_test_common.RESOURCE_DIR,
744 "stress_test.%s.ascii_proto" % test_name))
745 text_format.Merge(config_contents, config)
746 self.events = config.event
747 self.setup_commands = config.setup_command
748 self.steps = config.step
749 self.audio_tempfiles = {}
750 self.uuid = str(uuid.uuid4())
751 self.expected_result = None
752 self.iteration = 0
753 if config.expected_result:
754 self.expected_result = config.expected_result[0]
755
756 # Place all the audio files into temp files.
757 for step in self.steps:
758 if step.audio_file and step.audio_file not in self.audio_tempfiles:
759 # We can't delete the temp file on windows, since it gets nuked too
760 # early.
761 audio_tempfile = tempfile.NamedTemporaryFile(
762 delete=(platform.system() != "Windows"),
763 dir="." if platform.system().startswith("CYGWIN") else None
764 )
765 if platform.system().startswith("CYGWIN"):
766 audio_tempfile.name = os.path.basename(audio_tempfile.name)
767 self.audio_tempfiles[step.audio_file] = audio_tempfile
768 if FLAGS.use_sox:
769 # Write out the raw PCM samples as a wave file.
770 audio_tempfile.write(
771 stress_test_common.GetResourceContents(step.audio_file))
772 else:
773 # Make a temporary wave file for playout if we can't use sox.
774 wavefile = wave.open(audio_tempfile, "wb")
775 if step.audio_file_sample_rate <= 0:
776 step.audio_file_sample_rate = 16000
777 wavefile.setframerate(step.audio_file_sample_rate)
778 if step.audio_file_num_channels <= 0:
779 step.audio_file_num_channels = 1
780 wavefile.setnchannels(step.audio_file_num_channels)
781 if not step.audio_file_format:
782 wavefile.setsampwidth(2)
783 elif step.audio_file_format == "s8":
784 wavefile.setsampwidth(1)
785 elif step.audio_file_format == "s16":
786 wavefile.setsampwidth(2)
787 elif step.audio_file_format == "s32":
788 wavefile.setsampwidth(4)
789 else:
790 raise RuntimeError(
791 "Unsupported wave file format for %s" % step.audio_file)
792 wavefile.writeframes(stress_test_common.GetResourceContents(
793 step.audio_file))
794 wavefile.close()
795 audio_tempfile.flush()
796
797 if platform.system() == "Windows":
798 audio_tempfile.close()
799
800 # Create all the devices that are attached to this machine.
801 for serial_number in self.GetActiveSerialNumbers():
802 self.devices.append(
803 Device(serial_number, output_root, self.events, self.expected_result))
804 if not self.devices:
805 raise app.UsageError("No devices connected")
806
807 self.devices.sort(key=lambda x: x.name)
808
809 # Make sure every device is done with their work for setup.
810 for device in self.devices:
811 device.WaitForTasks()
812
813 # Write out the info meta-data proto. Useful for doing analysis of the logs
814 # after the stress test has completed.
815 stress_test_info = stress_test_pb2.StressTestInfo()
816 stress_test_info.test_name = self.test_name
817 stress_test_info.test_description = config.description
818 stress_test_info.uuid = self.uuid
819 for device in self.devices:
820 device_pb = stress_test_info.device.add()
821 device_pb.device_type = device.device_type
822 device_pb.serial_number = device.serial_number
823
824 text_format.PrintMessage(stress_test_info, open(os.path.join(
825 self.output_root, "stress_test_info.ascii_proto"), "w"))
826
827 def GetActiveSerialNumbers(self):
828 serial_numbers = []
829 for line in sorted(
830 subprocess.check_output(["adb", "devices"]).splitlines()):
831 if line.endswith(b"device"):
832 serial_number = line.split()[0].strip()
833 if FLAGS.devices and serial_number not in FLAGS.devices:
834 continue
835 serial_numbers.append(serial_number.decode("utf-8"))
836 return serial_numbers
837
838 def Start(self):
839 logging.info("Waiting for devices to settle")
840 time.sleep(5)
841 # Make a copy of the device list, as we'll be modifying this actual list.
842 devices = list(self.devices)
843 dropped_devices = []
844
845 # If we have any setup commands, run them.
846 for command in self.setup_commands:
847 logging.info("Running command %s", command)
848 # Can't use the async command helper function since we need to get at
849 # the device cmd_string_replacements.
850 for device in devices:
851 device.AsyncCommand(
852 shlex.split(command % device.cmd_string_replacements),
853 log_output=True)
854 for device in devices:
855 device.WaitForTasks()
856
857 for device in devices:
858 device.StartLookingForEvents()
859 device.AsyncCommand(["shell", "log", "-t", "STRESS_TEST",
860 "Starting {%s} TZ=$(getprop persist.sys.timezone) "
861 "YEAR=$(date +%%Y)" % self.uuid], True)
862 self.iteration = 0
863 while True:
864 logging.info("Starting iteration %d", self.iteration)
865 # Perform all the actions specified in the test.
866 RunAsyncCommand(devices, [
867 "shell", "log", "-t", "STRESS_TEST",
868 "Performing iteration %d $(head -n 3 "
869 "/proc/timer_list | tail -n 1)" % self.iteration
870 ])
871
872 for step in self.steps:
873 if step.delay_before:
874 logging.info("Waiting for %.2f seconds", step.delay_before)
875 time.sleep(step.delay_before)
876
877 if step.audio_file:
878 logging.info("Playing %s", step.audio_file)
879 RunAsyncCommand(devices, ["shell", "log", "-t", "STRESS_TEST",
880 "Playing %s" % step.audio_file])
881
882 if FLAGS.use_sox:
883 subprocess.check_call(["sox", "-q",
884 self.audio_tempfiles[step.audio_file].name,
885 "-d"])
886 elif platform.system() == "Windows":
887 import winsound # pylint:disable=g-import-not-at-top
888 winsound.PlaySound(self.audio_tempfiles[step.audio_file].name,
889 winsound.SND_FILENAME | winsound.SND_NODEFAULT)
890 else:
891 raise app.RuntimeError("Unsupported platform for audio playback")
892
893 if step.command:
894 logging.info("Running command %s", step.command)
895 # Can't use the async command helper function since we need to get at
896 # the device cmd_string_replacements.
897 for device in devices:
898 device.AsyncCommand(
899 shlex.split(step.command % device.cmd_string_replacements),
900 log_output=True)
901 for device in devices:
902 device.WaitForTasks()
903
904 if step.delay_after:
905 logging.info("Waiting for %.2f seconds", step.delay_after)
906 time.sleep(step.delay_after)
907
908 RunAsyncCommand(devices, [
909 "shell", "log", "-t", "STRESS_TEST",
910 "Iteration %d complete $(head -n 3 "
911 "/proc/timer_list | tail -n 1)" % self.iteration
912 ])
913 self.iteration += 1
914
915 # TODO(somebody): Sometimes the logcat seems to get stuck and buffers for
916 # a bit. This throws off the event counts, so we should probably add some
917 # synchronization rules before we trigger any events.
918
919 # Go through each device, update the event counter, and see if we need to
920 # trigger any events.
921 devices_to_remove = []
922 abort_requested = False
923 active_devices = self.GetActiveSerialNumbers()
924 for device in devices:
925 if device.serial_number in active_devices:
926 device.ProcessEvents()
927 else:
928 logging.error("Dropped device %s", device.name)
929 SendNotificationEmail(
930 "Dropped device %s" % device.name,
931 "Device %s is not longer present in the system" % device.name)
932 dropped_devices.append(device)
933 devices_to_remove.append(device)
934
935 # Check to see if any of the dropped devices have come back. If yes, grab
936 # a bug report.
937 for device in dropped_devices:
938 if device.serial_number in active_devices:
939 logging.info("Device %s reappeared", device.name)
940 device.Root()
941 device.TakeBugReport()
942
943 dropped_devices = [d for d in dropped_devices
944 if d.serial_number not in active_devices]
945
946 for device in devices:
947 device.WaitForTasks()
948 if device.remove_device:
949 devices_to_remove.append(device)
950 if device.abort_requested:
951 abort_requested = True
952
953 # Remove devices from our list of things to monitor if they've been marked
954 # for deletion.
955 if devices_to_remove:
956 for device in devices_to_remove:
957 device.Stop()
958 devices = [d for d in devices if d not in devices_to_remove]
959
960 # Print out the iteration summary.
961 if self.iteration % FLAGS.print_summary_every_n == 0:
962 for line in self.GetSummaryLines():
963 logging.info(line)
964
965 # See if we need to break out of the outer loop.
966 if abort_requested or not devices:
967 break
968 if FLAGS.num_iterations:
969 if self.iteration >= FLAGS.num_iterations:
970 logging.info("Completed full iteration : %d", self.iteration)
971 break
972 SendNotificationEmail(
973 "Stress test %s completed" % (FLAGS.test_name),
974 "\n".join(["Summary:"] + self.GetSummaryLines()))
975
976 def Stop(self):
977 logging.debug("Stopping devices")
978 for device in self.devices:
979 device.Stop()
980 for device in self.devices:
981 device.Join()
982
983 def GetSummaryLines(self):
984 lines = [
985 _SUMMARY_LINES,
986 "Conducted %d iterations out of %d" %
987 (self.iteration, FLAGS.num_iterations),
988 _SUMMARY_LINES
989 ]
990 for device in self.devices:
991 lines.extend(device.GetSummaryLines())
992 lines.append(_SUMMARY_LINES)
993 return lines
994
995
996def main(unused_argv):
997 # Check to make sure that there are no other instances of ADB running - if
998 # there are, print a warning and wait a bit for them to see it and decide if
999 # they want to keep running, knowing that logs may be invalid.
1000 try:
1001 if "adb" in subprocess.check_output(["ps", "-ale"]).decode("utf-8"):
1002 print("It looks like there are other instances of adb running. If these "
1003 "other instances are also cating log files, you will not be "
1004 "capturing everything in this stress test (so logs will be "
1005 "invalid).")
1006 print("Continuing in 3...", end=" ")
1007 sys.stdout.flush()
1008 for i in [2, 1, 0]:
1009 time.sleep(1)
1010 if i:
1011 print("%d..." % i, end=" ")
1012 else:
1013 print("")
1014 sys.stdout.flush()
1015 except OSError:
1016 print("Unexpected error:", sys.exc_info()[0])
1017 if sys.platform.startswith("win"):
1018 pass
1019 else:
1020 raise
1021
1022 # Make the base output directory.
1023 output_root = os.path.join(FLAGS.output_root, "%s_%s" % (
1024 FLAGS.test_name, datetime.datetime.now().strftime("%Y%m%d_%H%M%S")))
1025 # output_root = os.path.join(FLAGS.output_root, FLAGS.test_name)
1026 stress_test_common.MakeDirsIfNeeded(output_root)
1027
1028 # Set up logging.
1029 formatter = logging.Formatter(
1030 "%(levelname)-1.1s %(asctime)s [%(threadName)-16.16s] %(message)s")
1031 root_logger = logging.getLogger()
1032 root_logger.setLevel(logging.INFO)
1033 root_logger.setLevel(logging.DEBUG)
1034
1035 file_handler = logging.FileHandler(os.path.join(output_root,
1036 "stress_test.log"))
1037 file_handler.setFormatter(formatter)
1038 root_logger.addHandler(file_handler)
1039
1040 console_handler = logging.StreamHandler()
1041 console_handler.setFormatter(formatter)
1042 root_logger.addHandler(console_handler)
1043
1044 stress_test = StressTest(output_root, FLAGS.test_name)
1045 try:
1046 stress_test.Start()
1047 finally:
1048 logging.info("Stopping device logging threads")
1049 stress_test.Stop()
1050 for line in stress_test.GetSummaryLines():
1051 logging.info(line)
1052 if FLAGS.delete_data_dir:
1053 print("Deleting Data Dir")
1054 subprocess.check_output(["rm", "-r", "-f", output_root])
1055
1056
1057if __name__ == "__main__":
1058 app.run(main)