Merge "Merge "Add blackbox metrics to ping tests." am: e1204d1851" into pi-dev
diff --git a/acts/framework/acts/controllers/attenuator_lib/minicircuits/http.py b/acts/framework/acts/controllers/attenuator_lib/minicircuits/http.py
new file mode 100644
index 0000000..b68d79d
--- /dev/null
+++ b/acts/framework/acts/controllers/attenuator_lib/minicircuits/http.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python3
+
+#   Copyright 2016- 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.
+"""
+Class for HTTP control of Mini-Circuits RCDAT series attenuators
+
+This class provides a wrapper to the MC-RCDAT attenuator modules for purposes
+of simplifying and abstracting control down to the basic necessities. It is
+not the intention of the module to expose all functionality, but to allow
+interchangeable HW to be used.
+
+See http://www.minicircuits.com/softwaredownload/Prog_Manual-6-Programmable_Attenuator.pdf
+"""
+
+import urllib
+from acts.controllers import attenuator
+
+
+class AttenuatorInstrument(attenuator.AttenuatorInstrument):
+    """A specific HTTP-controlled implementation of AttenuatorInstrument for
+    Mini-Circuits RC-DAT attenuators.
+
+    With the exception of HTTP-specific commands, all functionality is defined
+    by the AttenuatorInstrument class.
+    """
+
+    def __init__(self, num_atten=1):
+        super(AttenuatorInstrument, self).__init__(num_atten)
+        self._ip_address = None
+        self._port = None
+        self._timeout = None
+
+    def open(self, host, port=80, timeout=2):
+        """Initializes the AttenuatorInstrument and queries basic information.
+
+        Args:
+            host: A valid hostname (IP address or DNS-resolvable name) to an
+            MC-DAT attenuator instrument.
+            port: An optional port number (defaults to http default 80)
+            timeout: An optional timeout for http requests
+        """
+        self._ip_address = host
+        self._port = port
+        self._timeout = timeout
+
+        att_req = urllib.request.urlopen('http://{}:{}/MN?'.format(
+            self._ip_address, self._port))
+        config_str = att_req.read().decode('utf-8')
+        if not config_str.startswith('MN='):
+            raise attenuator.InvalidDataError(
+                'Attenuator returned invalid data. Attenuator returned: {}'.
+                format(config_str))
+
+        config_str = config_str[len('MN='):]
+        self.properties = dict(
+            zip(['model', 'max_freq', 'max_atten'], config_str.split('-', 2)))
+        self.max_atten = float(self.properties['max_atten'])
+
+    def is_open(self):
+        """Returns True if the AttenuatorInstrument has an open connection.
+
+        Since this controller is based on HTTP requests, there is no connection
+        required and the attenuator is always ready to accept requests.
+        """
+        return True
+
+    def close(self):
+        """Closes the connection to the attenuator.
+
+        Since this controller is based on HTTP requests, there is no connection
+        teardowns required.
+        """
+        pass
+
+    def set_atten(self, idx, value):
+        """This function sets the attenuation of an attenuator given its index
+        in the instrument.
+
+        Args:
+            idx: A zero-based index that identifies a particular attenuator in
+                an instrument. For instruments that only have one channel, this
+                is ignored by the device.
+            value: A floating point value for nominal attenuation to be set.
+
+        Raises:
+            InvalidDataError if the attenuator does not respond with the
+            expected output.
+        """
+        if not (0 <= idx < self.num_atten):
+            raise IndexError('Attenuator index out of range!', self.num_atten,
+                             idx)
+
+        if value > self.max_atten:
+            raise ValueError('Attenuator value out of range!', self.max_atten,
+                             value)
+        # The actual device uses one-based index for channel numbers.
+        att_req = urllib.request.urlopen(
+            'http://{}:{}/CHAN:{}:SETATT:{}'.format(
+                self._ip_address, self._port, idx + 1, value),
+            timeout=self._timeout)
+        att_resp = att_req.read().decode('utf-8')
+        if att_resp != '1':
+            raise attenuator.InvalidDataError(
+                'Attenuator returned invalid data. Attenuator returned: {}'.
+                format(att_resp))
+
+    def get_atten(self, idx):
+        """Returns the current attenuation of the attenuator at the given index.
+
+        Args:
+            idx: The index of the attenuator.
+
+        Raises:
+            InvalidDataError if the attenuator does not respond with the
+            expected outpu
+
+        Returns:
+            the current attenuation value as a float
+        """
+        if not (0 <= idx < self.num_atten):
+            raise IndexError('Attenuator index out of range!', self.num_atten,
+                             idx)
+        att_req = urllib.request.urlopen(
+            'http://{}:{}/CHAN:{}:ATT?'.format(self._ip_address, self.port,
+                                               idx + 1),
+            timeout=self._timeout)
+        att_resp = att_req.read().decode('utf-8')
+        try:
+            atten_val = float(att_resp)
+        except:
+            raise attenuator.InvalidDataError(
+                'Attenuator returned invalid data. Attenuator returned: {}'.
+                format(att_resp))
+        return atten_val
diff --git a/acts/framework/acts/test_utils/wifi/wifi_retail_ap.py b/acts/framework/acts/test_utils/wifi/wifi_retail_ap.py
index 8949bb1..6efa6af 100644
--- a/acts/framework/acts/test_utils/wifi/wifi_retail_ap.py
+++ b/acts/framework/acts/test_utils/wifi/wifi_retail_ap.py
@@ -126,7 +126,11 @@
         self.quit()
         self.__enter__()
 
