Channel scan dwell time test

This test instructs a router to continually send beacon frames for a
predefined set of BSSes at a specific interval. The BSSes are named
with an index suffix in hex numerical order. The device under test is
instructed to perform a single channel scan while the beacon frames are
being sent by the router. Based on the SSIDs in the scan results, we
can estimate the device dwell time.

BUG=chromium:354685
TEST=Run this test

Change-Id: Ib8034a112b37ba6584a2b1d8009b2126f2dbd5e4
Reviewed-on: https://chromium-review.googlesource.com/191287
Reviewed-by: Paul Stewart <pstew@chromium.org>
Tested-by: Peter Qiu <zqiu@chromium.org>
Commit-Queue: Peter Qiu <zqiu@chromium.org>
diff --git a/client/common_lib/cros/network/iw_runner.py b/client/common_lib/cros/network/iw_runner.py
index d9e4682..e4b6e7b 100644
--- a/client/common_lib/cros/network/iw_runner.py
+++ b/client/common_lib/cros/network/iw_runner.py
@@ -444,8 +444,11 @@
             # The device was busy
             logging.debug('scan exit_status: %d', scan.exit_status)
             return None
-
-        bss_list = self._parse_scan_results(scan.stdout)
+        if not scan.stdout:
+            logging.debug('Empty scan result')
+            bss_list = []
+        else:
+            bss_list = self._parse_scan_results(scan.stdout)
         scan_time = float(scan.stderr)
         return IwTimedScan(scan_time, bss_list)
 
@@ -478,6 +481,18 @@
                   (self._command_iw, interface, power))
 
 
+    def set_freq(self, interface, freq):
+        """
+        Set the frequency for an interface.
+
+        @param interface: string name of interface to set frequency on.
+        @param freq: int frequency
+
+        """
+        self._run('%s dev %s set freq %d' %
+                  (self._command_iw, interface, freq))
+
+
     def set_regulatory_domain(self, domain_string):
         """
         Set the regulatory domain of the current machine.  Note that
diff --git a/server/cros/network/frame_sender.py b/server/cros/network/frame_sender.py
new file mode 100644
index 0000000..f73e703
--- /dev/null
+++ b/server/cros/network/frame_sender.py
@@ -0,0 +1,45 @@
+# Copyright (c) 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+class FrameSender(object):
+    """Context manager for sending management frames."""
+
+    def __init__(self, router, frame_type, channel, ssid_prefix=None,
+                 num_bss=None, frame_count=None, delay=None):
+        """
+        @param router: LinuxRouter object router to send frames from.
+        @param frame_type: int management frame type.
+        @param channel: int targeted channel.
+        @param ssid_prefix: string SSID prefix for BSSes in the frames.
+        @param num_bss: int number of BSSes configured for sending frames.
+        @param frame_count: int number of frames to send, frame_count of 0
+                implies infinite number of frames.
+        @param delay: int delay in between frames in milliseconds.
+        """
+        self._router = router
+        self._channel = channel
+        self._frame_type = frame_type
+        self._ssid_prefix = ssid_prefix
+        self._num_bss = num_bss
+        self._frame_count = frame_count
+        self._delay = delay
+        self._interface = None
+        self._pid = None
+
+    def __enter__(self):
+        self._interface = self._router.setup_management_frame_interface(
+                self._channel)
+        self._pid = self._router.send_management_frame(self._interface,
+                self._frame_type, self._channel, ssid_prefix=self._ssid_prefix,
+                num_bss=self._num_bss, frame_count=self._frame_count,
+                delay=self._delay)
+        return self
+
+
+    def __exit__(self, exception, value, traceback):
+        if self._interface:
+            self._router.release_interface(self._interface)
+        if self._pid:
+            self._router.host.run('kill %d' % self._pid, ignore_status=True)
+
diff --git a/server/site_linux_router.py b/server/site_linux_router.py
index 2f382fd..73edc6e 100644
--- a/server/site_linux_router.py
+++ b/server/site_linux_router.py
@@ -47,6 +47,8 @@
     STATION_LOG_FILE_PATTERN = '/tmp/wpa-supplicant-test-%s.log'
     STATION_PID_FILE_PATTERN = '/tmp/wpa-supplicant-test-%s.pid'
 
+    MGMT_FRAME_SENDER_LOG_FILE = '/tmp/send_management_frame-test.log'
+
     def get_capabilities(self):
         """@return iterable object of AP capabilities for this system."""
         caps = set([self.CAPABILITY_IBSS])
@@ -613,21 +615,75 @@
                         (self.cmd_hostapd_cli, control_if, client_mac))
 
 
-    def send_management_frame(self, frame_type, instance=0):
+    def send_management_frame_on_ap(self, frame_type, channel, instance=0):
         """Injects a management frame into an active hostapd session.
 
         @param frame_type string the type of frame to send.
