Implement SimpleChromeWorkflowTest in the Test endpoint.

BUG=chromium:999670
TEST=manual, run_tests

Change-Id: If274a040a7326a2b5c4a0cf9d81808d59b665b4b
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/1845358
Reviewed-by: Alex Klein <saklein@chromium.org>
Tested-by: Michael Mortensen <mmortensen@google.com>
Commit-Queue: Michael Mortensen <mmortensen@google.com>
diff --git a/api/contrib/call_templates/test__simple_chrome_workflow_test_example_input.json b/api/contrib/call_templates/test__simple_chrome_workflow_test_example_input.json
new file mode 100644
index 0000000..62490f7
--- /dev/null
+++ b/api/contrib/call_templates/test__simple_chrome_workflow_test_example_input.json
@@ -0,0 +1,14 @@
+{
+  "sysroot": {
+    "build_target": {
+      "name": "betty"
+    },
+    "path": "/build/betty"
+  },
+  "chrome_root": "/path/to/chrome",
+  "goma_config": {
+    "goma_dir": "/path/to/goma",
+    "goma_client_json": "/path/to/goma/client/json",
+    "chromeos_goma_dir": "/path/to/chromeos/goma/dir"
+  }
+}
diff --git a/api/controller/test.py b/api/controller/test.py
index 2a115d7..3b88757 100644
--- a/api/controller/test.py
+++ b/api/controller/test.py
@@ -17,6 +17,7 @@
 from chromite.api import validate
 from chromite.api.controller import controller_util
 from chromite.api.gen.chromite.api import test_pb2
+from chromite.cbuildbot import goma_util
 from chromite.lib import build_target_util
 from chromite.lib import constants
 from chromite.lib import cros_build_lib
@@ -111,6 +112,27 @@
     return controller.RETURN_CODE_COMPLETED_UNSUCCESSFULLY
 
 
+
+@faux.all_empty
+@validate.require('sysroot.path', 'sysroot.build_target.name', 'chrome_root')
+@validate.validation_complete
+def SimpleChromeWorkflowTest(input_proto, _output_proto, _config):
+  """Run SimpleChromeWorkflow tests."""
+  if input_proto.goma_config.goma_dir:
+    chromeos_goma_dir = input_proto.goma_config.chromeos_goma_dir or None
+    goma = goma_util.Goma(
+        input_proto.goma_config.goma_dir,
+        input_proto.goma_config.goma_client_json,
+        stage_name='BuildApiTestSimpleChrome',
+        chromeos_goma_dir=chromeos_goma_dir)
+  else:
+    goma = None
+  return test.SimpleChromeWorkflowTest(input_proto.sysroot.path,
+                                       input_proto.sysroot.build_target.name,
+                                       input_proto.chrome_root,
+                                       goma)
+
+
 @faux.all_empty
 @validate.require('build_target.name', 'vm_path.path', 'test_harness',
                   'vm_tests')
diff --git a/api/controller/test_unittest.py b/api/controller/test_unittest.py
index 2289895..86f1b50 100644
--- a/api/controller/test_unittest.py
+++ b/api/controller/test_unittest.py
@@ -139,6 +139,75 @@
     self.assertFalse(self.rc.call_count)
 
 
