diff options
author | craigdh@chromium.org <craigdh@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-01-09 21:46:52 +0000 |
---|---|---|
committer | craigdh@chromium.org <craigdh@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-01-09 21:46:52 +0000 |
commit | 35072c28e2399fb96ebbbfc6ff9de07779e7adc0 (patch) | |
tree | d16a6c24284167937c792b86abdc14a9ed5c5583 /build | |
parent | 01f0cad0a9302734c4a307a7c4ec28628110bac7 (diff) | |
download | chromium_src-35072c28e2399fb96ebbbfc6ff9de07779e7adc0.zip chromium_src-35072c28e2399fb96ebbbfc6ff9de07779e7adc0.tar.gz chromium_src-35072c28e2399fb96ebbbfc6ff9de07779e7adc0.tar.bz2 |
[Android] Break GTest emulator launching into a separate function.
BUG=167331
TEST=run_tests.py -v -e -n 2
Review URL: https://codereview.chromium.org/11801016
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@175894 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'build')
-rwxr-xr-x | build/android/emulator.py | 309 | ||||
-rwxr-xr-x | build/android/pylib/utils/emulator.py | 338 | ||||
-rwxr-xr-x | build/android/run_tests.py | 19 |
3 files changed, 355 insertions, 311 deletions
diff --git a/build/android/emulator.py b/build/android/emulator.py index 0cbe4c9..1c4fb55 100755 --- a/build/android/emulator.py +++ b/build/android/emulator.py @@ -4,311 +4,28 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -"""Provides an interface to start and stop Android emulator. +"""Script to launch Android emulators. Assumes system environment ANDROID_NDK_ROOT has been set. - - Emulator: The class provides the methods to launch/shutdown the emulator with - the android virtual device named 'avd_armeabi' . """ -import logging -import os -import signal -import subprocess +import optparse import sys -import time - -from pylib import android_commands -from pylib import cmd_helper - -# adb_interface.py is under ../../third_party/android_testrunner/ -sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', - '..', 'third_party', 'android_testrunner')) -import adb_interface -import errors -import run_command - -class EmulatorLaunchException(Exception): - """Emulator failed to launch.""" - pass - -def _KillAllEmulators(): - """Kill all running emulators that look like ones we started. - - There are odd 'sticky' cases where there can be no emulator process - running but a device slot is taken. A little bot trouble and and - we're out of room forever. - """ - emulators = android_commands.GetEmulators() - if not emulators: - return - for emu_name in emulators: - cmd_helper.GetCmdOutput(['adb', '-s', emu_name, 'emu', 'kill']) - logging.info('Emulator killing is async; give a few seconds for all to die.') - for i in range(5): - if not android_commands.GetEmulators(): - return - time.sleep(1) - - -def DeleteAllTempAVDs(): - """Delete all temporary AVDs which are created for tests. - - If the test exits abnormally and some temporary AVDs created when testing may - be left in the system. Clean these AVDs. - """ - avds = android_commands.GetAVDs() - if not avds: - return - for avd_name in avds: - if 'run_tests_avd' in avd_name: - cmd = ['android', '-s', 'delete', 'avd', '--name', avd_name] - cmd_helper.GetCmdOutput(cmd) - logging.info('Delete AVD %s' % avd_name) - - -class PortPool(object): - """Pool for emulator port starting position that changes over time.""" - _port_min = 5554 - _port_max = 5585 - _port_current_index = 0 - - @classmethod - def port_range(cls): - """Return a range of valid ports for emulator use. - - The port must be an even number between 5554 and 5584. Sometimes - a killed emulator "hangs on" to a port long enough to prevent - relaunch. This is especially true on slow machines (like a bot). - Cycling through a port start position helps make us resilient.""" - ports = range(cls._port_min, cls._port_max, 2) - n = cls._port_current_index - cls._port_current_index = (n + 1) % len(ports) - return ports[n:] + ports[:n] - - -def _GetAvailablePort(): - """Returns an available TCP port for the console.""" - used_ports = [] - emulators = android_commands.GetEmulators() - for emulator in emulators: - used_ports.append(emulator.split('-')[1]) - for port in PortPool.port_range(): - if str(port) not in used_ports: - return port - - -class Emulator(object): - """Provides the methods to lanuch/shutdown the emulator. - - The emulator has the android virtual device named 'avd_armeabi'. - - The emulator could use any even TCP port between 5554 and 5584 for the - console communication, and this port will be part of the device name like - 'emulator-5554'. Assume it is always True, as the device name is the id of - emulator managed in this class. - - Attributes: - emulator: Path of Android's emulator tool. - popen: Popen object of the running emulator process. - device: Device name of this emulator. - """ - - # Signals we listen for to kill the emulator on - _SIGNALS = (signal.SIGINT, signal.SIGHUP) - - # Time to wait for an emulator launch, in seconds. This includes - # the time to launch the emulator and a wait-for-device command. - _LAUNCH_TIMEOUT = 120 - - # Timeout interval of wait-for-device command before bouncing to a a - # process life check. - _WAITFORDEVICE_TIMEOUT = 5 - - # Time to wait for a "wait for boot complete" (property set on device). - _WAITFORBOOT_TIMEOUT = 300 - - def __init__(self, new_avd_name): - """Init an Emulator. - - Args: - nwe_avd_name: If set, will create a new temporary AVD. - """ - try: - android_sdk_root = os.environ['ANDROID_SDK_ROOT'] - except KeyError: - logging.critical('The ANDROID_SDK_ROOT must be set to run the test on ' - 'emulator.') - raise - self.emulator = os.path.join(android_sdk_root, 'tools', 'emulator') - self.android = os.path.join(android_sdk_root, 'tools', 'android') - self.popen = None - self.device = None - self.default_avd = True - self.abi = 'armeabi-v7a' - self.avd = 'avd_armeabi' - if 'x86' in os.environ.get('TARGET_PRODUCT', ''): - self.abi = 'x86' - self.avd = 'avd_x86' - if new_avd_name: - self.default_avd = False - self.avd = self._CreateAVD(new_avd_name) - - def _DeviceName(self): - """Return our device name.""" - port = _GetAvailablePort() - return ('emulator-%d' % port, port) - - def _CreateAVD(self, avd_name): - """Creates an AVD with the given name. - - Return avd_name. - """ - avd_command = [ - self.android, - '--silent', - 'create', 'avd', - '--name', avd_name, - '--abi', self.abi, - '--target', 'android-16', - '-c', '128M', - '--force', - ] - avd_process = subprocess.Popen(args=avd_command, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - avd_process.stdin.write('no\n') - avd_process.wait() - logging.info('Create AVD command: %s', ' '.join(avd_command)) - return avd_name - - def _DeleteAVD(self): - """Delete the AVD of this emulator.""" - avd_command = [ - self.android, - '--silent', - 'delete', - 'avd', - '--name', self.avd, - ] - avd_process = subprocess.Popen(args=avd_command, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - logging.info('Delete AVD command: %s', ' '.join(avd_command)) - avd_process.wait() - - def Launch(self, kill_all_emulators): - """Launches the emulator asynchronously. Call ConfirmLaunch() to ensure the - emulator is ready for use. - - If fails, an exception will be raised. - """ - if kill_all_emulators: - _KillAllEmulators() # just to be sure - self._AggressiveImageCleanup() - (self.device, port) = self._DeviceName() - emulator_command = [ - self.emulator, - # Speed up emulator launch by 40%. Really. - '-no-boot-anim', - # The default /data size is 64M. - # That's not enough for 8 unit test bundles and their data. - '-partition-size', '512', - # Enable GPU by default. - '-gpu', 'on', - # Use a familiar name and port. - '-avd', self.avd, - '-port', str(port)] - emulator_command.extend([ - # Wipe the data. We've seen cases where an emulator - # gets 'stuck' if we don't do this (every thousand runs or - # so). - '-wipe-data', - ]) - logging.info('Emulator launch command: %s', ' '.join(emulator_command)) - self.popen = subprocess.Popen(args=emulator_command, - stderr=subprocess.STDOUT) - self._InstallKillHandler() - - def _AggressiveImageCleanup(self): - """Aggressive cleanup of emulator images. - - Experimentally it looks like our current emulator use on the bot - leaves image files around in /tmp/android-$USER. If a "random" - name gets reused, we choke with a 'File exists' error. - TODO(jrg): is there a less hacky way to accomplish the same goal? - """ - logging.info('Aggressive Image Cleanup') - emulator_imagedir = '/tmp/android-%s' % os.environ['USER'] - if not os.path.exists(emulator_imagedir): - return - for image in os.listdir(emulator_imagedir): - full_name = os.path.join(emulator_imagedir, image) - if 'emulator' in full_name: - logging.info('Deleting emulator image %s', full_name) - os.unlink(full_name) - - def ConfirmLaunch(self, wait_for_boot=False): - """Confirm the emulator launched properly. - - Loop on a wait-for-device with a very small timeout. On each - timeout, check the emulator process is still alive. - After confirming a wait-for-device can be successful, make sure - it returns the right answer. - """ - seconds_waited = 0 - number_of_waits = 2 # Make sure we can wfd twice - adb_cmd = "adb -s %s %s" % (self.device, 'wait-for-device') - while seconds_waited < self._LAUNCH_TIMEOUT: - try: - run_command.RunCommand(adb_cmd, - timeout_time=self._WAITFORDEVICE_TIMEOUT, - retry_count=1) - number_of_waits -= 1 - if not number_of_waits: - break - except errors.WaitForResponseTimedOutError as e: - seconds_waited += self._WAITFORDEVICE_TIMEOUT - adb_cmd = "adb -s %s %s" % (self.device, 'kill-server') - run_command.RunCommand(adb_cmd) - self.popen.poll() - if self.popen.returncode != None: - raise EmulatorLaunchException('EMULATOR DIED') - if seconds_waited >= self._LAUNCH_TIMEOUT: - raise EmulatorLaunchException('TIMEOUT with wait-for-device') - logging.info('Seconds waited on wait-for-device: %d', seconds_waited) - if wait_for_boot: - # Now that we checked for obvious problems, wait for a boot complete. - # Waiting for the package manager is sometimes problematic. - a = android_commands.AndroidCommands(self.device) - a.WaitForSystemBootCompleted(self._WAITFORBOOT_TIMEOUT) - - def Shutdown(self): - """Shuts down the process started by launch.""" - if not self.default_avd: - self._DeleteAVD() - if self.popen: - self.popen.poll() - if self.popen.returncode == None: - self.popen.kill() - self.popen = None - def _ShutdownOnSignal(self, signum, frame): - logging.critical('emulator _ShutdownOnSignal') - for sig in self._SIGNALS: - signal.signal(sig, signal.SIG_DFL) - self.Shutdown() - raise KeyboardInterrupt # print a stack +from pylib.utils import emulator - def _InstallKillHandler(self): - """Install a handler to kill the emulator when we exit unexpectedly.""" - for sig in self._SIGNALS: - signal.signal(sig, self._ShutdownOnSignal) def main(argv): - Emulator(None, True).Launch(True) + option_parser = optparse.OptionParser() + option_parser.add_option('-n', '--num', dest='emulator_count', + help='Number of emulators to launch.', + type='int', + default=1) + option_parser.add_option('-w', '--wait', dest='wait_for_boot', + action='store_true', + help='If set, wait for the emulators to boot.') + options, args = option_parser.parse_args(argv) + emulator.LaunchEmulators(options.emulator_count, options.wait_for_boot) if __name__ == '__main__': diff --git a/build/android/pylib/utils/emulator.py b/build/android/pylib/utils/emulator.py new file mode 100755 index 0000000..70afffb --- /dev/null +++ b/build/android/pylib/utils/emulator.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python +# +# Copyright (c) 2012 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Provides an interface to start and stop Android emulator. + +Assumes system environment ANDROID_NDK_ROOT has been set. + + Emulator: The class provides the methods to launch/shutdown the emulator with + the android virtual device named 'avd_armeabi' . +""" + +import logging +import os +import signal +import subprocess +import sys +import time + +import time_profile +# TODO(craigdh): Move these pylib dependencies to pylib/utils/. +from pylib import android_commands +from pylib import cmd_helper + +# adb_interface.py is under ../../third_party/android_testrunner/ +sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', + '..', 'third_party', 'android_testrunner')) +import adb_interface +import errors +import run_command + +class EmulatorLaunchException(Exception): + """Emulator failed to launch.""" + pass + +def _KillAllEmulators(): + """Kill all running emulators that look like ones we started. + + There are odd 'sticky' cases where there can be no emulator process + running but a device slot is taken. A little bot trouble and and + we're out of room forever. + """ + emulators = android_commands.GetEmulators() + if not emulators: + return + for emu_name in emulators: + cmd_helper.GetCmdOutput(['adb', '-s', emu_name, 'emu', 'kill']) + logging.info('Emulator killing is async; give a few seconds for all to die.') + for i in range(5): + if not android_commands.GetEmulators(): + return + time.sleep(1) + + +def DeleteAllTempAVDs(): + """Delete all temporary AVDs which are created for tests. + + If the test exits abnormally and some temporary AVDs created when testing may + be left in the system. Clean these AVDs. + """ + avds = android_commands.GetAVDs() + if not avds: + return + for avd_name in avds: + if 'run_tests_avd' in avd_name: + cmd = ['android', '-s', 'delete', 'avd', '--name', avd_name] + cmd_helper.GetCmdOutput(cmd) + logging.info('Delete AVD %s' % avd_name) + + +class PortPool(object): + """Pool for emulator port starting position that changes over time.""" + _port_min = 5554 + _port_max = 5585 + _port_current_index = 0 + + @classmethod + def port_range(cls): + """Return a range of valid ports for emulator use. + + The port must be an even number between 5554 and 5584. Sometimes + a killed emulator "hangs on" to a port long enough to prevent + relaunch. This is especially true on slow machines (like a bot). + Cycling through a port start position helps make us resilient.""" + ports = range(cls._port_min, cls._port_max, 2) + n = cls._port_current_index + cls._port_current_index = (n + 1) % len(ports) + return ports[n:] + ports[:n] + + +def _GetAvailablePort(): + """Returns an available TCP port for the console.""" + used_ports = [] + emulators = android_commands.GetEmulators() + for emulator in emulators: + used_ports.append(emulator.split('-')[1]) + for port in PortPool.port_range(): + if str(port) not in used_ports: + return port + + +def LaunchEmulators(emulator_count, wait_for_boot=True): + """Launch multiple emulators and wait for them to boot. + + Args: + emulator_count: number of emulators to launch. + + Returns: + List of emulators. + """ + emulators = [] + for n in xrange(emulator_count): + t = time_profile.TimeProfile('Emulator launch %d' % n) + avd_name = None + if n > 0: + # Creates a temporary AVD for the extra emulators. + avd_name = 'run_tests_avd_%d' % n + logging.info('Emulator launch %d with avd_name=%s', n, avd_name) + emulator = Emulator(avd_name) + emulator.Launch(kill_all_emulators=n == 0) + t.Stop() + emulators.append(emulator) + # Wait for all emulators to boot completed. + if wait_for_boot: + for emulator in emulators: + emulator.ConfirmLaunch(True) + return emulators + + +class Emulator(object): + """Provides the methods to launch/shutdown the emulator. + + The emulator has the android virtual device named 'avd_armeabi'. + + The emulator could use any even TCP port between 5554 and 5584 for the + console communication, and this port will be part of the device name like + 'emulator-5554'. Assume it is always True, as the device name is the id of + emulator managed in this class. + + Attributes: + emulator: Path of Android's emulator tool. + popen: Popen object of the running emulator process. + device: Device name of this emulator. + """ + + # Signals we listen for to kill the emulator on + _SIGNALS = (signal.SIGINT, signal.SIGHUP) + + # Time to wait for an emulator launch, in seconds. This includes + # the time to launch the emulator and a wait-for-device command. + _LAUNCH_TIMEOUT = 120 + + # Timeout interval of wait-for-device command before bouncing to a a + # process life check. + _WAITFORDEVICE_TIMEOUT = 5 + + # Time to wait for a "wait for boot complete" (property set on device). + _WAITFORBOOT_TIMEOUT = 300 + + def __init__(self, new_avd_name): + """Init an Emulator. + + Args: + nwe_avd_name: If set, will create a new temporary AVD. + """ + try: + android_sdk_root = os.environ['ANDROID_SDK_ROOT'] + except KeyError: + logging.critical('The ANDROID_SDK_ROOT must be set to run the test on ' + 'emulator.') + raise + self.emulator = os.path.join(android_sdk_root, 'tools', 'emulator') + self.android = os.path.join(android_sdk_root, 'tools', 'android') + self.popen = None + self.device = None + self.default_avd = True + self.abi = 'armeabi-v7a' + self.avd = 'avd_armeabi' + if 'x86' in os.environ.get('TARGET_PRODUCT', ''): + self.abi = 'x86' + self.avd = 'avd_x86' + if new_avd_name: + self.default_avd = False + self.avd = self._CreateAVD(new_avd_name) + + def _DeviceName(self): + """Return our device name.""" + port = _GetAvailablePort() + return ('emulator-%d' % port, port) + + def _CreateAVD(self, avd_name): + """Creates an AVD with the given name. + + Return avd_name. + """ + avd_command = [ + self.android, + '--silent', + 'create', 'avd', + '--name', avd_name, + '--abi', self.abi, + '--target', 'android-16', + '-c', '128M', + '--force', + ] + avd_process = subprocess.Popen(args=avd_command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + avd_process.stdin.write('no\n') + avd_process.wait() + logging.info('Create AVD command: %s', ' '.join(avd_command)) + return avd_name + + def _DeleteAVD(self): + """Delete the AVD of this emulator.""" + avd_command = [ + self.android, + '--silent', + 'delete', + 'avd', + '--name', self.avd, + ] + avd_process = subprocess.Popen(args=avd_command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + logging.info('Delete AVD command: %s', ' '.join(avd_command)) + avd_process.wait() + + def Launch(self, kill_all_emulators): + """Launches the emulator asynchronously. Call ConfirmLaunch() to ensure the + emulator is ready for use. + + If fails, an exception will be raised. + """ + if kill_all_emulators: + _KillAllEmulators() # just to be sure + self._AggressiveImageCleanup() + (self.device, port) = self._DeviceName() + emulator_command = [ + self.emulator, + # Speed up emulator launch by 40%. Really. + '-no-boot-anim', + # The default /data size is 64M. + # That's not enough for 8 unit test bundles and their data. + '-partition-size', '512', + # Enable GPU by default. + '-gpu', 'on', + # Use a familiar name and port. + '-avd', self.avd, + '-port', str(port)] + emulator_command.extend([ + # Wipe the data. We've seen cases where an emulator + # gets 'stuck' if we don't do this (every thousand runs or + # so). + '-wipe-data', + ]) + logging.info('Emulator launch command: %s', ' '.join(emulator_command)) + self.popen = subprocess.Popen(args=emulator_command, + stderr=subprocess.STDOUT) + self._InstallKillHandler() + + def _AggressiveImageCleanup(self): + """Aggressive cleanup of emulator images. + + Experimentally it looks like our current emulator use on the bot + leaves image files around in /tmp/android-$USER. If a "random" + name gets reused, we choke with a 'File exists' error. + TODO(jrg): is there a less hacky way to accomplish the same goal? + """ + logging.info('Aggressive Image Cleanup') + emulator_imagedir = '/tmp/android-%s' % os.environ['USER'] + if not os.path.exists(emulator_imagedir): + return + for image in os.listdir(emulator_imagedir): + full_name = os.path.join(emulator_imagedir, image) + if 'emulator' in full_name: + logging.info('Deleting emulator image %s', full_name) + os.unlink(full_name) + + def ConfirmLaunch(self, wait_for_boot=False): + """Confirm the emulator launched properly. + + Loop on a wait-for-device with a very small timeout. On each + timeout, check the emulator process is still alive. + After confirming a wait-for-device can be successful, make sure + it returns the right answer. + """ + seconds_waited = 0 + number_of_waits = 2 # Make sure we can wfd twice + adb_cmd = "adb -s %s %s" % (self.device, 'wait-for-device') + while seconds_waited < self._LAUNCH_TIMEOUT: + try: + run_command.RunCommand(adb_cmd, + timeout_time=self._WAITFORDEVICE_TIMEOUT, + retry_count=1) + number_of_waits -= 1 + if not number_of_waits: + break + except errors.WaitForResponseTimedOutError as e: + seconds_waited += self._WAITFORDEVICE_TIMEOUT + adb_cmd = "adb -s %s %s" % (self.device, 'kill-server') + run_command.RunCommand(adb_cmd) + self.popen.poll() + if self.popen.returncode != None: + raise EmulatorLaunchException('EMULATOR DIED') + if seconds_waited >= self._LAUNCH_TIMEOUT: + raise EmulatorLaunchException('TIMEOUT with wait-for-device') + logging.info('Seconds waited on wait-for-device: %d', seconds_waited) + if wait_for_boot: + # Now that we checked for obvious problems, wait for a boot complete. + # Waiting for the package manager is sometimes problematic. + a = android_commands.AndroidCommands(self.device) + a.WaitForSystemBootCompleted(self._WAITFORBOOT_TIMEOUT) + + def Shutdown(self): + """Shuts down the process started by launch.""" + if not self.default_avd: + self._DeleteAVD() + if self.popen: + self.popen.poll() + if self.popen.returncode == None: + self.popen.kill() + self.popen = None + + def _ShutdownOnSignal(self, signum, frame): + logging.critical('emulator _ShutdownOnSignal') + for sig in self._SIGNALS: + signal.signal(sig, signal.SIG_DFL) + self.Shutdown() + raise KeyboardInterrupt # print a stack + + def _InstallKillHandler(self): + """Install a handler to kill the emulator when we exit unexpectedly.""" + for sig in self._SIGNALS: + signal.signal(sig, self._ShutdownOnSignal) diff --git a/build/android/run_tests.py b/build/android/run_tests.py index a4727f0..c4e1831 100755 --- a/build/android/run_tests.py +++ b/build/android/run_tests.py @@ -45,7 +45,6 @@ import subprocess import sys import time -import emulator from pylib import android_commands from pylib import buildbot_report from pylib import cmd_helper @@ -53,6 +52,7 @@ from pylib import ports from pylib.base_test_sharder import BaseTestSharder from pylib.gtest import debug_info from pylib.gtest.single_test_runner import SingleTestRunner +from pylib.utils import emulator from pylib.utils import run_tests_helper from pylib.utils import test_options_parser from pylib.utils import time_profile @@ -244,20 +244,9 @@ def _RunATestSuite(options): buildbot_emulators = [] if options.use_emulator: - for n in range(options.emulator_count): - t = time_profile.TimeProfile('Emulator launch %d' % n) - avd_name = None - if n > 0: - # Creates a temporary AVD for the extra emulators. - avd_name = 'run_tests_avd_%d' % n - buildbot_emulator = emulator.Emulator(avd_name) - buildbot_emulator.Launch(kill_all_emulators=n == 0) - t.Stop() - buildbot_emulators.append(buildbot_emulator) - attached_devices.append(buildbot_emulator.device) - # Wait for all emulators to boot completed. - map(lambda buildbot_emulator: buildbot_emulator.ConfirmLaunch(True), - buildbot_emulators) + buildbot_emulators = emulator.LaunchEmulators(options.emulator_count, + wait_for_boot=True) + attached_devices = [e.device for e in buildbot_emulators] elif options.test_device: attached_devices = [options.test_device] else: |