conman:  WifiSimulateWireless experiment.

Adds an experiment to simulate operating in wireless mode by making
connection checks fail.  This experiment will have no effect if cwmpd
has been failing to reach the ACS for at least 60 seconds.

Change-Id: I67c50979bc50e1b8019e33cd73d86c676f931a2c
diff --git a/conman/Makefile b/conman/Makefile
index 8ff1719..0faf301 100644
--- a/conman/Makefile
+++ b/conman/Makefile
@@ -9,6 +9,7 @@
       echo 'echo "(gpylint-missing)" >&2'; \
     fi \
 )
+NOINSTALL=%_test.py options.py experiment.py experiment_testutils.py
 
 all:
 
@@ -35,7 +36,7 @@
 
 install:
 	mkdir -p $(LIBDIR) $(BINDIR)
-	$(INSTALL) -m 0644 $(filter-out %_test.py, $(wildcard *.py)) $(LIBDIR)/
+	$(INSTALL) -m 0644 $(filter-out $(NOINSTALL), $(wildcard *.py)) $(LIBDIR)/
 	$(INSTALL) -m 0755 main.py $(LIBDIR)/
 	rm -f $(BINDIR)/conman
 	ln -s /usr/conman/main.py $(BINDIR)/conman
diff --git a/conman/experiment.py b/conman/experiment.py
new file mode 120000
index 0000000..b9d7638
--- /dev/null
+++ b/conman/experiment.py
@@ -0,0 +1 @@
+../experiment.py
\ No newline at end of file
diff --git a/conman/experiment_testutils.py b/conman/experiment_testutils.py
new file mode 120000
index 0000000..cca000f
--- /dev/null
+++ b/conman/experiment_testutils.py
@@ -0,0 +1 @@
+../experiment_testutils.py
\ No newline at end of file
diff --git a/conman/interface.py b/conman/interface.py
index d45f42e..71a7046 100755
--- a/conman/interface.py
+++ b/conman/interface.py
@@ -8,6 +8,7 @@
 import re
 import subprocess
 
+import experiment
 import wpactrl
 
 METRIC_5GHZ = 20
@@ -15,6 +16,10 @@
 METRIC_24GHZ = 22
 METRIC_TEMPORARY_CONNECTION_CHECK = 99
 
+experiment.register('WifiSimulateWireless')
+CWMP_PATH = '/tmp/cwmp'
+MAX_ACS_FAILURE_S = 60
+
 
 class Interface(object):
   """Represents an interface.
@@ -310,6 +315,33 @@
       os.unlink(self._acs_autoprovisioning_filepath)
     super(Bridge, self).delete_route()
 
+  def _connection_check(self, check_acs):
+    """Support for WifiSimulateWireless."""
+    failure_s = self._acs_session_failure_s()
+    if (experiment.enabled('WifiSimulateWireless')
+        and failure_s < MAX_ACS_FAILURE_S):
+      logging.debug('WifiSimulateWireless: failing bridge connection check (no '
+                    'ACS contact for %d seconds, max %d seconds)',
+                    failure_s, MAX_ACS_FAILURE_S)
+      return False
+
+    return super(Bridge, self)._connection_check(check_acs)
+
+  def _acs_session_failure_s(self):
+    """How long have we been failing to connect to the ACS?
+
+    Returns:
+      The number of seconds between the last attempted ACS session and the last
+      successful ACS session.
+    """
+    contact = os.path.join(CWMP_PATH, 'acscontact')
+    connected = os.path.join(CWMP_PATH, 'acsconnected')
+
+    if not os.path.exists(contact) or not os.path.exists(connected):
+      return 0
+
+    return os.stat(contact).st_mtime - os.stat(connected).st_mtime
+
 
 class Wifi(Interface):
   """Represents a wireless interface."""
diff --git a/conman/interface_test.py b/conman/interface_test.py
index 8a376a6..2c82aaa 100755
--- a/conman/interface_test.py
+++ b/conman/interface_test.py
@@ -7,17 +7,21 @@
 import os
 import shutil
 import tempfile
+import time
+
+# This has to be called before another module calls it with a higher log level.
+# pylint: disable=g-import-not-at-top
+logging.basicConfig(level=logging.DEBUG)
 
 # This is in site-packages on the device, but not when running tests, and so
 # raises lint errors.
 # pylint: disable=g-bad-import-order
 import wpactrl
 
+import experiment_testutils
 import interface
 from wvtest import wvtest
 
-logging.basicConfig(level=logging.DEBUG)
-
 
 class FakeInterfaceMixin(object):
   """Replace Interface methods which interact with the system."""
@@ -406,5 +410,69 @@
     shutil.rmtree(wifiinfo_path)
 
 
+@wvtest.wvtest
+def simulate_wireless_test():
+  """Test the WifiSimulateWireless experiment."""
+  unused_raii = experiment_testutils.MakeExperimentDirs()
+
+  tmp_dir = tempfile.mkdtemp()
+  interface.CWMP_PATH = tempfile.mkdtemp()
+  interface.MAX_ACS_FAILURE_S = 1
+
+  contact = os.path.join(interface.CWMP_PATH, 'acscontact')
+  connected = os.path.join(interface.CWMP_PATH, 'acsconnected')
+
+  try:
+    autoprov_filepath = os.path.join(tmp_dir, 'autoprov')
+    b = Bridge('br0', '10', acs_autoprovisioning_filepath=autoprov_filepath)
+    b.add_moca_station(0)
+    b.set_gateway_ip('192.168.1.1')
+    b.set_connection_check_result('succeed')
+    b.initialize()
+
+    # Initially, the connection check passes.
+    wvtest.WVPASS(b.internet())
+
+    # Enable the experiment.
+    experiment_testutils.enable('WifiSimulateWireless')
+    # Calling update_routes overwrites the connection status cache, which we
+    # need in order to see the effects we are looking for immediately
+    # (ConnectionManager calls this every few seconds).
+    b.update_routes()
+    wvtest.WVFAIL(b.internet())
+
+    # Create an ACS connection attempt.
+    open(contact, 'w')
+    b.update_routes()
+    wvtest.WVFAIL(b.internet())
+
+    # Record success.
+    open(connected, 'w')
+    b.update_routes()
+    wvtest.WVFAIL(b.internet())
+
+    # Disable the experiment and the connection check should pass again.
+    experiment_testutils.disable('WifiSimulateWireless')
+    b.update_routes()
+    wvtest.WVPASS(b.internet())
+
+    # Reenable the experiment and the connection check should fail again.
+    experiment_testutils.enable('WifiSimulateWireless')
+    b.update_routes()
+    wvtest.WVFAIL(b.internet())
+
+    # Wait until we've failed for long enough for the experiment to "expire",
+    # then log another attempt without success.  Make sure the connection check
+    # passes.
+    time.sleep(interface.MAX_ACS_FAILURE_S)
+    open(contact, 'w')
+    b.update_routes()
+    wvtest.WVPASS(b.internet())
+
+  finally:
+    shutil.rmtree(tmp_dir)
+    shutil.rmtree(interface.CWMP_PATH)
+
+
 if __name__ == '__main__':
   wvtest.wvtest_main()