deploy_chrome: Add an arg to deploy test binaries.

Some tast tests expect to find test binaries built within chromium at
/usr/local/libexec/chrome-binary-tests on the device.

This adds an arg to deploy_chrome to replace those binaries with any
found in the local build dir. This will allow those tests to be executed
with their respective binaries built at ToT instead of with the ones
shipped with the device image.

BUG=chromium:1099963
TEST=unittest
TEST=ran this on a failing chrome patch:
    https://chrome-swarming.appspot.com/task?id=4d25f16fceedf210

Change-Id: Id08a73644ca9e03c693b09d388e3f13dd6c8edb9
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/2277078
Reviewed-by: Achuith Bhandarkar <achuith@chromium.org>
Commit-Queue: Ben Pastene <bpastene@chromium.org>
Tested-by: Ben Pastene <bpastene@chromium.org>
diff --git a/lib/cros_test.py b/lib/cros_test.py
index 8850bfa..c2d0294 100644
--- a/lib/cros_test.py
+++ b/lib/cros_test.py
@@ -199,7 +199,8 @@
         'deploy_chrome', '--force',
         '--build-dir', self.build_dir,
         '--process-timeout', '180',
-        '--to', self._device.device
+        '--to', self._device.device,
+        '--deploy-test-binaries',
     ]
     if self._device.ssh_port:
       deploy_cmd += ['--port', str(self._device.ssh_port)]
diff --git a/scripts/deploy_chrome.py b/scripts/deploy_chrome.py
index be33b8f..c5fe299 100644
--- a/scripts/deploy_chrome.py
+++ b/scripts/deploy_chrome.py
@@ -64,12 +64,15 @@
 
 _CHROME_DIR = '/opt/google/chrome'
 _CHROME_DIR_MOUNT = '/mnt/stateful_partition/deploy_rootfs/opt/google/chrome'
+_CHROME_TEST_BIN_DIR = '/usr/local/libexec/chrome-binary-tests'
 
 _UMOUNT_DIR_IF_MOUNTPOINT_CMD = (
     'if mountpoint -q %(dir)s; then umount %(dir)s; fi')
 _BIND_TO_FINAL_DIR_CMD = 'mount --rbind %s %s'
 _SET_MOUNT_FLAGS_CMD = 'mount -o remount,exec,suid %s'
 _MKDIR_P_CMD = 'mkdir -p --mode 0775 %s'
+_FIND_TEST_BIN_CMD = 'find %s -maxdepth 1 -executable -type f' % (
+    _CHROME_TEST_BIN_DIR)
 
 DF_COMMAND = 'df -k %s'
 
@@ -305,6 +308,28 @@
       logging.info('Starting UI...')
       self.device.run('start ui')
 
+  def _DeployTestBinaries(self):
+    """Deploys any local test binary to _CHROME_TEST_BIN_DIR on the device.
+
+    There could be several binaries located in the local build dir, so compare
+    what's already present on the device in _CHROME_TEST_BIN_DIR , and copy
+    over any that we also built ourselves.
+    """
+    r = self.device.run(_FIND_TEST_BIN_CMD, check=False)
+    if r.returncode != 0:
+      raise DeployFailure('Unable to ls contents of %s' % _CHROME_TEST_BIN_DIR)
+    binaries_to_copy = []
+    for f in r.output.splitlines():
+      binaries_to_copy.append(
+          chrome_util.Path(os.path.basename(f), exe=True, optional=True))
+
+    staging_dir = os.path.join(
+        self.tempdir, os.path.basename(_CHROME_TEST_BIN_DIR))
+    _PrepareStagingDir(self.options, self.tempdir, staging_dir,
+                       copy_paths=binaries_to_copy)
+    self.device.CopyToDevice(
+        staging_dir, os.path.dirname(_CHROME_TEST_BIN_DIR), mode='rsync')
+
   def _CheckConnection(self):
     try:
       logging.info('Testing connection to the device...')
@@ -420,6 +445,8 @@
 
     # Actually deploy Chrome to the device.
     self._Deploy()
+    if self.options.deploy_test_binaries:
+      self._DeployTestBinaries()
 
 
 def ValidateStagingFlags(value):
@@ -483,6 +510,11 @@
                            'umounted first.')
   parser.add_argument('--noremove-rootfs-verification', action='store_true',
                       default=False, help='Never remove rootfs verification.')
+  parser.add_argument('--deploy-test-binaries', action='store_true',
+                      default=False,
+                      help='Also deploy any test binaries to %s. Useful for '
+                           'running any Tast tests that execute these '
+                           'binaries.' % _CHROME_TEST_BIN_DIR)
 
   group = parser.add_argument_group('Advanced Options')
   group.add_argument('-l', '--local-pkg-path', type='path',
diff --git a/scripts/deploy_chrome_unittest.py b/scripts/deploy_chrome_unittest.py
index b8c784a..b63d0c4 100644
--- a/scripts/deploy_chrome_unittest.py
+++ b/scripts/deploy_chrome_unittest.py
@@ -372,3 +372,46 @@
     self.deploy._CheckDeployType()
     self.assertTrue(self.getCopyPath('chrome'))
     self.assertFalse(self.getCopyPath('app_shell'))
+
+
+class TestDeployTestBinaries(cros_test_lib.RunCommandTempDirTestCase):
+  """Tests _DeployTestBinaries()."""
+
+  def setUp(self):
+    options = _ParseCommandLine(list(_REGULAR_TO) + [
+        '--board', _TARGET_BOARD, '--force', '--mount',
+        '--build-dir', os.path.join(self.tempdir, 'build_dir'),
+        '--nostrip'])
+    self.deploy = deploy_chrome.DeployChrome(
+        options, self.tempdir, os.path.join(self.tempdir, 'staging'))
+
+  def testFindError(self):
+    """Ensure an error is thrown if we can't inspect the device."""
+    self.rc.AddCmdResult(
+        partial_mock.In(deploy_chrome._FIND_TEST_BIN_CMD), 1)
+    self.assertRaises(
+        deploy_chrome.DeployFailure, self.deploy._DeployTestBinaries)
+
+  def testSuccess(self):
+    """Ensure the staging dir contains the right binaries to copy over."""
+    test_binaries = [
+        'run_a_tests',
+        'run_b_tests',
+        'run_c_tests',
+    ]
+    # Simulate having the binaries both on the device and in our local build
+    # dir.
+    self.rc.AddCmdResult(
+        partial_mock.In(deploy_chrome._FIND_TEST_BIN_CMD),
+        stdout='\n'.join(test_binaries))
+    for binary in test_binaries:
+      osutils.Touch(os.path.join(self.deploy.options.build_dir, binary),
+                    makedirs=True, mode=0o700)
+
+    self.deploy._DeployTestBinaries()
+
+    # Ensure the binaries were placed in the staging dir used to copy them over.
+    staging_dir = os.path.join(
+        self.tempdir, os.path.basename(deploy_chrome._CHROME_TEST_BIN_DIR))
+    for binary in test_binaries:
+      self.assertIn(binary, os.listdir(staging_dir))