+        @param channel int targeted channel
         @param instance int indicating which hostapd instance to inject into.
 
         """
         hostap_interface = self.hostapd_instances[instance]['interface']
         interface = self.get_wlanif(0, 'monitor', same_phy_as=hostap_interface)
         self.router.run("%s link set %s up" % (self.cmd_ip, interface))
-        self.router.run('%s %s %s' %
-                        (self.cmd_send_management_frame, interface, frame_type))
+        self.router.run('%s -i %s -t %s -c %d' %
+                        (self.cmd_send_management_frame, interface, frame_type,
+                         channel))
         self.release_interface(interface)
 
 
+    def setup_management_frame_interface(self, channel):
+        """
+        Setup interface for injecting management frames.
+
+        @param channel int channel to inject the frames.
+
+        @return string name of the interface.
+
+        """
+        frequency = hostap_config.HostapConfig.get_frequency_for_channel(
+                channel)
+        interface = self.get_wlanif(frequency, 'monitor')
+        self.iw_runner.set_freq(interface, frequency)
+        self.router.run('%s link set %s up' % (self.cmd_ip, interface))
+        return interface
+
+
+    def send_management_frame(self, interface, frame_type, channel,
+                              ssid_prefix=None, num_bss=None,
+                              frame_count=None, delay=None):
+        """
+        Injects management frames on specify channel |frequency|.
+
+        This function will spawn off a new process to inject specified
+        management frames |frame_type| at the specified interface |interface|.
+
+        @param interface string interface to inject frames.
+        @param frame_type string message type.
+        @param channel int targeted channel.
+        @param ssid_prefix string SSID prefix.
+        @param num_bss int number of BSS.
+        @param frame_count int number of frames to send.
+        @param delay int milliseconds delay between frames.
+
+        @return int PID of the newly created process.
+
+        """
+        command = '%s -i %s -t %s -c %d' % (self.cmd_send_management_frame,
+                                interface, frame_type, channel)
+        if ssid_prefix is not None:
+            command += ' -s %s' % (ssid_prefix)
+        if num_bss is not None:
+            command += ' -b %d' % (num_bss)
+        if frame_count is not None:
+            command += ' -n %d' % (frame_count)
+        if delay is not None:
+            command += ' -d %d' % (delay)
+        command += ' > %s 2>&1 & echo $!' % (self.MGMT_FRAME_SENDER_LOG_FILE)
+        pid = int(self.router.run(command).stdout)
+        return pid
+
+
     def detect_client_deauth(self, client_mac, instance=0):
         """Detects whether hostapd has logged a deauthentication from
         |client_mac|.
