Add VM tests to bbuildbot builder.

Parameterize lunch target and emulator binary names in the build config.

Add a RunLunchWrapper that setups up the lunch ENVs before invoking a
command.

Add a new stage that starts the brillo emulator as a background process,
waits for "adb devices" to report it's serial number, then runs tests
against the emulator.

BUG=chromium:570810
TEST=bin/cbuildbot --nobootstrap --noreexec --buildbot
     --debug --buildroot ~/tmp/buildroot bbuildbot

Change-Id: I16e355f43275df204b0fdbb2aa0d9d00baf8d679
diff --git a/builders/brillo_builders.py b/builders/brillo_builders.py
index 52b6206..5080558 100644
--- a/builders/brillo_builders.py
+++ b/builders/brillo_builders.py
@@ -6,21 +6,35 @@
 
 from __future__ import print_function
 
+import contextlib
 import os
+import re
+import subprocess
+import tempfile
+import time
 
-from chromite.lib import cros_build_lib
-from chromite.lib import osutils
-
+from chromite.cbuildbot import repository
 from chromite.cbuildbot.builders import generic_builders
 from chromite.cbuildbot.stages import generic_stages
 from chromite.cbuildbot.stages import sync_stages
-from chromite.cbuildbot import repository
+from chromite.lib import cros_build_lib
+from chromite.lib import cros_logging as logging
+from chromite.lib import osutils
+
+
+class EmulatorFailedToStart(Exception):
+  """The emulator process isn't running after 10 seconds."""
+
+
+class EmulatorNotReady(Exception):
+  """The adb devices command did not discover a valid emulator serial number."""
 
 
 class BrilloStageBase(generic_stages.BuilderStage):
   """Base class for all symbols build stages."""
 
   def BrilloRoot(self):
+    """Root for repo checkout of Brillo."""
     # Turn /mnt/data/b/cbuild/android -> /mnt/data/b/cbuild/android_brillo
 
     # We have to be OUTSIDE the build root, since this is a new repo checkout.
@@ -30,10 +44,29 @@
     return self._run.buildroot + '_brillo'
 
   def BuildOutput(self):
-    # We store brillo build output in brillo's default output directory.
-    #  (ex: brillo_root/out)
+    """Returns directory for brillo build output."""
     return os.path.join(self.BrilloRoot(), 'out')
 
+  def FindShellCmd(self, cmd):
+    target = self._run.config.lunch_target
+
+    cmd_list = []
+    cmd_list.append('. build/envsetup.sh')
+    cmd_list.append('lunch %s' % target)
+    cmd_list.append('OUT_DIR=%s' % self.BuildOutput())
+    cmd_list.append(' '.join(cmd))
+
+    return ' > /dev/null && '.join(cmd_list)
+
+  def RunLunchCommand(self, cmd, **kwargs):
+    """RunCommand with lunch setup."""
+    # Default directory to run in.
+    kwargs.setdefault('cwd', self.BrilloRoot())
+
+    # We use a shell invocation so environmental variables are preserved.
+    cmd = self.FindShellCmd(cmd)
+    return cros_build_lib.RunCommand(cmd, shell=True, **kwargs)
+
 
 class BrilloCleanStage(BrilloStageBase):
   """Compile the Brillo checkout."""
@@ -50,10 +83,8 @@
   """Sync Brillo code to a sub-directory."""
 
   def PerformStage(self):
-    """Do the sync work."""
+    """Fetch and/or update the brillo source code."""
     osutils.SafeMakedirs(self.BrilloRoot())
