Implement DevInstall binhost uploads in the Build API.

Caller passes in a directory that is populated with the packages
specific to dev-install, and a new Packages file is created in
that directory as well.

BUG=chromium:994242
TEST=manual, run_tests

Change-Id: I9cd7b87b645d4c55c36ee1d6235fd03ebbd7a4dd
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/1773756
Tested-by: Michael Mortensen <mmortensen@google.com>
Commit-Queue: Michael Mortensen <mmortensen@google.com>
Reviewed-by: Alex Klein <saklein@chromium.org>
diff --git a/api/controller/binhost_unittest.py b/api/controller/binhost_unittest.py
index 017e7fc..75285dc 100644
--- a/api/controller/binhost_unittest.py
+++ b/api/controller/binhost_unittest.py
@@ -7,6 +7,7 @@
 
 from __future__ import print_function
 
+import os
 import mock
 
 from chromite.api import api_config
@@ -14,6 +15,7 @@
 from chromite.api.gen.chromite.api import binhost_pb2
 from chromite.lib import cros_build_lib
 from chromite.lib import cros_test_lib
+from chromite.lib import osutils
 from chromite.service import binhost as binhost_service
 
 
@@ -135,3 +137,100 @@
     with self.assertRaises(cros_build_lib.DieSystemExit):
       binhost.RegenBuildCache(input_proto, self.response, self.api_config)
     regen_cache.assert_not_called()
+
+
+class PrepareDevInstallerBinhostUploadsTest(cros_test_lib.MockTempDirTestCase,
+                                            api_config.ApiConfigMixin):
+  """Tests for the UploadDevInstallerPrebuilts stage."""
+  def setUp(self):
+    # target packages dir
+    self.chroot_path = os.path.join(self.tempdir, 'chroot')
+    self.sysroot_path = '/build/target'
+    self.chroot_path = os.path.join(self.tempdir, 'chroot')
+    full_sysroot_path = os.path.join(self.chroot_path,
+                                     self.sysroot_path.lstrip(os.sep))
+    self.full_sysroot_package_path = os.path.join(full_sysroot_path,
+                                                  'packages')
+    osutils.SafeMakedirs(self.full_sysroot_package_path)
+    self.uploads_dir = os.path.join(self.tempdir, 'uploads_dir')
+    osutils.SafeMakedirs(self.uploads_dir)
+    # Create packages/Packages file
+    packages_file = os.path.join(self.full_sysroot_package_path, 'Packages')
+    packages_content = """\
+USE: test
+
+CPV: app-arch/brotli-1.0.6
+
+CPV: app-arch/zip-3.0-r3
+
+CPV: chromeos-base/shill-0.0.1-r1
+
+CPV: chromeos-base/test-0.0.1-r1
+
+CPV: virtual/chromium-os-printing-1-r4
+
+CPV: virtual/python-enum34-1
+
+"""
+    osutils.WriteFile(packages_file, packages_content)
+
+
+    # Create package.installable file
+    self.dev_install_packages = ['app-arch/zip-3.0-r3',
+                                 'virtual/chromium-os-printing-1-r4',
+                                 'virtual/python-enum34-1']
+    package_installable_dir = os.path.join(full_sysroot_path,
+                                           'build/dev-install')
+    osutils.SafeMakedirs(package_installable_dir)
+    package_installable_filename = os.path.join(package_installable_dir,
+                                                'package.installable')
+
+    # Create path to the dev_install_packages
+    packages_dir = os.path.join(full_sysroot_path, 'packages')
+    osutils.SafeMakedirs(packages_dir)
+    for package in self.dev_install_packages:
+      # Since a package has a category, such as app-arch/zip-3.0-r3, we need
+      # to create the packages_dir / category dir as needed.
+      category = package.split(os.sep)[0]
+      osutils.SafeMakedirs(os.path.join(packages_dir, category))
+      package_tbz2_file = os.path.join(packages_dir, package) + '.tbz2'
+      osutils.Touch(package_tbz2_file)
+    package_installable_file = open(package_installable_filename, 'w')
+    for package in self.dev_install_packages:
+      package_installable_file.write(package + '\n')
+    package_installable_file.close()
+    self.response = binhost_pb2.PrepareDevInstallBinhostUploadsResponse()
+
+  def testDevInstallerUpload(self):
+    """Basic sanity test testing uploads of dev installer prebuilts."""
+    # self.RunStage()
+    input_proto = binhost_pb2.PrepareDevInstallBinhostUploadsRequest()
+    input_proto.uri = 'gs://chromeos-prebuilt/target'
+    input_proto.chroot.path = self.chroot_path
+    input_proto.sysroot.path = self.sysroot_path
+    input_proto.uploads_dir = self.uploads_dir
+    # Call method under test
+    binhost.PrepareDevInstallBinhostUploads(input_proto, self.response,
+                                            self.api_config)
+    # Verify results
+    expected_upload_targets = ['app-arch/zip-3.0-r3.tbz2',
+                               'virtual/chromium-os-printing-1-r4.tbz2',
+                               'virtual/python-enum34-1.tbz2',
+                               'Packages']
+    self.assertItemsEqual(
+        [ut.path for ut in self.response.upload_targets],
+        expected_upload_targets)
+    # All of the upload_targets should also be in the uploads_directory
+    for target in self.response.upload_targets:
+      self.assertExists(os.path.join(input_proto.uploads_dir, target.path))
+
+  def testPrepareBinhostUploadsNonGsUri(self):
+    """PrepareBinhostUploads dies when URI does not point to GS."""
+    input_proto = binhost_pb2.PrepareDevInstallBinhostUploadsRequest()
+    input_proto.chroot.path = self.chroot_path
+    input_proto.sysroot.path = self.sysroot_path
+    input_proto.uploads_dir = self.uploads_dir
+    input_proto.uri = 'https://foo.bar'
+    with self.assertRaises(ValueError):
+      binhost.PrepareDevInstallBinhostUploads(input_proto, self.response,
+                                              self.api_config)