conman:  Update previous scan results.

Rather than completely resetting cached scan results, update them
based on the most recent scan.

BUG=29759539

Change-Id: I6a13fc871f438e3a0a5c1aad73a4fa1dc3048f4d
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
index 2ab4763..7e2b24b 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -8,6 +8,7 @@
 import json
 import logging
 import os
+import random
 import re
 import subprocess
 import time
@@ -202,7 +203,8 @@
                moca_tmp_dir='/tmp/cwmp/monitoring/moca2',
                wpa_control_interface='/var/run/wpa_supplicant',
                run_duration_s=1, interface_update_period=5,
-               wifi_scan_period_s=120, wlan_retry_s=15, acs_update_wait_s=10):
+               wifi_scan_period_s=120, wlan_retry_s=15, acs_update_wait_s=10,
+               bssid_cycle_length_s=30):
 
     self._tmp_dir = tmp_dir
     self._config_dir = config_dir
@@ -215,6 +217,7 @@
     self._wifi_scan_period_s = wifi_scan_period_s
     self._wlan_retry_s = wlan_retry_s
     self._acs_update_wait_s = acs_update_wait_s
+    self._bssid_cycle_length_s = bssid_cycle_length_s
     self._wlan_configuration = {}
 
     # Make sure all necessary directories exist.
@@ -658,7 +661,13 @@
     logging.info('Done scanning on %s', wifi.name)
     items = [(bss_info, 3) for bss_info in with_ie]
     items += [(bss_info, 1) for bss_info in without_ie]
-    wifi.cycler = cycler.AgingPriorityCycler(cycle_length_s=30, items=items)
+    if not hasattr(wifi, 'cycler'):
+      wifi.cycler = cycler.AgingPriorityCycler(
+          cycle_length_s=self._bssid_cycle_length_s)
+    # Shuffle items to undefined determinism in scan results + dict
+    # implementation unfairly biasing BSSID order.
+    random.shuffle(items)
+    wifi.cycler.update(items)
 
   def _find_bssids(self, band):
     def supports_autoprovisioning(oui, vendor_ie):
diff --git a/conman/connection_manager_test.py b/conman/connection_manager_test.py
index 0297ca1..954e2e3 100755
--- a/conman/connection_manager_test.py
+++ b/conman/connection_manager_test.py
@@ -6,6 +6,7 @@
 import os
 import shutil
 import tempfile
+import time
 
 import connection_manager
 import interface_test
@@ -539,6 +540,7 @@
                               run_duration_s=run_duration_s,
                               interface_update_period=interface_update_period,
                               wifi_scan_period_s=wifi_scan_period_s,
+                              bssid_cycle_length_s=0.05,
                               **cm_kwargs)
 
         c.test_interface_update_period = interface_update_period
@@ -805,8 +807,11 @@
   wvtest.WVFAIL(c.wifi_for_band(band).acs())
 
   c.can_connect_to_s2 = True
-  # Give it time to try all BSSIDs.
-  for _ in range(3):
+  # Give it time to try all BSSIDs.  This means sleeping long enough that
+  # everything in the cycler is active, then doing n+1 loops (the n+1st loop is
+  # when we decided that the SSID in the nth loop was successful).
+  time.sleep(c._bssid_cycle_length_s)
+  for _ in range(len(c.wifi_for_band(band).cycler) + 1):
     c.run_once()
   s2_bss = iw.BssInfo('01:23:45:67:89:ab', 's2')
   wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, s2_bss)
@@ -954,8 +959,8 @@
   # The next 2.4 GHz scan will have results.
   c.interface_with_scan_results = c.wifi_for_band('2.4').name
   c.run_until_scan('2.4')
-  # Now run 3 cycles, so that s2 will have been tried.
-  for _ in range(3):
+  # Now run for enough cycles that s2 will have been tried.
+  for _ in range(len(c.wifi_for_band('2.4').cycler)):
     c.run_once()
   c.run_until_interface_update()
   wvtest.WVPASS(c.acs())
@@ -1044,10 +1049,10 @@
   wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
   wvtest.WVFAIL(c.wifi_for_band('5').current_route())
 
