blob: da6f39b372f6dc817b741dd5a0431633c550b5e4 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright (C) 2021 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""
Tests STA handling of channel switch announcements.
"""
import random
import time
from acts import asserts
from acts.controllers.access_point import setup_ap
from acts.controllers.ap_lib import hostapd_constants
from acts.utils import rand_ascii_str
from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest
from acts_contrib.test_utils.abstract_devices.wlan_device import create_wlan_device
from typing import Sequence
class ChannelSwitchTest(WifiBaseTest):
# Time to wait between issuing channel switches
WAIT_BETWEEN_CHANNEL_SWITCHES_S = 15
# For operating class 115 tests.
GLOBAL_OPERATING_CLASS_115_CHANNELS = [36, 40, 44, 48]
# A channel outside the operating class.
NON_GLOBAL_OPERATING_CLASS_115_CHANNEL = 52
# For operating class 124 tests.
GLOBAL_OPERATING_CLASS_124_CHANNELS = [149, 153, 157, 161]
# A channel outside the operating class.
NON_GLOBAL_OPERATING_CLASS_124_CHANNEL = 52
def setup_class(self) -> None:
super().setup_class()
self.ssid = rand_ascii_str(10)
if 'dut' in self.user_params:
if self.user_params['dut'] == 'fuchsia_devices':
self.dut = create_wlan_device(self.fuchsia_devices[0])
elif self.user_params['dut'] == 'android_devices':
self.dut = create_wlan_device(self.android_devices[0])
else:
raise ValueError('Invalid DUT specified in config. (%s)' %
self.user_params['dut'])
else:
# Default is an android device, just like the other tests
self.dut = create_wlan_device(self.android_devices[0])
self.access_point = self.access_points[0]
self._stop_all_soft_aps()
self.in_use_interface = None
def teardown_test(self) -> None:
self.dut.disconnect()
self.dut.reset_wifi()
self.download_ap_logs()
self.access_point.stop_all_aps()
# TODO(fxbug.dev/85738): Change band type to an enum.
def channel_switch(self,
band: str,
starting_channel: int,
channel_switches: Sequence[int],
test_with_soft_ap: bool = False) -> None:
"""Setup and run a channel switch test with the given parameters.
Creates an AP, associates to it, and then issues channel switches
through the provided channels. After each channel switch, the test
checks that the DUT is connected for a period of time before considering
the channel switch successful. If directed to start a SoftAP, the test
will also check that the SoftAP is on the expected channel after each
channel switch.
Args:
band: band that AP will use, must be a valid band (e.g.
hostapd_constants.BAND_2G)
starting_channel: channel number that AP will use at startup
channel_switches: ordered list of channels that the test will
attempt to switch to
test_with_soft_ap: whether to start a SoftAP before beginning the
channel switches (default is False); note that if a SoftAP is
started, the test will also check that the SoftAP handles
channel switches correctly
"""
asserts.assert_true(
band in [hostapd_constants.BAND_2G, hostapd_constants.BAND_5G],
'Failed to setup AP, invalid band {}'.format(band))
self.current_channel_num = starting_channel
if band == hostapd_constants.BAND_5G:
self.in_use_interface = self.access_point.wlan_5g
elif band == hostapd_constants.BAND_2G:
self.in_use_interface = self.access_point.wlan_2g
asserts.assert_true(
self._channels_valid_for_band([self.current_channel_num], band),
'starting channel {} not a valid channel for band {}'.format(
self.current_channel_num, band))
setup_ap(access_point=self.access_point,
profile_name='whirlwind',
channel=self.current_channel_num,
ssid=self.ssid)
if test_with_soft_ap:
self._start_soft_ap()
self.log.info('sending associate command for ssid %s', self.ssid)
self.dut.associate(target_ssid=self.ssid)
asserts.assert_true(self.dut.is_connected(), 'Failed to connect.')
asserts.assert_true(channel_switches,
'Cannot run test, no channels to switch to')
asserts.assert_true(
self._channels_valid_for_band(channel_switches, band),
'channel_switches {} includes invalid channels for band {}'.format(
channel_switches, band))
for channel_num in channel_switches:
if channel_num == self.current_channel_num:
continue
self.log.info('channel switch: {} -> {}'.format(
self.current_channel_num, channel_num))
self.access_point.channel_switch(self.in_use_interface,
channel_num)
channel_num_after_switch = self.access_point.get_current_channel(
self.in_use_interface)
asserts.assert_equal(channel_num_after_switch, channel_num,
'AP failed to channel switch')
self.current_channel_num = channel_num
# Check periodically to see if DUT stays connected. Sometimes
# CSA-induced disconnects occur seconds after last channel switch.
for _ in range(self.WAIT_BETWEEN_CHANNEL_SWITCHES_S):
asserts.assert_true(
self.dut.is_connected(),
'Failed to stay connected after channel switch.')
client_channel = self._client_channel()
asserts.assert_equal(
client_channel, channel_num,
'Client interface on wrong channel ({})'.format(
client_channel))
if test_with_soft_ap:
soft_ap_channel = self._soft_ap_channel()
asserts.assert_equal(
soft_ap_channel, channel_num,
'SoftAP interface on wrong channel ({})'.format(
soft_ap_channel))
time.sleep(1)
def test_channel_switch_2g(self) -> None:
"""Channel switch through all (US only) channels in the 2 GHz band."""
self.channel_switch(
band=hostapd_constants.BAND_2G,
starting_channel=hostapd_constants.AP_DEFAULT_CHANNEL_2G,
channel_switches=hostapd_constants.US_CHANNELS_2G)
def test_channel_switch_2g_with_soft_ap(self) -> None:
"""Channel switch through (US only) 2 Ghz channels with SoftAP up."""
self.channel_switch(
band=hostapd_constants.BAND_2G,
starting_channel=hostapd_constants.AP_DEFAULT_CHANNEL_2G,
channel_switches=hostapd_constants.US_CHANNELS_2G,
test_with_soft_ap=True)
def test_channel_switch_2g_shuffled_with_soft_ap(self) -> None:
"""Switch through shuffled (US only) 2 Ghz channels with SoftAP up."""
channels = hostapd_constants.US_CHANNELS_2G
random.shuffle(channels)
self.log.info('Shuffled channel switch sequence: {}'.format(channels))
self.channel_switch(
band=hostapd_constants.BAND_2G,
starting_channel=hostapd_constants.AP_DEFAULT_CHANNEL_2G,
channel_switches=channels,
test_with_soft_ap=True)
# TODO(fxbug.dev/84777): This test fails.
def test_channel_switch_5g(self) -> None:
"""Channel switch through all (US only) channels in the 5 GHz band."""
self.channel_switch(
band=hostapd_constants.BAND_5G,
starting_channel=hostapd_constants.AP_DEFAULT_CHANNEL_5G,
channel_switches=hostapd_constants.US_CHANNELS_5G)
# TODO(fxbug.dev/84777): This test fails.
def test_channel_switch_5g_with_soft_ap(self) -> None:
"""Channel switch through (US only) 5 GHz channels with SoftAP up."""
self.channel_switch(
band=hostapd_constants.BAND_5G,
starting_channel=hostapd_constants.AP_DEFAULT_CHANNEL_5G,
channel_switches=hostapd_constants.US_CHANNELS_5G,
test_with_soft_ap=True)
def test_channel_switch_5g_shuffled_with_soft_ap(self) -> None:
"""Switch through shuffled (US only) 5 Ghz channels with SoftAP up."""
channels = hostapd_constants.US_CHANNELS_5G
random.shuffle(channels)
self.log.info('Shuffled channel switch sequence: {}'.format(channels))
self.channel_switch(
band=hostapd_constants.BAND_5G,
starting_channel=hostapd_constants.AP_DEFAULT_CHANNEL_5G,
channel_switches=channels,
test_with_soft_ap=True)
# TODO(fxbug.dev/84777): This test fails.
def test_channel_switch_regression_global_operating_class_115(
self) -> None:
"""Channel switch into, through, and out of global op. class 115 channels.
Global operating class 115 is described in IEEE 802.11-2016 Table E-4.
Regression test for fxbug.dev/84777.
"""
channels = self.GLOBAL_OPERATING_CLASS_115_CHANNELS + [
self.NON_GLOBAL_OPERATING_CLASS_115_CHANNEL
]
self.channel_switch(
band=hostapd_constants.BAND_5G,
starting_channel=self.NON_GLOBAL_OPERATING_CLASS_115_CHANNEL,
channel_switches=channels)
# TODO(fxbug.dev/84777): This test fails.
def test_channel_switch_regression_global_operating_class_115_with_soft_ap(
self) -> None:
"""Test global operating class 124 channel switches, with SoftAP.
Regression test for fxbug.dev/84777.
"""
channels = self.GLOBAL_OPERATING_CLASS_115_CHANNELS + [
self.NON_GLOBAL_OPERATING_CLASS_115_CHANNEL
]
self.channel_switch(
band=hostapd_constants.BAND_5G,
starting_channel=self.NON_GLOBAL_OPERATING_CLASS_115_CHANNEL,
channel_switches=channels,
test_with_soft_ap=True)
# TODO(fxbug.dev/84777): This test fails.
def test_channel_switch_regression_global_operating_class_124(
self) -> None:
"""Switch into, through, and out of global op. class 124 channels.
Global operating class 124 is described in IEEE 802.11-2016 Table E-4.
Regression test for fxbug.dev/64279.
"""
channels = self.GLOBAL_OPERATING_CLASS_124_CHANNELS + [
self.NON_GLOBAL_OPERATING_CLASS_124_CHANNEL
]
self.channel_switch(
band=hostapd_constants.BAND_5G,
starting_channel=self.NON_GLOBAL_OPERATING_CLASS_124_CHANNEL,
channel_switches=channels)
# TODO(fxbug.dev/84777): This test fails.
def test_channel_switch_regression_global_operating_class_124_with_soft_ap(
self) -> None:
"""Test global operating class 124 channel switches, with SoftAP.
Regression test for fxbug.dev/64279.
"""
channels = self.GLOBAL_OPERATING_CLASS_124_CHANNELS + [
self.NON_GLOBAL_OPERATING_CLASS_124_CHANNEL
]
self.channel_switch(
band=hostapd_constants.BAND_5G,
starting_channel=self.NON_GLOBAL_OPERATING_CLASS_124_CHANNEL,
channel_switches=channels,
test_with_soft_ap=True)
def _channels_valid_for_band(self, channels: Sequence[int],
band: str) -> bool:
"""Determine if the channels are valid for the band (US only).
Args:
channels: channel numbers
band: a valid band (e.g. hostapd_constants.BAND_2G)
"""
if band == hostapd_constants.BAND_2G:
band_channels = frozenset(hostapd_constants.US_CHANNELS_2G)
elif band == hostapd_constants.BAND_5G:
band_channels = frozenset(hostapd_constants.US_CHANNELS_5G)
else:
asserts.fail('Invalid band {}'.format(band))
channels_set = frozenset(channels)
if channels_set <= band_channels:
return True
return False
def _start_soft_ap(self) -> None:
"""Start a SoftAP on the DUT.
Raises:
EnvironmentError: if the SoftAP does not start
"""
ssid = rand_ascii_str(10)
security_type = 'none'
password = ''
connectivity_mode = 'local_only'
operating_band = 'any'
self.log.info('Starting SoftAP on DUT')
response = self.dut.device.sl4f.wlan_ap_policy_lib.wlanStartAccessPoint(
ssid, security_type, password, connectivity_mode, operating_band)
if response.get('error'):
raise EnvironmentError('SL4F: Failed to setup SoftAP. Err: %s' %
response['error'])
self.log.info('SoftAp network (%s) is up.' % ssid)
def _stop_all_soft_aps(self) -> None:
"""Stops all SoftAPs on Fuchsia Device.
Raises:
EnvironmentError: if SoftAP stop call fails
"""
response = self.dut.device.sl4f.wlan_ap_policy_lib.wlanStopAllAccessPoint(
)
if response.get('error'):
raise EnvironmentError(
'SL4F: Failed to stop all SoftAPs. Err: %s' %
response['error'])
def _client_channel(self) -> int:
"""Determine the channel of the DUT client interface.
If the interface is not connected, the method will assert a test
failure.
Returns: channel number
Raises:
EnvironmentError: if client interface channel cannot be
determined
"""
status = self.dut.status()
if status['error']:
raise EnvironmentError('Could not determine client channel')
result = status['result']
if isinstance(result, dict):
if result.get('Connected'):
return result['Connected']['channel']['primary']
asserts.fail('Client interface not connected')
raise EnvironmentError('Could not determine client channel')
def _soft_ap_channel(self) -> int:
"""Determine the channel of the DUT SoftAP interface.
If the interface is not connected, the method will assert a test
failure.
Returns: channel number
Raises:
EnvironmentError: if SoftAP interface channel cannot be determined.
"""
iface_ids = self.dut.get_wlan_interface_id_list()
for iface_id in iface_ids:
query = self.dut.device.sl4f.wlan_lib.wlanQueryInterface(iface_id)
if query['error']:
continue
query_result = query['result']
if type(query_result) is dict and query_result.get('role') == 'Ap':
status = self.dut.device.sl4f.wlan_lib.wlanStatus(iface_id)
if status['error']:
continue
status_result = status['result']
if isinstance(status_result, dict):
if status_result.get('Connected'):
return status_result['Connected']['channel']['primary']
asserts.fail('SoftAP interface not connected')
raise EnvironmentError('Could not determine SoftAP channel')