autotest (wifi): enable MaskedBSSID to run on a single-stream PHY

By default, _get_phy_for_frequency() ignores any PHY that doesn't
support dual-stream operation. This minimizes the possibility of
surprises in test results. For example, performance tests will
have hugely different results if run on single-stream PHY.

In the case of MaskedBSSID, however, this restriction puts us
in a situation where requirements are hard to satisfy. Specifically,
we'd require: two dual-stream PHYs that operate in the same band.
(We can't simply run multiple APs on a single PHY, because the APs
are deliberately configured to use the same BSSID. We'd rather not
run on multiple bands, because that takes us farther away from
the AP behavior this test is meant to emulate.)

To support MaskedBSSID on diverse hardware, we relax the requirements.
In particular, we allow the use of single-stream PHYs for MaskedBSSID.
Since MaskedBSSID tests correctness, but not performance, this should
not yield any surprising results.

To provide determinism, the single-stream PHY is chosen only after
more capable PHYs are already in use.

BUG=chromium:504529
TEST=network_WiFi_MaskedBSSID (logs showed dual-stream was used first)
TEST=network_WiFi_SimpleConnect.wifi_check5VHT80

Change-Id: I34433cf0a766a15a1528838481be514ce3bcfed9
Reviewed-on: https://chromium-review.googlesource.com/283621
Reviewed-by: mukesh agrawal <quiche@chromium.org>
Tested-by: mukesh agrawal <quiche@chromium.org>
Reviewed-by: Paul Stewart <pstew@chromium.org>
Commit-Queue: mukesh agrawal <quiche@chromium.org>
diff --git a/server/cros/network/hostap_config.py b/server/cros/network/hostap_config.py
index 3a063c2..c9ed94e 100644
--- a/server/cros/network/hostap_config.py
+++ b/server/cros/network/hostap_config.py
@@ -443,6 +443,12 @@
         return self._scenario_name
 
 
+    @property
+    def min_streams(self):
+        """@return int _min_streams value, or None."""
+        return self._min_streams
+
+
     def __init__(self, mode=MODE_11B, channel=None, frequency=None,
                  n_capabilities=[], hide_ssid=None, beacon_interval=None,
                  dtim_period=None, frag_threshold=None, ssid=None, bssid=None,
@@ -454,7 +460,8 @@
                  ac_capabilities=[],
                  beacon_footer='',
                  spectrum_mgmt_required=None,
-                 scenario_name=None):
+                 scenario_name=None,
+                 min_streams=None):
         """Construct a HostapConfig.
 
         You may specify channel or frequency, but not both.  Both options
@@ -487,6 +494,7 @@
             spectrum management.
         @param scenario_name string to be included in file names, instead
             of the interface name.
+        @param min_streams int number of spatial streams required.
 
         """
         super(HostapConfig, self).__init__()
@@ -555,6 +563,7 @@
         self._beacon_footer = beacon_footer
         self._spectrum_mgmt_required = spectrum_mgmt_required
         self._scenario_name = scenario_name
+        self._min_streams = min_streams
 
 
     def __repr__(self):
diff --git a/server/site_linux_router.py b/server/site_linux_router.py
index 479a2ae..fafa2c2 100644
--- a/server/site_linux_router.py
+++ b/server/site_linux_router.py
@@ -251,7 +251,11 @@
 
         """
         # Figure out the correct interface.
-        interface = self.get_wlanif(configuration.frequency, 'managed')
+        if configuration.min_streams is None:
+            interface = self.get_wlanif(configuration.frequency, 'managed')
+        else:
+            interface = self.get_wlanif(
+                configuration.frequency, 'managed', configuration.min_streams)
 
         conf_file = self.HOSTAPD_CONF_FILE_PATTERN % interface
         log_file = self.HOSTAPD_LOG_FILE_PATTERN % interface
diff --git a/server/site_linux_system.py b/server/site_linux_system.py
index ebfe7e3..b3d5529 100644
--- a/server/site_linux_system.py
+++ b/server/site_linux_system.py
@@ -315,7 +315,7 @@
                       (epoch_seconds, busybox_date))
 
 
-    def _get_phy_for_frequency(self, frequency, phytype):
+    def _get_phy_for_frequency(self, frequency, phytype, spatial_streams):
         """Get a phy appropriate for a frequency and phytype.
 
         Return the most appropriate phy interface for operating on the
