blob: 1f4beacb705edaae45ecf7d3dc316c5319b8d485 [file] [log] [blame]
# 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)