+class SimpleChromeWorkflowTestTest(cros_test_lib.MockTestCase,
+                                   api_config.ApiConfigMixin):
+  """Test the SimpleChromeWorkflowTest endpoint."""
+
+  @staticmethod
+  def _Output():
+    return test_pb2.SimpleChromeWorkflowTestResponse()
+
+  def _Input(self, sysroot_path=None, build_target=None, chrome_root=None,
+             goma_config=None):
+    proto = test_pb2.SimpleChromeWorkflowTestRequest()
+    if sysroot_path:
+      proto.sysroot.path = sysroot_path
+    if build_target:
+      proto.sysroot.build_target.name = build_target
+    if chrome_root:
+      proto.chrome_root = chrome_root
+    if goma_config:
+      proto.goma_config = goma_config
+    return proto
+
+  def setUp(self):
+    self.chrome_path = 'path/to/chrome'
+    self.sysroot_dir = 'build/board'
+    self.build_target = 'amd64'
+    self.mock_simple_chrome_workflow_test = self.PatchObject(
+        test_service, 'SimpleChromeWorkflowTest')
+
+  def testMissingBuildTarget(self):
+    """Test VmTest dies when build_target not set."""
+    input_proto = self._Input(build_target=None, sysroot_path='/sysroot/dir',
+                              chrome_root='/chrome/path')
+    with self.assertRaises(cros_build_lib.DieSystemExit):
+      test_controller.SimpleChromeWorkflowTest(input_proto, None,
+                                               self.api_config)
+
+  def testMissingSysrootPath(self):
+    """Test VmTest dies when build_target not set."""
+    input_proto = self._Input(build_target='board', sysroot_path=None,
+                              chrome_root='/chrome/path')
+    with self.assertRaises(cros_build_lib.DieSystemExit):
+      test_controller.SimpleChromeWorkflowTest(input_proto, None,
+                                               self.api_config)
+
+  def testMissingChromeRoot(self):
+    """Test VmTest dies when build_target not set."""
+    input_proto = self._Input(build_target='board', sysroot_path='/sysroot/dir',
+                              chrome_root=None)
+    with self.assertRaises(cros_build_lib.DieSystemExit):
+      test_controller.SimpleChromeWorkflowTest(input_proto, None,
+                                               self.api_config)
+
+  def testSimpleChromeWorkflowTest(self):
+    """Call SimpleChromeWorkflowTest with valid args and temp dir."""
+    request = self._Input(sysroot_path='sysroot_path', build_target='board',
+                          chrome_root='/path/to/chrome')
+    response = self._Output()
+
+    test_controller.SimpleChromeWorkflowTest(request, response, self.api_config)
+    self.mock_simple_chrome_workflow_test.assert_called()
+
+  def testValidateOnly(self):
+    request = self._Input(sysroot_path='sysroot_path', build_target='board',
+                          chrome_root='/path/to/chrome')
+    test_controller.SimpleChromeWorkflowTest(request, self._Output(),
+                                             self.validate_only_config)
+    self.mock_simple_chrome_workflow_test.assert_not_called()
+
+
 class VmTestTest(cros_test_lib.RunCommandTestCase, api_config.ApiConfigMixin):
   """Test the VmTest endpoint."""
 
diff --git a/service/test.py b/service/test.py
index 6cb831a..366855e 100644
--- a/service/test.py
+++ b/service/test.py
@@ -12,9 +12,12 @@
 
 import os
 import re
+import shutil
 
+from chromite.cbuildbot import commands
 from chromite.lib import constants
 from chromite.lib import cros_build_lib
+from chromite.lib import cros_logging as logging
 from chromite.lib import failures_lib
 from chromite.lib import moblab_vm
 from chromite.lib import osutils
@@ -198,6 +201,163 @@
     )
 
 