diff --git a/server/site_tests/network_WiFi_ChannelScanDwellTime/control b/server/site_tests/network_WiFi_ChannelScanDwellTime/control
new file mode 100644
index 0000000..a22ffd3
--- /dev/null
+++ b/server/site_tests/network_WiFi_ChannelScanDwellTime/control
@@ -0,0 +1,23 @@
+# Copyright (c) 2014 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.
+
+AUTHOR = 'zqiu, wiley, pstew, quiche'
+NAME = 'network_WiFi_ChannelScanDwellTime'
+TIME = 'SHORT'
+TEST_TYPE = 'Server'
+SUITE = 'wifi_matfunc'
+DEPENDENCIES = 'wificell'
+
+DOC = """
+This test is designed to determine the channel scan dwell time.
+"""
+
+def run(machine):
+    host = hosts.create_host(machine)
+    job.run_test('network_WiFi_ChannelScanDwellTime',
+                 host=host,
+                 raw_cmdline_args=args)
+
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/network_WiFi_ChannelScanDwellTime/network_WiFi_ChannelScanDwellTime.py b/server/site_tests/network_WiFi_ChannelScanDwellTime/network_WiFi_ChannelScanDwellTime.py
new file mode 100644
index 0000000..1494d3b
--- /dev/null
+++ b/server/site_tests/network_WiFi_ChannelScanDwellTime/network_WiFi_ChannelScanDwellTime.py
@@ -0,0 +1,123 @@
+# Copyright (c) 2014 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.
+
+import logging
+import random
+import string
+import time
+
+from autotest_lib.server.cros.network import frame_sender
+from autotest_lib.server import site_linux_system
+from autotest_lib.client.common_lib import error
+from autotest_lib.server.cros.network import wifi_cell_test_base
+
+
+class network_WiFi_ChannelScanDwellTime(wifi_cell_test_base.WiFiCellTestBase):
+    """Test for determine channel scan dwell time."""
+    version = 1
+
+    KNOWN_TEST_PREFIX = 'network_WiFi'
+    SUFFIX_LETTERS = string.ascii_lowercase + string.digits
+    DELAY_INTERVAL_MILLISECONDS = 1
+    SCAN_RETRY_TIMEOUT_SECONDS = 10
+    NUM_BSS = 1024
+    MISSING_BEACON_THRESHOLD = 2
+
+    def _build_ssid_prefix(self):
+        """Build ssid prefix."""
+        unique_salt = ''.join([random.choice(self.SUFFIX_LETTERS)
+                               for x in range(5)])
+        prefix = self.__class__.__name__[len(self.KNOWN_TEST_PREFIX):]
+        prefix = prefix.lstrip('_')
+        prefix += '_' + unique_salt + '_'
+        return prefix[-24:]
+
+
+    def _get_dwell_time(self, bss_list):
+        """Parse scan result to get dwell time.
+
+        Calculate dwell time based on the SSIDs in the scan result.
+
+        @param bss_list: List of BSSs
+
+        @return int dwell time in ms.
+        """
+        # Get ssid indices from the scan result.
+        # Expected SSID format: [testName]_[salt]_[index]
+        ssid_index = []
+        for bss in bss_list:
+            ssid = int(bss.ssid.split('_')[-1], 16)
+            ssid_index.append(ssid)
+        # Calculate dwell time based on the start ssid index and end ssid index.
+        ssid_index.sort()
+        index_diff = ssid_index[-1] - ssid_index[0]
+        dwell_time = index_diff * self.DELAY_INTERVAL_MILLISECONDS
+        # Check if number of missed beacon frames exceed the test threshold.
+        missed_beacons = index_diff - (len(ssid_index) - 1)
+        if missed_beacons > self.MISSING_BEACON_THRESHOLD:
+            logging.info('Missed %d beacon frames, SSID Index: %r',
+                         missed_beacons, ssid_index)
+            raise error.TestFail('DUT missed more than %d beacon frames',
+                                 missed_beacons)
+        return dwell_time
+
+
+    def _channel_dwell_time_test(self, single_channel):
+        """Perform test to determine channel dwell time.
+
+        This function invoke FrameSender to continuously send beacon frames
+        for specific number of BSSs with specific delay, the SSIDs of the
+        BSS are in hex numerical order. And at the same time, perform wifi scan
+        on the DUT. The index in the SSIDs of the scan result will be used to
+        interpret the relative start time and end time of the channel scan.
+
+        @param single_channel: bool perform single channel scan if true.
+
+        @return int dwell time in ms.
+
+        """
+        dwell_time = 0
+        ssid_prefix = self._build_ssid_prefix()
+        with frame_sender.FrameSender(self.context.router, 'beacon', 1,
+                                      ssid_prefix=ssid_prefix,
+                                      num_bss = self.NUM_BSS,
+                                      frame_count=0,
+                                      delay=self.DELAY_INTERVAL_MILLISECONDS):
+            if single_channel:
+                frequencies = [2412]
+            else:
+                frequencies = []
+            # Perform scan
+            start_time = time.time()
+            while time.time() - start_time < self.SCAN_RETRY_TIMEOUT_SECONDS:
+                bss_list = self.context.client.iw_runner.scan(
+                        self.context.client.wifi_if, frequencies=frequencies)
+
+                if bss_list is not None:
+                    break
+
+                time.sleep(0.5)
+            else:
+                raise error.TestFail('Unable to trigger scan on client.')
+            if not bss_list:
+                raise error.TestFail('Failed to find any BSS')
+            # Filter scan result based on ssid prefix to remove any cached
+            # BSSs from previous run.
+            result_list = [bss for bss in bss_list if
+                           bss.ssid.startswith(ssid_prefix)]
+            if result_list is None:
+                raise error.TestFail('Failed to find any BSS for this test')
+            dwell_time = self._get_dwell_time(result_list)
+        return dwell_time
+
+
+    def run_once(self):
+        self.context.router.require_capabilities(
+                  [site_linux_system.LinuxSystem.
+                          CAPABILITY_SEND_MANAGEMENT_FRAME])
+        # Get channel dwell time for single-channel scan
+        dwell_time = self._channel_dwell_time_test(True)
+        logging.info('Channel dwell time for single-channel scan: %d ms',
+                     dwell_time)
+        self.write_perf_keyval({'dwell_time_single_channel_scan': dwell_time})
diff --git a/server/site_tests/network_WiFi_Regulatory/network_WiFi_Regulatory.py b/server/site_tests/network_WiFi_Regulatory/network_WiFi_Regulatory.py
index 981c670..6e7a378 100644
--- a/server/site_tests/network_WiFi_Regulatory/network_WiFi_Regulatory.py
+++ b/server/site_tests/network_WiFi_Regulatory/network_WiFi_Regulatory.py
@@ -41,8 +41,8 @@
             for attempt in range(10):
                 # Since the client might be in power-save, we are not
                 # guaranteed it will hear this message the first time around.
-                self.context.router.send_management_frame(
-                        'channel_switch:%d' % alternate_channel)
+                self.context.router.send_management_frame_on_ap(
+                        'channel_switch', alternate_channel)
 
                 # Test to see if the router received a deauth message from
                 # the client.