-    def visit_persistent(self, url, page_load_timeout, num_tries):
+    def visit_persistent(self,
+                         url,
+                         page_load_timeout,
+                         num_tries,
+                         backup_url="about:blank"):
         """Method to visit webpages and retry upon failure.
 
         The function visits a web page and checks the the resulting URL matches
@@ -136,15 +140,20 @@
             url: the intended url
             page_load_timeout: timeout for page visits
             num_tries: number of tries before url is declared unreachable
+            backup_url: url to visit if first url is not reachable. This can be
+            use to simply refresh the browser and try again or to re-login to
+            the AP
         """
         self.driver.set_page_load_timeout(page_load_timeout)
         for idx in range(num_tries):
             try:
                 self.visit(url)
+                if self.url.split("/")[-1] == url.split("/")[-1]:
+                    break
+                else:
+                    self.visit(backup_url)
             except:
                 self.restart()
-            if self.url.split("/")[-1] == url.split("/")[-1]:
-                break
             if idx == num_tries - 1:
                 self.log.error("URL unreachable. Current URL: {}".format(
                     self.url))
@@ -479,7 +488,7 @@
             # Visit URL
             browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
             browser.visit_persistent(self.config_page_nologin,
-                                     BROWSER_WAIT_MED, 10)
+                                     BROWSER_WAIT_MED, 10, self.config_page)
 
             # Update region, and power/bandwidth for each network
             for key, value in self.config_page_fields.items():
@@ -815,7 +824,7 @@
             browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
             browser.visit_persistent(self.config_page_advanced,
                                      BROWSER_WAIT_MED, 10)
-            time.sleep(BROWSER_WAIT_SHORT)
+            time.sleep(BROWSER_WAIT_MED)
             wireless_button = browser.find_by_id("advanced_bt").first
             wireless_button.click()
             time.sleep(BROWSER_WAIT_SHORT)
@@ -853,6 +862,7 @@
             browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
             browser.visit_persistent(self.config_page_advanced,
                                      BROWSER_WAIT_MED, 10)
+            time.sleep(BROWSER_WAIT_MED)
             wireless_button = browser.find_by_id("advanced_bt").first
             wireless_button.click()
             time.sleep(BROWSER_WAIT_SHORT)
diff --git a/acts/tests/google/wifi/WifiPingTest.py b/acts/tests/google/wifi/WifiPingTest.py
index 32eaf83..3153c2f 100644
--- a/acts/tests/google/wifi/WifiPingTest.py
+++ b/acts/tests/google/wifi/WifiPingTest.py
@@ -466,6 +466,18 @@
     def test_slow_ping_rtt_ch1_VHT20(self):
         self._test_ping_rtt()
 
