284 lines
10 KiB
Python
284 lines
10 KiB
Python
# Copyright (c) 2014 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 logging
|
|
import time
|
|
|
|
from autotest_lib.client.common_lib import error
|
|
from autotest_lib.client.common_lib.cros.network import iw_runner
|
|
from autotest_lib.client.common_lib.cros.network import ping_runner
|
|
from autotest_lib.client.common_lib.cros.network import xmlrpc_datatypes
|
|
from autotest_lib.server import hosts
|
|
from autotest_lib.server.cros.network import wifi_client
|
|
from autotest_lib.server.cros.network import netperf_runner
|
|
|
|
WORK_CLIENT_CONNECTION_RETRIES = 3
|
|
WAIT_FOR_CONNECTION = 10
|
|
|
|
class ConnectionWorker(object):
|
|
""" ConnectionWorker is a thin layer of interfaces for worker classes """
|
|
|
|
@property
|
|
def name(self):
|
|
"""@return a string: representing name of the worker class"""
|
|
raise NotImplementedError('Missing subclass implementation')
|
|
|
|
|
|
@classmethod
|
|
def create_from_parent(cls, parent_obj, **init_args):
|
|
"""Creates a derived ConnectionWorker object from the provided parent
|
|
object.
|
|
|
|
@param cls: derived class object which we're trying to create.
|
|
@param obj: existing parent class object.
|
|
@param init_args: Args to be passed to the derived class constructor.
|
|
|
|
@returns Instance of cls with the required fields copied from parent.
|
|
"""
|
|
obj = cls(**init_args)
|
|
obj.work_client = parent_obj.work_client
|
|
obj.host = parent_obj.host
|
|
return obj
|
|
|
|
|
|
def prepare_work_client(self, work_client_machine):
|
|
"""Prepare the SSHHost object into WiFiClient object
|
|
|
|
@param work_client_machine: a SSHHost object to be wrapped
|
|
|
|
"""
|
|
work_client_host = hosts.create_host(work_client_machine.hostname)
|
|
# All packet captures in chaos lab have dual NICs. Let us use phy1 to
|
|
# be a radio dedicated for work client
|
|
iw = iw_runner.IwRunner(remote_host=work_client_host)
|
|
phys = iw.list_phys()
|
|
devs = iw.list_interfaces(desired_if_type='managed')
|
|
if len(devs) > 0:
|
|
logging.debug('Removing interfaces in work host machine %s', devs)
|
|
for i in range(len(devs)):
|
|
iw.remove_interface(devs[i].if_name)
|
|
if len(phys) > 1:
|
|
logging.debug('Adding interfaces in work host machine')
|
|
iw.add_interface('phy1', 'work0', 'managed')
|
|
logging.debug('Interfaces in work client %s', iw.list_interfaces())
|
|
elif len(phys) == 1:
|
|
raise error.TestError('Not enough phys available to create a'
|
|
'work client interface %s.' %
|
|
work_client_host.hostname)
|
|
self.work_client = wifi_client.WiFiClient(
|
|
work_client_host, './debug', False)
|
|
# Make the host object easily accessible
|
|
self.host = self.work_client.host
|
|
|
|
|
|
def connect_work_client(self, assoc_params):
|
|
"""
|
|
Connect client to the AP.
|
|
|
|
Tries to connect the work client to AP in WORK_CLIENT_CONNECTION_RETRIES
|
|
tries. If we fail to connect in all tries then we would return False
|
|
otherwise returns True on successful connection to the AP.
|
|
|
|
@param assoc_params: an AssociationParameters object.
|
|
@return a boolean: True if work client is successfully connected to AP
|
|
or False on failing to connect to the AP
|
|
|
|
"""
|
|
if not self.work_client.shill.init_test_network_state():
|
|
logging.error('Failed to set up isolated test context profile for '
|
|
'work client.')
|
|
return False
|
|
|
|
success = False
|
|
for i in range(WORK_CLIENT_CONNECTION_RETRIES):
|
|
logging.info('Connecting work client to AP')
|
|
assoc_result = xmlrpc_datatypes.deserialize(
|
|
self.work_client.shill.connect_wifi(assoc_params))
|
|
success = assoc_result.success
|
|
if not success:
|
|
logging.error('Connection attempt of work client failed, try %d'
|
|
' reason: %s', (i+1), assoc_result.failure_reason)
|
|
else:
|
|
logging.info('Work client connected to the AP')
|
|
self.ssid = assoc_params.ssid
|
|
break
|
|
return success
|
|
|
|
|
|
def cleanup(self):
|
|
"""Teardown work_client"""
|
|
self.work_client.shill.disconnect(self.ssid)
|
|
self.work_client.shill.clean_profiles()
|
|
|
|
|
|
def run(self, client):
|
|
"""Executes the connection worker
|
|
|
|
@param client: WiFiClient object representing the DUT
|
|
|
|
"""
|
|
raise NotImplementedError('Missing subclass implementation')
|
|
|
|
|
|
class ConnectionDuration(ConnectionWorker):
|
|
"""This test is to check the liveliness of the connection to the AP. """
|
|
|
|
def __init__(self, duration_sec=30):
|
|
"""
|
|
Holds WiFi connection open with periodic pings
|
|
|
|
@param duration_sec: amount of time to hold connection in seconds
|
|
|
|
"""
|
|
|
|
self.duration_sec = duration_sec
|
|
|
|
|
|
@property
|
|
def name(self):
|
|
"""@return a string: representing name of this class"""
|
|
return 'duration'
|
|
|
|
|
|
def run(self, client):
|
|
"""Periodically pings work client to check liveliness of the connection
|
|
|
|
@param client: WiFiClient object representing the DUT
|
|
|
|
"""
|
|
ping_config = ping_runner.PingConfig(self.work_client.wifi_ip, count=10)
|
|
logging.info('Pinging work client ip: %s', self.work_client.wifi_ip)
|
|
start_time = time.time()
|
|
while time.time() - start_time < self.duration_sec:
|
|
time.sleep(10)
|
|
ping_result = client.ping(ping_config)
|
|
logging.info('Connection liveness %r', ping_result)
|
|
|
|
|
|
class ConnectionSuspend(ConnectionWorker):
|
|
"""
|
|
This test is to check the liveliness of the connection to the AP with
|
|
suspend resume cycle involved.
|
|
|
|
"""
|
|
|
|
def __init__(self, suspend_sec=30):
|
|
"""
|
|
Construct a ConnectionSuspend.
|
|
|
|
@param suspend_sec: amount of time to suspend in seconds
|
|
|
|
"""
|
|
|
|
self._suspend_sec = suspend_sec
|
|
|
|
|
|
@property
|
|
def name(self):
|
|
"""@return a string: representing name of this class"""
|
|
return 'suspend'
|
|
|
|
|
|
def run(self, client):
|
|
"""
|
|
Check the liveliness of the connection to the AP by pinging the work
|
|
client before and after a suspend resume.
|
|
|
|
@param client: WiFiClient object representing the DUT
|
|
|
|
"""
|
|
ping_config = ping_runner.PingConfig(self.work_client.wifi_ip, count=10)
|
|
# pinging work client to ensure we have a connection
|
|
logging.info('work client ip: %s', self.work_client.wifi_ip)
|
|
ping_result = client.ping(ping_config)
|
|
logging.info('before suspend:%r', ping_result)
|
|
client.do_suspend(self._suspend_sec)
|
|
# When going to suspend, DUTs using ath9k devices do not disassociate
|
|
# from the AP. On resume, DUTs would re-use the association from prior
|
|
# to suspend. However, this leads to some confused state for some APs
|
|
# (see crbug.com/346417) where the AP responds to actions frames like
|
|
# NullFunc but not to any data frames like DHCP/ARP packets from the
|
|
# DUT. Let us sleep for:
|
|
# + 2 seconds for linkmonitor to detect failure if any
|
|
# + 10 seconds for ReconnectTimer timeout
|
|
# + 5 seconds to reconnect to the AP
|
|
# + 3 seconds let us not have a very strict timeline.
|
|
# 20 seconds before we start to query shill about the connection state.
|
|
# TODO (krisr): add board detection code in wifi_client and adjust the
|
|
# sleep time here based on the wireless chipset
|
|
time.sleep(20)
|
|
|
|
# Wait for WAIT_FOR_CONNECTION time before trying to ping.
|
|
success, state, elapsed_time = client.wait_for_service_states(
|
|
self.ssid, client.CONNECTED_STATES, WAIT_FOR_CONNECTION)
|
|
if not success:
|
|
raise error.TestFail('DUT failed to connect to AP (%s state) after'
|
|
'resume in %d seconds' %
|
|
(state, WAIT_FOR_CONNECTION))
|
|
else:
|
|
logging.info('DUT entered %s state after %s seconds',
|
|
state, elapsed_time)
|
|
# ping work client to ensure we have connection after resume.
|
|
ping_result = client.ping(ping_config)
|
|
logging.info('after resume:%r', ping_result)
|
|
|
|
|
|
class ConnectionNetperf(ConnectionWorker):
|
|
"""
|
|
This ConnectionWorker is used to run a sustained data transfer between the
|
|
DUT and the work_client through an AP.
|
|
|
|
"""
|
|
|
|
# Minimum expected throughput for netperf streaming tests
|
|
NETPERF_MIN_THROUGHPUT = 2.0 # Mbps
|
|
|
|
def __init__(self, netperf_config):
|
|
"""
|
|
Construct a ConnectionNetperf object.
|
|
|
|
@param netperf_config: NetperfConfig object to define transfer test.
|
|
|
|
"""
|
|
self._config = netperf_config
|
|
|
|
|
|
@property
|
|
def name(self):
|
|
"""@return a string: representing name of this class"""
|
|
return 'netperf_%s' % self._config.human_readable_tag
|
|
|
|
|
|
def run(self, client):
|
|
"""
|
|
Create a NetperfRunner, run netperf between DUT and work_client.
|
|
|
|
@param client: WiFiClient object representing the DUT
|
|
|
|
"""
|
|
with netperf_runner.NetperfRunner(
|
|
client, self.work_client, self._config) as netperf:
|
|
ping_config = ping_runner.PingConfig(
|
|
self.work_client.wifi_ip, count=10)
|
|
# pinging work client to ensure we have a connection
|
|
logging.info('work client ip: %s', self.work_client.wifi_ip)
|
|
ping_result = client.ping(ping_config)
|
|
|
|
result = netperf.run(self._config)
|
|
logging.info('Netperf Result: %s', result)
|
|
|
|
if result is None:
|
|
raise error.TestError('Failed to create NetperfResult')
|
|
|
|
if result.duration_seconds < self._config.test_time:
|
|
raise error.TestFail(
|
|
'Netperf duration too short: %0.2f < %0.2f' %
|
|
(result.duration_seconds, self._config.test_time))
|
|
|
|
# TODO: Convert this limit to a perf metric crbug.com/348780
|
|
if result.throughput <self.NETPERF_MIN_THROUGHPUT:
|
|
raise error.TestFail(
|
|
'Netperf throughput too low: %0.2f < %0.2f' %
|
|
(result.throughput, self.NETPERF_MIN_THROUGHPUT))
|