blob: a24daaace14b77669dfe3ebefd638f693c4a55b4 [file] [log] [blame]
# Copyright (c) 2020 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.
from datetime import datetime
import logging
import time
import os
from autotest_lib.server import site_linux_system
from autotest_lib.server.cros.network import hostap_config
from autotest_lib.server.cros.network import wifi_cell_test_base
from autotest_lib.server.cros.network import wpa_mon
from autotest_lib.client.common_lib.cros.network import xmlrpc_datatypes
class network_WiFi_RoamNatural(wifi_cell_test_base.WiFiCellTestBase):
"""Bring up two APs, connect, vary attenuation as if the device is moving
between the two APs (i.e. the signal gets weaker on one and stronger on the
other until the first one cannot be seen anymore). At some point before the
first AP is torn down, the device should have roamed to the second AP. If it
doesn't there will be an association failure, which we can then log and
write to a file. Ideally, there would be no association failures and a roam
every time we expected one. Realistically, RSSI can vary quite widely, and
we can't expect to see a good roam signal on every scan even where there
should be one.
This test is used to sanity check that "normal" roaming behavior is not
broken by any roaming algorithm changes. A couple failed associations is
acceptable, but any more than that is a good indication that roaming has
become too sticky."""
version = 1
MAX_CENTER = 100
MIN_CENTER = 84
MAX_ATTEN = 106
ATTEN_STEP = 2
def test_body(self, pair_num, ap_pair, logger, roam_stats, failure_stats):
"""
Execute the test with the given APs and record stats.
@param pair_num int: the nth time this function is called (for results
logging purposes).
@param ap_pair tuple of HostapConfig objects: the APs
@param logger WpaMon object: used for event monitoring
@roam_stats dict of tuple to int: used to log roam stats.
@failure_stats int list with a single int element: used to log assoc
failure stats
"""
# Reset the attenuation here since it won't have been reset after
# previous iterations of this function.
self.context.attenuator.set_variable_attenuation(0)
min_atten = self.context.attenuator.get_minimal_total_attenuation()
ap_pair[0].ssid = None
self.context.configure(ap_pair[0])
ssid = self.context.router.get_ssid()
self.context.assert_connect_wifi(
xmlrpc_datatypes.AssociationParameters(ssid=ssid))
ap_pair[0].ssid = ssid
ap_pair[1].ssid = ssid
self.context.configure(ap_pair[1], configure_pcap=True)
self.context.client.wait_for_bss(self.context.pcap_host.get_hostapd_mac(0))
skip_roam_log = open(
os.path.join(self.resultsdir,
str(pair_num) + '_skip_roam.txt'), 'w')
assoc_failure_log = open(
os.path.join(self.resultsdir,
str(pair_num) + '_failure.txt'), 'w')
for center in range(self.MIN_CENTER, self.MAX_CENTER,
2 * self.ATTEN_STEP):
# The attenuation should [con,di]verge around center. We move
# the attenuation out 2dBm at a time until self.MAX_ATTEN is hit
# on one AP, at which point we tear that AP down to simulate it
# disappearing from the DUT's view. This should trigger a deauth
# if the DUT is still associated.
max_offset = self.MAX_ATTEN - center
for _ in range(2):
ranges = [range(0, max_offset, self.ATTEN_STEP),
range(max_offset, 0, -self.ATTEN_STEP),
range(0, -max_offset, -self.ATTEN_STEP),
range(-max_offset, 0, self.ATTEN_STEP)]
for r, _ in enumerate(ranges):
self.context.client.clear_supplicant_blacklist()
logger.start_event_capture()
for offset in ranges[r]:
ap1_atten = max(center + offset, min_atten)
ap2_atten = max(center - offset, min_atten)
self.context.attenuator.set_total_attenuation(
ap1_atten, ap_pair[0].frequency, 0)
self.context.attenuator.set_total_attenuation(
ap1_atten, ap_pair[0].frequency, 1)
self.context.attenuator.set_total_attenuation(
ap2_atten, ap_pair[1].frequency, 2)
self.context.attenuator.set_total_attenuation(
ap2_atten, ap_pair[1].frequency, 3)
time.sleep(2)
if r % 2 == 1:
# The APs' RSSIs should have converged. No reason to
# check for disconnects/roams here.
continue
if r == 0:
# First AP is no longer in view
self.context.router.deconfig()
elif r == 2:
# Second AP is no longer in view
self.context.pcap_host.deconfig()
dc_events = logger.wait_for_event(
wpa_mon.WpaMon.CTRL_EVENT_DISCONNECTED, timeout=5)
if dc_events:
# Association failure happened, check if this
# was because a roam was skipped.
skip_roams = logger.get_events(
wpa_mon.WpaMon.CTRL_EVENT_SKIP_ROAM, True)
if skip_roams:
# Skipped roam caused association failure, log this
# so we can re-examine the roam decision.
for roam in skip_roams:
logging.info(roam)
skip_roam_log.write(str(roam) + '\n')
freq_pair = (int(roam.cur_freq) / 1000,
int(roam.sel_freq) / 1000)
roam_stats[freq_pair] += 1
else:
# Association failure happened for some other reason
# (likely because AP disappeared before scan
# results returned). Log the failure for the
# timestamp in case we'd like to take a closer look.
for event in dc_events:
dc = str(datetime.now()) + ' ' + str(event)
logging.info(dc)
assoc_failure_log.write(dc + '\n')
failure_stats[0] += 1
# Reset the attenuation here. In some groamer cells, the
# attenuation for 5GHz channels is miscalibrated such that
# the RSSI is lower than expected. If we bring the AP back
# up while it's still maximally attenuated, it may not be
# visible to the DUT (the test was written deliberately so
# that it wouldn't happen even at full attenuation for
# properly calibrated cells, but this is apparently not
# always a good assumption).
self.context.attenuator.set_variable_attenuation(0)
self.context.configure(ap_pair[r / 2],
configure_pcap=(r == 2))
host = self.context.router if r == 0 else \
self.context.pcap_host
self.context.client.wait_for_bss(host.get_hostapd_mac(0))
skip_roam_log.close()
assoc_failure_log.close()
self.context.client.shill.disconnect(ssid)
self.context.router.deconfig()
self.context.pcap_host.deconfig()
def output_roam_stats(self, roam_skip_stats, failure_stats):
"""Output roam stats."""
for pair, skips in roam_skip_stats.items():
logging.info('%d association failures caused by skipped roams ' \
'from %s GHz to %s GHz', skips, pair[0], pair[1])
self.output_perf_value('roam_natural_%s_%s' % (pair[0], pair[1]),
skips, units='roams skipped',
higher_is_better=False)
logging.info('%d association failures unrelated to skipped roams',
failure_stats)
self.output_perf_value('roam_natural_assoc_failures', failure_stats,
units='assocation failures',
higher_is_better=False)
def run_once(self):
"""Body of the test."""
self.context.client.require_capabilities(
[site_linux_system.LinuxSystem.CAPABILITY_SUPPLICANT_ROAMING])
mode = hostap_config.HostapConfig.MODE_11N_PURE
ap1 = hostap_config.HostapConfig(channel=1, mode=mode)
ap2 = hostap_config.HostapConfig(channel=2, mode=mode)
ap3 = hostap_config.HostapConfig(channel=36, mode=mode)
ap_configs = [(ap1, ap2), (ap1, ap3)]
# Dictionary of roams skipped keyed by a pair of ints representing the
# current AP's frequency band and the target AP's frequency band.
roam_stats = {(2, 2): 0,
(2, 5): 0,
(5, 2): 0}
failure_stats = [0]
with self.context.client._wpa_mon as logger:
for pair_num, ap_pair in enumerate(ap_configs):
self.test_body(pair_num, ap_pair, logger, roam_stats, failure_stats)
# Log roam skips and assoc failures and output perf values
self.output_roam_stats(roam_stats, failure_stats[0])