Initial xBuddy for devserver

Contains most of the basic functionality of the xBuddy rpc, as outlined
in the Design Doc found in ChromeOs Installer.
  - xbuddy, xbuddy_list, xbuddy_capacity rpcs on devserver
  - xbuddy's path translation:
    - defined default version aliases
    - defined default xbuddy artifact aliases
  - xbuddy build cache:
    - on build_id cache hit, serves corresponding image/artifact
    - on build_id cache miss, downloads from Google Storage, then serves
    - maintains a cache of 5 downloaded builds & record of their last
      access time in a separate timestamp directory

Plus some housekeeping of devserver constants

BUG=chromium:252941
TEST=manual and unit tests

Manual (for testing devserver rpcs): Run the devserver locally, attempt
to access each of the following addresses from browser

  1. http://localhost:8080/xbuddy?path=/parrot-release/R21-2461.0.0/
  test&return_update_url=t
  Expect: Several seconds of lag as image is downloaded, then a
  url to the image dir, such as
  http://localhost:8080/static/parrot-release/R21-2461.0.0
  [Note, using an IP address instead of localhost should return that IP
  address]

  2. http://localhost:8080/xbuddy?path=/parrot-release/R21-2461.0.0/test
  Expect: A download of the right chromeos_test_image.bin

  3. http://localhost:8080/xbuddy_capacity/
  Expect: Just '5', the default xbuddy capacity

  4. http://localhost:8080/xbuddy_list/
  Expect: A string that lists the previously requested build and how
  long ago it was accessed

  5. More combinations of board/version/alias should work as well, with
  xbuddy_list and the default devserver static folder's contents
  reflecting normal caching behavior.

Unit Tests (for xbuddy functions): run xbuddy_unittests.py

Change-Id: I612cbb3ee907bb70907669d6db20f266157c0244
Reviewed-on: https://gerrit.chromium.org/gerrit/59287
Reviewed-by: Joy Chen <joychen@chromium.org>
Tested-by: Joy Chen <joychen@chromium.org>
Commit-Queue: Joy Chen <joychen@chromium.org>
diff --git a/xbuddy_unittest.py b/xbuddy_unittest.py
new file mode 100644
index 0000000..0e07149
--- /dev/null
+++ b/xbuddy_unittest.py
@@ -0,0 +1,155 @@
+#!/usr/bin/python
+
+# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unit tests for xbuddy.py."""
+
+import os
+import shutil
+import time
+import unittest
+
+import mox
+
+import xbuddy
+
+#pylint: disable=W0212
+class xBuddyTest(mox.MoxTestBase):
+  """Regression tests for xbuddy."""
+  def setUp(self):
+    mox.MoxTestBase.setUp(self)
+
+    self.static_image_dir = '/tmp/static-dir/'
+
+    self.mock_xb = xbuddy.XBuddy(self.static_image_dir)
+    os.makedirs(self.static_image_dir)
+
+  def tearDown(self):
+    """Removes testing files."""
+    shutil.rmtree(self.static_image_dir)
+
+  def testParseBoolean(self):
+    """Check that some common True/False strings are handled."""
+    self.assertEqual(xbuddy.XBuddy.ParseBoolean(None), False)
+    self.assertEqual(xbuddy.XBuddy.ParseBoolean('false'), False)
+    self.assertEqual(xbuddy.XBuddy.ParseBoolean('bs'), False)
+    self.assertEqual(xbuddy.XBuddy.ParseBoolean('true'), True)
+    self.assertEqual(xbuddy.XBuddy.ParseBoolean('y'), True)
+
+  def _testResolveVersion(self):
+    # TODO (joyc)
+    pass
+
+  def testBasicInterpretPath(self):
+    """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=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=path), expected)
+
+    path = "parrot-release/R27-2455.0.0/bad_alias"
+    self.assertRaises(xbuddy.XBuddyException,
+                      self.mock_xb._InterpretPath,
+                      path=path)
+
+  def testUnpackArgsWithVersionAliases(self):
+    # TODO (joyc)
+    pass
+
+  def testLookupVersion(self):
+    # TODO (joyc)
+    pass
+
+  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)
+
+    # reference second one again
+    time.sleep(0.5)
+    self.mock_xb._UpdateTimestamp(build_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)
+
+  ############### 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"
+
+    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')
+      self.mock_xb._Download(mox.IsA(str), mox.IsA(str))
+
+    self.mox.ReplayAll()
+
+    # requires default capacity
+    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)
+
+    # check that b,c,d,e,f are still stored
+    result = self.mock_xb._ListBuilds()
+    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')
+
+    # Get b,a
+    self.mock_xb.Get(path_b, None)
+    time.sleep(0.5)
+    self.mock_xb.Get(path_a, None)
+    time.sleep(0.5)
+
+    # check that d,e,f,b,a are still stored
+    result = self.mock_xb._ListBuilds()
+    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')
+
+    self.mox.VerifyAll()
+
+
+if __name__ == '__main__':
+  unittest.main()