autotest: network_3GModemControl: add autoconnect and random commands

Randomly use commands either to the device, the technology or the
modem and do this for several iterations to look for additional bugs.

Add support for testing both autoconnect=True and autoconnect=False

BUG=None
TEST=run this test

Change-Id: I23feb2159acd486e56ce02a4e15c0fed615ebdbd
Reviewed-on: https://gerrit.chromium.org/gerrit/10836
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Commit-Ready: Jason Glasgow <jglasgow@chromium.org>
Reviewed-by: Jason Glasgow <jglasgow@chromium.org>
Tested-by: Jason Glasgow <jglasgow@chromium.org>
diff --git a/client/cros/cellular/cell_tools.py b/client/cros/cellular/cell_tools.py
index a0c2ddd..d3c1669 100644
--- a/client/cros/cellular/cell_tools.py
+++ b/client/cros/cellular/cell_tools.py
@@ -3,7 +3,7 @@
 # found in the LICENSE file.
 
 """Utilities for cellular tests."""
-import logging, string
+import dbus, logging, string
 
 from autotest_lib.client.bin import utils
 from autotest_lib.client.common_lib import error
@@ -117,26 +117,38 @@
 
         return False
 
-class DisableAutoConnectContext(object):
-    """Context manager which disables autoconnect.
+class AutoConnectContext(object):
+    """Context manager which sets autoconnect to either true or false
 
-       Disable autoconnect for all services associated with a device.
+       Enable or Disable autoconnect for the cellular service.
+       Restore it when done.
 
        Usage:
-           with cell_tools.DisableAutoConnectContext(device, flim):
+           with cell_tools.DisableAutoConnectContext(device, flim, autoconnect):
                block
     """
 
-    def __init__(self, device, flim):
+    def __init__(self, device, flim, autoconnect):
         self.device = device
         self.flim = flim
-        self.had_autoconnect = False
+        self.autoconnect = autoconnect
+        self.autoconnect_changed = False
+
+    def PowerOnDevice(self, device):
+        """Power on a flimflam device, ignoring in progress errors."""
+        logging.info('powered = %s' % device.GetProperties()['Powered'])
+        if device.GetProperties()['Powered']:
+            return
+        try:
+            device.SetProperty("Powered", True)
+        except dbus.exceptions.DBusException, e:
+            if e._dbus_error_name != 'org.chromium.flimflam.Error.InProgress':
+                raise e
 
     def __enter__(self):
         """Power up device, get the service and disable autoconnect."""
-        logging.info('powered = %s' % self.device.GetProperties()['Powered'])
-        if not self.device.GetProperties()['Powered']:
-            self.device.SetProperty("Powered", True)
+        changed = False
+        self.PowerOnDevice(self.device)
 
         # TODO(jglasgow): generalize to use services associated with device
         service = self.flim.FindCellularService(timeout=40)
@@ -156,39 +168,40 @@
         logging.info('Favorite = %s, AutoConnect = %s' % (
             favorite, autoconnect))
 
-        self.had_autoconnect = autoconnect
-
-        if autoconnect:
-            logging.info('Disabling AutoConnect.')
-            service.SetProperty('AutoConnect', dbus.Boolean(0))
+        if autoconnect != self.autoconnect:
+            logging.info('Setting AutoConnect = %s.', self.autoconnect)
+            service.SetProperty('AutoConnect', dbus.Boolean(self.autoconnect))
 
             props = service.GetProperties()
             favorite = props['Favorite']
             autoconnect = props['AutoConnect']
+            changed = True
 
         if not favorite:
             raise error.TestFail('Favorite=False, but we want it to be True')
 
-        if autoconnect:
-            raise error.TestFail('AutoConnect=True, but we want it to be False')
+        if autoconnect != self.autoconnect:
+            raise error.TestFail('AutoConnect is %s, but we want it to be %s' %
+                                 (autoconnect, self.autoconnect))
+
+        self.autoconnect_changed = changed
 
         return self
 
     def __exit__(self, exception, value, traceback):
         """Restore autoconnect state if we changed it."""
-        if not self.had_autoconnect:
+        if not self.autoconnect_changed:
             return
 
-        if not self.device.GetProperties()['Powered']:
-            self.device.SetProperty("Powered", True)
+        self.PowerOnDevice(self.device)
 
         # TODO(jglasgow): generalize to use services associated with
         # device, and restore state only on changed services
         service = self.flim.FindCellularService()
         if not service:
             logging.error('Cannot find cellular service.  '
-                          'Autoconnect left disabled.')
+                          'Autoconnect state not restored.')
             return
-        service.SetProperty('AutoConnect', True)
+        service.SetProperty('AutoConnect', dbus.Boolean(not self.autoconnect))
 
         return False
diff --git a/client/site_tests/network_3GModemControl/control b/client/site_tests/network_3GModemControl/control
index 81d5835..c1f1700 100644
--- a/client/site_tests/network_3GModemControl/control
+++ b/client/site_tests/network_3GModemControl/control
@@ -21,4 +21,5 @@
   if the commands are sent to the modem manager instead of flimflam.
 """
 
