Upload fingerprint MCU unittests tarball as artifact

This change takes the fingerprint MCU unittest binaries in a build
and uploads them as artifacts of that build. These unittest binaries
will be used in a Tast test where they are flashed to a fingerprint
MCU device in the test tab and executed on-device. We do this for
every build so that we can catch buggy CLs during a CQ run.

BUG=b:158580909
TEST=run_pytest cbuildbot/commands_unittest.py
     run_pytest service/artifacts_unittest.py

Change-Id: Ida3cb5afe4d576570de61d37100c11c387ae65fc
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/2405306
Tested-by: Yicheng Li <yichengli@chromium.org>
Reviewed-by: Sean McAllister <smcallis@google.com>
Commit-Queue: Yicheng Li <yichengli@chromium.org>
diff --git a/cbuildbot/commands.py b/cbuildbot/commands.py
index 7132e28..6bb81d4 100644
--- a/cbuildbot/commands.py
+++ b/cbuildbot/commands.py
@@ -3343,6 +3343,25 @@
     shutil.move(archive_file, os.path.join(archive_dir, archive_name))
     return archive_name
 
+def BuildFpmcuUnittestsArchive(buildroot,
+                               board,
+                               tarball_dir):
+  """Build fpmcu_unittests.tar.bz2 for fingerprint MCU on-device testing.
+
+  Args:
+    buildroot: Root directory where build occurs.
+    board: Board name of build target.
+    tarball_dir: Directory to store output file.
+
+  Returns:
+    The path of the archived file, or None if the target board does
+    not have fingerprint MCU unittest binaries.
+  """
+  sysroot = sysroot_lib.Sysroot(os.path.join('build', board))
+  chroot = chroot_lib.Chroot(path=os.path.join(buildroot, 'chroot'))
+
+  return artifacts_service.BundleFpmcuUnittests(
+      chroot, sysroot, tarball_dir)
 
 def CallBuildApiWithInputProto(buildroot, build_api_command, input_proto):
   """Call BuildApi with the input_proto and buildroot.
diff --git a/cbuildbot/commands_unittest.py b/cbuildbot/commands_unittest.py
index 01cdf91..1d02cad 100644
--- a/cbuildbot/commands_unittest.py
+++ b/cbuildbot/commands_unittest.py
@@ -1547,6 +1547,24 @@
     # Verify that we get back an archive file using the specified name.
     self.assertEqual(archive_name, returned_archive_name)
 
+  def testBuildFpmcuUnittestsArchive(self):
+    """Verifies that a tarball with the right name is created."""
+    unittest_files = (
+        'bloonchipper/test_rsa.bin',
+        'dartmonkey/test_utils.bin',
+    )
+    board = 'hatch'
+    unittest_files_root = os.path.join(
+        self.tempdir,
+        'chroot/build/%s/firmware/chromeos-fpmcu-unittests' % board)
+    cros_test_lib.CreateOnDiskHierarchy(unittest_files_root, unittest_files)
+
+    returned_archive_name = commands.BuildFpmcuUnittestsArchive(self.tempdir,
+                                                                board,
+                                                                self.tempdir)
+    self.assertEqual(
+        returned_archive_name,
+        os.path.join(self.tempdir, constants.FPMCU_UNITTESTS_ARCHIVE_NAME))
 
   def findFilesWithPatternExpectedResults(self, root, files):
     """Generate the expected results for testFindFilesWithPattern"""
diff --git a/cbuildbot/stages/artifact_stages.py b/cbuildbot/stages/artifact_stages.py
index 38157a5..5cad1d6 100644
--- a/cbuildbot/stages/artifact_stages.py
+++ b/cbuildbot/stages/artifact_stages.py
@@ -774,6 +774,15 @@
       if tarball:
         self.UploadArtifact(tarball)
 
+  def BuildFpmcuUnittestsTarball(self):
+    """Build the tarball containing fingerprint MCU on-device unittests."""
+    with osutils.TempDir(prefix='cbuildbot-fpmcu-unittests') as tempdir:
+      logging.info('Running commands.BuildFpmcuUnittestsArchive')
+      tarball = commands.BuildFpmcuUnittestsArchive(
+          self._build_root, self._current_board, tempdir)
+      if tarball:
+        self.UploadArtifact(tarball)
+
   def _GeneratePayloads(self, image_name, **kwargs):
     """Generate and upload payloads for |image_name|.
 
