XBuddy: latest-local image + fix xbuddy_list bug

Serve the latest-local image for a given board.

Symlinks the image directory to a corresponding directory in the devserver
/static folder, and deletes the symlinked file when past
XBUDDY_CAPACITY.

ALSO: fix lexicographic sorting of time stamps + pretty print
xbuddy_list

BUG=chromium:252941
BUG=chromium:252942
TEST=Manual

Either run ./build_image more than 5 times, or call
http://{your_ip}:8080/xbuddy?path=x86-generic/latest-local/test
on over 5 different boards.
WARNING: xBuddy's cache management is on, so this may delete some of
the images currently in your build/images directory.

Should correctly serve the test image every time, and delete the last
referenced image (and its corresponding timestamp file) from disk.

Locally built images and images downloaded from GS count equivalently.
i.e. There should never be more than 5 at any given time.

Change-Id: I2f5c4e2d67365f3523ddfdeec2ddedbd38009e32
Reviewed-on: https://gerrit.chromium.org/gerrit/60707
Reviewed-by: Ryan Cui <rcui@chromium.org>
Commit-Queue: Joy Chen <joychen@chromium.org>
Reviewed-by: Joy Chen <joychen@chromium.org>
Tested-by: Joy Chen <joychen@chromium.org>
diff --git a/xbuddy_unittest.py b/xbuddy_unittest.py
index bcea713..38eae34 100644
--- a/xbuddy_unittest.py
+++ b/xbuddy_unittest.py
@@ -8,6 +8,7 @@
 
 import os
 import shutil
+import tempfile
 import time
 import unittest
 
@@ -21,14 +22,20 @@
   def setUp(self):
     mox.MoxTestBase.setUp(self)
 
-    self.static_image_dir = '/tmp/static-dir/'
+    self.static_image_dir = tempfile.mkdtemp('xbuddy_unittest_static')
+    self.root_dir = tempfile.mkdtemp('xbuddy_unittest_ds_root')
 
-    self.mock_xb = xbuddy.XBuddy(self.static_image_dir)
-    os.makedirs(self.static_image_dir)
+    self.mock_xb = xbuddy.XBuddy(
+      root_dir=self.root_dir,
+      static_dir=self.static_image_dir
+    )
+    self.images_dir = tempfile.mkdtemp('xbuddy_unittest_images')
+    self.mock_xb.images_dir = self.images_dir
 
   def tearDown(self):
     """Removes testing files."""
     shutil.rmtree(self.static_image_dir)
+    shutil.rmtree(self.images_dir)
 
   def testParseBoolean(self):
     """Check that some common True/False strings are handled."""
@@ -46,29 +53,21 @@
     """Basic checks for splitting a path"""
     path = ('parrot-release', 'R27-2455.0.0', 'test')
     expected = ('parrot-release', 'R27-2455.0.0', 'test')
-    self.assertEqual(self.mock_xb._InterpretPath(path_parts=path), expected)
+    self.assertEqual(self.mock_xb._InterpretPath(path_list=path), expected)
 
     path = ('parrot-release', 'R27-2455.0.0', 'full_payload')
     expected = ('parrot-release', 'R27-2455.0.0', 'full_payload')
-    self.assertEqual(self.mock_xb._InterpretPath(path_parts=path), expected)
+    self.assertEqual(self.mock_xb._InterpretPath(path_list=path), expected)
 
     path = ('parrot-release', 'R27-2455.0.0')
     expected = ('parrot-release', 'R27-2455.0.0', 'test')
-    self.assertEqual(self.mock_xb._InterpretPath(path_parts=path), expected)
-
-    path = ('parrot-release', 'R27-2455.0.0', 'bad_alias')
-    self.assertRaises(xbuddy.XBuddyException,
-                      self.mock_xb._InterpretPath,
-                      path_parts=path)
+    self.assertEqual(self.mock_xb._InterpretPath(path_list=path), expected)
 
     path = ('parrot-release', 'R27-2455.0.0', 'too', 'many', 'pieces')
     self.assertRaises(xbuddy.XBuddyException,
                       self.mock_xb._InterpretPath,
-                      path_parts=path)
+                      path_list=path)
 
-  def testUnpackArgsWithVersionAliases(self):
-    # TODO (joyc)
-    pass
 
   def testLookupVersion(self):
     # TODO (joyc)
@@ -77,41 +76,54 @@
   def testTimestampsAndList(self):
     """Creation and listing of builds according to their timestamps."""
     # make 3 different timestamp files
-    build_id11 = 'b1/v1'
-    build_id12 = 'b1/v2'
-    build_id23 = 'b2/v3'
-    self.mock_xb._UpdateTimestamp(build_id11)
-    time.sleep(0.5)
-    self.mock_xb._UpdateTimestamp(build_id12)
-    time.sleep(0.5)
-    self.mock_xb._UpdateTimestamp(build_id23)
+    b_id11 = 'b1/v1'
+    b_id12 = 'b1/v2'
+    b_id23 = 'b2/v3'
+    xbuddy.Timestamp.UpdateTimestamp(self.mock_xb._timestamp_folder, b_id11)
+    time.sleep(0.05)
+    xbuddy.Timestamp.UpdateTimestamp(self.mock_xb._timestamp_folder, b_id12)
+    time.sleep(0.05)
+    xbuddy.Timestamp.UpdateTimestamp(self.mock_xb._timestamp_folder, b_id23)
 
     # reference second one again