+def SimpleChromeWorkflowTest(sysroot_path, build_target_name, chrome_root,
+                             goma):
+  """Execute SimpleChrome workflow tests
+
+  Args:
+    sysroot_path (str): The sysroot path for testing Chrome.
+    build_target_name (str): Board build target
+    chrome_root (str): Path to Chrome source root.
+    goma (goma_util.Goma): Goma object (or None).
+  """
+  board_dir = 'out_%s' % build_target_name
+
+  out_board_dir = os.path.join(chrome_root, board_dir, 'Release')
+  use_goma = goma != None
+  extra_args = []
+
+  with osutils.TempDir(prefix='chrome-sdk-cache') as tempdir:
+    sdk_cmd = _InitSimpleChromeSDK(tempdir, build_target_name, sysroot_path,
+                                   chrome_root, use_goma)
+
+    if goma:
+      extra_args.extend(['--nostart-goma', '--gomadir', goma.linux_goma_dir])
+
+    _BuildChrome(sdk_cmd, chrome_root, out_board_dir, goma)
+    _TestDeployChrome(sdk_cmd, out_board_dir)
+    _VMTestChrome(build_target_name, sdk_cmd)
+
+
+def _InitSimpleChromeSDK(tempdir, build_target_name, sysroot_path, chrome_root,
+                         use_goma):
+  """Create ChromeSDK object for executing 'cros chrome-sdk' commands.
+
+  Args:
+    tempdir (string): Tempdir for command execution.
+    build_target_name (string): Board build target.
+    sysroot_path (string): Sysroot for Chrome to use.
+    chrome_root (string): Path to Chrome.
+    use_goma (bool): Whether to use goma.
+
+  Returns:
+    A ChromeSDK object.
+  """
+  extra_args = ['--cwd', chrome_root, '--sdk-path', sysroot_path]
+  cache_dir = os.path.join(tempdir, 'cache')
+
+  sdk_cmd = commands.ChromeSDK(
+      constants.SOURCE_ROOT, build_target_name, chrome_src=chrome_root,
+      goma=use_goma, extra_args=extra_args, cache_dir=cache_dir)
+  return sdk_cmd
+
+
+def _VerifySDKEnvironment(out_board_dir):
+  """Make sure the SDK environment is set up properly.
+
+  Args:
+    out_board_dir (str): Output SDK dir for board.
+  """
+  if not os.path.exists(out_board_dir):
+    raise AssertionError('%s not created!' % out_board_dir)
+  logging.info('ARGS.GN=\n%s',
+               osutils.ReadFile(os.path.join(out_board_dir, 'args.gn')))
+
+
+def _BuildChrome(sdk_cmd, chrome_root, out_board_dir, goma):
+  """Build Chrome with SimpleChrome environment.
+
+  Args:
+    sdk_cmd (ChromeSDK object): sdk_cmd to run cros chrome-sdk commands.
+    chrome_root (string): Path to Chrome.
+    out_board_dir (string): Path to board directory.
+    goma (goma_util.Goma): Goma object
+  """
+  # Validate fetching of the SDK and setting everything up.
+  sdk_cmd.Run(['true'])
+
+  sdk_cmd.Run(['gclient', 'runhooks'])
+
+  # Generate args.gn and ninja files.
+  gn_cmd = os.path.join(chrome_root, 'buildtools', 'linux64', 'gn')
+  gn_gen_cmd = '%s gen "%s" --args="$GN_ARGS"' % (gn_cmd, out_board_dir)
+  sdk_cmd.Run(['bash', '-c', gn_gen_cmd])
+
+  _VerifySDKEnvironment(out_board_dir)
+
+  if goma:
+    # If goma is enabled, start goma compiler_proxy here, and record
+    # several information just before building Chrome is started.
+    goma.Start()
+    extra_env = goma.GetExtraEnv()
+    ninja_env_path = os.path.join(goma.goma_log_dir, 'ninja_env')
+    sdk_cmd.Run(['env', '--null'],
+                run_args={'extra_env': extra_env,
+                          'log_stdout_to_file': ninja_env_path})
+    osutils.WriteFile(os.path.join(goma.goma_log_dir, 'ninja_cwd'),
+                      sdk_cmd.cwd)
+    osutils.WriteFile(os.path.join(goma.goma_log_dir, 'ninja_command'),
+                      cros_build_lib.CmdToStr(sdk_cmd.GetNinjaCommand()))
+  else:
+    extra_env = None
+
+  result = None
+  try:
+    # Build chromium.
+    result = sdk_cmd.Ninja(run_args={'extra_env': extra_env})
+  finally:
+    # In teardown, if goma is enabled, stop the goma compiler proxy,
+    # and record/copy some information to log directory, which will be
+    # uploaded to the goma's server in a later stage.
+    if goma:
+      goma.Stop()
+      ninja_log_path = os.path.join(chrome_root,
+                                    sdk_cmd.GetNinjaLogPath())
+      if os.path.exists(ninja_log_path):
+        shutil.copy2(ninja_log_path,
+                     os.path.join(goma.goma_log_dir, 'ninja_log'))
+      if result:
+        osutils.WriteFile(os.path.join(goma.goma_log_dir, 'ninja_exit'),
+                          str(result.returncode))
+
+
+def _TestDeployChrome(sdk_cmd, out_board_dir):
+  """Test SDK deployment.
+
+  Args:
+    sdk_cmd (ChromeSDK object): sdk_cmd to run cros chrome-sdk commands.
+    out_board_dir (string): Path to board directory.
+  """
+  with osutils.TempDir(prefix='chrome-sdk-stage') as tempdir:
+    # Use the TOT deploy_chrome.
+    script_path = os.path.join(
+        constants.SOURCE_ROOT, constants.CHROMITE_BIN_SUBDIR, 'deploy_chrome')
+    sdk_cmd.Run([script_path, '--build-dir', out_board_dir,
+                 '--staging-only', '--staging-dir', tempdir])
+    # Verify chrome is deployed.
+    chromepath = os.path.join(tempdir, 'chrome')
+    if not os.path.exists(chromepath):
+      raise AssertionError(
+          'deploy_chrome did not run successfully! Searched %s' % (chromepath))
+
+
+def _VMTestChrome(board, sdk_cmd):
+  """Run cros_run_test."""
+
+  # This is how  generic_stages.py creates an image_dir_symlink in
+  # GetImageDirSymlink
+  image_dir_symlink = os.path.join(constants.SOURCE_ROOT, 'src', 'build',
+                                   'images', board, 'latest-cbuildbot')
+
+  image_path = os.path.join(image_dir_symlink,
+                            constants.VM_IMAGE_BIN)
+
+  # Run VM test for boards where we've built a VM.
+  if image_path and os.path.exists(image_path):
+    sdk_cmd.VMTest(image_path)
+
+
+
 def ValidateMoblabVmTest(results_dir):
   """Determine if the VM test passed or not.
 
diff --git a/service/test_unittest.py b/service/test_unittest.py
index 3db48d8..8b7df29 100644
--- a/service/test_unittest.py
+++ b/service/test_unittest.py
@@ -9,9 +9,13 @@
 
 import contextlib
 import os
+import shutil
 
 import mock
 
+from chromite.api.gen.chromiumos import common_pb2
+from chromite.cbuildbot import commands
+from chromite.cbuildbot import goma_util
 from chromite.lib import build_target_util
 from chromite.lib import chroot_lib
 from chromite.lib import cros_build_lib
@@ -235,6 +239,88 @@
     ], enter_chroot=True, chroot_args=self.chroot.get_enter_args())
 
 
+class SimpleChromeWorkflowTestTest(cros_test_lib.MockTempDirTestCase):
+  """Unit tests for SimpleChromeWorkflowTest."""
+
+  def setUp(self):
+    self.chrome_root = '/path/to/chrome/root'
+    self.sysroot_path = '/chroot/path/sysroot/path'
+    self.build_target = 'board'
+
+    self.goma_mock = self.PatchObject(goma_util, 'Goma')
+
+    self.chrome_sdk_run_mock = self.PatchObject(commands.ChromeSDK, 'Run')
+
+    # SimpleChromeTest workflow creates directories based on objects that are
+    # mocked for this test, so patch osutils.WriteFile
+    self.write_mock = self.PatchObject(osutils, 'WriteFile')
+
+    self.PatchObject(cros_build_lib, 'CmdToStr', return_value='CmdToStr value')
+    self.PatchObject(shutil, 'copy2')
+
+  def testSimpleChromeWorkflowTest(self):
+    goma_test_dir = os.path.join(self.tempdir, 'goma_test_dir')
+    goma_test_json_string = os.path.join(self.tempdir, 'goma_json_string.txt')
+    chromeos_goma_dir = os.path.join(self.tempdir, 'chromeos_goma_dir')
+    goma_config = common_pb2.GomaConfig(goma_dir=goma_test_dir,
+                                        goma_client_json=goma_test_json_string)
+    osutils.SafeMakedirs(goma_test_dir)
+    osutils.SafeMakedirs(chromeos_goma_dir)
+    osutils.Touch(goma_test_json_string)
+    goma = goma_util.Goma(
+        goma_config.goma_dir,
+        goma_config.goma_client_json,
+        stage_name='BuildApiTestSimpleChrome',
+        chromeos_goma_dir=chromeos_goma_dir)
+
+    mock_goma_log_dir = os.path.join(self.tempdir, 'goma_log_dir')
+    osutils.SafeMakedirs(mock_goma_log_dir)
+    goma.goma_log_dir = mock_goma_log_dir
+
+    # For this test, we avoid running test._VerifySDKEnvironment because use of
+    # other mocks prevent creating the SDK dir that _VerifySDKEnvironment checks
+    # for
+    self.PatchObject(test, '_VerifySDKEnvironment')
+
+    self.PatchObject(os.path, 'exists', return_value=True)
+
+
+    ninja_cmd = self.PatchObject(commands.ChromeSDK, 'GetNinjaCommand',
+                                 return_value='ninja command')
+
+    test.SimpleChromeWorkflowTest(self.sysroot_path, self.build_target,
+                                  self.chrome_root, goma)
+    # Verify ninja_cmd calls.
+    ninja_calls = [mock.call(), mock.call(debug=False)]
+    ninja_cmd.assert_has_calls(ninja_calls)
+
+    # Verify calls with args to chrome_sdk_run made by service/test.py.
+    gn_dir = os.path.join(self.chrome_root, 'buildtools/linux64/gn')
+    board_out_dir = os.path.join(self.chrome_root, 'out_board/Release')
+
+    self.chrome_sdk_run_mock.assert_any_call(['gclient', 'runhooks'])
+    self.chrome_sdk_run_mock.assert_any_call(['true'])
+    self.chrome_sdk_run_mock.assert_any_call(
+        ['bash', '-c', ('%s gen "%s" --args="$GN_ARGS"'
+                        % (gn_dir, board_out_dir))])
+    self.chrome_sdk_run_mock.assert_any_call(
+        ['env', '--null'], run_args=mock.ANY)
+    self.chrome_sdk_run_mock.assert_any_call('ninja command', run_args=mock.ANY)
+    self.chrome_sdk_run_mock.assert_any_call(
+        ['/mnt/host/source/chromite/bin/deploy_chrome',
+         '--build-dir', board_out_dir, '--staging-only',
+         '--staging-dir', mock.ANY])
+    self.chrome_sdk_run_mock.assert_any_call(
+        ['cros_run_test', '--copy-on-write', '--deploy', '--board=board',
+         ('--image-path=/mnt/host/source/src/build/images/'
+          'board/latest-cbuildbot/chromiumos_qemu_image.bin'),
+         '--build-dir=out_board/Release'])
+
+    # Verify goma mock was started and stopped.
+    self.goma_mock.Start.assert_called_once()
+    self.goma_mock.Stop.assert_called_once()
+
+
 class ValidateMoblabVmTestTest(MoblabVmTestCase):
   """Unit tests for ValidateMoblabVmTest."""