-  # The 2.4 GHz scan will have results that will lead to ACS access.
+  # The scan will have results that will lead to ACS access.
   c.interface_with_scan_results = c.wifi_for_band('2.4').name
   c.run_until_scan('5')
-  for _ in range(3):
+  for _ in range(len(c.wifi_for_band('2.4').cycler)):
     c.run_once()
   c.run_until_interface_update()
   wvtest.WVPASS(c.acs())
diff --git a/conman/cycler.py b/conman/cycler.py
index ff65bfc..23f2ce4 100755
--- a/conman/cycler.py
+++ b/conman/cycler.py
@@ -27,9 +27,10 @@
       queue after being automatically reinserted.
       items: Initial items for the queue, as tuples of (item, priority).
     """
-    t = time.time()
-    self._items = {item: [priority, t] for item, priority in items}
     self._min_time_in_queue_s = cycle_length_s
+    self._items = {}
+    if items:
+      self.update(items)
 
   def empty(self):
     return not self._items
@@ -79,3 +80,23 @@
 
     return result
 
+  def update(self, items):
+    """Update to the given items, adding new ones and removing old ones.
+
+    Args:
+      items:  An iterable of (item, priority).
+    """
+    now = time.time()
+    new_items = {}
+    for item, priority in items:
+      t = now
+      existing = self._items.get(item, None)
+      if existing:
+        t = existing[1]
+      new_items[item] = [priority, t]
+
+    self._items = new_items
+
+  def __len__(self):
+    return len(self._items)
+
diff --git a/conman/cycler_test.py b/conman/cycler_test.py
index c4e498b..de1e6c0 100755
--- a/conman/cycler_test.py
+++ b/conman/cycler_test.py
@@ -20,26 +20,40 @@
   # We should get all three in order, since they all have the same insertion
   # time.  They will all get slightly different insertion times, but next()
   # should be fast enough that the differences don't make much difference.
-  wvtest.WVPASS(c.peek() == 'A')
-  wvtest.WVPASS(c.next() == 'A')
-  wvtest.WVPASS(c.next() == 'B')
-  wvtest.WVPASS(c.next() == 'C')
+  wvtest.WVPASSEQ(c.peek(), 'A')
+  wvtest.WVPASSEQ(c.next(), 'A')
+  wvtest.WVPASSEQ(c.next(), 'B')
+  wvtest.WVPASSEQ(c.next(), 'C')
   wvtest.WVPASS(c.peek() is None)
   wvtest.WVPASS(c.next() is None)
   wvtest.WVPASS(c.next() is None)
 
   # Now, wait for items to be ready again and just cycle one of them.
   time.sleep(cycle_length_s)
-  wvtest.WVPASS(c.next() == 'A')
+  wvtest.WVPASSEQ(c.next(), 'A')
 
   # Now, if we wait 1.9 cycles, the aged priorities will be as follows:
   # A: 0.9 * 10 = 9
   # B: 1.9 * 5 = 9.5
   # C: 1.9 * 1 = 1.9
   time.sleep(cycle_length_s * 1.9)
-  wvtest.WVPASS(c.next() == 'B')
-  wvtest.WVPASS(c.next() == 'A')
-  wvtest.WVPASS(c.next() == 'C')
+  wvtest.WVPASSEQ(c.next(), 'B')
+  wvtest.WVPASSEQ(c.next(), 'A')
+  wvtest.WVPASSEQ(c.next(), 'C')
+
+  # Update c, keeping A as-is, removing B, updating C's priority, and adding D.
+  # Sleep for two cycles.  After the first cycle, D has priority 20 and A and C
+  # have priority 0 (since we just cycled them).  After the second cycle, the
+  # priorities are as follows:
+  # A: 1 * 10 = 10
+  # C: 1 * 20 = 20
+  # D: 2 * 20 = 40
+  c.update((('A', 10), ('C', 20), ('D', 20)))
+  time.sleep(cycle_length_s * 2)
+  wvtest.WVPASSEQ(c.next(), 'D')
+  wvtest.WVPASSEQ(c.next(), 'C')
+  wvtest.WVPASSEQ(c.next(), 'A')
+  wvtest.WVPASS(c.next() is None)
 
 if __name__ == '__main__':
   wvtest.wvtest_main()