@@ -325,28 +325,34 @@
 
         @param frequency int WiFi frequency of phy.
         @param phytype string key of phytype registered at construction time.
+        @param spatial_streams int number of spatial streams required.
         @return string name of phy to use.
 
         """
-        phys = []
-        for phy in self.phys_for_frequency[frequency]:
-            phy_obj = self._phy_by_name(phy)
+        phy_objs = []
+        for phy_name in self.phys_for_frequency[frequency]:
+            phy_obj = self._phy_by_name(phy_name)
             num_antennas = min(phy_obj.avail_rx_antennas,
                                phy_obj.avail_tx_antennas)
-            if num_antennas >= self.MIN_SPATIAL_STREAMS:
-                phys.append(phy)
+            if num_antennas >= spatial_streams:
+                phy_objs.append(phy_obj)
             elif num_antennas == 0:
                 logging.warning(
-                    'Allowing use of %s, which reports zero antennas', phy)
-                phys.append(phy)
+                    'Allowing use of %s, which reports zero antennas', phy_name)
+                phy_objs.append(phy_obj)
             else:
                 logging.debug(
                     'Filtering out %s, which reports only %d antennas',
-                    phy, num_antennas)
+                    phy_name, num_antennas)
 
         busy_phys = set(net_dev.phy for net_dev in self._wlanifs_in_use)
-        idle_phys = [phy for phy in phys if phy not in busy_phys]
-        phys = idle_phys or phys
+        idle_phy_objs = [phy_obj for phy_obj in phy_objs
+                         if phy_obj.name not in busy_phys]
+        phy_objs = idle_phy_objs or phy_objs
+        phy_objs.sort(key=lambda phy_obj: min(phy_obj.avail_rx_antennas,
+                                              phy_obj.avail_tx_antennas),
+                      reverse=True)
+        phys = [phy_obj.name for phy_obj in phy_objs]
 
         preferred_bus = {'monitor': 'usb', 'managed': 'pci'}.get(phytype)
         preferred_phys = [phy for phy in phys
@@ -356,15 +362,20 @@
         return phys[0]
 
 
-    def get_wlanif(self, frequency, phytype, same_phy_as=None):
+    def get_wlanif(self, frequency, phytype,
+                   spatial_streams=None, same_phy_as=None):
         """Get a WiFi device that supports the given frequency and type.
 
         @param frequency int WiFi frequency to support.
         @param phytype string type of phy (e.g. 'monitor').
+        @param spatial_streams int number of spatial streams required.
         @param same_phy_as string create the interface on the same phy as this.
         @return string WiFi device.
 
         """
+        if spatial_streams is None:
+            spatial_streams = self.MIN_SPATIAL_STREAMS
+
         if same_phy_as:
             for net_dev in self._interfaces:
                 if net_dev.if_name == same_phy_as:
@@ -374,7 +385,8 @@
                 raise error.TestFail('Unable to find phy for interface %s' %
                                      same_phy_as)
         elif frequency in self.phys_for_frequency:
-            phy = self._get_phy_for_frequency(frequency, phytype)
+            phy = self._get_phy_for_frequency(
+                frequency, phytype, spatial_streams)
         else:
             raise error.TestFail('Unable to find phy for frequency %d' %
                                  frequency)
diff --git a/server/site_tests/network_WiFi_MaskedBSSID/network_WiFi_MaskedBSSID.py b/server/site_tests/network_WiFi_MaskedBSSID/network_WiFi_MaskedBSSID.py
index 3577292..6cba1d6 100644
--- a/server/site_tests/network_WiFi_MaskedBSSID/network_WiFi_MaskedBSSID.py
+++ b/server/site_tests/network_WiFi_MaskedBSSID/network_WiFi_MaskedBSSID.py
@@ -26,7 +26,8 @@
                           frequency=frequency,
                           mode=hostap_config.HostapConfig.MODE_11B,
                           bssid='00:11:22:33:44:55',
-                          ssid=('CrOS_Masked%d' % i)) for i in range(2)]
+                          ssid=('CrOS_Masked%d' % i),
+                          min_streams=1) for i in range(2)]
         # Create an AP, manually specifying both the SSID and BSSID.
         self.context.configure(configurations[0])
         # Create a second AP that responds to probe requests with the same BSSID
@@ -40,7 +41,3 @@
         self.context.client.scan([frequency],
                                  [config.ssid for config in configurations])
         self.context.router.deconfig()
-
-
-
-