-    time.sleep(0.5)
-    self.mock_xb._UpdateTimestamp(build_id12)
+    time.sleep(0.05)
+    xbuddy.Timestamp.UpdateTimestamp(self.mock_xb._timestamp_folder, b_id12)
 
     # check that list returns the same 3 things, in last referenced order
-    result = self.mock_xb._ListBuilds()
-    self.assertEqual(result[0][0], build_id12)
-    self.assertEqual(result[1][0], build_id23)
-    self.assertEqual(result[2][0], build_id11)
+    result = self.mock_xb._ListBuildTimes()
+    self.assertEqual(result[0][0], b_id12)
+    self.assertEqual(result[1][0], b_id23)
+    self.assertEqual(result[2][0], b_id11)
+
+  def testSyncRegistry(self):
+    # check that there are no builds initially
+    result = self.mock_xb._ListBuildTimes()
+    self.assertEqual(len(result), 0)
+
+    # set up the dummy build/images directory with images
+    boards = ['a', 'b']
+    versions = ['v1', 'v2']
+    for b in boards:
+      os.makedirs(os.path.join(self.mock_xb.images_dir, b))
+      for v in versions:
+        os.makedirs(os.path.join(self.mock_xb.images_dir, b, v))
+
+    # Sync and check that they've been added to xBuddy's registry
+    self.mock_xb._SyncRegistryWithBuildImages()
+    result = self.mock_xb._ListBuildTimes()
+    self.assertEqual(len(result), 4)
 
   ############### Public Methods
   def testXBuddyCaching(self):
     """Caching & replacement of timestamp files."""
-
-    path_a = ('a', 'latest-local', 'test')
-    path_b = ('b', 'latest-local', 'test')
-    path_c = ('c', 'latest-local', 'test')
-    path_d = ('d', 'latest-local', 'test')
-    path_e = ('e', 'latest-local', 'test')
-    path_f = ('f', 'latest-local', 'test')
+    path_a = ('a', 'R0', 'test')
+    path_b = ('b', 'R0', 'test')
 
     self.mox.StubOutWithMock(self.mock_xb, '_ResolveVersion')
     self.mox.StubOutWithMock(self.mock_xb, '_Download')
     for _ in range(8):
       self.mock_xb._ResolveVersion(mox.IsA(str),
-                                   mox.IsA(str)).AndReturn('latest-local')
+                                   mox.IsA(str)).AndReturn('R0')
       self.mock_xb._Download(mox.IsA(str), mox.IsA(str))
 
     self.mox.ReplayAll()
@@ -120,42 +132,32 @@
     self.assertEqual(self.mock_xb.Capacity(), '5')
 
     # Get 6 different images: a,b,c,d,e,f
-    self.mock_xb.Get(path_a, None)
-    time.sleep(0.5)
-    self.mock_xb.Get(path_b, None)
-    time.sleep(0.5)
-    self.mock_xb.Get(path_c, None)
-    time.sleep(0.5)
-    self.mock_xb.Get(path_d, None)
-    time.sleep(0.5)
-    self.mock_xb.Get(path_e, None)
-    time.sleep(0.5)
-    self.mock_xb.Get(path_f, None)
-    time.sleep(0.5)
+    images = ['a', 'b', 'c', 'd', 'e', 'f']
+    for c in images:
+      self.mock_xb.Get((c, 'R0', 'test'), None)
+      time.sleep(0.05)
 
     # check that b,c,d,e,f are still stored
-    result = self.mock_xb._ListBuilds()
+    result = self.mock_xb._ListBuildTimes()
     self.assertEqual(len(result), 5)
-    self.assertEqual(result[4][0], 'b/latest-local')
-    self.assertEqual(result[3][0], 'c/latest-local')
-    self.assertEqual(result[2][0], 'd/latest-local')
-    self.assertEqual(result[1][0], 'e/latest-local')
-    self.assertEqual(result[0][0], 'f/latest-local')
+
+    # Flip the list to get reverse chronological order
+    images.reverse()
+    for i in range(5):
+      self.assertEqual(result[i][0], '%s/R0' % images[i])
 
     # Get b,a
     self.mock_xb.Get(path_b, None)
-    time.sleep(0.5)
+    time.sleep(0.05)
     self.mock_xb.Get(path_a, None)
-    time.sleep(0.5)
+    time.sleep(0.05)
 
     # check that d,e,f,b,a are still stored
-    result = self.mock_xb._ListBuilds()
+    result = self.mock_xb._ListBuildTimes()
     self.assertEqual(len(result), 5)
-    self.assertEqual(result[4][0], 'd/latest-local')
-    self.assertEqual(result[3][0], 'e/latest-local')
-    self.assertEqual(result[2][0], 'f/latest-local')
-    self.assertEqual(result[1][0], 'b/latest-local')
-    self.assertEqual(result[0][0], 'a/latest-local')
+    images_expected = ['a', 'b', 'f', 'e', 'd']
+    for i in range(5):
+      self.assertEqual(result[i][0], '%s/R0' % images_expected[i])
 
     self.mox.VerifyAll()