autotest: iw_runner.py: VHT support in _parse_scan_results

Add support for VHT output parsing in the IwRunner scanning utility.

BUG=chromium:1018812
TEST=Unit tests in iw_runner_unittest.py

Cq-Depend: chromium:1877829
Change-Id: Id77b99b6eb817cb4aacebd6c00cdafd80445891b
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/autotest/+/1894795
Commit-Queue: Jared Pauletti <pauletti@google.com>
Tested-by: Jared Pauletti <pauletti@google.com>
Reviewed-by: Brian Norris <briannorris@chromium.org>
diff --git a/client/common_lib/cros/network/iw_runner.py b/client/common_lib/cros/network/iw_runner.py
index 2bb7208..3b33938 100644
--- a/client/common_lib/cros/network/iw_runner.py
+++ b/client/common_lib/cros/network/iw_runner.py
@@ -42,6 +42,8 @@
 WIDTH_VHT160 = _PrintableWidth('VHT160')
 WIDTH_VHT80_80 = _PrintableWidth('VHT80+80')
 
+VHT160_CENTER_CHANNELS = ('50','114')
+
 SECURITY_OPEN = 'open'
 SECURITY_WEP = 'wep'
 SECURITY_WPA = 'wpa'
@@ -59,7 +61,7 @@
 IwBand = collections.namedtuple(
     'Band', ['num', 'frequencies', 'frequency_flags', 'mcs_indices'])
 IwBss = collections.namedtuple('IwBss', ['bss', 'frequency', 'ssid', 'security',
-                                         'ht', 'signal'])
+                                         'width', 'signal'])
 IwNetDev = collections.namedtuple('IwNetDev', ['phy', 'if_name', 'if_type'])
 IwTimedScan = collections.namedtuple('IwTimedScan', ['time', 'bss_list'])
 
@@ -310,19 +312,27 @@
         frequency = None
         ssid = None
         ht = None
+        vht = None
         signal = None
         security = None
         supported_securities = []
         bss_list = []
+        # TODO(crbug.com/1032892): The parsing logic here wasn't really designed
+        # for the presence of multiple information elements like HT, VHT, and
+        # (eventually) HE. We should eventually update it to check that we are
+        # in the right section (e.g., verify the '* channel width' match is a
+        # match in the VHT section and not a different section). Also, we should
+        # probably add in VHT20, and VHT40 whenever we finish this bug.
         for line in output.splitlines():
             line = line.strip()
             bss_match = re.match('BSS ([0-9a-f:]+)', line)
             if bss_match:
                 if bss != None:
                     security = self.determine_security(supported_securities)
-                    iwbss = IwBss(bss, frequency, ssid, security, ht, signal)
+                    iwbss = IwBss(bss, frequency, ssid, security,
+                                  vht if vht else ht, signal)
                     bss_list.append(iwbss)
-                    bss = frequency = ssid = security = ht = None
+                    bss = frequency = ssid = security = ht = vht = None
                     supported_securities = []
                 bss = bss_match.group(1)
             if line.startswith('freq:'):
@@ -333,12 +343,31 @@
                 _, ssid = line.split(': ', 1)
             if line.startswith('* secondary channel offset'):
                 ht = HT_TABLE[line.split(':')[1].strip()]
+            # Checking for the VHT channel width based on IEEE 802.11-2016
+            # Table 9-252.
+            if line.startswith('* channel width:'):
+                chan_width_subfield = line.split(':')[1].strip()[0]
+                if chan_width_subfield == '1':
+                    vht = WIDTH_VHT80
+                # 2 and 3 are deprecated but are included here for older APs.
+                if chan_width_subfield == '2':
+                    vht = WIDTH_VHT160
+                if chan_width_subfield == '3':
+                    vht = WIDTH_VHT80_80
+            if line.startswith('* center freq segment 2:'):
+                center_chan_two = line.split(':')[1].strip()
+                if vht == WIDTH_VHT80:
+                    if center_chan_two in VHT160_CENTER_CHANNELS:
+                        vht = WIDTH_VHT160
+                    elif center_chan_two != '0':
+                        vht = WIDTH_VHT80_80
             if line.startswith('WPA'):
                supported_securities.append(SECURITY_WPA)
             if line.startswith('RSN'):
                supported_securities.append(SECURITY_WPA2)
         security = self.determine_security(supported_securities)
