| #!/usr/bin/env python3 |
| # |
| # Copyright 2022 The Fuchsia Authors |
| # |
| # 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 logging |
| import random |
| import time |
| from typing import Sequence |
| |
| from honeydew.typing.wlan import ClientStatusConnected |
| from mobly import asserts, signals, test_runner |
| |
| from antlion.controllers.access_point import setup_ap |
| from antlion.controllers.ap_lib import hostapd_constants |
| from antlion.controllers.ap_lib.hostapd_security import FuchsiaSecurityType |
| from antlion.controllers.fuchsia_lib.wlan_ap_policy_lib import ( |
| ConnectivityMode, |
| OperatingBand, |
| ) |
| from antlion.controllers.fuchsia_lib.wlan_lib import WlanFailure, WlanMacRole |
| from antlion.test_utils.abstract_devices.wlan_device import ( |
| AssociationMode, |
| create_wlan_device, |
| ) |
| from antlion.test_utils.wifi import base_test |
| from antlion.utils import rand_ascii_str |
| |
| |
| class ChannelSwitchTest(base_test.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.log = logging.getLogger() |
| self.ssid = rand_ascii_str(10) |
| |
| device_type = self.user_params.get("dut", "fuchsia_devices") |
| if device_type == "fuchsia_devices": |
| if len(self.fuchsia_devices) < 1: |
| raise signals.TestAbortClass("At least one Fuchsia device is required") |
| self.fuchsia_device = self.fuchsia_devices[0] |
| self.dut = create_wlan_device(self.fuchsia_device, AssociationMode.POLICY) |
| elif device_type == "android_devices": |
| if len(self.android_devices) < 1: |
| raise signals.TestAbortClass("At least one Android device is required") |
| self.dut = create_wlan_device( |
| self.android_devices[0], AssociationMode.POLICY |
| ) |
| else: |
| raise ValueError( |
| f'Invalid "dut" type specified in config: "{device_type}".' |
| 'Expected "fuchsia_devices" or "android_devices".' |
| ) |
| |
| if len(self.access_points) < 1: |
| raise signals.TestAbortClass("At least one access point is required") |
| self.access_point = self.access_points[0] |
| self._stop_all_soft_aps() |
| self.in_use_interface: str | None = None |
| |
| def teardown_test(self) -> None: |
| self.dut.disconnect() |
| self.dut.reset_wifi() |
| self.download_ap_logs() |
| self.access_point.stop_all_aps() |
| super().teardown_test() |
| |
| # TODO(fxbug.dev/42166670): 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], |
| f"Failed to setup AP, invalid band {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 |
| else: |
| raise TypeError(f'Unknown band "{band}"') |
| |
| asserts.assert_true( |
| self._channels_valid_for_band([self.current_channel_num], band), |
| ( |
| f"starting channel {self.current_channel_num} not a valid channel " |
| f"for band {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), |
| ( |
| f"channel_switches {channel_switches} includes invalid channels " |
| f"for band {band}" |
| ), |
| ) |
| |
| for channel_num in channel_switches: |
| if channel_num == self.current_channel_num: |
| continue |
| self.log.info( |
| f"channel switch: {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.", |
| ) |
| status = self.fuchsia_device.sl4f.wlan_lib.status() |
| if isinstance(status, ClientStatusConnected): |
| client_channel = status.channel.primary |
| asserts.assert_equal( |
| client_channel, |
| channel_num, |
| f"Client interface on wrong channel ({client_channel})", |
| ) |
| if test_with_soft_ap: |
| soft_ap_channel = self._soft_ap_channel() |
| asserts.assert_equal( |
| soft_ap_channel, |
| channel_num, |
| f"SoftAP interface on wrong channel ({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(f"Shuffled channel switch sequence: {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/42165602): 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/42165602): 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(f"Shuffled channel switch sequence: {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/42165602): 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/42165602. |
| """ |
| 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/42165602): 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/42165602. |
| """ |
| 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/42165602): 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/42142868. |
| """ |
| 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/42165602): 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/42142868. |
| """ |
| 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(f"Invalid band {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) |
| self.log.info(f'Starting SoftAP on DUT with ssid "{ssid}"') |
| |
| response = self.fuchsia_device.sl4f.wlan_ap_policy_lib.wlanStartAccessPoint( |
| ssid, |
| FuchsiaSecurityType.NONE, |
| None, |
| ConnectivityMode.LOCAL_ONLY, |
| OperatingBand.ANY, |
| ) |
| if response.get("error"): |
| raise EnvironmentError( |
| f"SL4F: Failed to setup SoftAP. Err: {response['error']}" |
| ) |
| self.log.info(f"SoftAp network ({ssid}) is up.") |
| |
| def _stop_all_soft_aps(self) -> None: |
| """Stops all SoftAPs on Fuchsia Device. |
| |
| Raises: |
| EnvironmentError: if SoftAP stop call fails |
| """ |
| response = self.fuchsia_device.sl4f.wlan_ap_policy_lib.wlanStopAllAccessPoint() |
| if response.get("error"): |
| raise EnvironmentError( |
| f"SL4F: Failed to stop all SoftAPs. Err: {response['error']}" |
| ) |
| |
| 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: |
| try: |
| result = self.fuchsia_device.sl4f.wlan_lib.query_iface(iface_id) |
| except WlanFailure as e: |
| self.log.warn(f"Query iface {iface_id} failed: {e}") |
| continue |
| if result.role is WlanMacRole.AP: |
| status = self.fuchsia_device.sl4f.wlan_lib.status() |
| if not isinstance(status, ClientStatusConnected): |
| raise EnvironmentError("Client not connected") |
| return status.channel.primary |
| raise EnvironmentError("Could not determine SoftAP channel") |
| |
| |
| if __name__ == "__main__": |
| test_runner.main() |