-job.run_test('network_3GModemControl')
+job.run_test('network_3GModemControl', autoconnect=False, tag='no-autoconnect')
+job.run_test('network_3GModemControl', autoconnect=True, tag='autoconnect')
diff --git a/client/site_tests/network_3GModemControl/network_3GModemControl.py b/client/site_tests/network_3GModemControl/network_3GModemControl.py
index e6e792c..54b2925 100644
--- a/client/site_tests/network_3GModemControl/network_3GModemControl.py
+++ b/client/site_tests/network_3GModemControl/network_3GModemControl.py
@@ -2,7 +2,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import dbus, logging, time
+import dbus, logging, random, time
 
 from autotest_lib.client.bin import test, utils
 from autotest_lib.client.common_lib import error
@@ -30,7 +30,7 @@
         self.command_delegate.Connect()
 
     def Disconnect(self):
-        self.command_delegate.Disconnect()
+        return self.command_delegate.Disconnect()
 
     def __str__(self):
         return 'Technology Commands'
@@ -53,6 +53,12 @@
         self.simple_modem.Connect(connect_props)
 
     def Disconnect(self):
+        """
+        Disconnect Modem.
+
+        Returns:
+            True - to indicate that flimflam may autoconnect again.
+        """
         try:
             self.modem.Disconnect()
         except dbus.exceptions.DBusException, e:
@@ -61,6 +67,7 @@
                 pass
             else:
                 raise e
+        return True
 
     def __str__(self):
         return 'Modem Commands'
@@ -73,6 +80,13 @@
         self.device = device
         self.service = None
 
+    def GetService(self):
+        service = self.flim.FindCellularService()
+        if not service:
+            raise error.TestFail(
+                'Service failed to appear when using device commands.')
+        return service
+
     def Enable(self):
         self.device.SetProperty('Powered', True)
 
@@ -81,21 +95,54 @@
         self.device.SetProperty('Powered', False)
 
     def Connect(self):
-        service = self.flim.FindCellularService()
-        if not service:
-            raise error.TestFail('Service failed to appear when '
-                                 'using device commands.')
-        service.Connect()
-        self.service = service
+        self.GetService().Connect()
 
     def Disconnect(self):
-        self.service.Disconnect()
-        self.service = None
+        """
+        Disconnect Modem.
+
+        Returns:
+            False - to indicate that flimflam may not autoconnect again.
+        """
+        self.GetService().Disconnect()
+        return False
 
     def __str__(self):
         return 'Device Commands'
 
 
+class MixedRandomCommands():
+    """Control the modem using a mixture of commands on device, modems, etc."""
+    def __init__(self, commands_list):
+        self.commands_list = commands_list
+
+    def PickRandomCommands(self):
+        return self.commands_list[random.randrange(len(self.commands_list))]
+
+    def Enable(self):
+        cmds = self.PickRandomCommands()
+        logging.info('Enable with %s' % cmds)
+        cmds.Enable()
+
+    def Disable(self):
+        cmds = self.PickRandomCommands()
+        logging.info('Disable with %s' % cmds)
+        cmds.Disable()
+
+    def Connect(self):
+        cmds = self.PickRandomCommands()
+        logging.info('Connect with %s' % cmds)
+        cmds.Connect()
+
+    def Disconnect(self):
+        cmds = self.PickRandomCommands()
+        logging.info('Disconnect with %s' % cmds)
+        return cmds.Disconnect()
+
+    def __str__(self):
+        return 'Mixed Commands'
+
+
 class network_3GModemControl(test.test):
     version = 1
 
@@ -138,31 +185,14 @@
             lambda: not self.flim.FindCellularService(timeout=1),
             error.TestFail('Service should not be available.'))
 