+    def test_fast_ping_rtt_ch6_VHT20(self):
+        self._test_ping_rtt()
+
+    def test_slow_ping_rtt_ch6_VHT20(self):
+        self._test_ping_rtt()
+
+    def test_fast_ping_rtt_ch11_VHT20(self):
+        self._test_ping_rtt()
+
+    def test_slow_ping_rtt_ch11_VHT20(self):
+        self._test_ping_rtt()
+
     def test_fast_ping_rtt_ch36_VHT20(self):
         self._test_ping_rtt()
 
diff --git a/acts/tests/google/wifi/WifiRssiTest.py b/acts/tests/google/wifi/WifiRssiTest.py
index 08bc325..8dd0b52 100644
--- a/acts/tests/google/wifi/WifiRssiTest.py
+++ b/acts/tests/google/wifi/WifiRssiTest.py
@@ -25,6 +25,7 @@
 from acts import asserts
 from acts import base_test
 from acts import utils
+from acts.metrics.loggers.blackbox import BlackboxMetricLogger
 from acts.test_decorators import test_tracker_info
 from acts.test_utils.wifi import wifi_power_test_utils as wputils
 from acts.test_utils.wifi import wifi_retail_ap as retail_ap
@@ -53,6 +54,19 @@
 
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
+        test_metrics = [
+            "signal_poll_rssi_shift", "signal_poll_avg_rssi_shift",
+            "scan_rssi_shift", "chain_0_rssi_shift", "chain_1_rssi_shift",
+            "signal_poll_rssi_error", "signal_poll_avg_rssi_error",
+            "scan_rssi_error", "chain_0_rssi_error", "chain_1_rssi_error",
+            "signal_poll_rssi_stdev", "chain_0_rssi_stdev",
+            "chain_1_rssi_stdev"
+        ]
+        for metric in test_metrics:
+            setattr(
+                self,
+                "{}_metric".format(metric),
+                BlackboxMetricLogger.for_test_case(metric_name=metric))
 
     def setup_class(self):
         self.dut = self.android_devices[0]
@@ -85,6 +99,14 @@
         Args:
             postprocessed_results: compiled arrays of RSSI measurements
         """
+        # Set Blackbox metric values
+        self.signal_poll_rssi_stdev_metric.metric_value = max(
+            postprocessed_results["signal_poll_rssi"]["stdev"])
+        self.chain_0_rssi_stdev_metric.metric_value = max(
+            postprocessed_results["chain_0_rssi"]["stdev"])
+        self.chain_1_rssi_stdev_metric.metric_value = max(
+            postprocessed_results["chain_1_rssi"]["stdev"])
+        # Evaluate test pass/fail
         test_failed = any([
             stdev > self.test_params["stdev_tolerance"]
             for stdev in postprocessed_results["signal_poll_rssi"]["stdev"]
@@ -147,6 +169,14 @@
                 else:
                     avg_error = RSSI_ERROR_VAL
                     avg_shift = RSSI_ERROR_VAL
+                # Set Blackbox metric values
+                setattr(
+                    getattr(self, "{}_error_metric".format(key)),
+                    "metric_value", avg_error)
+                setattr(
+                    getattr(self, "{}_shift_metric".format(key)),
+                    "metric_value", avg_shift)
+                # Evaluate test pass/fail
                 rssi_failure = (avg_error > self.test_params["abs_tolerance"]
                                 ) or math.isnan(avg_error)
                 if rssi_failure and key in rssi_under_test:
@@ -165,7 +195,6 @@
                         "{} passed ({} error = {:.2f} dB, "
                         "shift = {:.2f} dB)\n").format(key, error_type,
                                                        avg_error, avg_shift)
-
         if test_failed:
             asserts.fail(test_message)
         asserts.explicit_pass(test_message)
@@ -909,7 +938,7 @@
 
 class WifiRssi_2GHz_ActiveTraffic_Test(WifiRssiTest):
     def __init__(self, controllers):
-        base_test.BaseTestClass.__init__(self, controllers)
+        super().__init__(controllers)
         self.tests = ("test_rssi_stability_ch1_VHT20_ActiveTraffic",
                       "test_rssi_vs_atten_ch1_VHT20_ActiveTraffic",
                       "test_rssi_stability_ch2_VHT20_ActiveTraffic",
@@ -924,7 +953,7 @@
 
 class WifiRssi_5GHz_ActiveTraffic_Test(WifiRssiTest):
     def __init__(self, controllers):
-        base_test.BaseTestClass.__init__(self, controllers)
+        super().__init__(controllers)
         self.tests = ("test_rssi_stability_ch36_VHT20_ActiveTraffic",
                       "test_rssi_vs_atten_ch36_VHT20_ActiveTraffic",
                       "test_rssi_stability_ch36_VHT40_ActiveTraffic",
diff --git a/acts/tests/google/wifi/WifiThroughputStabilityTest.py b/acts/tests/google/wifi/WifiThroughputStabilityTest.py
index f856f18..6d98fe9 100644
--- a/acts/tests/google/wifi/WifiThroughputStabilityTest.py
+++ b/acts/tests/google/wifi/WifiThroughputStabilityTest.py
@@ -46,12 +46,12 @@
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
         # Define metrics to be uploaded to BlackBox
-        BlackboxMetricLogger.for_test_case(
-            metric_name='min_throughput', result_attr='min_throughput')
-        BlackboxMetricLogger.for_test_case(
-            metric_name='avg_throughput', result_attr='avg_throughput')
-        BlackboxMetricLogger.for_test_case(
-            metric_name='std_dev_percent', result_attr='std_dev_percent')
+        self.min_throughput_metric = BlackboxMetricLogger.for_test_case(
+            metric_name='min_throughput')
+        self.avg_throughput_metric = BlackboxMetricLogger.for_test_case(
+            metric_name='avg_throughput')
+        self.std_dev_percent_metric = BlackboxMetricLogger.for_test_case(
+            metric_name='std_dev_percent')
 
         # Generate test cases
         modes = [(6, "VHT20"), (36, "VHT20"), (36, "VHT40"), (36, "VHT80"),
@@ -99,31 +99,34 @@
             meta data
         """
         #TODO(@oelayach): Check throughput vs RvR golden file
