Manual partial update from from https://crrev.com/337502
My goal is to get the changes from https://chromium.googlesource.com/chromium/src.git/+/5ee458982c5706abf29f456b4383aea413766cea.
My strategy is to take the latest version of build/android/test_runner.py and build/android/pylib
R=viettrungluu@chromium.org, vtl
Review URL: https://codereview.chromium.org/1222313015 .
diff --git a/build/android/pylib/base/base_setup.py b/build/android/pylib/base/base_setup.py
index 0b0daf1..a416380 100644
--- a/build/android/pylib/base/base_setup.py
+++ b/build/android/pylib/base/base_setup.py
@@ -59,7 +59,5 @@
def PushDataDeps(device, device_dir, test_options):
valgrind_tools.PushFilesForTool(test_options.tool, device)
if os.path.exists(constants.ISOLATE_DEPS_DIR):
- device.PushChangedFiles([
- (os.path.join(constants.ISOLATE_DEPS_DIR, p),
- '%s/%s' % (device_dir, p))
- for p in os.listdir(constants.ISOLATE_DEPS_DIR)])
+ device.PushChangedFiles([(constants.ISOLATE_DEPS_DIR, device_dir)],
+ delete_device_stale=test_options.delete_stale_data)
diff --git a/build/android/pylib/device/battery_utils.py b/build/android/pylib/device/battery_utils.py
index df5f826..d160bbb 100644
--- a/build/android/pylib/device/battery_utils.py
+++ b/build/android/pylib/device/battery_utils.py
@@ -20,15 +20,23 @@
_DEFAULT_TIMEOUT = 30
_DEFAULT_RETRIES = 3
-_CONTROL_CHARGING_COMMANDS = [
+
+_DEVICE_PROFILES = [
{
- # Nexus 4
+ 'name': 'Nexus 4',
'witness_file': '/sys/module/pm8921_charger/parameters/disabled',
- 'enable_command': 'echo 0 > /sys/module/pm8921_charger/parameters/disabled',
- 'disable_command':
- 'echo 1 > /sys/module/pm8921_charger/parameters/disabled',
+ 'enable_command': (
+ 'echo 0 > /sys/module/pm8921_charger/parameters/disabled && '
+ 'dumpsys battery reset'),
+ 'disable_command': (
+ 'echo 1 > /sys/module/pm8921_charger/parameters/disabled && '
+ 'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
+ 'charge_counter': None,
+ 'voltage': None,
+ 'current': None,
},
{
+ 'name': 'Nexus 5',
# Nexus 5
# Setting the HIZ bit of the bq24192 causes the charger to actually ignore
# energy coming from USB. Setting the power_supply offline just updates the
@@ -36,11 +44,57 @@
'witness_file': '/sys/kernel/debug/bq24192/INPUT_SRC_CONT',
'enable_command': (
'echo 0x4A > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
- 'echo 1 > /sys/class/power_supply/usb/online'),
+ 'echo 1 > /sys/class/power_supply/usb/online &&'
+ 'dumpsys battery reset'),
'disable_command': (
'echo 0xCA > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
'chmod 644 /sys/class/power_supply/usb/online && '
- 'echo 0 > /sys/class/power_supply/usb/online'),
+ 'echo 0 > /sys/class/power_supply/usb/online && '
+ 'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
+ 'charge_counter': None,
+ 'voltage': None,
+ 'current': None,
+ },
+ {
+ 'name': 'Nexus 6',
+ 'witness_file': None,
+ 'enable_command': (
+ 'echo 1 > /sys/class/power_supply/battery/charging_enabled && '
+ 'dumpsys battery reset'),
+ 'disable_command': (
+ 'echo 0 > /sys/class/power_supply/battery/charging_enabled && '
+ 'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
+ 'charge_counter': (
+ '/sys/class/power_supply/max170xx_battery/charge_counter_ext'),
+ 'voltage': '/sys/class/power_supply/max170xx_battery/voltage_now',
+ 'current': '/sys/class/power_supply/max170xx_battery/current_now',
+ },
+ {
+ 'name': 'Nexus 9',
+ 'witness_file': None,
+ 'enable_command': (
+ 'echo Disconnected > '
+ '/sys/bus/i2c/drivers/bq2419x/0-006b/input_cable_state && '
+ 'dumpsys battery reset'),
+ 'disable_command': (
+ 'echo Connected > '
+ '/sys/bus/i2c/drivers/bq2419x/0-006b/input_cable_state && '
+ 'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
+ 'charge_counter': (
+ '/sys/class/power_supply/max170xx_battery/charge_counter_ext'),
+ 'voltage': '/sys/class/power_supply/max170xx_battery/voltage_now',
+ 'current': '/sys/class/power_supply/max170xx_battery/current_now',
+ },
+ {
+ 'name': 'Nexus 10',
+ 'witness_file': None,
+ 'enable_command': None,
+ 'disable_command': None,
+ 'charge_counter': (
+ '/sys/class/power_supply/ds2784-fuelgauge/charge_counter_ext'),
+ 'voltage': '/sys/class/power_supply/ds2784-fuelgauge/voltage_now',
+ 'current': '/sys/class/power_supply/ds2784-fuelgauge/current_now',
+
},
]
@@ -88,8 +142,47 @@
self._default_retries = default_retries
@decorators.WithTimeoutAndRetriesFromInstance()
+ def SupportsFuelGauge(self, timeout=None, retries=None):
+ """Detect if fuel gauge chip is present.
+
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ True if known fuel gauge files are present.
+ False otherwise.
+ """
+ self._DiscoverDeviceProfile()
+ return (self._cache['profile']['enable_command'] != None
+ and self._cache['profile']['charge_counter'] != None)
+
+ @decorators.WithTimeoutAndRetriesFromInstance()
+ def GetFuelGaugeChargeCounter(self, timeout=None, retries=None):
+ """Get value of charge_counter on fuel gauge chip.
+
+ Device must have charging disabled for this, not just battery updates
+ disabled. The only device that this currently works with is the nexus 5.
+
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ value of charge_counter for fuel gauge chip in units of nAh.
+
+ Raises:
+ device_errors.CommandFailedError: If fuel gauge chip not found.
+ """
+ if self.SupportsFuelGauge():
+ return int(self._device.ReadFile(
+ self._cache['profile']['charge_counter']))
+ raise device_errors.CommandFailedError(
+ 'Unable to find fuel gauge.')
+
+ @decorators.WithTimeoutAndRetriesFromInstance()
def GetNetworkData(self, package, timeout=None, retries=None):
- """ Get network data for specific package.
+ """Get network data for specific package.
Args:
package: package name you want network data for.
@@ -127,7 +220,8 @@
@decorators.WithTimeoutAndRetriesFromInstance()
def GetPowerData(self, timeout=None, retries=None):
- """ Get power data for device.
+ """Get power data for device.
+
Args:
timeout: timeout in seconds
retries: number of retries
@@ -174,7 +268,7 @@
@decorators.WithTimeoutAndRetriesFromInstance()
def GetPackagePowerData(self, package, timeout=None, retries=None):
- """ Get power data for particular package.
+ """Get power data for particular package.
Args:
package: Package to get power data on.
@@ -246,19 +340,15 @@
device_errors.CommandFailedError: If method of disabling charging cannot
be determined.
"""
- if 'charging_config' not in self._cache:
- for c in _CONTROL_CHARGING_COMMANDS:
- if self._device.FileExists(c['witness_file']):
- self._cache['charging_config'] = c
- break
- else:
- raise device_errors.CommandFailedError(
- 'Unable to find charging commands.')
+ self._DiscoverDeviceProfile()
+ if not self._cache['profile']['enable_command']:
+ raise device_errors.CommandFailedError(
+ 'Unable to find charging commands.')
if enabled:
- command = self._cache['charging_config']['enable_command']
+ command = self._cache['profile']['enable_command']
else:
- command = self._cache['charging_config']['disable_command']
+ command = self._cache['profile']['disable_command']
def set_and_verify_charging():
self._device.RunShellCommand(command, check_return=True)
@@ -269,7 +359,7 @@
# TODO(rnephew): Make private when all use cases can use the context manager.
@decorators.WithTimeoutAndRetriesFromInstance()
def DisableBatteryUpdates(self, timeout=None, retries=None):
- """ Resets battery data and makes device appear like it is not
+ """Resets battery data and makes device appear like it is not
charging so that it will collect power data since last charge.
Args:
@@ -284,27 +374,7 @@
def battery_updates_disabled():
return self.GetCharging() is False
- if (self._device.build_version_sdk <
- constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP):
- raise device_errors.DeviceVersionError('Device must be L or higher.')
-
- self._device.RunShellCommand(
- ['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True)
- self._device.RunShellCommand(
- ['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True)
- self._device.RunShellCommand(
- ['dumpsys', 'batterystats', '--reset'], check_return=True)
- battery_data = self._device.RunShellCommand(
- ['dumpsys', 'batterystats', '--charged', '--checkin'],
- check_return=True, large_output=True)
- ROW_TYPE_INDEX = 3
- PWI_POWER_INDEX = 5
- for line in battery_data:
- l = line.split(',')
- if (len(l) > PWI_POWER_INDEX and l[ROW_TYPE_INDEX] == 'pwi'
- and l[PWI_POWER_INDEX] != 0):
- raise device_errors.CommandFailedError(
- 'Non-zero pmi value found after reset.')
+ self._ClearPowerData()
self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'ac', '0'],
check_return=True)
self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'usb', '0'],
@@ -314,7 +384,7 @@
# TODO(rnephew): Make private when all use cases can use the context manager.
@decorators.WithTimeoutAndRetriesFromInstance()
def EnableBatteryUpdates(self, timeout=None, retries=None):
- """ Restarts device charging so that dumpsys no longer collects power data.
+ """Restarts device charging so that dumpsys no longer collects power data.
Args:
timeout: timeout in seconds
@@ -324,11 +394,9 @@
device_errors.DeviceVersionError: If device is not L or higher.
"""
def battery_updates_enabled():
- return self.GetCharging() is True
-
- if (self._device.build_version_sdk <
- constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP):
- raise device_errors.DeviceVersionError('Device must be L or higher.')
+ return (self.GetCharging()
+ or not bool('UPDATES STOPPED' in self._device.RunShellCommand(
+ ['dumpsys', 'battery'], check_return=True)))
self._device.RunShellCommand(['dumpsys', 'battery', 'reset'],
check_return=True)
@@ -401,7 +469,127 @@
else:
logging.info('Current battery temperature: %s', temp)
return int(temp) <= target_temp
-
- logging.info('Waiting for the device to cool down to %s degrees.',
+ self.EnableBatteryUpdates()
+ logging.info('Waiting for the device to cool down to %s (0.1 C)',
target_temp)
timeout_retry.WaitFor(cool_device, wait_period=wait_period)
+
+ @decorators.WithTimeoutAndRetriesFromInstance()
+ def TieredSetCharging(self, enabled, timeout=None, retries=None):
+ """Enables or disables charging on the device.
+
+ Args:
+ enabled: A boolean indicating whether charging should be enabled or
+ disabled.
+ timeout: timeout in seconds
+ retries: number of retries
+ """
+ if self.GetCharging() == enabled:
+ logging.warning('Device charging already in expected state: %s', enabled)
+ return
+
+ if enabled:
+ try:
+ self.SetCharging(enabled)
+ except device_errors.CommandFailedError:
+ logging.info('Unable to enable charging via hardware.'
+ ' Falling back to software enabling.')
+ self.EnableBatteryUpdates()
+ else:
+ try:
+ self._ClearPowerData()
+ self.SetCharging(enabled)
+ except device_errors.CommandFailedError:
+ logging.info('Unable to disable charging via hardware.'
+ ' Falling back to software disabling.')
+ self.DisableBatteryUpdates()
+
+ @contextlib.contextmanager
+ def PowerMeasurement(self, timeout=None, retries=None):
+ """Context manager that enables battery power collection.
+
+ Once the with block is exited, charging is resumed. Will attempt to disable
+ charging at the hardware level, and if that fails will fall back to software
+ disabling of battery updates.
+
+ Only for devices L and higher.
+
+ Example usage:
+ with PowerMeasurement():
+ browser_actions()
+ get_power_data() # report usage within this block
+ after_measurements() # Anything that runs after power
+ # measurements are collected
+
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+ """
+ try:
+ self.TieredSetCharging(False, timeout=timeout, retries=retries)
+ yield
+ finally:
+ self.TieredSetCharging(True, timeout=timeout, retries=retries)
+
+ def _ClearPowerData(self):
+ """Resets battery data and makes device appear like it is not
+ charging so that it will collect power data since last charge.
+
+ Returns:
+ True if power data cleared.
+ False if power data clearing is not supported (pre-L)
+
+ Raises:
+ device_errors.DeviceVersionError: If power clearing is supported,
+ but fails.
+ """
+ if (self._device.build_version_sdk <
+ constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP):
+ logging.warning('Dumpsys power data only available on 5.0 and above. '
+ 'Cannot clear power data.')
+ return False
+
+ self._device.RunShellCommand(
+ ['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True)
+ self._device.RunShellCommand(
+ ['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True)
+ self._device.RunShellCommand(
+ ['dumpsys', 'batterystats', '--reset'], check_return=True)
+ battery_data = self._device.RunShellCommand(
+ ['dumpsys', 'batterystats', '--charged', '--checkin'],
+ check_return=True, large_output=True)
+ for line in battery_data:
+ l = line.split(',')
+ if (len(l) > _PWI_POWER_CONSUMPTION_INDEX and l[_ROW_TYPE_INDEX] == 'pwi'
+ and l[_PWI_POWER_CONSUMPTION_INDEX] != 0):
+ self._device.RunShellCommand(
+ ['dumpsys', 'battery', 'reset'], check_return=True)
+ raise device_errors.CommandFailedError(
+ 'Non-zero pmi value found after reset.')
+ self._device.RunShellCommand(
+ ['dumpsys', 'battery', 'reset'], check_return=True)
+ return True
+
+ def _DiscoverDeviceProfile(self):
+ """Checks and caches device information.
+
+ Returns:
+ True if profile is found, false otherwise.
+ """
+
+ if 'profile' in self._cache:
+ return True
+ for profile in _DEVICE_PROFILES:
+ if self._device.product_model == profile['name']:
+ self._cache['profile'] = profile
+ return True
+ self._cache['profile'] = {
+ 'name': None,
+ 'witness_file': None,
+ 'enable_command': None,
+ 'disable_command': None,
+ 'charge_counter': None,
+ 'voltage': None,
+ 'current': None,
+ }
+ return False
diff --git a/build/android/pylib/device/battery_utils_test.py b/build/android/pylib/device/battery_utils_test.py
index 51fae17..b968fa6 100755
--- a/build/android/pylib/device/battery_utils_test.py
+++ b/build/android/pylib/device/battery_utils_test.py
@@ -40,6 +40,43 @@
class BatteryUtilsTest(mock_calls.TestCase):
+ _NEXUS_5 = {
+ 'name': 'Nexus 5',
+ 'witness_file': '/sys/kernel/debug/bq24192/INPUT_SRC_CONT',
+ 'enable_command': (
+ 'echo 0x4A > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
+ 'echo 1 > /sys/class/power_supply/usb/online'),
+ 'disable_command': (
+ 'echo 0xCA > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
+ 'chmod 644 /sys/class/power_supply/usb/online && '
+ 'echo 0 > /sys/class/power_supply/usb/online'),
+ 'charge_counter': None,
+ 'voltage': None,
+ 'current': None,
+ }
+
+ _NEXUS_6 = {
+ 'name': 'Nexus 6',
+ 'witness_file': None,
+ 'enable_command': None,
+ 'disable_command': None,
+ 'charge_counter': (
+ '/sys/class/power_supply/max170xx_battery/charge_counter_ext'),
+ 'voltage': '/sys/class/power_supply/max170xx_battery/voltage_now',
+ 'current': '/sys/class/power_supply/max170xx_battery/current_now',
+ }
+
+ _NEXUS_10 = {
+ 'name': 'Nexus 10',
+ 'witness_file': None,
+ 'enable_command': None,
+ 'disable_command': None,
+ 'charge_counter': (
+ '/sys/class/power_supply/ds2784-fuelgauge/charge_counter_ext'),
+ 'voltage': '/sys/class/power_supply/ds2784-fuelgauge/voltage_now',
+ 'current': '/sys/class/power_supply/ds2784-fuelgauge/current_now',
+ }
+
def ShellError(self, output=None, status=1):
def action(cmd, *args, **kwargs):
raise device_errors.AdbShellCommandFailedError(
@@ -76,8 +113,8 @@
@mock.patch('time.sleep', mock.Mock())
def testSetCharging_enabled(self):
+ self.battery._cache['profile'] = self._NEXUS_5
with self.assertCalls(
- (self.call.device.FileExists(mock.ANY), True),
(self.call.device.RunShellCommand(mock.ANY, check_return=True), []),
(self.call.battery.GetCharging(), False),
(self.call.device.RunShellCommand(mock.ANY, check_return=True), []),
@@ -85,16 +122,16 @@
self.battery.SetCharging(True)
def testSetCharging_alreadyEnabled(self):
+ self.battery._cache['profile'] = self._NEXUS_5
with self.assertCalls(
- (self.call.device.FileExists(mock.ANY), True),
(self.call.device.RunShellCommand(mock.ANY, check_return=True), []),
(self.call.battery.GetCharging(), True)):
self.battery.SetCharging(True)
@mock.patch('time.sleep', mock.Mock())
def testSetCharging_disabled(self):
+ self.battery._cache['profile'] = self._NEXUS_5
with self.assertCalls(
- (self.call.device.FileExists(mock.ANY), True),
(self.call.device.RunShellCommand(mock.ANY, check_return=True), []),
(self.call.battery.GetCharging(), True),
(self.call.device.RunShellCommand(mock.ANY, check_return=True), []),
@@ -105,20 +142,12 @@
class BatteryUtilsSetBatteryMeasurementTest(BatteryUtilsTest):
@mock.patch('time.sleep', mock.Mock())
- def testBatteryMeasurement(self):
+ def testBatteryMeasurementWifi(self):
with self.assertCalls(
(self.call.device.RunShellCommand(
mock.ANY, retries=0, single_line=True,
timeout=10, check_return=True), '22'),
- (self.call.device.RunShellCommand(
- ['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True), []),
- (self.call.device.RunShellCommand(
- ['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True), []),
- (self.call.device.RunShellCommand(
- ['dumpsys', 'batterystats', '--reset'], check_return=True), []),
- (self.call.device.RunShellCommand(
- ['dumpsys', 'batterystats', '--charged', '--checkin'],
- check_return=True, large_output=True), []),
+ (self.call.battery._ClearPowerData(), True),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'set', 'ac', '0'], check_return=True), []),
(self.call.device.RunShellCommand(
@@ -126,6 +155,32 @@
(self.call.battery.GetCharging(), False),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'reset'], check_return=True), []),
+ (self.call.battery.GetCharging(), False),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery'], check_return=True), ['UPDATES STOPPED']),
+ (self.call.battery.GetCharging(), False),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery'], check_return=True), [])):
+ with self.battery.BatteryMeasurement():
+ pass
+
+ @mock.patch('time.sleep', mock.Mock())
+ def testBatteryMeasurementUsb(self):
+ with self.assertCalls(
+ (self.call.device.RunShellCommand(
+ mock.ANY, retries=0, single_line=True,
+ timeout=10, check_return=True), '22'),
+ (self.call.battery._ClearPowerData(), True),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery', 'set', 'ac', '0'], check_return=True), []),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery', 'set', 'usb', '0'], check_return=True), []),
+ (self.call.battery.GetCharging(), False),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery', 'reset'], check_return=True), []),
+ (self.call.battery.GetCharging(), False),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery'], check_return=True), ['UPDATES STOPPED']),
(self.call.battery.GetCharging(), True)):
with self.battery.BatteryMeasurement():
pass
@@ -311,21 +366,209 @@
self.battery._cache.clear()
self.assertEqual(self.battery.GetNetworkData('test_package1'), (1,2))
+
class BatteryUtilsLetBatteryCoolToTemperatureTest(BatteryUtilsTest):
@mock.patch('time.sleep', mock.Mock())
def testLetBatteryCoolToTemperature_startUnder(self):
with self.assertCalls(
+ (self.call.battery.EnableBatteryUpdates(), []),
(self.call.battery.GetBatteryInfo(), {'temperature': '500'})):
self.battery.LetBatteryCoolToTemperature(600)
@mock.patch('time.sleep', mock.Mock())
def testLetBatteryCoolToTemperature_startOver(self):
with self.assertCalls(
+ (self.call.battery.EnableBatteryUpdates(), []),
(self.call.battery.GetBatteryInfo(), {'temperature': '500'}),
(self.call.battery.GetBatteryInfo(), {'temperature': '400'})):
self.battery.LetBatteryCoolToTemperature(400)
+class BatteryUtilsSupportsFuelGaugeTest(BatteryUtilsTest):
+
+ def testSupportsFuelGauge_false(self):
+ self.battery._cache['profile'] = self._NEXUS_5
+ self.assertFalse(self.battery.SupportsFuelGauge())
+
+ def testSupportsFuelGauge_trueMax(self):
+ self.battery._cache['profile'] = self._NEXUS_6
+ # TODO(rnephew): Change this to assertTrue when we have support for
+ # disabling hardware charging on nexus 6.
+ self.assertFalse(self.battery.SupportsFuelGauge())
+
+ def testSupportsFuelGauge_trueDS(self):
+ self.battery._cache['profile'] = self._NEXUS_10
+ # TODO(rnephew): Change this to assertTrue when we have support for
+ # disabling hardware charging on nexus 10.
+ self.assertFalse(self.battery.SupportsFuelGauge())
+
+
+class BatteryUtilsGetFuelGaugeChargeCounterTest(BatteryUtilsTest):
+
+ def testGetFuelGaugeChargeCounter_noFuelGauge(self):
+ self.battery._cache['profile'] = self._NEXUS_5
+ with self.assertRaises(device_errors.CommandFailedError):
+ self.battery.GetFuelGaugeChargeCounter()
+
+ def testGetFuelGaugeChargeCounter_fuelGaugePresent(self):
+ self.battery._cache['profile']= self._NEXUS_6
+ with self.assertCalls(
+ (self.call.battery.SupportsFuelGauge(), True),
+ (self.call.device.ReadFile(mock.ANY), '123')):
+ self.assertEqual(self.battery.GetFuelGaugeChargeCounter(), 123)
+
+
+class BatteryUtilsTieredSetCharging(BatteryUtilsTest):
+
+ @mock.patch('time.sleep', mock.Mock())
+ def testTieredSetCharging_softwareSetTrue(self):
+ self.battery._cache['profile'] = self._NEXUS_6
+ with self.assertCalls(
+ (self.call.battery.GetCharging(), False),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery', 'reset'], check_return=True), []),
+ (self.call.battery.GetCharging(), False),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery'], check_return=True), ['UPDATES STOPPED']),
+ (self.call.battery.GetCharging(), True)):
+ self.battery.TieredSetCharging(True)
+
+ @mock.patch('time.sleep', mock.Mock())
+ def testTieredSetCharging_softwareSetFalse(self):
+ self.battery._cache['profile'] = self._NEXUS_6
+ with self.assertCalls(
+ (self.call.battery.GetCharging(), True),
+ (self.call.battery._ClearPowerData(), True),
+ (self.call.battery._ClearPowerData(), True),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery', 'set', 'ac', '0'], check_return=True), []),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery', 'set', 'usb', '0'], check_return=True), []),
+ (self.call.battery.GetCharging(), False)):
+ self.battery.TieredSetCharging(False)
+
+ @mock.patch('time.sleep', mock.Mock())
+ def testTieredSetCharging_hardwareSetTrue(self):
+ self.battery._cache['profile'] = self._NEXUS_5
+ with self.assertCalls(
+ (self.call.battery.GetCharging(), False),
+ (self.call.battery.SetCharging(True))):
+ self.battery.TieredSetCharging(True)
+
+ @mock.patch('time.sleep', mock.Mock())
+ def testTieredSetCharging_hardwareSetFalse(self):
+ self.battery._cache['profile'] = self._NEXUS_5
+ with self.assertCalls(
+ (self.call.battery.GetCharging(), True),
+ (self.call.battery._ClearPowerData(), True),
+ (self.call.battery.SetCharging(False))):
+ self.battery.TieredSetCharging(False)
+
+ def testTieredSetCharging_expectedStateAlreadyTrue(self):
+ with self.assertCalls((self.call.battery.GetCharging(), True)):
+ self.battery.TieredSetCharging(True)
+
+ def testTieredSetCharging_expectedStateAlreadyFalse(self):
+ with self.assertCalls((self.call.battery.GetCharging(), False)):
+ self.battery.TieredSetCharging(False)
+
+
+class BatteryUtilsPowerMeasurement(BatteryUtilsTest):
+
+ def testPowerMeasurement_hardware(self):
+ self.battery._cache['profile'] = self._NEXUS_5
+ with self.assertCalls(
+ (self.call.battery.GetCharging(), True),
+ (self.call.battery._ClearPowerData(), True),
+ (self.call.battery.SetCharging(False)),
+ (self.call.battery.GetCharging(), False),
+ (self.call.battery.SetCharging(True))):
+ with self.battery.PowerMeasurement():
+ pass
+
+ @mock.patch('time.sleep', mock.Mock())
+ def testPowerMeasurement_software(self):
+ self.battery._cache['profile'] = self._NEXUS_6
+ with self.assertCalls(
+ (self.call.battery.GetCharging(), True),
+ (self.call.battery._ClearPowerData(), True),
+ (self.call.battery._ClearPowerData(), True),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery', 'set', 'ac', '0'], check_return=True), []),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery', 'set', 'usb', '0'], check_return=True), []),
+ (self.call.battery.GetCharging(), False),
+ (self.call.battery.GetCharging(), False),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery', 'reset'], check_return=True), []),
+ (self.call.battery.GetCharging(), False),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery'], check_return=True), ['UPDATES STOPPED']),
+ (self.call.battery.GetCharging(), True)):
+ with self.battery.PowerMeasurement():
+ pass
+
+
+class BatteryUtilsDiscoverDeviceProfile(BatteryUtilsTest):
+
+ def testDiscoverDeviceProfile_known(self):
+ with self.assertCalls(
+ (self.call.adb.Shell('getprop ro.product.model'), "Nexus 4")):
+ self.battery._DiscoverDeviceProfile()
+ self.assertEqual(self.battery._cache['profile']['name'], "Nexus 4")
+
+ def testDiscoverDeviceProfile_unknown(self):
+ with self.assertCalls(
+ (self.call.adb.Shell('getprop ro.product.model'), "Other")):
+ self.battery._DiscoverDeviceProfile()
+ self.assertEqual(self.battery._cache['profile']['name'], None)
+
+
+class BatteryUtilsClearPowerData(BatteryUtilsTest):
+
+ def testClearPowerData_preL(self):
+ with self.assertCalls(
+ (self.call.device.RunShellCommand(mock.ANY, retries=0,
+ single_line=True, timeout=10, check_return=True), '20')):
+ self.assertFalse(self.battery._ClearPowerData())
+
+ def testClearPowerData_clearedL(self):
+ with self.assertCalls(
+ (self.call.device.RunShellCommand(mock.ANY, retries=0,
+ single_line=True, timeout=10, check_return=True), '22'),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True), []),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True), []),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'batterystats', '--reset'], check_return=True), []),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'batterystats', '--charged', '--checkin'],
+ check_return=True, large_output=True), []),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery', 'reset'], check_return=True), [])):
+ self.assertTrue(self.battery._ClearPowerData())
+
+ def testClearPowerData_notClearedL(self):
+ with self.assertCalls(
+ (self.call.device.RunShellCommand(mock.ANY, retries=0,
+ single_line=True, timeout=10, check_return=True), '22'),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True), []),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True), []),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'batterystats', '--reset'], check_return=True), []),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'batterystats', '--charged', '--checkin'],
+ check_return=True, large_output=True),
+ ['9,1000,l,pwi,uid,0.0327']),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'battery', 'reset'], check_return=True), [])):
+ with self.assertRaises(device_errors.CommandFailedError):
+ self.battery._ClearPowerData()
+
+
if __name__ == '__main__':
logging.getLogger().setLevel(logging.DEBUG)
unittest.main(verbosity=2)
diff --git a/build/android/pylib/device/device_utils.py b/build/android/pylib/device/device_utils.py
index c8b4718..3a6563e 100644
--- a/build/android/pylib/device/device_utils.py
+++ b/build/android/pylib/device/device_utils.py
@@ -26,6 +26,7 @@
from pylib import cmd_helper
from pylib import constants
from pylib import device_signal
+from pylib.constants import keyevent
from pylib.device import adb_wrapper
from pylib.device import decorators
from pylib.device import device_blacklist
@@ -33,6 +34,7 @@
from pylib.device import intent
from pylib.device import logcat_monitor
from pylib.device.commands import install_commands
+from pylib.sdk import split_select
from pylib.utils import apk_helper
from pylib.utils import base_error
from pylib.utils import device_temp_file
@@ -135,6 +137,8 @@
_MAX_ADB_COMMAND_LENGTH = 512
_MAX_ADB_OUTPUT_LENGTH = 32768
+ _LAUNCHER_FOCUSED_RE = re.compile(
+ '\s*mCurrentFocus.*(Launcher|launcher).*')
_VALID_SHELL_VARIABLE = re.compile('^[a-zA-Z_][a-zA-Z0-9_]*$')
# Property in /data/local.prop that controls Java assertions.
@@ -339,14 +343,14 @@
return value
@decorators.WithTimeoutAndRetriesFromInstance()
- def GetApplicationPath(self, package, timeout=None, retries=None):
- """Get the path of the installed apk on the device for the given package.
+ def GetApplicationPaths(self, package, timeout=None, retries=None):
+ """Get the paths of the installed apks on the device for the given package.
Args:
package: Name of the package.
Returns:
- Path to the apk on the device if it exists, None otherwise.
+ List of paths to the apks on the device for the given package.
"""
# 'pm path' is liable to incorrectly exit with a nonzero number starting
# in Lollipop.
@@ -354,14 +358,37 @@
# released to put an upper bound on this.
should_check_return = (self.build_version_sdk <
constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP)
- output = self.RunShellCommand(['pm', 'path', package], single_line=True,
- check_return=should_check_return)
- if not output:
- return None
- if not output.startswith('package:'):
- raise device_errors.CommandFailedError('pm path returned: %r' % output,
- str(self))
- return output[len('package:'):]
+ output = self.RunShellCommand(
+ ['pm', 'path', package], check_return=should_check_return)
+ apks = []
+ for line in output:
+ if not line.startswith('package:'):
+ raise device_errors.CommandFailedError(
+ 'pm path returned: %r' % '\n'.join(output), str(self))
+ apks.append(line[len('package:'):])
+ return apks
+
+ @decorators.WithTimeoutAndRetriesFromInstance()
+ def GetApplicationDataDirectory(self, package, timeout=None, retries=None):
+ """Get the data directory on the device for the given package.
+
+ Args:
+ package: Name of the package.
+
+ Returns:
+ The package's data directory, or None if the package doesn't exist on the
+ device.
+ """
+ try:
+ output = self._RunPipedShellCommand(
+ 'pm dump %s | grep dataDir=' % cmd_helper.SingleQuote(package))
+ for line in output:
+ _, _, dataDir = line.partition('dataDir=')
+ if dataDir:
+ return dataDir
+ except device_errors.CommandFailedError:
+ logging.exception('Could not find data directory for %s', package)
+ return None
@decorators.WithTimeoutAndRetriesFromInstance()
def WaitUntilFullyBooted(self, wifi=False, timeout=None, retries=None):
@@ -391,7 +418,7 @@
def pm_ready():
try:
- return self.GetApplicationPath('android')
+ return self.GetApplicationPaths('android')
except device_errors.CommandFailedError:
return False
@@ -461,9 +488,15 @@
DeviceUnreachableError on missing device.
"""
package_name = apk_helper.GetPackageName(apk_path)
- device_path = self.GetApplicationPath(package_name)
- if device_path is not None:
- should_install = bool(self._GetChangedFilesImpl(apk_path, device_path))
+ device_paths = self.GetApplicationPaths(package_name)
+ if device_paths:
+ if len(device_paths) > 1:
+ logging.warning(
+ 'Installing single APK (%s) when split APKs (%s) are currently '
+ 'installed.', apk_path, ' '.join(device_paths))
+ (files_to_push, _) = self._GetChangedAndStaleFiles(
+ apk_path, device_paths[0])
+ should_install = bool(files_to_push)
if should_install and not reinstall:
self.adb.Uninstall(package_name)
else:
@@ -471,6 +504,62 @@
if should_install:
self.adb.Install(apk_path, reinstall=reinstall)
+ @decorators.WithTimeoutAndRetriesDefaults(
+ INSTALL_DEFAULT_TIMEOUT,
+ INSTALL_DEFAULT_RETRIES)
+ def InstallSplitApk(self, base_apk, split_apks, reinstall=False,
+ timeout=None, retries=None):
+ """Install a split APK.
+
+ Noop if all of the APK splits are already installed.
+
+ Args:
+ base_apk: A string of the path to the base APK.
+ split_apks: A list of strings of paths of all of the APK splits.
+ reinstall: A boolean indicating if we should keep any existing app data.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandFailedError if the installation fails.
+ CommandTimeoutError if the installation times out.
+ DeviceUnreachableError on missing device.
+ DeviceVersionError if device SDK is less than Android L.
+ """
+ self._CheckSdkLevel(constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP)
+
+ all_apks = [base_apk] + split_select.SelectSplits(
+ self, base_apk, split_apks)
+ package_name = apk_helper.GetPackageName(base_apk)
+ device_apk_paths = self.GetApplicationPaths(package_name)
+
+ if device_apk_paths:
+ partial_install_package = package_name
+ device_checksums = md5sum.CalculateDeviceMd5Sums(device_apk_paths, self)
+ host_checksums = md5sum.CalculateHostMd5Sums(all_apks)
+ apks_to_install = [k for (k, v) in host_checksums.iteritems()
+ if v not in device_checksums.values()]
+ if apks_to_install and not reinstall:
+ self.adb.Uninstall(package_name)
+ partial_install_package = None
+ apks_to_install = all_apks
+ else:
+ partial_install_package = None
+ apks_to_install = all_apks
+ if apks_to_install:
+ self.adb.InstallMultiple(
+ apks_to_install, partial=partial_install_package, reinstall=reinstall)
+
+ def _CheckSdkLevel(self, required_sdk_level):
+ """Raises an exception if the device does not have the required SDK level.
+ """
+ if self.build_version_sdk < required_sdk_level:
+ raise device_errors.DeviceVersionError(
+ ('Requires SDK level %s, device is SDK level %s' %
+ (required_sdk_level, self.build_version_sdk)),
+ device_serial=self.adb.GetDeviceSerial())
+
+
@decorators.WithTimeoutAndRetriesFromInstance()
def RunShellCommand(self, cmd, check_return=False, cwd=None, env=None,
as_root=False, single_line=False, large_output=False,
@@ -557,8 +646,8 @@
if large_output_mode:
with device_temp_file.DeviceTempFile(self.adb) as large_output_file:
cmd = '%s > %s' % (cmd, large_output_file.name)
- logging.info('Large output mode enabled. Will write output to device '
- 'and read results from file.')
+ logging.debug('Large output mode enabled. Will write output to '
+ 'device and read results from file.')
handle_large_command(cmd)
return self.ReadFile(large_output_file.name, force_pull=True)
else:
@@ -714,7 +803,7 @@
for k, v in extras.iteritems():
cmd.extend(['-e', str(k), str(v)])
cmd.append(component)
- return self.RunShellCommand(cmd, check_return=True)
+ return self.RunShellCommand(cmd, check_return=True, large_output=True)
@decorators.WithTimeoutAndRetriesFromInstance()
def BroadcastIntent(self, intent_obj, timeout=None, retries=None):
@@ -734,7 +823,10 @@
@decorators.WithTimeoutAndRetriesFromInstance()
def GoHome(self, timeout=None, retries=None):
- """Return to the home screen.
+ """Return to the home screen and obtain launcher focus.
+
+ This command launches the home screen and attempts to obtain
+ launcher focus until the timeout is reached.
Args:
timeout: timeout in seconds
@@ -744,11 +836,30 @@
CommandTimeoutError on timeout.
DeviceUnreachableError on missing device.
"""
+ def is_launcher_focused():
+ output = self.RunShellCommand(['dumpsys', 'window', 'windows'],
+ check_return=True, large_output=True)
+ return any(self._LAUNCHER_FOCUSED_RE.match(l) for l in output)
+
+ def dismiss_popups():
+ # There is a dialog present; attempt to get rid of it.
+ # Not all dialogs can be dismissed with back.
+ self.SendKeyEvent(keyevent.KEYCODE_ENTER)
+ self.SendKeyEvent(keyevent.KEYCODE_BACK)
+ return is_launcher_focused()
+
+ # If Home is already focused, return early to avoid unnecessary work.
+ if is_launcher_focused():
+ return
+
self.StartActivity(
intent.Intent(action='android.intent.action.MAIN',
category='android.intent.category.HOME'),
blocking=True)
+ if not is_launcher_focused():
+ timeout_retry.WaitFor(dismiss_popups, wait_period=1)
+
@decorators.WithTimeoutAndRetriesFromInstance()
def ForceStop(self, package, timeout=None, retries=None):
"""Close the application.
@@ -782,7 +893,7 @@
# may never return.
if ((self.build_version_sdk >=
constants.ANDROID_SDK_VERSION_CODES.JELLY_BEAN_MR2)
- or self.GetApplicationPath(package)):
+ or self.GetApplicationPaths(package)):
self.RunShellCommand(['pm', 'clear', package], check_return=True)
@decorators.WithTimeoutAndRetriesFromInstance()
@@ -810,9 +921,14 @@
PUSH_CHANGED_FILES_DEFAULT_TIMEOUT,
PUSH_CHANGED_FILES_DEFAULT_RETRIES)
def PushChangedFiles(self, host_device_tuples, timeout=None,
- retries=None):
+ retries=None, delete_device_stale=False):
"""Push files to the device, skipping files that don't need updating.
+ When a directory is pushed, it is traversed recursively on the host and
+ all files in it are pushed to the device as needed.
+ Additionally, if delete_device_stale option is True,
+ files that exist on the device but don't exist on the host are deleted.
+
Args:
host_device_tuples: A list of (host_path, device_path) tuples, where
|host_path| is an absolute path of a file or directory on the host
@@ -820,6 +936,7 @@
an absolute path of the destination on the device.
timeout: timeout in seconds
retries: number of retries
+ delete_device_stale: option to delete stale files on device
Raises:
CommandFailedError on failure.
@@ -827,15 +944,72 @@
DeviceUnreachableError on missing device.
"""
- files = []
+ all_changed_files = []
+ all_stale_files = []
for h, d in host_device_tuples:
if os.path.isdir(h):
self.RunShellCommand(['mkdir', '-p', d], check_return=True)
- files += self._GetChangedFilesImpl(h, d)
+ (changed_files, stale_files) = self._GetChangedAndStaleFiles(h, d)
+ all_changed_files += changed_files
+ all_stale_files += stale_files
- if not files:
+ if delete_device_stale:
+ self.RunShellCommand(['rm', '-f'] + all_stale_files,
+ check_return=True)
+
+ if not all_changed_files:
return
+ self._PushFilesImpl(host_device_tuples, all_changed_files)
+
+ def _GetChangedAndStaleFiles(self, host_path, device_path):
+ """Get files to push and delete
+
+ Args:
+ host_path: an absolute path of a file or directory on the host
+ device_path: an absolute path of a file or directory on the device
+
+ Returns:
+ a two-element tuple
+ 1st element: a list of (host_files_path, device_files_path) tuples to push
+ 2nd element: a list of stale files under device_path
+ """
+ real_host_path = os.path.realpath(host_path)
+ try:
+ real_device_path = self.RunShellCommand(
+ ['realpath', device_path], single_line=True, check_return=True)
+ except device_errors.CommandFailedError:
+ real_device_path = None
+ if not real_device_path:
+ return ([(host_path, device_path)], [])
+
+ try:
+ host_checksums = md5sum.CalculateHostMd5Sums([real_host_path])
+ device_checksums = md5sum.CalculateDeviceMd5Sums(
+ [real_device_path], self)
+ except EnvironmentError as e:
+ logging.warning('Error calculating md5: %s', e)
+ return ([(host_path, device_path)], [])
+
+ if os.path.isfile(host_path):
+ host_checksum = host_checksums.get(real_host_path)
+ device_checksum = device_checksums.get(real_device_path)
+ if host_checksum != device_checksum:
+ return ([(host_path, device_path)], [])
+ else:
+ return ([], [])
+ else:
+ to_push = []
+ for host_abs_path, host_checksum in host_checksums.iteritems():
+ device_abs_path = '%s/%s' % (
+ real_device_path, os.path.relpath(host_abs_path, real_host_path))
+ device_checksum = device_checksums.pop(device_abs_path, None)
+ if device_checksum != host_checksum:
+ to_push.append((host_abs_path, device_abs_path))
+ to_delete = device_checksums.keys()
+ return (to_push, to_delete)
+
+ def _PushFilesImpl(self, host_device_tuples, files):
size = sum(host_utils.GetRecursiveDiskUsage(h) for h, _ in files)
file_count = len(files)
dir_size = sum(host_utils.GetRecursiveDiskUsage(h)
@@ -866,44 +1040,6 @@
['chmod', '-R', '777'] + [d for _, d in host_device_tuples],
as_root=True, check_return=True)
- def _GetChangedFilesImpl(self, host_path, device_path):
- real_host_path = os.path.realpath(host_path)
- try:
- real_device_path = self.RunShellCommand(
- ['realpath', device_path], single_line=True, check_return=True)
- except device_errors.CommandFailedError:
- real_device_path = None
- if not real_device_path:
- return [(host_path, device_path)]
-
- try:
- host_checksums = md5sum.CalculateHostMd5Sums([real_host_path])
- device_paths_to_md5 = (
- real_device_path if os.path.isfile(real_host_path)
- else ('%s/%s' % (real_device_path, os.path.relpath(p, real_host_path))
- for p in host_checksums.iterkeys()))
- device_checksums = md5sum.CalculateDeviceMd5Sums(
- device_paths_to_md5, self)
- except EnvironmentError as e:
- logging.warning('Error calculating md5: %s', e)
- return [(host_path, device_path)]
-
- if os.path.isfile(host_path):
- host_checksum = host_checksums.get(real_host_path)
- device_checksum = device_checksums.get(real_device_path)
- if host_checksum != device_checksum:
- return [(host_path, device_path)]
- else:
- return []
- else:
- to_push = []
- for host_abs_path, host_checksum in host_checksums.iteritems():
- device_abs_path = '%s/%s' % (
- real_device_path, os.path.relpath(host_abs_path, real_host_path))
- if (device_checksums.get(device_abs_path) != host_checksum):
- to_push.append((host_abs_path, device_abs_path))
- return to_push
-
def _InstallCommands(self):
if self._commands_installed is None:
try:
@@ -1244,6 +1380,29 @@
else:
return False
+ @property
+ def language(self):
+ """Returns the language setting on the device."""
+ return self.GetProp('persist.sys.language', cache=False)
+
+ @property
+ def country(self):
+ """Returns the country setting on the device."""
+ return self.GetProp('persist.sys.country', cache=False)
+
+ @property
+ def screen_density(self):
+ """Returns the screen density of the device."""
+ DPI_TO_DENSITY = {
+ 120: 'ldpi',
+ 160: 'mdpi',
+ 240: 'hdpi',
+ 320: 'xhdpi',
+ 480: 'xxhdpi',
+ 640: 'xxxhdpi',
+ }
+ dpi = int(self.GetProp('ro.sf.lcd_density', cache=True))
+ return DPI_TO_DENSITY.get(dpi, 'tvdpi')
@property
def build_description(self):
@@ -1585,4 +1744,3 @@
return [cls(adb) for adb in adb_wrapper.AdbWrapper.Devices()
if not blacklisted(adb)]
-
diff --git a/build/android/pylib/device/device_utils_device_test.py b/build/android/pylib/device/device_utils_device_test.py
new file mode 100755
index 0000000..daae2b6
--- /dev/null
+++ b/build/android/pylib/device/device_utils_device_test.py
@@ -0,0 +1,211 @@
+#!/usr/bin/env python
+# Copyright 2015 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.
+
+"""
+Unit tests for the contents of device_utils.py (mostly DeviceUtils).
+The test will invoke real devices
+"""
+
+import os
+import tempfile
+import unittest
+
+from pylib import cmd_helper
+from pylib import constants
+from pylib.device import adb_wrapper
+from pylib.device import device_utils
+from pylib.utils import md5sum
+
+_OLD_CONTENTS = "foo"
+_NEW_CONTENTS = "bar"
+_DEVICE_DIR = "/data/local/tmp/device_utils_test"
+_SUB_DIR = "sub"
+_SUB_DIR1 = "sub1"
+_SUB_DIR2 = "sub2"
+
+class DeviceUtilsPushDeleteFilesTest(unittest.TestCase):
+
+ def setUp(self):
+ devices = adb_wrapper.AdbWrapper.Devices()
+ assert devices, 'A device must be attached'
+ self.adb = devices[0]
+ self.adb.WaitForDevice()
+ self.device = device_utils.DeviceUtils(
+ self.adb, default_timeout=10, default_retries=0)
+ default_build_type = os.environ.get('BUILDTYPE', 'Debug')
+ constants.SetBuildType(default_build_type)
+
+ @staticmethod
+ def _MakeTempFile(contents):
+ """Make a temporary file with the given contents.
+
+ Args:
+ contents: string to write to the temporary file.
+
+ Returns:
+ the tuple contains the absolute path to the file and the file name
+ """
+ fi, path = tempfile.mkstemp(text=True)
+ with os.fdopen(fi, 'w') as f:
+ f.write(contents)
+ file_name = os.path.basename(path)
+ return (path, file_name)
+
+ @staticmethod
+ def _MakeTempFileGivenDir(directory, contents):
+ """Make a temporary file under the given directory
+ with the given contents
+
+ Args:
+ directory: the temp directory to create the file
+ contents: string to write to the temp file
+
+ Returns:
+ the list contains the absolute path to the file and the file name
+ """
+ fi, path = tempfile.mkstemp(dir=directory, text=True)
+ with os.fdopen(fi, 'w') as f:
+ f.write(contents)
+ file_name = os.path.basename(path)
+ return (path, file_name)
+
+ @staticmethod
+ def _ChangeTempFile(path, contents):
+ with os.open(path, 'w') as f:
+ f.write(contents)
+
+ @staticmethod
+ def _DeleteTempFile(path):
+ os.remove(path)
+
+ def testPushChangedFiles_noFileChange(self):
+ (host_file_path, file_name) = self._MakeTempFile(_OLD_CONTENTS)
+ device_file_path = "%s/%s" % (_DEVICE_DIR, file_name)
+ self.adb.Push(host_file_path, device_file_path)
+ self.device.PushChangedFiles([(host_file_path, device_file_path)])
+ result = self.device.RunShellCommand(['cat', device_file_path],
+ single_line=True)
+ self.assertEqual(_OLD_CONTENTS, result)
+
+ cmd_helper.RunCmd(['rm', host_file_path])
+ self.device.RunShellCommand(['rm', '-rf', _DEVICE_DIR])
+
+ def testPushChangedFiles_singleFileChange(self):
+ (host_file_path, file_name) = self._MakeTempFile(_OLD_CONTENTS)
+ device_file_path = "%s/%s" % (_DEVICE_DIR, file_name)
+ self.adb.Push(host_file_path, device_file_path)
+
+ with open(host_file_path, 'w') as f:
+ f.write(_NEW_CONTENTS)
+ self.device.PushChangedFiles([(host_file_path, device_file_path)])
+ result = self.device.RunShellCommand(['cat', device_file_path],
+ single_line=True)
+ self.assertEqual(_NEW_CONTENTS, result)
+
+ cmd_helper.RunCmd(['rm', host_file_path])
+ self.device.RunShellCommand(['rm', '-rf', _DEVICE_DIR])
+
+ def testDeleteFiles(self):
+ host_tmp_dir = tempfile.mkdtemp()
+ (host_file_path, file_name) = self._MakeTempFileGivenDir(
+ host_tmp_dir, _OLD_CONTENTS)
+
+ device_file_path = "%s/%s" % (_DEVICE_DIR, file_name)
+ self.adb.Push(host_file_path, device_file_path)
+
+ cmd_helper.RunCmd(['rm', host_file_path])
+ self.device.PushChangedFiles([(host_tmp_dir, _DEVICE_DIR)],
+ delete_device_stale=True)
+ result = self.device.RunShellCommand(['ls', _DEVICE_DIR], single_line=True)
+ self.assertEqual('', result)
+
+ cmd_helper.RunCmd(['rm', '-rf', host_tmp_dir])
+ self.device.RunShellCommand(['rm', '-rf', _DEVICE_DIR])
+
+ def testPushAndDeleteFiles_noSubDir(self):
+ host_tmp_dir = tempfile.mkdtemp()
+ (host_file_path1, file_name1) = self._MakeTempFileGivenDir(
+ host_tmp_dir, _OLD_CONTENTS)
+ (host_file_path2, file_name2) = self._MakeTempFileGivenDir(
+ host_tmp_dir, _OLD_CONTENTS)
+
+ device_file_path1 = "%s/%s" % (_DEVICE_DIR, file_name1)
+ device_file_path2 = "%s/%s" % (_DEVICE_DIR, file_name2)
+ self.adb.Push(host_file_path1, device_file_path1)
+ self.adb.Push(host_file_path2, device_file_path2)
+
+ with open(host_file_path1, 'w') as f:
+ f.write(_NEW_CONTENTS)
+ cmd_helper.RunCmd(['rm', host_file_path2])
+
+ self.device.PushChangedFiles([(host_tmp_dir, _DEVICE_DIR)],
+ delete_device_stale=True)
+ result = self.device.RunShellCommand(['cat', device_file_path1],
+ single_line=True)
+ self.assertEqual(_NEW_CONTENTS, result)
+ result = self.device.RunShellCommand(['ls', _DEVICE_DIR], single_line=True)
+ self.assertEqual(file_name1, result)
+
+ self.device.RunShellCommand(['rm', '-rf', _DEVICE_DIR])
+ cmd_helper.RunCmd(['rm', '-rf', host_tmp_dir])
+
+ def testPushAndDeleteFiles_SubDir(self):
+ host_tmp_dir = tempfile.mkdtemp()
+ host_sub_dir1 = "%s/%s" % (host_tmp_dir, _SUB_DIR1)
+ host_sub_dir2 = "%s/%s/%s" % (host_tmp_dir, _SUB_DIR, _SUB_DIR2)
+ cmd_helper.RunCmd(['mkdir', '-p', host_sub_dir1])
+ cmd_helper.RunCmd(['mkdir', '-p', host_sub_dir2])
+
+ (host_file_path1, file_name1) = self._MakeTempFileGivenDir(
+ host_tmp_dir, _OLD_CONTENTS)
+ (host_file_path2, file_name2) = self._MakeTempFileGivenDir(
+ host_tmp_dir, _OLD_CONTENTS)
+ (host_file_path3, file_name3) = self._MakeTempFileGivenDir(
+ host_sub_dir1, _OLD_CONTENTS)
+ (host_file_path4, file_name4) = self._MakeTempFileGivenDir(
+ host_sub_dir2, _OLD_CONTENTS)
+
+ device_file_path1 = "%s/%s" % (_DEVICE_DIR, file_name1)
+ device_file_path2 = "%s/%s" % (_DEVICE_DIR, file_name2)
+ device_file_path3 = "%s/%s/%s" % (_DEVICE_DIR, _SUB_DIR1, file_name3)
+ device_file_path4 = "%s/%s/%s/%s" % (_DEVICE_DIR, _SUB_DIR,
+ _SUB_DIR2, file_name4)
+
+ self.adb.Push(host_file_path1, device_file_path1)
+ self.adb.Push(host_file_path2, device_file_path2)
+ self.adb.Push(host_file_path3, device_file_path3)
+ self.adb.Push(host_file_path4, device_file_path4)
+
+ with open(host_file_path1, 'w') as f:
+ f.write(_NEW_CONTENTS)
+ cmd_helper.RunCmd(['rm', host_file_path2])
+ cmd_helper.RunCmd(['rm', host_file_path4])
+
+ self.device.PushChangedFiles([(host_tmp_dir, _DEVICE_DIR)],
+ delete_device_stale=True)
+ result = self.device.RunShellCommand(['cat', device_file_path1],
+ single_line=True)
+ self.assertEqual(_NEW_CONTENTS, result)
+
+ result = self.device.RunShellCommand(['ls', _DEVICE_DIR])
+ self.assertIn(file_name1, result)
+ self.assertIn(_SUB_DIR1, result)
+ self.assertIn(_SUB_DIR, result)
+ self.assertEqual(3, len(result))
+
+ result = self.device.RunShellCommand(['cat', device_file_path3],
+ single_line=True)
+ self.assertEqual(_OLD_CONTENTS, result)
+
+ result = self.device.RunShellCommand(["ls", "%s/%s/%s"
+ % (_DEVICE_DIR, _SUB_DIR, _SUB_DIR2)],
+ single_line=True)
+ self.assertEqual('', result)
+
+ self.device.RunShellCommand(['rm', '-rf', _DEVICE_DIR])
+ cmd_helper.RunCmd(['rm', '-rf', host_tmp_dir])
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/build/android/pylib/device/device_utils_test.py b/build/android/pylib/device/device_utils_test.py
index 317e81e..6699673 100755
--- a/build/android/pylib/device/device_utils_test.py
+++ b/build/android/pylib/device/device_utils_test.py
@@ -27,6 +27,7 @@
from pylib.device import device_errors
from pylib.device import device_utils
from pylib.device import intent
+from pylib.sdk import split_select
from pylib.utils import mock_calls
# RunCommand from third_party/android_testrunner/run_command.py is mocked
@@ -310,30 +311,49 @@
self.device.GetExternalStoragePath()
-class DeviceUtilsGetApplicationPathTest(DeviceUtilsTest):
+class DeviceUtilsGetApplicationPathsTest(DeviceUtilsTest):
- def testGetApplicationPath_exists(self):
+ def testGetApplicationPaths_exists(self):
with self.assertCalls(
(self.call.adb.Shell('getprop ro.build.version.sdk'), '19\n'),
(self.call.adb.Shell('pm path android'),
'package:/path/to/android.apk\n')):
- self.assertEquals('/path/to/android.apk',
- self.device.GetApplicationPath('android'))
+ self.assertEquals(['/path/to/android.apk'],
+ self.device.GetApplicationPaths('android'))
- def testGetApplicationPath_notExists(self):
+ def testGetApplicationPaths_notExists(self):
with self.assertCalls(
(self.call.adb.Shell('getprop ro.build.version.sdk'), '19\n'),
(self.call.adb.Shell('pm path not.installed.app'), '')):
- self.assertEquals(None,
- self.device.GetApplicationPath('not.installed.app'))
+ self.assertEquals([],
+ self.device.GetApplicationPaths('not.installed.app'))
- def testGetApplicationPath_fails(self):
+ def testGetApplicationPaths_fails(self):
with self.assertCalls(
(self.call.adb.Shell('getprop ro.build.version.sdk'), '19\n'),
(self.call.adb.Shell('pm path android'),
self.CommandError('ERROR. Is package manager running?\n'))):
with self.assertRaises(device_errors.CommandFailedError):
- self.device.GetApplicationPath('android')
+ self.device.GetApplicationPaths('android')
+
+
+class DeviceUtilsGetApplicationDataDirectoryTest(DeviceUtilsTest):
+
+ def testGetApplicationDataDirectory_exists(self):
+ with self.assertCall(
+ self.call.device._RunPipedShellCommand(
+ 'pm dump foo.bar.baz | grep dataDir='),
+ ['dataDir=/data/data/foo.bar.baz']):
+ self.assertEquals(
+ '/data/data/foo.bar.baz',
+ self.device.GetApplicationDataDirectory('foo.bar.baz'))
+
+ def testGetApplicationDataDirectory_notExists(self):
+ with self.assertCall(
+ self.call.device._RunPipedShellCommand(
+ 'pm dump foo.bar.baz | grep dataDir='),
+ self.ShellError()):
+ self.assertIsNone(self.device.GetApplicationDataDirectory('foo.bar.baz'))
@mock.patch('time.sleep', mock.Mock())
@@ -346,8 +366,8 @@
(self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
(self.call.adb.Shell('test -d /fake/storage/path'), ''),
# pm_ready
- (self.call.device.GetApplicationPath('android'),
- 'package:/some/fake/path'),
+ (self.call.device.GetApplicationPaths('android'),
+ ['package:/some/fake/path']),
# boot_completed
(self.call.device.GetProp('sys.boot_completed'), '1')):
self.device.WaitUntilFullyBooted(wifi=False)
@@ -359,8 +379,8 @@
(self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
(self.call.adb.Shell('test -d /fake/storage/path'), ''),
# pm_ready
- (self.call.device.GetApplicationPath('android'),
- 'package:/some/fake/path'),
+ (self.call.device.GetApplicationPaths('android'),
+ ['package:/some/fake/path']),
# boot_completed
(self.call.device.GetProp('sys.boot_completed'), '1'),
# wifi_enabled
@@ -383,8 +403,8 @@
(self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
(self.call.adb.Shell('test -d /fake/storage/path'), ''),
# pm_ready
- (self.call.device.GetApplicationPath('android'),
- 'package:/some/fake/path'),
+ (self.call.device.GetApplicationPaths('android'),
+ ['package:/some/fake/path']),
# boot_completed
(self.call.device.GetProp('sys.boot_completed'), '1')):
self.device.WaitUntilFullyBooted(wifi=False)
@@ -420,11 +440,11 @@
(self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
(self.call.adb.Shell('test -d /fake/storage/path'), ''),
# pm_ready
- (self.call.device.GetApplicationPath('android'), self.CommandError()),
+ (self.call.device.GetApplicationPaths('android'), self.CommandError()),
# pm_ready
- (self.call.device.GetApplicationPath('android'), self.CommandError()),
+ (self.call.device.GetApplicationPaths('android'), self.CommandError()),
# pm_ready
- (self.call.device.GetApplicationPath('android'), self.TimeoutError())):
+ (self.call.device.GetApplicationPaths('android'), self.TimeoutError())):
with self.assertRaises(device_errors.CommandTimeoutError):
self.device.WaitUntilFullyBooted(wifi=False)
@@ -435,8 +455,8 @@
(self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
(self.call.adb.Shell('test -d /fake/storage/path'), ''),
# pm_ready
- (self.call.device.GetApplicationPath('android'),
- 'package:/some/fake/path'),
+ (self.call.device.GetApplicationPaths('android'),
+ ['package:/some/fake/path']),
# boot_completed
(self.call.device.GetProp('sys.boot_completed'), '0'),
# boot_completed
@@ -453,8 +473,8 @@
(self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
(self.call.adb.Shell('test -d /fake/storage/path'), ''),
# pm_ready
- (self.call.device.GetApplicationPath('android'),
- 'package:/some/fake/path'),
+ (self.call.device.GetApplicationPaths('android'),
+ ['package:/some/fake/path']),
# boot_completed
(self.call.device.GetProp('sys.boot_completed'), '1'),
# wifi_enabled
@@ -500,7 +520,7 @@
with self.assertCalls(
(mock.call.pylib.utils.apk_helper.GetPackageName('/fake/test/app.apk'),
'this.is.a.test.package'),
- (self.call.device.GetApplicationPath('this.is.a.test.package'), None),
+ (self.call.device.GetApplicationPaths('this.is.a.test.package'), []),
self.call.adb.Install('/fake/test/app.apk', reinstall=False)):
self.device.Install('/fake/test/app.apk', retries=0)
@@ -508,11 +528,12 @@
with self.assertCalls(
(mock.call.pylib.utils.apk_helper.GetPackageName('/fake/test/app.apk'),
'this.is.a.test.package'),
- (self.call.device.GetApplicationPath('this.is.a.test.package'),
- '/fake/data/app/this.is.a.test.package.apk'),
- (self.call.device._GetChangedFilesImpl(
+ (self.call.device.GetApplicationPaths('this.is.a.test.package'),
+ ['/fake/data/app/this.is.a.test.package.apk']),
+ (self.call.device._GetChangedAndStaleFiles(
'/fake/test/app.apk', '/fake/data/app/this.is.a.test.package.apk'),
- [('/fake/test/app.apk', '/fake/data/app/this.is.a.test.package.apk')]),
+ ([('/fake/test/app.apk', '/fake/data/app/this.is.a.test.package.apk')],
+ [])),
self.call.adb.Uninstall('this.is.a.test.package'),
self.call.adb.Install('/fake/test/app.apk', reinstall=False)):
self.device.Install('/fake/test/app.apk', retries=0)
@@ -521,11 +542,12 @@
with self.assertCalls(
(mock.call.pylib.utils.apk_helper.GetPackageName('/fake/test/app.apk'),
'this.is.a.test.package'),
- (self.call.device.GetApplicationPath('this.is.a.test.package'),
- '/fake/data/app/this.is.a.test.package.apk'),
- (self.call.device._GetChangedFilesImpl(
+ (self.call.device.GetApplicationPaths('this.is.a.test.package'),
+ ['/fake/data/app/this.is.a.test.package.apk']),
+ (self.call.device._GetChangedAndStaleFiles(
'/fake/test/app.apk', '/fake/data/app/this.is.a.test.package.apk'),
- [('/fake/test/app.apk', '/fake/data/app/this.is.a.test.package.apk')]),
+ ([('/fake/test/app.apk', '/fake/data/app/this.is.a.test.package.apk')],
+ [])),
self.call.adb.Install('/fake/test/app.apk', reinstall=True)):
self.device.Install('/fake/test/app.apk', reinstall=True, retries=0)
@@ -533,23 +555,62 @@
with self.assertCalls(
(mock.call.pylib.utils.apk_helper.GetPackageName('/fake/test/app.apk'),
'this.is.a.test.package'),
- (self.call.device.GetApplicationPath('this.is.a.test.package'),
- '/fake/data/app/this.is.a.test.package.apk'),
- (self.call.device._GetChangedFilesImpl(
+ (self.call.device.GetApplicationPaths('this.is.a.test.package'),
+ ['/fake/data/app/this.is.a.test.package.apk']),
+ (self.call.device._GetChangedAndStaleFiles(
'/fake/test/app.apk', '/fake/data/app/this.is.a.test.package.apk'),
- [])):
+ ([], []))):
self.device.Install('/fake/test/app.apk', retries=0)
def testInstall_fails(self):
with self.assertCalls(
(mock.call.pylib.utils.apk_helper.GetPackageName('/fake/test/app.apk'),
'this.is.a.test.package'),
- (self.call.device.GetApplicationPath('this.is.a.test.package'), None),
+ (self.call.device.GetApplicationPaths('this.is.a.test.package'), []),
(self.call.adb.Install('/fake/test/app.apk', reinstall=False),
self.CommandError('Failure\r\n'))):
with self.assertRaises(device_errors.CommandFailedError):
self.device.Install('/fake/test/app.apk', retries=0)
+class DeviceUtilsInstallSplitApkTest(DeviceUtilsTest):
+
+ def testInstallSplitApk_noPriorInstall(self):
+ with self.assertCalls(
+ (self.call.device._CheckSdkLevel(21)),
+ (mock.call.pylib.sdk.split_select.SelectSplits(
+ self.device, 'base.apk',
+ ['split1.apk', 'split2.apk', 'split3.apk']),
+ ['split2.apk']),
+ (mock.call.pylib.utils.apk_helper.GetPackageName('base.apk'),
+ 'this.is.a.test.package'),
+ (self.call.device.GetApplicationPaths('this.is.a.test.package'), []),
+ (self.call.adb.InstallMultiple(
+ ['base.apk', 'split2.apk'], partial=None, reinstall=False))):
+ self.device.InstallSplitApk('base.apk',
+ ['split1.apk', 'split2.apk', 'split3.apk'], retries=0)
+
+ def testInstallSplitApk_partialInstall(self):
+ with self.assertCalls(
+ (self.call.device._CheckSdkLevel(21)),
+ (mock.call.pylib.sdk.split_select.SelectSplits(
+ self.device, 'base.apk',
+ ['split1.apk', 'split2.apk', 'split3.apk']),
+ ['split2.apk']),
+ (mock.call.pylib.utils.apk_helper.GetPackageName('base.apk'),
+ 'test.package'),
+ (self.call.device.GetApplicationPaths('test.package'),
+ ['base-on-device.apk', 'split2-on-device.apk']),
+ (mock.call.pylib.utils.md5sum.CalculateDeviceMd5Sums(
+ ['base-on-device.apk', 'split2-on-device.apk'], self.device),
+ {'base-on-device.apk': 'AAA', 'split2-on-device.apk': 'BBB'}),
+ (mock.call.pylib.utils.md5sum.CalculateHostMd5Sums(
+ ['base.apk', 'split2.apk']),
+ {'base.apk': 'AAA', 'split2.apk': 'CCC'}),
+ (self.call.adb.InstallMultiple(
+ ['split2.apk'], partial='test.package', reinstall=True))):
+ self.device.InstallSplitApk('base.apk',
+ ['split1.apk', 'split2.apk', 'split3.apk'], reinstall=True, retries=0)
+
class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
@@ -972,7 +1033,7 @@
with self.assertCalls(
self.call.device.RunShellCommand(
['am', 'instrument', 'test.package/.TestInstrumentation'],
- check_return=True)):
+ check_return=True, large_output=True)):
self.device.StartInstrumentation(
'test.package/.TestInstrumentation',
finish=False, raw=False, extras=None)
@@ -981,7 +1042,7 @@
with self.assertCalls(
(self.call.device.RunShellCommand(
['am', 'instrument', '-w', 'test.package/.TestInstrumentation'],
- check_return=True),
+ check_return=True, large_output=True),
['OK (1 test)'])):
output = self.device.StartInstrumentation(
'test.package/.TestInstrumentation',
@@ -992,7 +1053,7 @@
with self.assertCalls(
self.call.device.RunShellCommand(
['am', 'instrument', '-r', 'test.package/.TestInstrumentation'],
- check_return=True)):
+ check_return=True, large_output=True)):
self.device.StartInstrumentation(
'test.package/.TestInstrumentation',
finish=False, raw=True, extras=None)
@@ -1002,7 +1063,7 @@
self.call.device.RunShellCommand(
['am', 'instrument', '-e', 'foo', 'Foo', '-e', 'bar', 'Bar',
'test.package/.TestInstrumentation'],
- check_return=True)):
+ check_return=True, large_output=True)):
self.device.StartInstrumentation(
'test.package/.TestInstrumentation',
finish=False, raw=False, extras={'foo': 'Foo', 'bar': 'Bar'})
@@ -1038,13 +1099,88 @@
class DeviceUtilsGoHomeTest(DeviceUtilsTest):
- def testGoHome(self):
- with self.assertCall(
- self.call.adb.Shell('am start -W -a android.intent.action.MAIN '
- '-c android.intent.category.HOME'),
- 'Starting: Intent { act=android.intent.action.MAIN }\r\n'):
+ def testGoHome_popupsExist(self):
+ with self.assertCalls(
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'window', 'windows'], check_return=True,
+ large_output=True), []),
+ (self.call.device.RunShellCommand(
+ ['am', 'start', '-W', '-a', 'android.intent.action.MAIN',
+ '-c', 'android.intent.category.HOME'], check_return=True),
+ 'Starting: Intent { act=android.intent.action.MAIN }\r\n'''),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'window', 'windows'], check_return=True,
+ large_output=True), []),
+ (self.call.device.RunShellCommand(
+ ['input', 'keyevent', '66'], check_return=True)),
+ (self.call.device.RunShellCommand(
+ ['input', 'keyevent', '4'], check_return=True)),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'window', 'windows'], check_return=True,
+ large_output=True),
+ ['mCurrentFocus Launcher'])):
self.device.GoHome()
+ def testGoHome_willRetry(self):
+ with self.assertCalls(
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'window', 'windows'], check_return=True,
+ large_output=True), []),
+ (self.call.device.RunShellCommand(
+ ['am', 'start', '-W', '-a', 'android.intent.action.MAIN',
+ '-c', 'android.intent.category.HOME'], check_return=True),
+ 'Starting: Intent { act=android.intent.action.MAIN }\r\n'''),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'window', 'windows'], check_return=True,
+ large_output=True), []),
+ (self.call.device.RunShellCommand(
+ ['input', 'keyevent', '66'], check_return=True,)),
+ (self.call.device.RunShellCommand(
+ ['input', 'keyevent', '4'], check_return=True)),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'window', 'windows'], check_return=True,
+ large_output=True), []),
+ (self.call.device.RunShellCommand(
+ ['input', 'keyevent', '66'], check_return=True)),
+ (self.call.device.RunShellCommand(
+ ['input', 'keyevent', '4'], check_return=True)),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'window', 'windows'], check_return=True,
+ large_output=True),
+ self.TimeoutError())):
+ with self.assertRaises(device_errors.CommandTimeoutError):
+ self.device.GoHome()
+
+ def testGoHome_alreadyFocused(self):
+ with self.assertCall(
+ self.call.device.RunShellCommand(
+ ['dumpsys', 'window', 'windows'], check_return=True,
+ large_output=True),
+ ['mCurrentFocus Launcher']):
+ self.device.GoHome()
+
+ def testGoHome_alreadyFocusedAlternateCase(self):
+ with self.assertCall(
+ self.call.device.RunShellCommand(
+ ['dumpsys', 'window', 'windows'], check_return=True,
+ large_output=True),
+ [' mCurrentFocus .launcher/.']):
+ self.device.GoHome()
+
+ def testGoHome_obtainsFocusAfterGoingHome(self):
+ with self.assertCalls(
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'window', 'windows'], check_return=True,
+ large_output=True), []),
+ (self.call.device.RunShellCommand(
+ ['am', 'start', '-W', '-a', 'android.intent.action.MAIN',
+ '-c', 'android.intent.category.HOME'], check_return=True),
+ 'Starting: Intent { act=android.intent.action.MAIN }\r\n'''),
+ (self.call.device.RunShellCommand(
+ ['dumpsys', 'window', 'windows'], check_return=True,
+ large_output=True),
+ ['mCurrentFocus Launcher'])):
+ self.device.GoHome()
class DeviceUtilsForceStopTest(DeviceUtilsTest):
@@ -1060,8 +1196,8 @@
def testClearApplicationState_packageDoesntExist(self):
with self.assertCalls(
(self.call.adb.Shell('getprop ro.build.version.sdk'), '17\n'),
- (self.call.device.GetApplicationPath('this.package.does.not.exist'),
- None)):
+ (self.call.device.GetApplicationPaths('this.package.does.not.exist'),
+ [])):
self.device.ClearApplicationState('this.package.does.not.exist')
def testClearApplicationState_packageDoesntExistOnAndroidJBMR2OrAbove(self):
@@ -1074,8 +1210,8 @@
def testClearApplicationState_packageExists(self):
with self.assertCalls(
(self.call.adb.Shell('getprop ro.build.version.sdk'), '17\n'),
- (self.call.device.GetApplicationPath('this.package.exists'),
- '/data/app/this.package.exists.apk'),
+ (self.call.device.GetApplicationPaths('this.package.exists'),
+ ['/data/app/this.package.exists.apk']),
(self.call.adb.Shell('pm clear this.package.exists'),
'Success\r\n')):
self.device.ClearApplicationState('this.package.exists')
diff --git a/build/android/pylib/gtest/filter/cc_unittests_disabled b/build/android/pylib/gtest/filter/cc_unittests_disabled
index feab5ac..b49d2c6 100644
--- a/build/android/pylib/gtest/filter/cc_unittests_disabled
+++ b/build/android/pylib/gtest/filter/cc_unittests_disabled
@@ -1,5 +1,5 @@
# Death tests are not supported with apks.
-BeginFrameObserverMixInTest.OnBeginFrameImplementation
-BeginFrameSourceMixInTest.ObserverManipulation
+BeginFrameObserverBaseTest.OnBeginFrameImplementation
+BeginFrameSourceBaseTest.ObserverManipulation
BeginFrameSourceMultiplexerTest.SourcesManipulation
BeginFrameSourceMultiplexerTest.MinimumIntervalNegativeFails
diff --git a/build/android/pylib/gtest/gtest_test_instance.py b/build/android/pylib/gtest/gtest_test_instance.py
index cea5016..3285e0b 100644
--- a/build/android/pylib/gtest/gtest_test_instance.py
+++ b/build/android/pylib/gtest/gtest_test_instance.py
@@ -7,10 +7,12 @@
import re
import shutil
import sys
+import tempfile
from pylib import constants
from pylib.base import base_test_result
from pylib.base import test_instance
+from pylib.utils import apk_helper
sys.path.append(os.path.join(
constants.DIR_SOURCE_ROOT, 'build', 'util', 'lib', 'common'))
@@ -23,6 +25,29 @@
]
+_DEFAULT_ISOLATE_FILE_PATHS = {
+ 'base_unittests': 'base/base_unittests.isolate',
+ 'blink_heap_unittests':
+ 'third_party/WebKit/Source/platform/heap/BlinkHeapUnitTests.isolate',
+ 'breakpad_unittests': 'breakpad/breakpad_unittests.isolate',
+ 'cc_perftests': 'cc/cc_perftests.isolate',
+ 'components_browsertests': 'components/components_browsertests.isolate',
+ 'components_unittests': 'components/components_unittests.isolate',
+ 'content_browsertests': 'content/content_browsertests.isolate',
+ 'content_unittests': 'content/content_unittests.isolate',
+ 'media_perftests': 'media/media_perftests.isolate',
+ 'media_unittests': 'media/media_unittests.isolate',
+ 'midi_unittests': 'media/midi/midi_unittests.isolate',
+ 'net_unittests': 'net/net_unittests.isolate',
+ 'sql_unittests': 'sql/sql_unittests.isolate',
+ 'sync_unit_tests': 'sync/sync_unit_tests.isolate',
+ 'ui_base_unittests': 'ui/base/ui_base_tests.isolate',
+ 'unit_tests': 'chrome/unit_tests.isolate',
+ 'webkit_unit_tests':
+ 'third_party/WebKit/Source/web/WebKitUnitTests.isolate',
+}
+
+
# Used for filtering large data deps at a finer grain than what's allowed in
# isolate files since pushing deps to devices is expensive.
# Wildcards are allowed.
@@ -46,6 +71,13 @@
]
+_EXTRA_NATIVE_TEST_ACTIVITY = (
+ 'org.chromium.native_test.NativeTestInstrumentationTestRunner.'
+ 'NativeTestActivity')
+_EXTRA_SHARD_SIZE_LIMIT =(
+ 'org.chromium.native_test.NativeTestInstrumentationTestRunner.'
+ 'ShardSizeLimit')
+
# TODO(jbudorick): Remove these once we're no longer parsing stdout to generate
# results.
_RE_TEST_STATUS = re.compile(
@@ -105,6 +137,20 @@
self._suite)
if not os.path.exists(self._apk_path):
self._apk_path = None
+ self._activity = None
+ self._package = None
+ self._runner = None
+ else:
+ helper = apk_helper.ApkHelper(self._apk_path)
+ self._activity = helper.GetActivityName()
+ self._package = helper.GetPackageName()
+ self._runner = helper.GetInstrumentationName()
+ self._extras = {
+ _EXTRA_NATIVE_TEST_ACTIVITY: self._activity,
+ }
+ if self._suite in BROWSER_TEST_SUITES:
+ self._extras[_EXTRA_SHARD_SIZE_LIMIT] = 1
+
if not os.path.exists(self._exe_path):
self._exe_path = None
if not self._apk_path and not self._exe_path:
@@ -118,6 +164,13 @@
self._gtest_filter = ':'.join(l.strip() for l in f)
else:
self._gtest_filter = None
+
+ if not args.isolate_file_path:
+ default_isolate_file_path = _DEFAULT_ISOLATE_FILE_PATHS.get(self._suite)
+ if default_isolate_file_path:
+ args.isolate_file_path = os.path.join(
+ constants.DIR_SOURCE_ROOT, default_isolate_file_path)
+
if args.isolate_file_path:
self._isolate_abs_path = os.path.abspath(args.isolate_file_path)
self._isolate_delegate = isolate_delegate
@@ -127,6 +180,17 @@
logging.warning('No isolate file provided. No data deps will be pushed.');
self._isolate_delegate = None
+ if args.app_data_files:
+ self._app_data_files = args.app_data_files
+ if args.app_data_file_dir:
+ self._app_data_file_dir = args.app_data_file_dir
+ else:
+ self._app_data_file_dir = tempfile.mkdtemp()
+ logging.critical('Saving app files to %s', self._app_data_file_dir)
+ else:
+ self._app_data_files = None
+ self._app_data_file_dir = None
+
#override
def TestType(self):
return 'gtest'
@@ -228,14 +292,38 @@
self._isolate_delegate.Clear()
@property
+ def activity(self):
+ return self._activity
+
+ @property
def apk(self):
return self._apk_path
@property
+ def app_file_dir(self):
+ return self._app_data_file_dir
+
+ @property
+ def app_files(self):
+ return self._app_data_files
+
+ @property
def exe(self):
return self._exe_path
@property
+ def extras(self):
+ return self._extras
+
+ @property
+ def package(self):
+ return self._package
+
+ @property
+ def runner(self):
+ return self._runner
+
+ @property
def suite(self):
return self._suite
diff --git a/build/android/pylib/gtest/local_device_gtest_run.py b/build/android/pylib/gtest/local_device_gtest_run.py
index 15a58a4..f1cea4e 100644
--- a/build/android/pylib/gtest/local_device_gtest_run.py
+++ b/build/android/pylib/gtest/local_device_gtest_run.py
@@ -2,9 +2,10 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
-
+import itertools
import logging
import os
+import posixpath
from pylib import constants
from pylib import ports
@@ -15,7 +16,6 @@
from pylib.local import local_test_server_spawner
from pylib.local.device import local_device_environment
from pylib.local.device import local_device_test_run
-from pylib.utils import apk_helper
from pylib.utils import device_temp_file
_COMMAND_LINE_FLAGS_SUPPORTED = True
@@ -24,9 +24,9 @@
'org.chromium.native_test.NativeTestActivity.CommandLineFile')
_EXTRA_COMMAND_LINE_FLAGS = (
'org.chromium.native_test.NativeTestActivity.CommandLineFlags')
-_EXTRA_NATIVE_TEST_ACTIVITY = (
+_EXTRA_TEST_LIST = (
'org.chromium.native_test.NativeTestInstrumentationTestRunner'
- '.NativeTestActivity')
+ '.TestList')
_MAX_SHARD_SIZE = 256
@@ -37,38 +37,57 @@
'net_unittests', 'unit_tests'
]
-class _ApkDelegate(object):
- def __init__(self, apk):
- self._apk = apk
+# TODO(jbudorick): Move this inside _ApkDelegate once TestPackageApk is gone.
+def PullAppFilesImpl(device, package, files, directory):
+ device_dir = device.GetApplicationDataDirectory(package)
+ host_dir = os.path.join(directory, str(device))
+ for f in files:
+ device_file = posixpath.join(device_dir, f)
+ host_file = os.path.join(host_dir, *f.split(posixpath.sep))
+ host_file_base, ext = os.path.splitext(host_file)
+ for i in itertools.count():
+ host_file = '%s_%d%s' % (host_file_base, i, ext)
+ if not os.path.exists(host_file):
+ break
+ device.PullFile(device_file, host_file)
- helper = apk_helper.ApkHelper(self._apk)
- self._activity = helper.GetActivityName()
- self._package = helper.GetPackageName()
- self._runner = helper.GetInstrumentationName()
+class _ApkDelegate(object):
+ def __init__(self, test_instance):
+ self._activity = test_instance.activity
+ self._apk = test_instance.apk
+ self._package = test_instance.package
+ self._runner = test_instance.runner
+
self._component = '%s/%s' % (self._package, self._runner)
- self._enable_test_server_spawner = False
+ self._extras = test_instance.extras
def Install(self, device):
device.Install(self._apk)
- def RunWithFlags(self, device, flags, **kwargs):
+ def Run(self, test, device, flags=None, **kwargs):
+ extras = dict(self._extras)
+
with device_temp_file.DeviceTempFile(device.adb) as command_line_file:
- device.WriteFile(command_line_file.name, '_ %s' % flags)
+ device.WriteFile(command_line_file.name, '_ %s' % flags if flags else '_')
+ extras[_EXTRA_COMMAND_LINE_FILE] = command_line_file.name
- extras = {
- _EXTRA_COMMAND_LINE_FILE: command_line_file.name,
- _EXTRA_NATIVE_TEST_ACTIVITY: self._activity,
- }
+ with device_temp_file.DeviceTempFile(device.adb) as test_list_file:
+ if test:
+ device.WriteFile(test_list_file.name, '\n'.join(test))
+ extras[_EXTRA_TEST_LIST] = test_list_file.name
- return device.StartInstrumentation(
- self._component, extras=extras, raw=False, **kwargs)
+ return device.StartInstrumentation(
+ self._component, extras=extras, raw=False, **kwargs)
+
+ def PullAppFiles(self, device, files, directory):
+ PullAppFilesImpl(device, self._package, files, directory)
def Clear(self, device):
device.ClearApplicationState(self._package)
class _ExeDelegate(object):
- def __init__(self, exe, tr):
+ def __init__(self, tr, exe):
self._exe_host_path = exe
self._exe_file_name = os.path.split(exe)[-1]
self._exe_device_path = '%s/%s' % (
@@ -89,12 +108,15 @@
host_device_tuples.append((self._deps_host_path, self._deps_device_path))
device.PushChangedFiles(host_device_tuples)
- def RunWithFlags(self, device, flags, **kwargs):
+ def Run(self, test, device, flags=None, **kwargs):
cmd = [
self._test_run.GetTool(device).GetTestWrapper(),
self._exe_device_path,
- flags,
]
+ if test:
+ cmd.append('--gtest_filter=%s' % ':'.join(test))
+ if flags:
+ cmd.append(flags)
cwd = constants.TEST_EXECUTABLE_DIR
env = {
@@ -119,6 +141,9 @@
env=env, **kwargs)
return output
+ def PullAppFiles(self, device, files, directory):
+ pass
+
def Clear(self, device):
device.KillAll(self._exe_file_name, blocking=True, timeout=30, quiet=True)
@@ -131,7 +156,7 @@
super(LocalDeviceGtestRun, self).__init__(env, test_instance)
if self._test_instance.apk:
- self._delegate = _ApkDelegate(self._test_instance.apk)
+ self._delegate = _ApkDelegate(self._test_instance)
elif self._test_instance.exe:
self._delegate = _ExeDelegate(self, self._test_instance.exe)
@@ -173,21 +198,18 @@
#override
def _CreateShards(self, tests):
- if self._test_instance.suite in gtest_test_instance.BROWSER_TEST_SUITES:
- return tests
- else:
- device_count = len(self._env.devices)
- shards = []
- for i in xrange(0, device_count):
- unbounded_shard = tests[i::device_count]
- shards += [unbounded_shard[j:j+_MAX_SHARD_SIZE]
- for j in xrange(0, len(unbounded_shard), _MAX_SHARD_SIZE)]
- return [':'.join(s) for s in shards]
+ device_count = len(self._env.devices)
+ shards = []
+ for i in xrange(0, device_count):
+ unbounded_shard = tests[i::device_count]
+ shards += [unbounded_shard[j:j+_MAX_SHARD_SIZE]
+ for j in xrange(0, len(unbounded_shard), _MAX_SHARD_SIZE)]
+ return shards
#override
def _GetTests(self):
- tests = self._delegate.RunWithFlags(
- self._env.devices[0], '--gtest_list_tests')
+ tests = self._delegate.Run(
+ None, self._env.devices[0], flags='--gtest_list_tests')
tests = gtest_test_instance.ParseGTestListTests(tests)
tests = self._test_instance.FilterTests(tests)
return tests
@@ -195,10 +217,13 @@
#override
def _RunTest(self, device, test):
# Run the test.
- output = self._delegate.RunWithFlags(
- device, '--gtest_filter=%s' % test, timeout=900, retries=0)
+ output = self._delegate.Run(
+ test, device, timeout=900, retries=0)
for s in self._servers[str(device)]:
s.Reset()
+ if self._test_instance.app_files:
+ self._delegate.PullAppFiles(device, self._test_instance.app_files,
+ self._test_instance.app_file_dir)
self._delegate.Clear(device)
# Parse the output.
diff --git a/build/android/pylib/gtest/setup.py b/build/android/pylib/gtest/setup.py
index 8ba9a8d..f563ccf 100644
--- a/build/android/pylib/gtest/setup.py
+++ b/build/android/pylib/gtest/setup.py
@@ -26,27 +26,8 @@
import unittest_util # pylint: disable=F0401
-ISOLATE_FILE_PATHS = {
- 'base_unittests': 'base/base_unittests.isolate',
- 'blink_heap_unittests':
- 'third_party/WebKit/Source/platform/heap/BlinkHeapUnitTests.isolate',
- 'breakpad_unittests': 'breakpad/breakpad_unittests.isolate',
- 'cc_perftests': 'cc/cc_perftests.isolate',
- 'components_browsertests': 'components/components_browsertests.isolate',
- 'components_unittests': 'components/components_unittests.isolate',
- 'content_browsertests': 'content/content_browsertests.isolate',
- 'content_unittests': 'content/content_unittests.isolate',
- 'media_perftests': 'media/media_perftests.isolate',
- 'media_unittests': 'media/media_unittests.isolate',
- 'midi_unittests': 'media/midi/midi_unittests.isolate',
- 'net_unittests': 'net/net_unittests.isolate',
- 'sql_unittests': 'sql/sql_unittests.isolate',
- 'sync_unit_tests': 'sync/sync_unit_tests.isolate',
- 'ui_base_unittests': 'ui/base/ui_base_tests.isolate',
- 'unit_tests': 'chrome/unit_tests.isolate',
- 'webkit_unit_tests':
- 'third_party/WebKit/Source/web/WebKitUnitTests.isolate',
-}
+ISOLATE_FILE_PATHS = gtest_test_instance._DEFAULT_ISOLATE_FILE_PATHS
+
# Used for filtering large data deps at a finer grain than what's allowed in
# isolate files since pushing deps to devices is expensive.
diff --git a/build/android/pylib/gtest/test_options.py b/build/android/pylib/gtest/test_options.py
index 58cd82b..8bc6996 100644
--- a/build/android/pylib/gtest/test_options.py
+++ b/build/android/pylib/gtest/test_options.py
@@ -13,4 +13,7 @@
'test_arguments',
'timeout',
'isolate_file_path',
- 'suite_name'])
+ 'suite_name',
+ 'app_data_files',
+ 'app_data_file_dir',
+ 'delete_stale_data'])
diff --git a/build/android/pylib/gtest/test_package.py b/build/android/pylib/gtest/test_package.py
index dbd47bf..4042a98 100644
--- a/build/android/pylib/gtest/test_package.py
+++ b/build/android/pylib/gtest/test_package.py
@@ -22,7 +22,7 @@
Args:
device: Instance of DeviceUtils.
"""
- raise NotImplementedError('Method must be overriden.')
+ raise NotImplementedError('Method must be overridden.')
def CreateCommandLineFileOnDevice(self, device, test_filter, test_arguments):
"""Creates a test runner script and pushes to the device.
@@ -32,7 +32,7 @@
test_filter: A test_filter flag.
test_arguments: Additional arguments to pass to the test binary.
"""
- raise NotImplementedError('Method must be overriden.')
+ raise NotImplementedError('Method must be overridden.')
def GetAllTests(self, device):
"""Returns a list of all tests available in the test suite.
@@ -40,7 +40,7 @@
Args:
device: Instance of DeviceUtils.
"""
- raise NotImplementedError('Method must be overriden.')
+ raise NotImplementedError('Method must be overridden.')
def GetGTestReturnCode(self, _device):
return None
@@ -54,7 +54,7 @@
Returns:
An instance of pexpect spawn class.
"""
- raise NotImplementedError('Method must be overriden.')
+ raise NotImplementedError('Method must be overridden.')
def Install(self, device):
"""Install the test package to the device.
@@ -62,5 +62,15 @@
Args:
device: Instance of DeviceUtils.
"""
- raise NotImplementedError('Method must be overriden.')
+ raise NotImplementedError('Method must be overridden.')
+ def PullAppFiles(self, device, files, directory):
+ """Pull application data from the device.
+
+ Args:
+ device: Instance of DeviceUtils.
+ files: A list of paths relative to the application data directory to
+ retrieve from the device.
+ directory: The host directory to which files should be pulled.
+ """
+ raise NotImplementedError('Method must be overridden.')
diff --git a/build/android/pylib/gtest/test_package_apk.py b/build/android/pylib/gtest/test_package_apk.py
index 6447820..16ef21c 100644
--- a/build/android/pylib/gtest/test_package_apk.py
+++ b/build/android/pylib/gtest/test_package_apk.py
@@ -5,8 +5,10 @@
"""Defines TestPackageApk to help run APK-based native tests."""
# pylint: disable=W0212
+import itertools
import logging
import os
+import posixpath
import shlex
import sys
import tempfile
@@ -18,6 +20,7 @@
from pylib.device import device_errors
from pylib.device import intent
from pylib.gtest import gtest_test_instance
+from pylib.gtest import local_device_gtest_run
from pylib.gtest.test_package import TestPackage
@@ -141,3 +144,8 @@
def Install(self, device):
self.tool.CopyFiles(device)
device.Install(self.suite_path)
+
+ #override
+ def PullAppFiles(self, device, files, directory):
+ local_device_gtest_run.PullAppFilesImpl(
+ device, self._package_info.package, files, directory)
diff --git a/build/android/pylib/gtest/test_package_exe.py b/build/android/pylib/gtest/test_package_exe.py
index 7cdcb99..87071b5 100644
--- a/build/android/pylib/gtest/test_package_exe.py
+++ b/build/android/pylib/gtest/test_package_exe.py
@@ -157,3 +157,7 @@
deps_path = self.suite_path + '_deps'
if os.path.isdir(deps_path):
device.PushChangedFiles([(deps_path, test_binary_path + '_deps')])
+
+ #override
+ def PullAppFiles(self, device, files, directory):
+ pass
diff --git a/build/android/pylib/gtest/test_runner.py b/build/android/pylib/gtest/test_runner.py
index 6d01990..a48f18a 100644
--- a/build/android/pylib/gtest/test_runner.py
+++ b/build/android/pylib/gtest/test_runner.py
@@ -5,6 +5,7 @@
import logging
import os
import re
+import tempfile
from pylib import pexpect
from pylib import ports
@@ -80,6 +81,17 @@
else:
self._servers = []
+ if test_options.app_data_files:
+ self._app_data_files = test_options.app_data_files
+ if test_options.app_data_file_dir:
+ self._app_data_file_dir = test_options.app_data_file_dir
+ else:
+ self._app_data_file_dir = tempfile.mkdtemp()
+ logging.critical('Saving app files to %s', self._app_data_file_dir)
+ else:
+ self._app_data_files = None
+ self._app_data_file_dir = None
+
#override
def InstallTestPackage(self):
self.test_package.Install(self.device)
@@ -167,6 +179,9 @@
self.device, test, self._test_arguments)
test_results = self._ParseTestOutput(
self.test_package.SpawnTestProcess(self.device))
+ if self._app_data_files:
+ self.test_package.PullAppFiles(self.device, self._app_data_files,
+ self._app_data_file_dir)
finally:
for s in self._servers:
s.Reset()
diff --git a/build/android/pylib/instrumentation/test_jar.py b/build/android/pylib/instrumentation/test_jar.py
index 9c38510..7ad8997 100644
--- a/build/android/pylib/instrumentation/test_jar.py
+++ b/build/android/pylib/instrumentation/test_jar.py
@@ -65,7 +65,8 @@
try:
with open(self._pickled_proguard_name, 'r') as r:
d = pickle.loads(r.read())
- jar_md5 = md5sum.CalculateHostMd5Sums(self._jar_path)[self._jar_path]
+ jar_md5 = md5sum.CalculateHostMd5Sums(
+ self._jar_path)[os.path.realpath(self._jar_path)]
if (d['JAR_MD5SUM'] == jar_md5 and
d['VERSION'] == PICKLE_FORMAT_VERSION):
self._test_methods = d['TEST_METHODS']
@@ -106,7 +107,8 @@
d = {'VERSION': PICKLE_FORMAT_VERSION,
'TEST_METHODS': self._test_methods,
'JAR_MD5SUM':
- md5sum.CalculateHostMd5Sums(self._jar_path)[self._jar_path]}
+ md5sum.CalculateHostMd5Sums(
+ self._jar_path)[os.path.realpath(self._jar_path)]}
with open(self._pickled_proguard_name, 'w') as f:
f.write(pickle.dumps(d))
diff --git a/build/android/pylib/instrumentation/test_options.py b/build/android/pylib/instrumentation/test_options.py
index 792010b..e7b7a9f 100644
--- a/build/android/pylib/instrumentation/test_options.py
+++ b/build/android/pylib/instrumentation/test_options.py
@@ -23,4 +23,5 @@
'test_support_apk_path',
'device_flags',
'isolate_file_path',
- 'set_asserts'])
+ 'set_asserts',
+ 'delete_stale_data'])
diff --git a/build/android/pylib/instrumentation/test_runner.py b/build/android/pylib/instrumentation/test_runner.py
index 4ca0742..0f2e53f 100644
--- a/build/android/pylib/instrumentation/test_runner.py
+++ b/build/android/pylib/instrumentation/test_runner.py
@@ -343,6 +343,11 @@
try:
self.TestSetup(test)
+ try:
+ self.device.GoHome()
+ except device_errors.CommandTimeoutError:
+ logging.exception('Failed to focus the launcher.')
+
time_ms = lambda: int(time.time() * 1000)
start_ms = time_ms()
raw_output = self._RunTest(test, timeout)
diff --git a/build/android/pylib/local/device/local_device_environment.py b/build/android/pylib/local/device/local_device_environment.py
index 0d02ca3..04f9ab7 100644
--- a/build/android/pylib/local/device/local_device_environment.py
+++ b/build/android/pylib/local/device/local_device_environment.py
@@ -25,7 +25,7 @@
raise device_errors.NoDevicesError
if self._device_serial:
self._devices = [d for d in available_devices
- if d.adb.GetDeviceSerial == self._device_serial]
+ if d.adb.GetDeviceSerial() == self._device_serial]
if not self._devices:
raise device_errors.DeviceUnreachableError(
'Could not find device %r' % self._device_serial)
diff --git a/build/android/pylib/perf/test_options.py b/build/android/pylib/perf/test_options.py
index 0a0ace0..e6ef560 100644
--- a/build/android/pylib/perf/test_options.py
+++ b/build/android/pylib/perf/test_options.py
@@ -17,4 +17,5 @@
'single_step',
'collect_chartjson_data',
'output_chartjson_data',
+ 'max_battery_temp',
])
diff --git a/build/android/pylib/perf/test_runner.py b/build/android/pylib/perf/test_runner.py
index 0f464c3..31eb0e9 100644
--- a/build/android/pylib/perf/test_runner.py
+++ b/build/android/pylib/perf/test_runner.py
@@ -273,6 +273,10 @@
logging.info(
'temperature: %s (0.1 C)',
str(self._device_battery.GetBatteryInfo().get('temperature')))
+ if self._options.max_battery_temp:
+ self._device_battery.LetBatteryCoolToTemperature(
+ self._options.max_battery_temp)
+
logging.info('%s : %s', test_name, cmd)
start_time = datetime.datetime.now()
diff --git a/build/android/pylib/results/flakiness_dashboard/results_uploader.py b/build/android/pylib/results/flakiness_dashboard/results_uploader.py
index 856fa9c..b86d7ac 100644
--- a/build/android/pylib/results/flakiness_dashboard/results_uploader.py
+++ b/build/android/pylib/results/flakiness_dashboard/results_uploader.py
@@ -106,6 +106,9 @@
buildbot_branch = os.environ.get('BUILDBOT_BRANCH')
if not buildbot_branch:
buildbot_branch = 'master'
+ else:
+ # Ensure there's no leading "origin/"
+ buildbot_branch = buildbot_branch[buildbot_branch.find('/') + 1:]
self._master_name = '%s-%s' % (self._build_name, buildbot_branch)
self._test_results_map = {}
diff --git a/build/android/pylib/results/report_results.py b/build/android/pylib/results/report_results.py
index 4c9518e..4fc6aa0 100644
--- a/build/android/pylib/results/report_results.py
+++ b/build/android/pylib/results/report_results.py
@@ -43,7 +43,9 @@
if test_type == 'Instrumentation':
if flakiness_server == constants.UPSTREAM_FLAKINESS_SERVER:
assert test_package in ['ContentShellTest',
+ 'ChromePublicTest',
'ChromeShellTest',
+ 'ChromeSyncShellTest',
'AndroidWebViewTest']
dashboard_test_type = ('%s_instrumentation_tests' %
test_package.lower().rstrip('test'))
diff --git a/build/android/pylib/sdk/__init__.py b/build/android/pylib/sdk/__init__.py
new file mode 100644
index 0000000..50b23df
--- /dev/null
+++ b/build/android/pylib/sdk/__init__.py
@@ -0,0 +1,3 @@
+# Copyright 2015 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.
diff --git a/build/android/pylib/sdk/dexdump.py b/build/android/pylib/sdk/dexdump.py
new file mode 100644
index 0000000..ec10aba
--- /dev/null
+++ b/build/android/pylib/sdk/dexdump.py
@@ -0,0 +1,30 @@
+# Copyright 2015 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.
+
+import os
+
+from pylib import cmd_helper
+from pylib import constants
+
+_DEXDUMP_PATH = os.path.join(constants.ANDROID_SDK_TOOLS, 'dexdump')
+
+def DexDump(dexfiles, file_summary=False):
+ """A wrapper around the Android SDK's dexdump tool.
+
+ Args:
+ dexfiles: The dexfile or list of dex files to dump.
+ file_summary: Display summary information from the file header. (-f)
+
+ Returns:
+ An iterable over the output lines.
+ """
+ # TODO(jbudorick): Add support for more options as necessary.
+ if isinstance(dexfiles, basestring):
+ dexfiles = [dexfiles]
+ args = [_DEXDUMP_PATH] + dexfiles
+ if file_summary:
+ args.append('-f')
+
+ return cmd_helper.IterCmdOutputLines(args)
+
diff --git a/build/android/pylib/sdk/split_select.py b/build/android/pylib/sdk/split_select.py
new file mode 100644
index 0000000..8752129
--- /dev/null
+++ b/build/android/pylib/sdk/split_select.py
@@ -0,0 +1,66 @@
+# Copyright 2015 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.
+
+"""This module wraps Android's split-select tool."""
+# pylint: disable=unused-argument
+
+import os
+
+from pylib import cmd_helper
+from pylib import constants
+from pylib.utils import timeout_retry
+
+_SPLIT_SELECT_PATH = os.path.join(constants.ANDROID_SDK_TOOLS, 'split-select')
+_DEFAULT_TIMEOUT = 30
+_DEFAULT_RETRIES = 2
+
+def _RunSplitSelectCmd(args, timeout=None, retries=None):
+ """Runs a split-select command.
+
+ Args:
+ args: A list of arguments for split-select.
+ timeout: Timeout in seconds.
+ retries: Number of retries.
+
+ Returns:
+ The output of the command.
+ """
+ cmd = [_SPLIT_SELECT_PATH] + args
+ status, output = cmd_helper.GetCmdStatusAndOutputWithTimeout(
+ cmd, timeout_retry.CurrentTimeoutThread().GetRemainingTime())
+ if status != 0:
+ raise Exception('Failed running command %s' % str(cmd))
+ return output
+
+def _SplitConfig(device):
+ """Returns a config specifying which APK splits are required by the device.
+
+ Args:
+ device: A DeviceUtils object.
+ """
+ return ('%s-r%s-%s:%s' %
+ (device.language,
+ device.country,
+ device.screen_density,
+ device.product_cpu_abi))
+
+def SelectSplits(device, base_apk, split_apks,
+ timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
+ """Determines which APK splits the device requires.
+
+ Args:
+ device: A DeviceUtils object.
+ base_apk: The path of the base APK.
+ split_apks: A list of paths of APK splits.
+ timeout: Timeout in seconds.
+ retries: Number of retries.
+
+ Returns:
+ The list of APK splits that the device requires.
+ """
+ config = _SplitConfig(device)
+ args = ['--target', config, '--base', base_apk]
+ for split in split_apks:
+ args.extend(['--split', split])
+ return _RunSplitSelectCmd(args, timeout=timeout, retries=retries).splitlines()
\ No newline at end of file
diff --git a/build/android/pylib/uiautomator/test_runner.py b/build/android/pylib/uiautomator/test_runner.py
index 296bd47..bda6687 100644
--- a/build/android/pylib/uiautomator/test_runner.py
+++ b/build/android/pylib/uiautomator/test_runner.py
@@ -41,7 +41,8 @@
test_support_apk_path=None,
device_flags=None,
isolate_file_path=None,
- set_asserts=test_options.set_asserts)
+ set_asserts=test_options.set_asserts,
+ delete_stale_data=False)
super(TestRunner, self).__init__(instrumentation_options, device,
shard_index, test_pkg)
diff --git a/build/android/pylib/utils/emulator.py b/build/android/pylib/utils/emulator.py
index 635462f..cc07e61 100644
--- a/build/android/pylib/utils/emulator.py
+++ b/build/android/pylib/utils/emulator.py
@@ -86,17 +86,19 @@
"""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.
+ running but a device slot is taken. A little bot trouble and we're out of
+ room forever.
"""
- emulators = [d for d in device_utils.HealthyDevices() if d.adb.is_emulator]
+ emulators = [d for d in device_utils.DeviceUtils.HealthyDevices()
+ if d.adb.is_emulator]
if not emulators:
return
for e in emulators:
e.adb.Emu(['kill'])
logging.info('Emulator killing is async; give a few seconds for all to die.')
for _ in range(5):
- if not any(d.adb.is_emulator for d in device_utils.HealthyDevices()):
+ if not any(d.adb.is_emulator for d
+ in device_utils.DeviceUtils.HealthyDevices()):
return
time.sleep(1)
@@ -140,7 +142,8 @@
def _GetAvailablePort():
"""Returns an available TCP port for the console."""
used_ports = []
- emulators = [d for d in device_utils.HealthyDevices() if d.adb.is_emulator]
+ emulators = [d for d in device_utils.DeviceUtils.HealthyDevices()
+ if d.adb.is_emulator]
for emulator in emulators:
used_ports.append(emulator.adb.GetDeviceSerial().split('-')[1])
for port in PortPool.port_range():
diff --git a/build/android/test_runner.py b/build/android/test_runner.py
index 52de8fd..d508ef8 100755
--- a/build/android/test_runner.py
+++ b/build/android/test_runner.py
@@ -27,6 +27,8 @@
from pylib.device import device_errors
from pylib.device import device_utils
from pylib.gtest import gtest_config
+# TODO(jbudorick): Remove this once we stop selectively enabling platform mode.
+from pylib.gtest import gtest_test_instance
from pylib.gtest import setup as gtest_setup
from pylib.gtest import test_options as gtest_test_options
from pylib.linker import setup as linker_setup
@@ -215,6 +217,15 @@
dest='isolate_file_path',
help='.isolate file path to override the default '
'path')
+ group.add_argument('--app-data-file', action='append', dest='app_data_files',
+ help='A file path relative to the app data directory '
+ 'that should be saved to the host.')
+ group.add_argument('--app-data-file-dir',
+ help='Host directory to which app data files will be'
+ ' saved. Used with --app-data-file.')
+ group.add_argument('--delete-stale-data', dest='delete_stale_data',
+ action='store_true',
+ help='Delete stale test data on the device.')
filter_group = group.add_mutually_exclusive_group()
filter_group.add_argument('-f', '--gtest_filter', '--gtest-filter',
@@ -335,6 +346,9 @@
dest='isolate_file_path',
help='.isolate file path to override the default '
'path')
+ group.add_argument('--delete-stale-data', dest='delete_stale_data',
+ action='store_true',
+ help='Delete stale test data on the device.')
AddCommonOptions(parser)
AddDeviceOptions(parser)
@@ -388,7 +402,8 @@
args.test_support_apk_path,
args.device_flags,
args.isolate_file_path,
- args.set_asserts
+ args.set_asserts,
+ args.delete_stale_data
)
@@ -591,6 +606,11 @@
group.add_argument(
'--dry-run', action='store_true',
help='Just print the steps without executing.')
+ # Uses 0.1 degrees C because that's what Android does.
+ group.add_argument(
+ '--max-battery-temp', type=int,
+ help='Only start tests when the battery is at or below the given '
+ 'temperature (0.1 C)')
group.add_argument('single_step_command', nargs='*', action=SingleStepAction,
help='If --single-step is specified, the command to run.')
AddCommonOptions(parser)
@@ -615,7 +635,7 @@
args.steps, args.flaky_steps, args.output_json_list,
args.print_step, args.no_timeout, args.test_filter,
args.dry_run, args.single_step, args.collect_chartjson_data,
- args.output_chartjson_data)
+ args.output_chartjson_data, args.max_battery_temp)
def AddPythonTestOptions(parser):
@@ -640,7 +660,10 @@
args.test_arguments,
args.timeout,
args.isolate_file_path,
- suite_name)
+ suite_name,
+ args.app_data_files,
+ args.app_data_file_dir,
+ args.delete_stale_data)
runner_factory, tests = gtest_setup.Setup(gtest_options, devices)
results, test_exit_code = test_dispatcher.RunTests(
@@ -911,6 +934,8 @@
raise Exception('Failed to reset test server port.')
if command == 'gtest':
+ if args.suite_name[0] in gtest_test_instance.BROWSER_TEST_SUITES:
+ return RunTestsInPlatformMode(args, parser)
return _RunGTests(args, devices)
elif command == 'linker':
return _RunLinkerTests(args, devices)
diff --git a/build/gyp_chromium_test.py b/build/gyp_chromium_test.py.remove
similarity index 100%
rename from build/gyp_chromium_test.py
rename to build/gyp_chromium_test.py.remove