@@ -827,6 +836,7 @@
       steps.append(self.BuildAutotestTarballs)
       steps.append(self.BuildTastTarball)
       steps.append(self.BuildGuestImagesTarball)
+      steps.append(self.BuildFpmcuUnittestsTarball)
 
     parallel.RunParallelSteps(steps)
     # If we encountered any exceptions with any of the steps, they should have
diff --git a/lib/constants.py b/lib/constants.py
index 86f1330..7eb6d64 100644
--- a/lib/constants.py
+++ b/lib/constants.py
@@ -848,6 +848,7 @@
 DELTA_SYSROOT_BATCH = 'batch'
 
 FIRMWARE_ARCHIVE_NAME = 'firmware_from_source.tar.bz2'
+FPMCU_UNITTESTS_ARCHIVE_NAME = 'fpmcu_unittests.tar.bz2'
 
 # Global configuration constants.
 CHROMITE_CONFIG_DIR = os.path.expanduser('~/.chromite')
diff --git a/service/artifacts.py b/service/artifacts.py
index 3c37072..21aa4b7 100644
--- a/service/artifacts.py
+++ b/service/artifacts.py
@@ -113,6 +113,32 @@
 
   return archive_file
 
+def BundleFpmcuUnittests(chroot, sysroot, output_directory):
+  """Create artifact tarball for fingerprint MCU on-device unittests.
+
+  Args:
+    chroot (chroot_lib.Chroot): The chroot containing the sysroot.
+    sysroot (sysroot_lib.Sysroot): The sysroot whose artifacts are being
+      archived.
+    output_directory (str): The path were the completed archives should be put.
+
+  Returns:
+    str|None - The archive file path if created, None otherwise.
+  """
+  fpmcu_unittests_root = os.path.join(chroot.path, sysroot.path.lstrip(os.sep),
+                                      'firmware', 'chromeos-fpmcu-unittests')
+  files = [os.path.relpath(f, fpmcu_unittests_root)
+           for f in glob.iglob(os.path.join(fpmcu_unittests_root, '*'))]
+  if not files:
+    return None
+
+  archive_file = os.path.join(output_directory,
+                              constants.FPMCU_UNITTESTS_ARCHIVE_NAME)
+  cros_build_lib.CreateTarball(
+      archive_file, fpmcu_unittests_root, compression=cros_build_lib.COMP_BZIP2,
+      chroot=chroot.path, inputs=files)
+
+  return archive_file
 
 def BundleAutotestFiles(chroot, sysroot, output_directory):
   """Create the Autotest Hardware Test archives.
diff --git a/service/artifacts_unittest.py b/service/artifacts_unittest.py
index 67cc808..2418e13 100644
--- a/service/artifacts_unittest.py
+++ b/service/artifacts_unittest.py
@@ -416,6 +416,32 @@
     # Verify the tarball contents.
     cros_test_lib.VerifyTarball(tarball, fw_files)
 
+class BundleFpmcuUnittestsTest(cros_test_lib.TempDirTestCase):
+  """BundleFpmcuUnittests tests."""
+
+  def testBundleFpmcuUnittests(self):
+    """Verifies that the resulting tarball includes proper files"""
+    unittest_files = (
+        'bloonchipper/test_rsa.bin',
+        'dartmonkey/test_utils.bin',
+    )
+
+    board = 'hatch'
+    unittest_files_root = os.path.join(
+        self.tempdir,
+        'chroot/build/%s/firmware/chromeos-fpmcu-unittests' % board)
+    cros_test_lib.CreateOnDiskHierarchy(unittest_files_root, unittest_files)
+
+    chroot_path = os.path.join(self.tempdir, 'chroot')
+    chroot = chroot_lib.Chroot(path=chroot_path)
+    sysroot = sysroot_lib.Sysroot('/build/%s' % board)
+
+    tarball = os.path.join(
+        self.tempdir,
+        artifacts.BundleFpmcuUnittests(chroot, sysroot, self.tempdir))
+    cros_test_lib.VerifyTarball(
+        tarball,
+        unittest_files + ('bloonchipper/', 'dartmonkey/'))
 
 class BundleAFDOGenerationArtifacts(cros_test_lib.MockTempDirTestCase):
   """BundleAFDOGenerationArtifacts tests."""