-        bss_list.append(IwBss(bss, frequency, ssid, security, ht, signal))
+        bss_list.append(IwBss(bss, frequency, ssid, security,
+                              vht if vht else ht, signal))
         return bss_list
 
 
diff --git a/client/common_lib/cros/network/iw_runner_unittest.py b/client/common_lib/cros/network/iw_runner_unittest.py
index b17ee4b..83e3b28 100755
--- a/client/common_lib/cros/network/iw_runner_unittest.py
+++ b/client/common_lib/cros/network/iw_runner_unittest.py
@@ -117,6 +117,78 @@
                                    'no_ht_support', iw_runner.SECURITY_OPEN,
                                    None, -45.00)
 
+    VHT_CAPA_20 = str('BSS ff:ff:ff:ff:ff:ff (on wlan0)\n'
+        '    freq: 2462\n'
+        '    signal: -44.00 dBm\n'
+        '    SSID: vht_capable_20\n'
+        '    HT operation:\n'
+        '        * secondary channel offset: no secondary\n'
+        '    VHT capabilities:\n'
+        '            VHT Capabilities (0x0f8369b1):\n'
+        '                Max MPDU length: 7991\n'
+        '                Supported Channel Width: neither 160 nor 80+80\n'
+        '    VHT operation:\n'
+        '        * channel width: 0 (20 or 40 MHz)\n'
+        '        * center freq segment 1: 11\n')
+
+    VHT_CAPA_20_IW_BSS = iw_runner.IwBss('ff:ff:ff:ff:ff:ff', 2462,
+                                         'vht_capable_20',
+                                         iw_runner.SECURITY_OPEN,
+                                         iw_runner.WIDTH_HT20, -44.00)
+
+    VHT80 =  str('BSS ff:ff:ff:ff:ff:ff (on wlan0)\n'
+        '    freq: 2462\n'
+        '    signal: -44.00 dBm\n'
+        '    SSID: support_vht80\n'
+        '    HT operation:\n'
+        '        * secondary channel offset: below\n'
+        '    VHT capabilities:\n'
+        '            VHT Capabilities (0x0f8369b1):\n'
+        '                Max MPDU length: 7991\n'
+        '                Supported Channel Width: neither 160 nor 80+80\n'
+        '    VHT operation:\n'
+        '        * channel width: 1 (80 MHz)\n'
+        '        * center freq segment 1: 11\n'
+        '        * center freq segment 2: 0\n')
+
+    VHT80_IW_BSS = iw_runner.IwBss('ff:ff:ff:ff:ff:ff', 2462,
+                                   'support_vht80', iw_runner.SECURITY_OPEN,
+                                   iw_runner.WIDTH_VHT80, -44.00)
+
+    VHT160 =  str('BSS 12:34:56:78:90:aa (on wlan0)\n'
+        '    freq: 5180\n'
+        '    signal: -44.00 dBm\n'
+        '    SSID: support_vht160\n'
+        '    HT operation:\n'
+        '        * secondary channel offset: below\n'
+        '    VHT capabilities:\n'
+        '            VHT Capabilities (0x0f8369b1):\n'
+        '                Max MPDU length: 7991\n'
+        '                Supported Channel Width: 160 MHz\n'
+        '    VHT operation:\n'
+        '        * channel width: 1 (80 MHz)\n'
+        '        * center freq segment 1: 42\n'
+        '        * center freq segment 2: 50\n')
+
+    VHT160_IW_BSS = iw_runner.IwBss('12:34:56:78:90:aa', 5180,
+                                    'support_vht160', iw_runner.SECURITY_OPEN,
+                                    iw_runner.WIDTH_VHT160, -44.00)
+
+    VHT80_80 =  str('BSS ab:cd:ef:fe:dc:ba (on wlan0)\n'
+        '    freq: 5180\n'
+        '    signal: -44.00 dBm\n'
+        '    SSID: support_vht80_80\n'
+        '    HT operation:\n'
+        '        * secondary channel offset: below\n'
+        '    VHT operation:\n'
+        '        * channel width: 1 (80 MHz)\n'
+        '        * center freq segment 1: 42\n'
+        '        * center freq segment 2: 106\n')
+
+    VHT80_80_IW_BSS = iw_runner.IwBss('ab:cd:ef:fe:dc:ba', 5180,
+                                    'support_vht80_80', iw_runner.SECURITY_OPEN,
+                                    iw_runner.WIDTH_VHT80_80, -44.00)
+
     HIDDEN_SSID = str('BSS ee:ee:ee:ee:ee:ee (on wlan0)\n'
         '    freq: 2462\n'
         '    signal: -70.00 dBm\n'
@@ -514,11 +586,12 @@
         @param iw_bss_2: an IWBss object
 
         """
+
         self.assertEquals(iw_bss_1.bss, iw_bss_2.bss)
         self.assertEquals(iw_bss_1.ssid, iw_bss_2.ssid)
         self.assertEquals(iw_bss_1.frequency, iw_bss_2.frequency)
         self.assertEquals(iw_bss_1.security, iw_bss_2.security)
-        self.assertEquals(iw_bss_1.ht, iw_bss_2.ht)
+        self.assertEquals(iw_bss_1.width, iw_bss_2.width)
         self.assertEquals(iw_bss_1.signal, iw_bss_2.signal)
 
 
@@ -572,6 +645,29 @@
         self.search_by_bss(scan_output, self.NO_HT_IW_BSS)
 
 
+    def test_vht_20(self):
+        """Test with a network that supports vht but is 20 MHz wide."""
+        scan_output = self.HT20 + self.NO_HT + self.VHT_CAPA_20
+        self.search_by_bss(scan_output, self.VHT_CAPA_20_IW_BSS)
+
+
+    def test_vht80(self):
+        """Test with a VHT80 network."""
+        scan_output = self.HT20 + self.VHT80 + self.HT40_ABOVE
+        self.search_by_bss(scan_output, self.VHT80_IW_BSS)
+
+
+    def test_vht160(self):
+        """Test with a VHT160 network."""
+        scan_output = self.VHT160 + self.VHT80 + self.HT40_ABOVE
+        self.search_by_bss(scan_output, self.VHT160_IW_BSS)
+
+    def test_vht80_80(self):
+        """Test with a VHT80+80 network."""
+        scan_output = self.VHT160 + self.VHT80_80
+        self.search_by_bss(scan_output, self.VHT80_80_IW_BSS)
+
+
     def test_hidden_ssid(self):
         """Test with a network with a hidden ssid."""
         scan_output = self.HT20 + self.HIDDEN_SSID + self.NO_HT
diff --git a/server/cros/chaos_lib/chaos_runner.py b/server/cros/chaos_lib/chaos_runner.py
index a258ad7..9174d7e 100644
--- a/server/cros/chaos_lib/chaos_runner.py
+++ b/server/cros/chaos_lib/chaos_runner.py
@@ -245,7 +245,7 @@
                         result = job.run_test(self._test,
                                      capturer=capturer,
                                      capturer_frequency=networks[0].frequency,
-                                     capturer_ht_type=networks[0].ht,
+                                     capturer_ht_type=networks[0].width,
                                      host=self._host,
                                      assoc_params=assoc_params,
                                      client=client,
diff --git a/server/cros/chaos_lib/static_runner.py b/server/cros/chaos_lib/static_runner.py
index 2f3550c..5941321 100644
--- a/server/cros/chaos_lib/static_runner.py
+++ b/server/cros/chaos_lib/static_runner.py
@@ -199,7 +199,7 @@
                         result = job.run_test(self._test,
                                      capturer=capturer,
                                      capturer_frequency=networks[0].frequency,
-                                     capturer_ht_type=networks[0].ht,
+                                     capturer_ht_type=networks[0].width,
                                      host=self._host,
                                      assoc_params=assoc_params,
                                      client=client,
diff --git a/server/cros/clique_lib/clique_runner.py b/server/cros/clique_lib/clique_runner.py
index 91e0ba1..13511d9 100644
--- a/server/cros/clique_lib/clique_runner.py
+++ b/server/cros/clique_lib/clique_runner.py
@@ -4,15 +4,12 @@
 
 import datetime
 import logging
-import os
 import pprint
 import time
-import re
 
 import common
 from autotest_lib.client.common_lib import error
 from autotest_lib.client.common_lib.cros.network import ap_constants
-from autotest_lib.server import hosts
 from autotest_lib.server import site_linux_system
 from autotest_lib.server.cros import host_lock_manager
 from autotest_lib.server.cros.ap_configurators import ap_batch_locker
@@ -294,7 +291,7 @@
                     self._test,
                     capturer=capturer,
                     capturer_frequency=networks[0].frequency,
-                    capturer_ht_type=networks[0].ht,
+                    capturer_ht_type=networks[0].width,
                     dut_pool=self._dut_pool,
                     assoc_params_list=assoc_params_list,
                     tries=tries,