-    def EnsureEnabled(self):
+    def EnsureEnabled(self, check_idle):
         """
-        Ensure modem enabled, device powered on, and service idle.
+        Ensure modem enabled, device powered and service exists.
 
-        Raises:
-            error.TestFail if the states are not consistent
-        """
-        utils.poll_for_condition(
-            lambda: self.CompareModemPowerState(self.modem_manager,
-                                                self.modem_path, True),
-            error.TestFail('Modem failed to enter state Enabled'))
-        utils.poll_for_condition(
-            lambda: self.CompareDevicePowerState(self.device, True),
-            error.TestFail('Device failed to enter state Powered=True.'))
-        # wait for service to appear and then enter idle state
-        service = self.flim.FindCellularService()
-        if not service:
-            error.TestFail('Service failed to appear for enabled modem.')
-        utils.poll_for_condition(
-            lambda: self.CompareServiceState(service, ['idle']),
-            error.TestFail('Service failed to enter idle state.'))
-
-    def EnsureConnected(self):
-        """
-        Ensure modem connected, device powered on, service connected.
+        Args:
+            check_idle: if True, then ensure that the service is idle
+                        (i.e. not connected) otherwise ignore the
+                        service state
 
         Raises:
             error.TestFail if the states are not consistent.
@@ -174,10 +204,24 @@
         utils.poll_for_condition(
             lambda: self.CompareDevicePowerState(self.device, True),
             error.TestFail('Device failed to enter state Powered=True.'))
-        # wait for service to appear and then enter a connected state
+        # wait for service to appear
         service = self.flim.FindCellularService()
         if not service:
-            error.TestFail('Service failed to appear for connected modem.')
+            error.TestFail('Service failed to appear for enabled modem.')
+        if check_idle:
+            utils.poll_for_condition(
+                lambda: self.CompareServiceState(service, ['idle']),
+                error.TestFail('Service failed to enter idle state.'))
+
+    def EnsureConnected(self):
+        """
+        Ensure modem connected, device powered on, service connected.
+
+        Raises:
+            error.TestFail if the states are not consistent.
+        """
+        self.EnsureEnabled(check_idle=False)
+        service = self.flim.FindCellularService()
         utils.poll_for_condition(
             lambda: self.CompareServiceState(service,
                                              ['ready', 'portal', 'online']),
@@ -196,26 +240,35 @@
 
         """
         logging.info('Testing using %s' % commands)
+
         commands.Enable()
-        self.EnsureEnabled()
-        commands.Disable()
-        self.EnsureDisabled()
-        commands.Enable()
-        self.EnsureEnabled()
-        commands.Connect()
-        self.EnsureConnected()
-        commands.Disconnect()
-        self.EnsureEnabled()
-        commands.Connect()
-        self.EnsureConnected()
+        self.EnsureEnabled(check_idle=not self.autoconnect)
+
         commands.Disable()
         self.EnsureDisabled()
 
-    def run_once(self, connect_count=10, maximum_avg_assoc_time_seconds=5):
+        commands.Enable()
+        self.EnsureEnabled(check_idle=not self.autoconnect)
+
+        if not self.autoconnect:
+            commands.Connect()
+        self.EnsureConnected()
+
+        will_autoreconnect = commands.Disconnect()
+        if not (self.autoconnect and will_autoreconnect):
+            self.EnsureEnabled(check_idle=True)
+            commands.Connect()
+        self.EnsureConnected()
+
+        commands.Disable()
+        self.EnsureDisabled()
+
+    def run_once(self, autoconnect, mixed_iterations=2):
         # Use a backchannel so that flimflam will restart when the
         # test is over.  This ensures flimflam is in a known good
         # state even if this test fails.
         with backchannel.Backchannel():
+            self.autoconnect = autoconnect
             self.flim = flimflam.FlimFlam()
             self.device = self.flim.FindCellularDevice()
             self.modem_manager, self.modem_path = mm.PickOneModem('')
@@ -227,7 +280,9 @@
                                                      modem_commands)
             device_commands = DeviceCommands(self.flim, self.device)
 
-            with cell_tools.DisableAutoConnectContext(self.device, self.flim):
+            with cell_tools.AutoConnectContext(self.device,
+                                               self.flim,
+                                               autoconnect):
                 # Get to a well known state.
                 self.flim.DisableTechnology('cellular')
                 self.EnsureDisabled()
@@ -235,3 +290,10 @@
                 self.TestCommands(modem_commands)
                 self.TestCommands(technology_commands)
                 self.TestCommands(device_commands)
+
+                # Run several times using commands mixed from each type
+                mixed = MixedRandomCommands([modem_commands,
+                                             technology_commands,
+                                             device_commands])
+                for _ in range(mixed_iterations):
+                    self.TestCommands(mixed)