| # Copyright (c) 2013 The Chromium OS 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 contextlib |
| import dbus |
| import logging |
| import random |
| import time |
| |
| from autotest_lib.client.bin import test, utils |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.common_lib.cros import chrome |
| from autotest_lib.client.cros.cellular import cellular |
| from autotest_lib.client.cros.networking import cellular_proxy |
| from autotest_lib.client.cros.networking import shill_context |
| from autotest_lib.client.cros.networking import shill_proxy |
| |
| # Number of seconds we wait for the cellular service to perform an action. |
| DEVICE_TIMEOUT=45 |
| SERVICE_TIMEOUT=75 |
| |
| # Number of times and seconds between modem state checks to ensure that the |
| # modem is not in a temporary transition state. |
| NUM_MODEM_STATE_CHECKS=2 |
| MODEM_STATE_CHECK_PERIOD_SECONDS=5 |
| |
| # Number of seconds to sleep after a connect request in slow-connect mode. |
| SLOW_CONNECT_WAIT_SECONDS=20 |
| |
| |
| class TechnologyCommands(): |
| """Control the modem mostly using shill Technology interfaces.""" |
| def __init__(self, shill, command_delegate): |
| self.shill = shill |
| self.command_delegate = command_delegate |
| |
| def Enable(self): |
| self.shill.manager.EnableTechnology( |
| shill_proxy.ShillProxy.TECHNOLOGY_CELLULAR) |
| |
| def Disable(self): |
| self.shill.manager.DisableTechnology( |
| shill_proxy.ShillProxy.TECHNOLOGY_CELLULAR) |
| |
| def Connect(self, **kwargs): |
| self.command_delegate.Connect(**kwargs) |
| |
| def Disconnect(self): |
| return self.command_delegate.Disconnect() |
| |
| def __str__(self): |
| return 'Technology Commands' |
| |
| |
| class ModemCommands(): |
| """Control the modem using modem manager DBUS interfaces.""" |
| def __init__(self, modem, slow_connect): |
| self.modem = modem |
| self.slow_connect = slow_connect |
| |
| def Enable(self): |
| self.modem.Enable(True) |
| |
| def Disable(self): |
| self.modem.Enable(False) |
| |
| def Connect(self, simple_connect_props): |
| logging.debug('Connecting with properties: %r' % simple_connect_props) |
| self.modem.Connect(simple_connect_props) |
| if self.slow_connect: |
| time.sleep(SLOW_CONNECT_WAIT_SECONDS) |
| |
| def Disconnect(self): |
| """ |
| Disconnect Modem. |
| |
| Returns: |
| True - to indicate that shill may autoconnect again. |
| """ |
| try: |
| self.modem.Disconnect() |
| except dbus.DBusException as e: |
| if (e.get_dbus_name() != |
| 'org.chromium.ModemManager.Error.OperationInitiated'): |
| raise e |
| return True |
| |
| def __str__(self): |
| return 'Modem Commands' |
| |
| |
| class DeviceCommands(): |
| """Control the modem using shill device interfaces.""" |
| def __init__(self, shill, device, slow_connect): |
| self.shill = shill |
| self.device = device |
| self.slow_connect = slow_connect |
| self.service = None |
| |
| def GetService(self): |
| service = self.shill.find_cellular_service_object() |
| if not service: |
| raise error.TestFail( |
| 'Service failed to appear when using device commands.') |
| return service |
| |
| def Enable(self): |
| self.device.Enable(timeout=DEVICE_TIMEOUT) |
| |
| def Disable(self): |
| self.service = None |
| self.device.Disable(timeout=DEVICE_TIMEOUT) |
| |
| def Connect(self, **kwargs): |
| self.GetService().Connect() |
| if self.slow_connect: |
| time.sleep(SLOW_CONNECT_WAIT_SECONDS) |
| |
| def Disconnect(self): |
| """ |
| Disconnect Modem. |
| |
| Returns: |
| False - to indicate that shill may not autoconnect again. |
| """ |
| self.GetService().Disconnect() |
| return False |
| |
| def __str__(self): |
| return 'Device Commands' |
| |
| |
| class MixedRandomCommands(): |
| """Control the modem using a mixture of commands on device, modems, etc.""" |
| def __init__(self, commands_list): |
| self.commands_list = commands_list |
| |
| def PickRandomCommands(self): |
| return self.commands_list[random.randrange(len(self.commands_list))] |
| |
| def Enable(self): |
| cmds = self.PickRandomCommands() |
| logging.info('Enable with %s' % cmds) |
| cmds.Enable() |
| |
| def Disable(self): |
| cmds = self.PickRandomCommands() |
| logging.info('Disable with %s' % cmds) |
| cmds.Disable() |
| |
| def Connect(self, **kwargs): |
| cmds = self.PickRandomCommands() |
| logging.info('Connect with %s' % cmds) |
| cmds.Connect(**kwargs) |
| |
| def Disconnect(self): |
| cmds = self.PickRandomCommands() |
| logging.info('Disconnect with %s' % cmds) |
| return cmds.Disconnect() |
| |
| def __str__(self): |
| return 'Mixed Commands' |
| |
| |
| class cellular_ModemControl(test.test): |
| version = 1 |
| |
| def CompareModemPowerState(self, modem, expected_state): |
| """Compare modem manager power state of a modem to an expected state.""" |
| return modem.IsEnabled() == expected_state |
| |
| def CompareDevicePowerState(self, device, expected_state): |
| """Compare the shill device power state to an expected state.""" |
| state = self.test_env.shill.get_dbus_property( |
| device, shill_proxy.ShillProxy.DEVICE_PROPERTY_POWERED) |
| logging.info('Device Enabled = %s' % state) |
| return state == expected_state |
| |
| def CompareServiceState(self, service, expected_states): |
| """Compare the shill service state to a set of expected states.""" |
| if not service: |
| logging.info('Service not found.') |
| return False |
| |
| state = self.test_env.shill.get_dbus_property( |
| service, shill_proxy.ShillProxy.SERVICE_PROPERTY_STATE) |
| logging.info('Service State = %s' % state) |
| return state in expected_states |
| |
| def EnsureNotConnectingOrDisconnecting(self): |
| """ |
| Ensure modem is not connecting or disconnecting. |
| |
| Raises: |
| error.TestFail if it timed out waiting for the modem to finish |
| connecting or disconnecting. |
| """ |
| # Shill retries a failed connect attempt with a different APN so |
| # check a few times to ensure the modem is not in between connect |
| # attempts. |
| for _ in range(NUM_MODEM_STATE_CHECKS): |
| utils.poll_for_condition( |
| lambda: not self.test_env.modem.IsConnectingOrDisconnecting(), |
| error.TestFail('Timed out waiting for modem to finish ' + |
| 'connecting or disconnecting.'), |
| timeout=SERVICE_TIMEOUT) |
| time.sleep(MODEM_STATE_CHECK_PERIOD_SECONDS) |
| |
| def EnsureDisabled(self): |
| """ |
| Ensure modem disabled, device powered off, and no service. |
| |
| Raises: |
| error.TestFail if the states are not consistent. |
| """ |
| utils.poll_for_condition( |
| lambda: self.CompareModemPowerState(self.test_env.modem, False), |
| error.TestFail('Modem failed to enter state Disabled.')) |
| utils.poll_for_condition( |
| lambda: self.CompareDevicePowerState(self.device, False), |
| error.TestFail('Device failed to enter state Powered=False.')) |
| utils.poll_for_condition( |
| lambda: not self.test_env.shill.find_cellular_service_object(), |
| error.TestFail('Service should not be available.'), |
| timeout=SERVICE_TIMEOUT) |
| |
| def EnsureEnabled(self, check_idle): |
| """ |
| Ensure modem enabled, device powered and service exists. |
| |
| Args: |
| check_idle: if True, then ensure that the service is idle |
| (i.e. not connected) otherwise ignore the |
| service state |
| |
| Raises: |
| error.TestFail if the states are not consistent. |
| """ |
| utils.poll_for_condition( |
| lambda: self.CompareModemPowerState(self.test_env.modem, True), |
| error.TestFail('Modem failed to enter state Enabled')) |
| utils.poll_for_condition( |
| lambda: self.CompareDevicePowerState(self.device, True), |
| error.TestFail('Device failed to enter state Powered=True.'), |
| timeout=30) |
| |
| service = self.test_env.shill.wait_for_cellular_service_object() |
| if check_idle: |
| utils.poll_for_condition( |
| lambda: self.CompareServiceState(service, ['idle']), |
| error.TestFail('Service failed to enter idle state.'), |
| timeout=SERVICE_TIMEOUT) |
| |
| def EnsureConnected(self): |
| """ |
| Ensure modem connected, device powered on, service connected. |
| |
| Raises: |
| error.TestFail if the states are not consistent. |
| """ |
| self.EnsureEnabled(check_idle=False) |
| utils.poll_for_condition( |
| lambda: self.CompareServiceState( |
| self.test_env.shill.find_cellular_service_object(), |
| ['ready', 'portal', 'online']), |
| error.TestFail('Service failed to connect.'), |
| timeout=SERVICE_TIMEOUT) |
| |
| |
| def TestCommands(self, commands): |
| """ |
| Manipulate the modem using modem, device or technology commands. |
| |
| Changes the state of the modem in various ways including |
| disable while connected and then verifies the state of the |
| modem manager and shill. |
| |
| Raises: |
| error.TestFail if the states are not consistent. |
| |
| """ |
| logging.info('Testing using %s' % commands) |
| |
| logging.info('Enabling') |
| commands.Enable() |
| self.EnsureEnabled(check_idle=not self.autoconnect) |
| |
| technology_family = self.test_env.modem.GetCurrentTechnologyFamily() |
| if technology_family == cellular.TechnologyFamily.CDMA: |
| simple_connect_props = {'number': r'#777'} |
| else: |
| simple_connect_props = {'number': r'#777', 'apn': self.FindAPN()} |
| |
| # Icera modems behave weirdly if we cancel the operation while the |
| # modem is connecting. Work around the issue by waiting until the |
| # connect operation completes. |
| # TODO(benchan): Remove this workaround once the issue is addressed |
| # on the modem side. |
| self.EnsureNotConnectingOrDisconnecting() |
| |
| logging.info('Disabling') |
| commands.Disable() |
| self.EnsureDisabled() |
| |
| logging.info('Enabling again') |
| commands.Enable() |
| self.EnsureEnabled(check_idle=not self.autoconnect) |
| |
| if not self.autoconnect: |
| logging.info('Connecting') |
| commands.Connect(simple_connect_props=simple_connect_props) |
| else: |
| logging.info('Expecting AutoConnect to connect') |
| self.EnsureConnected() |
| |
| logging.info('Disconnecting') |
| will_autoreconnect = commands.Disconnect() |
| |
| if not (self.autoconnect and will_autoreconnect): |
| # Icera modems behave weirdly if we cancel the operation while the |
| # modem is disconnecting. Work around the issue by waiting until |
| # the disconnect operation completes. |
| # TODO(benchan): Remove this workaround once the issue is addressed |
| # on the modem side. |
| self.EnsureNotConnectingOrDisconnecting() |
| |
| self.EnsureEnabled(check_idle=True) |
| logging.info('Connecting manually, since AutoConnect was on') |
| commands.Connect(simple_connect_props=simple_connect_props) |
| self.EnsureConnected() |
| |
| logging.info('Disabling') |
| commands.Disable() |
| self.EnsureDisabled() |
| |
| def FindAPN(self): |
| default = 'None' |
| service = self.test_env.shill.find_cellular_service_object() |
| last_good_apn = self.test_env.shill.get_dbus_property( |
| service, |
| cellular_proxy.CellularProxy.SERVICE_PROPERTY_LAST_GOOD_APN) |
| if not last_good_apn: |
| return default |
| return last_good_apn.get( |
| cellular_proxy.CellularProxy.APN_INFO_PROPERTY_APN, default) |
| |
| def run_once(self, test_env, autoconnect, mixed_iterations=2, |
| slow_connect=False): |
| self.test_env = test_env |
| self.autoconnect = autoconnect |
| |
| with test_env: |
| self.device = self.test_env.shill.find_cellular_device_object() |
| |
| modem_commands = ModemCommands(self.test_env.modem, |
| slow_connect) |
| technology_commands = TechnologyCommands(self.test_env.shill, |
| modem_commands) |
| device_commands = DeviceCommands(self.test_env.shill, |
| self.device, |
| slow_connect) |
| |
| # shill disables autoconnect on any cellular service before a user |
| # logs in (CL:851267). To test the autoconnect scenario, we need a |
| # user session to run the test. |
| chrome_context = chrome.Chrome() |
| |
| # Set up the autoconnect context after starting a user session so |
| # that we ensure the autoconnect property is set on the cellular |
| # service that may be in the user profile. |
| autoconnect_context = shill_context.ServiceAutoConnectContext( |
| self.test_env.shill.wait_for_cellular_service_object, |
| self.autoconnect) |
| |
| with contextlib.nested(chrome_context, autoconnect_context): |
| # Start with cellular disabled. |
| self.test_env.shill.manager.DisableTechnology( |
| shill_proxy.ShillProxy.TECHNOLOGY_CELLULAR) |
| self.EnsureDisabled() |
| |
| # Run the device commands test first to make sure we have |
| # a valid APN needed to connect using the modem commands. |
| self.TestCommands(device_commands) |
| self.TestCommands(technology_commands) |
| self.TestCommands(modem_commands) |
| |
| # Run several times using commands mixed from each type |
| mixed = MixedRandomCommands([modem_commands, |
| technology_commands, |
| device_commands]) |
| for _ in range(mixed_iterations): |
| self.TestCommands(mixed) |
| |
| # Ensure cellular is re-enabled in order to restore AutoConnect |
| # settings when ServiceAutoConnectContext exits. |
| # TODO(benchan): Refactor this logic into |
| # ServiceAutoConnectContext and update other users of |
| # ServiceAutoConnectContext. |
| self.test_env.shill.manager.EnableTechnology( |
| shill_proxy.ShillProxy.TECHNOLOGY_CELLULAR) |
| self.EnsureEnabled(check_idle=False) |