-        self.avg_throughput = test_result_dict["iperf_results"][
-            "avg_throughput"]
-        self.min_throughput = test_result_dict["iperf_results"][
-            "min_throughput"]
-        self.std_dev_percent = (
+        avg_throughput = test_result_dict["iperf_results"]["avg_throughput"]
+        min_throughput = test_result_dict["iperf_results"]["min_throughput"]
+        std_dev_percent = (
             test_result_dict["iperf_results"]["std_deviation"] /
             test_result_dict["iperf_results"]["avg_throughput"]) * 100
+        # Set blackbox metrics
+        self.avg_throughput_metric.metric_value = avg_throughput
+        self.min_throughput_metric.metric_value = min_throughput
+        self.std_dev_percent_metric.metric_value = std_dev_percent
+        # Evaluate pass/fail
         min_throughput_check = (
-            (self.min_throughput / self.avg_throughput) *
+            (min_throughput / avg_throughput) *
             100) > self.test_params["min_throughput_threshold"]
-        std_deviation_check = self.std_dev_percent < self.test_params["std_deviation_threshold"]
+        std_deviation_check = std_dev_percent < self.test_params["std_deviation_threshold"]
 
         if min_throughput_check and std_deviation_check:
             asserts.explicit_pass(
                 "Test Passed. Throughput at {0:.2f}dB attenuation is stable. "
                 "Mean throughput is {1:.2f} Mbps with a standard deviation of "
                 "{2:.2f}% and dips down to {3:.2f} Mbps.".format(
-                    self.atten_level, self.avg_throughput,
-                    self.std_dev_percent, self.min_throughput))
+                    self.atten_level, avg_throughput, std_dev_percent,
+                    min_throughput))
         asserts.fail(
             "Test Failed. Throughput at {0:.2f}dB attenuation is unstable. "
             "Mean throughput is {1:.2f} Mbps with a standard deviation of "
             "{2:.2f}% and dips down to {3:.2f} Mbps.".format(
-                self.atten_level, self.avg_throughput, self.std_dev_percent,
-                self.min_throughput))
+                self.atten_level, avg_throughput, std_dev_percent,
+                min_throughput))
 
     def post_process_results(self, test_result):
         """Extracts results and saves plots and JSON formatted results.