-
-    # Fetch and/or update the brillo source code.
     brillo_repo = repository.RepoRepository(
         manifest_repo_url=self._run.config.brillo_manifest_url,
         branch=self._run.config.brillo_manifest_branch,
@@ -67,15 +98,109 @@
 
   def PerformStage(self):
     """Do the build work."""
-    cmd_list = []
-    cmd_list.append('. build/envsetup.sh')
-    cmd_list.append('lunch brilloemulator_arm-eng')
-    cmd_list.append('OUT_DIR=%s' % self.BuildOutput())
-    cmd_list.append('make -j 32')
+    self.RunLunchCommand(['make', '-j', '32'])
 
-    # We use a shell invocation so environmental variables are preserved.
-    cmd = ' && '.join(cmd_list)
-    cros_build_lib.RunCommand(cmd, shell=True, cwd=self.BrilloRoot())
+
+class BrilloVmTestStage(BrilloStageBase):
+  """Compile the Brillo checkout."""
+
+  @contextlib.contextmanager
+  def RunEmulator(self):
+    """Run an emulator process in the background, kill it on exit."""
+    with tempfile.NamedTemporaryFile(prefix='emulator') as logfile:
+      cmd = self.FindShellCmd([self._run.config.emulator])
+      logging.info('Starting emulator: %s', cmd)
+      p = subprocess.Popen(
+          args=(cmd,),
+          shell=True,
+          close_fds=True,
+          stdout=logfile,
+          stderr=subprocess.STDOUT,
+          cwd=self.BrilloRoot(),
+          )
+
+
+      try:
+        # Give the emulator a little time, and make sure it's still running.
+        # Failure could be an crash, another copy was left running, etc.
+        time.sleep(10)
+        if p.poll() is not None:
+          logging.error('Emulator is not running after 10 seconds, aborting.')
+          raise EmulatorFailedToStart()
+
+        yield
+      finally:
+        if p.poll() is None:
+          # Kill emulator, if it's still running.
+          logging.info('Stopping emulator.')
+          p.terminate()
+
+        p.wait()
+
+        # Read/dump the emulator output.
+        logging.info('*')
+        logging.info('* Emulator Output')
+        logging.info('*\n%s', osutils.ReadFile(logfile.name))
+        logging.info('*')
+        logging.info('* Emulator End')
+        logging.info('*')
+
+  def DiscoverEmulatorSerial(self):
+    """Query for the serial number of the emulator.
+
+    Returns:
+      String containing the serial number of the emulator, or None
+    """
+    result = self.RunLunchCommand(
+        ['adb', 'devices'],
+        redirect_stdout=True,
+        combine_stdout_stderr=True)
+
+    # Command output before we are ready:
+    #   List of devices attached
+    #   emulator-5554 offline
+
+    # Command output after we are ready:
+    #   List of devices attached
+    #   emulator-5554 device
+
+    m = re.search(r'^([\w-]+)\tdevice$', result.output, re.MULTILINE)
+    if m:
+      return m.group(1)
+    return None
+
+  def WaitForEmulatorSerial(self):
+    """Retry the query for the emulator serial number, until it's ready.
+
+    Returns:
+      String containing the serial number of the emulator.
+
+    Raises:
+      EmulatorNotReady if we timeout waiting (after several minutes).
+    """
+    for _ in xrange(20):
+      result = self.DiscoverEmulatorSerial()
+      if result:
+        return result
+      time.sleep(10)
+
+    raise EmulatorNotReady()
+
+  def PerformStage(self):
+    """Run the VM Tests."""
+    with self.RunEmulator():
+      # To see the emulator, we must sometimes kill/restart the adb server.
+      self.RunLunchCommand(['adb', 'kill-server'])
+
+      # Wait for the emulator to come up enough to give us a serial number.
+      serial = self.WaitForEmulatorSerial()
+
+      # Run the tests.
+      logging.info('Running tests against %s', serial)
+      self.RunLunchCommand(
+          ['external/autotest/site_utils/test_droid.py',
+           '--debug', serial, 'brillo_WhitelistedGtests'],
+          cwd=self.BrilloRoot())
 
 
 class BrilloBuilder(generic_builders.Builder):
@@ -90,3 +215,4 @@
     self._RunStage(BrilloCleanStage)
     self._RunStage(BrilloSyncStage)
     self._RunStage(BrilloBuildStage)
+    self._RunStage(BrilloVmTestStage)
diff --git a/config_dump.json b/config_dump.json
index 1c8c2b7..4842837 100644
--- a/config_dump.json
+++ b/config_dump.json
@@ -11,6 +11,8 @@
     "bbuildbot": {
         "builder_class_name": "config.builders.brillo_builders.BrilloBuilder",
         "brillo_manifest_url": "https://android.googlesource.com/brillo/manifest",
-        "brillo_manifest_branch": "master"
+        "brillo_manifest_branch": "master",
+        "lunch_target": "brilloemulator_arm-eng",
+        "emulator": "brilloemulator-arm"
     }
 }